Version 1.2.1 alexa-client-sdk

Changes in this update
* **Enhancements**..
  * Added comments to `AlexaClientSDKConfig.json`. These descriptions provide additional guidance for what is expected for each field...
  * Enabled pause and resume controls for Pandora...

* **Bug Fixes**..
  * Bug fix for [issue #329](https://github.com/alexa/avs-device-sdk/issues/329) - `HTTP2Transport` instances no longer leak when `SERVER_SIDE
  * Bug fix for [issue #189](https://github.com/alexa/avs-device-sdk/issues/189) - Fixed a race condition in the `Timer` class that sometimes
  * Bug fix for a race condition that caused `SpeechSynthesizer` to ignore subsequent `Speak` directives...
  * Bug fix for corrupted mime attachments.
This commit is contained in:
Andrey Mikheev 2017-11-16 18:49:28 -08:00
parent 2cc16d8cc4
commit a2b84e329c
27 changed files with 748 additions and 185 deletions

View File

@ -363,6 +363,9 @@ private:
/// Whether or not the @c networkLoop is stopping. Serialized by @c m_mutex. /// Whether or not the @c networkLoop is stopping. Serialized by @c m_mutex.
bool m_isStopping; bool m_isStopping;
/// Whether or not the onDisconnected() notification has been sent. Serialized by @c m_mutex.
bool m_disconnectedSent;
/// Queue of @c MessageRequest instances to send. Serialized by @c m_mutex. /// Queue of @c MessageRequest instances to send. Serialized by @c m_mutex.
std::deque<std::shared_ptr<avsCommon::avs::MessageRequest>> m_requestQueue; std::deque<std::shared_ptr<avsCommon::avs::MessageRequest>> m_requestQueue;

View File

@ -74,11 +74,13 @@ public:
void setObserver(std::shared_ptr<MessageRouterObserverInterface> observer) override; void setObserver(std::shared_ptr<MessageRouterObserverInterface> observer) override;
void onConnected() override; void onConnected(std::shared_ptr<TransportInterface> transport) override;
void onDisconnected(avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::ChangedReason reason) override; void onDisconnected(
std::shared_ptr<TransportInterface> transport,
avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::ChangedReason reason) override;
void onServerSideDisconnect() override; void onServerSideDisconnect(std::shared_ptr<TransportInterface> transport) override;
void consumeMessage(const std::string& contextId, const std::string& message) override; void consumeMessage(const std::string& contextId, const std::string& message) override;

View File

@ -60,9 +60,11 @@ public:
void onContextAvailable(const std::string& jsonContext) override; void onContextAvailable(const std::string& jsonContext) override;
void onContextFailure(const avsCommon::sdkInterfaces::ContextRequestError error) override; void onContextFailure(const avsCommon::sdkInterfaces::ContextRequestError error) override;
void onServerSideDisconnect() override; void onServerSideDisconnect(std::shared_ptr<TransportInterface> transport) override;
void onConnected() override; void onConnected(std::shared_ptr<TransportInterface> transport) override;
void onDisconnected(avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::ChangedReason reason) override; void onDisconnected(
std::shared_ptr<TransportInterface> transport,
avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::ChangedReason reason) override;
void addObserver(std::shared_ptr<PostConnectObserverInterface> observer) override; void addObserver(std::shared_ptr<PostConnectObserverInterface> observer) override;
void removeObserver(std::shared_ptr<PostConnectObserverInterface> observer) override; void removeObserver(std::shared_ptr<PostConnectObserverInterface> observer) override;

View File

@ -36,19 +36,27 @@ public:
/** /**
* Called when a connection to AVS is established. * Called when a connection to AVS is established.
*
* @param transport The transport that has connected.
*/ */
virtual void onConnected() = 0; virtual void onConnected(std::shared_ptr<TransportInterface> transport) = 0;
/** /**
* Called when we disconnect from AVS. * Called when we disconnect from AVS.
*
* @param transport The transport that is no longer connected (or attempting to connect).
* @param reason The reason that we disconnected. * @param reason The reason that we disconnected.
*/ */
virtual void onDisconnected(avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::ChangedReason reason) = 0; virtual void onDisconnected(
std::shared_ptr<TransportInterface> transport,
avsCommon::sdkInterfaces::ConnectionStatusObserverInterface::ChangedReason reason) = 0;
/** /**
* Called when the server asks the client to reconnect * Called when the server asks the client to reconnect
*
* @param transport The transport that has received the disconnect request.
*/ */
virtual void onServerSideDisconnect() = 0; virtual void onServerSideDisconnect(std::shared_ptr<TransportInterface> transport) = 0;
}; };
} // namespace acl } // namespace acl

View File

