diff options
Diffstat (limited to 'chrome/browser/extensions')
6 files changed, 921 insertions, 50 deletions
diff --git a/chrome/browser/extensions/api/audio_modem/OWNERS b/chrome/browser/extensions/api/audio_modem/OWNERS new file mode 100644 index 0000000..9364bf7 --- /dev/null +++ b/chrome/browser/extensions/api/audio_modem/OWNERS @@ -0,0 +1,2 @@ +ckehoe@chromium.org +rkc@chromium.org diff --git a/chrome/browser/extensions/api/audio_modem/audio_modem_api.cc b/chrome/browser/extensions/api/audio_modem/audio_modem_api.cc new file mode 100644 index 0000000..8521f63 --- /dev/null +++ b/chrome/browser/extensions/api/audio_modem/audio_modem_api.cc @@ -0,0 +1,348 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <map> +#include <string> +#include <vector> + +#include "base/base64.h" +#include "base/bind.h" +#include "base/guid.h" +#include "base/lazy_instance.h" +#include "base/logging.h" +#include "base/memory/scoped_ptr.h" +#include "base/stl_util.h" +#include "base/strings/string_util.h" +#include "base/timer/timer.h" +#include "chrome/browser/copresence/chrome_whispernet_client.h" +#include "chrome/browser/extensions/api/audio_modem/audio_modem_api.h" +#include "chrome/common/extensions/api/audio_modem.h" +#include "extensions/browser/event_router.h" + +// TODO(ckehoe): Implement transmit fail checking. + +namespace extensions { + +using api::audio_modem::AUDIOBAND_AUDIBLE; +using api::audio_modem::AUDIOBAND_INAUDIBLE; +using api::audio_modem::Audioband; +using api::audio_modem::STATUS_CODERERROR; +using api::audio_modem::STATUS_INUSE; +using api::audio_modem::STATUS_INVALIDREQUEST; +using api::audio_modem::STATUS_SUCCESS; +using api::audio_modem::ReceivedToken; +using api::audio_modem::RequestParams; +using api::audio_modem::Status; + +namespace Transmit = api::audio_modem::Transmit; +namespace StopTransmit = api::audio_modem::StopTransmit; +namespace Receive = api::audio_modem::Receive; +namespace StopReceive = api::audio_modem::StopReceive; +namespace OnReceived = api::audio_modem::OnReceived; + +using audio_modem::AUDIBLE; +using audio_modem::AudioToken; +using audio_modem::AudioType; +using audio_modem::INAUDIBLE; +using audio_modem::TokenParameters; + +namespace { + +const char kInitFailedError[] = "The audio modem is not available. " + "Failed to initialize the token encoder/decoder."; +const char kInvalidTokenLengthError[] = + "The token length must be greater than zero."; +const char kIncorrectTokenLengthError[] = + "The token provided did not match the declared token length."; +const char kInvalidTimeoutError[] = + "Transmit and receive timeouts must be greater than zero."; + +const int kMaxTransmitTimeout = 10 * 60 * 1000; // 10 minutes +const int kMaxReceiveTimeout = 60 * 60 * 1000; // 1 hour + +base::LazyInstance<BrowserContextKeyedAPIFactory<AudioModemAPI>> + g_factory = LAZY_INSTANCE_INITIALIZER; + +AudioType AudioTypeForBand(Audioband band) { + switch (band) { + case AUDIOBAND_AUDIBLE: + return AUDIBLE; + case AUDIOBAND_INAUDIBLE: + return INAUDIBLE; + default: + NOTREACHED(); + return audio_modem::AUDIO_TYPE_UNKNOWN; + } +} + +TokenParameters TokenParamsForEncoding( + const api::audio_modem::TokenEncoding& encoding) { + return TokenParameters(encoding.token_length, + encoding.crc ? *encoding.crc : false, + encoding.parity ? *encoding.parity : true); +} + +const std::string DecodeBase64Token(std::string encoded_token) { + // Make sure the token is padded correctly. + while (encoded_token.size() % 4 > 0) + encoded_token += "="; + + // Decode and return the token. + std::string raw_token; + bool decode_success = base::Base64Decode(encoded_token, &raw_token); + DCHECK(decode_success); + return raw_token; +} + +} // namespace + + +// Public functions. + +AudioModemAPI::AudioModemAPI(content::BrowserContext* context) + : AudioModemAPI(context, + make_scoped_ptr(new ChromeWhispernetClient(context)), + audio_modem::Modem::Create()) {} + +AudioModemAPI::AudioModemAPI( + content::BrowserContext* context, + scoped_ptr<audio_modem::WhispernetClient> whispernet_client, + scoped_ptr<audio_modem::Modem> modem) + : browser_context_(context), + whispernet_client_(whispernet_client.Pass()), + modem_(modem.Pass()), + init_failed_(false) { + // We own these objects, so these callbacks will not outlive us. + whispernet_client_->Initialize( + base::Bind(&AudioModemAPI::WhispernetInitComplete, + base::Unretained(this))); + modem_->Initialize(whispernet_client_.get(), + base::Bind(&AudioModemAPI::TokensReceived, + base::Unretained(this))); +} + +AudioModemAPI::~AudioModemAPI() { + for (const auto& timer_entry : receive_timers_[0]) + delete timer_entry.second; + for (const auto& timer_entry : receive_timers_[1]) + delete timer_entry.second; +} + +Status AudioModemAPI::StartTransmit(const std::string& app_id, + const RequestParams& params, + const std::string& token) { + AudioType audio_type = AudioTypeForBand(params.band); + if (transmitters_[audio_type].empty()) + transmitters_[audio_type] = app_id; + else if (transmitters_[audio_type] != app_id) + return STATUS_INUSE; + + DVLOG(3) << "Starting transmit for app " << app_id; + + std::string encoded_token; + base::Base64Encode(token, &encoded_token); + base::RemoveChars(encoded_token, "=", &encoded_token); + + modem_->SetToken(audio_type, encoded_token); + modem_->SetTokenParams(audio_type, TokenParamsForEncoding(params.encoding)); + modem_->StartPlaying(audio_type); + + transmit_timers_[audio_type].Start( + FROM_HERE, + base::TimeDelta::FromMilliseconds(params.timeout_millis), + base::Bind(base::IgnoreResult(&AudioModemAPI::StopTransmit), + base::Unretained(this), + app_id, + audio_type)); + return STATUS_SUCCESS; +} + +Status AudioModemAPI::StopTransmit(const std::string& app_id, + AudioType audio_type) { + if (transmitters_[audio_type] != app_id) + return STATUS_INVALIDREQUEST; + + DVLOG(3) << "Stopping transmit for app " << app_id; + transmitters_[audio_type].clear(); + modem_->StopPlaying(audio_type); + return STATUS_SUCCESS; +} + +void AudioModemAPI::StartReceive(const std::string& app_id, + const RequestParams& params) { + DVLOG(3) << "Starting receive for app " << app_id; + + AudioType audio_type = AudioTypeForBand(params.band); + modem_->SetTokenParams(audio_type, TokenParamsForEncoding(params.encoding)); + modem_->StartRecording(audio_type); + + if (receive_timers_[audio_type].count(app_id) == 0) + receive_timers_[audio_type][app_id] = new base::OneShotTimer<AudioModemAPI>; + DCHECK(receive_timers_[audio_type][app_id]); + receive_timers_[audio_type][app_id]->Start( + FROM_HERE, + base::TimeDelta::FromMilliseconds(params.timeout_millis), + base::Bind(base::IgnoreResult(&AudioModemAPI::StopReceive), + base::Unretained(this), + app_id, + audio_type)); +} + +Status AudioModemAPI::StopReceive(const std::string& app_id, + AudioType audio_type) { + if (receive_timers_[audio_type].count(app_id) == 0) + return STATUS_INVALIDREQUEST; + + DCHECK(receive_timers_[audio_type][app_id]); + delete receive_timers_[audio_type][app_id]; + receive_timers_[audio_type].erase(app_id); + + DVLOG(3) << "Stopping receive for app " << app_id; + if (receive_timers_[audio_type].empty()) + modem_->StopRecording(audio_type); + return STATUS_SUCCESS; +} + +// static +BrowserContextKeyedAPIFactory<AudioModemAPI>* +AudioModemAPI::GetFactoryInstance() { + return g_factory.Pointer(); +} + + +// Private functions. + +void AudioModemAPI::WhispernetInitComplete(bool success) { + if (success) { + VLOG(2) << "Whispernet initialized successfully."; + } else { + LOG(ERROR) << "Failed to initialize Whispernet!"; + init_failed_ = true; + } +} + +void AudioModemAPI::TokensReceived(const std::vector<AudioToken>& tokens) { + // Distribute the tokens to the appropriate app(s). + std::map<std::string, std::vector<linked_ptr<ReceivedToken>>> tokens_by_app; + for (const AudioToken& token : tokens) { + linked_ptr<ReceivedToken> api_token(new ReceivedToken); + const std::string& raw_token = DecodeBase64Token(token.token); + api_token->token.assign(raw_token.c_str(), + raw_token.c_str() + raw_token.size()); + api_token->band = token.audible ? AUDIOBAND_AUDIBLE : AUDIOBAND_INAUDIBLE; + for (const auto& receiver : + receive_timers_[token.audible ? AUDIBLE : INAUDIBLE]) { + tokens_by_app[receiver.first].push_back(api_token); + } + } + + // Send events to the appropriate app(s). + for (const auto& app_entry : tokens_by_app) { + const std::string& app_id = app_entry.first; + const auto& tokens = app_entry.second; + if (app_id.empty()) + continue; + + EventRouter::Get(browser_context_)->DispatchEventToExtension( + app_id, + make_scoped_ptr(new Event(OnReceived::kEventName, + OnReceived::Create(tokens)))); + } +} + + +// Functions outside the API scope. + +template <> +void +BrowserContextKeyedAPIFactory<AudioModemAPI>::DeclareFactoryDependencies() { + DependsOn(ExtensionsBrowserClient::Get()->GetExtensionSystemFactory()); +} + +ExtensionFunction::ResponseAction AudioModemTransmitFunction::Run() { + scoped_ptr<Transmit::Params> params(Transmit::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + AudioModemAPI* api = + AudioModemAPI::GetFactoryInstance()->Get(browser_context()); + if (api->init_failed()) { + return RespondNow(ErrorWithArguments( + Transmit::Results::Create(STATUS_CODERERROR), + kInitFailedError)); + } + + // Check the token length. + int token_length = params->params.encoding.token_length; + if (token_length <= 0) { + return RespondNow(ErrorWithArguments( + Transmit::Results::Create(STATUS_INVALIDREQUEST), + kInvalidTokenLengthError)); + } + const char* token = vector_as_array(¶ms->token); + std::string token_str(token, params->token.size()); + if (static_cast<int>(token_str.size()) != token_length) { + return RespondNow(ErrorWithArguments( + Transmit::Results::Create(STATUS_INVALIDREQUEST), + kIncorrectTokenLengthError)); + } + + // Check the timeout. + int64_t timeout_millis = params->params.timeout_millis; + if (timeout_millis <= 0) { + return RespondNow(ErrorWithArguments( + Transmit::Results::Create(STATUS_INVALIDREQUEST), + kInvalidTimeoutError)); + } + if (timeout_millis > kMaxTransmitTimeout) + timeout_millis = kMaxTransmitTimeout; + + // Start transmission. + Status status = api->StartTransmit(extension_id(), params->params, token_str); + return RespondNow(ArgumentList(Transmit::Results::Create(status))); +} + +ExtensionFunction::ResponseAction AudioModemStopTransmitFunction::Run() { + scoped_ptr<StopTransmit::Params> params(StopTransmit::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + + Status status = AudioModemAPI::GetFactoryInstance()->Get(browser_context()) + ->StopTransmit(extension_id(), AudioTypeForBand(params->band)); + return RespondNow(ArgumentList(StopTransmit::Results::Create(status))); +} + +ExtensionFunction::ResponseAction AudioModemReceiveFunction::Run() { + scoped_ptr<Receive::Params> params(Receive::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + AudioModemAPI* api = + AudioModemAPI::GetFactoryInstance()->Get(browser_context()); + if (api->init_failed()) { + return RespondNow(ErrorWithArguments( + Transmit::Results::Create(STATUS_CODERERROR), + kInitFailedError)); + } + + // Check the timeout. + int64_t timeout_millis = params->params.timeout_millis; + if (timeout_millis <= 0) { + return RespondNow(ErrorWithArguments( + Receive::Results::Create(STATUS_INVALIDREQUEST), + kInvalidTimeoutError)); + } + if (timeout_millis > kMaxReceiveTimeout) + timeout_millis = kMaxReceiveTimeout; + + // Start receiving. + api->StartReceive(extension_id(), params->params); + return RespondNow(ArgumentList(Receive::Results::Create(STATUS_SUCCESS))); +} + +ExtensionFunction::ResponseAction AudioModemStopReceiveFunction::Run() { + scoped_ptr<StopReceive::Params> params(StopReceive::Params::Create(*args_)); + EXTENSION_FUNCTION_VALIDATE(params.get()); + + Status status = AudioModemAPI::GetFactoryInstance()->Get(browser_context()) + ->StopReceive(extension_id(), AudioTypeForBand(params->band)); + return RespondNow(ArgumentList(StopReceive::Results::Create(status))); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/audio_modem/audio_modem_api.h b/chrome/browser/extensions/api/audio_modem/audio_modem_api.h new file mode 100644 index 0000000..557ecef --- /dev/null +++ b/chrome/browser/extensions/api/audio_modem/audio_modem_api.h @@ -0,0 +1,130 @@ +// Copyright 2015 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. + +#ifndef CHROME_BROWSER_EXTENSIONS_API_AUDIO_MODEM_AUDIO_MODEM_API_H_ +#define CHROME_BROWSER_EXTENSIONS_API_AUDIO_MODEM_AUDIO_MODEM_API_H_ + +#include <map> +#include <string> +#include <vector> + +#include "base/memory/scoped_ptr.h" +#include "chrome/common/extensions/api/audio_modem.h" +#include "components/audio_modem/public/modem.h" +#include "extensions/browser/browser_context_keyed_api_factory.h" +#include "extensions/browser/extension_function.h" +#include "extensions/browser/extension_function_histogram_value.h" + +namespace extensions { + +// Implementation of the chrome.audioModem API. +class AudioModemAPI final : public BrowserContextKeyedAPI { + public: + // Default constructor. + explicit AudioModemAPI(content::BrowserContext* context); + + // Testing constructor: pass in dependencies. + AudioModemAPI(content::BrowserContext* context, + scoped_ptr<audio_modem::WhispernetClient> whispernet_client, + scoped_ptr<audio_modem::Modem> modem); + + ~AudioModemAPI() override; + + // Starts transmitting a token, and returns the associated API status. + // Fails if another app is already transmitting the same AudioType. + api::audio_modem::Status StartTransmit( + const std::string& app_id, + const api::audio_modem::RequestParams& params, + const std::string& token); + + // Stops an in-progress transmit, and returns the associated API status. + // Fails if the specified app is not currently transmitting. + api::audio_modem::Status StopTransmit(const std::string& app_id, + audio_modem::AudioType audio_type); + + // Starts receiving for the specified app. + // Multiple apps may receive the same AudioType simultaneously. + void StartReceive(const std::string& app_id, + const api::audio_modem::RequestParams& params); + + // Stops receiving for the specified app, and returns the associated + // API status. Fails if that app is not currently receiving. + api::audio_modem::Status StopReceive(const std::string& app_id, + audio_modem::AudioType audio_type); + + bool init_failed() const { return init_failed_; } + + // BrowserContextKeyedAPI implementation. + static BrowserContextKeyedAPIFactory<AudioModemAPI>* GetFactoryInstance(); + + private: + friend class BrowserContextKeyedAPIFactory<AudioModemAPI>; + + void WhispernetInitComplete(bool success); + void TokensReceived(const std::vector<audio_modem::AudioToken>& tokens); + + content::BrowserContext* const browser_context_; + scoped_ptr<audio_modem::WhispernetClient> whispernet_client_; + scoped_ptr<audio_modem::Modem> modem_; + bool init_failed_; + + // IDs for the currently transmitting app (if any), indexed by AudioType. + std::string transmitters_[2]; + + // Timeouts for the currently active transmits, indexed by AudioType. + base::OneShotTimer<AudioModemAPI> transmit_timers_[2]; + + // Maps of currently receiving app ID => timeouts. Indexed by AudioType. + // We own all of these pointers. Do not remove them without calling delete. + std::map<std::string, base::OneShotTimer<AudioModemAPI>*> receive_timers_[2]; + + // BrowserContextKeyedAPI implementation. + static const char* service_name() { return "AudioModemAPI"; } + + DISALLOW_COPY_AND_ASSIGN(AudioModemAPI); +}; + +template<> +void BrowserContextKeyedAPIFactory<AudioModemAPI>::DeclareFactoryDependencies(); + +class AudioModemTransmitFunction : public UIThreadExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION("audioModem.transmit", AUDIOMODEM_TRANSMIT); + + protected: + ~AudioModemTransmitFunction() override {} + ExtensionFunction::ResponseAction Run() override; +}; + +class AudioModemStopTransmitFunction : public UIThreadExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION("audioModem.stopTransmit", + AUDIOMODEM_STOPTRANSMIT); + + protected: + ~AudioModemStopTransmitFunction() override {} + ExtensionFunction::ResponseAction Run() override; +}; + +class AudioModemReceiveFunction : public UIThreadExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION("audioModem.receive", AUDIOMODEM_RECEIVE); + + protected: + ~AudioModemReceiveFunction() override {} + ExtensionFunction::ResponseAction Run() override; +}; + +class AudioModemStopReceiveFunction : public UIThreadExtensionFunction { + public: + DECLARE_EXTENSION_FUNCTION("audioModem.stopReceive", AUDIOMODEM_STOPRECEIVE); + + protected: + ~AudioModemStopReceiveFunction() override {} + ExtensionFunction::ResponseAction Run() override; +}; + +} // namespace extensions + +#endif // CHROME_BROWSER_EXTENSIONS_API_AUDIO_MODEM_AUDIO_MODEM_API_H_ diff --git a/chrome/browser/extensions/api/audio_modem/audio_modem_api_unittest.cc b/chrome/browser/extensions/api/audio_modem/audio_modem_api_unittest.cc new file mode 100644 index 0000000..f1fa5ac --- /dev/null +++ b/chrome/browser/extensions/api/audio_modem/audio_modem_api_unittest.cc @@ -0,0 +1,380 @@ +// Copyright 2015 The Chromium Authors. All rights reserved. +// Use of this source code is governed by a BSD-style license that can be +// found in the LICENSE file. + +#include <map> +#include <string> +#include <vector> + +#include "base/callback.h" +#include "base/memory/ref_counted.h" +#include "base/memory/scoped_ptr.h" +#include "base/values.h" +#include "chrome/browser/extensions/api/audio_modem/audio_modem_api.h" +#include "chrome/browser/extensions/extension_api_unittest.h" +#include "chrome/browser/extensions/extension_function_test_utils.h" +#include "chrome/browser/extensions/test_extension_system.h" +#include "components/audio_modem/public/modem.h" +#include "components/audio_modem/test/stub_modem.h" +#include "components/audio_modem/test/stub_whispernet_client.h" +#include "extensions/browser/api_test_utils.h" +#include "extensions/browser/event_router.h" + +using audio_modem::AUDIBLE; +using audio_modem::AudioToken; +using audio_modem::INAUDIBLE; +using audio_modem::StubModem; +using audio_modem::StubWhispernetClient; + +using base::BinaryValue; +using base::DictionaryValue; +using base::ListValue; +using base::StringValue; +using base::Value; + +using content::BrowserContext; + +namespace ext_test_utils = extension_function_test_utils; + +namespace extensions { + +namespace { + +// The TestingFactoryFunction uses a BrowserContext as its context pointer. +// But each BrowserContext is still associated with a unit test. +// So we store the StubModem created in each test. +std::map<BrowserContext*, StubModem*> g_modems; + +// Create a test AudioModemAPI and store the modem it uses. +KeyedService* ApiFactoryFunction(BrowserContext* context) { + StubModem* modem = new StubModem; + g_modems[context] = modem; + return new AudioModemAPI( + context, + make_scoped_ptr<audio_modem::WhispernetClient>(new StubWhispernetClient), + make_scoped_ptr<audio_modem::Modem>(modem)); +} + +DictionaryValue* CreateParams(const std::string& audio_band) { + DictionaryValue* params = new DictionaryValue; + params->SetInteger("timeoutMillis", 60000); + params->SetString("band", audio_band); + params->SetInteger("encoding.tokenLength", 4); + return params; +} + +BinaryValue* CreateToken(const std::string& token) { + return BinaryValue::CreateWithCopiedBuffer(token.c_str(), token.size()); +} + +scoped_ptr<ListValue> CreateList(Value* single_elt) { + scoped_ptr<ListValue> list(new ListValue); + list->Append(single_elt); + return list.Pass(); +} + +scoped_ptr<ListValue> CreateList(Value* elt1, Value* elt2) { + scoped_ptr<ListValue> list(new ListValue); + list->Append(elt1); + list->Append(elt2); + return list.Pass(); +} + +DictionaryValue* CreateReceivedToken(const std::string& token, + const std::string& audio_band) { + DictionaryValue* out = new DictionaryValue; + out->Set("token", CreateToken(token)); + out->SetString("band", audio_band); + return out; +} + +bool ReceivedSingleToken(const Event* event, + const DictionaryValue* expected_token) { + ListValue* received_tokens; + event->event_args->GetList(0, &received_tokens); + if (received_tokens->GetSize() != 1) + return false; + + DictionaryValue* received_token; + received_tokens->GetDictionary(0, &received_token); + return received_token->Equals(expected_token); +} + +// TODO(ckehoe): Put this in //extensions/test. +// Then replace the other EventRouter mocks. +class StubEventRouter : public EventRouter { + public: + // Callback to receive events. First argument is + // the extension id to receive the event. + using EventCallback = base::Callback<void(const std::string&, + scoped_ptr<Event>)>; + + StubEventRouter(BrowserContext* context, EventCallback event_callback) + : EventRouter(context, nullptr), + event_callback_(event_callback) {} + + void DispatchEventToExtension(const std::string& extension_id, + scoped_ptr<Event> event) override { + event_callback_.Run(extension_id, event.Pass()); + } + + void ClearEventCallback() { + event_callback_.Reset(); + } + + private: + EventCallback event_callback_; +}; + +} // namespace + +class AudioModemApiUnittest : public ExtensionApiUnittest { + public: + AudioModemApiUnittest() {} + ~AudioModemApiUnittest() override { + for (const auto& events : events_by_extension_id_) { + for (const Event* event : events.second) + delete event; + } + } + + protected: + template<typename Function> + const std::string RunFunction(scoped_ptr<ListValue> args, + const Extension* extension) { + scoped_refptr<UIThreadExtensionFunction> function(new Function); + function->set_extension(extension); + function->set_browser_context(profile()); + function->set_has_callback(true); + ext_test_utils::RunFunction( + function.get(), args.Pass(), browser(), ext_test_utils::NONE); + + std::string result_status; + CHECK(function->GetResultList()->GetString(0, &result_status)); + return result_status; + } + + template<typename Function> + const std::string RunFunction(scoped_ptr<ListValue> args) { + return RunFunction<Function>(args.Pass(), GetExtension(std::string())); + } + + StubModem* GetModem() const { + return g_modems[profile()]; + } + + const Extension* GetExtension(const std::string& name) { + if (!extensions_by_name_[name].get()) { + scoped_ptr<DictionaryValue> extension_definition(new DictionaryValue); + extension_definition->SetString("name", name); + extension_definition->SetString("version", "1.0"); + extensions_by_name_[name] = api_test_utils::CreateExtension( + Manifest::INTERNAL, extension_definition.get(), name); + DVLOG(2) << "Created extension " << extensions_by_name_[name]->id(); + } + return extensions_by_name_[name].get(); + } + + const std::vector<const Event*>& + GetEventsForExtension(const std::string& name) { + const Extension* extension = extensions_by_name_[name].get(); + DCHECK(extension); + return events_by_extension_id_[extension->id()]; + } + + const std::vector<const Event*>& GetEvents() { + return GetEventsForExtension(std::string()); + } + + private: + void SetUp() override { + ExtensionApiUnittest::SetUp(); + AudioModemAPI::GetFactoryInstance()->SetTestingFactory( + profile(), &ApiFactoryFunction); + + scoped_ptr<EventRouter> router(new StubEventRouter( + profile(), + // The EventRouter is deleted in TearDown(). + // It will lose this callback before we are destructed. + base::Bind(&AudioModemApiUnittest::CaptureEvent, + base::Unretained(this)))); + static_cast<TestExtensionSystem*>(ExtensionSystem::Get(profile())) + ->SetEventRouter(router.Pass()); + } + + void CaptureEvent(const std::string& extension_id, + scoped_ptr<Event> event) { + // Since scoped_ptr (and ScopedVector) do not work inside STL containers, + // we must manage this memory manually. It is cleaned up by the destructor. + events_by_extension_id_[extension_id].push_back(event.release()); + } + + std::map<std::string, scoped_refptr<Extension>> extensions_by_name_; + + // We own all of these pointers. + // Do not remove them from the map without calling delete. + std::map<std::string, std::vector<const Event*>> events_by_extension_id_; +}; + +TEST_F(AudioModemApiUnittest, TransmitBasic) { + // Start transmitting inaudibly. + EXPECT_EQ("success", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("inaudible"), CreateToken("1234")))); + EXPECT_TRUE(GetModem()->IsPlaying(INAUDIBLE)); + + // Can't cancel audible transmit - we haven't started it yet. + EXPECT_EQ("invalidRequest", RunFunction<AudioModemStopTransmitFunction>( + CreateList(new StringValue("audible")))); + + // Start transmitting audibly. + EXPECT_EQ("success", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("audible"), CreateToken("ABCD")))); + EXPECT_TRUE(GetModem()->IsPlaying(AUDIBLE)); + + // Stop audible transmit. We're still transmitting inaudibly. + EXPECT_EQ("success", RunFunction<AudioModemStopTransmitFunction>( + CreateList(new StringValue("audible")))); + EXPECT_FALSE(GetModem()->IsPlaying(AUDIBLE)); + EXPECT_TRUE(GetModem()->IsPlaying(INAUDIBLE)); + + // Stop inaudible transmit. + EXPECT_EQ("success", RunFunction<AudioModemStopTransmitFunction>( + CreateList(new StringValue("inaudible")))); + EXPECT_FALSE(GetModem()->IsPlaying(INAUDIBLE)); +} + +TEST_F(AudioModemApiUnittest, ReceiveBasic) { + // Start listening for audible tokens. + EXPECT_EQ("success", RunFunction<AudioModemReceiveFunction>( + CreateList(CreateParams("audible")))); + EXPECT_TRUE(GetModem()->IsRecording(AUDIBLE)); + + // Can't cancel inaudible receive - we haven't started it yet. + EXPECT_EQ("invalidRequest", RunFunction<AudioModemStopReceiveFunction>( + CreateList(new StringValue("inaudible")))); + + // Send some audible tokens. + std::vector<AudioToken> tokens; + tokens.push_back(AudioToken("1234", true)); + tokens.push_back(AudioToken("ABCD", true)); + tokens.push_back(AudioToken("abcd", false)); + GetModem()->DeliverTokens(tokens); + + // Check the tokens received. + EXPECT_EQ(1u, GetEvents().size()); + scoped_ptr<ListValue> expected_tokens(new ListValue); + expected_tokens->Append(CreateReceivedToken("1234", "audible")); + expected_tokens->Append(CreateReceivedToken("ABCD", "audible")); + ListValue* received_tokens; + GetEvents()[0]->event_args->GetList(0, &received_tokens); + EXPECT_TRUE(received_tokens->Equals(expected_tokens.get())); + + // Start listening for inaudible tokens. + EXPECT_EQ("success", RunFunction<AudioModemReceiveFunction>( + CreateList(CreateParams("inaudible")))); + EXPECT_TRUE(GetModem()->IsRecording(INAUDIBLE)); + + // Send some more tokens. + tokens.push_back(AudioToken("5678", false)); + GetModem()->DeliverTokens(tokens); + + // Check the tokens received. + EXPECT_EQ(2u, GetEvents().size()); + expected_tokens->Append(CreateReceivedToken("abcd", "inaudible")); + expected_tokens->Append(CreateReceivedToken("5678", "inaudible")); + GetEvents()[1]->event_args->GetList(0, &received_tokens); + EXPECT_TRUE(received_tokens->Equals(expected_tokens.get())); + + // Stop audible receive. We're still receiving inaudible. + EXPECT_EQ("success", RunFunction<AudioModemStopReceiveFunction>( + CreateList(new StringValue("audible")))); + EXPECT_FALSE(GetModem()->IsRecording(AUDIBLE)); + EXPECT_TRUE(GetModem()->IsRecording(INAUDIBLE)); + + // Stop inaudible receive. + EXPECT_EQ("success", RunFunction<AudioModemStopReceiveFunction>( + CreateList(new StringValue("inaudible")))); + EXPECT_FALSE(GetModem()->IsRecording(INAUDIBLE)); +} + +TEST_F(AudioModemApiUnittest, TransmitMultiple) { + // Start transmit. + EXPECT_EQ("success", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("audible"), CreateToken("1234")), + GetExtension("ext1"))); + EXPECT_TRUE(GetModem()->IsPlaying(AUDIBLE)); + + // Another extension can't interfere with the first one. + EXPECT_EQ("inUse", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("audible"), CreateToken("ABCD")), + GetExtension("ext2"))); + EXPECT_EQ("invalidRequest", RunFunction<AudioModemStopTransmitFunction>( + CreateList(new StringValue("audible")), GetExtension("ext2"))); + EXPECT_TRUE(GetModem()->IsPlaying(AUDIBLE)); + + // The other extension can use the other audio band, however. + EXPECT_EQ("success", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("inaudible"), CreateToken("ABCD")), + GetExtension("ext2"))); + EXPECT_TRUE(GetModem()->IsPlaying(INAUDIBLE)); + + // The first extension can change its token. + // But the other band is still in use. + EXPECT_EQ("success", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("audible"), CreateToken("abcd")), + GetExtension("ext1"))); + EXPECT_EQ("inUse", RunFunction<AudioModemTransmitFunction>( + CreateList(CreateParams("inaudible"), CreateToken("1234")), + GetExtension("ext1"))); + + // Stop transmission. + EXPECT_EQ("success", RunFunction<AudioModemStopTransmitFunction>( + CreateList(new StringValue("audible")), GetExtension("ext1"))); + EXPECT_FALSE(GetModem()->IsPlaying(AUDIBLE)); + EXPECT_EQ("success", RunFunction<AudioModemStopTransmitFunction>( + CreateList(new StringValue("inaudible")), GetExtension("ext2"))); + EXPECT_FALSE(GetModem()->IsPlaying(INAUDIBLE)); +} + +TEST_F(AudioModemApiUnittest, ReceiveMultiple) { + // Start receive. Multiple extensions can receive on the same band. + EXPECT_EQ("success", RunFunction<AudioModemReceiveFunction>( + CreateList(CreateParams("inaudible")), GetExtension("ext1"))); + EXPECT_TRUE(GetModem()->IsRecording(INAUDIBLE)); + EXPECT_EQ("success", RunFunction<AudioModemReceiveFunction>( + CreateList(CreateParams("inaudible")), GetExtension("ext2"))); + + // Receive a token. + GetModem()->DeliverTokens(std::vector<AudioToken>( + 1, AudioToken("abcd", false))); + EXPECT_EQ(1u, GetEventsForExtension("ext1").size()); + EXPECT_EQ(1u, GetEventsForExtension("ext2").size()); + + // Check the token received. + scoped_ptr<DictionaryValue> expected_token( + CreateReceivedToken("abcd", "inaudible")); + EXPECT_TRUE(ReceivedSingleToken( + GetEventsForExtension("ext1")[0], expected_token.get())); + EXPECT_TRUE(ReceivedSingleToken( + GetEventsForExtension("ext2")[0], expected_token.get())); + + // If one extension stops, the modem is still receiving for the other. + EXPECT_EQ("success", RunFunction<AudioModemStopReceiveFunction>( + CreateList(new StringValue("inaudible")), GetExtension("ext1"))); + EXPECT_TRUE(GetModem()->IsRecording(INAUDIBLE)); + + // Receive another token. Should only go to ext2. + GetModem()->DeliverTokens(std::vector<AudioToken>( + 1, AudioToken("1234", false))); + EXPECT_EQ(1u, GetEventsForExtension("ext1").size()); + EXPECT_EQ(2u, GetEventsForExtension("ext2").size()); + expected_token.reset(CreateReceivedToken("1234", "inaudible")); + EXPECT_TRUE(ReceivedSingleToken( + GetEventsForExtension("ext2")[1], expected_token.get())); + + EXPECT_EQ("success", RunFunction<AudioModemStopReceiveFunction>( + CreateList(new StringValue("inaudible")), GetExtension("ext2"))); + EXPECT_FALSE(GetModem()->IsRecording(INAUDIBLE)); +} + +} // namespace extensions diff --git a/chrome/browser/extensions/api/copresence_private/copresence_private_api.cc b/chrome/browser/extensions/api/copresence_private/copresence_private_api.cc index fd6e508..54fba44 100644 --- a/chrome/browser/extensions/api/copresence_private/copresence_private_api.cc +++ b/chrome/browser/extensions/api/copresence_private/copresence_private_api.cc @@ -4,59 +4,77 @@ #include "chrome/browser/extensions/api/copresence_private/copresence_private_api.h" +#include <map> +#include <string> #include <vector> +#include "base/guid.h" #include "base/lazy_instance.h" #include "base/stl_util.h" #include "chrome/browser/copresence/chrome_whispernet_client.h" -#include "chrome/browser/extensions/api/copresence/copresence_api.h" #include "chrome/common/extensions/api/copresence_private.h" #include "media/base/audio_bus.h" +using audio_modem::WhispernetClient; + +namespace { + +base::LazyInstance<std::map<std::string, WhispernetClient*>> +g_whispernet_clients = LAZY_INSTANCE_INITIALIZER; + +WhispernetClient* GetWhispernetClient(const std::string& id) { + WhispernetClient* client = g_whispernet_clients.Get()[id]; + DCHECK(client); + return client; +} + +} // namespace + namespace extensions { namespace SendFound = api::copresence_private::SendFound; namespace SendSamples = api::copresence_private::SendSamples; -namespace SendDetect = api::copresence_private::SendDetect; namespace SendInitialized = api::copresence_private::SendInitialized; -// Copresence Private functions. +namespace copresence_private { -audio_modem::WhispernetClient* -CopresencePrivateFunction::GetWhispernetClient() { - CopresenceService* service = - CopresenceService::GetFactoryInstance()->Get(browser_context()); - return service ? service->whispernet_client() : NULL; +const std::string RegisterWhispernetClient(WhispernetClient* client) { + std::string id = base::GenerateGUID(); + g_whispernet_clients.Get()[id] = client; + return id; } +} // namespace copresence_private + +// Copresence Private functions. + // CopresenceSendFoundFunction implementation: ExtensionFunction::ResponseAction CopresencePrivateSendFoundFunction::Run() { - if (!GetWhispernetClient() || - GetWhispernetClient()->GetTokensCallback().is_null()) { - return RespondNow(NoArguments()); - } - scoped_ptr<SendFound::Params> params(SendFound::Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); + + WhispernetClient* whispernet_client = GetWhispernetClient(params->client_id); + if (whispernet_client->GetTokensCallback().is_null()) + return RespondNow(NoArguments()); + std::vector<audio_modem::AudioToken> tokens; for (size_t i = 0; i < params->tokens.size(); ++i) { tokens.push_back(audio_modem::AudioToken(params->tokens[i]->token, params->tokens[i]->audible)); } - GetWhispernetClient()->GetTokensCallback().Run(tokens); + whispernet_client->GetTokensCallback().Run(tokens); return RespondNow(NoArguments()); } // CopresenceSendEncodedFunction implementation: ExtensionFunction::ResponseAction CopresencePrivateSendSamplesFunction::Run() { - if (!GetWhispernetClient() || - GetWhispernetClient()->GetSamplesCallback().is_null()) { - return RespondNow(NoArguments()); - } - scoped_ptr<SendSamples::Params> params(SendSamples::Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); + WhispernetClient* whispernet_client = GetWhispernetClient(params->client_id); + if (whispernet_client->GetSamplesCallback().is_null()) + return RespondNow(NoArguments()); + scoped_refptr<media::AudioBusRefCounted> samples = media::AudioBusRefCounted::Create(1, // Mono params->samples.size() / sizeof(float)); @@ -64,39 +82,28 @@ ExtensionFunction::ResponseAction CopresencePrivateSendSamplesFunction::Run() { memcpy(samples->channel(0), vector_as_array(¶ms->samples), params->samples.size()); - GetWhispernetClient()->GetSamplesCallback().Run( + whispernet_client->GetSamplesCallback().Run( params->token.audible ? audio_modem::AUDIBLE : audio_modem::INAUDIBLE, params->token.token, samples); return RespondNow(NoArguments()); } -// CopresenceSendDetectFunction implementation: -ExtensionFunction::ResponseAction CopresencePrivateSendDetectFunction::Run() { - if (!GetWhispernetClient() || - GetWhispernetClient()->GetDetectBroadcastCallback().is_null()) { - return RespondNow(NoArguments()); - } - - scoped_ptr<SendDetect::Params> params(SendDetect::Params::Create(*args_)); - EXTENSION_FUNCTION_VALIDATE(params.get()); - - GetWhispernetClient()->GetDetectBroadcastCallback().Run(params->detected); - return RespondNow(NoArguments()); -} - // CopresenceSendInitializedFunction implementation: ExtensionFunction::ResponseAction CopresencePrivateSendInitializedFunction::Run() { - if (!GetWhispernetClient() || - GetWhispernetClient()->GetInitializedCallback().is_null()) { - return RespondNow(NoArguments()); - } - scoped_ptr<SendInitialized::Params> params( SendInitialized::Params::Create(*args_)); EXTENSION_FUNCTION_VALIDATE(params.get()); - GetWhispernetClient()->GetInitializedCallback().Run(params->success); + DVLOG(2) << "Forwarding init callback to " + << g_whispernet_clients.Get().size() << " clients"; + for (auto client_entry : g_whispernet_clients.Get()) { + audio_modem::SuccessCallback init_callback = + client_entry.second->GetInitializedCallback(); + if (!init_callback.is_null()) + init_callback.Run(params->success); + } + return RespondNow(NoArguments()); } diff --git a/chrome/browser/extensions/api/copresence_private/copresence_private_api.h b/chrome/browser/extensions/api/copresence_private/copresence_private_api.h index 3377336..d4c2098 100644 --- a/chrome/browser/extensions/api/copresence_private/copresence_private_api.h +++ b/chrome/browser/extensions/api/copresence_private/copresence_private_api.h @@ -5,7 +5,9 @@ #ifndef CHROME_BROWSER_EXTENSIONS_API_COPRESENCE_PRIVATE_COPRESENCE_PRIVATE_API_H_ #define CHROME_BROWSER_EXTENSIONS_API_COPRESENCE_PRIVATE_COPRESENCE_PRIVATE_API_H_ -#include "chrome/browser/extensions/chrome_extension_function.h" +#include <string> + +#include "extensions/browser/extension_function.h" namespace audio_modem { class WhispernetClient; @@ -13,13 +15,15 @@ class WhispernetClient; namespace extensions { -class CopresencePrivateFunction : public ChromeUIThreadExtensionFunction { - protected: - audio_modem::WhispernetClient* GetWhispernetClient(); - ~CopresencePrivateFunction() override {} -}; +namespace copresence_private { + +// Register a client to receive events from Whispernet. +const std::string +RegisterWhispernetClient(audio_modem::WhispernetClient* client); + +} // namespace copresence_private -class CopresencePrivateSendFoundFunction : public CopresencePrivateFunction { +class CopresencePrivateSendFoundFunction : public UIThreadExtensionFunction { public: DECLARE_EXTENSION_FUNCTION("copresencePrivate.sendFound", COPRESENCEPRIVATE_SENDFOUND); @@ -29,7 +33,7 @@ class CopresencePrivateSendFoundFunction : public CopresencePrivateFunction { ExtensionFunction::ResponseAction Run() override; }; -class CopresencePrivateSendSamplesFunction : public CopresencePrivateFunction { +class CopresencePrivateSendSamplesFunction : public UIThreadExtensionFunction { public: DECLARE_EXTENSION_FUNCTION("copresencePrivate.sendSamples", COPRESENCEPRIVATE_SENDSAMPLES); @@ -39,7 +43,7 @@ class CopresencePrivateSendSamplesFunction : public CopresencePrivateFunction { ExtensionFunction::ResponseAction Run() override; }; -class CopresencePrivateSendDetectFunction : public CopresencePrivateFunction { +class CopresencePrivateSendDetectFunction : public UIThreadExtensionFunction { public: DECLARE_EXTENSION_FUNCTION("copresencePrivate.sendDetect", COPRESENCEPRIVATE_SENDDETECT); @@ -50,7 +54,7 @@ class CopresencePrivateSendDetectFunction : public CopresencePrivateFunction { }; class CopresencePrivateSendInitializedFunction - : public CopresencePrivateFunction { + : public UIThreadExtensionFunction { public: DECLARE_EXTENSION_FUNCTION("copresencePrivate.sendInitialized", COPRESENCEPRIVATE_SENDINITIALIZED); |