diff options
38 files changed, 1477 insertions, 391 deletions
diff --git a/chrome/browser/copresence/chrome_whispernet_client.cc b/chrome/browser/copresence/chrome_whispernet_client.cc index d8b4869..fea965c 100644 --- a/chrome/browser/copresence/chrome_whispernet_client.cc +++ b/chrome/browser/copresence/chrome_whispernet_client.cc @@ -22,8 +22,22 @@ using audio_modem::INAUDIBLE; using audio_modem::SamplesCallback; using audio_modem::SuccessCallback; using audio_modem::TokensCallback; +using audio_modem::TokenParameters; -namespace copresence_private = extensions::api::copresence_private; +using extensions::api::copresence_private::AudioParameters; +using extensions::api::copresence_private::DecodeSamplesParameters; +using extensions::api::copresence_private::EncodeTokenParameters; +using ApiTokenParams = extensions::api::copresence_private::TokenParameters; + +namespace OnConfigAudio = + extensions::api::copresence_private::OnConfigAudio; +namespace OnDecodeSamplesRequest = + extensions::api::copresence_private::OnDecodeSamplesRequest; +namespace OnEncodeTokenRequest = + extensions::api::copresence_private::OnEncodeTokenRequest; + +using extensions::Event; +using extensions::copresence_private::RegisterWhispernetClient; namespace { @@ -45,13 +59,22 @@ AudioParamData GetDefaultAudioConfig() { return config_data; } +// ApiTokenParams is not copyable, so we must take it as an output argument. +// TODO(ckehoe): Pass protos to Whispernet to avoid all these conversions. +void ConvertTokenParams(const TokenParameters& in, ApiTokenParams* out) { + out->length = in.length; + out->crc = in.crc; + out->parity = in.parity; +} + } // namespace // static const char ChromeWhispernetClient::kWhispernetProxyExtensionId[] = "bpfmnplchembfbdgieamdodgaencleal"; -// Public: + +// Public functions. ChromeWhispernetClient::ChromeWhispernetClient( content::BrowserContext* browser_context) @@ -59,81 +82,67 @@ ChromeWhispernetClient::ChromeWhispernetClient( event_router_(extensions::EventRouter::Get(browser_context)), extension_loaded_(false) { DCHECK(browser_context_); - DCHECK(event_router_); } -ChromeWhispernetClient::~ChromeWhispernetClient() { -} +ChromeWhispernetClient::~ChromeWhispernetClient() {} void ChromeWhispernetClient::Initialize( const SuccessCallback& init_callback) { DVLOG(3) << "Initializing whispernet proxy client."; + init_callback_ = init_callback; + DCHECK(!init_callback_.is_null()); - extensions::ExtensionSystem* es = - extensions::ExtensionSystem::Get(browser_context_); - DCHECK(es); - ExtensionService* service = es->extension_service(); - DCHECK(service); - extensions::ComponentLoader* loader = service->component_loader(); + extensions::ComponentLoader* loader = + extensions::ExtensionSystem::Get(browser_context_) + ->extension_service()->component_loader(); DCHECK(loader); - // This callback is cancelled in Shutdown(). - extension_loaded_callback_ = base::Bind( - &ChromeWhispernetClient::OnExtensionLoaded, base::Unretained(this)); - if (!loader->Exists(kWhispernetProxyExtensionId)) { DVLOG(3) << "Loading Whispernet proxy."; loader->Add(IDR_WHISPERNET_PROXY_MANIFEST, base::FilePath(FILE_PATH_LITERAL("whispernet_proxy"))); } else { init_callback_.Run(true); + extension_loaded_ = true; } + client_id_ = RegisterWhispernetClient(this); AudioConfiguration(GetDefaultAudioConfig()); } -void ChromeWhispernetClient::EncodeToken(const std::string& token_str, - AudioType type) { - copresence_private::EncodeTokenParameters params; +void ChromeWhispernetClient::EncodeToken( + const std::string& token_str, + AudioType type, + const TokenParameters token_params[2]) { + DCHECK(type == AUDIBLE || type == INAUDIBLE); + + EncodeTokenParameters params; params.token.token = token_str; - params.token.audible = type == AUDIBLE; - scoped_ptr<extensions::Event> event(new extensions::Event( - copresence_private::OnEncodeTokenRequest::kEventName, - copresence_private::OnEncodeTokenRequest::Create(params), - browser_context_)); + params.token.audible = (type == AUDIBLE); + ConvertTokenParams(token_params[type], ¶ms.token_params); - SendEventIfLoaded(event.Pass()); + SendEventIfLoaded(make_scoped_ptr(new Event( + OnEncodeTokenRequest::kEventName, + OnEncodeTokenRequest::Create(client_id_, params), + browser_context_))); } -void ChromeWhispernetClient::DecodeSamples(AudioType type, - const std::string& samples, - const size_t token_length[2]) { - copresence_private::DecodeSamplesParameters params; +void ChromeWhispernetClient::DecodeSamples( + AudioType type, + const std::string& samples, + const TokenParameters token_params[2]) { + DecodeSamplesParameters params; params.samples.assign(samples.begin(), samples.end()); - params.decode_audible = - type == AUDIBLE || type == BOTH; - params.decode_inaudible = - type == INAUDIBLE || type == BOTH; - params.audible_token_length = token_length[AUDIBLE]; - params.inaudible_token_length = token_length[INAUDIBLE]; - - scoped_ptr<extensions::Event> event(new extensions::Event( - copresence_private::OnDecodeSamplesRequest::kEventName, - copresence_private::OnDecodeSamplesRequest::Create( - params), - browser_context_)); - - SendEventIfLoaded(event.Pass()); -} + params.decode_audible = (type == AUDIBLE || type == BOTH); + params.decode_inaudible = (type == INAUDIBLE || type == BOTH); + ConvertTokenParams(token_params[AUDIBLE], ¶ms.audible_token_params); + ConvertTokenParams(token_params[INAUDIBLE], ¶ms.inaudible_token_params); -void ChromeWhispernetClient::DetectBroadcast() { - scoped_ptr<extensions::Event> event(new extensions::Event( - copresence_private::OnDetectBroadcastRequest::kEventName, - make_scoped_ptr(new base::ListValue()), - browser_context_)); - - SendEventIfLoaded(event.Pass()); + SendEventIfLoaded(make_scoped_ptr(new Event( + OnDecodeSamplesRequest::kEventName, + OnDecodeSamplesRequest::Create(client_id_, params), + browser_context_))); } void ChromeWhispernetClient::RegisterTokensCallback( @@ -146,11 +155,6 @@ void ChromeWhispernetClient::RegisterSamplesCallback( samples_callback_ = samples_callback; } -void ChromeWhispernetClient::RegisterDetectBroadcastCallback( - const SuccessCallback& db_callback) { - db_callback_ = db_callback; -} - TokensCallback ChromeWhispernetClient::GetTokensCallback() { return tokens_callback_; } @@ -159,18 +163,16 @@ SamplesCallback ChromeWhispernetClient::GetSamplesCallback() { return samples_callback_; } -SuccessCallback ChromeWhispernetClient::GetDetectBroadcastCallback() { - return db_callback_; -} - SuccessCallback ChromeWhispernetClient::GetInitializedCallback() { - return extension_loaded_callback_; + return base::Bind(&ChromeWhispernetClient::OnExtensionLoaded, + base::Unretained(this)); } -// Private: + +// Private functions. void ChromeWhispernetClient::AudioConfiguration(const AudioParamData& params) { - copresence_private::AudioParameters audio_params; + AudioParameters audio_params; // We serialize AudioConfigData to a string and send it to the whispernet // nacl wrapper. @@ -178,16 +180,16 @@ void ChromeWhispernetClient::AudioConfiguration(const AudioParamData& params) { audio_params.param_data.resize(params_size); memcpy(vector_as_array(&audio_params.param_data), ¶ms, params_size); - scoped_ptr<extensions::Event> event(new extensions::Event( - copresence_private::OnConfigAudio::kEventName, - copresence_private::OnConfigAudio::Create(audio_params), - browser_context_)); - - SendEventIfLoaded(event.Pass()); + SendEventIfLoaded(make_scoped_ptr(new Event( + OnConfigAudio::kEventName, + OnConfigAudio::Create(client_id_, audio_params), + browser_context_))); } void ChromeWhispernetClient::SendEventIfLoaded( scoped_ptr<extensions::Event> event) { + DCHECK(event_router_); + if (extension_loaded_) { event_router_->DispatchEventToExtension(kWhispernetProxyExtensionId, event.Pass()); @@ -198,15 +200,16 @@ void ChromeWhispernetClient::SendEventIfLoaded( } void ChromeWhispernetClient::OnExtensionLoaded(bool success) { - if (!init_callback_.is_null()) - init_callback_.Run(success); + DCHECK(!init_callback_.is_null()); + init_callback_.Run(success); DVLOG(3) << "Sending " << queued_events_.size() << " queued requests to whispernet"; // In this loop, ownership of each Event is passed to a scoped_ptr instead. // Thus we can just discard the pointers at the end. - for (extensions::Event* event : queued_events_) { + DCHECK(event_router_); + for (Event* event : queued_events_) { event_router_->DispatchEventToExtension(kWhispernetProxyExtensionId, make_scoped_ptr(event)); } diff --git a/chrome/browser/copresence/chrome_whispernet_client.h b/chrome/browser/copresence/chrome_whispernet_client.h index 74c1e7d..5548104 100644 --- a/chrome/browser/copresence/chrome_whispernet_client.h +++ b/chrome/browser/copresence/chrome_whispernet_client.h @@ -45,21 +45,19 @@ class ChromeWhispernetClient final : public audio_modem::WhispernetClient { // WhispernetClient overrides: void Initialize(const audio_modem::SuccessCallback& init_callback) override; void EncodeToken(const std::string& token_str, - audio_modem::AudioType type) override; - void DecodeSamples(audio_modem::AudioType type, - const std::string& samples, - const size_t token_length[2]) override; - void DetectBroadcast() override; + audio_modem::AudioType type, + const audio_modem::TokenParameters token_params[2]) override; + void DecodeSamples( + audio_modem::AudioType type, + const std::string& samples, + const audio_modem::TokenParameters token_params[2]) override; void RegisterTokensCallback( const audio_modem::TokensCallback& tokens_callback) override; void RegisterSamplesCallback( const audio_modem::SamplesCallback& samples_callback) override; - void RegisterDetectBroadcastCallback( - const audio_modem::SuccessCallback& db_callback) override; audio_modem::TokensCallback GetTokensCallback() override; audio_modem::SamplesCallback GetSamplesCallback() override; - audio_modem::SuccessCallback GetDetectBroadcastCallback() override; audio_modem::SuccessCallback GetInitializedCallback() override; static const char kWhispernetProxyExtensionId[]; @@ -70,19 +68,18 @@ class ChromeWhispernetClient final : public audio_modem::WhispernetClient { void SendEventIfLoaded(scoped_ptr<extensions::Event> event); - // This gets called twice; once when the proxy extension loads, the second - // time when we have initialized the proxy extension's encoder and decoder. + // This gets called when the proxy extension loads. void OnExtensionLoaded(bool success); content::BrowserContext* const browser_context_; extensions::EventRouter* const event_router_; + std::string client_id_; audio_modem::SuccessCallback extension_loaded_callback_; audio_modem::SuccessCallback init_callback_; audio_modem::TokensCallback tokens_callback_; audio_modem::SamplesCallback samples_callback_; - audio_modem::SuccessCallback db_callback_; ScopedVector<extensions::Event> queued_events_; bool extension_loaded_; diff --git a/chrome/browser/copresence/chrome_whispernet_client_browsertest.cc b/chrome/browser/copresence/chrome_whispernet_client_browsertest.cc index 5c1f5b8..c6a9bc7 100644 --- a/chrome/browser/copresence/chrome_whispernet_client_browsertest.cc +++ b/chrome/browser/copresence/chrome_whispernet_client_browsertest.cc @@ -23,6 +23,7 @@ using audio_modem::WhispernetClient; using audio_modem::AUDIBLE; using audio_modem::INAUDIBLE; +using audio_modem::TokenParameters; namespace { @@ -31,13 +32,7 @@ const char kSixZeros[] = "MDAwMDAw"; const char kEightZeros[] = "MDAwMDAwMDA"; const char kNineZeros[] = "MDAwMDAwMDAw"; -const size_t kTokenLengths[] = {6, 6}; - -WhispernetClient* GetWhispernetClient(content::BrowserContext* context) { - extensions::CopresenceService* service = - extensions::CopresenceService::GetFactoryInstance()->Get(context); - return service ? service->whispernet_client() : NULL; -} +const size_t kTokenLengths[2] = {6, 6}; // Copied from src/components/copresence/mediums/audio/audio_recorder.cc std::string AudioBusToString(scoped_refptr<media::AudioBusRefCounted> source) { @@ -54,29 +49,38 @@ std::string AudioBusToString(scoped_refptr<media::AudioBusRefCounted> source) { return buffer; } +void GetTokenParamsForLengths(const size_t token_lengths[2], + TokenParameters* params) { + params[0].length = token_lengths[0]; + params[1].length = token_lengths[1]; +} + +void IgnoreResult(bool success) {} + } // namespace class ChromeWhispernetClientTest : public ExtensionBrowserTest { protected: ChromeWhispernetClientTest() - : context_(NULL), expected_audible_(false), initialized_(false) {} + : initialized_(false), expected_audible_(false) {} ~ChromeWhispernetClientTest() override {} void InitializeWhispernet() { - context_ = browser()->profile(); - run_loop_.reset(new base::RunLoop()); - GetWhispernetClient(context_)->Initialize(base::Bind( + scoped_ptr<WhispernetClient> client( + new ChromeWhispernetClient(browser()->profile())); + client->Initialize(base::Bind( &ChromeWhispernetClientTest::InitCallback, base::Unretained(this))); + + run_loop_.reset(new base::RunLoop()); run_loop_->Run(); EXPECT_TRUE(initialized_); } - void EncodeTokenAndSaveSamples(bool audible, const std::string& token) { - WhispernetClient* client = GetWhispernetClient(context_); - ASSERT_TRUE(client); - + void EncodeTokenAndSaveSamples(WhispernetClient* client, + bool audible, + const std::string& token) { run_loop_.reset(new base::RunLoop()); client->RegisterSamplesCallback( base::Bind(&ChromeWhispernetClientTest::SamplesCallback, @@ -84,18 +88,17 @@ class ChromeWhispernetClientTest : public ExtensionBrowserTest { expected_token_ = token; expected_audible_ = audible; - client->EncodeToken(token, audible ? AUDIBLE : INAUDIBLE); + TokenParameters token_params[2]; + client->EncodeToken(token, audible ? AUDIBLE : INAUDIBLE, token_params); run_loop_->Run(); EXPECT_GT(saved_samples_->frames(), 0); } - void DecodeSamplesAndVerifyToken(bool expect_audible, + void DecodeSamplesAndVerifyToken(WhispernetClient* client, + bool expect_audible, const std::string& expected_token, - const size_t token_length[2]) { - WhispernetClient* client = GetWhispernetClient(context_); - ASSERT_TRUE(client); - + const TokenParameters token_params[2]) { run_loop_.reset(new base::RunLoop()); client->RegisterTokensCallback(base::Bind( &ChromeWhispernetClientTest::TokensCallback, base::Unretained(this))); @@ -115,21 +118,9 @@ class ChromeWhispernetClientTest : public ExtensionBrowserTest { saved_samples_->channel(0), sizeof(float) * saved_samples_->frames()); - client->DecodeSamples( - expect_audible ? AUDIBLE : INAUDIBLE, - AudioBusToString(samples_bus), token_length); - run_loop_->Run(); - } - - void DetectBroadcast() { - WhispernetClient* client = GetWhispernetClient(context_); - ASSERT_TRUE(client); - - run_loop_.reset(new base::RunLoop()); - client->RegisterDetectBroadcastCallback( - base::Bind(&ChromeWhispernetClientTest::DetectBroadcastCallback, - base::Unretained(this))); - client->DetectBroadcast(); + client->DecodeSamples(expect_audible ? AUDIBLE : INAUDIBLE, + AudioBusToString(samples_bus), + token_params); run_loop_->Run(); } @@ -159,77 +150,83 @@ class ChromeWhispernetClientTest : public ExtensionBrowserTest { EXPECT_EQ(expected_audible_, tokens[0].audible); } - void DetectBroadcastCallback(bool success) { - EXPECT_TRUE(success); - ASSERT_TRUE(run_loop_); - run_loop_->Quit(); - } - - private: scoped_ptr<base::RunLoop> run_loop_; - content::BrowserContext* context_; + bool initialized_; + private: std::string expected_token_; bool expected_audible_; scoped_refptr<media::AudioBusRefCounted> saved_samples_; - bool initialized_; - DISALLOW_COPY_AND_ASSIGN(ChromeWhispernetClientTest); }; // These tests are irrelevant if NACL is disabled. See crbug.com/449198 #if defined(DISABLE_NACL) #define MAYBE_Initialize DISABLED_Initialize -#define MAYBE_EncodeToken DISABLED_EncodeToken -#define MAYBE_DecodeSamples DISABLED_DecodeSamples -#define MAYBE_DetectBroadcast DISABLED_DetectBroadcast -#define MAYBE_Audible DISABLED_Audible +#define MAYBE_EncodeAndDecode DISABLED_EncodeAndDecode #define MAYBE_TokenLengths DISABLED_TokenLengths +#define MAYBE_MultipleClients DISABLED_MultipleClients #else #define MAYBE_Initialize Initialize -#define MAYBE_EncodeToken EncodeToken -#define MAYBE_DecodeSamples DecodeSamples -#define MAYBE_DetectBroadcast DetectBroadcast -#define MAYBE_Audible Audible +#define MAYBE_EncodeAndDecode EncodeAndDecode #define MAYBE_TokenLengths TokenLengths +#define MAYBE_MultipleClients MultipleClients #endif IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_Initialize) { InitializeWhispernet(); } -IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_EncodeToken) { - InitializeWhispernet(); - EncodeTokenAndSaveSamples(false, kSixZeros); -} +IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_EncodeAndDecode) { + scoped_ptr<WhispernetClient> client( + new ChromeWhispernetClient(browser()->profile())); + client->Initialize(base::Bind(&IgnoreResult)); -IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_DecodeSamples) { - InitializeWhispernet(); - EncodeTokenAndSaveSamples(false, kSixZeros); - DecodeSamplesAndVerifyToken(false, kSixZeros, kTokenLengths); -} + TokenParameters token_params[2]; + GetTokenParamsForLengths(kTokenLengths, token_params); -IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_DetectBroadcast) { - InitializeWhispernet(); - EncodeTokenAndSaveSamples(false, kSixZeros); - DecodeSamplesAndVerifyToken(false, kSixZeros, kTokenLengths); - DetectBroadcast(); -} + EncodeTokenAndSaveSamples(client.get(), true, kSixZeros); + DecodeSamplesAndVerifyToken(client.get(), true, kSixZeros, token_params); -IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_Audible) { - InitializeWhispernet(); - EncodeTokenAndSaveSamples(true, kSixZeros); - DecodeSamplesAndVerifyToken(true, kSixZeros, kTokenLengths); + EncodeTokenAndSaveSamples(client.get(), false, kSixZeros); + DecodeSamplesAndVerifyToken(client.get(), false, kSixZeros, token_params); } IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_TokenLengths) { - InitializeWhispernet(); - size_t kLongTokenLengths[2] = {8, 9}; + scoped_ptr<WhispernetClient> client( + new ChromeWhispernetClient(browser()->profile())); + client->Initialize(base::Bind(&IgnoreResult)); + + const size_t kLongTokenLengths[2] = {8, 9}; + TokenParameters token_params[2]; + GetTokenParamsForLengths(kLongTokenLengths, token_params); + EncodeTokenAndSaveSamples(client.get(), true, kEightZeros); + DecodeSamplesAndVerifyToken(client.get(), true, kEightZeros, token_params); + + EncodeTokenAndSaveSamples(client.get(), false, kNineZeros); + DecodeSamplesAndVerifyToken(client.get(), false, kNineZeros, token_params); +} - EncodeTokenAndSaveSamples(true, kEightZeros); - DecodeSamplesAndVerifyToken(true, kEightZeros, kLongTokenLengths); +IN_PROC_BROWSER_TEST_F(ChromeWhispernetClientTest, MAYBE_MultipleClients) { + scoped_ptr<WhispernetClient> client_1( + new ChromeWhispernetClient(browser()->profile())); + scoped_ptr<WhispernetClient> client_2( + new ChromeWhispernetClient(browser()->profile())); - EncodeTokenAndSaveSamples(false, kNineZeros); - DecodeSamplesAndVerifyToken(false, kNineZeros, kLongTokenLengths); + TokenParameters token_params[2]; + GetTokenParamsForLengths(kTokenLengths, token_params); + + client_1->Initialize(base::Bind(&IgnoreResult)); + + EncodeTokenAndSaveSamples(client_1.get(), true, kSixZeros); + DecodeSamplesAndVerifyToken(client_1.get(), true, kSixZeros, token_params); + + client_2->Initialize(base::Bind(&IgnoreResult)); + + EncodeTokenAndSaveSamples(client_2.get(), true, kSixZeros); + DecodeSamplesAndVerifyToken(client_2.get(), true, kSixZeros, token_params); } + +// TODO(ckehoe): Test crc and parity +// TODO(ckehoe): More multi-client testing 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); diff --git a/chrome/browser/resources/whispernet_proxy/js/init.js b/chrome/browser/resources/whispernet_proxy/js/init.js index 87abe26..fada4f6 100644 --- a/chrome/browser/resources/whispernet_proxy/js/init.js +++ b/chrome/browser/resources/whispernet_proxy/js/init.js @@ -4,42 +4,40 @@ 'use strict'; -// Globals holding our encoder and decoder. We will never have more than one -// Global variable that will be used to access this Nacl bridge. +// Global holding our NaclBridge. var whispernetNacl = null; -// copy of an encoder or a decoder at a time. -var whisperEncoder = null; -var whisperDecoder = null; +// Encoders and decoders for each client. +var whisperEncoders = {}; +var whisperDecoders = {}; /** * Initialize the whispernet encoder and decoder. - * @param {Object} audioParams Object containing the parameters needed for - * setting up audio config. + * Call this before any other functions. + * @param {string} clientId A string identifying the requester. + * @param {Object} audioParams Audio parameters for token encoding and decoding. */ -function audioConfig(audioParams) { +function audioConfig(clientId, audioParams) { if (!whispernetNacl) { chrome.copresencePrivate.sendInitialized(false); return; } - console.log('Configuring encoder!'); - whisperEncoder = new WhisperEncoder(audioParams.paramData, whispernetNacl); - whisperEncoder.setAudioDataCallback(chrome.copresencePrivate.sendSamples); - - console.log('Configuring decoder!'); - whisperDecoder = new WhisperDecoder(audioParams.paramData, whispernetNacl); - whisperDecoder.setReceiveCallback(chrome.copresencePrivate.sendFound); - whisperDecoder.onDetectBroadcast(chrome.copresencePrivate.sendDetect); + console.log('Configuring encoder and decoder!'); + whisperEncoders[clientId] = + new WhisperEncoder(audioParams.paramData, whispernetNacl, clientId); + whisperDecoders[clientId] = + new WhisperDecoder(audioParams.paramData, whispernetNacl, clientId); } /** * Sends a request to whispernet to encode a token. + * @param {string} clientId A string identifying the requester. * @param {Object} params Encode token parameters object. */ -function encodeTokenRequest(params) { - if (whisperEncoder) { - whisperEncoder.encode(params); +function encodeTokenRequest(clientId, params) { + if (whisperEncoders[clientId]) { + whisperEncoders[clientId].encode(params); } else { console.error('encodeTokenRequest: Whisper not initialized!'); } @@ -47,29 +45,19 @@ function encodeTokenRequest(params) { /** * Sends a request to whispernet to decode samples. + * @param {string} clientId A string identifying the requester. * @param {Object} params Process samples parameters object. */ -function decodeSamplesRequest(params) { - if (whisperDecoder) { - whisperDecoder.processSamples(params); +function decodeSamplesRequest(clientId, params) { + if (whisperDecoders[clientId]) { + whisperDecoders[clientId].processSamples(params); } else { console.error('decodeSamplesRequest: Whisper not initialized!'); } } /** - * Sends a request to whispernet to detect broadcast. - */ -function detectBroadcastRequest() { - if (whisperDecoder) { - whisperDecoder.detectBroadcast(); - } else { - console.error('detectBroadcastRequest: Whisper not initialized!'); - } -} - -/** - * Initialize our listerners and signal that the extension is loaded. + * Initialize our listeners and signal that the extension is loaded. */ function onWhispernetLoaded() { console.log('init: Nacl ready!'); @@ -79,8 +67,6 @@ function onWhispernetLoaded() { chrome.copresencePrivate.onEncodeTokenRequest.addListener(encodeTokenRequest); chrome.copresencePrivate.onDecodeSamplesRequest.addListener( decodeSamplesRequest); - chrome.copresencePrivate.onDetectBroadcastRequest.addListener( - detectBroadcastRequest); // This first initialized is sent to indicate that the library is loaded. // Every other time, it will be sent only when Chrome wants to reinitialize diff --git a/chrome/browser/resources/whispernet_proxy/js/nacl.js b/chrome/browser/resources/whispernet_proxy/js/nacl.js index 6505728..06f566c 100644 --- a/chrome/browser/resources/whispernet_proxy/js/nacl.js +++ b/chrome/browser/resources/whispernet_proxy/js/nacl.js @@ -81,14 +81,13 @@ NaclBridge.prototype.loadNacl_ = function(manifestUrl) { }; /** - * Callback that is called when the Whispernet wrapper is loaded and forward - * that status to the callback registered with us in the constructor. + * Called when the Whispernet wrapper is loaded. * @private */ NaclBridge.prototype.onNaclReady_ = function() { + this.isEnabled_ = true; if (this.readyCallback_) this.readyCallback_(); - this.isEnabled_ = true; }; /** diff --git a/chrome/browser/resources/whispernet_proxy/js/wrapper.js b/chrome/browser/resources/whispernet_proxy/js/wrapper.js index 6bde1b3..d5f5701 100644 --- a/chrome/browser/resources/whispernet_proxy/js/wrapper.js +++ b/chrome/browser/resources/whispernet_proxy/js/wrapper.js @@ -32,14 +32,15 @@ function stringToArray(str) { /** * Creates a whispernet encoder. * @constructor - * @param {Object} params Dictionary of parameters used to initialize the - * whispernet encoder. - * @param {Object} whisperNacl The NaclBridge object to use to communicate with - * the whispernet wrapper. + * @param {Object} params Audio parameters for the whispernet encoder. + * @param {Object} whisperNacl The NaclBridge object, used to communicate with + * the whispernet wrapper. + * @param {string} clientId A string identifying the requester. */ -function WhisperEncoder(params, whisperNacl) { +function WhisperEncoder(params, whisperNacl, clientId) { this.whisperNacl_ = whisperNacl; this.whisperNacl_.addListener(this.onNaclMessage_.bind(this)); + this.clientId_ = clientId; var msg = { type: 'initialize_encoder', @@ -61,29 +62,22 @@ WhisperEncoder.prototype.encode = function(params) { var msg = { type: 'encode_token', + client_id: this.clientId_, // Trying to send the token in binary form to Nacl doesn't work correctly. // We end up with the correct string + a bunch of extra characters. This is // true of returning a binary string too; hence we communicate back and // forth by converting the bytes into an array of integers. token: stringToArray(atob(token)), repetitions: params.repetitions, - use_dtmf: params.token.audible + use_dtmf: params.token.audible, + use_crc: params.tokenParams.crc, + use_parity: params.tokenParams.parity }; this.whisperNacl_.send(msg); }; /** - * Method to set the callback for encoded audio data received from the encoder - * when we finish encoding a token. - * @param {function(string, ArrayBuffer)} callback Callback which will receive - * the audio samples. - */ -WhisperEncoder.prototype.setAudioDataCallback = function(callback) { - this.audioDataCallback_ = callback; -}; - -/** * Method to handle messages from the whispernet NaCl wrapper. * @param {Event} e Event from the whispernet wrapper. * @private @@ -91,7 +85,7 @@ WhisperEncoder.prototype.setAudioDataCallback = function(callback) { WhisperEncoder.prototype.onNaclMessage_ = function(e) { var msg = e.data; if (msg.type == 'encode_token_response') { - this.audioDataCallback_( + chrome.copresencePrivate.sendSamples(this.clientId_, { token: bytesToBase64(msg.token), audible: msg.audible }, msg.samples); } }; @@ -99,14 +93,15 @@ WhisperEncoder.prototype.onNaclMessage_ = function(e) { /** * Creates a whispernet decoder. * @constructor - * @param {Object} params Dictionary of parameters used to initialize the - * whispernet decoder. - * @param {Object} whisperNacl The NaclBridge object to use to communicate with - * the whispernet wrapper. + * @param {Object} params Audio parameters for the whispernet decoder. + * @param {Object} whisperNacl The NaclBridge object, used to communicate with + * the whispernet wrapper. + * @param {string} clientId A string identifying the requester. */ -function WhisperDecoder(params, whisperNacl) { +function WhisperDecoder(params, whisperNacl, clientId) { this.whisperNacl_ = whisperNacl; this.whisperNacl_.addListener(this.onNaclMessage_.bind(this)); + this.clientId_ = clientId; var msg = { type: 'initialize_decoder', @@ -126,52 +121,30 @@ WhisperDecoder.prototype.wipeDecoder = function() { }; /** - * Method to request the decoder to detect a broadcast. - */ -WhisperDecoder.prototype.detectBroadcast = function() { - var msg = { - type: 'detect_broadcast' - }; - this.whisperNacl_.send(msg); -}; - -/** * Method to request the decoder to process samples. * @param {Object} params Process samples parameters object. */ WhisperDecoder.prototype.processSamples = function(params) { var msg = { type: 'decode_tokens', + client_id: this.clientId_, + data: params.samples, + decode_audible: params.decodeAudible, + token_length_dtmf: params.audibleTokenParams.length, + crc_dtmf: params.audibleTokenParams.crc, + parity_dtmf: params.audibleTokenParams.parity, + decode_inaudible: params.decodeInaudible, - data: params.samples, - token_length_dtmf: params.audibleTokenLength, - token_length_dsss: params.inaudibleTokenLength + token_length_dsss: params.inaudibleTokenParams.length, + crc_dsss: params.inaudibleTokenParams.crc, + parity_dsss: params.inaudibleTokenParams.parity, }; this.whisperNacl_.send(msg); }; /** - * Method to set the callback for decoded tokens received from the decoder. - * @param {function(!Array.string)} callback Callback to receive the list of - * decoded tokens. - */ -WhisperDecoder.prototype.setReceiveCallback = function(callback) { - this.tokenCallback_ = callback; -}; - -/** - * Method to set the callback for receiving the detect callback status received - * from the decoder. - * @param {function()} callback Callback to set to receive the detect broadcast - * status. - */ -WhisperDecoder.prototype.onDetectBroadcast = function(callback) { - this.detectBroadcastCallback_ = callback; -}; - -/** * Method to handle messages from the whispernet NaCl wrapper. * @param {Event} e Event from the whispernet wrapper. * @private @@ -180,8 +153,6 @@ WhisperDecoder.prototype.onNaclMessage_ = function(e) { var msg = e.data; if (msg.type == 'decode_tokens_response') { this.handleCandidates_(msg.tokens, msg.audible); - } else if (msg.type == 'detect_broadcast_response') { - this.detectBroadcastCallback_(msg.detected); } }; @@ -194,7 +165,7 @@ WhisperDecoder.prototype.onNaclMessage_ = function(e) { * @private */ WhisperDecoder.prototype.handleCandidates_ = function(candidates, audible) { - if (!this.tokenCallback_ || !candidates || candidates.length == 0) + if (!candidates || candidates.length == 0) return; var returnCandidates = []; @@ -202,6 +173,5 @@ WhisperDecoder.prototype.handleCandidates_ = function(candidates, audible) { returnCandidates[i] = { token: bytesToBase64(candidates[i]), audible: audible }; } - this.tokenCallback_(returnCandidates); + chrome.copresencePrivate.sendFound(this.clientId_, returnCandidates); }; - diff --git a/chrome/chrome_browser_extensions.gypi b/chrome/chrome_browser_extensions.gypi index 1d9fda9..caf0fc3 100644 --- a/chrome/chrome_browser_extensions.gypi +++ b/chrome/chrome_browser_extensions.gypi @@ -110,6 +110,8 @@ 'browser/extensions/activity_log/uma_policy.h', 'browser/extensions/api/activity_log_private/activity_log_private_api.cc', 'browser/extensions/api/activity_log_private/activity_log_private_api.h', + 'browser/extensions/api/audio_modem/audio_modem_api.cc', + 'browser/extensions/api/audio_modem/audio_modem_api.h', 'browser/extensions/api/automation_internal/automation_action_adapter.h', 'browser/extensions/api/automation_internal/automation_internal_api.cc', 'browser/extensions/api/automation_internal/automation_internal_api.h', diff --git a/chrome/chrome_tests_unit.gypi b/chrome/chrome_tests_unit.gypi index f561266..b76008a 100644 --- a/chrome/chrome_tests_unit.gypi +++ b/chrome/chrome_tests_unit.gypi @@ -657,6 +657,7 @@ 'browser/extensions/activity_log/hashed_ad_network_database_unittest.cc', 'browser/extensions/activity_log/uma_policy_unittest.cc', 'browser/extensions/api/activity_log_private/activity_log_private_api_unittest.cc', + 'browser/extensions/api/audio_modem/audio_modem_api_unittest.cc', 'browser/extensions/api/bookmarks/bookmark_api_helpers_unittest.cc', 'browser/extensions/api/content_settings/content_settings_store_unittest.cc', 'browser/extensions/api/content_settings/content_settings_unittest.cc', @@ -2165,6 +2166,7 @@ 'sources': [ '<@(chrome_unit_tests_extensions_sources)' ], 'dependencies': [ 'common/extensions/api/api.gyp:chrome_api', + '../components/components.gyp:audio_modem_test_support', '../extensions/extensions_resources.gyp:extensions_resources', '../extensions/extensions_strings.gyp:extensions_strings', ], diff --git a/chrome/common/extensions/api/_api_features.json b/chrome/common/extensions/api/_api_features.json index 17947a2..cba0f94 100644 --- a/chrome/common/extensions/api/_api_features.json +++ b/chrome/common/extensions/api/_api_features.json @@ -75,6 +75,10 @@ "contexts": ["blessed_extension", "unblessed_extension", "content_script"], "matches": [] }, + "audioModem": { + "dependencies": ["permission:audioModem"], + "contexts": ["blessed_extension"] + }, "automationInternal": { "internal": true, "dependencies": ["manifest:automation"], diff --git a/chrome/common/extensions/api/_permission_features.json b/chrome/common/extensions/api/_permission_features.json index 6b11547..e684aeb 100644 --- a/chrome/common/extensions/api/_permission_features.json +++ b/chrome/common/extensions/api/_permission_features.json @@ -49,6 +49,23 @@ "5107DE9024C329EEA9C9A72D94C16723790C6422" // Apps Developer Tool Dev. ] }, + "audioModem": [ + { + "channel": "dev", + "extension_types": ["extension", "platform_app"] + }, + { + "channel": "stable", + "extension_types": ["extension", "platform_app"], + "whitelist": [ + "05EBA3051DFCA6AF17070AEE5FE8C66322FF4738", // http://crbug.com/431978 + "11B478CEC461C766A2DC1E5BEEB7970AE06DC9C2", // http://crbug.com/458218 + "0EFB879311E9EFBB7C45251F89EC655711B1F6ED", // http://crbug.com/458218 + "9193D3A51E2FE33B496CDA53EA330423166E7F02", // http://crbug.com/458218 + "F9119B8B18C7C82B51E7BC6FF816B694F2EC3E89" // http://crbug.com/458218 + ] + } + ], "autotestPrivate": { "channel": "stable", "extension_types": ["extension", "legacy_packaged_app"], diff --git a/chrome/common/extensions/api/audio_modem.idl b/chrome/common/extensions/api/audio_modem.idl new file mode 100644 index 0000000..a306f7c --- /dev/null +++ b/chrome/common/extensions/api/audio_modem.idl @@ -0,0 +1,86 @@ +// 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. + +// Use the <code>chrome.audio_modem</code> API +// to transmit and receive short tokens over audio. +namespace audioModem { + // The audio bands supported. + enum Audioband { + // Audible (up to 3 kHz) + audible, + // Inaudible (18-20 kHz) + inaudible + }; + + // Details for how a token is encoded in audio. + dictionary TokenEncoding { + // The length of the tokens to transmit, in bytes. + // For now, apps must always use the same token length. + long tokenLength; + // Whether to use a 2-byte CRC checksum. Defaults to false. + boolean? crc; + // Whether to use a parity symbol. Defaults to false. + boolean? parity; + }; + + // Details of a transmit or receive request. + dictionary RequestParams { + // How long to transmit or receive for. + // The timeout has a maximum of 10 minutes for transmit, + // or 1 hour for receive. + long timeoutMillis; + // The audio band to use. + Audioband band; + // The token encoding details. + TokenEncoding encoding; + }; + + // Results of token decoding. + dictionary ReceivedToken { + // The token contents in raw bytes. + ArrayBuffer token; + // The audio band the token was heard on. + Audioband band; + }; + + // The result of a requested operation. + enum Status { + // The requested operation was processed successfully. + success, + // The request was invalid. See chrome.runtime.lastError for details. + invalidRequest, + // The requested audio band is already in use by another client. + // Eventually, simultaneous tokens will be time-sliced, + // and this error will no longer occur. + inUse, + // Audio encoding or decoding failed. + coderError + }; + + // A callback to report the status of a request. + callback StatusCallback = void(Status status); + + interface Functions { + // Transmit a token. Only one can be transmitted at a time. + // Transmission of any previous tokens (by this app) will stop. + static void transmit( + RequestParams params, ArrayBuffer token, StatusCallback callback); + // Stop any active transmission on the specified band. + static void stopTransmit(Audioband band, StatusCallback callback); + // Start listening for audio tokens. For now, + // only one app will be able to listen at a time. + static void receive(RequestParams params, StatusCallback callback); + // Stop any active listening on the specified band. + static void stopReceive(Audioband band, StatusCallback callback); + }; + + interface Events { + // Audio tokens have been received. + static void onReceived(ReceivedToken[] tokens); + // Transmit could not be confirmed. + // The speaker volume might be too low. + static void onTransmitFail(Audioband band); + }; +}; + diff --git a/chrome/common/extensions/api/copresence_private.idl b/chrome/common/extensions/api/copresence_private.idl index 691cdab..6db3420 100644 --- a/chrome/common/extensions/api/copresence_private.idl +++ b/chrome/common/extensions/api/copresence_private.idl @@ -10,19 +10,26 @@ namespace copresencePrivate { boolean audible; }; + dictionary TokenParameters { + long length; + boolean crc; + boolean parity; + }; + dictionary DecodeSamplesParameters { ArrayBuffer samples; boolean decodeAudible; boolean decodeInaudible; - long audibleTokenLength; - long inaudibleTokenLength; + TokenParameters audibleTokenParams; + TokenParameters inaudibleTokenParams; }; dictionary EncodeTokenParameters { Token token; long repetitions; + TokenParameters tokenParams; }; dictionary AudioParameters { @@ -35,22 +42,26 @@ namespace copresencePrivate { interface Functions { // Send a boolean indicating whether our initialization was successful. static void sendInitialized(boolean success); + // Sends an array of found tokens to Chrome. - static void sendFound(Token[] tokens); + static void sendFound(DOMString clientId, Token[] tokens); + // Send an array buffer of samples encoded for the specified token. - static void sendSamples(Token token, ArrayBuffer samples); - // Send a boolean indicating whether we detected a broadcast or not. - static void sendDetect(boolean detected); + static void sendSamples(DOMString clientId, + Token token, + ArrayBuffer samples); }; interface Events { // Fired to request audio configuration of the whisper.net library. - static void onConfigAudio(AudioParameters audioParams); + static void onConfigAudio(DOMString clientId, AudioParameters audioParams); + // Fired to request encoding of the given token. - static void onEncodeTokenRequest(EncodeTokenParameters encodeParams); + static void onEncodeTokenRequest(DOMString clientId, + EncodeTokenParameters encodeParams); + // Fired when we have new samples to decode. - static void onDecodeSamplesRequest(DecodeSamplesParameters decodeParams); - // Fired to request a DetectBroadcast. - static void onDetectBroadcastRequest(); + static void onDecodeSamplesRequest(DOMString clientId, + DecodeSamplesParameters decodeParams); }; }; diff --git a/chrome/common/extensions/api/schemas.gypi b/chrome/common/extensions/api/schemas.gypi index f8ac601..f29dbc6 100644 --- a/chrome/common/extensions/api/schemas.gypi +++ b/chrome/common/extensions/api/schemas.gypi @@ -11,6 +11,7 @@ 'accessibility_features.json', 'accessibility_private.json', 'activity_log_private.json', + 'audio_modem.idl', 'automation.idl', 'automation_internal.idl', 'autotest_private.idl', diff --git a/chrome/common/extensions/permissions/chrome_api_permissions.cc b/chrome/common/extensions/permissions/chrome_api_permissions.cc index bec99e9..0e50e6e 100644 --- a/chrome/common/extensions/permissions/chrome_api_permissions.cc +++ b/chrome/common/extensions/permissions/chrome_api_permissions.cc @@ -86,6 +86,7 @@ std::vector<APIPermissionInfo*> ChromeAPIPermissions::GetAllPermissions() "accessibilityPrivate", APIPermissionInfo::kFlagCannotBeOptional}, {APIPermission::kActiveTab, "activeTab"}, + {APIPermission::kAudioModem, "audioModem"}, {APIPermission::kBookmark, "bookmarks", APIPermissionInfo::kFlagNone, diff --git a/chrome/common/extensions/permissions/permission_set_unittest.cc b/chrome/common/extensions/permissions/permission_set_unittest.cc index dae5a7d..d4bfb1d 100644 --- a/chrome/common/extensions/permissions/permission_set_unittest.cc +++ b/chrome/common/extensions/permissions/permission_set_unittest.cc @@ -641,6 +641,7 @@ TEST(PermissionsTest, PermissionMessages) { skip.insert(APIPermission::kAlwaysOnTopWindows); skip.insert(APIPermission::kAppView); skip.insert(APIPermission::kAudio); + skip.insert(APIPermission::kAudioModem); skip.insert(APIPermission::kBrowsingData); skip.insert(APIPermission::kCastStreaming); skip.insert(APIPermission::kCommandsAccessibility); diff --git a/chrome/test/BUILD.gn b/chrome/test/BUILD.gn index 2c5cb50..49909a8 100644 --- a/chrome/test/BUILD.gn +++ b/chrome/test/BUILD.gn @@ -1220,6 +1220,7 @@ if (!is_android && (!is_win || link_chrome_on_windows)) { if (!is_ios) { deps += [ + "//components/audio_modem:audio_modem_test_support", "//components/autofill/content/browser:test_support", "//components/metrics/proto", "//components/data_reduction_proxy/core/browser:test_support", diff --git a/components/audio_modem.gypi b/components/audio_modem.gypi index 79d2dbe..7d2219b 100644 --- a/components/audio_modem.gypi +++ b/components/audio_modem.gypi @@ -43,6 +43,8 @@ 'sources': [ 'audio_modem/test/random_samples.cc', 'audio_modem/test/random_samples.h', + 'audio_modem/test/stub_modem.cc', + 'audio_modem/test/stub_modem.h', 'audio_modem/test/stub_whispernet_client.cc', 'audio_modem/test/stub_whispernet_client.h', ], diff --git a/components/audio_modem/BUILD.gn b/components/audio_modem/BUILD.gn index 6676e64..93c9fd6 100644 --- a/components/audio_modem/BUILD.gn +++ b/components/audio_modem/BUILD.gn @@ -27,3 +27,19 @@ static_library("audio_modem") { "//third_party/webrtc/common_audio", ] } + +static_library("audio_modem_test_support") { + sources = [ + "test/random_samples.cc", + "test/random_samples.h", + "test/stub_modem.cc", + "test/stub_modem.h", + "test/stub_whispernet_client.cc", + "test/stub_whispernet_client.h", + ] + + deps = [ + "//base", + "//media", + ] +} diff --git a/components/audio_modem/modem_impl.cc b/components/audio_modem/modem_impl.cc index 293b1f9..23d75d0 100644 --- a/components/audio_modem/modem_impl.cc +++ b/components/audio_modem/modem_impl.cc @@ -92,8 +92,6 @@ ModemImpl::ModemImpl() : client_(nullptr), recorder_(nullptr) { switches::kAudioModemEnableInaudibleBroadcast, true); player_[AUDIBLE] = nullptr; player_[INAUDIBLE] = nullptr; - token_length_[0] = 0; - token_length_[1] = 0; samples_caches_.resize(2); samples_caches_[AUDIBLE] = new SamplesMap(kMaxSamples); @@ -191,21 +189,21 @@ void ModemImpl::StopRecording(AudioType type) { } void ModemImpl::SetToken(AudioType type, - const std::string& url_safe_token) { + const std::string& url_safe_token) { DCHECK(type == AUDIBLE || type == INAUDIBLE); std::string token = FromUrlSafe(url_safe_token); if (samples_caches_[type]->Get(token) == samples_caches_[type]->end()) { - client_->EncodeToken(token, type); + client_->EncodeToken(token, type, token_params_); } else { UpdateToken(type, token); } } -const std::string ModemImpl::GetToken(AudioType type) { +const std::string ModemImpl::GetToken(AudioType type) const { return playing_token_[type]; } -bool ModemImpl::IsPlayingTokenHeard(AudioType type) { +bool ModemImpl::IsPlayingTokenHeard(AudioType type) const { base::TimeDelta tokenTimeout = base::TimeDelta::FromMilliseconds(kTokenTimeoutMs); @@ -217,8 +215,12 @@ bool ModemImpl::IsPlayingTokenHeard(AudioType type) { return base::Time::Now() - heard_own_token_[type] < tokenTimeout; } -void ModemImpl::SetTokenLength(AudioType type, size_t token_length) { - token_length_[type] = token_length; +void ModemImpl::SetTokenParams(AudioType type, const TokenParameters& params) { + DCHECK_GT(params.length, 0u); + token_params_[type] = params; + + // TODO(ckehoe): Make whispernet handle different token lengths + // simultaneously without reinitializing the decoder over and over. } // static @@ -294,11 +296,11 @@ void ModemImpl::DecodeSamplesConnector(const std::string& samples) { should_be_recording_[INAUDIBLE] || should_be_playing_[INAUDIBLE]; if (decode_audible && decode_inaudible) { - client_->DecodeSamples(BOTH, samples, token_length_); + client_->DecodeSamples(BOTH, samples, token_params_); } else if (decode_audible) { - client_->DecodeSamples(AUDIBLE, samples, token_length_); + client_->DecodeSamples(AUDIBLE, samples, token_params_); } else if (decode_inaudible) { - client_->DecodeSamples(INAUDIBLE, samples, token_length_); + client_->DecodeSamples(INAUDIBLE, samples, token_params_); } } diff --git a/components/audio_modem/modem_impl.h b/components/audio_modem/modem_impl.h index 108d4bb..ec6beb5 100644 --- a/components/audio_modem/modem_impl.h +++ b/components/audio_modem/modem_impl.h @@ -45,9 +45,9 @@ class ModemImpl final : public Modem { void StartRecording(AudioType type) override; void StopRecording(AudioType type) override; void SetToken(AudioType type, const std::string& url_safe_token) override; - const std::string GetToken(AudioType type) override; - bool IsPlayingTokenHeard(AudioType type) override; - void SetTokenLength(AudioType type, size_t token_length) override; + const std::string GetToken(AudioType type) const override; + bool IsPlayingTokenHeard(AudioType type) const override; + void SetTokenParams(AudioType type, const TokenParameters& params) override; void set_player_for_testing(AudioType type, AudioPlayer* player) { player_[type] = player; @@ -108,7 +108,7 @@ class ModemImpl final : public Modem { // Indexed using enum AudioType. std::string playing_token_[2]; - size_t token_length_[2]; + TokenParameters token_params_[2]; base::Time started_playing_[2]; base::Time heard_own_token_[2]; diff --git a/components/audio_modem/public/audio_modem_types.h b/components/audio_modem/public/audio_modem_types.h index 32c6757..036a6a1 100644 --- a/components/audio_modem/public/audio_modem_types.h +++ b/components/audio_modem/public/audio_modem_types.h @@ -50,6 +50,22 @@ struct AudioToken final { bool audible; }; +// Struct to hold the encoding parameters for tokens. +// Parity is on by default. +struct TokenParameters { + TokenParameters() : TokenParameters(0, false, true) {} + + explicit TokenParameters(size_t length) + : TokenParameters(length, false, true) {} + + TokenParameters(size_t length, bool crc, bool parity) + : length(length), crc(crc), parity(parity) {} + + size_t length; + bool crc; + bool parity; +}; + // Callback to pass around found tokens. using TokensCallback = base::Callback<void(const std::vector<AudioToken>&)>; diff --git a/components/audio_modem/public/modem.h b/components/audio_modem/public/modem.h index 304614c..09ab399 100644 --- a/components/audio_modem/public/modem.h +++ b/components/audio_modem/public/modem.h @@ -31,11 +31,12 @@ class Modem { virtual void SetToken(AudioType type, const std::string& url_safe_token) = 0; - virtual const std::string GetToken(AudioType type) = 0; + virtual const std::string GetToken(AudioType type) const = 0; - virtual bool IsPlayingTokenHeard(AudioType type) = 0; + virtual bool IsPlayingTokenHeard(AudioType type) const = 0; - virtual void SetTokenLength(AudioType type, size_t token_length) = 0; + virtual void SetTokenParams(AudioType type, + const TokenParameters& params) = 0; static scoped_ptr<Modem> Create(); }; diff --git a/components/audio_modem/public/whispernet_client.h b/components/audio_modem/public/whispernet_client.h index c7219c8..48d1efa 100644 --- a/components/audio_modem/public/whispernet_client.h +++ b/components/audio_modem/public/whispernet_client.h @@ -34,27 +34,24 @@ class WhispernetClient { virtual void Initialize(const SuccessCallback& init_callback) = 0; // Fires an event to request a token encode. - virtual void EncodeToken(const std::string& token, AudioType type) = 0; + virtual void EncodeToken(const std::string& token, + AudioType type, + const TokenParameters token_params[2]) = 0; // Fires an event to request a decode for the given samples. virtual void DecodeSamples(AudioType type, const std::string& samples, - const size_t token_length[2]) = 0; - // Fires an event to request detection of a whispernet broadcast. - virtual void DetectBroadcast() = 0; + const TokenParameters token_params[2]) = 0; // Callback registration methods. The modem will set these to receive data. virtual void RegisterTokensCallback( const TokensCallback& tokens_callback) = 0; virtual void RegisterSamplesCallback( const SamplesCallback& samples_callback) = 0; - virtual void RegisterDetectBroadcastCallback( - const SuccessCallback& db_callback) = 0; // Don't cache these callbacks, as they may become invalid at any time. // Always invoke callbacks directly through these accessors. virtual TokensCallback GetTokensCallback() = 0; virtual SamplesCallback GetSamplesCallback() = 0; - virtual SuccessCallback GetDetectBroadcastCallback() = 0; virtual SuccessCallback GetInitializedCallback() = 0; virtual ~WhispernetClient() {} diff --git a/components/audio_modem/test/stub_modem.cc b/components/audio_modem/test/stub_modem.cc new file mode 100644 index 0000000..ea0fb08 --- /dev/null +++ b/components/audio_modem/test/stub_modem.cc @@ -0,0 +1,69 @@ +// 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 "components/audio_modem/test/stub_modem.h" + +#include "base/base64.h" +#include "base/logging.h" + +namespace audio_modem { + +StubModem::StubModem() { + playing_[AUDIBLE] = false; + playing_[INAUDIBLE] = false; + recording_[AUDIBLE] = false; + recording_[INAUDIBLE] = false; +} + +StubModem::~StubModem() {} + +void StubModem::Initialize(WhispernetClient* whispernet_client, + const TokensCallback& tokens_cb) { + tokens_callback_ = tokens_cb; +} + +void StubModem::StartPlaying(AudioType type) { + playing_[type] = true; +} + +void StubModem::StopPlaying(AudioType type) { + playing_[type] = false; +} + +void StubModem::StartRecording(AudioType type) { + recording_[type] = true; +} + +void StubModem::StopRecording(AudioType type) { + recording_[type] = false; +} + +const std::string StubModem::GetToken(AudioType type) const { + return std::string(); +} + +bool StubModem::IsPlayingTokenHeard(AudioType type) const { + return false; +} + +bool StubModem::IsRecording(AudioType type) const { + return recording_[type]; +} + +bool StubModem::IsPlaying(AudioType type) const { + return playing_[type]; +} + +void StubModem::DeliverTokens(const std::vector<AudioToken>& tokens) { + std::vector<AudioToken> encoded_tokens; + for (const AudioToken& token : tokens) { + std::string encoded_token; + base::Base64Encode(token.token, & encoded_token); + encoded_tokens.push_back(AudioToken(encoded_token, token.audible)); + } + DCHECK_EQ(tokens.size(), encoded_tokens.size()); + tokens_callback_.Run(encoded_tokens); +} + +} // namespace audio_modem diff --git a/components/audio_modem/test/stub_modem.h b/components/audio_modem/test/stub_modem.h new file mode 100644 index 0000000..e76f5e3 --- /dev/null +++ b/components/audio_modem/test/stub_modem.h @@ -0,0 +1,51 @@ +// 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 COMPONENTS_AUDIO_MODEM_TEST_STUB_MODEM_H_ +#define COMPONENTS_AUDIO_MODEM_TEST_STUB_MODEM_H_ + +#include <string> +#include <vector> + +#include "base/callback.h" +#include "components/audio_modem/public/modem.h" + +namespace audio_modem { + +class StubModem final : public Modem { + public: + StubModem(); + ~StubModem() override; + + // AudioManager overrides: + void Initialize(WhispernetClient* whispernet_client, + const TokensCallback& tokens_cb) override; + void StartPlaying(AudioType type) override; + void StopPlaying(AudioType type) override; + void StartRecording(AudioType type) override; + void StopRecording(AudioType type) override; + void SetToken(AudioType type, const std::string& url_unsafe_token) override {} + const std::string GetToken(AudioType type) const override; + bool IsPlayingTokenHeard(AudioType type) const override; + void SetTokenParams(AudioType type, const TokenParameters& params) override {} + + bool IsRecording(AudioType type) const; + bool IsPlaying(AudioType type) const; + + // Encodes tokens as base64 and then delivers them. + void DeliverTokens(const std::vector<AudioToken>& tokens); + + private: + // Indexed using enum AudioType. + bool playing_[2]; + bool recording_[2]; + + TokensCallback tokens_callback_; + + DISALLOW_COPY_AND_ASSIGN(StubModem); +}; + +} // namespace audio_modem + +#endif // COMPONENTS_AUDIO_MODEM_TEST_STUB_MODEM_H_ diff --git a/components/audio_modem/test/stub_whispernet_client.cc b/components/audio_modem/test/stub_whispernet_client.cc index 9b0a50c..e2763ce 100644 --- a/components/audio_modem/test/stub_whispernet_client.cc +++ b/components/audio_modem/test/stub_whispernet_client.cc @@ -18,14 +18,16 @@ StubWhispernetClient::~StubWhispernetClient() {} void StubWhispernetClient::Initialize(const SuccessCallback& init_callback) {} void StubWhispernetClient::EncodeToken(const std::string& token, - AudioType type) { + AudioType type, + const TokenParameters token_params[2]) { if (!samples_cb_.is_null()) samples_cb_.Run(type, token, samples_); } -void StubWhispernetClient::DecodeSamples(AudioType type, - const std::string& samples, - const size_t token_length[2]) { +void StubWhispernetClient::DecodeSamples( + AudioType type, + const std::string& samples, + const TokenParameters token_params[2]) { if (!tokens_cb_.is_null()) tokens_cb_.Run(tokens_); } @@ -48,10 +50,6 @@ SamplesCallback StubWhispernetClient::GetSamplesCallback() { return samples_cb_; } -SuccessCallback StubWhispernetClient::GetDetectBroadcastCallback() { - return SuccessCallback(); -} - SuccessCallback StubWhispernetClient::GetInitializedCallback() { return SuccessCallback(); } diff --git a/components/audio_modem/test/stub_whispernet_client.h b/components/audio_modem/test/stub_whispernet_client.h index 3225863c..9a0e8c6 100644 --- a/components/audio_modem/test/stub_whispernet_client.h +++ b/components/audio_modem/test/stub_whispernet_client.h @@ -30,20 +30,18 @@ class StubWhispernetClient final : public WhispernetClient { void Initialize(const SuccessCallback& init_callback) override; - void EncodeToken(const std::string& token, AudioType type) override; + void EncodeToken(const std::string& token, + AudioType type, + const TokenParameters token_params[2]) override; void DecodeSamples(AudioType type, const std::string& samples, - const size_t token_length[2]) override; - void DetectBroadcast() override {} + const TokenParameters token_params[2]) override; void RegisterTokensCallback(const TokensCallback& tokens_cb) override; void RegisterSamplesCallback(const SamplesCallback& samples_cb) override; - void RegisterDetectBroadcastCallback( - const SuccessCallback& /* db_cb */) override {} TokensCallback GetTokensCallback() override; SamplesCallback GetSamplesCallback() override; - SuccessCallback GetDetectBroadcastCallback() override; SuccessCallback GetInitializedCallback() override; private: diff --git a/components/copresence/handlers/audio/audio_directive_handler_impl.cc b/components/copresence/handlers/audio/audio_directive_handler_impl.cc index 16b0269..d91b4d8 100644 --- a/components/copresence/handlers/audio/audio_directive_handler_impl.cc +++ b/components/copresence/handlers/audio/audio_directive_handler_impl.cc @@ -22,6 +22,7 @@ using audio_modem::AUDIBLE; using audio_modem::INAUDIBLE; +using audio_modem::TokenParameters; namespace copresence { @@ -102,12 +103,13 @@ void AudioDirectiveHandlerImpl::AddInstruction( DCHECK_GT(token_length, 0u); switch (instruction.medium()) { case AUDIO_ULTRASOUND_PASSBAND: - audio_modem_->SetTokenLength(INAUDIBLE, token_length); + audio_modem_->SetTokenParams(INAUDIBLE, + TokenParameters(token_length)); transmits_lists_[INAUDIBLE]->AddDirective(op_id, directive); audio_modem_->SetToken(INAUDIBLE, instruction.token_id()); break; case AUDIO_AUDIBLE_DTMF: - audio_modem_->SetTokenLength(AUDIBLE, token_length); + audio_modem_->SetTokenParams(AUDIBLE, TokenParameters(token_length)); transmits_lists_[AUDIBLE]->AddDirective(op_id, directive); audio_modem_->SetToken(AUDIBLE, instruction.token_id()); break; @@ -123,11 +125,12 @@ void AudioDirectiveHandlerImpl::AddInstruction( DCHECK_GT(token_length, 0u); switch (instruction.medium()) { case AUDIO_ULTRASOUND_PASSBAND: - audio_modem_->SetTokenLength(INAUDIBLE, token_length); + audio_modem_->SetTokenParams(INAUDIBLE, + TokenParameters(token_length)); receives_lists_[INAUDIBLE]->AddDirective(op_id, directive); break; case AUDIO_AUDIBLE_DTMF: - audio_modem_->SetTokenLength(AUDIBLE, token_length); + audio_modem_->SetTokenParams(AUDIBLE, TokenParameters(token_length)); receives_lists_[AUDIBLE]->AddDirective(op_id, directive); break; default: diff --git a/components/copresence/handlers/audio/audio_directive_handler_unittest.cc b/components/copresence/handlers/audio/audio_directive_handler_unittest.cc index d414dd2..24da3ca 100644 --- a/components/copresence/handlers/audio/audio_directive_handler_unittest.cc +++ b/components/copresence/handlers/audio/audio_directive_handler_unittest.cc @@ -12,6 +12,7 @@ #include "base/timer/mock_timer.h" #include "components/audio_modem/public/modem.h" #include "components/audio_modem/test/random_samples.h" +#include "components/audio_modem/test/stub_modem.h" #include "components/copresence/handlers/audio/audio_directive_handler_impl.h" #include "components/copresence/handlers/audio/tick_clock_ref_counted.h" #include "components/copresence/proto/data.pb.h" @@ -20,6 +21,7 @@ using audio_modem::AUDIBLE; using audio_modem::AudioType; using audio_modem::INAUDIBLE; +using audio_modem::StubModem; namespace copresence { @@ -39,34 +41,6 @@ const Directive CreateDirective(TokenInstructionType type, } // namespace -class StubModem final : public audio_modem::Modem { - public: - StubModem() {} - ~StubModem() override {} - - // AudioManager overrides: - void Initialize(audio_modem::WhispernetClient* whispernet_client, - const audio_modem::TokensCallback& tokens_cb) override {} - void StartPlaying(AudioType type) override { playing_[type] = true; } - void StopPlaying(AudioType type) override { playing_[type] = false; } - void StartRecording(AudioType type) override { recording_[type] = true; } - void StopRecording(AudioType type) override { recording_[type] = false; } - void SetToken(AudioType type, const std::string& url_unsafe_token) override {} - const std::string GetToken(AudioType type) override { return std::string(); } - bool IsPlayingTokenHeard(AudioType type) override { return false; } - void SetTokenLength(AudioType type, size_t token_length) override {} - - bool IsRecording(AudioType type) { return recording_[type]; } - bool IsPlaying(AudioType type) { return playing_[type]; } - - private: - // Indexed using enum AudioType. - bool playing_[2]; - bool recording_[2]; - - DISALLOW_COPY_AND_ASSIGN(StubModem); -}; - class AudioDirectiveHandlerTest : public testing::Test { public: AudioDirectiveHandlerTest() { diff --git a/extensions/browser/extension_function_histogram_value.h b/extensions/browser/extension_function_histogram_value.h index d5ec166..416a678 100644 --- a/extensions/browser/extension_function_histogram_value.h +++ b/extensions/browser/extension_function_histogram_value.h @@ -1022,6 +1022,10 @@ enum HistogramValue { WEBVIEWINTERNAL_SETALLOWSCALING, PLATFORMKEYSINTERNAL_GETPUBLICKEY, RUNTIME_OPENOPTIONSPAGE, + AUDIOMODEM_TRANSMIT, + AUDIOMODEM_STOPTRANSMIT, + AUDIOMODEM_RECEIVE, + AUDIOMODEM_STOPRECEIVE, // Last entry: Add new entries above and ensure to update // tools/metrics/histograms/histograms.xml. ENUM_BOUNDARY diff --git a/extensions/common/permissions/api_permission.h b/extensions/common/permissions/api_permission.h index c7acd83..9c6712d 100644 --- a/extensions/common/permissions/api_permission.h +++ b/extensions/common/permissions/api_permission.h @@ -8,6 +8,7 @@ #include <map> #include <set> #include <string> +#include <vector> #include "base/callback.h" #include "base/memory/scoped_ptr.h" @@ -54,6 +55,7 @@ class APIPermission { kAppView, kAudio, kAudioCapture, + kAudioModem, kAutomation, kAutoTestPrivate, kBackground, diff --git a/tools/metrics/histograms/histograms.xml b/tools/metrics/histograms/histograms.xml index 2ef1fb2..62c0d40 100644 --- a/tools/metrics/histograms/histograms.xml +++ b/tools/metrics/histograms/histograms.xml @@ -48211,6 +48211,10 @@ Therefore, the affected-histogram name has to have at least one dot in it. <int value="961" label="WEBVIEWINTERNAL_SETALLOWSCALING"/> <int value="962" label="PLATFORMKEYSINTERNAL_GETPUBLICKEY"/> <int value="963" label="RUNTIME_OPENOPTIONSPAGE"/> + <int value="964" label="AUDIOMODEM_TRANSMIT"/> + <int value="965" label="AUDIOMODEM_STOPTRANSMIT"/> + <int value="966" label="AUDIOMODEM_RECEIVE"/> + <int value="967" label="AUDIOMODEM_STOPRECEIVE"/> </enum> <enum name="ExtensionInstallCause" type="int"> |