@ -169,6 +169,7 @@ HTTP2Transport::HTTP2Transport(
m_isNetworkThreadRunning{false}, m_isNetworkThreadRunning{false},
m_isConnected{false}, m_isConnected{false},
m_isStopping{false}, m_isStopping{false},
m_disconnectedSent{false},
m_postConnectObject{postConnectObject} { m_postConnectObject{postConnectObject} {
m_observers.insert(observer); m_observers.insert(observer);
@ -700,9 +701,10 @@ void HTTP2Transport::setIsConnectedFalse() {
auto disconnectReason = ConnectionStatusObserverInterface::ChangedReason::INTERNAL_ERROR; auto disconnectReason = ConnectionStatusObserverInterface::ChangedReason::INTERNAL_ERROR;
{ {
std::lock_guard<std::mutex> lock(m_mutex); std::lock_guard<std::mutex> lock(m_mutex);
if (!m_isConnected) { if (m_disconnectedSent) {
return; return;
} }
m_disconnectedSent = true;
m_isConnected = false; m_isConnected = false;
disconnectReason = m_disconnectReason; disconnectReason = m_disconnectReason;
} }
@ -775,7 +777,7 @@ void HTTP2Transport::notifyObserversOnServerSideDisconnect() {
lock.unlock(); lock.unlock();
for (auto observer : observers) { for (auto observer : observers) {
observer->onServerSideDisconnect(); observer->onServerSideDisconnect(shared_from_this());
} }
} }
@ -785,7 +787,7 @@ void HTTP2Transport::notifyObserversOnDisconnect(ConnectionStatusObserverInterfa
lock.unlock(); lock.unlock();
for (auto observer : observers) { for (auto observer : observers) {
observer->onDisconnected(reason); observer->onDisconnected(shared_from_this(), reason);
} }
} }
@ -795,7 +797,7 @@ void HTTP2Transport::notifyObserversOnConnected() {
lock.unlock(); lock.unlock();
for (auto observer : observers) { for (auto observer : observers) {
observer->onConnected(); observer->onConnected(shared_from_this());
} }
} }

View File

@ -112,7 +112,7 @@ void MessageRouter::setAVSEndpoint(const std::string& avsEndpoint) {
} }
} }
void MessageRouter::onConnected() { void MessageRouter::onConnected(std::shared_ptr<TransportInterface> transport) {
std::unique_lock<std::mutex> lock{m_connectionMutex}; std::unique_lock<std::mutex> lock{m_connectionMutex};
if (m_isEnabled) { if (m_isEnabled) {
setConnectionStatusLocked( setConnectionStatusLocked(
@ -121,45 +121,34 @@ void MessageRouter::onConnected() {
} }
} }
void MessageRouter::onDisconnected(ConnectionStatusObserverInterface::ChangedReason reason) { void MessageRouter::onDisconnected(
std::shared_ptr<TransportInterface> transport,
ConnectionStatusObserverInterface::ChangedReason reason) {
std::lock_guard<std::mutex> lock{m_connectionMutex}; std::lock_guard<std::mutex> lock{m_connectionMutex};
if (ConnectionStatusObserverInterface::Status::CONNECTED == m_connectionStatus) {
// Reset m_activeTransport of it is not longer connected.
if (m_activeTransport && !m_activeTransport->isConnected()) {
safelyResetActiveTransportLocked();
}
// Trim m_transports to just those transports still connected. Also build list of disconnected transports. for (auto it = m_transports.begin(); it != m_transports.end(); it++) {
std::vector<std::shared_ptr<TransportInterface>> connected; if (*it == transport) {
std::vector<std::shared_ptr<TransportInterface>> disconnected; m_transports.erase(it);
for (auto transport : m_transports) {
if (transport->isConnected()) {
connected.push_back(transport);
} else {
disconnected.push_back(transport);
}
}
m_transports.clear();
std::swap(m_transports, connected);
// Release all the disconnected transports.
for (auto transport : disconnected) {
safelyReleaseTransport(transport); safelyReleaseTransport(transport);
break;
} }
}
if (transport == m_activeTransport) {
m_activeTransport.reset();
// Update status. If transitioning to PENDING, also initiate the reconnect. // Update status. If transitioning to PENDING, also initiate the reconnect.
if (m_isEnabled) { if (ConnectionStatusObserverInterface::Status::CONNECTED == m_connectionStatus) {
if (!m_activeTransport) { if (m_isEnabled) {
setConnectionStatusLocked(ConnectionStatusObserverInterface::Status::PENDING, reason); setConnectionStatusLocked(ConnectionStatusObserverInterface::Status::PENDING, reason);
createActiveTransportLocked(); createActiveTransportLocked();
} else if (m_transports.empty()) {
setConnectionStatusLocked(ConnectionStatusObserverInterface::Status::DISCONNECTED, reason);
} }
} else if (m_transports.empty()) {
setConnectionStatusLocked(ConnectionStatusObserverInterface::Status::DISCONNECTED, reason);
} }
} }
} }
void MessageRouter::onServerSideDisconnect() { void MessageRouter::onServerSideDisconnect(std::shared_ptr<TransportInterface> transport) {
std::unique_lock<std::mutex> lock{m_connectionMutex}; std::unique_lock<std::mutex> lock{m_connectionMutex};
if (m_isEnabled) { if (m_isEnabled) {
setConnectionStatusLocked( setConnectionStatusLocked(

View File

@ -162,6 +162,11 @@ MimeParser::DataParsedStatus MimeParser::writeDataToAttachment(const char* buffe
void MimeParser::partDataCallback(const char* buffer, size_t size, void* userData) { void MimeParser::partDataCallback(const char* buffer, size_t size, void* userData) {
MimeParser* parser = static_cast<MimeParser*>(userData); MimeParser* parser = static_cast<MimeParser*>(userData);
if (MimeParser::DataParsedStatus::INCOMPLETE == parser->m_dataParsedStatus) {
ACSDK_DEBUG9(LX("partDataCallbackIgnored").d("reason", "attachmentWriterFullBuffer"));
return;
}
if (parser->m_dataParsedStatus != MimeParser::DataParsedStatus::OK) { if (parser->m_dataParsedStatus != MimeParser::DataParsedStatus::OK) {
ACSDK_ERROR( ACSDK_ERROR(
LX("partDataCallbackFailed").d("reason", "mimeParsingError").d("status", parser->m_dataParsedStatus)); LX("partDataCallbackFailed").d("reason", "mimeParsingError").d("status", parser->m_dataParsedStatus));
@ -170,7 +175,7 @@ void MimeParser::partDataCallback(const char* buffer, size_t size, void* userDat
// If we've already processed any of this part in a previous incomplete iteration, let's not process it twice. // If we've already processed any of this part in a previous incomplete iteration, let's not process it twice.
if (!parser->shouldProcessBytes(size)) { if (!parser->shouldProcessBytes(size)) {
ACSDK_DEBUG(LX("partDataCallbackSkipped").d("reason", "bytesAlreadyProcessed")); ACSDK_DEBUG9(LX("partDataCallbackSkipped").d("reason", "bytesAlreadyProcessed"));
parser->updateCurrentByteProgress(size); parser->updateCurrentByteProgress(size);
parser->m_dataParsedStatus = MimeParser::DataParsedStatus::OK; parser->m_dataParsedStatus = MimeParser::DataParsedStatus::OK;
return; return;
@ -304,6 +309,7 @@ MimeParser::DataParsedStatus MimeParser::feed(char* data, size_t length) {
} }
// Initialize this before all the feed() callbacks happen (since this persists from previous call). // Initialize this before all the feed() callbacks happen (since this persists from previous call).
m_currentByteProgress = 0;
m_dataParsedStatus = DataParsedStatus::OK; m_dataParsedStatus = DataParsedStatus::OK;
m_multipartReader.feed(data, length); m_multipartReader.feed(data, length);
@ -350,7 +356,7 @@ void MimeParser::setAttachmentWriterBufferFull(bool isFull) {
if (isFull == m_isAttachmentWriterBufferFull) { if (isFull == m_isAttachmentWriterBufferFull) {
return; return;
} }
ACSDK_DEBUG0(LX("setAttachmentWriterBufferFull").d("full", isFull)); ACSDK_DEBUG9(LX("setAttachmentWriterBufferFull").d("full", isFull));
m_isAttachmentWriterBufferFull = isFull; m_isAttachmentWriterBufferFull = isFull;
} }

View File

@ -240,16 +240,18 @@ void PostConnectSynchronizer::notifyObservers() {
} }
} }
void PostConnectSynchronizer::onServerSideDisconnect() { void PostConnectSynchronizer::onServerSideDisconnect(std::shared_ptr<TransportInterface> transport) {
ACSDK_DEBUG(LX("onServerSideDisconnect()")); ACSDK_DEBUG(LX("onServerSideDisconnect()"));
doShutdown(); doShutdown();
} }
void PostConnectSynchronizer::onConnected() { void PostConnectSynchronizer::onConnected(std::shared_ptr<TransportInterface> transport) {
ACSDK_DEBUG(LX("onConnected()")); ACSDK_DEBUG(LX("onConnected()"));
} }
void PostConnectSynchronizer::onDisconnected(ConnectionStatusObserverInterface::ChangedReason reason) { void PostConnectSynchronizer::onDisconnected(
std::shared_ptr<TransportInterface> transport,
ConnectionStatusObserverInterface::ChangedReason reason) {
ACSDK_DEBUG(LX("onDisconnected()")); ACSDK_DEBUG(LX("onDisconnected()"));
doShutdown(); doShutdown();
} }

View File

@ -39,7 +39,7 @@ TEST_F(MessageRouterTest, getConnectionStatusReturnsConnectedAfterConnectionEsta
} }
TEST_F(MessageRouterTest, getConnectionStatusReturnsConnectedAfterDisconnected) { TEST_F(MessageRouterTest, getConnectionStatusReturnsConnectedAfterDisconnected) {
m_router->onDisconnected(ConnectionStatusObserverInterface::ChangedReason::ACL_DISABLED); m_router->onDisconnected(m_mockTransport, ConnectionStatusObserverInterface::ChangedReason::ACL_DISABLED);
ASSERT_EQ(m_router->getConnectionStatus().first, ConnectionStatusObserverInterface::Status::DISCONNECTED); ASSERT_EQ(m_router->getConnectionStatus().first, ConnectionStatusObserverInterface::Status::DISCONNECTED);
} }
@ -74,7 +74,7 @@ TEST_F(MessageRouterTest, ensureTheMessageRouterObserverIsInformedOfTransportDis
auto reason = ConnectionStatusObserverInterface::ChangedReason::ACL_DISABLED; auto reason = ConnectionStatusObserverInterface::ChangedReason::ACL_DISABLED;
disconnectMockTransport(m_mockTransport.get()); disconnectMockTransport(m_mockTransport.get());
m_router->onDisconnected(reason); m_router->onDisconnected(m_mockTransport, reason);
// wait for the result to propagate by scheduling a task on the client executor // wait for the result to propagate by scheduling a task on the client executor
waitOnMessageRouter(SHORT_TIMEOUT_MS); waitOnMessageRouter(SHORT_TIMEOUT_MS);
@ -179,7 +179,7 @@ TEST_F(MessageRouterTest, serverSideDisconnectCreatesANewTransport) {
m_router->setMockTransport(newTransport); m_router->setMockTransport(newTransport);
// Reset the MessageRouterObserver, there should be no interactions with the observer // Reset the MessageRouterObserver, there should be no interactions with the observer
m_router->onServerSideDisconnect(); m_router->onServerSideDisconnect(oldTransport);
waitOnMessageRouter(SHORT_TIMEOUT_MS); waitOnMessageRouter(SHORT_TIMEOUT_MS);
@ -191,7 +191,7 @@ TEST_F(MessageRouterTest, serverSideDisconnectCreatesANewTransport) {
// mock the new transports connection // mock the new transports connection
connectMockTransport(newTransport.get()); connectMockTransport(newTransport.get());
m_router->onConnected(); m_router->onConnected(newTransport);
waitOnMessageRouter(SHORT_TIMEOUT_MS); waitOnMessageRouter(SHORT_TIMEOUT_MS);
@ -203,7 +203,7 @@ TEST_F(MessageRouterTest, serverSideDisconnectCreatesANewTransport) {
// mock the old transport disconnecting completely // mock the old transport disconnecting completely
disconnectMockTransport(oldTransport.get()); disconnectMockTransport(oldTransport.get());
m_router->onDisconnected(ConnectionStatusObserverInterface::ChangedReason::ACL_CLIENT_REQUEST); m_router->onDisconnected(oldTransport, ConnectionStatusObserverInterface::ChangedReason::ACL_CLIENT_REQUEST);
auto messageRequest = createMessageRequest(); auto messageRequest = createMessageRequest();

View File

@ -120,7 +120,7 @@ public:
void setupStateToConnected() { void setupStateToConnected() {
setupStateToPending(); setupStateToPending();
m_router->onConnected(); m_router->onConnected(m_mockTransport);
connectMockTransport(m_mockTransport.get()); connectMockTransport(m_mockTransport.get());
} }

View File

@ -0,0 +1,269 @@
/*
* MimeParserFuzzTest.cpp
*
* Copyright 2017 Amazon.com, Inc. or its affiliates. All Rights Reserved.
*
* Licensed under the Apache License, Version 2.0 (the "License").
* You may not use this file except in compliance with the License.
* A copy of the License is located at
*
* http://aws.amazon.com/apache2.0/
*
* or in the "license" file accompanying this file. This file is distributed
* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either
* express or implied. See the License for the specific language governing
* permissions and limitations under the License.
*/
/// @file MimeParserFuzzTest.cpp
#include <memory>
#include <random>
#include <thread>
#include <curl/curl.h>
#include <gtest/gtest.h>
#include "ACL/Transport/MessageConsumerInterface.h"
#include "ACL/Transport/HTTP2Stream.h"
#include <AVSCommon/AVS/Attachment/AttachmentReader.h>
#include <AVSCommon/AVS/Attachment/AttachmentWriter.h>
#include "AVSCommon/AVS/Attachment/InProcessAttachment.h"
#include <AVSCommon/SDKInterfaces/MessageObserverInterface.h>
#include <AVSCommon/Utils/Logger/Logger.h>
#include "Common/TestableAttachmentManager.h"
#include "Common/Common.h"
#include "Common/MimeUtils.h"
#include "Common/TestableMessageObserver.h"
#include "MockMessageRequest.h"
#include "TestableConsumer.h"
namespace alexaClientSDK {
namespace acl {
namespace test {
using namespace avsCommon::sdkInterfaces;
using namespace avsCommon::avs::attachment;
/// String to identify log entries originating from this file.
static const std::string TAG("MimeParserFuzzTest");
/**
* Create a LogEntry using this file's TAG and the specified event string.
*
* @param The event string for this @c LogEntry.
*/
#define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event)
/// The number of feed() calls to be made in @c MimeParserFuzzTest::feed()
static const int FEED_COUNT = 0x100;
/// Max pseudo-random buffer size to pass to @c feed() (large enough to assure we trigger attachment buffer full)
static const size_t MAX_FEED_SIZE = (InProcessAttachment::SDS_BUFFER_DEFAULT_SIZE_IN_BYTES / FEED_COUNT) * 4;
/// Max buffer size to read from the attachment reader (small enough trigger multiple reads per feed)
static const size_t MAX_READ_SIZE = MAX_FEED_SIZE / 8;
/// Consistent seed (for repeatable tests) with which to generate pseudo-random bytes.
static const unsigned int BYTES_SEED = 1;
/// Consistent seed (for repeatable tests) with which to generate pseudo-random feed sizes.
static const unsigned int FEED_SIZE_SEED = 2;
/// Consistent but different seed (for repeatable tests) with which to generate pseudo-random read sizes.
static const unsigned int READ_SIZE_SEED = FEED_SIZE_SEED + 1;
/// Content ID for mime part.
static const std::string CONTENT_ID = "CONTENT_ID";
/// Context ID for generating attachment ID.
static const std::string CONTEXT_ID = "CONTEXT_ID";
/// CR,LF to separate lines in mime headers and boundaries.
static const std::string CRLF = "\r\n";
/// Dashes used as a prefix or suffix to mime boundaries.
static const std::string DASHES = "--";
/// Mime Boundary string (without CR,LF or dashes).
static const std::string BOUNDARY = "BoundaryBoundaryBoundaryBoundaryBoundaryBoundaryBoundary";
/// Mime prefix to our attachment.
static const std::string ATTACHMENT_PREFIX = CRLF + DASHES + BOUNDARY + CRLF + "Content-ID: " + CONTENT_ID + CRLF +
"Content-Type: application/octet-stream" + CRLF + CRLF;
/// Mime suffix terminating our attachment.
static const std::string ATTACHMENT_SUFFIX = CRLF + DASHES + BOUNDARY + DASHES;
/**
* Class to generate consistent pseudo-random byte stream.
*/
class ByteGenerator {
public:
ByteGenerator();
/**
* Generate the next set of bytes in the stream.
*
* @param[out] buffer The buffer in which to place the generated bytes.
* @param size The number of bytes to generate.
*/
void generateBytes(std::vector<uint8_t>* buffer, size_t size);
private:
/// The underlying random byte generator.
std::independent_bits_engine<std::minstd_rand, CHAR_BIT, uint8_t> m_engine;
};
ByteGenerator::ByteGenerator() {
m_engine.seed(BYTES_SEED);
}
void ByteGenerator::generateBytes(std::vector<uint8_t>* buffer, size_t size) {
for (size_t ix = 0; ix < size; ix++) {
(*buffer)[ix] = m_engine();
}
}
/**
* Our GTest class.
*/
class MimeParserFuzzTest : public ::testing::Test {
public:
/**
* Construct the objects we will use across tests.
*/
void SetUp() override;
/**
* Feed a pseudo-random stream of bytes to the mime parser, making a number of
* calls with pseudo-random sizes.
*/
void feed();
/**
* Read the attachment the mime parser is rendering to, requesting a pseudo-random
* number of bytes with each read.
*/
void read();
/// The AttachmentManager.
std::shared_ptr<AttachmentManager> m_attachmentManager;
/// The MimeParser which we will be primarily testing.
std::shared_ptr<MimeParser> m_parser;
/// Flag to indicate the test has failed and loops should exit.
std::atomic<bool> m_failed;
};
void MimeParserFuzzTest::SetUp() {
m_attachmentManager = std::make_shared<AttachmentManager>(AttachmentManager::AttachmentType::IN_PROCESS);
auto testableConsumer = std::make_shared<TestableConsumer>();
testableConsumer->setMessageObserver(std::make_shared<TestableMessageObserver>());
m_parser = std::make_shared<MimeParser>(testableConsumer, m_attachmentManager);
m_parser->setAttachmentContextId(CONTEXT_ID);
m_parser->setBoundaryString(BOUNDARY);
m_failed = false;
}
void MimeParserFuzzTest::feed() {
m_parser->feed(const_cast<char*>(ATTACHMENT_PREFIX.c_str()), ATTACHMENT_PREFIX.size());
ByteGenerator feedBytesSource;
std::default_random_engine feedSizeGenerator;
feedSizeGenerator.seed(FEED_SIZE_SEED);
std::uniform_int_distribution<int> feedSizeDistribution(1, MAX_FEED_SIZE);
auto feedSizeSource = std::bind(feedSizeDistribution, feedSizeGenerator);
std::vector<uint8_t> feedBuffer(MAX_FEED_SIZE);
size_t totalBytesFed = 0;
int incompleteCount = 0;
for (int ix = 0; !m_failed && ix < FEED_COUNT; ix++) {
auto feedSize = feedSizeSource();
feedBytesSource.generateBytes(&feedBuffer, feedSize);
while (true) {
ACSDK_DEBUG9(LX("callingFeed").d("totalBytesFed", totalBytesFed).d("feedSize", feedSize));
auto status = m_parser->feed((char*)(&feedBuffer[0]), feedSize);
if (MimeParser::DataParsedStatus::OK == status) {
totalBytesFed += feedSize;
break;
} else if (MimeParser::DataParsedStatus::INCOMPLETE == status) {
ACSDK_DEBUG9(LX("feedIncomplete").d("action", "waitAndReDrive"));
++incompleteCount;
std::this_thread::sleep_for(std::chrono::milliseconds(1));
} else {
ASSERT_NE(status, status);
}
}
}
m_parser->feed(const_cast<char*>(ATTACHMENT_SUFFIX.c_str()), ATTACHMENT_SUFFIX.size());
// Make sure there were enough INCOMPLETE return statuses.
ACSDK_DEBUG9(LX("doneFeeding").d("incompleteCount", incompleteCount).d("FEED_COUNT", FEED_COUNT));
ASSERT_GT(incompleteCount, FEED_COUNT);
}
void MimeParserFuzzTest::read() {
ByteGenerator verifyBytesSource;
std::default_random_engine readSizeGenerator;
readSizeGenerator.seed(READ_SIZE_SEED);
std::uniform_int_distribution<int> readSizeDistribution(1, MAX_READ_SIZE);
auto readSizeSource = std::bind(readSizeDistribution, readSizeGenerator);
auto attachmentId = m_attachmentManager->generateAttachmentId(CONTEXT_ID, CONTENT_ID);
auto reader = m_attachmentManager->createReader(attachmentId, AttachmentReader::Policy::BLOCKING);
std::vector<uint8_t> readBuffer(MAX_READ_SIZE);
std::vector<uint8_t> verifyBuffer(MAX_READ_SIZE);
size_t totalBytesRead = 0;
while (true) {
// Delay reads so that AnotherMimeParserTest::feed() blocks on a full attachment buffer
// in a reliable way. This makes the test results consistent run to run.
std::this_thread::sleep_for(std::chrono::milliseconds(5));
auto readSizeIn = readSizeSource();
auto readStatus = AttachmentReader::ReadStatus::OK;
ACSDK_DEBUG9(LX("callingRead").d("totalBytesRead", totalBytesRead).d("readSizeIn", readSizeIn));
auto readSizeOut = reader->read(readBuffer.data(), readSizeIn, &readStatus);
if (readSizeOut != 0) {
ACSDK_DEBUG9(LX("readReturned").d("readSizeOut", readSizeOut));
verifyBytesSource.generateBytes(&verifyBuffer, readSizeOut);
for (size_t ix = 0; ix < readSizeOut; ix++) {
auto good = readBuffer[ix] == verifyBuffer[ix];
if (!good) {
m_failed = true;
ASSERT_EQ(readBuffer[ix], verifyBuffer[ix]) << "UnexpectedByte at: " << totalBytesRead + ix;
}
}
totalBytesRead += readSizeOut;
}
switch (readStatus) {
case AttachmentReader::ReadStatus::OK:
break;
case AttachmentReader::ReadStatus::CLOSED:
ACSDK_DEBUG9(LX("readClosed"));
return;
case AttachmentReader::ReadStatus::OK_WOULDBLOCK:
case AttachmentReader::ReadStatus::OK_TIMEDOUT:
case AttachmentReader::ReadStatus::ERROR_OVERRUN:
case AttachmentReader::ReadStatus::ERROR_BYTES_LESS_THAN_WORD_SIZE:
case AttachmentReader::ReadStatus::ERROR_INTERNAL:
ASSERT_NE(readStatus, readStatus);
break;
}
}
}
/**
* Exercise MimeParser's re-drive, parsing, and attachment buffering by feeding in a large attachment
* consisting of pseudo-random bytes sent in pseudo-random sized chunks. At the same time, read the
* resulting attachment in pseudo-random sized requests. Expect that the data read matches the data
* that was fed.
*/
TEST_F(MimeParserFuzzTest, testRandomFeedAndReadSizesOfRandomData) {
auto readThread = std::thread(&MimeParserFuzzTest::read, this);
feed();
readThread.join();
}
} // namespace test
} // namespace acl
} // namespace alexaClientSDK

View File

@ -367,6 +367,7 @@ void Timer::callTask(
break; break;
} }
} }
m_stopping = false;
m_running = false; m_running = false;
} }

View File

@ -104,7 +104,7 @@ bool ConfigurationNode::initialize(const std::vector<std::istream*>& jsonStreams
} }
IStreamWrapper wrapper(*jsonStream); IStreamWrapper wrapper(*jsonStream);
Document overlay(&m_document.GetAllocator()); Document overlay(&m_document.GetAllocator());
overlay.ParseStream(wrapper); overlay.ParseStream<kParseCommentsFlag>(wrapper);
if (overlay.HasParseError()) { if (overlay.HasParseError()) {
ACSDK_ERROR(LX("initializeFailed") ACSDK_ERROR(LX("initializeFailed")
.d("reason", "parseFailure") .d("reason", "parseFailure")

View File

@ -439,7 +439,24 @@ TEST_F(TimerTest, deleteDuringTask) {
verifyTimestamps(t0, SHORT_DELAY, SHORT_DELAY, Timer::PeriodType::ABSOLUTE, SHORT_DELAY); verifyTimestamps(t0, SHORT_DELAY, SHORT_DELAY, Timer::PeriodType::ABSOLUTE, SHORT_DELAY);
} }
/// This test verifies that /**
* This test verifies that a call to start() succeeds on a timer which was previously stopped while running a task, but
* which is inactive at the time the new start() is called.
*/
TEST_F(TimerTest, startRunningAfterStopDuringTask) {
ASSERT_TRUE(m_timer->start(NO_DELAY, std::bind(&TimerTest::simpleTask, this, MEDIUM_DELAY)).valid());
ASSERT_TRUE(m_timer->isActive());
std::this_thread::sleep_for(SHORT_DELAY);
m_timer->stop();
ASSERT_TRUE(waitForInactive());
m_timestamps.clear();
auto t0 = std::chrono::steady_clock::now();
ASSERT_EQ(
m_timer->start(MEDIUM_DELAY, std::bind(&TimerTest::simpleTask, this, NO_DELAY)).wait_for(TIMEOUT),
std::future_status::ready);
ASSERT_TRUE(waitForInactive());
verifyTimestamps(t0, MEDIUM_DELAY, MEDIUM_DELAY, Timer::PeriodType::ABSOLUTE, NO_DELAY);
}
} // namespace test } // namespace test
} // namespace timing } // namespace timing

View File

@ -1,5 +1,17 @@
## ChangeLog ## ChangeLog
### [1.2.1] - 2017-11-16
* **Enhancements**
* Added comments to `AlexaClientSDKConfig.json`. These descriptions provide additional guidance for what is expected for each field.
* Enabled pause and resume controls for Pandora.
* **Bug Fixes**
* Bug fix for [issue #329](https://github.com/alexa/avs-device-sdk/issues/329) - `HTTP2Transport` instances no longer leak when `SERVER_SIDE_DISCONNECT` is encountered.
* Bug fix for [issue #189](https://github.com/alexa/avs-device-sdk/issues/189) - Fixed a race condition in the `Timer` class that sometimes caused `SpeechSynthesizer` to get stuck in the "Speaking" state.
* Bug fix for a race condition that caused `SpeechSynthesizer` to ignore subsequent `Speak` directives.
* Bug fix for corrupted mime attachments.
### [1.2.0] - 2017-10-27 ### [1.2.0] - 2017-10-27
* **Enhancements** * **Enhancements**
* Updated MediaPlayer to solve stability issues * Updated MediaPlayer to solve stability issues

View File

@ -1,7 +1,7 @@
cmake_minimum_required(VERSION 3.1 FATAL_ERROR) cmake_minimum_required(VERSION 3.1 FATAL_ERROR)
# Set project information # Set project information
project(AlexaClientSDK VERSION 1.2.0 LANGUAGES CXX) project(AlexaClientSDK VERSION 1.2.1 LANGUAGES CXX)
set(PROJECT_BRIEF "A cross-platform, modular SDK for interacting with the Alexa Voice Service") set(PROJECT_BRIEF "A cross-platform, modular SDK for interacting with the Alexa Voice Service")
include(build/BuildDefaults.cmake) include(build/BuildDefaults.cmake)

View File

@ -598,8 +598,13 @@ void AudioPlayer::executeOnFocusChanged(FocusState newFocus) {
case PlayerActivity::STOPPED: case PlayerActivity::STOPPED:
case PlayerActivity::FINISHED: case PlayerActivity::FINISHED:
// We see a focus change to foreground in these states if we are starting to play a new song. // We see a focus change to foreground in these states if we are starting to play a new song.
ACSDK_DEBUG1(LX("executeOnFocusChanged").d("action", "playNextItem")); if (!m_audioItems.empty()) {
playNextItem(); ACSDK_DEBUG1(LX("executeOnFocusChanged").d("action", "playNextItem"));
playNextItem();
} else {
ACSDK_DEBUG1(LX("executeOnFocusChanged").d("action", "releaseChannel"));
m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this());
}
return; return;
case PlayerActivity::PAUSED: { case PlayerActivity::PAUSED: {
// A focus change to foreground when paused means we should resume the current song. // A focus change to foreground when paused means we should resume the current song.
@ -784,7 +789,12 @@ void AudioPlayer::executeOnPlaybackError(SourceId id, const ErrorType& type, std
} }
sendPlaybackFailedEvent(m_token, type, error); sendPlaybackFailedEvent(m_token, type, error);
executeStop();
/*
* There's no need to call stop() here as the MediaPlayer has already stopped due to the playback error. Instead,
* call executeOnPlaybackStopped() so that the states in AudioPlayer are reset properly.
*/
executeOnPlaybackStopped(m_sourceId);
} }
void AudioPlayer::executeOnPlaybackPaused(SourceId id) { void AudioPlayer::executeOnPlaybackPaused(SourceId id) {

View File

@ -1147,11 +1147,14 @@ TEST_F(AudioPlayerTest, testFocusChangeToNoneInIdleState) {
} }
/** /**
* DISABLE this test because the behavior has been changed such that we no longer expect
* a PlaybackFailedEvent
*
* Test focus change to Foreground in IDLE state * Test focus change to Foreground in IDLE state
* Expect a PlaybackFailedEvent * Expect a PlaybackFailedEvent
*/ */
TEST_F(AudioPlayerTest, testFocusChangeToForegroundInIdleState) { TEST_F(AudioPlayerTest, DISABLED_testFocusChangeToForegroundInIdleState) {
bool messageSentResult = false; bool messageSentResult = false;
// expect PlaybackFailed since there are no queued AudioItems // expect PlaybackFailed since there are no queued AudioItems
@ -1215,11 +1218,14 @@ TEST_F(AudioPlayerTest, testFocusChangesInPlayingState) {
} }
/** /**
* * DISABLE this test because the behavior has been changed such that we no longer expect
* a PlaybackFailedEvent
*
* Test focus changes in STOPPED state * Test focus changes in STOPPED state
* Expect a PlaybackFailedEvent when attempting to switch to FOREGROUND * Expect a PlaybackFailedEvent when attempting to switch to FOREGROUND
*/ */
TEST_F(AudioPlayerTest, testFocusChangesInStoppedState) { TEST_F(AudioPlayerTest, DISABLED_testFocusChangesInStoppedState) {
sendPlayDirective(); sendPlayDirective();
// push AudioPlayer into stopped state // push AudioPlayer into stopped state

View File

@ -620,9 +620,19 @@ void SpeechSynthesizer::executePlaybackError(const avsCommon::utils::mediaPlayer
} }
m_waitOnStateChange.notify_one(); m_waitOnStateChange.notify_one();
releaseForegroundFocus(); releaseForegroundFocus();
sendExceptionEncounteredAndReportFailed(m_currentInfo, avsCommon::avs::ExceptionErrorType::INTERNAL_ERROR, error);
resetCurrentInfo(); resetCurrentInfo();
resetMediaSourceId(); resetMediaSourceId();
{
std::unique_lock<std::mutex> lock(m_mutex);
while (!m_speakInfoQueue.empty()) {
auto speakInfo = m_speakInfoQueue.front();
m_speakInfoQueue.pop_front();
lock.unlock();
sendExceptionEncounteredAndReportFailed(
speakInfo, avsCommon::avs::ExceptionErrorType::INTERNAL_ERROR, error);
lock.lock();
}
}
} }
std::string SpeechSynthesizer::buildState(std::string& token, int64_t offsetInMilliseconds) const { std::string SpeechSynthesizer::buildState(std::string& token, int64_t offsetInMilliseconds) const {

View File

@ -259,6 +259,17 @@ public:
/// Future to notify when @c sendMessage is called. /// Future to notify when @c sendMessage is called.
std::future<void> m_wakeSendMessageFuture; std::future<void> m_wakeSendMessageFuture;
/**
* Fulfills the @c m_wakeStoppedPromise. This is invoked in response to a @c stop call.
*/
void wakeOnStopped();
/// Promise to be fulfilled when @c stop is called.
std::promise<void> m_wakeStoppedPromise;
/// Future to notify when @c stop is called.
std::future<void> m_wakeStoppedFuture;
/// An exception sender used to send exception encountered events to AVS. /// An exception sender used to send exception encountered events to AVS.
std::shared_ptr<MockExceptionEncounteredSender> m_mockExceptionSender; std::shared_ptr<MockExceptionEncounteredSender> m_mockExceptionSender;
@ -278,7 +289,9 @@ SpeechSynthesizerTest::SpeechSynthesizerTest() :
m_wakeSetFailedPromise{}, m_wakeSetFailedPromise{},
m_wakeSetFailedFuture{m_wakeSetFailedPromise.get_future()}, m_wakeSetFailedFuture{m_wakeSetFailedPromise.get_future()},
m_wakeSendMessagePromise{}, m_wakeSendMessagePromise{},
m_wakeSendMessageFuture{m_wakeSendMessagePromise.get_future()} { m_wakeSendMessageFuture{m_wakeSendMessagePromise.get_future()},
m_wakeStoppedPromise{},
m_wakeStoppedFuture{m_wakeStoppedPromise.get_future()} {
} }
void SpeechSynthesizerTest::SetUp() { void SpeechSynthesizerTest::SetUp() {
@ -334,6 +347,10 @@ void SpeechSynthesizerTest::wakeOnSendMessage() {
m_wakeSendMessagePromise.set_value(); m_wakeSendMessagePromise.set_value();
} }
void SpeechSynthesizerTest::wakeOnStopped() {
m_wakeStoppedPromise.set_value();
}
/** /**
* Test call to handleDirectiveImmediately. * Test call to handleDirectiveImmediately.
* Expected result is that @c acquireChannel is called with the correct channel. On focus changed @c FOREGROUND, audio * Expected result is that @c acquireChannel is called with the correct channel. On focus changed @c FOREGROUND, audio
@ -632,6 +649,113 @@ TEST_F(SpeechSynthesizerTest, testBargeInWhilePlaying) {
ASSERT_TRUE(std::future_status::ready == m_wakeAcquireChannelFuture.wait_for(WAIT_TIMEOUT)); ASSERT_TRUE(std::future_status::ready == m_wakeAcquireChannelFuture.wait_for(WAIT_TIMEOUT));
} }
/**
* Testing calling stop() in @c MediaPlayer twice won't get the @c SpeechSynthesizer into a bad state.
* Call preHandle with a valid SPEAK directive. Then call handleDirective. Expected result is that @c acquireChannel
* is called once. On Focus Changed to foreground, audio should play.
* Call cancel directive. Expect the stop() to be called once.
* Call onFocusChanged, expect the stop() to be called again, and this time the @c MediaPlayer will return false to
* report an error. Expect when handleDirectiveImmediately with a valid SPEAK directive is called, @c SpeechSynthesizer
* will react correctly.
*/
TEST_F(SpeechSynthesizerTest, testCallingStopTwice) {
auto avsMessageHeader = std::make_shared<AVSMessageHeader>(
NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK, MESSAGE_ID_TEST, DIALOG_REQUEST_ID_TEST);
std::shared_ptr<AVSDirective> directive =
AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST);
auto avsMessageHeader2 =
std::make_shared<AVSMessageHeader>(NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK, MESSAGE_ID_TEST_2);
std::shared_ptr<AVSDirective> directive2 =
AVSDirective::create("", avsMessageHeader2, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST_2);
EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID))
.Times(AtLeast(1))
.WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel));
EXPECT_CALL(
*(m_mockSpeechPlayer.get()),
attachmentSetSource(A<std::shared_ptr<avsCommon::avs::attachment::AttachmentReader>>()))
.Times(AtLeast(1));
EXPECT_CALL(*(m_mockSpeechPlayer.get()), play(_)).Times(AtLeast(1));
EXPECT_CALL(*(m_mockSpeechPlayer.get()), getOffset(_))
.Times(AtLeast(1))
.WillRepeatedly(Return(OFFSET_IN_CHRONO_MILLISECONDS_TEST));
EXPECT_CALL(
*(m_mockContextManager.get()),
setState(NAMESPACE_AND_NAME_SPEECH_STATE, PLAYING_STATE_TEST, StateRefreshPolicy::ALWAYS, 0))
.Times(AtLeast(1))
.WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnSetState));
EXPECT_CALL(
*(m_mockContextManager.get()),
setState(NAMESPACE_AND_NAME_SPEECH_STATE, FINISHED_STATE_TEST, StateRefreshPolicy::NEVER, 0))
.Times(AtLeast(1))
.WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnSetState));
EXPECT_CALL(*(m_mockMessageSender.get()), sendMessage(_))
.Times(AtLeast(1))
.WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnSendMessage));
EXPECT_CALL(*(m_mockFocusManager.get()), releaseChannel(CHANNEL_NAME, _))
.Times(AtLeast(1))
.WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnReleaseChannel));
EXPECT_CALL(*(m_mockSpeechPlayer.get()), stop(_))
.Times(AtLeast(1))
.WillOnce(Invoke([this](avsCommon::utils::mediaPlayer::MediaPlayerInterface::SourceId id) {
wakeOnStopped();
return true;
}))
.WillOnce(Invoke([this](avsCommon::utils::mediaPlayer::MediaPlayerInterface::SourceId id) {
wakeOnStopped();
return false;
}))
.WillRepeatedly(Return(true));
EXPECT_CALL(*m_mockDirHandlerResult, setFailed(_)).Times(AtLeast(1));
// send Speak directive and getting focus and wait until playback started
m_speechSynthesizer->CapabilityAgent::preHandleDirective(directive, std::move(m_mockDirHandlerResult));
m_speechSynthesizer->CapabilityAgent::handleDirective(MESSAGE_ID_TEST);
ASSERT_TRUE(std::future_status::ready == m_wakeAcquireChannelFuture.wait_for(WAIT_TIMEOUT));
m_wakeAcquireChannelPromise = std::promise<void>();
m_wakeAcquireChannelFuture = m_wakeAcquireChannelPromise.get_future();
m_speechSynthesizer->onFocusChanged(FocusState::FOREGROUND);
ASSERT_TRUE(m_mockSpeechPlayer->waitUntilPlaybackStarted());
ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT));
m_wakeSetStatePromise = std::promise<void>();
m_wakeSetStateFuture = m_wakeSetStatePromise.get_future();
ASSERT_TRUE(std::future_status::ready == m_wakeSendMessageFuture.wait_for(WAIT_TIMEOUT));
m_wakeSendMessagePromise = std::promise<void>();
m_wakeSendMessageFuture = m_wakeSendMessagePromise.get_future();
// cancel directive, this should result in calling stop()
m_speechSynthesizer->CapabilityAgent::cancelDirective(MESSAGE_ID_TEST);
ASSERT_TRUE(std::future_status::ready == m_wakeStoppedFuture.wait_for(WAIT_TIMEOUT));
m_wakeStoppedPromise = std::promise<void>();
m_wakeStoppedFuture = m_wakeStoppedPromise.get_future();
// goes to background, this should result in calling the 2nd stop() and MediaPlayer returning an error
m_speechSynthesizer->onFocusChanged(FocusState::BACKGROUND);
ASSERT_TRUE(std::future_status::ready == m_wakeStoppedFuture.wait_for(WAIT_TIMEOUT));
ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT));
m_wakeSetStatePromise = std::promise<void>();
m_wakeSetStateFuture = m_wakeSetStatePromise.get_future();
/*
* onPlaybackStopped, this will result in an error with reason=nullptrDirectiveInfo. But this shouldn't break the
* SpeechSynthesizer
*/
m_speechSynthesizer->onPlaybackStopped(m_mockSpeechPlayer->m_sourceId);
ASSERT_TRUE(std::future_status::ready == m_wakeReleaseChannelFuture.wait_for(WAIT_TIMEOUT));
m_wakeReleaseChannelPromise = std::promise<void>();
m_wakeReleaseChannelFuture = m_wakeReleaseChannelPromise.get_future();
// send second speak directive and make sure it working
m_speechSynthesizer->handleDirectiveImmediately(directive2);
ASSERT_TRUE(std::future_status::ready == m_wakeAcquireChannelFuture.wait_for(WAIT_TIMEOUT));
m_speechSynthesizer->onFocusChanged(FocusState::FOREGROUND);
ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT));
m_wakeSetStatePromise = std::promise<void>();
m_wakeSetStateFuture = m_wakeSetStatePromise.get_future();
ASSERT_TRUE(m_mockSpeechPlayer->waitUntilPlaybackStarted());
}
} // namespace test } // namespace test
} // namespace speechSynthesizer } // namespace speechSynthesizer
} // namespace capabilityAgents } // namespace capabilityAgents

View File

@ -1,28 +1,78 @@
{ {
"authDelegate":{ "authDelegate":{
// The Client Secret of the Product from developer.amazon.com
"clientSecret":"${SDK_CONFIG_CLIENT_SECRET}", "clientSecret":"${SDK_CONFIG_CLIENT_SECRET}",
// Unique device serial number. e.g. 123456
"deviceSerialNumber":"${SDK_CONFIG_DEVICE_SERIAL_NUMBER}", "deviceSerialNumber":"${SDK_CONFIG_DEVICE_SERIAL_NUMBER}",
"refreshToken":"${SDK_CONFIG_REFRESH_TOKEN}", // Refresh Token populated by running AuthServer.py
"refreshToken":"{SDK_CONFIG_REFRESH_TOKEN}",
// The Client ID of the Product from developer.amazon.com
"clientId":"${SDK_CONFIG_CLIENT_ID}", "clientId":"${SDK_CONFIG_CLIENT_ID}",
// Product ID from developer.amazon.com
"productId":"${SDK_CONFIG_PRODUCT_ID}" "productId":"${SDK_CONFIG_PRODUCT_ID}"
}, },
"alertsCapabilityAgent":{ "alertsCapabilityAgent":{
// Path to Alerts database file. e.g. /home/ubuntu/Build/alerts.db
// Note: The directory specified must be valid.
// The database file (alerts.db) will be created by SampleApp, do not create it yourself.
// The database file should only be used for alerts (don't use it for other components of SDK)
"databaseFilePath":"${SDK_SQLITE_DATABASE_FILE_PATH}", "databaseFilePath":"${SDK_SQLITE_DATABASE_FILE_PATH}",
// Path to default Alarm sound file. e.g. /home/ubuntu/alert_sounds/alarm_normal.mp3
// Note: The audio file must exist and be a valid file.
"alarmSoundFilePath":"${SDK_ALARM_DEFAULT_SOUND_FILE_PATH}", "alarmSoundFilePath":"${SDK_ALARM_DEFAULT_SOUND_FILE_PATH}",
// Path to short Alarm sound file. e.g. /home/ubuntu/alert_sounds/alarm_short.wav
// Note: The audio file must exist and be a valid file.
"alarmShortSoundFilePath":"${SDK_ALARM_SHORT_SOUND_FILE_PATH}", "alarmShortSoundFilePath":"${SDK_ALARM_SHORT_SOUND_FILE_PATH}",
// Path to default timer sound file. e.g. /home/ubuntu/alert_sounds/timer_normal.mp3
// Note: The audio file must exist and be a valid file.
"timerSoundFilePath":"${SDK_TIMER_DEFAULT_SOUND_FILE_PATH}", "timerSoundFilePath":"${SDK_TIMER_DEFAULT_SOUND_FILE_PATH}",
// Path to short timer sound file. e.g. /home/ubuntu/alert_sounds/timer_short.wav
// Note: The audio file must exist and be a valid file.
"timerShortSoundFilePath":"${SDK_TIMER_SHORT_SOUND_FILE_PATH}" "timerShortSoundFilePath":"${SDK_TIMER_SHORT_SOUND_FILE_PATH}"
}, },
"settings":{ "settings":{
// Path to Settings database file. e.g. /home/ubuntu/Build/settings.db
// Note: The directory specified must be valid.
// The database file (settings.db) will be created by SampleApp, do not create it yourself.
// The database file should only be used for settings (don't use it for other components of SDK)
"databaseFilePath":"${SDK_SQLITE_SETTINGS_DATABASE_FILE_PATH}", "databaseFilePath":"${SDK_SQLITE_SETTINGS_DATABASE_FILE_PATH}",
"defaultAVSClientSettings":{ "defaultAVSClientSettings":{
// Default language for Alexa.
// See https://developer.amazon.com/docs/alexa-voice-service/settings.html#settingsupdated for valid values.
"locale":"${SETTING_LOCALE_VALUE}" "locale":"${SETTING_LOCALE_VALUE}"
} }
}, },
"certifiedSender":{ "certifiedSender":{
"databaseFilePath":"{SDK_CERTIFIED_SENDER_DATABASE_FILE_PATH}" // Path to Certified Sender database file. e.g. /home/ubuntu/Build/certifiedsender.db
// Note: The directory specified must be valid.
// The database file (certifiedsender.db) will be created by SampleApp, do not create it yourself.
// The database file should only be used for certifiedSender (don't use it for other components of SDK)
"databaseFilePath":"${SDK_CERTIFIED_SENDER_DATABASE_FILE_PATH}"
} }
} }
// Notes for logging
// The log levels are supported to debug when SampleApp is not working as expected.
// There are 14 levels of logging with DEBUG9 providing the highest level of logging and CRITICAL providing
// the lowest level of logging i.e. if DEBUG9 is specified while running the SampleApp, all the logs at DEBUG9 and
// below are displayed, whereas if CRITICAL is specified, only logs of CRITICAL are displayed.
// The 14 levels are:
// DEBUG9, DEBUG8, DEBUG7, DEBUG6, DEBUG5, DEBUG4, DEBUG3, DEBUG2, DEBUG1, DEBUG0, INFO, WARN, ERROR, CRTITICAL.
// To selectively see the logging for a particular module, you can specify logging level in this json file.
// Some examples are:
// To only see logs of level INFO and below for ACL and MediaPlayer modules,
// - grep for ACSDK_LOG_MODULE in source folder. Find the log module for ACL and MediaPlayer.
// - Put the following in json:
// "acl":{
// "logLevel":"INFO"
// },
// "mediaPlayer":{
// "logLevel":"INFO"
// }
// To enable DEBUG, build with cmake option -DCMAKE_BUILD_TYPE=DEBUG. By default it is built with RELEASE build.
// And run the SampleApp similar to the following command.
// e.g. TZ=UTC ./SampleApp /home/ubuntu/.../AlexaClientSDKConfig.json /home/ubuntu/KittAiModels/ DEBUG9"

View File

@ -954,11 +954,14 @@ TEST_F(AlertsTest, cancelAlertBeforeItIsActive) {
} }
/** /**
* Disabled until consistent failures can be investigated. Failures are due to the test's expectation of event
* ordering.
*
* Test when the storage is removed before an alert is set * Test when the storage is removed before an alert is set
* *
* Close the storage before asking for a 5 second timer. SetAlertFailed and DeleteAlertFailed events are then sent. * Close the storage before asking for a 5 second timer. SetAlertFailed and DeleteAlertFailed events are then sent.
*/ */
TEST_F(AlertsTest, RemoveStorageBeforeAlarmIsSet) { TEST_F(AlertsTest, DISABLED_RemoveStorageBeforeAlarmIsSet) {
m_alertStorage->close(); m_alertStorage->close();
// Write audio to SDS saying "Set a timer for 5 seconds" // Write audio to SDS saying "Set a timer for 5 seconds"

View File

@ -114,13 +114,16 @@ private:
* The @c AudioPipeline consists of the following elements: * The @c AudioPipeline consists of the following elements:
* @li @c appsrc The appsrc element is used as the source to which audio data is provided. * @li @c appsrc The appsrc element is used as the source to which audio data is provided.
* @li @c decoder Decodebin is used as the decoder element to decode audio. * @li @c decoder Decodebin is used as the decoder element to decode audio.
* @li @c decodedQueue A queue is used to store the decoded data.
* @li @c converter An audio-converter is used to convert between audio formats. * @li @c converter An audio-converter is used to convert between audio formats.
* @li @c volume The volume element is used as a volume control. * @li @c volume The volume element is used as a volume control.
* @li @c audioSink Sink for the audio. * @li @c audioSink Sink for the audio.
* @li @c pipeline The pipeline is a bin consisting of the @c appsrc, the @c decoder, the @c converter, and the * @li @c pipeline The pipeline is a bin consisting of the @c appsrc, the @c decoder, the @c converter, and the
* @c audioSink. * @c audioSink.
* *
* The data flow through the elements is appsrc -> decoder -> converter -> volume -> audioSink. * The data flow through the elements is appsrc -> decoder -> decodedQueue -> converter -> volume -> audioSink.
* Ideally we would want to use playsink or playbin directly to automate as much as possible. However, this
* causes problems with multiple pipelines and volume settings in pulse audio. Pending further investigation.
*/ */
struct AudioPipeline { struct AudioPipeline {
/// The source element. /// The source element.
@ -129,6 +132,9 @@ private:
/// The decoder element. /// The decoder element.
GstElement* decoder; GstElement* decoder;
/// A queue for decoded elements.
GstElement* decodedQueue;
/// The converter element. /// The converter element.
GstElement* converter; GstElement* converter;
@ -145,6 +151,7 @@ private:
AudioPipeline() : AudioPipeline() :
appsrc{nullptr}, appsrc{nullptr},
decoder{nullptr}, decoder{nullptr},
decodedQueue{nullptr},
converter{nullptr}, converter{nullptr},
volume{nullptr}, volume{nullptr},
audioSink{nullptr}, audioSink{nullptr},

View File

@ -533,6 +533,14 @@ bool MediaPlayer::init() {
} }
bool MediaPlayer::setupPipeline() { bool MediaPlayer::setupPipeline() {
m_pipeline.decodedQueue = gst_element_factory_make("queue", "decodedQueue");
// Do not send signals or messages. Let the decoder buffer messages dictate application logic.
g_object_set(m_pipeline.decodedQueue, "silent", TRUE, NULL);
if (!m_pipeline.decodedQueue) {
ACSDK_ERROR(LX("setupPipelineFailed").d("reason", "createQueueElementFailed"));
return false;
}
m_pipeline.converter = gst_element_factory_make("audioconvert", "converter"); m_pipeline.converter = gst_element_factory_make("audioconvert", "converter");
if (!m_pipeline.converter) { if (!m_pipeline.converter) {
ACSDK_ERROR(LX("setupPipelineFailed").d("reason", "createConverterElementFailed")); ACSDK_ERROR(LX("setupPipelineFailed").d("reason", "createConverterElementFailed"));
@ -561,11 +569,17 @@ bool MediaPlayer::setupPipeline() {
m_busWatchId = gst_bus_add_watch(bus, &MediaPlayer::onBusMessage, this); m_busWatchId = gst_bus_add_watch(bus, &MediaPlayer::onBusMessage, this);
gst_object_unref(bus); gst_object_unref(bus);
// Link only the volume, converter, and sink here. Src will be linked in respective source files. // Link only the queue, converter, volume, and sink here. Src will be linked in respective source files.
gst_bin_add_many( gst_bin_add_many(
GST_BIN(m_pipeline.pipeline), m_pipeline.converter, m_pipeline.volume, m_pipeline.audioSink, nullptr); GST_BIN(m_pipeline.pipeline),
m_pipeline.decodedQueue,
m_pipeline.converter,
m_pipeline.volume,
m_pipeline.audioSink,
nullptr);
if (!gst_element_link_many(m_pipeline.converter, m_pipeline.volume, m_pipeline.audioSink, nullptr)) { if (!gst_element_link_many(
m_pipeline.decodedQueue, m_pipeline.converter, m_pipeline.volume, m_pipeline.audioSink, nullptr)) {
ACSDK_ERROR(LX("setupPipelineFailed").d("reason", "createVolumeToConverterToSinkLinkFailed")); ACSDK_ERROR(LX("setupPipelineFailed").d("reason", "createVolumeToConverterToSinkLinkFailed"));
return false; return false;
} }
@ -611,6 +625,7 @@ void MediaPlayer::resetPipeline() {
m_pipeline.pipeline = nullptr; m_pipeline.pipeline = nullptr;
m_pipeline.appsrc = nullptr; m_pipeline.appsrc = nullptr;
m_pipeline.decoder = nullptr; m_pipeline.decoder = nullptr;
m_pipeline.decodedQueue = nullptr;
m_pipeline.converter = nullptr; m_pipeline.converter = nullptr;
m_pipeline.volume = nullptr; m_pipeline.volume = nullptr;
m_pipeline.audioSink = nullptr; m_pipeline.audioSink = nullptr;
@ -698,8 +713,7 @@ void MediaPlayer::onPadAdded(GstElement* decoder, GstPad* pad, gpointer pointer)
void MediaPlayer::handlePadAdded(std::promise<void>* promise, GstElement* decoder, GstPad* pad) { void MediaPlayer::handlePadAdded(std::promise<void>* promise, GstElement* decoder, GstPad* pad) {
ACSDK_DEBUG9(LX("handlePadAddedSignalCalled")); ACSDK_DEBUG9(LX("handlePadAddedSignalCalled"));
GstElement* converter = m_pipeline.converter; gst_element_link(decoder, m_pipeline.decodedQueue);
gst_element_link(decoder, converter);
promise->set_value(); promise->set_value();
} }
@ -907,7 +921,7 @@ void MediaPlayer::handleSetAttachmentReaderSource(
/* /*
* Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal * Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal
* to the callback which performs the linking of the decoder source pad to the converter sink pad. * to the callback which performs the linking of the decoder source pad to decodedQueue sink pad.
*/ */
if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) { if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) {
ACSDK_ERROR(LX("handleSetAttachmentReaderSourceFailed").d("reason", "connectPadAddedSignalFailed")); ACSDK_ERROR(LX("handleSetAttachmentReaderSourceFailed").d("reason", "connectPadAddedSignalFailed"));
@ -938,7 +952,7 @@ void MediaPlayer::handleSetIStreamSource(
/* /*
* Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal * Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal
* to the callback which performs the linking of the decoder source pad to the converter sink pad. * to the callback which performs the linking of the decoder source pad to the decodedQueue sink pad.
*/ */
if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) { if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) {
ACSDK_ERROR(LX("handleSetIStreamSourceFailed").d("reason", "connectPadAddedSignalFailed")); ACSDK_ERROR(LX("handleSetIStreamSourceFailed").d("reason", "connectPadAddedSignalFailed"));
@ -966,7 +980,7 @@ void MediaPlayer::handleSetSource(std::string url, std::promise<MediaPlayer::Sou
* The first pad that is added may not be the correct stream (ie may be a video stream), and will fail. * The first pad that is added may not be the correct stream (ie may be a video stream), and will fail.
* *
* Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal * Once the source pad for the decoder has been added, the decoder emits the pad-added signal. Connect the signal
* to the callback which performs the linking of the decoder source pad to the converter sink pad. * to the callback which performs the linking of the decoder source pad to the decodedQueue sink pad.
*/ */
if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) { if (!g_signal_connect(m_pipeline.decoder, "pad-added", G_CALLBACK(onPadAdded), this)) {
ACSDK_ERROR(LX("handleSetSourceForUrlFailed").d("reason", "connectPadAddedSignalFailed")); ACSDK_ERROR(LX("handleSetSourceForUrlFailed").d("reason", "connectPadAddedSignalFailed"));

View File

@ -86,39 +86,16 @@ Focus management is not specific to Capability Agents or Directive Handlers, and
**Note**: Features, updates, and resolved issues from previous releases are available to view in [CHANGELOG.md](https://github.com/alexa/alexa-client-sdk/blob/master/CHANGELOG.md). **Note**: Features, updates, and resolved issues from previous releases are available to view in [CHANGELOG.md](https://github.com/alexa/alexa-client-sdk/blob/master/CHANGELOG.md).
v1.2 released 10/27/2017: v1.2.1 released 11/16/2017:
**Enhancements** **Enhancements**
* Updated MediaPlayer to solve stability issues * Added comments to `AlexaClientSDKConfig.json`. These descriptions provide additional guidance for what is expected for each field.
* All capability agents were refined to work with the updated MediaPlayer * Enabled pause and resume controls for Pandora.
* Added the TemplateRuntime capability agent
* Added the SpeakerManager capability agent
* Added a configuration option ("sampleApp":"endpoint") that allows the endpoint that SampleApp connects to to be specified without changing code or rebuilding
* Added very verbose capture of libcurl debug information
* Added an observer interface to observer audio state changes from AudioPlayer
* Added support for StreamMetadataExtracted Event. Stream tags found in the stream are represented in JSON and sent to AVS
* Added to the SampleApp a simple GuiRenderer as an observer to the TemplateRuntime Capability Agent
* Moved shared libcurl functionality to AVSCommon/Utils
* Added a CMake option to exclude tests from the "make all" build. Use "cmake <absolute-path-to-source>
-DACSDK_EXCLUDE_TEST_FROM_ALL=ON" to enable it. When this option is enabled "make unit" and "make integration" still could be used to build and run the tests
**Bug Fixes** **Bug Fixes**
* Previously scheduled alerts now play following a restart * Bug fix for [issue #329](https://github.com/alexa/avs-device-sdk/issues/329) - `HTTP2Transport` instances no longer leak when `SERVER_SIDE_DISCONNECT` is encountered.
* General stability fixes * Bug fix for [issue #189](https://github.com/alexa/avs-device-sdk/issues/189) - Fixed a race condition in the `Timer` class that sometimes caused `SpeechSynthesizer` to get stuck in the "Speaking" state.
* Bug fix for CertifiedSender blocking forever if the network goes down while it's trying to send a message to AVS * Bug fix for a race condition that caused `SpeechSynthesizer` to ignore subsequent `Speak` directives.
* Fixes for known issue of Alerts integration tests fail: AlertsTest.UserLongUnrelatedBargeInOnActiveTimer and AlertsTest.handleOneTimerWithVocalStop * Bug fix for corrupted mime attachments.
* Attempting to end a tap-to-talk interaction with the tap-to-talk button wouldn't work
* SharedDataStream could encounter a race condition due to a combination of a blocking Reader and a Writer closing before writing any data
* Bug-fix for the ordering of notifications within alerts scheduling. This fixes the issue where a local-stop on an alert would also stop a subsequent alert if it were to begin without delay
**Known Issues**
* Capability agent for Notifications is not included in this release
* Inconsistent playback behavior when resuming content ("Alexa, pause." / "Alexa, resume."). Specifically, handling playback offsets, which causes the content to play from the start. This behavior is also occasionally seen with "Next" /
"Previous" as well
* `ACL`'s asynchronous receipt of audio attachments may manage resources poorly in scenarios where attachments are received but not consumed
* Music playback failures may result in an error Text to Speech being rendered repeatedly
* Reminders and named timers don't sound when there is no connection
* GUI cards don't show for Kindle

33
issue_template.md Normal file
View File

@ -0,0 +1,33 @@
**IMPORTANT**: Before you create an issue, please take a look at our [Issue Reporting Guide](https://github.com/alexa/avs-device-sdk/wiki/Issue-Reporting-Guide).
## Briefly summarize your issue:
<!--- Provide a more detailed introduction to the issue itself, and why you consider it to be a bug -->
## What is the expected behavior?
<!--- Tell us what should happen -->
## What behavior are you observing?
<!--- Tell us what's happening instead -->
## Provide the steps to reproduce the issue, if applicable:
<!--- Provide the steps for reproduce -->
## Tell us about your environment:
<!--- Include as many relevant details about the environment you experienced the bug in -->
### What version of the AVS Device SDK are you using?
<x.y.z>
### Tell us what hardware you're using:
- [ ] Desktop / Laptop
- [ ] Raspberry Pi
- [ ] Other - tell us more:
### Tell us about your OS (Type & version):
- [ ] Linux
- [ ] MacOS
- [ ] Raspbian Stretch
- [ ] Raspbian Jessy
- [ ] Other - tell us more:

View File

@ -16,6 +16,7 @@
from flask import Flask, redirect, request from flask import Flask, redirect, request
import requests import requests
import json import json
import commentjson
from os.path import abspath, isfile, dirname from os.path import abspath, isfile, dirname
import sys import sys
@ -47,6 +48,9 @@ amazonLwaApiHeaders = {'Content-Type': 'application/x-www-form-urlencoded'}
# Default configuration filename, to be filled by CMake # Default configuration filename, to be filled by CMake
defaultConfigFilename = "${SDK_CONFIG_FILE_TARGET}" defaultConfigFilename = "${SDK_CONFIG_FILE_TARGET}"
# Default string for refresh token in configuration file.
defaultRefreshTokenString = "{SDK_CONFIG_REFRESH_TOKEN}"
# JSON keys for config file # JSON keys for config file
CLIENT_ID = 'clientId' CLIENT_ID = 'clientId'
CLIENT_SECRET = 'clientSecret' CLIENT_SECRET = 'clientSecret'
@ -71,12 +75,13 @@ if not isfile(configFilename):
try: try:
configFile = open(configFilename,'r') configFile = open(configFilename,'r')
except IOError: except IOError:
print 'File "' + configFilename + '" not found!' print 'File "' + configFilename + '" not found!'
sys.exit(1) sys.exit(1)
else: else:
with configFile: with configFile:
configData = json.load(configFile) configData = commentjson.load(configFile)
if not configData.has_key(authDelegateKey): if not configData.has_key(authDelegateKey):
print 'The config file "' + \ print 'The config file "' + \
configFilename + \ configFilename + \
@ -110,6 +115,10 @@ if authDelegateDict.has_key(REFRESH_TOKEN):
amazonLwaApiUrl, amazonLwaApiUrl,
data=urlencode(postData), data=urlencode(postData),
headers=amazonLwaApiHeaders) headers=amazonLwaApiHeaders)
defaultRefreshTokenString = authDelegateDict[REFRESH_TOKEN];
if defaultRefreshTokenString == "":
print 'Refresh token in the file cannot be empty. Please enter {SDK_CONFIG_REFRESH_TOKEN} in the refresh token'
sys.exit(0)
if 200 == tokenRefreshRequest.status_code: if 200 == tokenRefreshRequest.status_code:
print 'You have a valid refresh token already in the file.' print 'You have a valid refresh token already in the file.'
sys.exit(0) sys.exit(0)
@ -159,7 +168,7 @@ def get_refresh_token():
'<br/>' + shutdown() '<br/>' + shutdown()
authDelegateDict['refreshToken'] = tokenRequest.json()['refresh_token'] authDelegateDict['refreshToken'] = tokenRequest.json()['refresh_token']
try: try:
configFile = open(configFilename,'w') configFile = open(configFilename,'r')
except IOError: except IOError:
print 'File "' + configFilename + '" cannot be opened!' print 'File "' + configFilename + '" cannot be opened!'
return '<h1>The file "' + \ return '<h1>The file "' + \
@ -168,8 +177,15 @@ def get_refresh_token():
shutdown() + \ shutdown() + \
'</h1>' '</h1>'
else: else:
with configFile: fileContent = configFile.read()
json.dump(configData, configFile, indent=4, separators=(',',':')) if defaultRefreshTokenString not in fileContent:
return '<h1>The file is written successfully.<br/>' + shutdown() + '</h1>' print '"{defaultRefreshTokenString} not in {configFilename}"'
return '<h1>' + defaultRefreshTokenString + ' string not present in ' + configFilename + \
' please check the file ' + shutdown() + '</h1>'
else:
configFile = open(configFilename,'w')
fileContent = fileContent.replace(defaultRefreshTokenString, tokenRequest.json()['refresh_token'])
configFile.write(fileContent)
return '<h1>The file is written successfully.<br/>' + shutdown() + '</h1>'
app.run(host='127.0.0.1',port='3000') app.run(host='127.0.0.1',port='3000')