From 46fd5482bcc481e2fb7445d434d21ab7e5685529 Mon Sep 17 00:00:00 2001 From: Sanjay Devireddy Date: Thu, 8 Mar 2018 16:55:39 -0800 Subject: [PATCH] Verison 1.6 of the avs-device-sdk Changes in this update: **Enhancements** * `rapidJson` is now included with "make install". * Updated the `TemplateRuntimeObserverInterface` to support clearing of `displayCards`. * Added Windows SDK support, along with an installation script (MinGW-w64). * Updated `ContextManager` to ignore context reported by a state provider. * The `SharedDataStream` object is now associated by playlist, rather than by URL. * Added the `RegistrationManager` component. Now, when a user logs out all persistent user-specific data is cleared from the SDK. The log out functionality can be exercised in the sample app with the new command: `k`. **Bug Fixes** * [Issue 400](https://github.com/alexa/avs-device-sdk/issues/400) Fixed a bug where the alert reminder did not iterate as intended after loss of network connection. * [Issue 477](https://github.com/alexa/avs-device-sdk/issues/477) Fixed a bug in which Alexa's weather response was being truncated. * Fixed an issue in which there were reports of instability related to the Sensory engine. To correct this, the `portAudio` [`suggestedLatency`](https://github.com/alexa/avs-device-sdk/blob/master/Integration/AlexaClientSDKConfig.json#L62) value can now be configured. **Known Issues** * The `ACL` may encounter issues if audio attachments are received but not consumed. * `SpeechSynthesizerState` currently uses `GAINING_FOCUS` and `LOSING_FOCUS` as a workaround for handling intermediate state. These states may be removed in a future release. * Music playback doesn't immediately stop when a user barges-in on iHeartRadio. * The Windows sample app sometimes hangs on exit. * GDP receives a `SIGPIPE` when troubleshooting the Windows sample app. --- ACL/include/ACL/Transport/HTTP2Stream.h | 11 + .../Transport/TransportObserverInterface.h | 2 + ACL/src/Transport/HTTP2Stream.cpp | 7 +- ACL/test/Transport/MimeParserTest.cpp | 5 + ADSL/include/ADSL/DirectiveProcessor.h | 16 + ADSL/include/ADSL/DirectiveSequencer.h | 7 + ADSL/src/DirectiveProcessor.cpp | 19 +- ADSL/src/DirectiveSequencer.cpp | 24 +- ADSL/test/ADSL/MockDirectiveSequencer.h | 4 + ADSL/test/DirectiveProcessorTest.cpp | 13 + ADSL/test/DirectiveSequencerTest.cpp | 104 ++++ AFML/include/AFML/ActivityTrackerInterface.h | 47 ++ AFML/include/AFML/AudioActivityTracker.h | 135 +++++ AFML/include/AFML/Channel.h | 84 ++- AFML/include/AFML/FocusManager.h | 72 ++- AFML/include/AFML/VisualActivityTracker.h | 112 ++++ AFML/src/AudioActivityTracker.cpp | 161 ++++++ AFML/src/CMakeLists.txt | 4 +- AFML/src/Channel.cpp | 59 ++- AFML/src/FocusManager.cpp | 94 +++- AFML/src/VisualActivityTracker.cpp | 139 +++++ AFML/test/AudioActivityTrackerTest.cpp | 309 +++++++++++ AFML/test/CMakeLists.txt | 6 +- AFML/test/FocusManagerTest.cpp | 397 +++++++++++--- AFML/test/VisualActivityTrackerTest.cpp | 270 ++++++++++ .../AVS/Attachment/AttachmentWriter.h | 1 + .../AVS}/ExternalMediaPlayer/AdapterUtils.h | 12 +- .../AVS/SpeakerConstants/SpeakerConstants.h | 2 + .../AVSCommon/AVS/StateRefreshPolicy.h | 9 +- .../src/ExternalMediaPlayer}/AdapterUtils.cpp | 8 +- AVSCommon/CMakeLists.txt | 3 + .../SDKInterfaces/AuthDelegateInterface.h | 4 +- .../SDKInterfaces/AuthObserverInterface.h | 8 +- .../SDKInterfaces/ContextManagerInterface.h | 3 +- .../DirectiveSequencerInterface.h | 15 +- .../ExternalMediaPlayerInterface.h | 54 ++ .../SDKInterfaces/FocusManagerInterface.h | 34 +- .../FocusManagerObserverInterface.h | 43 ++ .../HTTPContentFetcherInterface.h | 5 +- .../SDKInterfaces/MessageObserverInterface.h | 2 + .../SingleSettingObserverInterface.h | 2 + .../SoftwareInfoSenderObserverInterface.h | 3 + .../SDKInterfaces/SpeakerManagerInterface.h | 1 + .../TemplateRuntimeObserverInterface.h | 20 +- .../SDKInterfaces/MockDirectiveSequencer.h | 2 + .../SDKInterfaces/MockFocusManager.h | 8 +- .../SDKInterfaces/MockFocusManagerObserver.h | 85 +++ .../include/AVSCommon/Utils/AudioFormat.h | 8 +- .../Utils/Configuration/ConfigurationNode.h | 64 ++- .../LibcurlUtils/CurlEasyHandleWrapper.h | 49 +- .../AVSCommon/Utils/LibcurlUtils/HttpPost.h | 35 +- .../LibcurlUtils/LibCurlHttpContentFetcher.h | 8 +- .../AVSCommon/Utils/Logger/ConsoleLogger.h | 5 + .../Utils/Logger/LogStringFormatter.h | 60 +++ .../AVSCommon/Utils/Logger/LoggerUtils.h | 15 - .../Utils/Logger/SinkObserverInterface.h | 2 + .../PlaylistParserObserverInterface.h | 1 + .../include/AVSCommon/Utils/SDKVersion.h | 55 ++ .../AVSCommon/Utils/String/StringUtils.h | 8 + .../AVSCommon/Utils/Timing/SafeCTimeAccess.h | 86 +++ .../AVSCommon/Utils/Timing/TimePoint.h | 5 + .../AVSCommon/Utils/Timing/TimeUtils.h | 131 +++-- .../include/AVSCommon/Utils/functional/hash.h | 1 + .../src/Configuration/ConfigurationNode.cpp | 26 - .../LibcurlUtils/CurlEasyHandleWrapper.cpp | 170 ++---- AVSCommon/Utils/src/LibcurlUtils/HttpPost.cpp | 75 +-- .../LibCurlHttpContentFetcher.cpp | 45 +- AVSCommon/Utils/src/Logger/ConsoleLogger.cpp | 2 +- .../Utils/src/Logger/LogStringFormatter.cpp | 90 ++++ AVSCommon/Utils/src/Logger/LoggerUtils.cpp | 57 -- AVSCommon/Utils/src/SafeCTimeAccess.cpp | 62 +++ AVSCommon/Utils/src/StringUtils.cpp | 15 +- AVSCommon/Utils/src/TimePoint.cpp | 3 +- AVSCommon/Utils/src/TimeUtils.cpp | 96 ++-- AVSCommon/Utils/test/SafeTimeAccessTest.cpp | 219 ++++++++ AVSCommon/Utils/test/StringUtilsTest.cpp | 28 + AVSCommon/Utils/test/TimeUtilsTest.cpp | 72 ++- .../include/DefaultClient/DefaultClient.h | 49 +- .../DefaultClient/src/CMakeLists.txt | 3 +- .../DefaultClient/src/DefaultClient.cpp | 181 +++++-- .../src/ExampleAuthDelegateClient.cpp | 8 +- .../include/AuthDelegate/AuthDelegate.h | 4 +- AuthDelegate/src/AuthDelegate.cpp | 12 +- AuthDelegate/test/AuthDelegateTest.cpp | 16 +- CHANGELOG.md | 23 + CMakeLists.txt | 7 +- CapabilityAgents/AIP/include/AIP/ASRProfile.h | 4 + CapabilityAgents/AIP/include/AIP/Initiator.h | 2 + .../AIP/src/AudioInputProcessor.cpp | 61 ++- .../AIP/test/AudioInputProcessorTest.cpp | 50 +- .../Alerts/include/Alerts/Alert.h | 4 +- .../Alerts/include/Alerts/AlertScheduler.h | 10 +- .../include/Alerts/AlertsCapabilityAgent.h | 19 +- .../Alerts/include/Alerts/Renderer/Renderer.h | 19 +- .../Alerts/Storage/AlertStorageInterface.h | 74 +-- .../Alerts/Storage/SQLiteAlertStorage.h | 67 ++- CapabilityAgents/Alerts/src/Alert.cpp | 5 + .../Alerts/src/AlertScheduler.cpp | 20 +- .../Alerts/src/AlertsCapabilityAgent.cpp | 44 +- CapabilityAgents/Alerts/src/CMakeLists.txt | 5 +- .../Alerts/src/Renderer/Renderer.cpp | 89 ++-- .../Alerts/src/Storage/SQLiteAlertStorage.cpp | 488 ++++++++---------- .../Alerts/test/AlertSchedulerTest.cpp | 41 +- CapabilityAgents/Alerts/test/AlertTest.cpp | 6 +- .../Alerts/test/Renderer/RendererTest.cpp | 2 +- .../include/AudioPlayer/AudioItem.h | 6 + .../include/AudioPlayer/AudioPlayer.h | 4 + .../AudioPlayer/src/AudioPlayer.cpp | 30 +- .../AudioPlayer/test/AudioPlayerTest.cpp | 26 +- .../ExternalMediaPlayer/ExternalMediaPlayer.h | 77 +-- .../ExternalMediaPlayer/src/CMakeLists.txt | 10 +- .../src/ExternalMediaPlayer.cpp | 39 +- .../GadgetManager/src/CMakeLists.txt | 26 + .../GadgetManager/src/GadgetProtocol.cpp | 333 ++++++++++++ .../GadgetManager/test/GadgetProtocolTest.cpp | 343 ++++++++++++ .../GadgetManager/test/GenerateRandomVector.h | 52 ++ .../NotificationsCapabilityAgent.h | 15 +- .../NotificationsCapabilityAgentState.h | 3 + .../NotificationsStorageInterface.h | 39 +- .../SQLiteNotificationsStorage.h | 38 +- .../Notifications/src/CMakeLists.txt | 5 +- .../src/NotificationsCapabilityAgent.cpp | 61 ++- .../src/SQLiteNotificationsStorage.cpp | 217 ++++---- .../test/NotificationsCapabilityAgentTest.cpp | 90 +++- .../test/NotificationsStorageTest.cpp | 62 ++- .../include/Settings/SQLiteSettingStorage.h | 28 +- .../Settings/include/Settings/Settings.h | 24 +- .../Settings/SettingsStorageInterface.h | 31 +- CapabilityAgents/Settings/src/CMakeLists.txt | 5 +- .../Settings/src/SQLiteSettingStorage.cpp | 163 +++--- CapabilityAgents/Settings/src/Settings.cpp | 45 +- .../Settings/test/SettingsTest.cpp | 47 +- .../src/SpeechSynthesizer.cpp | 8 +- .../test/SpeechSynthesizerTest.cpp | 21 +- .../System/src/UserInactivityMonitor.cpp | 2 +- .../include/TemplateRuntime/TemplateRuntime.h | 143 ++++- .../TemplateRuntime/src/TemplateRuntime.cpp | 396 ++++++++++++-- .../test/TemplateRuntimeTest.cpp | 367 +++++++++---- .../include/CertifiedSender/CertifiedSender.h | 16 +- .../CertifiedSender/MessageStorageInterface.h | 29 +- .../CertifiedSender/SQLiteMessageStorage.h | 32 +- CertifiedSender/src/CMakeLists.txt | 3 +- CertifiedSender/src/CertifiedSender.cpp | 35 +- CertifiedSender/src/SQLiteMessageStorage.cpp | 133 ++--- CertifiedSender/test/CMakeLists.txt | 5 +- CertifiedSender/test/CertifiedSenderTest.cpp | 98 ++++ CertifiedSender/test/MessageStorageTest.cpp | 45 +- ContextManager/src/ContextManager.cpp | 13 +- ContextManager/test/ContextManagerTest.cpp | 44 ++ ESP/CMakeLists.txt | 6 + ESP/include/ESP/DummyESPDataProvider.h | 62 +++ ESP/include/ESP/ESPDataModifierInterface.h | 53 ++ ESP/include/ESP/ESPDataProvider.h | 118 +++++ ESP/include/ESP/ESPDataProviderInterface.h | 61 +++ ESP/src/CMakeLists.txt | 19 + ESP/src/DummyESPDataProvider.cpp | 51 ++ ESP/src/ESPDataProvider.cpp | 166 ++++++ Integration/AlexaClientSDKConfig.json | 23 + .../include/Integration/AuthObserver.h | 2 +- Integration/include/Integration/JsonHeader.h | 2 + .../inputs/recognize_long_timer_test.wav | Bin 269408 -> 307328 bytes Integration/inputs/recognize_timer_test.wav | Bin 273438 -> 311098 bytes .../inputs/recognize_very_long_timer_test.wav | Bin 277036 -> 314852 bytes .../inputs/utterance_time_success.opus | Bin 0 -> 28880 bytes Integration/src/AuthObserver.cpp | 2 +- Integration/src/CMakeLists.txt | 1 + Integration/test/AlertsIntegrationTest.cpp | 22 +- .../AudioInputProcessorIntegrationTest.cpp | 138 +++-- .../test/SpeechSynthesizerIntegrationTest.cpp | 2 +- KWD/CMakeLists.txt | 3 + MediaPlayer/include/MediaPlayer/MediaPlayer.h | 5 + MediaPlayer/src/BaseStreamSource.cpp | 16 +- MediaPlayer/src/MediaPlayer.cpp | 37 +- MediaPlayer/test/MediaPlayerTest.cpp | 17 +- NOTICE.txt | 11 +- .../UrlContentToAttachmentConverter.h | 16 +- .../src/UrlContentToAttachmentConverter.cpp | 106 +--- PlaylistParser/test/PlaylistParserTest.cpp | 4 +- README.md | 30 +- RegistrationManager/CMakeLists.txt | 7 + .../RegistrationManager/CustomerDataHandler.h | 72 +++ .../RegistrationManager/CustomerDataManager.h | 67 +++ .../RegistrationManager/RegistrationManager.h | 101 ++++ .../RegistrationObserverInterface.h | 44 ++ RegistrationManager/src/CMakeLists.txt | 12 + .../src/CustomerDataHandler.cpp | 52 ++ .../src/CustomerDataManager.cpp | 63 +++ .../src/RegistrationManager.cpp | 78 +++ RegistrationManager/test/CMakeLists.txt | 8 + .../test/CustomerDataManagerTest.cpp | 79 +++ .../test/RegistrationManagerTest.cpp | 134 +++++ SampleApp/include/SampleApp/ConsolePrinter.h | 6 + SampleApp/include/SampleApp/GuiRenderer.h | 10 +- .../include/SampleApp/InteractionManager.h | 22 +- SampleApp/include/SampleApp/KeywordObserver.h | 18 +- .../SampleApp/PortAudioMicrophoneWrapper.h | 9 + .../include/SampleApp/SampleApplication.h | 83 ++- SampleApp/include/SampleApp/UIManager.h | 20 + SampleApp/src/CMakeLists.txt | 36 +- SampleApp/src/ConsolePrinter.cpp | 4 +- SampleApp/src/GuiRenderer.cpp | 27 +- SampleApp/src/InteractionManager.cpp | 63 ++- SampleApp/src/KeywordObserver.cpp | 11 +- SampleApp/src/PortAudioMicrophoneWrapper.cpp | 72 ++- SampleApp/src/SampleApplication.cpp | 129 ++++- SampleApp/src/UIManager.cpp | 38 +- SampleApp/src/UserInputManager.cpp | 24 + Storage/SQLiteStorage/CMakeLists.txt | 1 + .../include/SQLiteStorage/SQLiteDatabase.h | 121 +++++ .../include/SQLiteStorage/SQLiteStatement.h | 7 - .../include/SQLiteStorage/SQLiteUtils.h | 15 +- Storage/SQLiteStorage/src/CMakeLists.txt | 3 +- Storage/SQLiteStorage/src/SQLiteDatabase.cpp | 142 +++++ Storage/SQLiteStorage/src/SQLiteStatement.cpp | 6 +- Storage/SQLiteStorage/src/SQLiteUtils.cpp | 41 +- Storage/SQLiteStorage/test/CMakeLists.txt | 7 + .../SQLiteStorage/test/SQLiteDatabaseTest.cpp | 148 ++++++ ThirdParty/rapidjson/CMakeLists.txt | 1 + build/BuildDefaults.cmake | 10 + build/cmake/BuildOptions.cmake | 2 +- build/cmake/ESP.cmake | 22 + build/cmake/KeywordDetector.cmake | 30 +- build/cmake/Platforms.cmake | 21 + tools/{RaspberryPi => Install}/config.txt | 0 tools/Install/mingw.sh | 67 +++ tools/Install/pi.sh | 76 +++ tools/{RaspberryPi => Install}/setup.sh | 152 +++--- 227 files changed, 9092 insertions(+), 2482 deletions(-) create mode 100644 AFML/include/AFML/ActivityTrackerInterface.h create mode 100644 AFML/include/AFML/AudioActivityTracker.h create mode 100644 AFML/include/AFML/VisualActivityTracker.h create mode 100644 AFML/src/AudioActivityTracker.cpp create mode 100644 AFML/src/VisualActivityTracker.cpp create mode 100644 AFML/test/AudioActivityTrackerTest.cpp create mode 100644 AFML/test/VisualActivityTrackerTest.cpp rename {CapabilityAgents/ExternalMediaPlayer/include => AVSCommon/AVS/include/AVSCommon/AVS}/ExternalMediaPlayer/AdapterUtils.h (92%) rename {CapabilityAgents/ExternalMediaPlayer/src => AVSCommon/AVS/src/ExternalMediaPlayer}/AdapterUtils.cpp (98%) create mode 100644 AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ExternalMediaPlayerInterface.h create mode 100644 AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerObserverInterface.h create mode 100644 AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManagerObserver.h create mode 100644 AVSCommon/Utils/include/AVSCommon/Utils/Logger/LogStringFormatter.h create mode 100644 AVSCommon/Utils/include/AVSCommon/Utils/SDKVersion.h create mode 100644 AVSCommon/Utils/include/AVSCommon/Utils/Timing/SafeCTimeAccess.h create mode 100644 AVSCommon/Utils/src/Logger/LogStringFormatter.cpp create mode 100644 AVSCommon/Utils/src/SafeCTimeAccess.cpp create mode 100644 AVSCommon/Utils/test/SafeTimeAccessTest.cpp create mode 100644 CapabilityAgents/GadgetManager/src/CMakeLists.txt create mode 100644 CapabilityAgents/GadgetManager/src/GadgetProtocol.cpp create mode 100644 CapabilityAgents/GadgetManager/test/GadgetProtocolTest.cpp create mode 100644 CapabilityAgents/GadgetManager/test/GenerateRandomVector.h create mode 100644 CertifiedSender/test/CertifiedSenderTest.cpp create mode 100644 ESP/CMakeLists.txt create mode 100644 ESP/include/ESP/DummyESPDataProvider.h create mode 100644 ESP/include/ESP/ESPDataModifierInterface.h create mode 100644 ESP/include/ESP/ESPDataProvider.h create mode 100644 ESP/include/ESP/ESPDataProviderInterface.h create mode 100644 ESP/src/CMakeLists.txt create mode 100644 ESP/src/DummyESPDataProvider.cpp create mode 100644 ESP/src/ESPDataProvider.cpp create mode 100644 Integration/inputs/utterance_time_success.opus create mode 100644 RegistrationManager/CMakeLists.txt create mode 100644 RegistrationManager/include/RegistrationManager/CustomerDataHandler.h create mode 100644 RegistrationManager/include/RegistrationManager/CustomerDataManager.h create mode 100644 RegistrationManager/include/RegistrationManager/RegistrationManager.h create mode 100644 RegistrationManager/include/RegistrationManager/RegistrationObserverInterface.h create mode 100644 RegistrationManager/src/CMakeLists.txt create mode 100644 RegistrationManager/src/CustomerDataHandler.cpp create mode 100644 RegistrationManager/src/CustomerDataManager.cpp create mode 100644 RegistrationManager/src/RegistrationManager.cpp create mode 100644 RegistrationManager/test/CMakeLists.txt create mode 100644 RegistrationManager/test/CustomerDataManagerTest.cpp create mode 100644 RegistrationManager/test/RegistrationManagerTest.cpp create mode 100644 Storage/SQLiteStorage/include/SQLiteStorage/SQLiteDatabase.h create mode 100644 Storage/SQLiteStorage/src/SQLiteDatabase.cpp create mode 100644 Storage/SQLiteStorage/test/CMakeLists.txt create mode 100644 Storage/SQLiteStorage/test/SQLiteDatabaseTest.cpp create mode 100644 build/cmake/ESP.cmake create mode 100644 build/cmake/Platforms.cmake rename tools/{RaspberryPi => Install}/config.txt (100%) create mode 100644 tools/Install/mingw.sh create mode 100644 tools/Install/pi.sh rename tools/{RaspberryPi => Install}/setup.sh (81%) diff --git a/ACL/include/ACL/Transport/HTTP2Stream.h b/ACL/include/ACL/Transport/HTTP2Stream.h index 7fc1f06952..0ddb72bf8f 100644 --- a/ACL/include/ACL/Transport/HTTP2Stream.h +++ b/ACL/include/ACL/Transport/HTTP2Stream.h @@ -24,6 +24,7 @@ #include #include +#include #include #include @@ -202,6 +203,14 @@ class HTTP2Stream { */ bool hasProgressTimedOut() const; + /** + * Return a reference to the LogStringFormatter owned by this object. This is to allow a callback that uses this + * object to get access to a known good LogStringFormatter. + * + * @return A reference to a LogStringFormatter. + */ + const avsCommon::utils::logger::LogStringFormatter& getLogFormatter() const; + private: /** * Configure the associated curl easy handle with options common to GET and POST @@ -273,6 +282,8 @@ class HTTP2Stream { std::atomic m_progressTimeout; /// Last time something was transferred. std::atomic m_timeOfLastTransfer; + /// Object to format log strings correctly. + avsCommon::utils::logger::LogStringFormatter m_logFormatter; }; template diff --git a/ACL/include/ACL/Transport/TransportObserverInterface.h b/ACL/include/ACL/Transport/TransportObserverInterface.h index 148f3916be..afea786da1 100644 --- a/ACL/include/ACL/Transport/TransportObserverInterface.h +++ b/ACL/include/ACL/Transport/TransportObserverInterface.h @@ -17,6 +17,8 @@ #define ALEXA_CLIENT_SDK_ACL_INCLUDE_ACL_TRANSPORT_TRANSPORTOBSERVERINTERFACE_H_ #include + +#include #include namespace alexaClientSDK { diff --git a/ACL/src/Transport/HTTP2Stream.cpp b/ACL/src/Transport/HTTP2Stream.cpp index b2a3b4d15a..a0d5533ce3 100644 --- a/ACL/src/Transport/HTTP2Stream.cpp +++ b/ACL/src/Transport/HTTP2Stream.cpp @@ -488,6 +488,10 @@ bool HTTP2Stream::hasProgressTimedOut() const { return (getNow() - m_timeOfLastTransfer) > m_progressTimeout; } +const avsCommon::utils::logger::LogStringFormatter& HTTP2Stream::getLogFormatter() const { + return m_logFormatter; +} + template bool HTTP2Stream::setopt(CURLoption option, const char* optionName, ParamType value) { auto result = curl_easy_setopt(m_transfer.getCurlHandle(), option, value); @@ -563,7 +567,8 @@ int HTTP2Stream::debugFunction(CURL* handle, curl_infotype type, char* data, siz return 0; } if (stream->m_streamLog) { - (*stream->m_streamLog) << logger::formatLogString( + auto logFormatter = stream->getLogFormatter(); + (*stream->m_streamLog) << logFormatter.format( logger::Level::INFO, std::chrono::system_clock::now(), logger::ThreadMoniker::getThisThreadMoniker().c_str(), diff --git a/ACL/test/Transport/MimeParserTest.cpp b/ACL/test/Transport/MimeParserTest.cpp index 3131323500..26d5e93435 100644 --- a/ACL/test/Transport/MimeParserTest.cpp +++ b/ACL/test/Transport/MimeParserTest.cpp @@ -315,5 +315,10 @@ TEST_F(MimeParserTest, testDuplicateBounaries) { int main(int argc, char** argv) { ::testing::InitGoogleTest(&argc, argv); + +// ACSDK-1051 - MimeParser tests with attachments fail on Windows +#if defined(_WIN32) && !defined(RESOLVED_ACSDK_1051) + ::testing::GTEST_FLAG(filter) = "-MimeParserTest*Attachment*"; +#endif return RUN_ALL_TESTS(); } diff --git a/ADSL/include/ADSL/DirectiveProcessor.h b/ADSL/include/ADSL/DirectiveProcessor.h index f4c0d39ed8..224723b3a3 100644 --- a/ADSL/include/ADSL/DirectiveProcessor.h +++ b/ADSL/include/ADSL/DirectiveProcessor.h @@ -93,6 +93,19 @@ class DirectiveProcessor { */ void shutdown(); + /** + * Disable the DirectiveProcessor, queues all outstanding @c AVSDirectives for cancellation and + * blocks until the processing of all @c AVSDirectives has completed. + */ + void disable(); + + /** + * Enable the DirectiveProcessor. + * + * @return Whether it succeeded to enable the directive processor. + */ + bool enable(); + private: /** * Handle used to identify @c DirectiveProcessor instances referenced by @c DirectiveHandlerResult. @@ -222,6 +235,9 @@ class DirectiveProcessor { /// Whether or not the @c DirectiveProcessor is shutting down. bool m_isShuttingDown; + /// Whether or not the @c DirectiveProcessor is enabled. + bool m_isEnabled; + /// The current @c dialogRequestId std::string m_dialogRequestId; diff --git a/ADSL/include/ADSL/DirectiveSequencer.h b/ADSL/include/ADSL/DirectiveSequencer.h index 3befc830c0..995e7fc0f0 100644 --- a/ADSL/include/ADSL/DirectiveSequencer.h +++ b/ADSL/include/ADSL/DirectiveSequencer.h @@ -54,6 +54,10 @@ class DirectiveSequencer : public avsCommon::sdkInterfaces::DirectiveSequencerIn bool onDirective(std::shared_ptr directive) override; + void disable() override; + + void enable() override; + private: /** * Constructor. @@ -93,6 +97,9 @@ class DirectiveSequencer : public avsCommon::sdkInterfaces::DirectiveSequencerIn /// Whether or not the @c DirectiveReceiver is shutting down. bool m_isShuttingDown; + /// Whether or not the @c DirectiveSequencer is enabled. + bool m_isEnabled; + /// Object used to route directives to their assigned handler. DirectiveRouter m_directiveRouter; diff --git a/ADSL/src/DirectiveProcessor.cpp b/ADSL/src/DirectiveProcessor.cpp index cd9cc497af..e5b8fc5a66 100644 --- a/ADSL/src/DirectiveProcessor.cpp +++ b/ADSL/src/DirectiveProcessor.cpp @@ -47,6 +47,7 @@ std::unordered_map Dir DirectiveProcessor::DirectiveProcessor(DirectiveRouter* directiveRouter) : m_directiveRouter{directiveRouter}, m_isShuttingDown{false}, + m_isEnabled{true}, m_isHandlingDirective{false} { std::lock_guard lock(m_handleMapMutex); m_handle = ++m_nextProcessorHandle; @@ -70,11 +71,11 @@ bool DirectiveProcessor::onDirective(std::shared_ptr directive) { } std::lock_guard onDirectiveLock(m_onDirectiveMutex); std::unique_lock lock(m_mutex); - if (m_isShuttingDown) { + if (m_isShuttingDown || !m_isEnabled) { ACSDK_WARN(LX("onDirectiveFailed") .d("messageId", directive->getMessageId()) .d("action", "ignored") - .d("reason", "shuttingDown")); + .d("reason", m_isShuttingDown ? "shuttingDown" : "disabled")); return false; } if (!directive->getDialogRequestId().empty() && directive->getDialogRequestId() != m_dialogRequestId) { @@ -118,6 +119,20 @@ void DirectiveProcessor::shutdown() { } } +void DirectiveProcessor::disable() { + std::lock_guard lock(m_mutex); + ACSDK_DEBUG(LX("disable")); + queueAllDirectivesForCancellationLocked(); + m_isEnabled = false; + m_wakeProcessingLoop.notify_one(); +} + +bool DirectiveProcessor::enable() { + std::lock_guard lock{m_mutex}; + m_isEnabled = true; + return m_isEnabled; +} + DirectiveProcessor::DirectiveHandlerResult::DirectiveHandlerResult( DirectiveProcessor::ProcessorHandle processorHandle, std::shared_ptr directive) : diff --git a/ADSL/src/DirectiveSequencer.cpp b/ADSL/src/DirectiveSequencer.cpp index 23390795b0..d9e09f44f7 100644 --- a/ADSL/src/DirectiveSequencer.cpp +++ b/ADSL/src/DirectiveSequencer.cpp @@ -68,11 +68,11 @@ bool DirectiveSequencer::onDirective(std::shared_ptr directive) { return false; } std::lock_guard lock(m_mutex); - if (m_isShuttingDown) { + if (m_isShuttingDown || !m_isEnabled) { ACSDK_WARN(LX("onDirectiveFailed") .d("directive", directive->getHeaderAsString()) .d("action", "ignored") - .d("reason", "isShuttingDown")); + .d("reason", m_isShuttingDown ? "isShuttingDown" : "disabled")); return false; } ACSDK_INFO(LX("onDirective").d("directive", directive->getHeaderAsString())); @@ -86,7 +86,8 @@ DirectiveSequencer::DirectiveSequencer( DirectiveSequencerInterface{"DirectiveSequencer"}, m_mutex{}, m_exceptionSender{exceptionSender}, - m_isShuttingDown{false} { + m_isShuttingDown{false}, + m_isEnabled{true} { m_directiveProcessor = std::make_shared(&m_directiveRouter); m_receivingThread = std::thread(&DirectiveSequencer::receivingLoop, this); } @@ -106,6 +107,23 @@ void DirectiveSequencer::doShutdown() { m_exceptionSender.reset(); } +void DirectiveSequencer::disable() { + ACSDK_DEBUG9(LX("disable")); + std::lock_guard lock(m_mutex); + m_isEnabled = false; + m_directiveProcessor->setDialogRequestId(""); + m_directiveProcessor->disable(); + m_wakeReceivingLoop.notify_one(); +} + +void DirectiveSequencer::enable() { + ACSDK_DEBUG9(LX("disable")); + std::lock_guard lock(m_mutex); + m_isEnabled = true; + m_directiveProcessor->enable(); + m_wakeReceivingLoop.notify_one(); +} + void DirectiveSequencer::receivingLoop() { auto wake = [this]() { return !m_receivingQueue.empty() || m_isShuttingDown; }; diff --git a/ADSL/test/ADSL/MockDirectiveSequencer.h b/ADSL/test/ADSL/MockDirectiveSequencer.h index dd545e5096..69c6645dad 100644 --- a/ADSL/test/ADSL/MockDirectiveSequencer.h +++ b/ADSL/test/ADSL/MockDirectiveSequencer.h @@ -46,6 +46,10 @@ class MockDirectiveSequencer : public avsCommon::sdkInterfaces::DirectiveSequenc MOCK_METHOD1(setDialogRequestId, void(const std::string& dialogRequestId)); MOCK_METHOD1(onDirective, bool(std::shared_ptr directive)); + + MOCK_METHOD0(disable, void()); + + MOCK_METHOD0(enable, void()); }; inline MockDirectiveSequencer::MockDirectiveSequencer() : diff --git a/ADSL/test/DirectiveProcessorTest.cpp b/ADSL/test/DirectiveProcessorTest.cpp index 46863a3d16..c09dcc2fbb 100644 --- a/ADSL/test/DirectiveProcessorTest.cpp +++ b/ADSL/test/DirectiveProcessorTest.cpp @@ -305,6 +305,19 @@ TEST_F(DirectiveProcessorTest, testSetDialogRequestIdCancelsOutstandingDirective ASSERT_TRUE(handler2->waitUntilCompleted()); } +TEST_F(DirectiveProcessorTest, testAddDirectiveWhileDisabled) { + m_processor->disable(); + ASSERT_FALSE(m_processor->onDirective(m_directive_0_0)); +} + +TEST_F(DirectiveProcessorTest, testAddDirectiveAfterReEnabled) { + m_processor->disable(); + ASSERT_FALSE(m_processor->onDirective(m_directive_0_0)); + + m_processor->enable(); + ASSERT_TRUE(m_processor->onDirective(m_directive_0_0)); +} + } // namespace test } // namespace adsl } // namespace alexaClientSDK diff --git a/ADSL/test/DirectiveSequencerTest.cpp b/ADSL/test/DirectiveSequencerTest.cpp index 8fbc8fc8e8..5dbd00d7a5 100644 --- a/ADSL/test/DirectiveSequencerTest.cpp +++ b/ADSL/test/DirectiveSequencerTest.cpp @@ -787,6 +787,110 @@ TEST_F(DirectiveSequencerTest, testHandleBlockingThenImmediatelyThenNonBockingOn ASSERT_TRUE(handler2->waitUntilCompleted()); } +/** + * Check that the @ DirectiveSequencer does not handle directives when it is disabled + */ +TEST_F(DirectiveSequencerTest, testAddDirectiveAfterDisabled) { + auto avsMessageHeader = + std::make_shared(NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK, MESSAGE_ID_0, DIALOG_REQUEST_ID_0); + std::shared_ptr directive = AVSDirective::create( + UNPARSED_DIRECTIVE, avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, TEST_ATTACHMENT_CONTEXT_ID); + + DirectiveHandlerConfiguration handlerConfig; + handlerConfig[{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}] = BlockingPolicy::BLOCKING; + auto handler = MockDirectiveHandler::create(handlerConfig, LONG_HANDLING_TIME_MS); + + EXPECT_CALL(*handler, handleDirectiveImmediately(_)).Times(0); + EXPECT_CALL(*handler, preHandleDirective(directive, _)).Times(0); + EXPECT_CALL(*handler, handleDirective(MESSAGE_ID_2)).Times(0); + EXPECT_CALL(*handler, cancelDirective(_)).Times(0); + + ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler)); + m_sequencer->disable(); + m_sequencer->setDialogRequestId(DIALOG_REQUEST_ID_0); + ASSERT_FALSE(m_sequencer->onDirective(directive)); + + // Tear down method expects the sequencer to be enabled + m_sequencer->enable(); +} + +/** + * Check that the @ DirectiveSequencer.disable() cancel directive being handled + */ +TEST_F(DirectiveSequencerTest, testDisableCancelsDirective) { + auto avsMessageHeader = + std::make_shared(NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK, MESSAGE_ID_0, DIALOG_REQUEST_ID_0); + std::shared_ptr directive = AVSDirective::create( + UNPARSED_DIRECTIVE, avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, TEST_ATTACHMENT_CONTEXT_ID); + + DirectiveHandlerConfiguration handlerConfig; + handlerConfig[{NAMESPACE_SPEECH_SYNTHESIZER, NAME_SPEAK}] = BlockingPolicy::BLOCKING; + auto handler = MockDirectiveHandler::create(handlerConfig, LONG_HANDLING_TIME_MS); + + EXPECT_CALL(*handler, handleDirectiveImmediately(_)).Times(0); + EXPECT_CALL(*handler, preHandleDirective(directive, _)).Times(1); + EXPECT_CALL(*handler, handleDirective(MESSAGE_ID_0)).Times(AtMost(1)); + EXPECT_CALL(*handler, cancelDirective(_)).Times(1); + + // Add directive and wait till prehandling + ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler)); + m_sequencer->setDialogRequestId(DIALOG_REQUEST_ID_0); + ASSERT_TRUE(m_sequencer->onDirective(directive)); + ASSERT_TRUE(handler->waitUntilPreHandling()); + + // Disable + m_sequencer->disable(); + ASSERT_TRUE(handler->waitUntilCanceling()); + + // Tear down method expects the sequencer to be enabled + m_sequencer->enable(); +} + +/** + * Check that the @ DirectiveSequencer can handle directives after being re-enabled + */ +TEST_F(DirectiveSequencerTest, testAddDirectiveAfterReEnabled) { + auto avsMessageHeader0 = + std::make_shared(NAMESPACE_AUDIO_PLAYER, NAME_PLAY, MESSAGE_ID_0, DIALOG_REQUEST_ID_0); + std::shared_ptr directive0 = AVSDirective::create( + UNPARSED_DIRECTIVE, avsMessageHeader0, PAYLOAD_TEST, m_attachmentManager, TEST_ATTACHMENT_CONTEXT_ID); + auto avsMessageHeader1 = + std::make_shared(NAMESPACE_AUDIO_PLAYER, NAME_PLAY, MESSAGE_ID_1, DIALOG_REQUEST_ID_1); + std::shared_ptr ignoredDirective1 = AVSDirective::create( + "ignoreDirective", avsMessageHeader1, PAYLOAD_TEST, m_attachmentManager, TEST_ATTACHMENT_CONTEXT_ID); + auto avsMessageHeader2 = + std::make_shared(NAMESPACE_AUDIO_PLAYER, NAME_PLAY, MESSAGE_ID_2, DIALOG_REQUEST_ID_2); + std::shared_ptr ignoredDirective2 = AVSDirective::create( + "anotherIgnored", avsMessageHeader2, PAYLOAD_TEST, m_attachmentManager, TEST_ATTACHMENT_CONTEXT_ID); + + DirectiveHandlerConfiguration handlerConfig; + handlerConfig[{NAMESPACE_AUDIO_PLAYER, NAME_PLAY}] = BlockingPolicy::NON_BLOCKING; + auto handler = MockDirectiveHandler::create(handlerConfig); + + // No handle calls are expected + EXPECT_CALL(*handler, handleDirectiveImmediately(_)).Times(0); + EXPECT_CALL(*handler, preHandleDirective(_, _)).Times(0); + EXPECT_CALL(*handler, handleDirective(_)).Times(0); + EXPECT_CALL(*handler, cancelDirective(_)).Times(0); + + // Except for the ones handling the directive0 + EXPECT_CALL(*handler, preHandleDirective(directive0, _)).Times(1); + EXPECT_CALL(*handler, handleDirective(MESSAGE_ID_0)).Times(1); + + ASSERT_TRUE(m_sequencer->addDirectiveHandler(handler)); + m_sequencer->disable(); + + // Make sure these directives are ignored and never processed + ASSERT_FALSE(m_sequencer->onDirective(ignoredDirective1)); + ASSERT_FALSE(m_sequencer->onDirective(ignoredDirective2)); + + m_sequencer->enable(); + m_sequencer->setDialogRequestId(DIALOG_REQUEST_ID_0); + + ASSERT_TRUE(m_sequencer->onDirective(directive0)); + ASSERT_TRUE(handler->waitUntilCompleted()); +} + } // namespace test } // namespace adsl } // namespace alexaClientSDK diff --git a/AFML/include/AFML/ActivityTrackerInterface.h b/AFML/include/AFML/ActivityTrackerInterface.h new file mode 100644 index 0000000000..2c2a689fa9 --- /dev/null +++ b/AFML/include/AFML/ActivityTrackerInterface.h @@ -0,0 +1,47 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_ACTIVITYTRACKERINTERFACE_H_ +#define ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_ACTIVITYTRACKERINTERFACE_H_ + +#include + +#include "AFML/Channel.h" + +namespace alexaClientSDK { +namespace afml { + +/** + * This @c ActivityTrackerInterface interface is used by the @c FocusManager to notify its activity tracker + * any focus updates to a vector of @c Channel::State due to @c acquireChannel(), @c releaseChannel() or + * stopForegroundActivity(). + */ +class ActivityTrackerInterface { +public: + /// Destructor. + virtual ~ActivityTrackerInterface() = default; + + /** + * This function is called whenever an activity in @c FocusManager causes updates to a vector of channels. + * + * @param channelStates A vector of @c Channel::State that has been updated. + */ + virtual void notifyOfActivityUpdates(const std::vector& channelStates) = 0; +}; + +} // namespace afml +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_ACTIVITYTRACKERINTERFACE_H_ diff --git a/AFML/include/AFML/AudioActivityTracker.h b/AFML/include/AFML/AudioActivityTracker.h new file mode 100644 index 0000000000..f1c1d99ed2 --- /dev/null +++ b/AFML/include/AFML/AudioActivityTracker.h @@ -0,0 +1,135 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_AUDIOACTIVITYTRACKER_H_ +#define ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_AUDIOACTIVITYTRACKER_H_ + +#include +#include +#include +#include +#include + +#include +#include +#include +#include + +#include "AFML/Channel.h" +#include "AFML/ActivityTrackerInterface.h" + +namespace alexaClientSDK { +namespace afml { + +/** + * The @c AudioActivityTracker implements the @c ActivityTrackerInterface and gets notification from the @c + * FocusManager any activities in the audio channels. It also implements the @c StateProviderInterface and will provide + * to AVS the activity of the audio channels as described in Focus Management. + */ +class AudioActivityTracker + : public avsCommon::utils::RequiresShutdown + , public ActivityTrackerInterface + , public avsCommon::sdkInterfaces::StateProviderInterface { +public: + /** + * Creates a new @c AudioActivityTracker instance. + * + * @param contextManager The AVS Context manager used to generate system context for events. + * @return A @c std::shared_ptr to the new @c AudioActivityTracker instance, or @c nullptr if the operation failed. + */ + static std::shared_ptr create( + std::shared_ptr contextManager); + + /// @name StateProviderInterface Functions + /// @{ + void provideState(const avsCommon::avs::NamespaceAndName& stateProviderName, unsigned int stateRequestToken) + override; + /// @} + + /// @name ActivityTrackerInterface Functions + /// @{ + void notifyOfActivityUpdates(const std::vector& channelStates) override; + /// @} + +private: + /** + * Constructor. + * + * @param contextManager The AVS Context manager used to generate system context for events. + */ + AudioActivityTracker(std::shared_ptr contextManager); + + /// @name RequiresShutdown Functions + /// @{ + void doShutdown() override; + /// @} + + /** + * This function processes the vector of @c Channel::State and stores them inside @c m_channelsContext. + * + * @param channelStates The vector of @c Channel::State that has been updated as notified by the FocusManager via + * the @c notifyOfActivityUpdates() callback. + */ + void executeNotifyOfActivityUpdates(const std::vector& channelStates); + + /** + * This function provides updated context information for AudioActivityTracker to @c ContextManager. This function + * is called when @c ContextManager calls @c provideState(). + * + * @param stateRequestToken The token @c ContextManager passed to the @c provideState() call, which will be passed + * along to the ContextManager::setState() call. + */ + void executeProvideState(unsigned int stateRequestToken); + + /** + * This function returns the channelName passed in to lower case. + * + * @param channelName The name of the @c Channel. + * @return The name of the @c Channel in lower case. + */ + const std::string& executeChannelNameInLowerCase(const std::string& channelName); + + /// The @c ContextManager used to generate system context for events. + std::shared_ptr m_contextManager; + + /** + * @name Executor Thread Variables + * + * These member variables are only accessed by functions in the @c m_executor worker thread, and do not require any + * synchronization. + */ + /// @{ + /// A map to store the @c Channel::State to all the audio channels. The key of this map is the name of the @c + /// Channel. + std::unordered_map m_channelStates; + + /// A map to store the channel name in lower cases as required by the AudioActivity context. The key of this map is + /// the name of the @c Channel. + std::unordered_map m_channelNamesInLowerCase; + /// @} + + /** + * @c Executor which queues up operations from asynchronous API calls. + * + * @note This declaration needs to come *after* the Executor Thread Variables above so that the thread shuts down + * before the Executor Thread Variables are destroyed. + */ + avsCommon::utils::threading::Executor m_executor; +}; + +} // namespace afml +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_AUDIOACTIVITYTRACKER_H_ diff --git a/AFML/include/AFML/Channel.h b/AFML/include/AFML/Channel.h index 1395019771..57d1cc7889 100644 --- a/AFML/include/AFML/Channel.h +++ b/AFML/include/AFML/Channel.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_CHANNEL_H_ #define ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_CHANNEL_H_ +#include #include #include @@ -31,12 +32,47 @@ namespace afml { */ class Channel { public: + /* + * This class contains the states of the @c Channel. The states inside this structure are intended to be shared via + * the @c ActivityTrackerInterface. + */ + struct State { + /// Constructor with @c Channel name as the parameter. + State(const std::string& name); + + /// Constructor. + State(); + + /* + * The channel's name. Although the name is not dynamic, it is useful for identifying which channel the state + * belongs to. + */ + std::string name; + + /// The current Focus of the Channel. + avsCommon::avs::FocusState focusState; + + /// The name of the AVS interface that is occupying the Channel. + std::string interfaceName; + + /// Time at which the channel goes to NONE focus. + std::chrono::steady_clock::time_point timeAtIdle; + }; + /** * Constructs a new Channel object with the provided priority. * + * @param name The channel's name. * @param priority The priority of the channel. */ - Channel(const unsigned int priority); + Channel(const std::string& name, const unsigned int priority); + + /** + * Returns the name of a channel. + * + * @return The channel's name. + */ + const std::string& getName() const; /** * Returns the priority of a Channel. @@ -51,45 +87,45 @@ class Channel { * @c NONE, the observer will be removed from the Channel. * * @param focus The focus of the Channel. + * @return @c true if focus changed, else @c false. */ - void setFocus(avsCommon::avs::FocusState focus); + bool setFocus(avsCommon::avs::FocusState focus); /** - * Sets a new observer and notifies the old observer, if there is one, that it lost focus. + * Sets a new observer. * * @param observer The observer of the Channel. */ void setObserver(std::shared_ptr observer); /** - * Compares this Channel and another Channel and checks which is higher priority. A Channel is considered higher - * priority than another Channel if its m_priority is lower than the other Channel's. + * Checks whether the Channel has an observer. * - * @param rhs The Channel to compare with this Channel. + * @return @c true if the Channel has an observer, else @c false. */ - bool operator>(const Channel& rhs) const; + bool hasObserver() const; /** - * Updates the Channel's activity id. + * Compares this Channel and another Channel and checks which is higher priority. A Channel is considered higher + * priority than another Channel if its m_priority is lower than the other Channel's. * - * @param activityId The activity id of the Channel. + * @param rhs The Channel to compare with this Channel. */ - void setActivityId(const std::string& activityId); + bool operator>(const Channel& rhs) const; /** - * Returns the activity id of the Channel. + * Updates the AVS interface occupying the Channel. * - * @return The Channel activity id. + * @param interface The name of the interface occupying the Channel. */ - std::string getActivityId() const; + void setInterface(const std::string& interface); /** - * Notifies the Channel's observer to stop if the @c activityId matches the Channel's activity id. + * Returns the name of the AVS interface occupying the Channel. * - * @param activityId The activity id to compare. - * @return @c true if the activity on the Channel was stopped and @c false otherwise. + * @return The name of the AVS interface. */ - bool stopActivity(const std::string& activityId); + std::string getInterface() const; /** * Checks whether the observer passed in currently owns the Channel. @@ -99,18 +135,22 @@ class Channel { */ bool doesObserverOwnChannel(std::shared_ptr observer) const; + /** + * Returns the @c State of the @c Channel. + * + * @return The @c State. + */ + Channel::State getState() const; + private: /// The priority of the Channel. const unsigned int m_priority; - /// The current Focus of the Channel. - avsCommon::avs::FocusState m_focusState; + /// The @c State of the @c Channel. + State m_state; /// The current observer of the Channel. std::shared_ptr m_observer; - - /// An identifier which should be unique to any activity that can occur on any Channel. - std::string m_currentActivityId; }; } // namespace afml diff --git a/AFML/include/AFML/FocusManager.h b/AFML/include/AFML/FocusManager.h index 736de29f26..cac2aa2a89 100644 --- a/AFML/include/AFML/FocusManager.h +++ b/AFML/include/AFML/FocusManager.h @@ -19,12 +19,14 @@ #include #include #include +#include #include #include #include #include "AFML/Channel.h" +#include "AFML/ActivityTrackerInterface.h" #include "AVSCommon/Utils/Threading/Executor.h" namespace alexaClientSDK { @@ -36,12 +38,12 @@ namespace afml { * operations are provided: * * acquire Channel - clients should call the acquireChannel() method, passing in the name of the Channel they wish to - * acquire, a pointer to the observer that they want to be notified once they get focus, and a unique activity id. + * acquire, a pointer to the observer that they want to be notified once they get focus, and a unique interface name. * * release Channel - clients should call the releaseChannel() method, passing in the name of the Channel and the * observer of the Channel they wish to release. * - * stop foreground Channel - clients should call the stopForegroundActivitiy() method. + * stop foreground Channel - clients should call the stopForegroundActivity() method. * * All of these methods will notify the observer of the Channel of focus changes via an asynchronous callback to the * ChannelObserverInterface##onFocusChanged() method, at which point the client should make a user observable change @@ -82,24 +84,29 @@ class FocusManager : public avsCommon::sdkInterfaces::FocusManagerInterface { unsigned int priority; }; + /// The default @c ChannelConfiguration for AVS audio channels. + static const std::vector DEFAULT_AUDIO_CHANNELS; + + /// The default @c ChannelConfiguration for AVS visual channels. + static const std::vector DEFAULT_VISUAL_CHANNELS; + /** - * This constructor creates Channels based on the provided configurations. This is defaulted to the default - * AVS Channel configuration names and priorities if no input parameter is provided. + * This constructor creates Channels based on the provided configurations. * * @param channelConfigurations A vector of @c channelConfiguration objects that will be used to create the * Channels. No two Channels should have the same name or priority. If there are multiple configurations with the * same name or priority, the latter Channels with that name or priority will not be created. + * @param activityTrackerInterface The interface to notify the activity tracker a vector of channel states that has + * been updated. */ FocusManager( - const std::vector& channelConfigurations = { - {DIALOG_CHANNEL_NAME, DIALOG_CHANNEL_PRIORITY}, - {ALERTS_CHANNEL_NAME, ALERTS_CHANNEL_PRIORITY}, - {CONTENT_CHANNEL_NAME, CONTENT_CHANNEL_PRIORITY}}); + const std::vector& channelConfigurations, + std::shared_ptr activityTrackerInterface = nullptr); bool acquireChannel( const std::string& channelName, std::shared_ptr channelObserver, - const std::string& activityId) override; + const std::string& interface) override; std::future releaseChannel( const std::string& channelName, @@ -107,6 +114,11 @@ class FocusManager : public avsCommon::sdkInterfaces::FocusManagerInterface { void stopForegroundActivity() override; + void addObserver(const std::shared_ptr& observer) override; + + void removeObserver( + const std::shared_ptr& observer) override; + private: /** * Functor so that we can compare Channel objects via shared_ptr. @@ -125,18 +137,26 @@ class FocusManager : public avsCommon::sdkInterfaces::FocusManagerInterface { } }; + /** + * Sets the @c FocusState for @c channel and notifies observers of the change. + * + * @param channel The @c Channel to set the @c FocusState for. + * @param focus The @c FocusState to set @c channel to. + */ + void setChannelFocus(const std::shared_ptr& channel, avsCommon::avs::FocusState focus); + /** * Grants access to the Channel specified and updates other Channels as needed. This function provides the full * implementation which the public method will call. * * @param channelToAcquire The Channel to acquire. * @param channelObserver The new observer of the Channel. - * @param activityId The id of the new activity on the Channel. + * @param interface The name of the AVS inferface on the Channel. */ void acquireChannelHelper( std::shared_ptr channelToAcquire, std::shared_ptr channelObserver, - const std::string& activityId); + const std::string& interface); /** * Releases the Channel specified and updates other Channels as needed. This function provides the full @@ -154,15 +174,16 @@ class FocusManager : public avsCommon::sdkInterfaces::FocusManagerInterface { const std::string& channelName); /** - * Stops the Channel specified and updates other Channels as needed if the activity id passed in matches the - * activity id of the Channel. This function provides the full implementation which the public method will call. + * Stops the Channel specified and updates other Channels as needed if the interface name passed in matches the + * interface occupying the Channel. This function provides the full implementation which the public method will + * call. * * @param foregroundChannel The Channel to stop. - * @param foregroundChannelActivityId The id of the activity to stop. + * @param foregroundChannelInterface The name of the interface to stop. */ void stopForegroundActivityHelper( std::shared_ptr foregroundChannel, - std::string foregroundChannelActivityId); + std::string foregroundChannelInterface); /** * Finds the channel from the given channel name. @@ -210,15 +231,34 @@ class FocusManager : public avsCommon::sdkInterfaces::FocusManagerInterface { */ void foregroundHighestPriorityActiveChannel(); + /** + * This function is called inside the executor context of @c FocusManager to notify its activityTracker of updates + * to a set of @c Channel via the @c ActivityTrackerInterface interface. + */ + void notifyActivityTracker(); + /// Map of channel names to shared_ptrs of Channel objects and contains every channel. std::unordered_map> m_allChannels; /// Set of currently observed Channels ordered by Channel priority. std::set, ChannelPtrComparator> m_activeChannels; - /// Mutex used to lock m_activeChannels and Channels' activity ids. + /// The set of observers to notify about focus changes. + std::unordered_set> m_observers; + + /// Mutex used to lock m_activeChannels, m_observers and Channels' interface name. std::mutex m_mutex; + /* + * A vector of channel's State that has been updated due to @c acquireChannel(), @c releaseChannel() or + * stopForegroundActivity(). This is accessed by functions in the @c m_executor worker thread, and do not require + * any synchronization. + */ + std::vector m_activityUpdates; + + /// The interface to notify its activity tracker of any changes to its channels. + std::shared_ptr m_activityTracker; + /** * @c Executor which queues up operations from asynchronous API calls. * diff --git a/AFML/include/AFML/VisualActivityTracker.h b/AFML/include/AFML/VisualActivityTracker.h new file mode 100644 index 0000000000..1494188941 --- /dev/null +++ b/AFML/include/AFML/VisualActivityTracker.h @@ -0,0 +1,112 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_VISUALACTIVITYTRACKER_H_ +#define ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_VISUALACTIVITYTRACKER_H_ + +#include +#include +#include + +#include +#include +#include +#include + +#include "AFML/Channel.h" +#include "AFML/ActivityTrackerInterface.h" + +namespace alexaClientSDK { +namespace afml { + +/** + * The @c VisualActivityTracker implements the @c ActivityTrackerInterface and gets notification from the @c + * FocusManager any activities in the visual channels. It also implements the @c StateProviderInterface and will + * provide to AVS the activity of the visual channels as described in Focus Management. + */ +class VisualActivityTracker + : public avsCommon::utils::RequiresShutdown + , public ActivityTrackerInterface + , public avsCommon::sdkInterfaces::StateProviderInterface { +public: + /** + * Creates a new @c VisualActivityTracker instance. + * + * @param contextManager The AVS Context manager used to generate system context for events. + * @return A @c std::shared_ptr to the new @c VisualActivityTracker instance, or @c nullptr if the operation failed. + */ + static std::shared_ptr create( + std::shared_ptr contextManager); + + /// @name StateProviderInterface Functions + /// @{ + void provideState(const avsCommon::avs::NamespaceAndName& stateProviderName, unsigned int stateRequestToken) + override; + /// @} + + /// @name ActivityTrackerInterface Functions + /// @{ + void notifyOfActivityUpdates(const std::vector& channelStates) override; + /// @} + +private: + /** + * Constructor. + * + * @param contextManager The AVS Context manager used to generate system context for events. + */ + VisualActivityTracker(std::shared_ptr contextManager); + + /// @name RequiresShutdown Functions + /// @{ + void doShutdown() override; + /// @} + + /** + * This function provides updated context information for VisualActivityTracker to @c ContextManager. This function + * is called when @c ContextManager calls @c provideState(). + * + * @param stateRequestToken The token @c ContextManager passed to the @c provideState() call, which will be passed + * along to the ContextManager::setState() call. + */ + void executeProvideState(unsigned int stateRequestToken); + + /// The @c ContextManager used to generate system context for events. + std::shared_ptr m_contextManager; + + /** + * @name Executor Thread Variables + * + * These member variables are only accessed by functions in the @c m_executor worker thread, and do not require any + * synchronization. + */ + /// @{ + /// Stores the @c Channel::State to the visual channels. + Channel::State m_channelState; + /// @} + + /** + * @c Executor which queues up operations from asynchronous API calls. + * + * @note This declaration needs to come *after* the Executor Thread Variables above so that the thread shuts down + * before the Executor Thread Variables are destroyed. + */ + avsCommon::utils::threading::Executor m_executor; +}; + +} // namespace afml +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AFML_INCLUDE_AFML_VISUALACTIVITYTRACKER_H_ diff --git a/AFML/src/AudioActivityTracker.cpp b/AFML/src/AudioActivityTracker.cpp new file mode 100644 index 0000000000..8fd84279ad --- /dev/null +++ b/AFML/src/AudioActivityTracker.cpp @@ -0,0 +1,161 @@ +/* + * Copyright 2018 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. + */ +#include + +#include +#include +#include + +#include +#include +#include + +#include "AFML/AudioActivityTracker.h" + +namespace alexaClientSDK { +namespace afml { + +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::utils; +using namespace avsCommon::avs; + +/// String to identify log entries originating from this file. +static const std::string TAG("AudioActivityTracker"); + +/** + * 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 state information @c NamespaceAndName to send to the @c ContextManager for AudioActivityTracker. +static const NamespaceAndName CONTEXT_MANAGER_STATE{"AudioActivityTracker", "ActivityState"}; + +/// The idleTime key used in the AudioActivityTracker context. +static const char IDLE_TIME_KEY[] = "idleTimeInMilliseconds"; + +/// The interface key used in the AudioActivityTracker context. +static const char INTERFACE_KEY[] = "interface"; + +std::shared_ptr AudioActivityTracker::create( + std::shared_ptr contextManager) { + if (!contextManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullContextManager")); + return nullptr; + } + + auto audioActivityTracker = std::shared_ptr(new AudioActivityTracker(contextManager)); + + contextManager->setStateProvider(CONTEXT_MANAGER_STATE, audioActivityTracker); + + return audioActivityTracker; +} + +void AudioActivityTracker::provideState( + const avsCommon::avs::NamespaceAndName& stateProviderName, + unsigned int stateRequestToken) { + ACSDK_DEBUG5(LX("provideState")); + m_executor.submit([this, stateRequestToken]() { executeProvideState(stateRequestToken); }); +} + +void AudioActivityTracker::notifyOfActivityUpdates(const std::vector& channelStates) { + ACSDK_DEBUG5(LX("notifyOfActivityUpdates")); + m_executor.submit([this, channelStates]() { executeNotifyOfActivityUpdates(channelStates); }); +} + +AudioActivityTracker::AudioActivityTracker( + std::shared_ptr contextManager) : + RequiresShutdown{"AudioActivityTracker"}, + m_contextManager{contextManager} { +} + +void AudioActivityTracker::doShutdown() { + m_executor.shutdown(); + m_contextManager.reset(); +} + +void AudioActivityTracker::executeNotifyOfActivityUpdates(const std::vector& channelStates) { + ACSDK_DEBUG5(LX("executeNotifyOfActivityUpdates")); + for (const auto& state : channelStates) { + /* + * Special logic to ignore the SpeechRecognizer interface as specified by the Focus Management requirements. + */ + if ("SpeechRecognizer" == state.interfaceName && FocusManagerInterface::DIALOG_CHANNEL_NAME == state.name) { + continue; + } + m_channelStates[state.name] = state; + } +} + +void AudioActivityTracker::executeProvideState(unsigned int stateRequestToken) { + ACSDK_DEBUG5(LX("executeProvideState")); + rapidjson::Document payload(rapidjson::kObjectType); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + + auto currentTime = std::chrono::steady_clock::now(); + auto sendEmptyContext = true; + + if (!m_channelStates.empty()) { + for (auto it = m_channelStates.begin(); it != m_channelStates.end(); ++it) { + rapidjson::Value contextJson(rapidjson::kObjectType); + auto idleTime = std::chrono::milliseconds::zero(); + auto& channelContext = it->second; + + if (FocusState::NONE == channelContext.focusState) { + idleTime = + std::chrono::duration_cast(currentTime - channelContext.timeAtIdle); + } + + contextJson.AddMember(INTERFACE_KEY, channelContext.interfaceName, payload.GetAllocator()); + contextJson.AddMember(IDLE_TIME_KEY, idleTime.count(), payload.GetAllocator()); + + payload.AddMember( + rapidjson::StringRef(executeChannelNameInLowerCase(it->first)), contextJson, payload.GetAllocator()); + } + + if (!payload.Accept(writer)) { + ACSDK_ERROR(LX("executeProvideState").d("reason", "writerRefusedJsonObject")); + } else { + sendEmptyContext = false; + } + } + + if (sendEmptyContext) { + m_contextManager->setState( + CONTEXT_MANAGER_STATE, "", avsCommon::avs::StateRefreshPolicy::SOMETIMES, stateRequestToken); + } else { + m_contextManager->setState( + CONTEXT_MANAGER_STATE, + buffer.GetString(), + avsCommon::avs::StateRefreshPolicy::SOMETIMES, + stateRequestToken); + } +} + +const std::string& AudioActivityTracker::executeChannelNameInLowerCase(const std::string& channelName) { + auto it = m_channelNamesInLowerCase.find(channelName); + + // Store channel name in lower case if not done so yet. + if (it == m_channelNamesInLowerCase.end()) { + auto channelNameInLowerCase = avsCommon::utils::string::stringToLowerCase(channelName); + it = m_channelNamesInLowerCase.insert(it, std::make_pair(channelName, channelNameInLowerCase)); + } + return it->second; +} + +} // namespace afml +} // namespace alexaClientSDK diff --git a/AFML/src/CMakeLists.txt b/AFML/src/CMakeLists.txt index 1e8c73fe07..1b34e49d63 100644 --- a/AFML/src/CMakeLists.txt +++ b/AFML/src/CMakeLists.txt @@ -1,6 +1,8 @@ add_library(AFML SHARED + AudioActivityTracker.cpp Channel.cpp - FocusManager.cpp) + FocusManager.cpp + VisualActivityTracker.cpp) add_definitions("-DACSDK_LOG_MODULE=afml") include_directories(AFML "${AFML_SOURCE_DIR}/include") diff --git a/AFML/src/Channel.cpp b/AFML/src/Channel.cpp index f3d2c30c47..f515d39447 100644 --- a/AFML/src/Channel.cpp +++ b/AFML/src/Channel.cpp @@ -21,62 +21,73 @@ namespace afml { using namespace avsCommon::avs; using namespace avsCommon::sdkInterfaces; -Channel::Channel(const unsigned int priority) : +Channel::State::State(const std::string& name) : + name{name}, + focusState{FocusState::NONE}, + timeAtIdle{std::chrono::steady_clock::now()} { +} + +Channel::State::State() : focusState{FocusState::NONE}, timeAtIdle{std::chrono::steady_clock::now()} { +} + +Channel::Channel(const std::string& name, const unsigned int priority) : m_priority{priority}, - m_focusState{FocusState::NONE}, + m_state{name}, m_observer{nullptr} { } +const std::string& Channel::getName() const { + return m_state.name; +} + unsigned int Channel::getPriority() const { return m_priority; } -void Channel::setFocus(FocusState focus) { - if (focus == m_focusState) { - return; +bool Channel::setFocus(FocusState focus) { + if (focus == m_state.focusState) { + return false; } - m_focusState = focus; + m_state.focusState = focus; if (m_observer) { - m_observer->onFocusChanged(m_focusState); + m_observer->onFocusChanged(m_state.focusState); } - if (FocusState::NONE == m_focusState) { + if (FocusState::NONE == m_state.focusState) { m_observer = nullptr; + m_state.timeAtIdle = std::chrono::steady_clock::now(); } + return true; } void Channel::setObserver(std::shared_ptr observer) { - setFocus(FocusState::NONE); m_observer = observer; } -bool Channel::operator>(const Channel& rhs) const { - return m_priority < rhs.getPriority(); +bool Channel::hasObserver() const { + return m_observer != nullptr; } -void Channel::setActivityId(const std::string& activityId) { - m_currentActivityId = activityId; +bool Channel::operator>(const Channel& rhs) const { + return m_priority < rhs.getPriority(); } -std::string Channel::getActivityId() const { - return m_currentActivityId; +void Channel::setInterface(const std::string& interface) { + m_state.interfaceName = interface; } -bool Channel::stopActivity(const std::string& activityId) { - if (activityId != m_currentActivityId) { - return false; - } - if (!m_observer) { - return false; - } - setFocus(FocusState::NONE); - return true; +std::string Channel::getInterface() const { + return m_state.interfaceName; } bool Channel::doesObserverOwnChannel(std::shared_ptr observer) const { return observer == m_observer; } +Channel::State Channel::getState() const { + return m_state; +} + } // namespace afml } // namespace alexaClientSDK diff --git a/AFML/src/FocusManager.cpp b/AFML/src/FocusManager.cpp index ab10c0209d..74660da491 100644 --- a/AFML/src/FocusManager.cpp +++ b/AFML/src/FocusManager.cpp @@ -33,7 +33,18 @@ static const std::string TAG("FocusManager"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) -FocusManager::FocusManager(const std::vector& channelConfigurations) { +const std::vector FocusManager::DEFAULT_AUDIO_CHANNELS = { + {FocusManagerInterface::DIALOG_CHANNEL_NAME, FocusManagerInterface::DIALOG_CHANNEL_PRIORITY}, + {FocusManagerInterface::ALERTS_CHANNEL_NAME, FocusManagerInterface::ALERTS_CHANNEL_PRIORITY}, + {FocusManagerInterface::CONTENT_CHANNEL_NAME, FocusManagerInterface::CONTENT_CHANNEL_PRIORITY}}; + +const std::vector FocusManager::DEFAULT_VISUAL_CHANNELS = { + {FocusManagerInterface::VISUAL_CHANNEL_NAME, FocusManagerInterface::VISUAL_CHANNEL_PRIORITY}}; + +FocusManager::FocusManager( + const std::vector& channelConfigurations, + std::shared_ptr activityTrackerInterface) : + m_activityTracker{activityTrackerInterface} { for (auto config : channelConfigurations) { if (doesChannelNameExist(config.name)) { ACSDK_ERROR(LX("createChannelFailed").d("reason", "channelNameExists").d("config", config.toString())); @@ -44,7 +55,7 @@ FocusManager::FocusManager(const std::vector& channelConfi continue; } - auto channel = std::make_shared(config.priority); + auto channel = std::make_shared(config.name, config.priority); m_allChannels.insert({config.name, channel}); } } @@ -52,16 +63,16 @@ FocusManager::FocusManager(const std::vector& channelConfi bool FocusManager::acquireChannel( const std::string& channelName, std::shared_ptr channelObserver, - const std::string& activityId) { - ACSDK_DEBUG1(LX("acquireChannel").d("channelName", channelName).d("activityId", activityId)); + const std::string& interface) { + ACSDK_DEBUG1(LX("acquireChannel").d("channelName", channelName).d("interface", interface)); std::shared_ptr channelToAcquire = getChannel(channelName); if (!channelToAcquire) { ACSDK_ERROR(LX("acquireChannelFailed").d("reason", "channelNotFound").d("channelName", channelName)); return false; } - m_executor.submit([this, channelToAcquire, channelObserver, activityId]() { - acquireChannelHelper(channelToAcquire, channelObserver, activityId); + m_executor.submit([this, channelToAcquire, channelObserver, interface]() { + acquireChannelHelper(channelToAcquire, channelObserver, interface); }); return true; } @@ -97,36 +108,65 @@ void FocusManager::stopForegroundActivity() { return; } - std::string foregroundChannelActivityId = foregroundChannel->getActivityId(); + std::string foregroundChannelInterface = foregroundChannel->getInterface(); lock.unlock(); - m_executor.submitToFront([this, foregroundChannel, foregroundChannelActivityId]() { - stopForegroundActivityHelper(foregroundChannel, foregroundChannelActivityId); + m_executor.submitToFront([this, foregroundChannel, foregroundChannelInterface]() { + stopForegroundActivityHelper(foregroundChannel, foregroundChannelInterface); }); } +void FocusManager::addObserver(const std::shared_ptr& observer) { + std::lock_guard lock(m_mutex); + m_observers.insert(observer); +} + +void FocusManager::removeObserver(const std::shared_ptr& observer) { + std::lock_guard lock(m_mutex); + m_observers.erase(observer); +} + +void FocusManager::setChannelFocus(const std::shared_ptr& channel, FocusState focus) { + if (!channel->setFocus(focus)) { + return; + } + std::unique_lock lock(m_mutex); + auto observers = m_observers; + lock.unlock(); + for (auto& observer : observers) { + observer->onFocusChanged(channel->getName(), focus); + } + m_activityUpdates.push_back(channel->getState()); +} + void FocusManager::acquireChannelHelper( std::shared_ptr channelToAcquire, std::shared_ptr channelObserver, - const std::string& activityId) { + const std::string& interface) { + // Notify the old observer, if there is one, that it lost focus. + setChannelFocus(channelToAcquire, FocusState::NONE); + // Lock here to update internal state which stopForegroundActivity may concurrently access. std::unique_lock lock(m_mutex); std::shared_ptr foregroundChannel = getHighestPriorityActiveChannelLocked(); - channelToAcquire->setActivityId(activityId); + channelToAcquire->setInterface(interface); m_activeChannels.insert(channelToAcquire); lock.unlock(); + // Set the new observer. channelToAcquire->setObserver(channelObserver); + if (!foregroundChannel) { - channelToAcquire->setFocus(FocusState::FOREGROUND); + setChannelFocus(channelToAcquire, FocusState::FOREGROUND); } else if (foregroundChannel == channelToAcquire) { - channelToAcquire->setFocus(FocusState::FOREGROUND); + setChannelFocus(channelToAcquire, FocusState::FOREGROUND); } else if (*channelToAcquire > *foregroundChannel) { - foregroundChannel->setFocus(FocusState::BACKGROUND); - channelToAcquire->setFocus(FocusState::FOREGROUND); + setChannelFocus(foregroundChannel, FocusState::BACKGROUND); + setChannelFocus(channelToAcquire, FocusState::FOREGROUND); } else { - channelToAcquire->setFocus(FocusState::BACKGROUND); + setChannelFocus(channelToAcquire, FocusState::BACKGROUND); } + notifyActivityTracker(); } void FocusManager::releaseChannelHelper( @@ -147,25 +187,30 @@ void FocusManager::releaseChannelHelper( m_activeChannels.erase(channelToRelease); lock.unlock(); - channelToRelease->setFocus(FocusState::NONE); + setChannelFocus(channelToRelease, FocusState::NONE); if (wasForegrounded) { foregroundHighestPriorityActiveChannel(); } + notifyActivityTracker(); } void FocusManager::stopForegroundActivityHelper( std::shared_ptr foregroundChannel, - std::string foregroundChannelActivityId) { - if (!foregroundChannel->stopActivity(foregroundChannelActivityId)) { + std::string foregroundChannelInterface) { + if (foregroundChannelInterface != foregroundChannel->getInterface()) { return; } + if (!foregroundChannel->hasObserver()) { + return; + } + setChannelFocus(foregroundChannel, FocusState::NONE); // Lock here to update internal state which stopForegroundActivity may concurrently access. std::unique_lock lock(m_mutex); - foregroundChannel->setActivityId(""); m_activeChannels.erase(foregroundChannel); lock.unlock(); foregroundHighestPriorityActiveChannel(); + notifyActivityTracker(); } std::shared_ptr FocusManager::getChannel(const std::string& channelName) const { @@ -207,8 +252,15 @@ void FocusManager::foregroundHighestPriorityActiveChannel() { lock.unlock(); if (channelToForeground) { - channelToForeground->setFocus(FocusState::FOREGROUND); + setChannelFocus(channelToForeground, FocusState::FOREGROUND); + } +} + +void FocusManager::notifyActivityTracker() { + if (m_activityTracker) { + m_activityTracker->notifyOfActivityUpdates(m_activityUpdates); } + m_activityUpdates.clear(); } } // namespace afml diff --git a/AFML/src/VisualActivityTracker.cpp b/AFML/src/VisualActivityTracker.cpp new file mode 100644 index 0000000000..8296722738 --- /dev/null +++ b/AFML/src/VisualActivityTracker.cpp @@ -0,0 +1,139 @@ +/* + * Copyright 2018 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. + */ +#include +#include +#include + +#include +#include + +#include "AFML/VisualActivityTracker.h" + +namespace alexaClientSDK { +namespace afml { + +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::utils; +using namespace avsCommon::avs; + +/// String to identify log entries originating from this file. +static const std::string TAG("VisualActivityTracker"); + +/** + * 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 state information @c NamespaceAndName to send to the @c ContextManager for VisualActivityTracker. +static const NamespaceAndName CONTEXT_MANAGER_STATE{"VisualActivityTracker", "ActivityState"}; + +/// The interface key used in the VisualActivityTracker context. +static const char FOCUSED_KEY[] = "focused"; + +/// The interface key used in the VisualActivityTracker context. +static const char INTERFACE_KEY[] = "interface"; + +std::shared_ptr VisualActivityTracker::create( + std::shared_ptr contextManager) { + if (!contextManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullContextManager")); + return nullptr; + } + + auto visualActivityTracker = std::shared_ptr(new VisualActivityTracker(contextManager)); + + contextManager->setStateProvider(CONTEXT_MANAGER_STATE, visualActivityTracker); + + return visualActivityTracker; +} + +void VisualActivityTracker::provideState( + const avsCommon::avs::NamespaceAndName& stateProviderName, + unsigned int stateRequestToken) { + ACSDK_DEBUG5(LX("provideState")); + m_executor.submit([this, stateRequestToken]() { executeProvideState(stateRequestToken); }); +} + +void VisualActivityTracker::notifyOfActivityUpdates(const std::vector& channels) { + ACSDK_DEBUG5(LX("notifyOfActivityUpdates")); + // Return error if vector is empty. + if (channels.empty()) { + ACSDK_WARN(LX("notifyOfActivityUpdates").d("reason", "emptyVector")); + return; + } + + /* + * Currently we only have one visual channel, so we log an error if we are not getting updates with the expected + * one. + */ + for (auto& channel : channels) { + if (FocusManagerInterface::VISUAL_CHANNEL_NAME != channel.name) { + ACSDK_ERROR(LX("notifyOfActivityUpdates").d("reason", "InvalidChannelName").d("name", channel.name)); + return; + } + } + + m_executor.submit([this, channels]() { + // The last element of the vector is the most recent channel state. + m_channelState = channels.back(); + }); +} + +VisualActivityTracker::VisualActivityTracker( + std::shared_ptr contextManager) : + RequiresShutdown{"VisualActivityTracker"}, + m_contextManager{contextManager} { +} + +void VisualActivityTracker::doShutdown() { + m_executor.shutdown(); + m_contextManager.reset(); +} + +void VisualActivityTracker::executeProvideState(unsigned int stateRequestToken) { + ACSDK_DEBUG5(LX("executeProvideState")); + rapidjson::Document payload(rapidjson::kObjectType); + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + auto sendEmptyContext = true; + + if (FocusState::NONE != m_channelState.focusState) { + rapidjson::Value contextJson(rapidjson::kObjectType); + contextJson.AddMember(INTERFACE_KEY, m_channelState.interfaceName, payload.GetAllocator()); + payload.AddMember(FOCUSED_KEY, contextJson, payload.GetAllocator()); + + if (!payload.Accept(writer)) { + ACSDK_ERROR(LX("executeProvideState").d("reason", "writerRefusedJsonObject")); + } else { + sendEmptyContext = false; + } + } + + if (sendEmptyContext) { + m_contextManager->setState( + CONTEXT_MANAGER_STATE, "", avsCommon::avs::StateRefreshPolicy::SOMETIMES, stateRequestToken); + } else { + m_contextManager->setState( + CONTEXT_MANAGER_STATE, + buffer.GetString(), + avsCommon::avs::StateRefreshPolicy::SOMETIMES, + stateRequestToken); + } +} + +} // namespace afml +} // namespace alexaClientSDK diff --git a/AFML/test/AudioActivityTrackerTest.cpp b/AFML/test/AudioActivityTrackerTest.cpp new file mode 100644 index 0000000000..f3aa38e6ca --- /dev/null +++ b/AFML/test/AudioActivityTrackerTest.cpp @@ -0,0 +1,309 @@ +/* + * Copyright 2018 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 AudioActivityTrackerTest.cpp +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "AFML/AudioActivityTracker.h" + +namespace alexaClientSDK { +namespace afml { +namespace test { + +using namespace avsCommon::avs; +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::sdkInterfaces::test; +using namespace avsCommon::utils::json; +using namespace ::testing; + +/// Plenty of time for a test to complete. +static std::chrono::milliseconds WAIT_TIMEOUT(1000); + +/// Namespace for AudioActivityTracke. +static const std::string NAMESPACE_AUDIO_ACTIVITY_TRACKER("AudioActivityTracker"); + +/// The @c NamespaceAndName to send to the @c ContextManager. +static const NamespaceAndName NAMESPACE_AND_NAME_STATE{NAMESPACE_AUDIO_ACTIVITY_TRACKER, "ActivityState"}; + +/// Provide State Token for testing. +static const unsigned int PROVIDE_STATE_TOKEN_TEST{1}; + +/// The default Dialog Channel name. +static const std::string DIALOG_CHANNEL_NAME{"Dialog"}; + +/// The default dialog Channel priority. +static unsigned int DIALOG_CHANNEL_PRIORITY{100}; + +/// The default Dialog interface name. +static const std::string DIALOG_INTERFACE_NAME{"SpeechSynthesizer"}; + +/// The SpeechRecognizer interface name. +static const std::string AIP_INTERFACE_NAME{"SpeechRecognizer"}; + +/// The default Content Channel name. +static const std::string CONTENT_CHANNEL_NAME{"Content"}; + +/// The default Content interface name. +static const std::string CONTENT_INTERFACE_NAME{"AudioPlayer"}; + +/// The default Content Channel priority. +static unsigned int CONTENT_CHANNEL_PRIORITY{300}; + +/// Timeout to sleep before asking for provideState(). +static const std::chrono::milliseconds SHORT_TIMEOUT_MS = std::chrono::milliseconds(5); + +class AudioActivityTrackerTest : public ::testing::Test { +public: + AudioActivityTrackerTest(); + + void SetUp() override; + void TearDown() override; + + /// @c AudioActivityTracker to test + std::shared_ptr m_audioActivityTracker; + + /// @c ContextManager to provide state and update state. + std::shared_ptr m_mockContextManager; + + /// A dialogChannel used for testing. + std::shared_ptr m_dialogChannel; + + /// A contentChannel used for testing. + std::shared_ptr m_contentChannel; + + /** + * Verify that the provided state matches the expected state + * + * @param jsonState The state to verify + * @param channels The set of channels that's passed into the AudioActivityTracker + */ + void verifyState(const std::string& providedState, const std::vector& channels); + + /** + * A helper function to verify the context provided by the AudioActivityTracker matches the set the channels + * notified via notifyOfActivityUpdates(). + * + * @param channels The set of channels that's passed into the AudioActivityTracker + */ + void provideUpdate(const std::vector& channels); + + /** + * This is invoked in response to a @c setState call. + * + * @return @c SUCCESS. + */ + SetStateResult wakeOnSetState(); + + /// Promise to be fulfilled when @c setState is called. + std::promise m_wakeSetStatePromise; + + /// Future to notify when @c setState is called. + std::future m_wakeSetStateFuture; +}; + +AudioActivityTrackerTest::AudioActivityTrackerTest() : + m_wakeSetStatePromise{}, + m_wakeSetStateFuture{m_wakeSetStatePromise.get_future()} { +} + +void AudioActivityTrackerTest::SetUp() { + m_mockContextManager = std::make_shared>(); + m_audioActivityTracker = AudioActivityTracker::create(m_mockContextManager); + ASSERT_TRUE(m_mockContextManager != nullptr); + + m_dialogChannel = std::make_shared(DIALOG_CHANNEL_NAME, DIALOG_CHANNEL_PRIORITY); + m_dialogChannel->setInterface(DIALOG_INTERFACE_NAME); + ASSERT_TRUE(m_dialogChannel != nullptr); + + m_contentChannel = std::make_shared(CONTENT_CHANNEL_NAME, CONTENT_CHANNEL_PRIORITY); + m_contentChannel->setInterface(CONTENT_INTERFACE_NAME); + ASSERT_TRUE(m_contentChannel != nullptr); +} + +void AudioActivityTrackerTest::TearDown() { + m_audioActivityTracker->shutdown(); +} + +void AudioActivityTrackerTest::verifyState( + const std::string& providedState, + const std::vector& channels) { + rapidjson::Document jsonContent; + jsonContent.Parse(providedState); + + for (auto& channel : channels) { + rapidjson::Value::ConstMemberIterator channelNode; + + // channel name needs to be in lower case. + auto channelName = avsCommon::utils::string::stringToLowerCase(channel.name); + ASSERT_TRUE(jsonUtils::findNode(jsonContent, channelName, &channelNode)); + + // Get and verify interface name. + std::string interfaceName; + std::string expectedInterfaceName = channel.interfaceName; + // There is an requirement such that "SpeechRecognizer" is not a valid interface for the Dialog Channel. + if (AIP_INTERFACE_NAME == expectedInterfaceName) { + expectedInterfaceName = DIALOG_INTERFACE_NAME; + } + ASSERT_TRUE(jsonUtils::retrieveValue(channelNode->value, "interface", &interfaceName)); + ASSERT_EQ(interfaceName, expectedInterfaceName); + + // Get and verify idleTimeInMilliseconds. + int64_t idleTime; + bool isChannelActive = FocusState::NONE != channel.focusState; + // If interface is "SpeechRecognizer", then channel should expected to be not active instead. + if (AIP_INTERFACE_NAME == channel.interfaceName) { + isChannelActive = false; + } + ASSERT_TRUE(jsonUtils::retrieveValue(channelNode->value, "idleTimeInMilliseconds", &idleTime)); + if (isChannelActive) { + ASSERT_EQ(idleTime, 0); + } else { + ASSERT_NE(idleTime, 0); + } + } +} + +void AudioActivityTrackerTest::provideUpdate(const std::vector& channels) { + EXPECT_CALL( + *(m_mockContextManager.get()), + setState(NAMESPACE_AND_NAME_STATE, _, StateRefreshPolicy::SOMETIMES, PROVIDE_STATE_TOKEN_TEST)) + .Times(1) + .WillOnce(DoAll( + // need to include all four arguments, but only care about jsonState + Invoke([this, &channels]( + const avsCommon::avs::NamespaceAndName& namespaceAndName, + const std::string& jsonState, + const avsCommon::avs::StateRefreshPolicy& refreshPolicy, + const unsigned int stateRequestToken) { verifyState(jsonState, channels); }), + InvokeWithoutArgs(this, &AudioActivityTrackerTest::wakeOnSetState))); + + m_audioActivityTracker->notifyOfActivityUpdates(channels); + std::this_thread::sleep_for(SHORT_TIMEOUT_MS); + m_audioActivityTracker->provideState(NAMESPACE_AND_NAME_STATE, PROVIDE_STATE_TOKEN_TEST); + ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); +} + +SetStateResult AudioActivityTrackerTest::wakeOnSetState() { + m_wakeSetStatePromise.set_value(); + return SetStateResult::SUCCESS; +} + +/// Test if there's no activity updates, AudioActivityTracker will return an empty context. +TEST_F(AudioActivityTrackerTest, noActivityUpdate) { + EXPECT_CALL( + *(m_mockContextManager.get()), + setState(NAMESPACE_AND_NAME_STATE, "", StateRefreshPolicy::SOMETIMES, PROVIDE_STATE_TOKEN_TEST)) + .Times(1) + .WillOnce(InvokeWithoutArgs(this, &AudioActivityTrackerTest::wakeOnSetState)); + + m_audioActivityTracker->provideState(NAMESPACE_AND_NAME_STATE, PROVIDE_STATE_TOKEN_TEST); + ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); +} + +/// Test if there's an empty set of activity updates, AudioActivityTracker will return an empty context. +TEST_F(AudioActivityTrackerTest, emptyActivityUpdate) { + const std::vector channels; + EXPECT_CALL( + *(m_mockContextManager.get()), + setState(NAMESPACE_AND_NAME_STATE, "", StateRefreshPolicy::SOMETIMES, PROVIDE_STATE_TOKEN_TEST)) + .Times(1) + .WillOnce(InvokeWithoutArgs(this, &AudioActivityTrackerTest::wakeOnSetState)); + + m_audioActivityTracker->notifyOfActivityUpdates(channels); + m_audioActivityTracker->provideState(NAMESPACE_AND_NAME_STATE, PROVIDE_STATE_TOKEN_TEST); + ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); +} + +/// Test if there's an activityUpdate for one active channel, context will be reported correctly. +TEST_F(AudioActivityTrackerTest, oneActiveChannel) { + std::vector channels; + m_dialogChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_dialogChannel->getState()); + provideUpdate(channels); +} + +/* + * Test if there's an activityUpdate for one Dialog channel with "SpeechRecognizer" as an interface, + * AudioActivityTracker will ignore it and report empty context. + */ +TEST_F(AudioActivityTrackerTest, oneActiveChannelWithAIPAsInterface) { + std::vector channels; + m_dialogChannel->setInterface(AIP_INTERFACE_NAME); + m_dialogChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_dialogChannel->getState()); + EXPECT_CALL( + *(m_mockContextManager.get()), + setState(NAMESPACE_AND_NAME_STATE, "", StateRefreshPolicy::SOMETIMES, PROVIDE_STATE_TOKEN_TEST)) + .Times(1) + .WillOnce(InvokeWithoutArgs(this, &AudioActivityTrackerTest::wakeOnSetState)); + + m_audioActivityTracker->notifyOfActivityUpdates(channels); + std::this_thread::sleep_for(SHORT_TIMEOUT_MS); + m_audioActivityTracker->provideState(NAMESPACE_AND_NAME_STATE, PROVIDE_STATE_TOKEN_TEST); + ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); +} + +/* + * Test if there's an activityUpdate for one Dialog channel with "SpeechSynthesizer" and then "SpeechRecognizer" as an + * interface, AudioActivityTracker will ignore the "SpeechRecognizer" interface going active but report + * "SpeechSynthesizer" with idleTime not equal to zero. + */ +TEST_F(AudioActivityTrackerTest, oneActiveChannelWithDefaultAndAIPAsInterfaces) { + std::vector channels; + m_dialogChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_dialogChannel->getState()); + provideUpdate(channels); +} + +/// Test if there's an activityUpdate for two active channels, context will be reported correctly. +TEST_F(AudioActivityTrackerTest, twoActiveChannels) { + std::vector channels; + m_dialogChannel->setFocus(FocusState::FOREGROUND); + m_contentChannel->setFocus(FocusState::BACKGROUND); + channels.push_back(m_dialogChannel->getState()); + channels.push_back(m_contentChannel->getState()); + provideUpdate(channels); +} + +/// Test if there's an activityUpdate for one active and one idle channels, context will be reported correctly. +TEST_F(AudioActivityTrackerTest, oneActiveOneIdleChannels) { + std::vector channels; + m_dialogChannel->setFocus(FocusState::FOREGROUND); + m_contentChannel->setFocus(FocusState::BACKGROUND); + m_dialogChannel->setFocus(FocusState::NONE); + m_contentChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_dialogChannel->getState()); + channels.push_back(m_contentChannel->getState()); + provideUpdate(channels); +} + +} // namespace test +} // namespace afml +} // namespace alexaClientSDK diff --git a/AFML/test/CMakeLists.txt b/AFML/test/CMakeLists.txt index 670263f024..d376070274 100644 --- a/AFML/test/CMakeLists.txt +++ b/AFML/test/CMakeLists.txt @@ -1 +1,5 @@ -discover_unit_tests("${AFML_SOURCE_DIR}/include" AFML) +set(INCLUDE_PATH + "${AFML_SOURCE_DIR}/include" + "${AVSCommon_SOURCE_DIR}/SDKInterfaces/test") + +discover_unit_tests("${INCLUDE_PATH}" AFML) diff --git a/AFML/test/FocusManagerTest.cpp b/AFML/test/FocusManagerTest.cpp index 91e3cae166..215e18e1bf 100644 --- a/AFML/test/FocusManagerTest.cpp +++ b/AFML/test/FocusManagerTest.cpp @@ -16,6 +16,7 @@ #include #include +#include #include "AFML/FocusManager.h" @@ -24,6 +25,7 @@ namespace afml { namespace test { using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::sdkInterfaces::test; using namespace avsCommon::avs; /// Short time out for when callbacks are expected not to occur. @@ -32,38 +34,38 @@ static const auto SHORT_TIMEOUT = std::chrono::milliseconds(50); /// Long time out for the Channel observer to wait for the focus change callback (we should not reach this). static const auto DEFAULT_TIMEOUT = std::chrono::seconds(15); -/// The dialog Channel name used in intializing the FocusManager. +/// The dialog Channel name used in initializing the FocusManager. static const std::string DIALOG_CHANNEL_NAME = "DialogChannel"; -/// The alerts Channel name used in intializing the FocusManager. +/// The alerts Channel name used in initializing the FocusManager. static const std::string ALERTS_CHANNEL_NAME = "AlertsChannel"; -/// The content Channel name used in intializing the FocusManager. +/// The content Channel name used in initializing the FocusManager. static const std::string CONTENT_CHANNEL_NAME = "ContentChannel"; /// An incorrect Channel name that is never initialized as a Channel. static const std::string INCORRECT_CHANNEL_NAME = "aksdjfl;aksdjfl;akdsjf"; -/// The priority of the dialog Channel used in intializing the FocusManager. +/// The priority of the dialog Channel used in initializing the FocusManager. static const unsigned int DIALOG_CHANNEL_PRIORITY = 10; -/// The priority of the alerts Channel used in intializing the FocusManager. +/// The priority of the alerts Channel used in initializing the FocusManager. static const unsigned int ALERTS_CHANNEL_PRIORITY = 20; -/// The priority of the content Channel used in intializing the FocusManager. +/// The priority of the content Channel used in initializing the FocusManager. static const unsigned int CONTENT_CHANNEL_PRIORITY = 30; -/// Sample dialog activity id. -static const std::string DIALOG_ACTIVITY_ID = "dialog"; +/// Sample dialog interface name. +static const std::string DIALOG_INTERFACE_NAME = "dialog"; -/// Sample alerts activity id. -static const std::string ALERTS_ACTIVITY_ID = "alerts"; +/// Sample alerts interface name. +static const std::string ALERTS_INTERFACE_NAME = "alerts"; -/// Sample content activity id. -static const std::string CONTENT_ACTIVITY_ID = "content"; +/// Sample content interface name. +static const std::string CONTENT_INTERFACE_NAME = "content"; -/// Another sample dialog activity id. -static const std::string DIFFERENT_DIALOG_ACTIVITY_ID = "different dialog"; +/// Another sample dialog interface name. +static const std::string DIFFERENT_DIALOG_INTERFACE_NAME = "different dialog"; /// A test observer that mocks out the ChannelObserverInterface##onFocusChanged() call. class TestClient : public ChannelObserverInterface { @@ -120,6 +122,86 @@ class TestClient : public ChannelObserverInterface { bool m_focusChangeOccurred; }; +/// A test observer that mocks out the ActivityTrackerInterface##notifyOfActivityUpdates() call. +class MockActivityTrackerInterface : public ActivityTrackerInterface { +public: + /** + * Constructor. + */ + MockActivityTrackerInterface() : m_activityUpdatesOccurred{false} { + } + + /// Structure of expected Channel::State result from tests. + struct ExpectedChannelStateResult { + /// The expected channel name. + const std::string name; + + /// The expected interface name. + const std::string interfaceName; + + /// The expected focus state. + const FocusState focusState; + }; + + /** + * Implementation of the ActivityTrackerInterface##notifyOfActivityUpdates() callback. + * + * @param channelStates A vector of @c Channel::State that has been updated. + */ + void notifyOfActivityUpdates(const std::vector& channelStates) override { + std::unique_lock lock(m_mutex); + m_updatedChannels.clear(); + for (auto& channel : channelStates) { + m_updatedChannels.push_back(channel); + } + m_activityUpdatesOccurred = true; + m_activityChanged.notify_one(); + } + + /** + * Waits for the ActivityTrackerInterface##notifyOfActivityUpdates() callback. + * + * @param timeout The amount of time to wait for the callback. + * @param expected The expected channel state results + */ + void waitForActivityUpdates( + std::chrono::milliseconds timeout, + const std::vector& expected) { + std::unique_lock lock(m_mutex); + bool success = m_activityChanged.wait_for(lock, timeout, [this, &expected]() { + if (m_activityUpdatesOccurred) { + EXPECT_EQ(m_updatedChannels.size(), expected.size()); + auto count = 0; + for (auto& channel : m_updatedChannels) { + EXPECT_EQ(channel.name, expected[count].name); + EXPECT_EQ(channel.interfaceName, expected[count].interfaceName); + EXPECT_EQ(channel.focusState, expected[count].focusState); + count++; + } + } + return m_activityUpdatesOccurred; + }); + + if (success) { + m_activityUpdatesOccurred = false; + } + ASSERT_TRUE(success); + } + +private: + /// The focus state of the observer. + std::vector m_updatedChannels; + + /// A lock to guard against activity changes. + std::mutex m_mutex; + + /// A condition variable to wait for activity changes. + std::condition_variable m_activityChanged; + + /// A boolean flag so that we can re-use the observer even after a callback has occurred. + bool m_activityUpdatesOccurred; +}; + /// Manages testing focus changes class FocusChangeManager { public: @@ -169,7 +251,11 @@ class FocusManagerTest /// A client that acquires the content Channel. std::shared_ptr contentClient; + std::shared_ptr m_activityTracker; + virtual void SetUp() { + m_activityTracker = std::make_shared(); + FocusManager::ChannelConfiguration dialogChannelConfig{DIALOG_CHANNEL_NAME, DIALOG_CHANNEL_PRIORITY}; FocusManager::ChannelConfiguration alertsChannelConfig{ALERTS_CHANNEL_NAME, ALERTS_CHANNEL_PRIORITY}; @@ -179,7 +265,7 @@ class FocusManagerTest std::vector channelConfigurations{ dialogChannelConfig, alertsChannelConfig, contentChannelConfig}; - m_focusManager = std::make_shared(channelConfigurations); + m_focusManager = std::make_shared(channelConfigurations, m_activityTracker); dialogClient = std::make_shared(); alertsClient = std::make_shared(); @@ -190,12 +276,12 @@ class FocusManagerTest /// Tests acquireChannel with an invalid Channel name, expecting no focus changes to be made. TEST_F(FocusManagerTest, acquireInvalidChannelName) { - ASSERT_FALSE(m_focusManager->acquireChannel(INCORRECT_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_FALSE(m_focusManager->acquireChannel(INCORRECT_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); } /// Tests acquireChannel, expecting to get Foreground status since no other Channels are active. TEST_F(FocusManagerTest, acquireChannelWithNoOtherChannelsActive) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); } @@ -204,8 +290,8 @@ TEST_F(FocusManagerTest, acquireChannelWithNoOtherChannelsActive) { * priority Channel should get Foreground focus. */ TEST_F(FocusManagerTest, acquireLowerPriorityChannelWithOneHigherPriorityChannelTaken) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); - ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); + ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); assertFocusChange(alertsClient, FocusState::BACKGROUND); } @@ -215,9 +301,9 @@ TEST_F(FocusManagerTest, acquireLowerPriorityChannelWithOneHigherPriorityChannel * highest priority Channel should be Foreground focused. */ TEST_F(FocusManagerTest, aquireLowerPriorityChannelWithTwoHigherPriorityChannelsTaken) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); - ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_ACTIVITY_ID)); - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); + ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_INTERFACE_NAME)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); assertFocusChange(alertsClient, FocusState::BACKGROUND); assertFocusChange(contentClient, FocusState::BACKGROUND); @@ -229,10 +315,10 @@ TEST_F(FocusManagerTest, aquireLowerPriorityChannelWithTwoHigherPriorityChannels * should be Foreground focused. */ TEST_F(FocusManagerTest, acquireHigherPriorityChannelWithOneLowerPriorityChannelTaken) { - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::FOREGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::BACKGROUND); assertFocusChange(dialogClient, FocusState::FOREGROUND); } @@ -242,10 +328,11 @@ TEST_F(FocusManagerTest, acquireHigherPriorityChannelWithOneLowerPriorityChannel * should obtain Foreground focus. */ TEST_F(FocusManagerTest, kickOutActivityOnSameChannel) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, anotherDialogClient, DIFFERENT_DIALOG_ACTIVITY_ID)); + ASSERT_TRUE( + m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, anotherDialogClient, DIFFERENT_DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::NONE); assertFocusChange(anotherDialogClient, FocusState::FOREGROUND); } @@ -254,7 +341,7 @@ TEST_F(FocusManagerTest, kickOutActivityOnSameChannel) { * Tests releaseChannel with a single Channel. The observer should be notified to stop. */ TEST_F(FocusManagerTest, simpleReleaseChannel) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); ASSERT_TRUE(m_focusManager->releaseChannel(DIALOG_CHANNEL_NAME, dialogClient).get()); @@ -265,7 +352,7 @@ TEST_F(FocusManagerTest, simpleReleaseChannel) { * Tests releaseChannel on a Channel with an incorrect observer. The client should not receive any callback. */ TEST_F(FocusManagerTest, simpleReleaseChannelWithIncorrectObserver) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); ASSERT_FALSE(m_focusManager->releaseChannel(CONTENT_CHANNEL_NAME, dialogClient).get()); @@ -281,10 +368,10 @@ TEST_F(FocusManagerTest, simpleReleaseChannelWithIncorrectObserver) { * be notified to stop. */ TEST_F(FocusManagerTest, releaseForegroundChannelWhileBackgroundChannelTaken) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::BACKGROUND); ASSERT_TRUE(m_focusManager->releaseChannel(DIALOG_CHANNEL_NAME, dialogClient).get()); @@ -296,7 +383,7 @@ TEST_F(FocusManagerTest, releaseForegroundChannelWhileBackgroundChannelTaken) { * Tests stopForegroundActivity with a single Channel. The observer should be notified to stop. */ TEST_F(FocusManagerTest, simpleNonTargetedStop) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); m_focusManager->stopForegroundActivity(); @@ -308,13 +395,13 @@ TEST_F(FocusManagerTest, simpleNonTargetedStop) { * stop each time and the next highest priority background Channel should be brought to the foreground each time. */ TEST_F(FocusManagerTest, threeNonTargetedStopsWithThreeActivitiesHappening) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_INTERFACE_NAME)); assertFocusChange(alertsClient, FocusState::BACKGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::BACKGROUND); m_focusManager->stopForegroundActivity(); @@ -334,13 +421,13 @@ TEST_F(FocusManagerTest, threeNonTargetedStopsWithThreeActivitiesHappening) { * foreground focus. */ TEST_F(FocusManagerTest, stopForegroundActivityAndAcquireDifferentChannel) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); m_focusManager->stopForegroundActivity(); assertFocusChange(dialogClient, FocusState::NONE); - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::FOREGROUND); } @@ -349,13 +436,13 @@ TEST_F(FocusManagerTest, stopForegroundActivityAndAcquireDifferentChannel) { * foreground focus. */ TEST_F(FocusManagerTest, stopForegroundActivityAndAcquireSameChannel) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); m_focusManager->stopForegroundActivity(); assertFocusChange(dialogClient, FocusState::NONE); - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); } @@ -364,10 +451,10 @@ TEST_F(FocusManagerTest, stopForegroundActivityAndAcquireSameChannel) { * should remain foregrounded while the background Channel's observer should be notified to stop. */ TEST_F(FocusManagerTest, releaseBackgroundChannelWhileTwoChannelsTaken) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::BACKGROUND); ASSERT_TRUE(m_focusManager->releaseChannel(CONTENT_CHANNEL_NAME, contentClient).get()); @@ -382,19 +469,172 @@ TEST_F(FocusManagerTest, releaseBackgroundChannelWhileTwoChannelsTaken) { * Foreground focus. The originally backgrounded Channel should not change focus. */ TEST_F(FocusManagerTest, kickOutActivityOnSameChannelWhileOtherChannelsActive) { - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::FOREGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_ACTIVITY_ID)); + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); assertFocusChange(contentClient, FocusState::BACKGROUND); - ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, anotherDialogClient, DIFFERENT_DIALOG_ACTIVITY_ID)); + ASSERT_TRUE( + m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, anotherDialogClient, DIFFERENT_DIALOG_INTERFACE_NAME)); assertFocusChange(dialogClient, FocusState::NONE); assertFocusChange(anotherDialogClient, FocusState::FOREGROUND); assertNoFocusChange(contentClient); } +/// Tests that multiple observers can be added, and that they are notified of all focus changes. +TEST_F(FocusManagerTest, addObserver) { + // These are all the observers that will be added. + std::vector> observers; + observers.push_back(std::make_shared()); + observers.push_back(std::make_shared()); + + for (auto& observer : observers) { + m_focusManager->addObserver(observer); + } + + // Focus change on DIALOG channel. + for (auto& observer : observers) { + observer->expectFocusChange(DIALOG_CHANNEL_NAME, FocusState::FOREGROUND); + } + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); + + // Focus change on CONTENT channel. + for (auto& observer : observers) { + observer->expectFocusChange(CONTENT_CHANNEL_NAME, FocusState::BACKGROUND); + } + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); + + // Wait for all pending changes to complete. + for (auto& observer : observers) { + ASSERT_TRUE(observer->waitForFocusChanges(DEFAULT_TIMEOUT)); + } + + // Drop foreground focus. + for (auto& observer : observers) { + observer->expectFocusChange(DIALOG_CHANNEL_NAME, FocusState::NONE); + observer->expectFocusChange(CONTENT_CHANNEL_NAME, FocusState::FOREGROUND); + } + m_focusManager->stopForegroundActivity(); + + for (auto& observer : observers) { + ASSERT_TRUE(observer->waitForFocusChanges(DEFAULT_TIMEOUT)); + } +} + +/// Tests that observers can be removed, and that they are no longer notified of focus changes after removal. +TEST_F(FocusManagerTest, removeObserver) { + // These are all the observers that will ever be added. + std::vector> allObservers; + + // Note: StrictMock here so that we fail on unexpected observer callbacks. + allObservers.push_back(std::make_shared>()); + allObservers.push_back(std::make_shared>()); + + for (auto& observer : allObservers) { + m_focusManager->addObserver(observer); + } + + // These are the observers which are currently added. + auto activeObservers = allObservers; + + // One focus change with all observers added. + for (auto& observer : activeObservers) { + observer->expectFocusChange(DIALOG_CHANNEL_NAME, FocusState::FOREGROUND); + } + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); + + // Wait for all pending changes to complete. + for (auto& observer : allObservers) { + ASSERT_TRUE(observer->waitForFocusChanges(DEFAULT_TIMEOUT)); + } + + // Remove an observer. + m_focusManager->removeObserver(activeObservers.back()); + activeObservers.pop_back(); + + // Now another focus change with the removed observer. + for (auto& observer : activeObservers) { + observer->expectFocusChange(CONTENT_CHANNEL_NAME, FocusState::BACKGROUND); + } + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); + + // Wait for all pending changes to complete. + for (auto& observer : allObservers) { + ASSERT_TRUE(observer->waitForFocusChanges(DEFAULT_TIMEOUT)); + } + + // Remove all remaining observers. + for (auto& observer : activeObservers) { + m_focusManager->removeObserver(observer); + } + activeObservers.clear(); + + // And a final focus change with no observers. + m_focusManager->stopForegroundActivity(); + + for (auto& observer : allObservers) { + ASSERT_TRUE(observer->waitForFocusChanges(DEFAULT_TIMEOUT)); + } +} + +/** + * Tests activityTracker with three Channels and make sure notifyOfActivityUpdates() is called correctly. + */ +TEST_F(FocusManagerTest, activityTracker) { + // Acquire Content channel and expect notifyOfActivityUpdates() to notify activities on the Content channel. + const std::vector test1 = { + {CONTENT_CHANNEL_NAME, CONTENT_INTERFACE_NAME, FocusState::FOREGROUND}}; + ASSERT_TRUE(m_focusManager->acquireChannel(CONTENT_CHANNEL_NAME, contentClient, CONTENT_INTERFACE_NAME)); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test1); + + // Acquire Alert channel and expect notifyOfActivityUpdates() to notify activities to both Content and Alert + // channels. + const std::vector test2 = { + {CONTENT_CHANNEL_NAME, CONTENT_INTERFACE_NAME, FocusState::BACKGROUND}, + {ALERTS_CHANNEL_NAME, ALERTS_INTERFACE_NAME, FocusState::FOREGROUND}}; + ASSERT_TRUE(m_focusManager->acquireChannel(ALERTS_CHANNEL_NAME, alertsClient, ALERTS_INTERFACE_NAME)); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test2); + + // Acquire Dialog channel and expect notifyOfActivityUpdates() to notify activities to both Alert and Dialog + // channels. + const std::vector test3 = { + {ALERTS_CHANNEL_NAME, ALERTS_INTERFACE_NAME, FocusState::BACKGROUND}, + {DIALOG_CHANNEL_NAME, DIALOG_INTERFACE_NAME, FocusState::FOREGROUND}}; + ASSERT_TRUE(m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, dialogClient, DIALOG_INTERFACE_NAME)); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test3); + + // Release Content channel and expect notifyOfActivityUpdates() to notify activities to Content channel. + const std::vector test4 = { + {CONTENT_CHANNEL_NAME, CONTENT_INTERFACE_NAME, FocusState::NONE}}; + ASSERT_TRUE(m_focusManager->releaseChannel(CONTENT_CHANNEL_NAME, contentClient).get()); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test4); + + // Acquire Dialog channel with a different interface and expect notifyOfActivityUpdates() to notify activities to + // Dialog channels first with focus change on the previous one, and then later with the updated interface. + const std::vector test5 = { + {DIALOG_CHANNEL_NAME, DIALOG_INTERFACE_NAME, FocusState::NONE}, + {DIALOG_CHANNEL_NAME, DIFFERENT_DIALOG_INTERFACE_NAME, FocusState::FOREGROUND}}; + ASSERT_TRUE( + m_focusManager->acquireChannel(DIALOG_CHANNEL_NAME, anotherDialogClient, DIFFERENT_DIALOG_INTERFACE_NAME)); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test5); + + // Release Dialog channel and expect notifyOfActivityUpdates() to notify activities to both Dialog and Alerts + // channels. + const std::vector test6 = { + {DIALOG_CHANNEL_NAME, DIFFERENT_DIALOG_INTERFACE_NAME, FocusState::NONE}, + {ALERTS_CHANNEL_NAME, ALERTS_INTERFACE_NAME, FocusState::FOREGROUND}}; + ASSERT_TRUE(m_focusManager->releaseChannel(DIALOG_CHANNEL_NAME, anotherDialogClient).get()); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test6); + + // Release Alerts channel and expect notifyOfActivityUpdates() to notify activities to Alerts channel. + const std::vector test7 = { + {ALERTS_CHANNEL_NAME, ALERTS_INTERFACE_NAME, FocusState::NONE}}; + ASSERT_TRUE(m_focusManager->releaseChannel(ALERTS_CHANNEL_NAME, alertsClient).get()); + m_activityTracker->waitForActivityUpdates(DEFAULT_TIMEOUT, test7); +} + /// Test fixture for testing Channel. class ChannelTest : public ::testing::Test @@ -412,63 +652,72 @@ class ChannelTest virtual void SetUp() { clientA = std::make_shared(); clientB = std::make_shared(); - testChannel = std::make_shared(DIALOG_CHANNEL_PRIORITY); + testChannel = std::make_shared(DIALOG_CHANNEL_NAME, DIALOG_CHANNEL_PRIORITY); } }; -/// Tests the that the getPriority method of Channel works properly. -TEST_F(ChannelTest, getPriority) { - ASSERT_EQ(testChannel->getPriority(), DIALOG_CHANNEL_PRIORITY); +/// Tests that the getName method of Channel works properly. +TEST_F(ChannelTest, getName) { + ASSERT_EQ(testChannel->getName(), DIALOG_CHANNEL_NAME); } -/// Tests that an old observer is kicked out on a Channel when a new observer is set. -TEST_F(ChannelTest, kickoutOldObserver) { - testChannel->setObserver(clientA); - - testChannel->setFocus(FocusState::FOREGROUND); - assertFocusChange(clientA, FocusState::FOREGROUND); - - testChannel->setObserver(clientB); - assertFocusChange(clientA, FocusState::NONE); +/// Tests that the getPriority method of Channel works properly. +TEST_F(ChannelTest, getPriority) { + ASSERT_EQ(testChannel->getPriority(), DIALOG_CHANNEL_PRIORITY); } /// Tests that the observer properly gets notified of focus changes. TEST_F(ChannelTest, setObserverThenSetFocus) { testChannel->setObserver(clientA); - testChannel->setFocus(FocusState::FOREGROUND); + ASSERT_TRUE(testChannel->setFocus(FocusState::FOREGROUND)); assertFocusChange(clientA, FocusState::FOREGROUND); - testChannel->setFocus(FocusState::BACKGROUND); + ASSERT_TRUE(testChannel->setFocus(FocusState::BACKGROUND)); assertFocusChange(clientA, FocusState::BACKGROUND); - testChannel->setFocus(FocusState::NONE); + ASSERT_TRUE(testChannel->setFocus(FocusState::NONE)); assertFocusChange(clientA, FocusState::NONE); + + ASSERT_FALSE(testChannel->setFocus(FocusState::NONE)); } /// Tests that Channels are compared properly TEST_F(ChannelTest, priorityComparison) { - std::shared_ptr lowerPriorityChannel = std::make_shared(CONTENT_CHANNEL_PRIORITY); + std::shared_ptr lowerPriorityChannel = + std::make_shared(CONTENT_CHANNEL_NAME, CONTENT_CHANNEL_PRIORITY); ASSERT_TRUE(*testChannel > *lowerPriorityChannel); ASSERT_FALSE(*lowerPriorityChannel > *testChannel); } -/** - * Tests that the stopActivity method on Channel works properly and that observers are stopped if the activity id - * matches the the Channel's activity and doesn't get stopped if the ids don't match. - */ -TEST_F(ChannelTest, testStopActivity) { - testChannel->setActivityId(DIALOG_ACTIVITY_ID); +/// Tests that a Channel correctly reports whether it has an observer. +TEST_F(ChannelTest, hasObserver) { + ASSERT_FALSE(testChannel->hasObserver()); testChannel->setObserver(clientA); + ASSERT_TRUE(testChannel->hasObserver()); + testChannel->setObserver(clientB); + ASSERT_TRUE(testChannel->hasObserver()); + testChannel->setObserver(nullptr); + ASSERT_FALSE(testChannel->hasObserver()); +} - testChannel->setFocus(FocusState::FOREGROUND); - assertFocusChange(clientA, FocusState::FOREGROUND); - - ASSERT_FALSE(testChannel->stopActivity(CONTENT_ACTIVITY_ID)); - assertNoFocusChange(clientA); - - ASSERT_TRUE(testChannel->stopActivity(DIALOG_ACTIVITY_ID)); - assertFocusChange(clientA, FocusState::NONE); +/* + * Tests that the timeAtIdle only gets updated when channel goes to idle and not when channel goes to Foreground or + * Background. + */ +TEST_F(ChannelTest, getTimeAtIdle) { + auto startTime = testChannel->getState().timeAtIdle; + ASSERT_TRUE(testChannel->setFocus(FocusState::FOREGROUND)); + auto afterForegroundTime = testChannel->getState().timeAtIdle; + ASSERT_EQ(startTime, afterForegroundTime); + + ASSERT_TRUE(testChannel->setFocus(FocusState::BACKGROUND)); + auto afterBackgroundTime = testChannel->getState().timeAtIdle; + ASSERT_EQ(afterBackgroundTime, afterForegroundTime); + + ASSERT_TRUE(testChannel->setFocus(FocusState::NONE)); + auto afterNoneTime = testChannel->getState().timeAtIdle; + ASSERT_GT(afterNoneTime, afterBackgroundTime); } } // namespace test diff --git a/AFML/test/VisualActivityTrackerTest.cpp b/AFML/test/VisualActivityTrackerTest.cpp new file mode 100644 index 0000000000..a4e572c158 --- /dev/null +++ b/AFML/test/VisualActivityTrackerTest.cpp @@ -0,0 +1,270 @@ +/* + * Copyright 2018 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 VisualActivityTrackerTest.cpp +#include +#include +#include +#include + +#include +#include + +#include +#include +#include +#include + +#include +#include +#include + +#include "AFML/VisualActivityTracker.h" + +namespace alexaClientSDK { +namespace afml { +namespace test { + +using namespace avsCommon::avs; +using namespace avsCommon::sdkInterfaces; +using namespace avsCommon::sdkInterfaces::test; +using namespace avsCommon::utils::json; +using namespace ::testing; + +/// Plenty of time for a test to complete. +static std::chrono::milliseconds WAIT_TIMEOUT(1000); + +/// Namespace for AudioActivityTracke. +static const std::string NAMESPACE_AUDIO_ACTIVITY_TRACKER("VisualActivityTracker"); + +/// The @c NamespaceAndName to send to the @c ContextManager. +static const NamespaceAndName NAMESPACE_AND_NAME_STATE{NAMESPACE_AUDIO_ACTIVITY_TRACKER, "ActivityState"}; + +/// Provide State Token for testing. +static const unsigned int PROVIDE_STATE_TOKEN_TEST{1}; + +/// The default Visual Channel name. +static const std::string VISUAL_CHANNEL_NAME{"Visual"}; + +/// The default Visual Interface name. +static const std::string VISUAL_INTERFACE_NAME{"TempateRuntime"}; + +/// The default Visual Channel priority. +static unsigned int VISUAL_CHANNEL_PRIORITY{100}; + +/// The default Invalid Channel name. +static const std::string INVALID_CHANNEL_NAME{"Invalid"}; + +/// The default Channel priority for the invalid channel. +static unsigned int INVALID_CHANNEL_PRIORITY{300}; + +/// Timeout to sleep before asking for provideState(). +static const std::chrono::milliseconds SHORT_TIMEOUT_MS = std::chrono::milliseconds(5); + +/// Test harness for @c VisualActivityTrackerTest class. +class VisualActivityTrackerTest : public ::testing::Test { +public: + /// A constructor which initializes the promises and futures needed for the test class. + VisualActivityTrackerTest(); + + /// Set up the test harness for running a test. + void SetUp() override; + + /// Clean up the test harness after running a test. + void TearDown() override; + + /// @c VisualActivityTracker to test + std::shared_ptr m_VisualActivityTracker; + + /// @c ContextManager to provide state and update state. + std::shared_ptr m_mockContextManager; + + /// A visualChannel used for testing. + std::shared_ptr m_visualChannel; + + /** + * Verify that the provided state matches the expected state + * + * @param jsonState The state to verify + * @param channels The set of channels that's passed into the VisualActivityTracker + */ + void verifyState(const std::string& providedState, const std::vector& channels); + + /** + * A helper function to verify the context provided by the VisualActivityTracker matches the set the channels + * notified via notifyOfActivityUpdates(). + * + * @param channels The set of channels that's passed into the VisualActivityTracker + */ + void provideUpdate(const std::vector& channels); + + /** + * This is invoked in response to a @c setState call. + * + * @return @c SUCCESS. + */ + SetStateResult wakeOnSetState(); + + /// Promise to be fulfilled when @c setState is called. + std::promise m_wakeSetStatePromise; + + /// Future to notify when @c setState is called. + std::future m_wakeSetStateFuture; +}; + +VisualActivityTrackerTest::VisualActivityTrackerTest() : m_wakeSetStateFuture{m_wakeSetStatePromise.get_future()} { +} + +void VisualActivityTrackerTest::SetUp() { + m_mockContextManager = std::make_shared>(); + m_VisualActivityTracker = VisualActivityTracker::create(m_mockContextManager); + ASSERT_TRUE(m_mockContextManager != nullptr); + + m_visualChannel = std::make_shared(VISUAL_CHANNEL_NAME, VISUAL_CHANNEL_PRIORITY); + m_visualChannel->setInterface(VISUAL_INTERFACE_NAME); + ASSERT_TRUE(m_visualChannel != nullptr); +} + +void VisualActivityTrackerTest::TearDown() { + m_VisualActivityTracker->shutdown(); +} + +void VisualActivityTrackerTest::verifyState( + const std::string& providedState, + const std::vector& channels) { + rapidjson::Document jsonContent; + jsonContent.Parse(providedState); + + // VisualActivityTracker should return empty context if otherwise if the vector is empty. + if (channels.size() == 0) { + ASSERT_TRUE(providedState.empty()); + return; + } + + // VisualActivityTracker should return empty context if any channel is not VISUAL_CHANNEL. + for (auto& channel : channels) { + if (FocusManagerInterface::VISUAL_CHANNEL_NAME != channel.name) { + ASSERT_TRUE(providedState.empty()); + return; + } + } + + // Get the last element of the channels vector as that's the most recent updates. + const auto& channel = channels.back(); + + // If channel is not active, VisualActivityTracker should return empty context as well. + if (FocusState::NONE == channel.focusState) { + ASSERT_TRUE(providedState.empty()); + return; + } + + // Get "focused" node. + rapidjson::Value::ConstMemberIterator focusNode; + ASSERT_TRUE(jsonUtils::findNode(jsonContent, "focused", &focusNode)); + + // Get and verify interface name. + std::string interfaceName; + ASSERT_TRUE(jsonUtils::retrieveValue(focusNode->value, "interface", &interfaceName)); + ASSERT_EQ(interfaceName, channel.interfaceName); +} + +void VisualActivityTrackerTest::provideUpdate(const std::vector& channels) { + EXPECT_CALL( + *(m_mockContextManager.get()), + setState(NAMESPACE_AND_NAME_STATE, _, StateRefreshPolicy::SOMETIMES, PROVIDE_STATE_TOKEN_TEST)) + .Times(1) + .WillOnce(DoAll( + // need to include all four arguments, but only care about jsonState + Invoke([this, &channels]( + const avsCommon::avs::NamespaceAndName& namespaceAndName, + const std::string& jsonState, + const avsCommon::avs::StateRefreshPolicy& refreshPolicy, + const unsigned int stateRequestToken) { verifyState(jsonState, channels); }), + InvokeWithoutArgs(this, &VisualActivityTrackerTest::wakeOnSetState))); + + m_VisualActivityTracker->notifyOfActivityUpdates(channels); + std::this_thread::sleep_for(SHORT_TIMEOUT_MS); + m_VisualActivityTracker->provideState(NAMESPACE_AND_NAME_STATE, PROVIDE_STATE_TOKEN_TEST); + ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); +} + +SetStateResult VisualActivityTrackerTest::wakeOnSetState() { + m_wakeSetStatePromise.set_value(); + return SetStateResult::SUCCESS; +} + +/// Test if there's no activity updates, VisualActivityTracker will return an empty context. +TEST_F(VisualActivityTrackerTest, noActivityUpdate) { + EXPECT_CALL( + *(m_mockContextManager.get()), + setState(NAMESPACE_AND_NAME_STATE, "", StateRefreshPolicy::SOMETIMES, PROVIDE_STATE_TOKEN_TEST)) + .Times(1) + .WillOnce(InvokeWithoutArgs(this, &VisualActivityTrackerTest::wakeOnSetState)); + + m_VisualActivityTracker->provideState(NAMESPACE_AND_NAME_STATE, PROVIDE_STATE_TOKEN_TEST); + ASSERT_TRUE(std::future_status::ready == m_wakeSetStateFuture.wait_for(WAIT_TIMEOUT)); +} + +/// Test if there's an empty vector of activity updates, VisualActivityTracker will return an empty context. +TEST_F(VisualActivityTrackerTest, emptyActivityUpdate) { + std::vector channels; + provideUpdate(channels); +} + +/// Test if there's an activityUpdate for one idle channel, VisualActivityTracker will return an empty context. +TEST_F(VisualActivityTrackerTest, oneIdleChannel) { + std::vector channels; + m_visualChannel->setFocus(FocusState::NONE); + channels.push_back(m_visualChannel->getState()); + provideUpdate(channels); +} + +/// Test if there's an activityUpdate for one active channel, context will be reported correctly. +TEST_F(VisualActivityTrackerTest, oneActiveChannel) { + std::vector channels; + m_visualChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_visualChannel->getState()); + provideUpdate(channels); +} + +/* + * Test if there's an vector of activity updates with one valid and one invalid channel, VisualActivityTracker will + * return an empty context. + */ +TEST_F(VisualActivityTrackerTest, invalidChannelActivityUpdate) { + std::vector channels; + auto invalidChannel = std::make_shared(INVALID_CHANNEL_NAME, INVALID_CHANNEL_PRIORITY); + m_visualChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_visualChannel->getState()); + channels.push_back(invalidChannel->getState()); + provideUpdate(channels); +} + +/* + * Test if there's an vector of activity updates with one valid channel, VisualActivityTracker take the state from the + * last element of the vector. + */ +TEST_F(VisualActivityTrackerTest, validChannelTwoActivityUpdates) { + std::vector channels; + m_visualChannel->setFocus(FocusState::FOREGROUND); + channels.push_back(m_visualChannel->getState()); + m_visualChannel->setFocus(FocusState::BACKGROUND); + channels.push_back(m_visualChannel->getState()); + provideUpdate(channels); +} + +} // namespace test +} // namespace afml +} // namespace alexaClientSDK \ No newline at end of file diff --git a/AVSCommon/AVS/include/AVSCommon/AVS/Attachment/AttachmentWriter.h b/AVSCommon/AVS/include/AVSCommon/AVS/Attachment/AttachmentWriter.h index 4f609cdd1e..440a5d090a 100644 --- a/AVSCommon/AVS/include/AVSCommon/AVS/Attachment/AttachmentWriter.h +++ b/AVSCommon/AVS/include/AVSCommon/AVS/Attachment/AttachmentWriter.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_ATTACHMENT_ATTACHMENTWRITER_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_ATTACHMENT_ATTACHMENTWRITER_H_ +#include #include namespace alexaClientSDK { diff --git a/CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/AdapterUtils.h b/AVSCommon/AVS/include/AVSCommon/AVS/ExternalMediaPlayer/AdapterUtils.h similarity index 92% rename from CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/AdapterUtils.h rename to AVSCommon/AVS/include/AVSCommon/AVS/ExternalMediaPlayer/AdapterUtils.h index e9f4bfd846..cd5672e952 100644 --- a/CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/AdapterUtils.h +++ b/AVSCommon/AVS/include/AVSCommon/AVS/ExternalMediaPlayer/AdapterUtils.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_EXTERNALMEDIAPLAYER_INCLUDE_EXTERNALMEDIAPLAYER_ADAPTERUTILS_H_ -#define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_EXTERNALMEDIAPLAYER_INCLUDE_EXTERNALMEDIAPLAYER_ADAPTERUTILS_H_ +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_EXTERNALMEDIAPLAYER_ADAPTERUTILS_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_EXTERNALMEDIAPLAYER_ADAPTERUTILS_H_ #include #include @@ -26,7 +26,8 @@ #include "AVSCommon/Utils/RetryTimer.h" namespace alexaClientSDK { -namespace capabilityAgents { +namespace avsCommon { +namespace avs { namespace externalMediaPlayer { /// Enumeration class for events sent by adapters to AVS. @@ -111,8 +112,9 @@ rapidjson::Value buildSessionState( bool buildDefaultPlayerState(rapidjson::Value* document, rapidjson::Document::AllocatorType& allocator); } // namespace externalMediaPlayer -} // namespace capabilityAgents +} // namespace avs +} // namespace avsCommon } // namespace alexaClientSDK #endif // end -// ALEXA_CLIENT_SDK_CAPABILITYAGENTS_EXTERNALMEDIAPLAYER_INCLUDE_EXTERNALMEDIAPLAYER_ADAPTERUTILS_H_ +// ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_EXTERNALMEDIAPLAYER_ADAPTERUTILS_H_ diff --git a/AVSCommon/AVS/include/AVSCommon/AVS/SpeakerConstants/SpeakerConstants.h b/AVSCommon/AVS/include/AVSCommon/AVS/SpeakerConstants/SpeakerConstants.h index afd6bbc5f1..b6f1f08ec9 100644 --- a/AVSCommon/AVS/include/AVSCommon/AVS/SpeakerConstants/SpeakerConstants.h +++ b/AVSCommon/AVS/include/AVSCommon/AVS/SpeakerConstants/SpeakerConstants.h @@ -19,6 +19,8 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_SPEAKERCONSTANTS_SPEAKERCONSTANTS_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_AVS_INCLUDE_AVSCOMMON_AVS_SPEAKERCONSTANTS_SPEAKERCONSTANTS_H_ +#include + namespace alexaClientSDK { namespace avsCommon { namespace avs { diff --git a/AVSCommon/AVS/include/AVSCommon/AVS/StateRefreshPolicy.h b/AVSCommon/AVS/include/AVSCommon/AVS/StateRefreshPolicy.h index 19c0a9c331..d7068aed84 100644 --- a/AVSCommon/AVS/include/AVSCommon/AVS/StateRefreshPolicy.h +++ b/AVSCommon/AVS/include/AVSCommon/AVS/StateRefreshPolicy.h @@ -35,7 +35,14 @@ enum class StateRefreshPolicy { * Indicates to the @c ContextManager that the stateProvider needs to be queried and the state refreshed every time * it processes a @c getContext request. */ - ALWAYS + ALWAYS, + + /** + * Indicates to the @c ContextManager that the stateProvider needs to be queried and the state refreshed every time + * it processes a @c getContext request. The stateProvider may choose to not report context by supplying an empty + * @c jsonState via @c setState. + */ + SOMETIMES }; } // namespace avs diff --git a/CapabilityAgents/ExternalMediaPlayer/src/AdapterUtils.cpp b/AVSCommon/AVS/src/ExternalMediaPlayer/AdapterUtils.cpp similarity index 98% rename from CapabilityAgents/ExternalMediaPlayer/src/AdapterUtils.cpp rename to AVSCommon/AVS/src/ExternalMediaPlayer/AdapterUtils.cpp index 42ab971179..5c20ceb6d3 100644 --- a/CapabilityAgents/ExternalMediaPlayer/src/AdapterUtils.cpp +++ b/AVSCommon/AVS/src/ExternalMediaPlayer/AdapterUtils.cpp @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -#include "ExternalMediaPlayer/AdapterUtils.h" +#include "AVSCommon/AVS/ExternalMediaPlayer/AdapterUtils.h" #include #include @@ -21,7 +21,8 @@ #include namespace alexaClientSDK { -namespace capabilityAgents { +namespace avsCommon { +namespace avs { namespace externalMediaPlayer { using namespace avsCommon::avs; @@ -190,5 +191,6 @@ bool buildDefaultPlayerState(rapidjson::Value* document, rapidjson::Document::Al } } // namespace externalMediaPlayer -} // namespace capabilityAgents +} // namespace avs +} // namespace avsCommon } // namespace alexaClientSDK diff --git a/AVSCommon/CMakeLists.txt b/AVSCommon/CMakeLists.txt index d972754840..03c24ecbae 100644 --- a/AVSCommon/CMakeLists.txt +++ b/AVSCommon/CMakeLists.txt @@ -12,6 +12,7 @@ add_library(AVSCommon SHARED AVS/src/AVSMessage.cpp AVS/src/AVSMessageHeader.cpp AVS/src/AbstractConnection.cpp + AVS/src/ExternalMediaPlayer/AdapterUtils.cpp AVS/src/AlexaClientSDKInit.cpp AVS/src/Attachment/Attachment.cpp AVS/src/Attachment/AttachmentManager.cpp @@ -43,11 +44,13 @@ add_library(AVSCommon SHARED Utils/src/Logger/Logger.cpp Utils/src/Logger/LoggerSinkManager.cpp Utils/src/Logger/LoggerUtils.cpp + Utils/src/Logger/LogStringFormatter.cpp Utils/src/Logger/ModuleLogger.cpp Utils/src/Logger/ThreadMoniker.cpp Utils/src/Metrics.cpp Utils/src/RequiresShutdown.cpp Utils/src/RetryTimer.cpp + Utils/src/SafeCTimeAccess.cpp Utils/src/Stream/StreamFunctions.cpp Utils/src/Stream/Streambuf.cpp Utils/src/StringUtils.cpp diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthDelegateInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthDelegateInterface.h index bae9eabb64..6a3e0ac60e 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthDelegateInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthDelegateInterface.h @@ -26,12 +26,12 @@ namespace avsCommon { namespace sdkInterfaces { /** - * The AuthDelegateInterface is used to provide clients with valid LWA authroization + * The AuthDelegateInterface is used to provide clients with valid LWA authorization * tokens. @see * https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/content/avs-api-overview#authorization * Given an @c AuthDelegateInterface pointer, the client is expected to call @c getAuthToken() immediately before * making AVS requests. The returned value is passed in the HTTP/2 header of requests sent to AVS. - * These authroization tokens may expire, so AuthDelegates also track the state of authorization (essentially, + * These authorization tokens may expire, so AuthDelegates also track the state of authorization (essentially, * whether an immediate call to @c getAuthToken() will return a token that is expected to be viable). The client * may elect to receive callbacks when this state changes by calling @c setAuthObserver(). This allows the client * to avoid sending requests tha are doomed to fail because the authorization token has already expired. This also diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthObserverInterface.h index 9247dc4f3a..3c68f6de53 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthObserverInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/AuthObserverInterface.h @@ -44,8 +44,8 @@ class AuthObserverInterface { /// The enum Error encodes possible errors which may occur when changing state. enum class Error { - /// No error. - NO_ERROR, + /// Success. + SUCCESS, /// An unknown body containing no error field has been encountered. UNKNOWN_ERROR, /// The client authorization failed. @@ -110,8 +110,8 @@ inline std::ostream& operator<<(std::ostream& stream, const AuthObserverInterfac */ inline std::ostream& operator<<(std::ostream& stream, const AuthObserverInterface::Error& error) { switch (error) { - case AuthObserverInterface::Error::NO_ERROR: - stream << "NO_ERROR"; + case AuthObserverInterface::Error::SUCCESS: + stream << "SUCCESS"; break; case AuthObserverInterface::Error::UNKNOWN_ERROR: stream << "UNKNOWN_ERROR"; diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ContextManagerInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ContextManagerInterface.h index f7a358da42..f98fd6c8f4 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ContextManagerInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ContextManagerInterface.h @@ -91,7 +91,8 @@ class ContextManagerInterface { * The @c jsonState is the json value that is associated with the key "payload". * * @param namespaceAndName The namespace and name of the @c StateProviderInterface whose state is being updated. - * @param jsonState The state of the @c StateProviderInterface. + * @param jsonState The state of the @c StateProviderInterface. The @c StateProviderInterface with a @c + * refreshPolicy of SOMETIMES can pass in an empty string to indicate no contexts needs to be sent by the provider. * @param refreshPolicy The refresh policy for the state. * @param stateRequestToken The token that was provided in a @c provideState request. Defaults to 0. * diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/DirectiveSequencerInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/DirectiveSequencerInterface.h index 7dc265ad31..f529403df1 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/DirectiveSequencerInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/DirectiveSequencerInterface.h @@ -71,9 +71,6 @@ class DirectiveSequencerInterface : public utils::RequiresShutdown { * implementations of this should call the handler's getConfiguration() method to get the namespace(s), name(s), and * policy(ies) of the handler. If the handler's configurations are unable to be removed, the entire operation is * refused. - - specified mappings from @c NamespaceAndName values to @c HandlerAndPolicy values. If any of - * the specified mappings do not match an existing mapping, the entire operation is refused. * * @param handler The handler to remove. * @return Whether the handler was removed. @@ -98,6 +95,18 @@ class DirectiveSequencerInterface : public utils::RequiresShutdown { * @return Whether or not the directive was accepted. */ virtual bool onDirective(std::shared_ptr directive) = 0; + + /** + * Disable the DirectiveSequencer. + * + * @note While disabled the DirectiveSequencer should not be able to handle directives. + */ + virtual void disable() = 0; + + /** + * Enable the DirectiveSequencer. + */ + virtual void enable() = 0; }; inline DirectiveSequencerInterface::DirectiveSequencerInterface(const std::string& name) : diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ExternalMediaPlayerInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ExternalMediaPlayerInterface.h new file mode 100644 index 0000000000..4b676228ce --- /dev/null +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/ExternalMediaPlayerInterface.h @@ -0,0 +1,54 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_EXTERNALMEDIAPLAYERINTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_EXTERNALMEDIAPLAYERINTERFACE_H_ + +#include +#include +#include +#include +#include +#include +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { + +/** + * This class provides an interface to the @c ExternalMediaPlayer. + * Currently it provides an interface for adapters to set the player in focus when they acquire focus. + */ +class ExternalMediaPlayerInterface { +public: + /** + * Destructor + */ + virtual ~ExternalMediaPlayerInterface() = default; + + /** + * Method to set the player in focus after an adapter has acquired the channel. + * + * @param playerInFocus The business name of the adapter that has currently acquired focus. + */ + virtual void setPlayerInFocus(const std::string& playerInFocus) = 0; +}; + +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_EXTERNALMEDIAPLAYERINTERFACE_H_ diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerInterface.h index 7d941aa847..93abccd617 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerInterface.h @@ -21,6 +21,7 @@ #include #include "ChannelObserverInterface.h" +#include "FocusManagerObserverInterface.h" namespace alexaClientSDK { namespace avsCommon { @@ -32,12 +33,12 @@ namespace sdkInterfaces { * operations are provided: * * acquire Channel - clients should call the acquireChannel() method, passing in the name of the Channel they wish to - * acquire, a pointer to the observer that they want to be notified once they get focus, and a unique activity id. + * acquire, a pointer to the observer that they want to be notified once they get focus, and a unique interface name. * * release Channel - clients should call the releaseChannel() method, passing in the name of the Channel and the * observer of the Channel they wish to release. * - * stop foreground Channel - clients should call the stopForegroundActivitiy() method. + * stop foreground Channel - clients should call the stopForegroundActivity() method. * * All of these methods will notify the observer of the Channel of focus changes via an asynchronous callback to the * ChannelObserverInterface##onFocusChanged() method, at which point the client should make a user observable change @@ -63,6 +64,12 @@ class FocusManagerInterface { /// The default Content Channel priority. static constexpr unsigned int CONTENT_CHANNEL_PRIORITY = 300; + /// The default Visual Channel name. + static constexpr const char* VISUAL_CHANNEL_NAME = "Visual"; + + /// The default Visual Channel priority. + static constexpr unsigned int VISUAL_CHANNEL_PRIORITY = 100; + /// Destructor. virtual ~FocusManagerInterface() = default; @@ -74,15 +81,16 @@ class FocusManagerInterface { * * @param channelName The name of the Channel to acquire. * @param channelObserver The observer that will be acquiring the Channel and be notified of focus changes. - * @param activityId The id of the new activity on the Channel. This should be unique and represents the activity - * using the Channel. + * @param interface The name of the AVS interface occupying the Channel. This should be unique and represents the + * name of the AVS interface using the Channel. The name of the AVS interface is used by the ActivityTracker to + * send Context to AVS. * * @return Returns @c true if the Channel can be acquired and @c false otherwise. */ virtual bool acquireChannel( const std::string& channelName, std::shared_ptr channelObserver, - const std::string& activityId) = 0; + const std::string& interface) = 0; /** * This method will release the Channel and notify the observer of the Channel, if the observer is the same as the @@ -105,6 +113,22 @@ class FocusManagerInterface { * to the foreground. */ virtual void stopForegroundActivity() = 0; + + /** + * Add an observer to the focus manager. + * + * @param observer The observer to add. + */ + virtual void addObserver( + const std::shared_ptr& observer) = 0; + + /** + * Remove an observer from the focus manager. + * + * @param observer The observer to remove. + */ + virtual void removeObserver( + const std::shared_ptr& observer) = 0; }; } // namespace sdkInterfaces diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerObserverInterface.h new file mode 100644 index 0000000000..4ce79f98bc --- /dev/null +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/FocusManagerObserverInterface.h @@ -0,0 +1,43 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_FOCUSMANAGEROBSERVERINTERFACE_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_FOCUSMANAGEROBSERVERINTERFACE_H_ + +#include "AVSCommon/AVS/FocusState.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { + +/// An interface that clients can extend to register to observe focus changes. +class FocusManagerObserverInterface { +public: + virtual ~FocusManagerObserverInterface() = default; + + /** + * Used to notify the observer of focus changes. This function should return quickly. + * + * @param channelName The name of the channel which changed @c FocusState. + * @param newFocus The new @c FocusState of @c channelName. + */ + virtual void onFocusChanged(const std::string& channelName, avsCommon::avs::FocusState newFocus) = 0; +}; + +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_FOCUSMANAGEROBSERVERINTERFACE_H_ diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/HTTPContentFetcherInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/HTTPContentFetcherInterface.h index eb3d1cc31e..5d8c381b1f 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/HTTPContentFetcherInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/HTTPContentFetcherInterface.h @@ -47,9 +47,12 @@ class HTTPContentFetcherInterface { * This function retrieves content from a remote location. No thread safety is guaranteed. * * @param option Flag indicating desired content. + * @param writer An optional writer parameter to be used when writing to an external stream. * @return A new @c HTTPContent object or @c nullptr if a failure occured. */ - virtual std::unique_ptr getContent(FetchOptions option) = 0; + virtual std::unique_ptr getContent( + FetchOptions option, + std::shared_ptr writer = nullptr) = 0; }; } // namespace sdkInterfaces diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/MessageObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/MessageObserverInterface.h index aa9f349122..1e10d954e0 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/MessageObserverInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/MessageObserverInterface.h @@ -20,6 +20,8 @@ namespace alexaClientSDK { namespace avsCommon { namespace sdkInterfaces { +#include + /** * This class allows a client to receive messages from AVS. */ diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SingleSettingObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SingleSettingObserverInterface.h index 6a456cf009..8bc458eb5c 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SingleSettingObserverInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SingleSettingObserverInterface.h @@ -16,6 +16,8 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_SINGLESETTINGOBSERVERINTERFACE_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_SINGLESETTINGOBSERVERINTERFACE_H_ +#include + namespace alexaClientSDK { namespace avsCommon { namespace sdkInterfaces { diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SoftwareInfoSenderObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SoftwareInfoSenderObserverInterface.h index 864adcf15a..5c2164235d 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SoftwareInfoSenderObserverInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SoftwareInfoSenderObserverInterface.h @@ -16,6 +16,9 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_SOFTWAREINFOSENDEROBSERVERINTERFACE_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_SOFTWAREINFOSENDEROBSERVERINTERFACE_H_ +#include +#include + namespace alexaClientSDK { namespace avsCommon { namespace sdkInterfaces { diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeakerManagerInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeakerManagerInterface.h index a35d0ee68f..18d14274b3 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeakerManagerInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/SpeakerManagerInterface.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_SPEAKERMANAGERINTERFACE_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_INCLUDE_AVSCOMMON_SDKINTERFACES_SPEAKERMANAGERINTERFACE_H_ +#include #include #include diff --git a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/TemplateRuntimeObserverInterface.h b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/TemplateRuntimeObserverInterface.h index d9258a47b7..921deed045 100644 --- a/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/TemplateRuntimeObserverInterface.h +++ b/AVSCommon/SDKInterfaces/include/AVSCommon/SDKInterfaces/TemplateRuntimeObserverInterface.h @@ -19,6 +19,7 @@ #include #include +#include "AVSCommon/AVS/FocusState.h" #include namespace alexaClientSDK { @@ -64,8 +65,15 @@ class TemplateRuntimeObserverInterface { * Failure to do so may result in exposing or mishandling of customer data. * * @param jsonPayload The payload of the RenderTemplate directive in structured JSON format. + * @param focusState The @c FocusState of the channel used by TemplateRuntime interface. */ - virtual void renderTemplateCard(const std::string& jsonPayload) = 0; + virtual void renderTemplateCard(const std::string& jsonPayload, avsCommon::avs::FocusState focusState) = 0; + + /** + * Used to notify the observer when the client should clear the Template display card. Once the card is cleared, + * the client should call templateCardCleared(). + */ + virtual void clearTemplateCard() = 0; /** * Used to notify the observer when a RenderPlayerInfo directive is received. Once called, the client should @@ -74,10 +82,18 @@ class TemplateRuntimeObserverInterface { * * @param jsonPayload The payload of the RenderPlayerInfo directive in structured JSON format. * @param audioPlayerInfo Information on the @c AudioPlayer. + * @param focusState The @c FocusState of the channel used by TemplateRuntime interface. */ virtual void renderPlayerInfoCard( const std::string& jsonPayload, - TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo) = 0; + TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo, + avsCommon::avs::FocusState focusState) = 0; + + /** + * Used to notify the observer when the client should clear the PlayerInfo display card. Once the card is cleared, + * the client should call templateCardCleared(). + */ + virtual void clearPlayerInfoCard() = 0; }; } // namespace sdkInterfaces diff --git a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockDirectiveSequencer.h b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockDirectiveSequencer.h index 42b99c550e..96c34b9adc 100644 --- a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockDirectiveSequencer.h +++ b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockDirectiveSequencer.h @@ -33,6 +33,8 @@ class MockDirectiveSequencer : public DirectiveSequencerInterface { MOCK_METHOD1(setDialogRequestId, void(const std::string& dialogRequestId)); MOCK_METHOD1(onDirective, bool(std::shared_ptr directive)); MOCK_METHOD0(doShutdown, void()); + MOCK_METHOD0(disable, void()); + MOCK_METHOD0(enable, void()); }; inline MockDirectiveSequencer::MockDirectiveSequencer() : diff --git a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManager.h b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManager.h index 08aea8abae..f3ede1512c 100644 --- a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManager.h +++ b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManager.h @@ -32,13 +32,19 @@ class MockFocusManager : public FocusManagerInterface { bool( const std::string& channelName, std::shared_ptr channelObserver, - const std::string& activityId)); + const std::string& interface)); MOCK_METHOD2( releaseChannel, std::future( const std::string& channelName, std::shared_ptr channelObserver)); MOCK_METHOD0(stopForegroundActivity, void()); + MOCK_METHOD1( + addObserver, + void(const std::shared_ptr& observer)); + MOCK_METHOD1( + removeObserver, + void(const std::shared_ptr& observer)); }; } // namespace test diff --git a/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManagerObserver.h b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManagerObserver.h new file mode 100644 index 0000000000..32e47714c3 --- /dev/null +++ b/AVSCommon/SDKInterfaces/test/AVSCommon/SDKInterfaces/MockFocusManagerObserver.h @@ -0,0 +1,85 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_TEST_AVSCOMMON_SDKINTERFACES_MOCKFOCUSMANAGEROBSERVER_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_TEST_AVSCOMMON_SDKINTERFACES_MOCKFOCUSMANAGEROBSERVER_H_ + +#include "AVSCommon/SDKInterfaces/FocusManagerObserverInterface.h" + +#include + +#include +#include +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace sdkInterfaces { +namespace test { + +/// Mock class that implements the FocusManagerObserver. +class MockFocusManagerObserver : public FocusManagerObserverInterface { +public: + MockFocusManagerObserver(); + + MOCK_METHOD2(onFocusChanged, void(const std::string& channelName, avs::FocusState newFocus)); + + /** + * EXPECT_CALL wrapper which tracks the number of onFocusChanged calls we are expecting. + * + * @param channelName The name of the channel which changed @c FocusState. + * @param newFocus The new @c FocusState of @c channelName. + */ + void expectFocusChange(const std::string& channelName, avs::FocusState newFocus); + + /** + * Waits for @c expectFocusChange() calls to complete. + * + * @param timeout Amount of time to wait for all calls to complete. + * @return @c true if all calls completed, else @c false. + */ + bool waitForFocusChanges(std::chrono::milliseconds timeout = std::chrono::milliseconds::zero()); + +private: + size_t m_expects; + std::mutex m_mutex; + std::condition_variable m_conditionVariable; +}; + +inline MockFocusManagerObserver::MockFocusManagerObserver() : m_expects{0} { +} + +inline void MockFocusManagerObserver::expectFocusChange(const std::string& channelName, avs::FocusState newFocus) { + std::lock_guard lock(m_mutex); + EXPECT_CALL(*this, onFocusChanged(testing::StrEq(channelName), newFocus)) + .WillOnce(testing::InvokeWithoutArgs([this] { + std::lock_guard lock(m_mutex); + --m_expects; + m_conditionVariable.notify_all(); + })); + ++m_expects; +} + +inline bool MockFocusManagerObserver::waitForFocusChanges(std::chrono::milliseconds timeout) { + std::unique_lock lock(m_mutex); + return m_conditionVariable.wait_for(lock, timeout, [this] { return !m_expects; }); +} + +} // namespace test +} // namespace sdkInterfaces +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVSCOMMON_SDKINTERFACES_TEST_AVSCOMMON_SDKINTERFACES_MOCKFOCUSMANAGEROBSERVER_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/AudioFormat.h b/AVSCommon/Utils/include/AVSCommon/Utils/AudioFormat.h index c2c3c1ab71..73e853536a 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/AudioFormat.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/AudioFormat.h @@ -34,7 +34,10 @@ struct AudioFormat { */ enum class Encoding { /// Represents LPCM (Linear pulse code modulation) encoding. - LPCM + LPCM, + + /// Represents OPUS encoding. + OPUS }; /** @@ -93,6 +96,9 @@ inline std::ostream& operator<<(std::ostream& stream, const AudioFormat::Encodin case AudioFormat::Encoding::LPCM: stream << "LPCM"; break; + case AudioFormat::Encoding::OPUS: + stream << "OPUS"; + break; } return stream; } diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Configuration/ConfigurationNode.h b/AVSCommon/Utils/include/AVSCommon/Utils/Configuration/ConfigurationNode.h index ac5d9b99fa..4b8ab9dc67 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Configuration/ConfigurationNode.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Configuration/ConfigurationNode.h @@ -172,6 +172,25 @@ class ConfigurationNode { */ operator bool() const; + /** + * Common logic for getting a value of a specific type. + * + * @tparam Type The type to be gotten. + * @param key The key of the value to get. + * @param out Pointer to receive the value. May be nullptr to just test for the presence of the value. + * @param defaultValue A default output value if no value of the desired type for @c key is present. + * @param isType rapidjson::Value member function to test for the desired type. + * @param getType rapidjson::Value member function to get the desired type. + * @return Whether a value of the specified @c Type is present for @c key. + */ + template + bool getValue( + const std::string& key, + Type* out, + Type defaultValue, + bool (rapidjson::Value::*isType)() const, + Type (rapidjson::Value::*getType)() const) const; + private: /** * Constructor. @@ -192,25 +211,6 @@ class ConfigurationNode { */ bool getString(const std::string& key, const char** out, const char* defaultValue) const; - /** - * Common logic for getting a value of a specific type. - * - * @tparam Type The type to be gotten. - * @param key The key of the value to get. - * @param out Pointer to receive the value. May be nullptr to just test for the presence of the value. - * @param defaultValue A default output value if no value of the desired type for @c key is present. - * @param isType rapidjson::Value member function to test for the desired type. - * @param getType rapidjson::Value member function to get the desired type. - * @return Whether a value of the specified @c Type is present for @c key. - */ - template - bool getValue( - const std::string& key, - Type* out, - Type defaultValue, - bool (rapidjson::Value::*isType)() const, - Type (rapidjson::Value::*getType)() const) const; - /// Object value within the global configuration that this @c ConfigurationNode represents. const rapidjson::Value* m_object; @@ -237,6 +237,32 @@ bool ConfigurationNode::getDuration(const std::string& key, OutputType* out, Def return result; } +template +bool ConfigurationNode::getValue( + const std::string& key, + Type* out, + Type defaultValue, + bool (rapidjson::Value::*isType)() const, + Type (rapidjson::Value::*getType)() const) const { + if (key.empty() || !m_object) { + if (out) { + *out = defaultValue; + } + return false; + } + auto it = m_object->FindMember(key.c_str()); + if (m_object->MemberEnd() == it || !(it->value.*isType)()) { + if (out) { + *out = defaultValue; + } + return false; + } + if (out) { + *out = (it->value.*getType)(); + } + return true; +} + } // namespace configuration } // namespace utils } // namespace avsCommon diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/CurlEasyHandleWrapper.h b/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/CurlEasyHandleWrapper.h index 1ea1260fcf..be830bf33f 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/CurlEasyHandleWrapper.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/CurlEasyHandleWrapper.h @@ -20,6 +20,9 @@ #include #include +#include +#include + namespace alexaClientSDK { namespace avsCommon { namespace utils { @@ -38,7 +41,7 @@ class CurlEasyHandleWrapper { * @param numBlocks The number of "blocks" to read or write * @param userData Some user data passed in with CURLOPT_XDATA (where X = READ, WRITE, or HEADER) */ - typedef size_t (*CurlCallback)(char* buffer, size_t blockSize, size_t numBlocks, void* userData); + using CurlCallback = size_t (*)(char* buffer, size_t blockSize, size_t numBlocks, void* userData); /** * Definitions for HTTP action types @@ -81,6 +84,13 @@ class CurlEasyHandleWrapper { */ CURL* getCurlHandle(); + /** + * Used to check if curl is correctly initialized + * + * @return true if curl handler is valid + */ + bool isValid(); + /* * Adds an HTTP Header to the current easy handle * @@ -143,6 +153,14 @@ class CurlEasyHandleWrapper { */ bool setPostStream(const std::string& fieldName, void* userData); + /** + * Sets the data to be sent in the next POST operation. + * + * @param data String buffer to the full data to send in a HTTP POST operation. + * @returns Whether the operation was successful. + */ + bool setPostData(const std::string& data); + /** * Sets how long the stream should take, in seconds, to establish a connection. * If not set explicitly there is no timeout. @@ -160,8 +178,8 @@ class CurlEasyHandleWrapper { * @param userData Any data to be passed to the callback * @return Whether the addition was successful */ - bool setWriteCallback(CurlCallback callback, void* userData); + /** * Sets the callback to call when libcurl has HTTP header data available * NOTE: Each header line is provided individually @@ -170,8 +188,8 @@ class CurlEasyHandleWrapper { * @param userData Any data to be passed to the callback * @return Whether the addition was successful */ - bool setHeaderCallback(CurlCallback callback, void* userData); + /** * Sets the callback to call when libcurl requires data to POST * @@ -181,6 +199,16 @@ class CurlEasyHandleWrapper { */ bool setReadCallback(CurlCallback callback, void* userData); + /** + * Helper function for calling curl_easy_setopt and checking the result. + * + * @param option The option parameter to pass through to curl_easy_setopt. + * @param param The param option to pass through to curl_easy_setopt. + * @return @c true of the operation was successful. + */ + template + bool setopt(CURLoption option, ParamType param); + private: /** * Frees and sets the following attributes to NULL: @@ -209,6 +237,21 @@ class CurlEasyHandleWrapper { curl_httppost* m_post; }; +template +bool CurlEasyHandleWrapper::setopt(CURLoption option, ParamType value) { + auto result = curl_easy_setopt(m_handle, option, value); + if (result != CURLE_OK) { + logger::acsdkError(logger::LogEntry("CurlEasyHandleWrapper", "setoptFailed") + .d("reason", "curl_easy_setopt failed") + .d("option", option) + .sensitive("value", value) + .d("result", result) + .d("error", curl_easy_strerror(result))); + return false; + } + return true; +} + } // namespace libcurlUtils } // namespace utils } // namespace avsCommon diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/HttpPost.h b/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/HttpPost.h index 5c0e390a71..fbf770b246 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/HttpPost.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/HttpPost.h @@ -23,6 +23,7 @@ #include #include +#include "AVSCommon/Utils/LibcurlUtils/CurlEasyHandleWrapper.h" #include "AVSCommon/Utils/LibcurlUtils/HttpPostInterface.h" namespace alexaClientSDK { @@ -34,7 +35,7 @@ namespace libcurlUtils { class HttpPost : public HttpPostInterface { public: /// HttpPost destructor - ~HttpPost(); + ~HttpPost() = default; /** * Deleted copy constructor. @@ -59,34 +60,15 @@ class HttpPost : public HttpPostInterface { static std::unique_ptr create(); bool addHTTPHeader(const std::string& header) override; - long doPost(const std::string& m_url, const std::string& data, std::chrono::seconds timeout, std::string& body) + + long doPost(const std::string& url, const std::string& data, std::chrono::seconds timeout, std::string& body) override; private: /** - * HttpPost constructor. - * - * @param curl CURL handle with which to make requests. - */ - HttpPost(); - - /** - * init() is used by create() to perform initialization after construction but before returning the - * HttpPost instance so that clients only get access to fully formed instances. - * - * @return @c true if initialization is successful. + * Default HttpPost constructor. */ - bool init(); - - /** - * Helper function for calling curl_easy_setopt and checking the result. - * - * @param option The option parameter to pass through to curl_easy_setopt. - * @param param The param option to pass through to curl_easy_setopt. - * @return @c true of the operation was successful. - */ - template - bool setopt(CURLoption option, ParamType param); + HttpPost() = default; /** * Callback function used to accumulate the body of the HTTP Post response @@ -104,10 +86,7 @@ class HttpPost : public HttpPostInterface { std::mutex m_mutex; /// CURL handle with which to make requests - CURL* m_curl; - - /// A list of headers needed to be added at the HTTP level - curl_slist* m_requestHeaders; + CurlEasyHandleWrapper m_curl; /// String used to accumuate the response body. std::string m_bodyAccumulator; diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/LibCurlHttpContentFetcher.h b/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/LibCurlHttpContentFetcher.h index 8419a8f352..af91b4fa0c 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/LibCurlHttpContentFetcher.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/LibcurlUtils/LibCurlHttpContentFetcher.h @@ -41,7 +41,9 @@ class LibCurlHttpContentFetcher : public avsCommon::sdkInterfaces::HTTPContentFe * @copydoc * In this implementation, the function may only be called once. Subsequent calls will return @c nullptr. */ - std::unique_ptr getContent(FetchOptions fetchOption) override; + std::unique_ptr getContent( + FetchOptions option, + std::shared_ptr writer) override; /* * Destructor. @@ -93,8 +95,8 @@ class LibCurlHttpContentFetcher : public avsCommon::sdkInterfaces::HTTPContentFe */ std::string m_lastContentType; - /// Flag to indicate if a shutdown is occurring. - std::atomic m_shuttingDown; + /// Flag to indicate that the data-fetch operation has completed. + std::atomic m_done; /** * Internal thread that does the curl_easy_perform. The reason for using a thread is that curl_easy_perform may diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ConsoleLogger.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ConsoleLogger.h index 93d39e6efe..202d8aca74 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ConsoleLogger.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/ConsoleLogger.h @@ -17,6 +17,8 @@ #define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_LOGGER_CONSOLELOGGER_H_ #include "AVSCommon/Utils/Logger/Logger.h" +#include "AVSCommon/Utils/Logger/LoggerUtils.h" +#include "AVSCommon/Utils/Logger/LogStringFormatter.h" namespace alexaClientSDK { namespace avsCommon { @@ -45,6 +47,9 @@ class ConsoleLogger : public Logger { ConsoleLogger(); std::mutex m_coutMutex; + + /// Object to format log strings correctly. + LogStringFormatter m_logFormatter; }; /** diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LogStringFormatter.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LogStringFormatter.h new file mode 100644 index 0000000000..0f9f562356 --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LogStringFormatter.h @@ -0,0 +1,60 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_LOGGER_LOGSTRINGFORMATTER_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_LOGGER_LOGSTRINGFORMATTER_H_ + +#include + +#include "AVSCommon/Utils/Logger/Logger.h" +#include "AVSCommon/Utils/Timing/SafeCTimeAccess.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace logger { + +/** + * A class used to format log strings. + */ +class LogStringFormatter { +public: + LogStringFormatter(); + + /** + * Formats a log message into a printable string with other metadata regarding the log message. + * + * @param level The severity Level of this log line. + * @param time The time that the event to log occurred. + * @param threadMoniker Moniker of the thread that generated the event. + * @param text The text of the entry to log. + * @return The formatted string. + */ + std::string format( + Level level, + std::chrono::system_clock::time_point time, + const char* threadMoniker, + const char* text); + +private: + std::shared_ptr m_safeCTimeAccess; +}; + +} // namespace logger +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_LOGGER_LOGSTRINGFORMATTER_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerUtils.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerUtils.h index aa54190da5..99ab880e6e 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerUtils.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/LoggerUtils.h @@ -139,21 +139,6 @@ void acsdkCritical(const LogEntry& entry); */ void logEntry(Level level, const LogEntry& entry); -/** - * Formats a log message into a printable string with other metadata regarding the log message. - * - * @param level The severity Level of this log line. - * @param time The time that the event to log occurred. - * @param threadMoniker Moniker of the thread that generated the event. - * @param text The text of the entry to log. - * @return The formatted string. - */ -std::string formatLogString( - Level level, - std::chrono::system_clock::time_point time, - const char* threadMoniker, - const char* text); - /** * Stream out an array of bytes as a hex dump. * diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h index 2354c2c99b..4273370904 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Logger/SinkObserverInterface.h @@ -16,6 +16,8 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_LOGGER_SINKOBSERVERINTERFACE_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_LOGGER_SINKOBSERVERINTERFACE_H_ +#include + namespace alexaClientSDK { namespace avsCommon { namespace utils { diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h b/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h index 0e2347ee0f..be057bde7e 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/PlaylistParser/PlaylistParserObserverInterface.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_PLAYLISTPARSER_PLAYLISTPARSEROBSERVERINTERFACE_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_PLAYLISTPARSER_PLAYLISTPARSEROBSERVERINTERFACE_H_ +#include #include #include #include diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/SDKVersion.h b/AVSCommon/Utils/include/AVSCommon/Utils/SDKVersion.h new file mode 100644 index 0000000000..0840b16617 --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/SDKVersion.h @@ -0,0 +1,55 @@ +/* + * SDKVersion.h + * + * 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. + */ + + +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_SDKVERSION_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_SDKVERSION_H_ + +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { + +/// These functions are responsible for providing access to the current SDK version. +/// NOTE: To make changes to this file you *MUST* do so via SDKVersion.h.in. +namespace sdkVersion{ + +inline static std::string getCurrentVersion(){ + return "0.0.0"; +} + +inline static int getMajorVersion(){ + return 0; +} + +inline static int getMinorVersion(){ + return 0; +} + +inline static int getPatchVersion(){ + return 0; +} + + + +} // namespace sdkVersion +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_SDKVERSION_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/String/StringUtils.h b/AVSCommon/Utils/include/AVSCommon/Utils/String/StringUtils.h index bb28caac8d..721befb1a4 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/String/StringUtils.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/String/StringUtils.h @@ -53,6 +53,14 @@ bool stringToInt(const char* str, int* result); */ std::string byteVectorToString(const std::vector& byteVector); +/** + * A utility function to convert a string into lower case. + * + * @param input The input string to be converted. + * @return The converted string in lower case. + */ +std::string stringToLowerCase(const std::string& input); + } // namespace string } // namespace utils } // namespace avsCommon diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Timing/SafeCTimeAccess.h b/AVSCommon/Utils/include/AVSCommon/Utils/Timing/SafeCTimeAccess.h new file mode 100644 index 0000000000..dea27b6698 --- /dev/null +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Timing/SafeCTimeAccess.h @@ -0,0 +1,86 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_TIMING_SAFECTIMEACCESS_H_ +#define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_TIMING_SAFECTIMEACCESS_H_ + +#include +#include +#include + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace timing { + +/** + * This class allows access safe access to the multithreaded-unsafe time functions. It is a singleton because there + * needs to be a single lock that protects the time functions. That single lock is contained within this class. + */ +class SafeCTimeAccess { +public: + /** + * The method for accessing the singleton SafeCTimeAccess class. It returns a shared_ptr so classes that depend on + * this class can keep it alive until they are destroyed. + * + * @return std::shared_ptr to the singleton SafeCTimeAccess object. + */ + static std::shared_ptr instance(); + + /** + * Function to safely call std::gmtime. std::gmtime uses a static internal data structure that is not thread-safe. + * + * @param time The time since epoch to convert. + * @param[out] time The output in calendar time, expressed in UTC time. + * @return true if successful, false otherwise. + */ + bool getGmtime(const std::time_t& time, std::tm* calendarTime); + + /** + * Function to safely call std::localtime. std::localtime uses a static internal data structure that is not + * thread-safe. + * + * @param time The time since epoch to convert. + * @param[out] time The output in calendar time, expressed in UTC time. + * @return true if successful, false otherwise. + */ + bool getLocaltime(const std::time_t& time, std::tm* calendarTime); + +private: + SafeCTimeAccess() = default; + + /** + * Helper function to eliminate duplicate code, because getGmtime and getLocaltime are almost identical. + * + * @param timeAccessFunction One of two funtions (std::gmtime and std::localtime) that need to be safely accessed. + * @param time The time since epoch to convert. + * @param[out] time The output in calendar time, expressed in UTC time. + * @return true if successful, false otherwise. + */ + bool safeAccess( + std::tm* (*timeAccessFunction)(const std::time_t* time), + const std::time_t& time, + std::tm* calendarTime); + + /// Mutex used to protect access to the ctime functions. + std::mutex m_timeLock; +}; + +} // namespace timing +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_TIMING_SAFECTIMEACCESS_H_ diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimePoint.h b/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimePoint.h index fcf38c7eb8..6b90da3a32 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimePoint.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimePoint.h @@ -18,6 +18,8 @@ #include +#include "AVSCommon/Utils/Timing/TimeUtils.h" + namespace alexaClientSDK { namespace avsCommon { namespace utils { @@ -59,6 +61,9 @@ class TimePoint { std::string m_time_ISO_8601; /// The scheduled time for the alert in Unix epoch format. int64_t m_time_Unix; + + /// Object used to safely access time utilities. + TimeUtils m_timeUtils; }; } // namespace timing diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimeUtils.h b/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimeUtils.h index 7371a11334..3f2af49e6f 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimeUtils.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/Timing/TimeUtils.h @@ -16,9 +16,11 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_TIMING_TIMEUTILS_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_TIMING_TIMEUTILS_H_ -#include #include +#include + #include "AVSCommon/Utils/RetryTimer.h" +#include "AVSCommon/Utils/Timing/SafeCTimeAccess.h" namespace alexaClientSDK { namespace avsCommon { @@ -26,52 +28,93 @@ namespace utils { namespace timing { /** - * This function converts a string representing time, encoded in the ISO-8601 format, to what is commonly - * known as Unix time (epoch). - * - * For completeness, the expected format of the input string is as follows: - * - * YYYY-MM-DDTHH:MM:SS+0000 - * - * Where (in order of listing) : - * Y means year - * M means month - * D means day - * H means hour - * M means minute - * S means second - * - * So, for example: - * - * 1986-08-08T21:30:00+0000 - * - * means the year 1986, August 8th, 9:30pm. - * - * @param timeString The time string, formatted as described above. - * @param[out] unixTime The converted time into Unix epoch time. - * @return Whether the conversion was successful. + * Class used to safely access the time utilities. */ -bool convert8601TimeStringToUnix(const std::string& timeString, int64_t* unixTime); +class TimeUtils { +public: + /** + * Constructor. + */ + TimeUtils(); -/** - * Gets the current time in Unix epoch time, as a 64 bit integer. - * - * @param[out] currentTime The current time in Unix epoch time, as a 64 bit integer. - * @return Whether the get time was successful. - */ -bool getCurrentUnixTime(int64_t* currentTime); + /** + * Convert tm struct to time_t in UTC time + * + * This function is needed because mktime uses the current timezone. Hence, we calculate the current timezone + * difference, and adjust the converted time. + * + * @param utcTm time to be converted. This should be in UTC time + * @param[out] ret The converted UTC time to time_t + * @return Whether the conversion was successful. + */ + bool convertToUtcTimeT(const std::tm* utcTm, std::time_t* ret); -/** - * Convert tm struct to time_t in UTC time - * - * This function is needed because mktime uses the current timezone. Hence, we calculate the current timezone - * difference, and adjust the converted time. - * - * @param utcTm time to be converted. This should be in UTC time - * @param[out] ret The converted UTC time to time_t - * @return Whether the conversion was successful. - */ -bool convertToUtcTimeT(const std::tm* utcTm, std::time_t* ret); + /** + * This function converts a string representing time, encoded in the ISO-8601 format, to what is commonly + * known as Unix time (epoch). + * + * For completeness, the expected format of the input string is as follows: + * + * YYYY-MM-DDTHH:MM:SS+0000 + * + * Where (in order of listing) : + * Y means year + * M means month + * D means day + * H means hour + * M means minute + * S means second + * + * So, for example: + * + * 1986-08-08T21:30:00+0000 + * + * means the year 1986, August 8th, 9:30pm. + * + * @param timeString The time string, formatted as described above. + * @param[out] unixTime The converted time into Unix epoch time. + * @return Whether the conversion was successful. + */ + bool convert8601TimeStringToUnix(const std::string& timeString, int64_t* unixTime); + + /** + * Gets the current time in Unix epoch time, as a 64 bit integer. + * + * @param[out] currentTime The current time in Unix epoch time, as a 64 bit integer. + * @return Whether the get time was successful. + */ + bool getCurrentUnixTime(int64_t* currentTime); + + /** + * Convert timeval struct to a ISO 8601 RFC 3339 date-time string. This follows these specifications: + * - https://tools.ietf.org/html/rfc3339 + * + * The end result will look like "1970-01-01T00:00:00.000Z" + * + * @param t The time from the Unix epoch to convert. + * @param[out] iso8601TimeString The resulting time string. + * @return True, if successful, false otherwise. + */ + bool convertTimeToUtcIso8601Rfc3339(const struct timeval& t, std::string* iso8601TimeString); + +private: + /** + * Calculate localtime offset in std::time_t. + * + * In order to calculate the timezone offset, we call gmtime and localtime giving the same arbitrary time point. + * Then, we convert them back to time_t and calculate the conversion difference. The arbitrary time point is 24 + * hours past epoch, so we don't have to deal with negative time_t values. + * + * This function uses non-threadsafe time functions. Thus, it is important to use the SafeCTimeAccess class. + * + * @param[out] ret Required pointer to object where the result will be saved. + * @return Whether it succeeded to calculate the localtime offset. + */ + bool localtimeOffset(std::time_t* ret); + + /// Object used to safely access the system ctime functions. + std::shared_ptr m_safeCTimeAccess; +}; } // namespace timing } // namespace utils diff --git a/AVSCommon/Utils/include/AVSCommon/Utils/functional/hash.h b/AVSCommon/Utils/include/AVSCommon/Utils/functional/hash.h index 54902d4cd0..5aaade7e12 100644 --- a/AVSCommon/Utils/include/AVSCommon/Utils/functional/hash.h +++ b/AVSCommon/Utils/include/AVSCommon/Utils/functional/hash.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_FUNCTIONAL_HASH_H_ #define ALEXA_CLIENT_SDK_AVSCOMMON_UTILS_INCLUDE_AVSCOMMON_UTILS_FUNCTIONAL_HASH_H_ +#include #include #include diff --git a/AVSCommon/Utils/src/Configuration/ConfigurationNode.cpp b/AVSCommon/Utils/src/Configuration/ConfigurationNode.cpp index 871aa4d6c3..48e5036f16 100644 --- a/AVSCommon/Utils/src/Configuration/ConfigurationNode.cpp +++ b/AVSCommon/Utils/src/Configuration/ConfigurationNode.cpp @@ -154,32 +154,6 @@ bool ConfigurationNode::getString(const std::string& key, const char** out, cons return getValue(key, out, defaultValue, &Value::IsString, &Value::GetString); } -template -bool ConfigurationNode::getValue( - const std::string& key, - Type* out, - Type defaultValue, - bool (rapidjson::Value::*isType)() const, - Type (rapidjson::Value::*getType)() const) const { - if (key.empty() || !m_object) { - if (out) { - *out = defaultValue; - } - return false; - } - auto it = m_object->FindMember(key.c_str()); - if (m_object->MemberEnd() == it || !(it->value.*isType)()) { - if (out) { - *out = defaultValue; - } - return false; - } - if (out) { - *out = (it->value.*getType)(); - } - return true; -} - ConfigurationNode ConfigurationNode::operator[](const std::string& key) const { if (!*this) { return ConfigurationNode(); diff --git a/AVSCommon/Utils/src/LibcurlUtils/CurlEasyHandleWrapper.cpp b/AVSCommon/Utils/src/LibcurlUtils/CurlEasyHandleWrapper.cpp index 959e744408..57c4448cf7 100644 --- a/AVSCommon/Utils/src/LibcurlUtils/CurlEasyHandleWrapper.cpp +++ b/AVSCommon/Utils/src/LibcurlUtils/CurlEasyHandleWrapper.cpp @@ -47,12 +47,18 @@ CurlEasyHandleWrapper::CurlEasyHandleWrapper() : m_requestHeaders{nullptr}, m_postHeaders{nullptr}, m_post{nullptr} { - setDefaultOptions(); + if (m_handle == nullptr) { + ACSDK_ERROR(LX("CurlEasyHandleWrapperFailed").d("reason", "curl_easy_init failed")); + } else { + setDefaultOptions(); + } }; CurlEasyHandleWrapper::~CurlEasyHandleWrapper() { cleanupResources(); - curl_easy_cleanup(m_handle); + if (m_handle != nullptr) { + curl_easy_cleanup(m_handle); + } }; bool CurlEasyHandleWrapper::reset() { @@ -81,8 +87,6 @@ bool CurlEasyHandleWrapper::reset() { * if we receive a 204. * * This may be related to an older curl version. This workaround is confirmed unneeded for curl 7.55.1 - * - * TODO: ACSDK-104 Find a way to re-use all handles, or re-evaluate the easy handle pooling scheme */ if (HTTPResponseCode::SUCCESS_NO_CONTENT == responseCode) { ACSDK_DEBUG(LX("reset").d("responseCode", "HTTP_RESPONSE_SUCCESS_NO_CONTENT")); @@ -98,6 +102,10 @@ bool CurlEasyHandleWrapper::reset() { return setDefaultOptions(); } +bool CurlEasyHandleWrapper::isValid() { + return m_handle != nullptr; +} + CURL* CurlEasyHandleWrapper::getCurlHandle() { return m_handle; } @@ -109,16 +117,7 @@ bool CurlEasyHandleWrapper::addHTTPHeader(const std::string& header) { ACSDK_DEBUG(LX("addHTTPHeaderFailed").sensitive("header", header)); return false; } - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_HTTPHEADER, m_requestHeaders); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("addHTTPHeaderFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_HTTPHEADER") - .d("error", curl_easy_strerror(ret))); - return false; - } - return true; + return setopt(CURLOPT_HTTPHEADER, m_requestHeaders); } bool CurlEasyHandleWrapper::addPostHeader(const std::string& header) { @@ -132,46 +131,20 @@ bool CurlEasyHandleWrapper::addPostHeader(const std::string& header) { } bool CurlEasyHandleWrapper::setURL(const std::string& url) { - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_URL, url.c_str()); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setUrlFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_URL") - .sensitive("url", url) - .d("error", curl_easy_strerror(ret))); - return false; - } - return true; + return setopt(CURLOPT_URL, url.c_str()); } bool CurlEasyHandleWrapper::setTransferType(TransferType type) { - CURLcode ret; + bool ret = false; switch (type) { case TransferType::kGET: - ret = curl_easy_setopt(m_handle, CURLOPT_HTTPGET, 1L); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setTransferTypeFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_HTTPGET") - .d("error", curl_easy_strerror(ret))); - return false; - } + ret = setopt(CURLOPT_HTTPGET, 1L); break; case TransferType::kPOST: - ret = curl_easy_setopt(m_handle, CURLOPT_HTTPPOST, m_post); - if (!m_post || ret != CURLE_OK) { - ACSDK_ERROR(LX("setTransferTypeFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_HTTPPOST") - .d("error", curl_easy_strerror(ret))); - return false; - } + ret = setopt(CURLOPT_HTTPPOST, m_post); break; } - return true; + return ret; } bool CurlEasyHandleWrapper::setPostContent(const std::string& fieldName, const std::string& payload) { @@ -202,17 +175,7 @@ bool CurlEasyHandleWrapper::setPostContent(const std::string& fieldName, const s } bool CurlEasyHandleWrapper::setTransferTimeout(const long timeoutSeconds) { - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_TIMEOUT, timeoutSeconds); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setTransferTimeoutFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_TIMEOUT") - .d("timeOut", timeoutSeconds) - .d("error", curl_easy_strerror(ret))); - return false; - } - return true; + return setopt(CURLOPT_TIMEOUT, timeoutSeconds); } bool CurlEasyHandleWrapper::setPostStream(const std::string& fieldName, void* userData) { @@ -238,91 +201,24 @@ bool CurlEasyHandleWrapper::setPostStream(const std::string& fieldName, void* us return true; } -bool CurlEasyHandleWrapper::setConnectionTimeout(const std::chrono::seconds timeoutSeconds) { - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_CONNECTTIMEOUT, timeoutSeconds.count()); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setConnectionTimeoutFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_TIMEOUT") - .d("timeOut", timeoutSeconds.count()) - .d("error", curl_easy_strerror(ret))); - return false; - } +bool CurlEasyHandleWrapper::setPostData(const std::string& data) { + return setopt(CURLOPT_POSTFIELDS, data.c_str()); +} - return true; +bool CurlEasyHandleWrapper::setConnectionTimeout(const std::chrono::seconds timeoutSeconds) { + return setopt(CURLOPT_CONNECTTIMEOUT, timeoutSeconds.count()); } bool CurlEasyHandleWrapper::setWriteCallback(CurlCallback callback, void* userData) { - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_WRITEFUNCTION, callback); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setWriteCallbackFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_WRITEFUNCTION") - .d("error", curl_easy_strerror(ret))); - return false; - } - if (userData) { - ret = curl_easy_setopt(m_handle, CURLOPT_WRITEDATA, userData); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setWriteCallbackFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_WRITEDATA") - .d("error", curl_easy_strerror(ret))); - return false; - } - } - return true; + return setopt(CURLOPT_WRITEFUNCTION, callback) && (userData == nullptr || setopt(CURLOPT_WRITEDATA, userData)); } bool CurlEasyHandleWrapper::setHeaderCallback(CurlCallback callback, void* userData) { - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_HEADERFUNCTION, callback); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setHeaderCallbackFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_HEADERFUNCTION") - .d("error", curl_easy_strerror(ret))); - return false; - } - if (userData) { - ret = curl_easy_setopt(m_handle, CURLOPT_HEADERDATA, userData); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setHeaderCallbackFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_HEADERDATA") - .d("error", curl_easy_strerror(ret))); - return false; - } - } - return true; + return setopt(CURLOPT_HEADERFUNCTION, callback) && (userData == nullptr || setopt(CURLOPT_HEADERDATA, userData)); } bool CurlEasyHandleWrapper::setReadCallback(CurlCallback callback, void* userData) { - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_READFUNCTION, callback); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setReadCallbackFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_READFUNCTION") - .d("error", curl_easy_strerror(ret))); - return false; - } - if (userData) { - ret = curl_easy_setopt(m_handle, CURLOPT_READDATA, userData); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setReadCallbackFailed") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_READDATA") - .d("error", curl_easy_strerror(ret))); - return false; - } - } - return true; + return setopt(CURLOPT_READFUNCTION, callback) && (userData == nullptr || setopt(CURLOPT_READDATA, userData)); } void CurlEasyHandleWrapper::cleanupResources() { @@ -348,17 +244,9 @@ bool CurlEasyHandleWrapper::setDefaultOptions() { * The documentation from libcurl recommends setting CURLOPT_NOSIGNAL to 1 for multi-threaded applications. * https://curl.haxx.se/libcurl/c/threadsafe.html */ - CURLcode ret = curl_easy_setopt(m_handle, CURLOPT_NOSIGNAL, 1); - if (ret != CURLE_OK) { - ACSDK_ERROR(LX("setDefaultOptions") - .d("reason", "curlFailure") - .d("method", "curl_easy_setopt") - .d("option", "CURLOPT_NOSIGNAL") - .d("error", curl_easy_strerror(ret))); - return false; - } - return true; + return setopt(CURLOPT_NOSIGNAL, 1); } + ACSDK_ERROR(LX("setDefaultOptions").d("reason", "prepareForTLS failed")); curl_easy_cleanup(m_handle); m_handle = nullptr; return false; diff --git a/AVSCommon/Utils/src/LibcurlUtils/HttpPost.cpp b/AVSCommon/Utils/src/LibcurlUtils/HttpPost.cpp index e865b1fbd6..58dc60e2d5 100644 --- a/AVSCommon/Utils/src/LibcurlUtils/HttpPost.cpp +++ b/AVSCommon/Utils/src/LibcurlUtils/HttpPost.cpp @@ -37,61 +37,14 @@ static const std::string TAG("HttpPost"); std::unique_ptr HttpPost::create() { std::unique_ptr httpPost(new HttpPost()); - if (httpPost->init()) { + if (httpPost->m_curl.isValid()) { return httpPost; } return nullptr; } -HttpPost::HttpPost() : m_curl{nullptr}, m_requestHeaders{nullptr} { -} - -bool HttpPost::init() { - m_curl = curl_easy_init(); - if (!m_curl) { - ACSDK_ERROR(LX("initFailed").d("reason", "curl_easy_initFailed")); - return false; - } - if (!libcurlUtils::prepareForTLS(m_curl)) { - return false; - } - if (!setopt(CURLOPT_WRITEFUNCTION, staticWriteCallbackLocked)) { - return false; - } - /* - * The documentation from libcurl recommends setting CURLOPT_NOSIGNAL to 1 for multi-threaded applications. - * https://curl.haxx.se/libcurl/c/threadsafe.html - */ - if (!setopt(CURLOPT_NOSIGNAL, 1)) { - return false; - } - return true; -} - -HttpPost::~HttpPost() { - if (m_curl) { - curl_easy_cleanup(m_curl); - } - - if (m_requestHeaders) { - curl_slist_free_all(m_requestHeaders); - m_requestHeaders = nullptr; - } -} - bool HttpPost::addHTTPHeader(const std::string& header) { - m_requestHeaders = curl_slist_append(m_requestHeaders, header.c_str()); - if (!m_requestHeaders) { - ACSDK_ERROR(LX("addHTTPHeaderFailed") - .d("reason", "curlFailure") - .d("method", "curl_slist_append") - .sensitive("header", header)); - return false; - } - if (!setopt(CURLOPT_HTTPHEADER, m_requestHeaders)) { - return false; - } - return true; + return m_curl.addHTTPHeader(header); } long HttpPost::doPost( @@ -103,12 +56,13 @@ long HttpPost::doPost( body.clear(); - if (!setopt(CURLOPT_TIMEOUT, static_cast(timeout.count())) || !setopt(CURLOPT_URL, url.c_str()) || - !setopt(CURLOPT_POSTFIELDS, data.c_str()) || !setopt(CURLOPT_WRITEDATA, &body)) { + if (!m_curl.setTransferTimeout(static_cast(timeout.count())) || !m_curl.setURL(url) || + !m_curl.setPostData(data) || !m_curl.setWriteCallback(staticWriteCallbackLocked, &body)) { return HTTPResponseCode::HTTP_RESPONSE_CODE_UNDEFINED; } - auto result = curl_easy_perform(m_curl); + auto curlHandle = m_curl.getCurlHandle(); + auto result = curl_easy_perform(curlHandle); if (result != CURLE_OK) { ACSDK_ERROR(LX("doPostFailed") @@ -120,7 +74,7 @@ long HttpPost::doPost( } long responseCode = 0; - result = curl_easy_getinfo(m_curl, CURLINFO_RESPONSE_CODE, &responseCode); + result = curl_easy_getinfo(curlHandle, CURLINFO_RESPONSE_CODE, &responseCode); if (result != CURLE_OK) { ACSDK_ERROR(LX("doPostFailed") .d("reason", "curl_easy_getinfoFailed") @@ -135,21 +89,6 @@ long HttpPost::doPost( } } -template -bool HttpPost::setopt(CURLoption option, ParamType value) { - auto result = curl_easy_setopt(m_curl, option, value); - if (result != CURLE_OK) { - ACSDK_ERROR(LX("setoptFailed") - .d("reason", "nullCurlHandle") - .d("option", option) - .sensitive("value", value) - .d("result", result) - .d("error", curl_easy_strerror(result))); - return false; - } - return true; -} - size_t HttpPost::staticWriteCallbackLocked(char* ptr, size_t size, size_t nmemb, void* userdata) { if (!userdata) { ACSDK_ERROR(LX("staticWriteCallbackFailed").d("reason", "nullUserData")); diff --git a/AVSCommon/Utils/src/LibcurlUtils/LibCurlHttpContentFetcher.cpp b/AVSCommon/Utils/src/LibcurlUtils/LibCurlHttpContentFetcher.cpp index de4b73cad4..2e3c40bdcb 100644 --- a/AVSCommon/Utils/src/LibcurlUtils/LibCurlHttpContentFetcher.cpp +++ b/AVSCommon/Utils/src/LibcurlUtils/LibCurlHttpContentFetcher.cpp @@ -80,7 +80,7 @@ size_t LibCurlHttpContentFetcher::bodyCallback(char* data, size_t size, size_t n return 0; } LibCurlHttpContentFetcher* thisObject = static_cast(userData); - if (thisObject->m_shuttingDown) { + if (thisObject->m_done) { // In order to properly quit when downloading live content, which block forever when performing a GET request return 0; } @@ -95,7 +95,7 @@ size_t LibCurlHttpContentFetcher::bodyCallback(char* data, size_t size, size_t n if (streamWriter) { size_t targetNumBytes = size * nmemb; - while (totalBytesWritten < targetNumBytes && !thisObject->m_shuttingDown) { + while (totalBytesWritten < targetNumBytes && !thisObject->m_done) { avsCommon::avs::attachment::AttachmentWriter::WriteStatus writeStatus = avsCommon::avs::attachment::AttachmentWriter::WriteStatus::OK; @@ -135,11 +135,13 @@ LibCurlHttpContentFetcher::LibCurlHttpContentFetcher(const std::string& url) : m_url{url}, m_bodyCallbackBegan{false}, m_lastStatusCode{0}, - m_shuttingDown{false} { + m_done{false} { m_hasObjectBeenUsed.clear(); } -std::unique_ptr LibCurlHttpContentFetcher::getContent(FetchOptions fetchOption) { +std::unique_ptr LibCurlHttpContentFetcher::getContent( + FetchOptions fetchOption, + std::shared_ptr writer) { if (m_hasObjectBeenUsed.test_and_set()) { return nullptr; } @@ -165,7 +167,12 @@ std::unique_ptr LibCurlHttpContentFetcher::getCon } auto httpStatusCodeFuture = m_statusCodePromise.get_future(); auto contentTypeFuture = m_contentTypePromise.get_future(); + std::shared_ptr stream = nullptr; + + // This flag will remain false if the caller of getContent() passed in their own writer. + bool writerWasCreatedLocally = false; + switch (fetchOption) { case FetchOptions::CONTENT_TYPE: /* @@ -204,9 +211,15 @@ std::unique_ptr LibCurlHttpContentFetcher::getCon }); break; case FetchOptions::ENTIRE_BODY: - // Using the url as the identifier for the attachment - stream = std::make_shared(m_url); - m_streamWriter = stream->createWriter(sds::WriterPolicy::BLOCKING); + if (!writer) { + // Using the url as the identifier for the attachment + stream = std::make_shared(m_url); + writer = stream->createWriter(sds::WriterPolicy::BLOCKING); + writerWasCreatedLocally = true; + } + + m_streamWriter = writer; + if (!m_streamWriter) { ACSDK_ERROR(LX("getContentFailed").d("reason", "failedToCreateWriter")); return nullptr; @@ -219,7 +232,7 @@ std::unique_ptr LibCurlHttpContentFetcher::getCon ACSDK_ERROR(LX("getContentFailed").d("reason", "failedToSetCurlHeaderCallback")); return nullptr; } - m_thread = std::thread([this]() { + m_thread = std::thread([this, writerWasCreatedLocally]() { auto curlReturnValue = curl_easy_perform(m_curlWrapper.getCurlHandle()); if (curlReturnValue != CURLE_OK) { ACSDK_ERROR(LX("curlEasyPerformFailed").d("error", curl_easy_strerror(curlReturnValue))); @@ -229,10 +242,19 @@ std::unique_ptr LibCurlHttpContentFetcher::getCon m_contentTypePromise.set_value(m_lastContentType); } /* - * Curl easy perform has finished and all data has been written. Closing writer so that readers know - * when they have caught up and read everything. + * If the writer was created locally, its job is done and can be safely closed. + */ + if (writerWasCreatedLocally) { + m_streamWriter->close(); + } + + /* + * Note: If the writer was not created locally, its owner must ensure that it closes when necessary. + * In the case of a livestream, if the writer is not closed the LibCurlHttpContentFetcher + * will continue to download data indefinitely. */ - m_streamWriter->close(); + + m_done = true; }); break; default: @@ -243,7 +265,6 @@ std::unique_ptr LibCurlHttpContentFetcher::getCon } LibCurlHttpContentFetcher::~LibCurlHttpContentFetcher() { - m_shuttingDown = true; if (m_thread.joinable()) { m_thread.join(); } diff --git a/AVSCommon/Utils/src/Logger/ConsoleLogger.cpp b/AVSCommon/Utils/src/Logger/ConsoleLogger.cpp index f521d3ae88..1fe922af80 100644 --- a/AVSCommon/Utils/src/Logger/ConsoleLogger.cpp +++ b/AVSCommon/Utils/src/Logger/ConsoleLogger.cpp @@ -42,7 +42,7 @@ void ConsoleLogger::emit( const char* threadMoniker, const char* text) { std::lock_guard lock(m_coutMutex); - std::cout << formatLogString(level, time, threadMoniker, text) << std::endl; + std::cout << m_logFormatter.format(level, time, threadMoniker, text) << std::endl; } ConsoleLogger::ConsoleLogger() : Logger(Level::UNKNOWN) { diff --git a/AVSCommon/Utils/src/Logger/LogStringFormatter.cpp b/AVSCommon/Utils/src/Logger/LogStringFormatter.cpp new file mode 100644 index 0000000000..b851942e2e --- /dev/null +++ b/AVSCommon/Utils/src/Logger/LogStringFormatter.cpp @@ -0,0 +1,90 @@ +/* + * Copyright 2018 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. + */ + +#include +#include + +#include "AVSCommon/Utils/Logger/LogStringFormatter.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace logger { + +/// Format string for strftime() to produce date and time in the format "YYYY-MM-DD HH:MM:SS". +static const char* STRFTIME_FORMAT_STRING = "%Y-%m-%d %H:%M:%S"; + +/// Size of buffer needed to hold "YYYY-MM-DD HH:MM:SS" and a null terminator. +static const int DATE_AND_TIME_STRING_SIZE = 20; + +/// Separator between date/time and millis. +static const char TIME_AND_MILLIS_SEPARATOR = '.'; + +// Format string for sprintf() to produce millis in the format "nnn". +static const char* MILLIS_FORMAT_STRING = "%03d"; + +/// Size of buffer needed to hold "nnn" (milliseconds value) and a null terminator +static const int MILLIS_STRING_SIZE = 4; + +/// Separator string between milliseconds value and ExampleLogger name. +static const std::string MILLIS_AND_THREAD_SEPARATOR = " ["; + +/// Separator between thread ID and level indicator in log lines. +static const std::string THREAD_AND_LEVEL_SEPARATOR = "] "; + +/// Separator between level indicator and text in log lines. +static const char LEVEL_AND_TEXT_SEPARATOR = ' '; + +/// Number of milliseconds per second. +static const int MILLISECONDS_PER_SECOND = 1000; + +LogStringFormatter::LogStringFormatter() : m_safeCTimeAccess(timing::SafeCTimeAccess::instance()) { +} + +std::string LogStringFormatter::format( + Level level, + std::chrono::system_clock::time_point time, + const char* threadMoniker, + const char* text) { + bool dateTimeFailure = false; + bool millisecondFailure = false; + char dateTimeString[DATE_AND_TIME_STRING_SIZE]; + auto timeAsTime_t = std::chrono::system_clock::to_time_t(time); + std::tm timeAsTm; + if (!m_safeCTimeAccess->getGmtime(timeAsTime_t, &timeAsTm) || + 0 == strftime(dateTimeString, sizeof(dateTimeString), STRFTIME_FORMAT_STRING, &timeAsTm)) { + dateTimeFailure = true; + } + auto timeMillisPart = static_cast( + std::chrono::duration_cast(time.time_since_epoch()).count() % + MILLISECONDS_PER_SECOND); + char millisString[MILLIS_STRING_SIZE]; + if (std::snprintf(millisString, sizeof(millisString), MILLIS_FORMAT_STRING, timeMillisPart) < 0) { + millisecondFailure = true; + } + + std::stringstream stringToEmit; + stringToEmit << (dateTimeFailure ? "ERROR: strftime() failed. Date and time not logged." : dateTimeString) + << TIME_AND_MILLIS_SEPARATOR + << (millisecondFailure ? "ERROR: snprintf() failed. Milliseconds not logged." : millisString) + << MILLIS_AND_THREAD_SEPARATOR << threadMoniker << THREAD_AND_LEVEL_SEPARATOR + << convertLevelToChar(level) << LEVEL_AND_TEXT_SEPARATOR << text; + return stringToEmit.str(); +} + +} // namespace logger +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK diff --git a/AVSCommon/Utils/src/Logger/LoggerUtils.cpp b/AVSCommon/Utils/src/Logger/LoggerUtils.cpp index 6e14017666..be88e5f2bb 100644 --- a/AVSCommon/Utils/src/Logger/LoggerUtils.cpp +++ b/AVSCommon/Utils/src/Logger/LoggerUtils.cpp @@ -25,33 +25,6 @@ namespace avsCommon { namespace utils { namespace logger { -/// Format string for strftime() to produce date and time in the format "YYYY-MM-DD HH:MM:SS". -static const char* STRFTIME_FORMAT_STRING = "%Y-%m-%d %H:%M:%S"; - -/// Size of buffer needed to hold "YYYY-MM-DD HH:MM:SS" and a null terminator. -static const int DATE_AND_TIME_STRING_SIZE = 20; - -/// Separator between date/time and millis. -static const char TIME_AND_MILLIS_SEPARATOR = '.'; - -// Format string for sprintf() to produce millis in the format "nnn". -static const char* MILLIS_FORMAT_STRING = "%03d"; - -/// Size of buffer needed to hold "nnn" (milliseconds value) and a null terminator -static const int MILLIS_STRING_SIZE = 4; - -/// Separator string between milliseconds value and ExampleLogger name. -static const std::string MILLIS_AND_THREAD_SEPARATOR = " ["; - -/// Separator between thread ID and level indicator in log lines. -static const std::string THREAD_AND_LEVEL_SEPARATOR = "] "; - -/// Separator between level indicator and text in log lines. -static const char LEVEL_AND_TEXT_SEPARATOR = ' '; - -/// Number of milliseconds per second. -static const int MILLISECONDS_PER_SECOND = 1000; - void acsdkDebug9(const LogEntry& entry) { logEntry(Level::DEBUG9, entry); } @@ -117,36 +90,6 @@ void logEntry(Level level, const LogEntry& entry) { loggerInstance.log(level, entry); } -std::string formatLogString( - Level level, - std::chrono::system_clock::time_point time, - const char* threadMoniker, - const char* text) { - bool dateTimeFailure = false; - bool millisecondFailure = false; - char dateTimeString[DATE_AND_TIME_STRING_SIZE]; - auto timeAsTime_t = std::chrono::system_clock::to_time_t(time); - auto timeAsTmPtr = std::gmtime(&timeAsTime_t); - if (!timeAsTmPtr || 0 == strftime(dateTimeString, sizeof(dateTimeString), STRFTIME_FORMAT_STRING, timeAsTmPtr)) { - dateTimeFailure = true; - } - auto timeMillisPart = static_cast( - std::chrono::duration_cast(time.time_since_epoch()).count() % - MILLISECONDS_PER_SECOND); - char millisString[MILLIS_STRING_SIZE]; - if (std::snprintf(millisString, sizeof(millisString), MILLIS_FORMAT_STRING, timeMillisPart) < 0) { - millisecondFailure = true; - } - - std::stringstream stringToEmit; - stringToEmit << (dateTimeFailure ? "ERROR: strftime() failed. Date and time not logged." : dateTimeString) - << TIME_AND_MILLIS_SEPARATOR - << (millisecondFailure ? "ERROR: snprintf() failed. Milliseconds not logged." : millisString) - << MILLIS_AND_THREAD_SEPARATOR << threadMoniker << THREAD_AND_LEVEL_SEPARATOR - << convertLevelToChar(level) << LEVEL_AND_TEXT_SEPARATOR << text; - return stringToEmit.str(); -} - void dumpBytesToStream(std::ostream& stream, const char* prefix, size_t width, const unsigned char* data, size_t size) { std::ios incomingFormat(nullptr); incomingFormat.copyfmt(stream); diff --git a/AVSCommon/Utils/src/SafeCTimeAccess.cpp b/AVSCommon/Utils/src/SafeCTimeAccess.cpp new file mode 100644 index 0000000000..f1751d3dd4 --- /dev/null +++ b/AVSCommon/Utils/src/SafeCTimeAccess.cpp @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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. + */ + +#include "AVSCommon/Utils/Timing/SafeCTimeAccess.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace timing { + +std::shared_ptr SafeCTimeAccess::instance() { + static std::shared_ptr s_safeCTimeAccess(new SafeCTimeAccess); + return s_safeCTimeAccess; +} + +bool SafeCTimeAccess::safeAccess( + std::tm* (*timeAccessFunction)(const std::time_t* time), + const std::time_t& time, + std::tm* calendarTime) { + // No logging on errors, because it's known that logging calls this function, which can cause recursion problems. + + if (!calendarTime) { + return false; + } + + bool succeeded = false; + { + std::lock_guard lock{m_timeLock}; + auto tempCalendarTime = timeAccessFunction(&time); + if (tempCalendarTime) { + *calendarTime = *tempCalendarTime; + succeeded = true; + } + } + + return succeeded; +} + +bool SafeCTimeAccess::getGmtime(const std::time_t& time, std::tm* calendarTime) { + return safeAccess(std::gmtime, time, calendarTime); +} + +bool SafeCTimeAccess::getLocaltime(const std::time_t& time, std::tm* calendarTime) { + return safeAccess(std::localtime, time, calendarTime); +} + +} // namespace timing +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK diff --git a/AVSCommon/Utils/src/StringUtils.cpp b/AVSCommon/Utils/src/StringUtils.cpp index 7772cdb7dd..874e218c6e 100644 --- a/AVSCommon/Utils/src/StringUtils.cpp +++ b/AVSCommon/Utils/src/StringUtils.cpp @@ -12,17 +12,16 @@ * express or implied. See the License for the specific language governing * permissions and limitations under the License. */ - -#include "AVSCommon/Utils/String/StringUtils.h" - +#include +#include #include #include - -#include #include #include "AVSCommon/Utils/Logger/Logger.h" +#include "AVSCommon/Utils/String/StringUtils.h" + namespace alexaClientSDK { namespace avsCommon { namespace utils { @@ -99,6 +98,12 @@ std::string byteVectorToString(const std::vector& byteVector) { return ss.str(); } +std::string stringToLowerCase(const std::string& input) { + std::string lowerCaseString = input; + std::transform(lowerCaseString.begin(), lowerCaseString.end(), lowerCaseString.begin(), ::tolower); + return lowerCaseString; +} + } // namespace string } // namespace utils } // namespace avsCommon diff --git a/AVSCommon/Utils/src/TimePoint.cpp b/AVSCommon/Utils/src/TimePoint.cpp index 61ee1b0e3d..078f343ff9 100644 --- a/AVSCommon/Utils/src/TimePoint.cpp +++ b/AVSCommon/Utils/src/TimePoint.cpp @@ -16,7 +16,6 @@ #include "AVSCommon/Utils/Timing/TimePoint.h" #include "AVSCommon/Utils/Logger/Logger.h" -#include "AVSCommon/Utils/Timing/TimeUtils.h" namespace alexaClientSDK { namespace avsCommon { @@ -40,7 +39,7 @@ TimePoint::TimePoint() : m_time_Unix{0} { bool TimePoint::setTime_ISO_8601(const std::string& time_ISO_8601) { int64_t tempUnixTime = 0; - if (!convert8601TimeStringToUnix(time_ISO_8601, &tempUnixTime)) { + if (!m_timeUtils.convert8601TimeStringToUnix(time_ISO_8601, &tempUnixTime)) { ACSDK_ERROR(LX("setTime_ISO_8601Failed").d("input", time_ISO_8601).m("Could not convert to Unix time.")); return false; } diff --git a/AVSCommon/Utils/src/TimeUtils.cpp b/AVSCommon/Utils/src/TimeUtils.cpp index 5c694ef0de..f61a249b5e 100644 --- a/AVSCommon/Utils/src/TimeUtils.cpp +++ b/AVSCommon/Utils/src/TimeUtils.cpp @@ -13,10 +13,12 @@ * permissions and limitations under the License. */ -#include #include +#include +#include #include #include +#include #include "AVSCommon/Utils/Timing/TimeUtils.h" #include "AVSCommon/Utils/Logger/Logger.h" @@ -91,14 +93,6 @@ static const unsigned long ENCODED_TIME_STRING_EXPECTED_LENGTH = ENCODED_TIME_STRING_SECOND_OFFSET + ENCODED_TIME_STRING_SECOND_STRING_LENGTH + ENCODED_TIME_STRING_PLUS_SEPARATOR_STRING.length() + ENCODED_TIME_STRING_POSTFIX_STRING_LENGTH; -/** - * Mutex to guard calls to gmtime and localtime. - * - * Both functions use an internal shared structure making them non-threadsafe. - * @todo: ACSDK-897 We should wrap all time access functions and this mutex into one singleton class. - */ -static std::mutex timeLock; - /** * Utility function that wraps localtime conversion to std::time_t. * @@ -119,37 +113,10 @@ static bool convertToLocalTimeT(const std::tm* timeStruct, std::time_t* ret) { return *ret >= 0; } -/** - * Calculate localtime offset in std::time_t. - * - * In order to calculate the timezone offset, we call gmtime and localtime giving the same arbitrary time point. Then, - * we convert them back to time_t and calculate the conversion difference. The arbitrary time point is 24 hours past - * epoch, so we don't have to deal with negative time_t values. - * - * This function uses non-threadsafe time functions. Thus, it is important to use timeLock to guard these calls. - * - * @param[out] ret Required pointer to object where the result will be saved. - * @return Whether it succeeded to calculate the localtime offset. - */ -static bool localtimeOffset(std::time_t* ret) { - std::lock_guard lock{timeLock}; - - static const std::chrono::time_point timePoint{std::chrono::hours(24)}; - auto fixedTime = std::chrono::system_clock::to_time_t(timePoint); - - std::time_t utc; - std::time_t local; - if (!convertToLocalTimeT(std::gmtime(&fixedTime), &utc) || - !convertToLocalTimeT(std::localtime(&fixedTime), &local)) { - ACSDK_ERROR(LX("localtimeOffset").m("cannot retrieve tm struct")); - return false; - } - - *ret = utc - local; - return true; +TimeUtils::TimeUtils() : m_safeCTimeAccess{SafeCTimeAccess::instance()} { } -bool convertToUtcTimeT(const std::tm* utcTm, std::time_t* ret) { +bool TimeUtils::convertToUtcTimeT(const std::tm* utcTm, std::time_t* ret) { std::time_t converted; std::time_t offset; @@ -168,7 +135,7 @@ bool convertToUtcTimeT(const std::tm* utcTm, std::time_t* ret) { return true; } -bool convert8601TimeStringToUnix(const std::string& timeString, int64_t* convertedTime) { +bool TimeUtils::convert8601TimeStringToUnix(const std::string& timeString, int64_t* convertedTime) { // TODO : Use std::get_time once we only support compilers that implement this function (GCC 5.1+ / Clang 3.3+) if (!convertedTime) { @@ -241,7 +208,7 @@ bool convert8601TimeStringToUnix(const std::string& timeString, int64_t* convert return true; } -bool getCurrentUnixTime(int64_t* currentTime) { +bool TimeUtils::getCurrentUnixTime(int64_t* currentTime) { if (!currentTime) { ACSDK_ERROR(LX("getCurrentUnixTimeFailed").m("currentTime parameter was nullptr.")); return false; @@ -253,6 +220,55 @@ bool getCurrentUnixTime(int64_t* currentTime) { return now >= 0; } +bool TimeUtils::convertTimeToUtcIso8601Rfc3339(const struct timeval& timeVal, std::string* iso8601TimeString) { + // The length of the RFC 3339 string for the time is maximum 28 characters, include an extra byte for the '\0' + // terminator. + char buf[29]; + memset(buf, 0, sizeof(buf)); + + // Need to assign it to time_t since time_t in some platforms is long long + // and timeVal.tv_sec is long in some platforms + const time_t timeSecs = timeVal.tv_sec; + + std::tm utcTm; + if (!m_safeCTimeAccess->getGmtime(timeSecs, &utcTm)) { + ACSDK_ERROR(LX("convertTimeToUtcIso8601Rfc3339").m("cannot retrieve tm struct")); + return false; + } + + // it's possible for std::strftime to correctly return length = 0, but not with the format string used. In this + // case length == 0 is an error. + auto strftimeResult = std::strftime(buf, sizeof(buf) - 1, "%Y-%m-%dT%H:%M:%S", &utcTm); + if (strftimeResult == 0) { + ACSDK_ERROR(LX("convertTimeToUtcIso8601Rfc3339Failed").m("strftime(..) failed")); + return false; + } + + std::stringstream millisecondTrailer; + millisecondTrailer << buf << "." << std::setfill('0') << std::setw(3) << (timeVal.tv_usec / 1000) << "Z"; + + *iso8601TimeString = millisecondTrailer.str(); + return true; +} + +bool TimeUtils::localtimeOffset(std::time_t* ret) { + static const std::chrono::time_point timePoint{std::chrono::hours(24)}; + auto fixedTime = std::chrono::system_clock::to_time_t(timePoint); + + std::tm utcTm; + std::time_t utc; + std::tm localTm; + std::time_t local; + if (!m_safeCTimeAccess->getGmtime(fixedTime, &utcTm) || !convertToLocalTimeT(&utcTm, &utc) || + !m_safeCTimeAccess->getLocaltime(fixedTime, &localTm) || !convertToLocalTimeT(&localTm, &local)) { + ACSDK_ERROR(LX("localtimeOffset").m("cannot retrieve tm struct")); + return false; + } + + *ret = utc - local; + return true; +} + } // namespace timing } // namespace utils } // namespace avsCommon diff --git a/AVSCommon/Utils/test/SafeTimeAccessTest.cpp b/AVSCommon/Utils/test/SafeTimeAccessTest.cpp new file mode 100644 index 0000000000..6e56dc436d --- /dev/null +++ b/AVSCommon/Utils/test/SafeTimeAccessTest.cpp @@ -0,0 +1,219 @@ +/* + * Copyright 2018 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. + */ + +#include +#include + +#include + +#include "AVSCommon/Utils/Timing/SafeCTimeAccess.h" + +namespace alexaClientSDK { +namespace avsCommon { +namespace utils { +namespace timing { +namespace test { + +/// A bound on the upper time to check. +const time_t LARGE_TIME_VALUE = (0x1 << 30) - 1; + +/// Test to verify that getGmtime returns failure if a nullptr is passed for the result variable. +TEST(SafeCTimeAccessTest, getGmtimeNullReturnValue) { + auto safeCTimeAccess = SafeCTimeAccess::instance(); + ASSERT_FALSE(safeCTimeAccess->getGmtime(0, nullptr)); +} + +/// Test to verify that getLocaltime returns failure if a nullptr is passed for the result variable. +TEST(SafeCTimeAccessTest, getLocaltimeNullReturnValue) { + auto safeCTimeAccess = SafeCTimeAccess::instance(); + ASSERT_FALSE(safeCTimeAccess->getLocaltime(0, nullptr)); +} + +/** + * Utility function to check to see if two std::tm objects are equal. + * + * @param a One of the structures to check. + * @param b The other structure to check. + */ +static void checkTm(const std::tm& a, const std::tm& b) { + EXPECT_EQ(a.tm_sec, b.tm_sec); + EXPECT_EQ(a.tm_min, b.tm_min); + EXPECT_EQ(a.tm_hour, b.tm_hour); + EXPECT_EQ(a.tm_mday, b.tm_mday); + EXPECT_EQ(a.tm_mon, b.tm_mon); + EXPECT_EQ(a.tm_year, b.tm_year); + EXPECT_EQ(a.tm_wday, b.tm_wday); + EXPECT_EQ(a.tm_yday, b.tm_yday); + EXPECT_EQ(a.tm_isdst, b.tm_isdst); +} + +/** + * Helper function to run through the test cases for getGmtime testing. + * + * @param expected The std::tm structure string that getGmtime should generate. + * @param t The time_t to convert. + */ +static void testGmtimeHelper(const std::tm& expected, const time_t& t) { + auto safeCTimeAccess = SafeCTimeAccess::instance(); + std::tm result; + EXPECT_TRUE(safeCTimeAccess->getGmtime(t, &result)); + checkTm(expected, result); +} + +/** + * Helper function to run through the test cases for getLocaltime testing. + * + * @param expected The std::tm structure string that getLocaltime should generate. + * @param t The time_t to convert. + */ +static void testLocaltimeHelper(const std::tm& expected, const time_t& t) { + auto safeCTimeAccess = SafeCTimeAccess::instance(); + std::tm result; + EXPECT_TRUE(safeCTimeAccess->getLocaltime(t, &result)); + checkTm(expected, result); +} + +/// Test to verify that getGmtime returns the correct calendar date for the Unix epoch. +TEST(SafeCTimeAccessTest, getGmtimeAtTheEpoch) { + std::tm epoch; + epoch.tm_sec = 0; + epoch.tm_min = 0; + epoch.tm_hour = 0; + epoch.tm_mday = 1; + epoch.tm_mon = 0; + epoch.tm_year = 70; + epoch.tm_wday = 4; + epoch.tm_yday = 0; + epoch.tm_isdst = 0; + testGmtimeHelper(epoch, 0); +} + +/// Test to verify that getGmtime returns the same calendar date as std::gmtime. +TEST(SafeCTimeAccessTest, getGmtime) { + for (time_t t = 0; t < LARGE_TIME_VALUE; t = 2 * (t + 1)) { + auto gmtimeResult = std::gmtime(&t); + ASSERT_NE(nullptr, gmtimeResult); + testGmtimeHelper(*gmtimeResult, t); + } +} + +/// Test to verify that getLocaltime returns the same calendar date as std::localtime. +TEST(SafeCTimeAccessTest, getLocaltime) { + for (time_t t = 0; t < LARGE_TIME_VALUE; t = 2 * (t + 1)) { + auto localtimeResult = std::localtime(&t); + ASSERT_NE(nullptr, localtimeResult); + testLocaltimeHelper(*localtimeResult, t); + } +} + +/** + * The test code for SafeCTimeTest::getGmtime and SafeCTimeTest::getLocaltime is almost identical, this allows switching + * between them. + */ +enum class TestType { GMTIME, LOCALTIME }; + +/// Mutex to guard access to the results vector. +static std::mutex g_resultsLock; + +/** + * Function that accesses the safe time functions that is called from many threads. We call the function in a tight + * loop with no lock and save the results to be checked later. + * + * @param startingSeed This is used to make sure that different time values are checked. + * @param type The flavor of testing to perform. Can be GMTIME or LOCALTIME. + * @param results A vector where the results are appended to be checked later. + */ +static void callSafeCTimeFunction( + int startingSeed, + const TestType& type, + std::vector>* results) { + auto safeCTimeAccess = SafeCTimeAccess::instance(); + std::vector> internalResults; + for (int i = 0; i < 4; ++i) { + for (time_t t = startingSeed; t < LARGE_TIME_VALUE; t = 1.5 * (t + 1)) { + std::tm result; + switch (type) { + case TestType::GMTIME: + EXPECT_TRUE(safeCTimeAccess->getGmtime(t, &result)); + break; + case TestType::LOCALTIME: + EXPECT_TRUE(safeCTimeAccess->getLocaltime(t, &result)); + break; + default: + EXPECT_TRUE(false); + break; + }; + internalResults.push_back(std::make_pair(t, result)); + } + } + + std::lock_guard lock{g_resultsLock}; + results->insert(std::end(*results), std::begin(internalResults), std::end(internalResults)); +} + +/** + * Main function for testing multithreaded access to the safe time functions. This function starts multiple threads + * that continuously access the time functions in an externally thread-unsafe manner. The results are collected in a + * single vector, where they are compared against the matching system function. + * + * @param type The flavor of testing to perform. Can be GMTIME or LOCALTIME. + */ +static void checkSafeCTimeFunction(const TestType& type) { + std::vector> results; + const int NUMBER_OF_THREADS = 254; + std::thread threads[NUMBER_OF_THREADS]; + for (int threadIndex = 0; threadIndex < NUMBER_OF_THREADS; ++threadIndex) { + threads[threadIndex] = std::thread(callSafeCTimeFunction, threadIndex, type, &results); + } + + for (int threadIndex = 0; threadIndex < NUMBER_OF_THREADS; ++threadIndex) { + threads[threadIndex].join(); + } + + for (const auto& result : results) { + std::tm* stdResult; + switch (type) { + case TestType::GMTIME: + stdResult = std::gmtime(&result.first); + break; + case TestType::LOCALTIME: + stdResult = std::localtime(&result.first); + break; + default: + EXPECT_TRUE(false); + break; + }; + EXPECT_NE(nullptr, stdResult); + checkTm(*stdResult, result.second); + } +} + +/// Test to make sure that multithreaded access SafeCTimeAccess::getGmtime is safe. +TEST(SafeCTimeAccessTest, DISABLED_gmTimeMultithreadedAccess) { + // TODO: ACSDK-1208 investigate Pi failure + checkSafeCTimeFunction(TestType::GMTIME); +} + +/// Test to make sure that multithreaded access SafeCTimeAccess::getLocaltimetime is safe. +TEST(SafeCTimeAccessTest, DISABLED_localtimeMultithreadedAccess) { + // TODO: ACSDK-1208 investigate Pi failure + checkSafeCTimeFunction(TestType::LOCALTIME); +} + +} // namespace test +} // namespace timing +} // namespace utils +} // namespace avsCommon +} // namespace alexaClientSDK diff --git a/AVSCommon/Utils/test/StringUtilsTest.cpp b/AVSCommon/Utils/test/StringUtilsTest.cpp index b55da83e5b..12cf1eb84c 100644 --- a/AVSCommon/Utils/test/StringUtilsTest.cpp +++ b/AVSCommon/Utils/test/StringUtilsTest.cpp @@ -188,6 +188,34 @@ TEST(StringUtilsTest, testMultipleNumbers) { ASSERT_FALSE(stringToInt("1 2 3", &result)); } +/** + * Verify that converting a empty string to lower case works. + */ +TEST(StringUtilsTest, testToLowerEmptyString) { + ASSERT_EQ(stringToLowerCase(""), ""); +} + +/** + * Verify that converting a lower case string to lower case works. + */ +TEST(StringUtilsTest, testToLowerCaseString) { + ASSERT_EQ(stringToLowerCase("abc"), "abc"); +} + +/** + * Verify that converting a Upper case string to lower case works. + */ +TEST(StringUtilsTest, testToUpperCaseString) { + ASSERT_EQ(stringToLowerCase("ABC"), "abc"); +} + +/** + * Verify that converting a Camel case string to lower case works. + */ +TEST(StringUtilsTest, testToCamelCaseString) { + ASSERT_EQ(stringToLowerCase("AbCd"), "abcd"); +} + } // namespace test } // namespace utils } // namespace avsCommon diff --git a/AVSCommon/Utils/test/TimeUtilsTest.cpp b/AVSCommon/Utils/test/TimeUtilsTest.cpp index b4010d0a62..46a6fb1b73 100644 --- a/AVSCommon/Utils/test/TimeUtilsTest.cpp +++ b/AVSCommon/Utils/test/TimeUtilsTest.cpp @@ -13,10 +13,12 @@ * permissions and limitations under the License. */ -#include #include +#include + #include +#include "AVSCommon/Utils/Timing/SafeCTimeAccess.h" #include "AVSCommon/Utils/Timing/TimeUtils.h" namespace alexaClientSDK { @@ -26,56 +28,98 @@ namespace timing { namespace test { TEST(TimeTest, testStringConversion) { + TimeUtils timeUtils; std::string dateStr{"1986-08-10T21:30:00+0000"}; int64_t date; - auto success = convert8601TimeStringToUnix(dateStr, &date); + auto success = timeUtils.convert8601TimeStringToUnix(dateStr, &date); ASSERT_TRUE(success); auto dateTimeT = static_cast(date); - auto dateTm = std::gmtime(&dateTimeT); - ASSERT_EQ(dateTm->tm_year, 86); - ASSERT_EQ(dateTm->tm_mon, 7); - ASSERT_EQ(dateTm->tm_mday, 10); - ASSERT_EQ(dateTm->tm_hour, 21); - ASSERT_EQ(dateTm->tm_min, 30); + std::tm dateTm; + auto safeCTimeAccess = SafeCTimeAccess::instance(); + ASSERT_TRUE(safeCTimeAccess->getGmtime(dateTimeT, &dateTm)); + ASSERT_EQ(dateTm.tm_year, 86); + ASSERT_EQ(dateTm.tm_mon, 7); + ASSERT_EQ(dateTm.tm_mday, 10); + ASSERT_EQ(dateTm.tm_hour, 21); + ASSERT_EQ(dateTm.tm_min, 30); } TEST(TimeTest, testStringConversionError) { + TimeUtils timeUtils; std::string dateStr{"1986-8-10T21:30:00+0000"}; int64_t date; - auto success = convert8601TimeStringToUnix(dateStr, &date); + auto success = timeUtils.convert8601TimeStringToUnix(dateStr, &date); ASSERT_FALSE(success); } TEST(TimeTest, testStringConversionNullParam) { + TimeUtils timeUtils; std::string dateStr{"1986-8-10T21:30:00+0000"}; - auto success = convert8601TimeStringToUnix(dateStr, nullptr); + auto success = timeUtils.convert8601TimeStringToUnix(dateStr, nullptr); ASSERT_FALSE(success); } TEST(TimeTest, testTimeConversion) { + TimeUtils timeUtils; std::time_t randomDate = 524089800; - auto date = std::gmtime(&randomDate); + std::tm date; + auto safeCTimeAccess = SafeCTimeAccess::instance(); + ASSERT_TRUE(safeCTimeAccess->getGmtime(randomDate, &date)); std::time_t convertBack; - auto success = convertToUtcTimeT(date, &convertBack); + auto success = timeUtils.convertToUtcTimeT(&date, &convertBack); ASSERT_TRUE(success); ASSERT_EQ(randomDate, convertBack); } TEST(TimeTest, testCurrentTime) { + TimeUtils timeUtils; int64_t time = -1; - auto success = getCurrentUnixTime(&time); + auto success = timeUtils.getCurrentUnixTime(&time); ASSERT_TRUE(success); ASSERT_GT(time, 0); } TEST(TimeTest, testCurrentTimeNullParam) { - auto success = getCurrentUnixTime(nullptr); + TimeUtils timeUtils; + auto success = timeUtils.getCurrentUnixTime(nullptr); ASSERT_FALSE(success); } +/** + * Helper function to run through the test cases for time to string conversions. + * + * @param expectedString The string that convertTimeToUtcIso8601Rfc3339 should generate. + * @param t The timeval to convert. + */ +static void testIso8601ConversionHelper(const std::string& expectedString, const struct timeval& t) { + TimeUtils timeUtils; + std::string resultString; + EXPECT_TRUE(timeUtils.convertTimeToUtcIso8601Rfc3339(t, &resultString)); + EXPECT_EQ(expectedString, resultString); +} + +TEST(TimeTest, testIso8601Conversion) { + testIso8601ConversionHelper("1970-01-01T00:00:00.000Z", {0, 0}); + testIso8601ConversionHelper("1970-01-01T00:00:01.000Z", {1, 0}); + testIso8601ConversionHelper("1970-01-01T00:00:00.001Z", {0, 1000}); + testIso8601ConversionHelper("1970-01-01T00:01:00.000Z", {60, 0}); + testIso8601ConversionHelper("1970-01-01T01:00:00.000Z", {60 * 60, 0}); + testIso8601ConversionHelper("1970-01-02T00:00:00.000Z", {60 * 60 * 24, 0}); + testIso8601ConversionHelper("1970-02-01T00:00:00.000Z", {60 * 60 * 24 * 31, 0}); + testIso8601ConversionHelper("1971-01-01T00:00:00.000Z", {60 * 60 * 24 * 365, 0}); + + testIso8601ConversionHelper("1970-01-02T00:00:00.000Z", {60 * 60 * 24, 999}); + testIso8601ConversionHelper("1970-01-02T00:00:00.001Z", {60 * 60 * 24, 1000}); + testIso8601ConversionHelper("1970-01-02T00:00:00.001Z", {60 * 60 * 24, 1001}); + testIso8601ConversionHelper("1970-01-02T00:00:00.001Z", {60 * 60 * 24, 1999}); + testIso8601ConversionHelper("1970-01-02T00:00:00.002Z", {60 * 60 * 24, 2000}); + testIso8601ConversionHelper("1970-01-02T00:00:00.002Z", {60 * 60 * 24, 2001}); + testIso8601ConversionHelper("1970-01-02T00:00:00.202Z", {60 * 60 * 24, 202001}); +} + } // namespace test } // namespace timing } // namespace utils diff --git a/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h b/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h index 5017a0b4fb..2f7f335c7b 100644 --- a/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h +++ b/ApplicationUtilities/DefaultClient/include/DefaultClient/DefaultClient.h @@ -19,7 +19,9 @@ #include #include #include +#include #include +#include #include #include #include @@ -45,6 +47,7 @@ #include #include #include +#include #include #include #include @@ -71,6 +74,8 @@ class DefaultClient { * * @param externalMusicProviderMediaPlayers The map of to use to play content from each * external music provider. + * @param adapterCreationMap The map of to use when creating the adapters for the + * different music providers supported by ExternalMediaPlayer. * @param speakMediaPlayer The media player to use to play Alexa speech from. * @param audioMediaPlayer The media player to use to play Alexa audio content from. * @param alertsMediaPlayer The media player to use to play alerts from. @@ -83,10 +88,12 @@ class DefaultClient { * @param audioFactory The audioFactory is a component that provides unique audio streams. * @param authDelegate The component that provides the client with valid LWA authorization. * @param alertStorage The storage interface that will be used to store alerts. + * @param messageStorage The storage interface that will be used to store certified sender messages. * @param notificationsStorage The storage interface that will be used to store notification indicators. * @param alexaDialogStateObservers Observers that can be used to be notified of Alexa dialog related UX state * changes. * @param connectionObservers Observers that can be used to be notified of connection status changes. + * @param isGuiSupported Whether the device supports GUI. * @param firmwareVersion The firmware version to report to @c AVS or @c INVALID_FIRMWARE_VERSION. * @param sendSoftwareInfoOnConnected Whether to send SoftwareInfo upon connecting to @c AVS. * @param softwareInfoSenderObserver Object to receive notifications about sending SoftwareInfo. @@ -97,8 +104,9 @@ class DefaultClient { * TODO: Allow the user to pass in a MediaPlayer factory rather than each media player individually. */ static std::unique_ptr create( - std::unordered_map>& + const std::unordered_map>& externalMusicProviderMediaPlayers, + const capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::AdapterCreationMap& adapterCreationMap, std::shared_ptr speakMediaPlayer, std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, @@ -111,12 +119,14 @@ class DefaultClient { std::shared_ptr audioFactory, std::shared_ptr authDelegate, std::shared_ptr alertStorage, + std::shared_ptr messageStorage, std::shared_ptr notificationsStorage, std::shared_ptr settingsStorage, std::unordered_set> alexaDialogStateObservers, std::unordered_set> connectionObservers, + bool isGuiSupported, avsCommon::sdkInterfaces::softwareInfo::FirmwareVersion firmwareVersion = avsCommon::sdkInterfaces::softwareInfo::INVALID_FIRMWARE_VERSION, bool sendSoftwareInfoOnConnected = false, @@ -225,6 +235,11 @@ class DefaultClient { void removeTemplateRuntimeObserver( std::shared_ptr observer); + /** + * Notify the TemplateRuntime Capability Agent that the display card is cleared from the screen. + */ + void TemplateRuntimeDisplayCardCleared(); + /** * Adds an observer to a single setting to be notified of that setting change. * @@ -302,6 +317,13 @@ class DefaultClient { */ std::shared_ptr getSpeakerManager(); + /** + * Get a shared_ptr to the RegistrationManager. + * + * @return shared_ptr to the RegistrationManager. + */ + std::shared_ptr getRegistrationManager(); + /** * Update the firmware version. * @@ -376,6 +398,8 @@ class DefaultClient { * * @param externalMusicProviderMediaPlayers The map of to use to play content from each * external music provider. + * @param adapterCreationMap The map of to use when creating the adapters for the + * different music providers supported by ExternalMediaPlayer. * @param speakMediaPlayer The media player to use to play Alexa speech from. * @param audioMediaPlayer The media player to use to play Alexa audio content from. * @param alertsMediaPlayer The media player to use to play alerts from. @@ -388,18 +412,21 @@ class DefaultClient { * @param audioFactory The audioFactory is a component the provides unique audio streams. * @param authDelegate The component that provides the client with valid LWA authorization. * @param alertStorage The storage interface that will be used to store alerts. + * @param messageStorage The storage interface that will be used to store certified sender messages. * @param notificationsStorage The storage interface that will be used to store notification indicators. * @param alexaDialogStateObservers Observers that can be used to be notified of Alexa dialog related UX state * changes. * @param connectionObservers Observers that can be used to be notified of connection status changes. + * @param isGuiSupported Whether the device supports GUI. * @param firmwareVersion The firmware version to report to @c AVS or @c INVALID_FIRMWARE_VERSION. * @param sendSoftwareInfoOnConnected Whether to send SoftwareInfo upon connecting to @c AVS. * @param softwareInfoSenderObserver Object to receive notifications about sending SoftwareInfo. * @return Whether the SDK was initialized properly. */ bool initialize( - std::unordered_map>& + const std::unordered_map>& externalMusicProviderMediaPlayers, + const capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::AdapterCreationMap& adapterCreationMap, std::shared_ptr speakMediaPlayer, std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, @@ -412,12 +439,14 @@ class DefaultClient { std::shared_ptr audioFactory, std::shared_ptr authDelegate, std::shared_ptr alertStorage, + std::shared_ptr messageStorage, std::shared_ptr notificationsStorage, std::shared_ptr settingsStorage, std::unordered_set> alexaDialogStateObservers, std::unordered_set> connectionObservers, + bool isGuiSupported, avsCommon::sdkInterfaces::softwareInfo::FirmwareVersion firmwareVersion, bool sendSoftwareInfoOnConnected, std::shared_ptr softwareInfoSenderObserver); @@ -425,8 +454,17 @@ class DefaultClient { /// The directive sequencer. std::shared_ptr m_directiveSequencer; - /// The focus manager. - std::shared_ptr m_focusManager; + /// The focus manager for audio channels. + std::shared_ptr m_audioFocusManager; + + /// The focus manager for visual channels. + std::shared_ptr m_visualFocusManager; + + /// The audio activity tracker. + std::shared_ptr m_audioActivityTracker; + + /// The visual activity tracker. + std::shared_ptr m_visualActivityTracker; /// The message router. std::shared_ptr m_messageRouter; @@ -481,6 +519,9 @@ class DefaultClient { /// The System.SoftwareInfoSender capability agent. std::shared_ptr m_softwareInfoSender; + + /// The RegistrationManager used to control customer registration. + std::shared_ptr m_registrationManager; }; } // namespace defaultClient diff --git a/ApplicationUtilities/DefaultClient/src/CMakeLists.txt b/ApplicationUtilities/DefaultClient/src/CMakeLists.txt index 6afc983806..ebfe54544c 100644 --- a/ApplicationUtilities/DefaultClient/src/CMakeLists.txt +++ b/ApplicationUtilities/DefaultClient/src/CMakeLists.txt @@ -21,7 +21,8 @@ target_link_libraries(DefaultClient AudioPlayer ExternalMediaPlayer AVSSystem - ContextManager) + ContextManager + RegistrationManager) # install target asdk_install() diff --git a/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp b/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp index 3cce1286a3..f8ba4a13c0 100644 --- a/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp +++ b/ApplicationUtilities/DefaultClient/src/DefaultClient.cpp @@ -39,8 +39,9 @@ static const std::string TAG("DefaultClient"); #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) std::unique_ptr DefaultClient::create( - std::unordered_map>& + const std::unordered_map>& externalMusicProviderMediaPlayers, + const capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::AdapterCreationMap& adapterCreationMap, std::shared_ptr speakMediaPlayer, std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, @@ -53,18 +54,21 @@ std::unique_ptr DefaultClient::create( std::shared_ptr audioFactory, std::shared_ptr authDelegate, std::shared_ptr alertStorage, + std::shared_ptr messageStorage, std::shared_ptr notificationsStorage, std::shared_ptr settingsStorage, std::unordered_set> alexaDialogStateObservers, std::unordered_set> connectionObservers, + bool isGuiSupported, avsCommon::sdkInterfaces::softwareInfo::FirmwareVersion firmwareVersion, bool sendSoftwareInfoOnConnected, std::shared_ptr softwareInfoSenderObserver) { std::unique_ptr defaultClient(new DefaultClient()); if (!defaultClient->initialize( externalMusicProviderMediaPlayers, + adapterCreationMap, speakMediaPlayer, audioMediaPlayer, alertsMediaPlayer, @@ -77,10 +81,12 @@ std::unique_ptr DefaultClient::create( audioFactory, authDelegate, alertStorage, + messageStorage, notificationsStorage, settingsStorage, alexaDialogStateObservers, connectionObservers, + isGuiSupported, firmwareVersion, sendSoftwareInfoOnConnected, softwareInfoSenderObserver)) { @@ -91,8 +97,9 @@ std::unique_ptr DefaultClient::create( } bool DefaultClient::initialize( - std::unordered_map>& + const std::unordered_map>& externalMusicProviderMediaPlayers, + const capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::AdapterCreationMap& adapterCreationMap, std::shared_ptr speakMediaPlayer, std::shared_ptr audioMediaPlayer, std::shared_ptr alertsMediaPlayer, @@ -105,12 +112,14 @@ bool DefaultClient::initialize( std::shared_ptr audioFactory, std::shared_ptr authDelegate, std::shared_ptr alertStorage, + std::shared_ptr messageStorage, std::shared_ptr notificationsStorage, std::shared_ptr settingsStorage, std::unordered_set> alexaDialogStateObservers, std::unordered_set> connectionObservers, + bool isGuiSupported, avsCommon::sdkInterfaces::softwareInfo::FirmwareVersion firmwareVersion, bool sendSoftwareInfoOnConnected, std::shared_ptr softwareInfoSenderObserver) { @@ -151,12 +160,10 @@ bool DefaultClient::initialize( } /* - * Creating the Focus Manager - This component deals with the management of layered audio focus across various - * components. It handles granting access to Channels as well as pushing different "Channels" to foreground, - * background, or no focus based on which other Channels are active and the priorities of those Channels. Each - * Capability Agent will require the Focus Manager in order to request access to the Channel it wishes to play on. + * Creating customerDataManager which will be used by the registrationManager and all classes that extend + * CustomerDataHandler */ - m_focusManager = std::make_shared(); + auto customerDataManager = std::make_shared(); /* * Creating the Attachment Manager - This component deals with managing attachments and allows for readers and @@ -188,9 +195,8 @@ bool DefaultClient::initialize( * formatted AVS Events) will be sent to AVS. This nicely decouples strict message sending from components which * require an Event be sent, even in conditions when there is no active AVS connection. */ - auto messageStorage = std::make_shared(); - m_certifiedSender = - certifiedSender::CertifiedSender::create(m_connectionManager, m_connectionManager, messageStorage); + m_certifiedSender = certifiedSender::CertifiedSender::create( + m_connectionManager, m_connectionManager, messageStorage, customerDataManager); if (!m_certifiedSender) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateCertifiedSender")); return false; @@ -227,6 +233,13 @@ bool DefaultClient::initialize( m_connectionManager->addMessageObserver(messageInterpreter); + /* + * Creating the Registration Manager - This component is responsible for implementing any customer registration + * operation such as login and logout + */ + m_registrationManager = std::make_shared( + m_directiveSequencer, m_connectionManager, customerDataManager); + /* * Creating the Context Manager - This component manages the context of each of the components to update to AVS. * It is required for each of the capability agents so that they may provide their state just before any event is @@ -239,6 +252,21 @@ bool DefaultClient::initialize( } acl::PostConnectObject::init(contextManager); + /* + * Creating the Audio Activity Tracker - This component is responsibly for reporting the audio channel focus + * information to AVS. + */ + m_audioActivityTracker = afml::AudioActivityTracker::create(contextManager); + + /* + * Creating the Focus Manager - This component deals with the management of layered audio focus across various + * components. It handles granting access to Channels as well as pushing different "Channels" to foreground, + * background, or no focus based on which other Channels are active and the priorities of those Channels. Each + * Capability Agent will require the Focus Manager in order to request access to the Channel it wishes to play on. + */ + m_audioFocusManager = + std::make_shared(afml::FocusManager::DEFAULT_AUDIO_CHANNELS, m_audioActivityTracker); + /* * Creating the User Inactivity Monitor - This component is responsibly for updating AVS of user inactivity as * described in the System Interface of AVS. @@ -258,7 +286,7 @@ bool DefaultClient::initialize( m_directiveSequencer, m_connectionManager, contextManager, - m_focusManager, + m_audioFocusManager, m_dialogUXStateAggregator, m_exceptionSender, userInactivityMonitor); @@ -276,7 +304,7 @@ bool DefaultClient::initialize( m_speechSynthesizer = capabilityAgents::speechSynthesizer::SpeechSynthesizer::create( speakMediaPlayer, m_connectionManager, - m_focusManager, + m_audioFocusManager, contextManager, m_exceptionSender, m_dialogUXStateAggregator); @@ -313,7 +341,12 @@ bool DefaultClient::initialize( * interface of AVS. */ m_audioPlayer = capabilityAgents::audioPlayer::AudioPlayer::create( - audioMediaPlayer, m_connectionManager, m_focusManager, contextManager, m_exceptionSender, m_playbackRouter); + audioMediaPlayer, + m_connectionManager, + m_audioFocusManager, + contextManager, + m_exceptionSender, + m_playbackRouter); if (!m_audioPlayer) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateAudioPlayer")); return false; @@ -326,12 +359,13 @@ bool DefaultClient::initialize( m_alertsCapabilityAgent = capabilityAgents::alerts::AlertsCapabilityAgent::create( m_connectionManager, m_certifiedSender, - m_focusManager, + m_audioFocusManager, contextManager, m_exceptionSender, alertStorage, audioFactory->alerts(), - capabilityAgents::alerts::renderer::Renderer::create(alertsMediaPlayer)); + capabilityAgents::alerts::renderer::Renderer::create(alertsMediaPlayer), + customerDataManager); if (!m_alertsCapabilityAgent) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateAlertsCapabilityAgent")); return false; @@ -350,7 +384,8 @@ bool DefaultClient::initialize( capabilityAgents::notifications::NotificationRenderer::create(notificationsMediaPlayer), contextManager, m_exceptionSender, - audioFactory->notifications()); + audioFactory->notifications(), + customerDataManager); if (!m_notificationsCapabilityAgent) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateNotificationsCapabilityAgent")); return false; @@ -366,7 +401,8 @@ bool DefaultClient::initialize( /* * Creating the Setting object - This component implements the Setting interface of AVS. */ - m_settings = capabilityAgents::settings::Settings::create(settingsStorage, {settingsUpdatedEventSender}); + m_settings = capabilityAgents::settings::Settings::create( + settingsStorage, {settingsUpdatedEventSender}, customerDataManager); if (!m_settings) { ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateSettingsObject")); @@ -394,9 +430,10 @@ bool DefaultClient::initialize( */ m_externalMediaPlayer = capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::create( externalMusicProviderMediaPlayers, + adapterCreationMap, m_speakerManager, m_connectionManager, - m_focusManager, + m_audioFocusManager, contextManager, m_exceptionSender, m_playbackRouter); @@ -407,14 +444,34 @@ bool DefaultClient::initialize( m_speakerManager->addSpeaker(m_externalMediaPlayer); - /* - * Creating the TemplateRuntime Capability Agent - This component is the Capability Agent that implements the - * TemplateRuntime interface of AVS. - */ - m_templateRuntime = capabilityAgents::templateRuntime::TemplateRuntime::create(m_audioPlayer, m_exceptionSender); - if (!m_templateRuntime) { - ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateTemplateRuntimeCapabilityAgent")); - return false; + if (isGuiSupported) { + /* + * Creating the Visual Activity Tracker - This component is responsibly for reporting the visual channel focus + * information to AVS. + */ + m_visualActivityTracker = afml::VisualActivityTracker::create(contextManager); + + /* + * Creating the Visual Focus Manager - This component deals with the management of visual focus across various + * components. It handles granting access to Channels as well as pushing different "Channels" to foreground, + * background, or no focus based on which other Channels are active and the priorities of those Channels. Each + * Capability Agent will require the Focus Manager in order to request access to the Channel it wishes to play + * on. + */ + m_visualFocusManager = + std::make_shared(afml::FocusManager::DEFAULT_VISUAL_CHANNELS, m_visualActivityTracker); + + /* + * Creating the TemplateRuntime Capability Agent - This component is the Capability Agent that implements the + * TemplateRuntime interface of AVS. + */ + m_templateRuntime = capabilityAgents::templateRuntime::TemplateRuntime::create( + m_audioPlayer, m_visualFocusManager, m_exceptionSender); + if (!m_templateRuntime) { + ACSDK_ERROR(LX("initializeFailed").d("reason", "unableToCreateTemplateRuntimeCapabilityAgent")); + return false; + } + m_dialogUXStateAggregator->addObserver(m_templateRuntime); } /* @@ -503,11 +560,13 @@ bool DefaultClient::initialize( return false; } - if (!m_directiveSequencer->addDirectiveHandler(m_templateRuntime)) { - ACSDK_ERROR(LX("initializeFailed") - .d("reason", "unableToRegisterDirectiveHandler") - .d("directiveHandler", "TemplateRuntime")); - return false; + if (isGuiSupported) { + if (!m_directiveSequencer->addDirectiveHandler(m_templateRuntime)) { + ACSDK_ERROR(LX("initializeFailed") + .d("reason", "unableToRegisterDirectiveHandler") + .d("directiveHandler", "TemplateRuntime")); + return false; + } } if (!m_directiveSequencer->addDirectiveHandler(m_notificationsCapabilityAgent)) { @@ -531,7 +590,7 @@ void DefaultClient::disconnect() { } void DefaultClient::stopForegroundActivity() { - m_focusManager->stopForegroundActivity(); + m_audioFocusManager->stopForegroundActivity(); } void DefaultClient::addAlexaDialogStateObserver( @@ -574,14 +633,30 @@ void DefaultClient::removeAudioPlayerObserver( void DefaultClient::addTemplateRuntimeObserver( std::shared_ptr observer) { + if (!m_templateRuntime) { + ACSDK_ERROR(LX("addTemplateRuntimeObserverFailed").d("reason", "guiNotSupported")); + return; + } m_templateRuntime->addObserver(observer); } void DefaultClient::removeTemplateRuntimeObserver( std::shared_ptr observer) { + if (!m_templateRuntime) { + ACSDK_ERROR(LX("removeTemplateRuntimeObserverFailed").d("reason", "guiNotSupported")); + return; + } m_templateRuntime->removeObserver(observer); } +void DefaultClient::TemplateRuntimeDisplayCardCleared() { + if (!m_templateRuntime) { + ACSDK_ERROR(LX("TemplateRuntimeDisplayCardClearedFailed").d("reason", "guiNotSupported")); + return; + } + m_templateRuntime->displayCardCleared(); +} + void DefaultClient::addSettingObserver( const std::string& key, std::shared_ptr observer) { @@ -616,6 +691,10 @@ std::shared_ptr DefaultClient return m_playbackRouter; } +std::shared_ptr DefaultClient::getRegistrationManager() { + return m_registrationManager; +} + void DefaultClient::addSpeakerManagerObserver( std::shared_ptr observer) { m_speakerManager->addSpeakerManagerObserver(observer); @@ -679,63 +758,71 @@ std::future DefaultClient::notifyOfTapToTalkEnd() { DefaultClient::~DefaultClient() { if (m_directiveSequencer) { - ACSDK_DEBUG(LX("DirectiveSequencerShutdown")); + ACSDK_DEBUG5(LX("DirectiveSequencerShutdown")); m_directiveSequencer->shutdown(); } if (m_speakerManager) { - ACSDK_DEBUG(LX("SpeakerManagerShutdown")); + ACSDK_DEBUG5(LX("SpeakerManagerShutdown")); m_speakerManager->shutdown(); } if (m_templateRuntime) { - ACSDK_DEBUG(LX("TemplateRuntimeShutdown")); + ACSDK_DEBUG5(LX("TemplateRuntimeShutdown")); m_templateRuntime->shutdown(); } if (m_audioInputProcessor) { - ACSDK_DEBUG(LX("AIPShutdown")); + ACSDK_DEBUG5(LX("AIPShutdown")); m_audioInputProcessor->shutdown(); } if (m_audioPlayer) { - ACSDK_DEBUG(LX("AudioPlayerShutdown")); + ACSDK_DEBUG5(LX("AudioPlayerShutdown")); m_audioPlayer->shutdown(); } if (m_externalMediaPlayer) { - ACSDK_DEBUG(LX("ExternalMediaPlayerShutdown")); + ACSDK_DEBUG5(LX("ExternalMediaPlayerShutdown")); m_externalMediaPlayer->shutdown(); } if (m_speechSynthesizer) { - ACSDK_DEBUG(LX("SpeechSynthesizerShutdown")); + ACSDK_DEBUG5(LX("SpeechSynthesizerShutdown")); m_speechSynthesizer->shutdown(); } if (m_alertsCapabilityAgent) { - ACSDK_DEBUG(LX("AlertsShutdown")); + ACSDK_DEBUG5(LX("AlertsShutdown")); m_alertsCapabilityAgent->shutdown(); } if (m_playbackController) { - ACSDK_DEBUG(LX("PlaybackControllerShutdown")); + ACSDK_DEBUG5(LX("PlaybackControllerShutdown")); m_playbackController->shutdown(); } if (m_softwareInfoSender) { - ACSDK_DEBUG(LX("SoftwareInfoShutdown")); + ACSDK_DEBUG5(LX("SoftwareInfoShutdown")); m_softwareInfoSender->shutdown(); } if (m_messageRouter) { - ACSDK_DEBUG(LX("MessageRouterShutdown.")); + ACSDK_DEBUG5(LX("MessageRouterShutdown.")); m_messageRouter->shutdown(); } if (m_connectionManager) { - ACSDK_DEBUG(LX("ConnectionManagerShutdown.")); + ACSDK_DEBUG5(LX("ConnectionManagerShutdown.")); m_connectionManager->shutdown(); } if (m_certifiedSender) { - ACSDK_DEBUG(LX("CertifiedSenderShutdown.")); + ACSDK_DEBUG5(LX("CertifiedSenderShutdown.")); m_certifiedSender->shutdown(); } + if (m_audioActivityTracker) { + ACSDK_DEBUG5(LX("AudioActivityTrackerShutdown.")); + m_audioActivityTracker->shutdown(); + } + if (m_visualActivityTracker) { + ACSDK_DEBUG5(LX("VisualActivityTrackerShutdown.")); + m_visualActivityTracker->shutdown(); + } if (m_playbackRouter) { - ACSDK_DEBUG(LX("PlaybackRouter.")); + ACSDK_DEBUG5(LX("PlaybackRouterShutdown.")); m_playbackRouter->shutdown(); } if (m_notificationsCapabilityAgent) { - ACSDK_DEBUG(LX("NotificationsShutdown.")); + ACSDK_DEBUG5(LX("NotificationsShutdown.")); m_notificationsCapabilityAgent->shutdown(); } } diff --git a/AuthDelegate/examples/ExampleAuthDelegateClient/src/ExampleAuthDelegateClient.cpp b/AuthDelegate/examples/ExampleAuthDelegateClient/src/ExampleAuthDelegateClient.cpp index 580d1c587b..f0806b762c 100644 --- a/AuthDelegate/examples/ExampleAuthDelegateClient/src/ExampleAuthDelegateClient.cpp +++ b/AuthDelegate/examples/ExampleAuthDelegateClient/src/ExampleAuthDelegateClient.cpp @@ -38,14 +38,14 @@ static const std::string TAG("AlexAuthDelegateClient"); /// Simple implementation of the AuthDelegateObserverInterface. class Observer : public AuthObserverInterface { public: - /// Construct an Observer, initialized as not authroized. + /// Construct an Observer, initialized as not authorized. Observer() : m_state(AuthObserverInterface::State::UNINITIALIZED) { } void onAuthStateChange( AuthObserverInterface::State newState, - AuthObserverInterface::Error error = AuthObserverInterface::Error::NO_ERROR) override { - if (error == AuthObserverInterface::Error::NO_ERROR) { + AuthObserverInterface::Error error = AuthObserverInterface::Error::SUCCESS) override { + if (error == AuthObserverInterface::Error::SUCCESS) { ACSDK_DEBUG(LX("onAuthStateChange").d("newState", newState)); } else { ACSDK_ERROR(LX("onAuthStateChangeError").d("newState", newState).d("error", error)); @@ -59,7 +59,7 @@ class Observer : public AuthObserverInterface { /** * Wait until we are authorized. - * @return true if we are authroized. + * @return true if we are authorized. */ bool wait() { std::unique_lock lock(m_mutex); diff --git a/AuthDelegate/include/AuthDelegate/AuthDelegate.h b/AuthDelegate/include/AuthDelegate/AuthDelegate.h index 10d976c72e..f09d0ddce9 100644 --- a/AuthDelegate/include/AuthDelegate/AuthDelegate.h +++ b/AuthDelegate/include/AuthDelegate/AuthDelegate.h @@ -111,7 +111,7 @@ class AuthDelegate : public avsCommon::sdkInterfaces::AuthDelegateInterface { /** * Attempt to refresh the auth token. * - * @return @c NO_ERROR if the authorization token is successfully refreshed. Otherwise, return the error encountered + * @return @c SUCCESS if the authorization token is successfully refreshed. Otherwise, return the error encountered * in the process. */ avsCommon::sdkInterfaces::AuthObserverInterface::Error refreshAuthToken(); @@ -121,7 +121,7 @@ class AuthDelegate : public avsCommon::sdkInterfaces::AuthDelegateInterface { * * @param code The response code returned from the LWA request * @param body The response body (if any) returned from the LWA request. - * @return @c NO_ERROR if the auth token was refreshed, otherwise the error encountered in the process. + * @return @c SUCCESS if the auth token was refreshed, otherwise the error encountered in the process. */ avsCommon::sdkInterfaces::AuthObserverInterface::Error handleLwaResponse(long code, const std::string& body); diff --git a/AuthDelegate/src/AuthDelegate.cpp b/AuthDelegate/src/AuthDelegate.cpp index baa2870379..905f2a766d 100644 --- a/AuthDelegate/src/AuthDelegate.cpp +++ b/AuthDelegate/src/AuthDelegate.cpp @@ -146,12 +146,12 @@ static bool isUnrecoverable(AuthObserverInterface::Error error) { * Helper function that retrieves the Error enum value. * * @param error The string in the @c error field of packet body. - * @return the Error enum code corresponding to @c error. If error is "", returns NO_ERROR. If it is an unknown error, + * @return the Error enum code corresponding to @c error. If error is "", returns SUCCESS. If it is an unknown error, * returns UNKNOWN_ERROR. */ static AuthObserverInterface::Error getErrorCode(const std::string& error) { if (error.empty()) { - return AuthObserverInterface::Error::NO_ERROR; + return AuthObserverInterface::Error::SUCCESS; } else { auto errorIterator = g_recoverableErrorCodeMap.find(error); if (g_recoverableErrorCodeMap.end() != errorIterator) { @@ -213,7 +213,7 @@ std::unique_ptr AuthDelegate::create( AuthDelegate::AuthDelegate(std::unique_ptr httpPost) : m_authState{AuthObserverInterface::State::UNINITIALIZED}, - m_authError{AuthObserverInterface::Error::NO_ERROR}, + m_authError{AuthObserverInterface::Error::SUCCESS}, m_isStopping{false}, m_expirationTime{std::chrono::time_point::max()}, m_retryCount{0}, @@ -316,7 +316,7 @@ void AuthDelegate::refreshAndNotifyThreadFunction() { lock.unlock(); } else { lock.unlock(); - if (AuthObserverInterface::Error::NO_ERROR == refreshAuthToken()) { + if (AuthObserverInterface::Error::SUCCESS == refreshAuthToken()) { nextState = AuthObserverInterface::State::REFRESHED; } } @@ -343,7 +343,7 @@ AuthObserverInterface::Error AuthDelegate::refreshAuthToken() { auto code = m_HttpPost->doPost(m_lwaUrl, postData.str(), timeout, body); auto newError = handleLwaResponse(code, body); - if (AuthObserverInterface::Error::NO_ERROR == newError) { + if (AuthObserverInterface::Error::SUCCESS == newError) { m_retryCount = 0; } else { m_timeToRefresh = calculateTimeToRetry(m_retryCount++); @@ -406,7 +406,7 @@ AuthObserverInterface::Error AuthDelegate::handleLwaResponse(long code, const st std::lock_guard lock(m_mutex); m_authToken = authToken; } - return AuthObserverInterface::Error::NO_ERROR; + return AuthObserverInterface::Error::SUCCESS; } else { std::string error; diff --git a/AuthDelegate/test/AuthDelegateTest.cpp b/AuthDelegate/test/AuthDelegateTest.cpp index 646fd75fe8..fd8aa9c320 100644 --- a/AuthDelegate/test/AuthDelegateTest.cpp +++ b/AuthDelegate/test/AuthDelegateTest.cpp @@ -275,12 +275,12 @@ TEST_F(AuthDelegateTest, retry) { EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::SUCCESS)) .Times(AtMost(1)); EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::SUCCESS)) .WillOnce(InvokeWithoutArgs([this, &tokenRefreshed]() { tokenRefreshed = true; m_cv.notify_all(); @@ -308,12 +308,12 @@ TEST_F(AuthDelegateTest, expirationNotification) { ::testing::InSequence s; EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::SUCCESS)) .Times(AtMost(1)); EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::SUCCESS)) .Times(1); EXPECT_CALL( @@ -349,12 +349,12 @@ TEST_F(AuthDelegateTest, recoverAfterExpiration) { ::testing::InSequence s; EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::SUCCESS)) .Times(AtMost(1)); EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::SUCCESS)) .Times(1); EXPECT_CALL( @@ -364,7 +364,7 @@ TEST_F(AuthDelegateTest, recoverAfterExpiration) { EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::REFRESHED, AuthObserverInterface::Error::SUCCESS)) .WillOnce(InvokeWithoutArgs([this, &tokenRefreshed]() { tokenRefreshed = true; m_cv.notify_all(); @@ -391,7 +391,7 @@ TEST_F(AuthDelegateTest, unrecoverableErrorNotification) { EXPECT_CALL( *m_mockAuthObserver, - onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::NO_ERROR)) + onAuthStateChange(AuthObserverInterface::State::UNINITIALIZED, AuthObserverInterface::Error::SUCCESS)) .Times(AtMost(1)); EXPECT_CALL( diff --git a/CHANGELOG.md b/CHANGELOG.md index 389cf62f38..b58f0e552a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,28 @@ ## ChangeLog +### [1.6.0] - 2018-03-08 + +**Enhancements** +* `rapidJson` is now included with "make install". +* Updated the `TemplateRuntimeObserverInterface` to support clearing of `displayCards`. +* Added Windows SDK support, along with an installation script (MinGW-w64). +* Updated `ContextManager` to ignore context reported by a state provider. +* The `SharedDataStream` object is now associated by playlist, rather than by URL. +* Added the `RegistrationManager` component. Now, when a user logs out all persistent user-specific data is cleared from the SDK. The log out functionality can be exercised in the sample app with the new command: `k`. + +**Bug Fixes** +* [Issue 400](https://github.com/alexa/avs-device-sdk/issues/400) Fixed a bug where the alert reminder did not iterate as intended after loss of network connection. +* [Issue 477](https://github.com/alexa/avs-device-sdk/issues/477) Fixed a bug in which Alexa's weather response was being truncated. +* Fixed an issue in which there were reports of instability related to the Sensory engine. To correct this, the `portAudio` [`suggestedLatency`](https://github.com/alexa/avs-device-sdk/blob/master/Integration/AlexaClientSDKConfig.json#L62) value can now be configured. + +**Known Issues** +* The `ACL` may encounter issues if audio attachments are received but not consumed. +* `SpeechSynthesizerState` currently uses `GAINING_FOCUS` and `LOSING_FOCUS` as a workaround for handling intermediate state. These states may be removed in a future release. +* Music playback doesn't immediately stop when a user barges-in on iHeartRadio. +* The Windows sample app hangs on exit. +* GDB receives a `SIGTRAP` when troubleshooting the Windows sample app. +* `make integration` doesn't work on Windows. Integration tests will need to be run individually. + ### [1.5.0] - 2018-02-12 **Enhancements** diff --git a/CMakeLists.txt b/CMakeLists.txt index 55e91fe02b..461ddca999 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -1,8 +1,11 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) # Set project information -project(AlexaClientSDK VERSION 1.5.0 LANGUAGES CXX) +project(AlexaClientSDK VERSION 1.6.0 LANGUAGES CXX) set(PROJECT_BRIEF "A cross-platform, modular SDK for interacting with the Alexa Voice Service") +if (EXISTS "${CMAKE_CURRENT_SOURCE_DIR}/CapabilityAgents/ExternalMediaPlayer/src/ExternalMediaPlayerAdapters") + set(HAS_EXTERNAL_MEDIA_PLAYER_ADAPTERS ON) +endif() include(build/BuildDefaults.cmake) include(tools/Testing.cmake) @@ -36,6 +39,8 @@ else() add_subdirectory("Integration") endif() add_subdirectory("ApplicationUtilities") +add_subdirectory("ESP") +add_subdirectory("RegistrationManager") add_subdirectory("SampleApp") add_subdirectory("Storage") add_subdirectory("doc") diff --git a/CapabilityAgents/AIP/include/AIP/ASRProfile.h b/CapabilityAgents/AIP/include/AIP/ASRProfile.h index 016f65bb4f..5e5397bdf4 100644 --- a/CapabilityAgents/AIP/include/AIP/ASRProfile.h +++ b/CapabilityAgents/AIP/include/AIP/ASRProfile.h @@ -16,10 +16,14 @@ #ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_AIP_INCLUDE_AIP_ASRPROFILE_H_ #define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_AIP_INCLUDE_AIP_ASRPROFILE_H_ +#include + namespace alexaClientSDK { namespace capabilityAgents { namespace aip { +#include + /** * Enumerates the different ASR profiles supported by AVS. * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/speechrecognizer#profiles diff --git a/CapabilityAgents/AIP/include/AIP/Initiator.h b/CapabilityAgents/AIP/include/AIP/Initiator.h index 42a9f6750a..fa6735c276 100644 --- a/CapabilityAgents/AIP/include/AIP/Initiator.h +++ b/CapabilityAgents/AIP/include/AIP/Initiator.h @@ -20,6 +20,8 @@ namespace alexaClientSDK { namespace capabilityAgents { namespace aip { +#include + /** * Enumerates the different initiators supported by AVS. */ diff --git a/CapabilityAgents/AIP/src/AudioInputProcessor.cpp b/CapabilityAgents/AIP/src/AudioInputProcessor.cpp index d8a9548b04..3029debc17 100644 --- a/CapabilityAgents/AIP/src/AudioInputProcessor.cpp +++ b/CapabilityAgents/AIP/src/AudioInputProcessor.cpp @@ -44,9 +44,6 @@ static const std::string TAG("AudioInputProcessor"); /// The name of the @c FocusManager channel used by @c AudioInputProvider. static const std::string CHANNEL_NAME = avsCommon::sdkInterfaces::FocusManagerInterface::DIALOG_CHANNEL_NAME; -/// The activityId string used with @c FocusManager by @c AudioInputProvider. -static const std::string ACTIVITY_ID = "SpeechRecognizer.Recognize"; - /// The namespace for this capability agent. static const std::string NAMESPACE = "SpeechRecognizer"; @@ -393,25 +390,49 @@ bool AudioInputProcessor::executeRecognize( return false; } - if (provider.format.encoding != avsCommon::utils::AudioFormat::Encoding::LPCM) { - ACSDK_ERROR( - LX("executeRecognizeFailed").d("reason", "unsupportedEncoding").d("encoding", provider.format.encoding)); - return false; - } else if (provider.format.endianness != avsCommon::utils::AudioFormat::Endianness::LITTLE) { + std::unordered_map mapSampleRatesAVSEncoding = {{32000, "OPUS"}}; + std::string avsEncodingFormat; + std::unordered_map::iterator itSampleRateAVSEncoding; + + switch (provider.format.encoding) { + case avsCommon::utils::AudioFormat::Encoding::LPCM: + if (provider.format.sampleRateHz != 16000) { + ACSDK_ERROR(LX("executeRecognizeFailed") + .d("reason", "unsupportedSampleRateForPCM") + .d("sampleRate", provider.format.sampleRateHz)); + return false; + } else if (provider.format.sampleSizeInBits != 16) { + ACSDK_ERROR(LX("executeRecognizeFailed") + .d("reason", "unsupportedSampleSize") + .d("sampleSize", provider.format.sampleSizeInBits)); + return false; + } + + avsEncodingFormat = "AUDIO_L16_RATE_16000_CHANNELS_1"; + break; + case avsCommon::utils::AudioFormat::Encoding::OPUS: + itSampleRateAVSEncoding = mapSampleRatesAVSEncoding.find(provider.format.sampleRateHz); + if (itSampleRateAVSEncoding == mapSampleRatesAVSEncoding.end()) { + ACSDK_ERROR(LX("executeRecognizeFailed") + .d("reason", "unsupportedSampleRateForOPUS") + .d("sampleRate", provider.format.sampleRateHz)); + return false; + } + + avsEncodingFormat = itSampleRateAVSEncoding->second; + break; + default: + ACSDK_ERROR(LX("executeRecognizeFailed") + .d("reason", "unsupportedEncoding") + .d("encoding", provider.format.encoding)); + return false; + } + + if (provider.format.endianness != avsCommon::utils::AudioFormat::Endianness::LITTLE) { ACSDK_ERROR(LX("executeRecognizeFailed") .d("reason", "unsupportedEndianness") .d("endianness", provider.format.endianness)); return false; - } else if (provider.format.sampleSizeInBits != 16) { - ACSDK_ERROR(LX("executeRecognizeFailed") - .d("reason", "unsupportedSampleSize") - .d("sampleSize", provider.format.sampleSizeInBits)); - return false; - } else if (provider.format.sampleRateHz != 16000) { - ACSDK_ERROR(LX("executeRecognizeFailed") - .d("reason", "unsupportedSampleRate") - .d("sampleRate", provider.format.sampleRateHz)); - return false; } else if (provider.format.numChannels != 1) { ACSDK_ERROR(LX("executeRecognizeFailed") .d("reason", "unsupportedNumChannels") @@ -452,7 +473,7 @@ bool AudioInputProcessor::executeRecognize( // clang-format off payload << R"({)" R"("profile":")" << provider.profile << R"(",)" - R"("format":"AUDIO_L16_RATE_16000_CHANNELS_1")"; + R"("format":")" << avsEncodingFormat << R"(")"; // The initiator (or lack thereof) from a previous ExpectSpeech has precedence. if (m_precedingExpectSpeechInitiator) { @@ -538,7 +559,7 @@ void AudioInputProcessor::executeOnContextAvailable(const std::string jsonContex // Start acquiring the channel right away; we'll service the callback after assembling our Recognize event. if (m_focusState != avsCommon::avs::FocusState::FOREGROUND) { - if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), ACTIVITY_ID)) { + if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), NAMESPACE)) { ACSDK_ERROR(LX("executeOnContextAvailableFailed").d("reason", "Unable to acquire channel")); executeResetState(); return; diff --git a/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp b/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp index ffe30e86a5..323afdf393 100644 --- a/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp +++ b/CapabilityAgents/AIP/test/AudioInputProcessorTest.cpp @@ -52,10 +52,7 @@ namespace test { using avsCommon::sdkInterfaces::AudioInputProcessorObserverInterface; /// The name of the @c FocusManager channel used by @c AudioInputProvider. -static const std::string CHANNEL_NAME = "Dialog"; - -/// The activityId string used with @c FocusManager by @c AudioInputProvider. -static const std::string ACTIVITY_ID = "SpeechRecognizer.Recognize"; +static const std::string CHANNEL_NAME = avsCommon::sdkInterfaces::FocusManagerInterface::DIALOG_CHANNEL_NAME; /// The namespace for this capability agent. static const std::string NAMESPACE = "SpeechRecognizer"; @@ -159,8 +156,8 @@ static const std::string ASR_PROFILE_KEY = "profile"; /// JSON key for the audio format field of a recognize event. static const std::string AUDIO_FORMAT_KEY = "format"; -/// JSON value for a recognize event's audio format. -static const std::string AUDIO_FORMAT_VALUE = "AUDIO_L16_RATE_16000_CHANNELS_1"; +/// Accepted JSON values for a recognize event's audio format. +static const std::unordered_set AUDIO_FORMAT_VALUES = {"AUDIO_L16_RATE_16000_CHANNELS_1", "OPUS"}; /// JSON key for the initiator field of a recognize event. static const std::string RECOGNIZE_INITIATOR_KEY = "initiator"; @@ -430,8 +427,14 @@ void RecognizeEvent::verifyMessage( std::ostringstream profile; profile << m_audioProvider.profile; + + std::ostringstream encodingFormat; + encodingFormat << m_audioProvider.format.encoding; + EXPECT_EQ(getJsonString(payload->value, ASR_PROFILE_KEY), profile.str()); - EXPECT_EQ(getJsonString(payload->value, AUDIO_FORMAT_KEY), AUDIO_FORMAT_VALUE); + + EXPECT_FALSE( + AUDIO_FORMAT_VALUES.find(getJsonString(payload->value, AUDIO_FORMAT_KEY)) == AUDIO_FORMAT_VALUES.end()); auto initiator = payload->value.FindMember(RECOGNIZE_INITIATOR_KEY); EXPECT_NE(initiator, payload->value.MemberEnd()); @@ -851,7 +854,7 @@ bool AudioInputProcessorTest::testRecognizeSucceeds( if (!bargeIn) { EXPECT_CALL(*m_mockUserActivityNotifier, onUserActive()).Times(2); EXPECT_CALL(*m_mockObserver, onStateChanged(AudioInputProcessorObserverInterface::State::RECOGNIZING)); - EXPECT_CALL(*m_mockFocusManager, acquireChannel(CHANNEL_NAME, _, ACTIVITY_ID)) + EXPECT_CALL(*m_mockFocusManager, acquireChannel(CHANNEL_NAME, _, NAMESPACE)) .WillOnce(InvokeWithoutArgs([this, stopPoint] { m_audioInputProcessor->onFocusChanged(avsCommon::avs::FocusState::FOREGROUND); if (RecognizeStopPoint::AFTER_FOCUS == stopPoint) { @@ -1929,6 +1932,37 @@ TEST_F(AudioInputProcessorTest, recognizeWakewordWithInvalidESPWithKeyword) { *m_audioProvider, Initiator::WAKEWORD, begin, end, KEYWORD_TEXT, RecognizeStopPoint::NONE, nullptr, espData)); } +/* + * This function verifies that @c AudioInputProcessor::recognize() works with OPUS encoding used with + * @c Initiator::TAP. + */ +TEST_F(AudioInputProcessorTest, recognizeOPUSWithTap) { + m_audioProvider->format.encoding = avsCommon::utils::AudioFormat::Encoding::OPUS; + m_audioProvider->format.sampleRateHz = 32000; + ASSERT_TRUE(testRecognizeSucceeds(*m_audioProvider, Initiator::TAP)); +} + +/* + * This function verifies that @c AudioInputProcessor::recognize() works with OPUS encoding used with + * @c Initiator::PRESS_AND_HOLD. + */ +TEST_F(AudioInputProcessorTest, recognizeOPUSWithPressAndHold) { + m_audioProvider->format.encoding = avsCommon::utils::AudioFormat::Encoding::OPUS; + m_audioProvider->format.sampleRateHz = 32000; + ASSERT_TRUE(testRecognizeSucceeds(*m_audioProvider, Initiator::PRESS_AND_HOLD)); +} + +/** + * This function verifies that @c AudioInputProcessor::recognize() works with OPUS encoding used with + * @c Initiator::WAKEWORD valid begin and end indices. + */ +TEST_F(AudioInputProcessorTest, recognizeOPUSWithWakeWord) { + avsCommon::avs::AudioInputStream::Index begin = 0; + avsCommon::avs::AudioInputStream::Index end = AudioInputProcessor::INVALID_INDEX; + m_audioProvider->format.encoding = avsCommon::utils::AudioFormat::Encoding::OPUS; + m_audioProvider->format.sampleRateHz = 32000; + EXPECT_TRUE(testRecognizeSucceeds(*m_audioProvider, Initiator::WAKEWORD, begin, end, KEYWORD_TEXT)); +} } // namespace test } // namespace aip } // namespace capabilityAgents diff --git a/CapabilityAgents/Alerts/include/Alerts/Alert.h b/CapabilityAgents/Alerts/include/Alerts/Alert.h index 17ae1540c3..fbe6ab4568 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Alert.h +++ b/CapabilityAgents/Alerts/include/Alerts/Alert.h @@ -91,7 +91,9 @@ class Alert /// The alert has been stopped due to a local user action. LOCAL_STOP, /// The alert is being stopped due to an SDK shutdown operation. - SHUTDOWN + SHUTDOWN, + /// Logout customer logged out or deregistered. + LOG_OUT }; /** diff --git a/CapabilityAgents/Alerts/include/Alerts/AlertScheduler.h b/CapabilityAgents/Alerts/include/Alerts/AlertScheduler.h index 2e929ca97a..fa00893c65 100644 --- a/CapabilityAgents/Alerts/include/Alerts/AlertScheduler.h +++ b/CapabilityAgents/Alerts/include/Alerts/AlertScheduler.h @@ -63,11 +63,10 @@ class AlertScheduler : public AlertObserverInterface { * * @note This function must be called before other use of an object this class. * - * @param storageFilePath The file we will expect to use for our database. * @param observer An observer which we will notify of all alert state changes. * @return Whether initialization was successful. */ - bool initialize(const std::string& storageFilePath, std::shared_ptr observer); + bool initialize(std::shared_ptr observer); /** * Schedule an alert for rendering. @@ -131,8 +130,10 @@ class AlertScheduler : public AlertObserverInterface { /** * Clear all data being managed. This includes database storage. + * + * @param reason What triggered the data to be cleared. */ - void clearData(); + void clearData(Alert::StopReason reason = Alert::StopReason::SHUTDOWN); /** * Handle shutdown. @@ -215,6 +216,9 @@ class AlertScheduler : public AlertObserverInterface { */ void deactivateActiveAlertHelperLocked(Alert::StopReason reason); + /// This is used to safely access the time utilities. + avsCommon::utils::timing::TimeUtils m_timeUtils; + /** * Our observer. Once initialized, this is only accessed within executor functions, so does not need mutex * protection. diff --git a/CapabilityAgents/Alerts/include/Alerts/AlertsCapabilityAgent.h b/CapabilityAgents/Alerts/include/Alerts/AlertsCapabilityAgent.h index 5cffbacc5b..066f167ddd 100644 --- a/CapabilityAgents/Alerts/include/Alerts/AlertsCapabilityAgent.h +++ b/CapabilityAgents/Alerts/include/Alerts/AlertsCapabilityAgent.h @@ -19,6 +19,7 @@ #include "Alerts/AlertObserverInterface.h" #include "Alerts/Alert.h" #include "Alerts/AlertScheduler.h" +#include "RegistrationManager/CustomerDataHandler.h" #include #include @@ -52,6 +53,7 @@ class AlertsCapabilityAgent , public avsCommon::sdkInterfaces::ConnectionStatusObserverInterface , public AlertObserverInterface , public avsCommon::utils::RequiresShutdown + , public registrationManager::CustomerDataHandler , public std::enable_shared_from_this { public: /** @@ -65,6 +67,7 @@ class AlertsCapabilityAgent * @param alertStorage An interface to store, load, modify and delete Alerts. * @param alertsAudioFactory A provider of audio streams specific to Alerts. * @param alertRenderer An alert renderer, which Alerts will use to generate user-perceivable effects when active. + * @param dataManager A dataManager object that will track the CustomerDataHandler. * @return A pointer to an object of this type, or nullptr if there were problems during construction. */ static std::shared_ptr create( @@ -75,7 +78,8 @@ class AlertsCapabilityAgent std::shared_ptr exceptionEncounteredSender, std::shared_ptr alertStorage, std::shared_ptr alertsAudioFactory, - std::shared_ptr alertRenderer); + std::shared_ptr alertRenderer, + std::shared_ptr dataManager); avsCommon::avs::DirectiveHandlerConfiguration getConfiguration() const override; @@ -123,6 +127,11 @@ class AlertsCapabilityAgent */ void onLocalStop(); + /** + * Clear all scheduled alerts. + */ + void clearData() override; + private: /** * Constructor. @@ -135,6 +144,7 @@ class AlertsCapabilityAgent * @param alertStorage An interface to store, load, modify and delete Alerts. * @param alertsAudioFactory A provider of audio streams specific to Alerts. * @param alertRenderer An alert renderer, which Alerts will use to generate user-perceivable effects when active. + * @param dataManager A dataManager object that will track the CustomerDataHandler. */ AlertsCapabilityAgent( std::shared_ptr messageSender, @@ -144,7 +154,8 @@ class AlertsCapabilityAgent std::shared_ptr exceptionEncounteredSender, std::shared_ptr alertStorage, std::shared_ptr alertsAudioFactory, - std::shared_ptr alertRenderer); + std::shared_ptr alertRenderer, + std::shared_ptr dataManager); void doShutdown() override; @@ -156,9 +167,9 @@ class AlertsCapabilityAgent /** * Initializes the alerts for this object. * - * @param configurationRoot The configuration object parsed during SDK initialization. + * @return True if successful, false otherwise. */ - bool initializeAlerts(const avsCommon::utils::configuration::ConfigurationNode& configurationRoot); + bool initializeAlerts(); /** * @name Executor Thread Functions diff --git a/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h b/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h index bcf952693c..d409778554 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h +++ b/CapabilityAgents/Alerts/include/Alerts/Renderer/Renderer.h @@ -160,14 +160,14 @@ class Renderer void resetSourceId(); /** - * Utility function to handle the rendering of the next url, with respect to @c m_loopCount and - * @c m_nextUrlIndexToRender. If all urls within a loop have completed, and there are further loops to render, - * this function will also perform a sleep for the @c m_loopPause duration. + * Utility function to handle the rendering of the next audio asset, with respect to @c m_loopCount and @c + * m_nextUrlIndexToRender. If all urls within a loop have completed, and there are further loops to render, this + * function will also perform a sleep for the @c m_loopPause duration. * - * @return @c true if there are more urls to render, and the next one has been successfully sent to the - * @c m_mediaPlayer to be played. Returns @c false otherwise. + * @return @c true if there are more audio assets to render, and the next one has been successfully sent to the @c + * m_mediaPlayer to be played. Returns @c false otherwise. */ - bool renderNextUrl(); + bool renderNextAudioAsset(); /// @} @@ -189,8 +189,8 @@ class Renderer /// rendered instead. std::vector m_urls; - /// The next url in the url vector to render. - int m_nextUrlIndexToRender; + /// The number of streams that have been rendered during the processing of the current loop. + int m_numberOfStreamsRenderedThisLoop; /// The number of times @c m_urls should be rendered. int m_loopCount; @@ -198,6 +198,9 @@ class Renderer /// The time to pause between the rendering of the @c m_urls sequence. std::chrono::milliseconds m_loopPause; + /// A pointer to a stream to use as the default audio to use when the audio assets aren't available. + std::shared_ptr m_defaultAudio; + /// A flag to capture if the renderer has been asked to stop by its owner. bool m_isStopping; diff --git a/CapabilityAgents/Alerts/include/Alerts/Storage/AlertStorageInterface.h b/CapabilityAgents/Alerts/include/Alerts/Storage/AlertStorageInterface.h index 9adce98541..c457ed8bbb 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Storage/AlertStorageInterface.h +++ b/CapabilityAgents/Alerts/include/Alerts/Storage/AlertStorageInterface.h @@ -39,46 +39,29 @@ class AlertStorageInterface { virtual ~AlertStorageInterface() = default; /** - * Creates a new database with the given filepath. - * If the file specified already exists, or if a database is already being handled by this object, then - * this function returns false. + * Creates a new database. + * If a database is already being handled by this object or there is another internal error, then this function + * returns false. * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is created ok, or @c false if either the file exists or a database is already - * being handled by this object. + * @return @c true If the database is created ok, or @c false if a database is already being handled by this object + * or there is a problem creating the database. */ - virtual bool createDatabase(const std::string& filePath) = 0; + virtual bool createDatabase() = 0; /** - * Open a database with the given filepath. If this object is already managing an open database, or the file - * does not exist, or there is a problem opening the database, this function returns false. + * Open an existing database. If this object is already managing an open database, or there is a problem opening + * the database, this function returns false. * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is opened ok, @c false if either the file does not exist, if this object is - * already managing an open database, or if there is another internal reason the database could not be opened. + * @return @c true If the database is opened ok, @c false if this object is already managing an open database, or if + * there is another internal reason the database could not be opened. */ - virtual bool open(const std::string& filePath) = 0; - - /** - * Query if this object is currently managing an open database. - * - * @return @c true If a database is being currently managed by this object, @c false otherwise. - */ - virtual bool isOpen() = 0; + virtual bool open() = 0; /** * Close the currently open database, if one is open. */ virtual void close() = 0; - /** - * Query whether an alert is currently stored with the given token. - * - * @param token The AVS token which uniquely identifies an alert. - * @return @c true If the alert is stored in the database, @c false otherwise. - */ - virtual bool alertExists(const std::string& token) = 0; - /** * Stores a single @c Alert in the database. * @@ -97,9 +80,8 @@ class AlertStorageInterface { /** * Updates a database record of the @c Alert parameter. - * The fields which are updated by this operation are the state and scheduled times of the alert. - * All other fields of an alert do not change over time, and so will not be captured in the database - * when calling this function. + * The fields which are updated by this operation are the state and scheduled times of the alert. All other fields + * of an alert do not change over time, and so will not be captured in the database when calling this function. * * @param alert The @c Alert to be modified. * @return Whether the @c Alert was successfully modified. @@ -114,18 +96,6 @@ class AlertStorageInterface { */ virtual bool erase(std::shared_ptr alert) = 0; - /** - * Erases a collection of alerts from the database. - * NOTE: The input to this function is expected to be a collection of database-ids for each row in the - * alerts table. This is different from the 'token' which AVS uses to identify an alert. The reason for - * this decision is that database operations should be more efficient overall when keying off a small integer, - * rather than a long string. - * - * @param alertDbIds A container of alert ids. - * @return Whether the alerts were successfully erased. - */ - virtual bool erase(const std::vector& alertDbIds) = 0; - /** * A utility function to clear the database of all records. Note that the database will still exist, as will * the tables. Only the rows will be erased. @@ -133,24 +103,6 @@ class AlertStorageInterface { * @return Whether the database was successfully cleared. */ virtual bool clearDatabase() = 0; - - /** - * An enum class to help debug database contents. This type is used in the printStats function below. - */ - enum class StatLevel { - /// Print only a single line, providing a count of rows from each table. - ONE_LINE, - /// Print all details of the Alerts table, summarizing the other tables. - ALERTS_SUMMARY, - /// Print all details of all records. - EVERYTHING - }; - - /** - * A utility function to print the contents of the database to the SDK logger output. - * This function is provided for debug use only. - */ - virtual void printStats(StatLevel level = StatLevel::ONE_LINE) = 0; }; } // namespace storage diff --git a/CapabilityAgents/Alerts/include/Alerts/Storage/SQLiteAlertStorage.h b/CapabilityAgents/Alerts/include/Alerts/Storage/SQLiteAlertStorage.h index 4235de2339..bf91f3ddbc 100644 --- a/CapabilityAgents/Alerts/include/Alerts/Storage/SQLiteAlertStorage.h +++ b/CapabilityAgents/Alerts/include/Alerts/Storage/SQLiteAlertStorage.h @@ -18,9 +18,11 @@ #include "Alerts/Storage/AlertStorageInterface.h" -#include +#include #include +#include +#include namespace alexaClientSDK { namespace capabilityAgents { @@ -36,21 +38,27 @@ namespace storage { class SQLiteAlertStorage : public AlertStorageInterface { public: /** - * Constructor. + * Factory method for creating a storage object for Alerts based on an SQLite database. + * + * @param configurationRoot The global config object. + * @param alertsAudioFactory A factory that can produce default alert sounds. + * @return Pointer to the SQLiteAlertStorage object, nullptr if there's an error creating it. */ - SQLiteAlertStorage( + static std::unique_ptr create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot, const std::shared_ptr& alertsAudioFactory); - bool createDatabase(const std::string& filePath) override; + /** + * On destruction, close the underlying database. + */ + ~SQLiteAlertStorage(); - bool open(const std::string& filePath) override; + bool createDatabase() override; - bool isOpen() override; + bool open() override; void close() override; - bool alertExists(const std::string& token) override; - bool store(std::shared_ptr alert) override; bool load(std::vector>* alertContainer) override; @@ -59,13 +67,37 @@ class SQLiteAlertStorage : public AlertStorageInterface { bool erase(std::shared_ptr alert) override; - bool erase(const std::vector& alertDbIds) override; - bool clearDatabase() override; - void printStats(StatLevel level) override; + /** + * An enum class to help debug database contents. This type is used in the printStats function below. + */ + enum class StatLevel { + /// Print only a single line, providing a count of rows from each table. + ONE_LINE, + /// Print all details of the Alerts table, summarizing the other tables. + ALERTS_SUMMARY, + /// Print all details of all records. + EVERYTHING + }; + + /** + * A utility function to print the contents of the database to the SDK logger output. + * This function is provided for debug use only. + */ + void printStats(StatLevel level = StatLevel::ONE_LINE); private: + /** + * Constructor. + * + * @param dbFilePath The location of the SQLite database file. + * @param alertsAudioFactory A factory that can produce default alert sounds. + */ + SQLiteAlertStorage( + const std::string& dbFilePath, + const std::shared_ptr& alertsAudioFactory); + /** * Utility function to migrate an existing V1 Alerts database file to the V2 format. * @@ -74,7 +106,6 @@ class SQLiteAlertStorage : public AlertStorageInterface { * If this table does not exist, then this function will create it, and the additional tables that V2 expects, * and then load all alerts from the V1 table and save them into the V2 table. * - * @param dbHandle A SQLite handle to an open database. * @return Whether the migration was successful. Returns true by default if the db is already V2. */ bool migrateAlertsDbFromV1ToV2(); @@ -89,11 +120,19 @@ class SQLiteAlertStorage : public AlertStorageInterface { */ bool loadHelper(int dbVersion, std::vector>* alertContainer); - /// The sqlite database handle. - sqlite3* m_dbHandle; + /** + * Query whether an alert is currently stored with the given token. + * + * @param token The AVS token which uniquely identifies an alert. + * @return @c true If the alert is stored in the database, @c false otherwise. + */ + bool alertExists(const std::string& token); /// A member that stores a factory that produces audio streams for alerts. std::shared_ptr m_alertsAudioFactory; + + /// The underlying database class. + alexaClientSDK::storage::sqliteStorage::SQLiteDatabase m_db; }; } // namespace storage diff --git a/CapabilityAgents/Alerts/src/Alert.cpp b/CapabilityAgents/Alerts/src/Alert.cpp index 8f1eddb5e3..a45809e3c4 100644 --- a/CapabilityAgents/Alerts/src/Alert.cpp +++ b/CapabilityAgents/Alerts/src/Alert.cpp @@ -498,6 +498,9 @@ void Alert::startRenderer() { auto loopCount = m_assetConfiguration.loopCount; auto loopPause = m_assetConfiguration.loopPause; + // If there are no assets to play (due to the alert not providing any assets), or there was a previous error + // (indicated by m_assetConfiguration.hasRenderingFailed), we call rendererCopy->start(..) with an empty vector of + // urls. This causes the default audio to be rendered. auto audioFactory = getDefaultAudioFactory(); if (avsCommon::avs::FocusState::BACKGROUND == m_focusState) { audioFactory = getShortAudioFactory(); @@ -590,6 +593,8 @@ std::string Alert::stopReasonToString(Alert::StopReason stopReason) { return "LOCAL_STOP"; case Alert::StopReason::SHUTDOWN: return "SHUTDOWN"; + case Alert::StopReason::LOG_OUT: + return "LOG_OUT"; } ACSDK_ERROR(LX("stopReasonToStringFailed").d("unhandledCase", stopReason)); diff --git a/CapabilityAgents/Alerts/src/AlertScheduler.cpp b/CapabilityAgents/Alerts/src/AlertScheduler.cpp index 1fda469897..186f6f1f4a 100644 --- a/CapabilityAgents/Alerts/src/AlertScheduler.cpp +++ b/CapabilityAgents/Alerts/src/AlertScheduler.cpp @@ -51,7 +51,7 @@ void AlertScheduler::onAlertStateChange(const std::string& alertToken, State sta m_executor.submit([this, alertToken, state, reason]() { executeOnAlertStateChange(alertToken, state, reason); }); } -bool AlertScheduler::initialize(const std::string& storageFilePath, std::shared_ptr observer) { +bool AlertScheduler::initialize(std::shared_ptr observer) { if (!observer) { ACSDK_ERROR(LX("initializeFailed").m("observer was nullptr.")); return false; @@ -59,16 +59,16 @@ bool AlertScheduler::initialize(const std::string& storageFilePath, std::shared_ m_observer = observer; - if (!m_alertStorage->open(storageFilePath)) { - ACSDK_INFO(LX("initialize").m("storage file does not exist. Creating.")); - if (!m_alertStorage->createDatabase(storageFilePath)) { - ACSDK_ERROR(LX("initializeFailed").m("Could not create database file.")); + if (!m_alertStorage->open()) { + ACSDK_INFO(LX("initialize").m("Couldn't open database. Creating.")); + if (!m_alertStorage->createDatabase()) { + ACSDK_ERROR(LX("initializeFailed").m("Could not create database.")); return false; } } int64_t unixEpochNow = 0; - if (!getCurrentUnixTime(&unixEpochNow)) { + if (!m_timeUtils.getCurrentUnixTime(&unixEpochNow)) { ACSDK_ERROR(LX("initializeFailed").d("reason", "could not get current unix time.")); return false; } @@ -106,7 +106,7 @@ bool AlertScheduler::initialize(const std::string& storageFilePath, std::shared_ bool AlertScheduler::scheduleAlert(std::shared_ptr alert) { ACSDK_DEBUG9(LX("scheduleAlert")); int64_t unixEpochNow = 0; - if (!getCurrentUnixTime(&unixEpochNow)) { + if (!m_timeUtils.getCurrentUnixTime(&unixEpochNow)) { ACSDK_ERROR(LX("scheduleAlertFailed").d("reason", "could not get current unix time.")); return false; } @@ -253,11 +253,11 @@ void AlertScheduler::onLocalStop() { deactivateActiveAlertHelperLocked(Alert::StopReason::LOCAL_STOP); } -void AlertScheduler::clearData() { +void AlertScheduler::clearData(Alert::StopReason reason) { ACSDK_DEBUG9(LX("clearData")); std::lock_guard lock(m_mutex); - deactivateActiveAlertHelperLocked(Alert::StopReason::SHUTDOWN); + deactivateActiveAlertHelperLocked(reason); if (m_scheduledAlertTimer.isActive()) { m_scheduledAlertTimer.stop(); @@ -412,7 +412,7 @@ void AlertScheduler::setTimerForNextAlertLocked() { auto alert = (*m_scheduledAlerts.begin()); int64_t timeNow; - if (!getCurrentUnixTime(&timeNow)) { + if (!m_timeUtils.getCurrentUnixTime(&timeNow)) { ACSDK_ERROR(LX("executeScheduleNextAlertForRenderingFailed").d("reason", "could not get current unix time.")); return; } diff --git a/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp b/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp index 36847199cd..14d31e4760 100644 --- a/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp +++ b/CapabilityAgents/Alerts/src/AlertsCapabilityAgent.cpp @@ -51,11 +51,6 @@ static const std::string DIRECTIVE_NAME_SET_ALERT = "SetAlert"; /// The value of the DeleteAlert Directive. static const std::string DIRECTIVE_NAME_DELETE_ALERT = "DeleteAlert"; -/// The key in our config file to find the root of settings for this Capability Agent. -static const std::string ALERTS_CAPABILITY_AGENT_CONFIGURATION_ROOT_KEY = "alertsCapabilityAgent"; -/// The key in our config file to find the database file path. -static const std::string ALERTS_CAPABILITY_AGENT_DB_FILE_PATH_KEY = "databaseFilePath"; - /// The value of the SetAlertSucceeded Event name. static const std::string SET_ALERT_SUCCEEDED_EVENT_NAME = "SetAlertSucceeded"; /// The value of the SetAlertFailed Event name. @@ -102,8 +97,6 @@ static const std::string NAMESPACE = "Alerts"; static const avsCommon::avs::NamespaceAndName SET_ALERT{NAMESPACE, "SetAlert"}; /// The DeleteAlert directive signature. static const avsCommon::avs::NamespaceAndName DELETE_ALERT{NAMESPACE, "DeleteAlert"}; -/// The activityId string used with @c FocusManager by @c AlertsCapabilityAgent. -static const std::string ACTIVITY_ID = "Alerts.AlertStarted"; /// String to identify log entries originating from this file. static const std::string TAG("AlertsCapabilityAgent"); @@ -176,7 +169,8 @@ std::shared_ptr AlertsCapabilityAgent::create( std::shared_ptr exceptionEncounteredSender, std::shared_ptr alertStorage, std::shared_ptr alertsAudioFactory, - std::shared_ptr alertRenderer) { + std::shared_ptr alertRenderer, + std::shared_ptr dataManager) { auto alertsCA = std::shared_ptr(new AlertsCapabilityAgent( messageSender, certifiedMessageSender, @@ -185,7 +179,8 @@ std::shared_ptr AlertsCapabilityAgent::create( exceptionEncounteredSender, alertStorage, alertsAudioFactory, - alertRenderer)); + alertRenderer, + dataManager)); if (!alertsCA->initialize()) { ACSDK_ERROR(LX("createFailed").d("reason", "Initialization error.")); @@ -281,9 +276,11 @@ AlertsCapabilityAgent::AlertsCapabilityAgent( std::shared_ptr exceptionEncounteredSender, std::shared_ptr alertStorage, std::shared_ptr alertsAudioFactory, - std::shared_ptr alertRenderer) : + std::shared_ptr alertRenderer, + std::shared_ptr dataManager) : CapabilityAgent("Alerts", exceptionEncounteredSender), RequiresShutdown("AlertsCapabilityAgent"), + CustomerDataHandler(dataManager), m_messageSender{messageSender}, m_certifiedSender{certifiedMessageSender}, m_focusManager{focusManager}, @@ -305,13 +302,7 @@ void AlertsCapabilityAgent::doShutdown() { } bool AlertsCapabilityAgent::initialize() { - auto configurationRoot = ConfigurationNode::getRoot()[ALERTS_CAPABILITY_AGENT_CONFIGURATION_ROOT_KEY]; - if (!configurationRoot) { - ACSDK_ERROR(LX("initializeFailed").m("could not load AlertsCapabilityAgent configuration root.")); - return false; - } - - if (!initializeAlerts(configurationRoot)) { + if (!initializeAlerts()) { ACSDK_ERROR(LX("initializeFailed").m("Could not initialize alerts.")); return false; } @@ -321,16 +312,8 @@ bool AlertsCapabilityAgent::initialize() { return true; } -bool AlertsCapabilityAgent::initializeAlerts(const ConfigurationNode& configurationRoot) { - std::string storageFilePath; - - if (!configurationRoot.getString(ALERTS_CAPABILITY_AGENT_DB_FILE_PATH_KEY, &storageFilePath) || - storageFilePath.empty()) { - ACSDK_ERROR(LX("initializeAlertsFailed").m("could not load storage file path.")); - return false; - } - - return m_alertScheduler.initialize(storageFilePath, shared_from_this()); +bool AlertsCapabilityAgent::initializeAlerts() { + return m_alertScheduler.initialize(shared_from_this()); } bool AlertsCapabilityAgent::handleSetAlert( @@ -449,7 +432,7 @@ void AlertsCapabilityAgent::sendProcessingDirectiveException( void AlertsCapabilityAgent::acquireChannel() { ACSDK_DEBUG9(LX("acquireChannel")); - m_focusManager->acquireChannel(FocusManagerInterface::ALERTS_CHANNEL_NAME, shared_from_this(), ACTIVITY_ID); + m_focusManager->acquireChannel(FocusManagerInterface::ALERTS_CHANNEL_NAME, shared_from_this(), NAMESPACE); } void AlertsCapabilityAgent::releaseChannel() { @@ -619,6 +602,11 @@ std::string AlertsCapabilityAgent::getContextString() { return buffer.GetString(); } +void AlertsCapabilityAgent::clearData() { + auto result = m_executor.submit([this]() { m_alertScheduler.clearData(Alert::StopReason::LOG_OUT); }); + result.wait(); +} + } // namespace alerts } // namespace capabilityAgents } // namespace alexaClientSDK diff --git a/CapabilityAgents/Alerts/src/CMakeLists.txt b/CapabilityAgents/Alerts/src/CMakeLists.txt index f593b0323f..516faf1dbb 100644 --- a/CapabilityAgents/Alerts/src/CMakeLists.txt +++ b/CapabilityAgents/Alerts/src/CMakeLists.txt @@ -14,9 +14,10 @@ target_include_directories(Alerts PUBLIC "${Alerts_SOURCE_DIR}/include" "${AudioResources_SOURCE_DIR}/include" "${CertifiedSender_SOURCE_DIR}/include" - "${SQLiteStorage_SOURCE_DIR}/include") + "${SQLiteStorage_SOURCE_DIR}/include" + "${RegistrationManager_SOURCE_DIR}/include") -target_link_libraries(Alerts AudioResources AVSCommon CertifiedSender SQLiteStorage) +target_link_libraries(Alerts AudioResources AVSCommon CertifiedSender SQLiteStorage RegistrationManager) # install target asdk_install() diff --git a/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp b/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp index 1093a88d44..e13d14e3a7 100644 --- a/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp +++ b/CapabilityAgents/Alerts/src/Renderer/Renderer.cpp @@ -67,12 +67,10 @@ void Renderer::start( const std::vector& urls, int loopCount, std::chrono::milliseconds loopPause) { - { - auto defaultAudio = audioFactory(); - if ((!defaultAudio || !defaultAudio->good()) && urls.empty()) { - ACSDK_ERROR(LX("startFailed").m("default audio is bad and urls are empty.")); - return; - } + std::unique_ptr defaultAudio = audioFactory(); + if (!defaultAudio) { + ACSDK_ERROR(LX("startFailed").m("default audio is nullptr")); + return; } if (loopCount < 0) { @@ -115,7 +113,7 @@ void Renderer::onPlaybackError( Renderer::Renderer(std::shared_ptr mediaPlayer) : m_mediaPlayer{mediaPlayer}, m_observer{nullptr}, - m_nextUrlIndexToRender{0}, + m_numberOfStreamsRenderedThisLoop{0}, m_loopCount{0}, m_loopPause{std::chrono::milliseconds{0}}, m_isStopping{false} { @@ -139,6 +137,8 @@ void Renderer::executeStart( m_urls = urls; m_loopCount = loopCount; m_loopPause = loopPause; + m_defaultAudio = audioFactory(); + ACSDK_DEBUG9( LX("executeStart") .d("m_urls.size", m_urls.size()) @@ -146,14 +146,13 @@ void Renderer::executeStart( .d("m_loopPause (ms)", std::chrono::duration_cast(m_loopPause).count())); m_isStopping = false; - // TODO : ACSDK-389 to update the local audio to being streams rather than file paths. - + m_numberOfStreamsRenderedThisLoop = 0; if (urls.empty()) { - m_currentSourceId = m_mediaPlayer->setSource(audioFactory(), true); + ACSDK_DEBUG5(LX("executeStart").m("setSource with default alert audio stream")); + m_currentSourceId = m_mediaPlayer->setSource(m_defaultAudio, m_loopCount <= 0); } else { - m_nextUrlIndexToRender = 0; - ACSDK_DEBUG9(LX("executeStart").d("setSource", m_nextUrlIndexToRender)); - m_currentSourceId = m_mediaPlayer->setSource(m_urls[m_nextUrlIndexToRender++]); + ACSDK_DEBUG5(LX("executeStart").d("setSource", m_urls[m_numberOfStreamsRenderedThisLoop])); + m_currentSourceId = m_mediaPlayer->setSource(m_urls[m_numberOfStreamsRenderedThisLoop]); } ACSDK_INFO(LX("executeStart").d("m_currentSourceId", m_currentSourceId)); @@ -219,8 +218,10 @@ void Renderer::executeOnPlaybackFinished(SourceId sourceId) { RendererObserverInterface::State finalState = RendererObserverInterface::State::STOPPED; - if (!m_isStopping && !m_urls.empty()) { - if (renderNextUrl()) { + ++m_numberOfStreamsRenderedThisLoop; + + if (!m_isStopping && 0 < m_loopCount) { + if (renderNextAudioAsset()) { return; } @@ -232,55 +233,58 @@ void Renderer::executeOnPlaybackFinished(SourceId sourceId) { m_observer = nullptr; } -bool Renderer::renderNextUrl() { - // sanity check. - if (m_urls.empty()) { - return false; - } - - bool shouldRenderNextUrl = true; +bool Renderer::renderNextAudioAsset() { + bool shouldRenderAnotherAudioAsset = true; - // If we have completed a loop, then update our counters, and determine what to do next. - if (m_nextUrlIndexToRender >= static_cast(m_urls.size())) { - ACSDK_DEBUG9(LX("renderNextUrl") - .d("loopCount", m_loopCount) - .d("nextUrlIndex", m_nextUrlIndexToRender) - .m("updating counters.")); + // If we have completed a loop, then update our counters, and determine what to do next. If the URLs aren't + // reachable, m_urls will be empty. + if (m_numberOfStreamsRenderedThisLoop >= static_cast(m_urls.size())) { m_loopCount--; - m_nextUrlIndexToRender = 0; + m_numberOfStreamsRenderedThisLoop = 0; + ACSDK_DEBUG5(LX("renderNextAudioAsset") + .d("loopCount", m_loopCount) + .d("nextAudioIndex", m_numberOfStreamsRenderedThisLoop) + .m("Preparing the audio loop counters.")); - if (0 == m_loopCount) { - shouldRenderNextUrl = false; + if (m_loopCount <= 0) { + shouldRenderAnotherAudioAsset = false; } else if (m_loopPause.count() > 0) { std::this_thread::sleep_for(m_loopPause); } } - // If we should continue to the next url, let's kick it off. - if (shouldRenderNextUrl) { - ACSDK_DEBUG9(LX("renderNextUrl").d("setSource", m_nextUrlIndexToRender)); + // If we should continue to the next url, let's kick it off. If there aren't any urls, use the default audio. + if (shouldRenderAnotherAudioAsset) { + ACSDK_DEBUG9(LX("renderNextAudioAsset").d("setSource", m_numberOfStreamsRenderedThisLoop)); + + if (m_urls.empty()) { + m_currentSourceId = m_mediaPlayer->setSource(m_defaultAudio, false); + } else { + std::string url = m_urls[m_numberOfStreamsRenderedThisLoop]; + m_currentSourceId = m_mediaPlayer->setSource(url); + } - std::string url = m_urls[m_nextUrlIndexToRender++]; - m_currentSourceId = m_mediaPlayer->setSource(url); if (!isSourceIdOk(m_currentSourceId)) { std::string errorMessage = "SourceId response from setSource was invalid."; - ACSDK_ERROR(LX("renderNextUrl").d("SourceId", m_currentSourceId).sensitive("url", url).m(errorMessage)); + ACSDK_ERROR(LX("renderNextAudioAsset").d("SourceId", m_currentSourceId).m(errorMessage)); notifyObserver(RendererObserverInterface::State::ERROR, errorMessage); return false; } + if (!m_mediaPlayer->play(m_currentSourceId)) { std::string errorMessage = "MediaPlayer was unable to play next media item."; - ACSDK_ERROR(LX("renderNextUrl").d("SourceId", m_currentSourceId).sensitive("url", url).m(errorMessage)); + ACSDK_ERROR(LX("renderNextUrl").d("SourceId", m_currentSourceId).m(errorMessage)); notifyObserver(RendererObserverInterface::State::ERROR, errorMessage); return false; } - ACSDK_DEBUG9(LX("renderNextUrl").m("Next url started successfully")); + ACSDK_DEBUG9(LX("renderNextAudioAsset").m("Next source started successfully")); + } else { - ACSDK_DEBUG9(LX("renderNextUrl").m("No more urls to render.")); + ACSDK_DEBUG9(LX("renderNextAudioAsset").m("No more sounds to render.")); } - return shouldRenderNextUrl; + return shouldRenderAnotherAudioAsset; } void Renderer::executeOnPlaybackError( @@ -296,6 +300,9 @@ void Renderer::executeOnPlaybackError( } resetSourceId(); + + // This will cause a retry (through Renderer::start) using the same code paths as before, except in this case the + // urls to render will be empty. notifyObserver(RendererObserverInterface::State::ERROR, error); m_observer = nullptr; } diff --git a/CapabilityAgents/Alerts/src/Storage/SQLiteAlertStorage.cpp b/CapabilityAgents/Alerts/src/Storage/SQLiteAlertStorage.cpp index c514583315..d7f7d7fc53 100644 --- a/CapabilityAgents/Alerts/src/Storage/SQLiteAlertStorage.cpp +++ b/CapabilityAgents/Alerts/src/Storage/SQLiteAlertStorage.cpp @@ -48,6 +48,12 @@ static const std::string TAG("SQLiteAlertStorage"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) +/// The key in our config file to find the root of settings for this Capability Agent. +static const std::string ALERTS_CAPABILITY_AGENT_CONFIGURATION_ROOT_KEY = "alertsCapabilityAgent"; + +/// The key in our config file to find the database file path. +static const std::string ALERTS_CAPABILITY_AGENT_DB_FILE_PATH_KEY = "databaseFilePath"; + /// A definition which we will store in the database to indicate Alarm type. static const int ALERT_EVENT_TYPE_ALARM = 1; /// A definition which we will store in the database to indicate Timer type. @@ -261,60 +267,53 @@ static bool dbFieldToAlertState(int dbState, Alert::State* state) { return false; } -/** - * A utility function to query if an alert exists in the database, given its database id. - * - * @param dbHandle The database handle. - * @param alertId The database id of the alert. - * @return Whether the alert was successfully found in the database. - */ -static bool alertExistsByAlertId(sqlite3* dbHandle, int alertId) { - const std::string sqlString = "SELECT COUNT(*) FROM " + ALERTS_V2_TABLE_NAME + " WHERE id=?;"; - - SQLiteStatement statement(dbHandle, sqlString); - - if (!statement.isValid()) { - ACSDK_ERROR(LX("alertExistsByAlertIdFailed").m("Could not create statement.")); - return false; +std::unique_ptr SQLiteAlertStorage::create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot, + const std::shared_ptr& alertsAudioFactory) { + auto alertsConfigurationRoot = configurationRoot[ALERTS_CAPABILITY_AGENT_CONFIGURATION_ROOT_KEY]; + if (!alertsConfigurationRoot) { + ACSDK_ERROR(LX("createFailed") + .d("reason", "Could not load config for the Alerts capability agent") + .d("key", ALERTS_CAPABILITY_AGENT_CONFIGURATION_ROOT_KEY)); + return nullptr; } - int boundParam = 1; - if (!statement.bindIntParameter(boundParam, alertId)) { - ACSDK_ERROR(LX("alertExistsByAlertIdFailed").m("Could not bind a parameter.")); - return false; + std::string alertDbFilePath; + if (!alertsConfigurationRoot.getString(ALERTS_CAPABILITY_AGENT_DB_FILE_PATH_KEY, &alertDbFilePath) || + alertDbFilePath.empty()) { + ACSDK_ERROR(LX("createFailed") + .d("reason", "Could not load config value") + .d("key", ALERTS_CAPABILITY_AGENT_DB_FILE_PATH_KEY)); + return nullptr; } - if (!statement.step()) { - ACSDK_ERROR(LX("alertExistsByAlertIdFailed").m("Could not step to next row.")); - return false; - } - - const int RESULT_COLUMN_POSITION = 0; - std::string rowValue = statement.getColumnText(RESULT_COLUMN_POSITION); - - int countValue = 0; - if (!stringToInt(rowValue.c_str(), &countValue)) { - ACSDK_ERROR(LX("alertExistsByAlertIdFailed").d("Could not convert string to integer", rowValue)); - return false; - } - - return countValue > 0; + return std::unique_ptr(new SQLiteAlertStorage(alertDbFilePath, alertsAudioFactory)); } SQLiteAlertStorage::SQLiteAlertStorage( + const std::string& dbFilePath, const std::shared_ptr& alertsAudioFactory) : - m_dbHandle{nullptr}, - m_alertsAudioFactory{alertsAudioFactory} { + m_alertsAudioFactory{alertsAudioFactory}, + m_db{dbFilePath} { +} + +SQLiteAlertStorage::~SQLiteAlertStorage() { + close(); } /** * Utility function to create the Alerts table within the database. * - * @param dbHandle A SQLite handle to an open database. + * @param db The SQLiteDatabase object. * @return Whether the table was successfully created. */ -static bool createAlertsTable(sqlite3* dbHandle) { - if (!performQuery(dbHandle, CREATE_ALERTS_TABLE_SQL_STRING)) { +static bool createAlertsTable(SQLiteDatabase* db) { + if (!db) { + ACSDK_ERROR(LX("createAlertsTableFailed").m("null db")); + return false; + } + + if (!db->performQuery(CREATE_ALERTS_TABLE_SQL_STRING)) { ACSDK_ERROR(LX("createAlertsTableFailed").m("Table could not be created.")); return false; } @@ -325,11 +324,16 @@ static bool createAlertsTable(sqlite3* dbHandle) { /** * Utility function to create the AlertAssets table within the database. * - * @param dbHandle A SQLite handle to an open database. + * @param db The SQLiteDatabase object. * @return Whether the table was successfully created. */ -static bool createAlertAssetsTable(sqlite3* dbHandle) { - if (!performQuery(dbHandle, CREATE_ALERT_ASSETS_TABLE_SQL_STRING)) { +static bool createAlertAssetsTable(SQLiteDatabase* db) { + if (!db) { + ACSDK_ERROR(LX("createAlertAssetsTableFailed").m("null db")); + return false; + } + + if (!db->performQuery(CREATE_ALERT_ASSETS_TABLE_SQL_STRING)) { ACSDK_ERROR(LX("createAlertAssetsTableFailed").m("Table could not be created.")); return false; } @@ -340,48 +344,42 @@ static bool createAlertAssetsTable(sqlite3* dbHandle) { /** * Utility function to create the AlertAssetPlayOrderItems table within the database. * - * @param dbHandle A SQLite handle to an open database. + * @param db The SQLiteDatabase object. * @return Whether the table was successfully created. */ -static bool createAlertAssetPlayOrderItemsTable(sqlite3* dbHandle) { - if (!performQuery(dbHandle, CREATE_ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_SQL_STRING)) { - ACSDK_ERROR(LX("createAlertAssetPlayOrderItemsTableFailed").m("Table could not be created.")); +static bool createAlertAssetPlayOrderItemsTable(SQLiteDatabase* db) { + if (!db) { + ACSDK_ERROR(LX("createAlertAssetPlayOrderItemsTableFailed").m("null db")); return false; } - return true; -} - -bool SQLiteAlertStorage::createDatabase(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").m("Database handle is already open.")); + if (!db->performQuery(CREATE_ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_SQL_STRING)) { + ACSDK_ERROR(LX("createAlertAssetPlayOrderItemsTableFailed").m("Table could not be created.")); return false; } - if (fileExists(filePath)) { - ACSDK_ERROR(LX("createDatabaseFailed").m("File specified already exists.").d("file path", filePath)); - return false; - } + return true; +} - m_dbHandle = createSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").m("Database could not be created.").d("file path", filePath)); +bool SQLiteAlertStorage::createDatabase() { + if (!m_db.initialize()) { + ACSDK_ERROR(LX("createDatabaseFailed")); return false; } - if (!createAlertsTable(m_dbHandle)) { + if (!createAlertsTable(&m_db)) { ACSDK_ERROR(LX("createDatabaseFailed").m("Alerts table could not be created.")); close(); return false; } - if (!createAlertAssetsTable(m_dbHandle)) { + if (!createAlertAssetsTable(&m_db)) { ACSDK_ERROR(LX("createDatabaseFailed").m("AlertAssets table could not be created.")); close(); return false; } - if (!createAlertAssetPlayOrderItemsTable(m_dbHandle)) { + if (!createAlertAssetPlayOrderItemsTable(&m_db)) { ACSDK_ERROR(LX("createDatabaseFailed").m("AlertAssetPlayOrderItems table could not be created.")); close(); return false; @@ -392,24 +390,24 @@ bool SQLiteAlertStorage::createDatabase(const std::string& filePath) { bool SQLiteAlertStorage::migrateAlertsDbFromV1ToV2() { // The good case - the db file is already up to date. - if (tableExists(m_dbHandle, ALERTS_V2_TABLE_NAME)) { + if (m_db.tableExists(ALERTS_V2_TABLE_NAME)) { return true; } - if (!createAlertsTable(m_dbHandle)) { - ACSDK_ERROR(LX("migrateAlertsDbFromV1ToV2Failed").m("AlertAssets table could not be created.")); + if (!createAlertsTable(&m_db)) { + ACSDK_ERROR(LX("migrateAlertsDbFromV1ToV2Failed").m("Alert table could not be created.")); return false; } - if (!tableExists(m_dbHandle, ALERT_ASSETS_TABLE_NAME)) { - if (!createAlertAssetsTable(m_dbHandle)) { + if (!m_db.tableExists(ALERT_ASSETS_TABLE_NAME)) { + if (!createAlertAssetsTable(&m_db)) { ACSDK_ERROR(LX("migrateAlertsDbFromV1ToV2Failed").m("AlertAssets table could not be created.")); return false; } } - if (!tableExists(m_dbHandle, ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME)) { - if (!createAlertAssetPlayOrderItemsTable(m_dbHandle)) { + if (!m_db.tableExists(ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME)) { + if (!createAlertAssetPlayOrderItemsTable(&m_db)) { ACSDK_ERROR( LX("migrateAlertsDbFromV1ToV2Failed").m("AlertAssetPlayOrderItems table could not be created.")); return false; @@ -419,7 +417,7 @@ bool SQLiteAlertStorage::migrateAlertsDbFromV1ToV2() { // We have created the new alerts tables, and the expectation is that the old alerts table exists. // Just for an edge case where it does not, let's not fail out if the old table does not exist - in that case, // the migration is fine. - if (tableExists(m_dbHandle, ALERTS_TABLE_NAME)) { + if (m_db.tableExists(ALERTS_TABLE_NAME)) { std::vector> alertContainer; if (!loadHelper(ALERTS_DATABASE_VERSION_ONE, &alertContainer)) { ACSDK_ERROR(LX("migrateAlertsDbFromV1ToV2Failed").m("Could not load V1 alert records.")); @@ -434,7 +432,9 @@ bool SQLiteAlertStorage::migrateAlertsDbFromV1ToV2() { } } - if (!dropTable(m_dbHandle, ALERTS_TABLE_NAME)) { + const std::string sqlString = "DROP TABLE IF EXISTS " + ALERTS_TABLE_NAME + ";"; + + if (!m_db.performQuery(sqlString)) { ACSDK_ERROR(LX("migrateAlertsDbFromV1ToV2Failed").m("Alerts table could not be dropped.")); return false; } @@ -443,66 +443,36 @@ bool SQLiteAlertStorage::migrateAlertsDbFromV1ToV2() { return true; } -bool SQLiteAlertStorage::open(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("openFailed").m("Database handle is already open.")); - return false; - } - - if (!fileExists(filePath)) { - ACSDK_ERROR(LX("openFailed").m("File specified does not exist.").d("file path", filePath)); - return false; - } - - m_dbHandle = openSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("openFailed").m("Database could not be opened.").d("file path", filePath)); - return false; - } - - if (!migrateAlertsDbFromV1ToV2()) { - ACSDK_ERROR(LX("openFailed").m("Could not migrate database file from V1 to V2.")); - close(); - return false; - } - - return true; -} - -bool SQLiteAlertStorage::isOpen() { - return (nullptr != m_dbHandle); +bool SQLiteAlertStorage::open() { + return m_db.open(); } void SQLiteAlertStorage::close() { - if (m_dbHandle) { - closeSQLiteDatabase(m_dbHandle); - m_dbHandle = nullptr; - } + m_db.close(); } bool SQLiteAlertStorage::alertExists(const std::string& token) { const std::string sqlString = "SELECT COUNT(*) FROM " + ALERTS_V2_TABLE_NAME + " WHERE token=?;"; + auto statement = m_db.createStatement(sqlString); - SQLiteStatement statement(m_dbHandle, sqlString); - - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("alertExistsFailed").m("Could not create statement.")); return false; } int boundParam = 1; - if (!statement.bindStringParameter(boundParam, token)) { + if (!statement->bindStringParameter(boundParam, token)) { ACSDK_ERROR(LX("alertExistsFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("alertExistsFailed").m("Could not step to next row.")); return false; } const int RESULT_COLUMN_POSITION = 0; - std::string rowValue = statement.getColumnText(RESULT_COLUMN_POSITION); + std::string rowValue = statement->getColumnText(RESULT_COLUMN_POSITION); int countValue = 0; if (!stringToInt(rowValue.c_str(), &countValue)) { @@ -513,8 +483,11 @@ bool SQLiteAlertStorage::alertExists(const std::string& token) { return countValue > 0; } -static bool storeAlertAssets(sqlite3* dbHandle, int alertId, const Alert::AssetConfiguration& assetConfiguration) { - if (assetConfiguration.assets.empty()) { +static bool storeAlertAssets( + SQLiteDatabase* db, + int alertId, + const std::unordered_map& assets) { + if (assets.empty()) { return true; } @@ -527,37 +500,36 @@ static bool storeAlertAssets(sqlite3* dbHandle, int alertId, const Alert::AssetC // clang-format on int id = 0; - if (!getTableMaxIntValue(dbHandle, ALERT_ASSETS_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &id)) { + if (!getTableMaxIntValue(db, ALERT_ASSETS_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &id)) { ACSDK_ERROR(LX("storeAlertAssetsFailed").m("Cannot generate asset id.")); return false; } id++; - SQLiteStatement statement(dbHandle, sqlString); - - if (!statement.isValid()) { + auto statement = db->createStatement(sqlString); + if (!statement) { ACSDK_ERROR(LX("storeAlertAssetsFailed").m("Could not create statement.")); return false; } // go through each asset in the alert, and store in the database. - for (auto& assetIter : assetConfiguration.assets) { + for (auto& assetIter : assets) { auto& asset = assetIter.second; int boundParam = 1; - if (!statement.bindIntParameter(boundParam++, id) || !statement.bindIntParameter(boundParam++, alertId) || - !statement.bindStringParameter(boundParam++, asset.id) || - !statement.bindStringParameter(boundParam, asset.url)) { + if (!statement->bindIntParameter(boundParam++, id) || !statement->bindIntParameter(boundParam++, alertId) || + !statement->bindStringParameter(boundParam++, asset.id) || + !statement->bindStringParameter(boundParam, asset.url)) { ACSDK_ERROR(LX("storeAlertAssetsFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("storeAlertAssetsFailed").m("Could not step to next row.")); return false; } - if (!statement.reset()) { + if (!statement->reset()) { ACSDK_ERROR(LX("storeAlertAssetsFailed").m("Could not reset the statement.")); return false; } @@ -569,10 +541,10 @@ static bool storeAlertAssets(sqlite3* dbHandle, int alertId, const Alert::AssetC } static bool storeAlertAssetPlayOrderItems( - sqlite3* dbHandle, + SQLiteDatabase* db, int alertId, - const Alert::AssetConfiguration& assetConfiguration) { - if (assetConfiguration.assetPlayOrderItems.empty()) { + const std::vector& assetPlayOrderItems) { + if (assetPlayOrderItems.empty()) { return true; } @@ -585,36 +557,36 @@ static bool storeAlertAssetPlayOrderItems( // clang-format on int id = 0; - if (!getTableMaxIntValue(dbHandle, ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &id)) { + if (!getTableMaxIntValue(db, ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &id)) { ACSDK_ERROR(LX("storeAlertAssetPlayOrderItemsFailed").m("Cannot generate asset id.")); return false; } id++; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("storeAlertAssetPlayOrderItemsFailed").m("Could not create statement.")); return false; } // go through each assetPlayOrderItem in the alert, and store in the database. int itemIndex = 1; - for (auto& assetId : assetConfiguration.assetPlayOrderItems) { + for (auto& assetId : assetPlayOrderItems) { int boundParam = 1; - if (!statement.bindIntParameter(boundParam++, id) || !statement.bindIntParameter(boundParam++, alertId) || - !statement.bindIntParameter(boundParam++, itemIndex) || - !statement.bindStringParameter(boundParam, assetId)) { + if (!statement->bindIntParameter(boundParam++, id) || !statement->bindIntParameter(boundParam++, alertId) || + !statement->bindIntParameter(boundParam++, itemIndex) || + !statement->bindStringParameter(boundParam, assetId)) { ACSDK_ERROR(LX("storeAlertAssetPlayOrderItemsFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("storeAlertAssetPlayOrderItemsFailed").m("Could not step to next row.")); return false; } - if (!statement.reset()) { + if (!statement->reset()) { ACSDK_ERROR(LX("storeAlertAssetPlayOrderItemsFailed").m("Could not reset the statement.")); return false; } @@ -627,11 +599,6 @@ static bool storeAlertAssetPlayOrderItems( } bool SQLiteAlertStorage::store(std::shared_ptr alert) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("storeFailed").m("Database handle is not open.")); - return false; - } - if (!alert) { ACSDK_ERROR(LX("storeFailed").m("Alert parameter is nullptr")); return false; @@ -655,7 +622,7 @@ bool SQLiteAlertStorage::store(std::shared_ptr alert) { // clang-format on int id = 0; - if (!getTableMaxIntValue(m_dbHandle, ALERTS_V2_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &id)) { + if (!getTableMaxIntValue(&m_db, ALERTS_V2_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &id)) { ACSDK_ERROR(LX("storeFailed").m("Cannot generate alert id.")); return false; } @@ -673,9 +640,9 @@ bool SQLiteAlertStorage::store(std::shared_ptr alert) { return false; } - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_db.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("storeFailed").m("Could not create statement.")); return false; } @@ -684,18 +651,19 @@ bool SQLiteAlertStorage::store(std::shared_ptr alert) { auto token = alert->m_token; auto iso8601 = alert->getScheduledTime_ISO_8601(); auto assetId = alert->getBackgroundAssetId(); - if (!statement.bindIntParameter(boundParam++, id) || !statement.bindStringParameter(boundParam++, token) || - !statement.bindIntParameter(boundParam++, alertType) || !statement.bindIntParameter(boundParam++, alertState) || - !statement.bindInt64Parameter(boundParam++, alert->getScheduledTime_Unix()) || - !statement.bindStringParameter(boundParam++, iso8601) || - !statement.bindIntParameter(boundParam++, alert->getLoopCount()) || - !statement.bindIntParameter(boundParam++, alert->getLoopPause().count()) || - !statement.bindStringParameter(boundParam, assetId)) { + if (!statement->bindIntParameter(boundParam++, id) || !statement->bindStringParameter(boundParam++, token) || + !statement->bindIntParameter(boundParam++, alertType) || + !statement->bindIntParameter(boundParam++, alertState) || + !statement->bindInt64Parameter(boundParam++, alert->getScheduledTime_Unix()) || + !statement->bindStringParameter(boundParam++, iso8601) || + !statement->bindIntParameter(boundParam++, alert->getLoopCount()) || + !statement->bindIntParameter(boundParam++, alert->getLoopPause().count()) || + !statement->bindStringParameter(boundParam, assetId)) { ACSDK_ERROR(LX("storeFailed").m("Could not bind parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("storeFailed").m("Could not perform step.")); return false; } @@ -703,14 +671,12 @@ bool SQLiteAlertStorage::store(std::shared_ptr alert) { // capture the generated database id in the alert object. alert->m_dbId = id; - statement.finalize(); - - if (!storeAlertAssets(m_dbHandle, id, alert->m_assetConfiguration)) { + if (!storeAlertAssets(&m_db, id, alert->m_assetConfiguration.assets)) { ACSDK_ERROR(LX("storeFailed").m("Could not store alertAssets.")); return false; } - if (!storeAlertAssetPlayOrderItems(m_dbHandle, id, alert->m_assetConfiguration)) { + if (!storeAlertAssetPlayOrderItems(&m_db, id, alert->m_assetConfiguration.assetPlayOrderItems)) { ACSDK_ERROR(LX("storeFailed").m("Could not store alertAssetPlayOrderItems.")); return false; } @@ -718,12 +684,12 @@ bool SQLiteAlertStorage::store(std::shared_ptr alert) { return true; } -static bool loadAlertAssets(sqlite3* dbHandle, std::map>* alertAssetsMap) { +static bool loadAlertAssets(SQLiteDatabase* db, std::map>* alertAssetsMap) { const std::string sqlString = "SELECT * FROM " + ALERT_ASSETS_TABLE_NAME + ";"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("loadAlertAssetsFailed").m("Could not create statement.")); return false; } @@ -732,42 +698,42 @@ static bool loadAlertAssets(sqlite3* dbHandle, std::mapstep()) { ACSDK_ERROR(LX("loadAlertAssetsFailed").m("Could not perform step.")); return false; } - while (SQLITE_ROW == statement.getStepResult()) { - int numberColumns = statement.getColumnCount(); + while (SQLITE_ROW == statement->getStepResult()) { + int numberColumns = statement->getColumnCount(); for (int i = 0; i < numberColumns; i++) { - std::string columnName = statement.getColumnName(i); + std::string columnName = statement->getColumnName(i); if ("alert_id" == columnName) { - alertId = statement.getColumnInt(i); + alertId = statement->getColumnInt(i); } else if ("avs_id" == columnName) { - avsId = statement.getColumnText(i); + avsId = statement->getColumnText(i); } else if ("url" == columnName) { - url = statement.getColumnText(i); + url = statement->getColumnText(i); } } (*alertAssetsMap)[alertId].push_back(Alert::Asset(avsId, url)); - statement.step(); + statement->step(); } return true; } static bool loadAlertAssetPlayOrderItems( - sqlite3* dbHandle, + SQLiteDatabase* db, std::map>* alertAssetOrderItemsMap) { const std::string sqlString = "SELECT * FROM " + ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME + ";"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("loadAlertAssetPlayOrderItemsFailed").m("Could not create statement.")); return false; } @@ -776,40 +742,35 @@ static bool loadAlertAssetPlayOrderItems( int playOrderPosition = 0; std::string playOrderToken; - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("loadAlertAssetPlayOrderItemsFailed").m("Could not perform step.")); return false; } - while (SQLITE_ROW == statement.getStepResult()) { - int numberColumns = statement.getColumnCount(); + while (SQLITE_ROW == statement->getStepResult()) { + int numberColumns = statement->getColumnCount(); for (int i = 0; i < numberColumns; i++) { - std::string columnName = statement.getColumnName(i); + std::string columnName = statement->getColumnName(i); if ("alert_id" == columnName) { - alertId = statement.getColumnInt(i); + alertId = statement->getColumnInt(i); } else if ("asset_play_order_position" == columnName) { - playOrderPosition = statement.getColumnInt(i); + playOrderPosition = statement->getColumnInt(i); } else if ("asset_play_order_token" == columnName) { - playOrderToken = statement.getColumnText(i); + playOrderToken = statement->getColumnText(i); } } (*alertAssetOrderItemsMap)[alertId].insert(AssetOrderItem{playOrderPosition, playOrderToken}); - statement.step(); + statement->step(); } return true; } bool SQLiteAlertStorage::loadHelper(int dbVersion, std::vector>* alertContainer) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("loadHelperFailed").m("Database handle is not open.")); - return false; - } - if (!alertContainer) { ACSDK_ERROR(LX("loadHelperFailed").m("Alert container parameter is nullptr.")); return false; @@ -827,9 +788,9 @@ bool SQLiteAlertStorage::loadHelper(int dbVersion, std::vectorstep()) { ACSDK_ERROR(LX("loadHelperFailed").m("Could not perform step.")); return false; } - while (SQLITE_ROW == statement.getStepResult()) { - int numberColumns = statement.getColumnCount(); + while (SQLITE_ROW == statement->getStepResult()) { + int numberColumns = statement->getColumnCount(); // SQLite cannot guarantee the order of the columns in a given row, so this logic is required. for (int i = 0; i < numberColumns; i++) { - std::string columnName = statement.getColumnName(i); + std::string columnName = statement->getColumnName(i); if ("id" == columnName) { - id = statement.getColumnInt(i); + id = statement->getColumnInt(i); } else if ("token" == columnName) { - token = statement.getColumnText(i); + token = statement->getColumnText(i); } else if ("type" == columnName) { - type = statement.getColumnInt(i); + type = statement->getColumnInt(i); } else if ("state" == columnName) { - state = statement.getColumnInt(i); + state = statement->getColumnInt(i); } else if ("scheduled_time_iso_8601" == columnName) { - scheduledTime_ISO_8601 = statement.getColumnText(i); + scheduledTime_ISO_8601 = statement->getColumnText(i); } else if ("asset_loop_count" == columnName) { - loopCount = statement.getColumnInt(i); + loopCount = statement->getColumnInt(i); } else if ("asset_loop_pause_milliseconds" == columnName) { - loopPauseInMilliseconds = statement.getColumnInt(i); + loopPauseInMilliseconds = statement->getColumnInt(i); } else if ("background_asset" == columnName) { - backgroundAssetId = statement.getColumnText(i); + backgroundAssetId = statement->getColumnText(i); } } @@ -903,19 +864,19 @@ bool SQLiteAlertStorage::loadHelper(int dbVersion, std::vectorpush_back(alert); - statement.step(); + statement->step(); } - statement.finalize(); + statement->finalize(); std::map> alertAssetsMap; - if (!loadAlertAssets(m_dbHandle, &alertAssetsMap)) { + if (!loadAlertAssets(&m_db, &alertAssetsMap)) { ACSDK_ERROR(LX("loadHelperFailed").m("Could not load alert assets.")); return false; } std::map> alertAssetOrderItemsMap; - if (!loadAlertAssetPlayOrderItems(m_dbHandle, &alertAssetOrderItemsMap)) { + if (!loadAlertAssetPlayOrderItems(&m_db, &alertAssetOrderItemsMap)) { ACSDK_ERROR(LX("loadHelperFailed").m("Could not load alert asset play order items.")); return false; } @@ -941,11 +902,6 @@ bool SQLiteAlertStorage::load(std::vector>* alertContaine } bool SQLiteAlertStorage::modify(std::shared_ptr alert) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("modifyFailed").m("Database handle is not open.")); - return false; - } - if (!alert) { ACSDK_ERROR(LX("modifyFailed").m("Alert parameter is nullptr.")); return false; @@ -965,24 +921,24 @@ bool SQLiteAlertStorage::modify(std::shared_ptr alert) { return false; } - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_db.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("modifyFailed").m("Could not create statement.")); return false; } int boundParam = 1; auto iso8601 = alert->getScheduledTime_ISO_8601(); - if (!statement.bindIntParameter(boundParam++, alertState) || - !statement.bindInt64Parameter(boundParam++, alert->getScheduledTime_Unix()) || - !statement.bindStringParameter(boundParam++, iso8601) || - !statement.bindIntParameter(boundParam++, alert->m_dbId)) { + if (!statement->bindIntParameter(boundParam++, alertState) || + !statement->bindInt64Parameter(boundParam++, alert->getScheduledTime_Unix()) || + !statement->bindStringParameter(boundParam++, iso8601) || + !statement->bindIntParameter(boundParam++, alert->m_dbId)) { ACSDK_ERROR(LX("modifyFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("modifyFailed").m("Could not perform step.")); return false; } @@ -994,27 +950,27 @@ bool SQLiteAlertStorage::modify(std::shared_ptr alert) { * A utility function to delete alert records from the database for a given alert id. * This function will clean up records in the alerts table. * - * @param dbHandle The database handle. + * @param db The database object. * @param alertId The alert id of the alert to be deleted. * @return Whether the delete operation was successful. */ -static bool eraseAlert(sqlite3* dbHandle, int alertId) { +static bool eraseAlert(SQLiteDatabase* db, int alertId) { const std::string sqlString = "DELETE FROM " + ALERTS_V2_TABLE_NAME + " WHERE id=?;"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("Could not create statement.")); return false; } int boundParam = 1; - if (!statement.bindIntParameter(boundParam, alertId)) { + if (!statement->bindIntParameter(boundParam, alertId)) { ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("Could not perform step.")); return false; } @@ -1026,27 +982,27 @@ static bool eraseAlert(sqlite3* dbHandle, int alertId) { * A utility function to delete alert records from the database for a given alert id. * This function will clean up records in the alertAssets table. * - * @param dbHandle The database handle. + * @param db The database object. * @param alertId The alert id of the alert to be deleted. * @return Whether the delete operation was successful. */ -static bool eraseAlertAssets(sqlite3* dbHandle, int alertId) { +static bool eraseAlertAssets(SQLiteDatabase* db, int alertId) { const std::string sqlString = "DELETE FROM " + ALERT_ASSETS_TABLE_NAME + " WHERE alert_id=?;"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("eraseAlertAssetsFailed").m("Could not create statement.")); return false; } int boundParam = 1; - if (!statement.bindIntParameter(boundParam, alertId)) { + if (!statement->bindIntParameter(boundParam, alertId)) { ACSDK_ERROR(LX("eraseAlertAssetsFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("eraseAlertAssetsFailed").m("Could not perform step.")); return false; } @@ -1058,27 +1014,27 @@ static bool eraseAlertAssets(sqlite3* dbHandle, int alertId) { * A utility function to delete alert records from the database for a given alert id. * This function will clean up records in the alertAssetPlayOrderItems table. * - * @param dbHandle The database handle. + * @param db The database object. * @param alertId The alert id of the alert to be deleted. * @return Whether the delete operation was successful. */ -static bool eraseAlertAssetPlayOrderItems(sqlite3* dbHandle, int alertId) { +static bool eraseAlertAssetPlayOrderItems(SQLiteDatabase* db, int alertId) { const std::string sqlString = "DELETE FROM " + ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME + " WHERE alert_id=?;"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("eraseAlertAssetPlayOrderItemsFailed").m("Could not create statement.")); return false; } int boundParam = 1; - if (!statement.bindIntParameter(boundParam, alertId)) { + if (!statement->bindIntParameter(boundParam, alertId)) { ACSDK_ERROR(LX("eraseAlertAssetPlayOrderItemsFailed").m("Could not bind a parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("eraseAlertAssetPlayOrderItemsFailed").m("Could not perform step.")); return false; } @@ -1090,27 +1046,27 @@ static bool eraseAlertAssetPlayOrderItems(sqlite3* dbHandle, int alertId) { * A utility function to delete an alert from the database for a given alert id. This will clean up records in * all tables which are associated with the alert. * - * @param dbHandle The database handle. + * @param db The DB object. * @param alertId The alert id of the alert to be deleted. * @return Whether the delete operation was successful. */ -static bool eraseAlertByAlertId(sqlite3* dbHandle, int alertId) { - if (!dbHandle) { - ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("dbHandle is nullptr.")); +static bool eraseAlertByAlertId(SQLiteDatabase* db, int alertId) { + if (!db) { + ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("db is nullptr.")); return false; } - if (!eraseAlert(dbHandle, alertId)) { + if (!eraseAlert(db, alertId)) { ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("Could not erase alert table items.")); return false; } - if (!eraseAlertAssets(dbHandle, alertId)) { + if (!eraseAlertAssets(db, alertId)) { ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("Could not erase alertAsset table items.")); return false; } - if (!eraseAlertAssetPlayOrderItems(dbHandle, alertId)) { + if (!eraseAlertAssetPlayOrderItems(db, alertId)) { ACSDK_ERROR(LX("eraseAlertByAlertIdFailed").m("Could not erase alertAssetPlayOrderItems table items.")); return false; } @@ -1119,11 +1075,6 @@ static bool eraseAlertByAlertId(sqlite3* dbHandle, int alertId) { } bool SQLiteAlertStorage::erase(std::shared_ptr alert) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("eraseFailed").m("Database handle is not open.")); - return false; - } - if (!alert) { ACSDK_ERROR(LX("eraseFailed").m("Alert parameter is nullptr.")); return false; @@ -1134,23 +1085,15 @@ bool SQLiteAlertStorage::erase(std::shared_ptr alert) { return false; } - return eraseAlertByAlertId(m_dbHandle, alert->m_dbId); + return eraseAlertByAlertId(&m_db, alert->m_dbId); } -bool SQLiteAlertStorage::erase(const std::vector& alertDbIds) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("eraseFailed").m("Database handle is not open.")); - return false; - } - - for (auto id : alertDbIds) { - if (!alertExistsByAlertId(m_dbHandle, id)) { - ACSDK_ERROR(LX("eraseFailed").m("Cannot erase an alert - does not exist in db.").d("id", id)); - return false; - } - - if (!eraseAlertByAlertId(m_dbHandle, id)) { - ACSDK_ERROR(LX("eraseFailed").m("Cannot erase an alert.").d("id", id)); +bool SQLiteAlertStorage::clearDatabase() { + const std::vector tablesToClear = { + ALERTS_V2_TABLE_NAME, ALERT_ASSETS_TABLE_NAME, ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME}; + for (auto& tableName : tablesToClear) { + if (!m_db.clearTable(tableName)) { + ACSDK_ERROR(LX("clearDatabaseFailed").d("could not clear table", tableName)); return false; } } @@ -1158,32 +1101,15 @@ bool SQLiteAlertStorage::erase(const std::vector& alertDbIds) { return true; } -bool SQLiteAlertStorage::clearDatabase() { - if (!clearTable(m_dbHandle, ALERTS_V2_TABLE_NAME)) { - ACSDK_ERROR(LX("clearDatabaseFailed").m("could not clear alerts table.")); - return false; - } - if (!clearTable(m_dbHandle, ALERT_ASSETS_TABLE_NAME)) { - ACSDK_ERROR(LX("clearDatabaseFailed").m("could not clear alertAssets table.")); - return false; - } - if (!clearTable(m_dbHandle, ALERT_ASSET_PLAY_ORDER_ITEMS_TABLE_NAME)) { - ACSDK_ERROR(LX("clearDatabaseFailed").m("could not clear alertAssetPlayOrderItems table.")); - return false; - } - - return true; -} - /** * Utility diagnostic function to print a one-line summary of all alerts in the database. * - * @param dbHandle The database handle. + * @param db The database object. */ -static void printOneLineSummary(sqlite3* dbHandle) { +static void printOneLineSummary(SQLiteDatabase* db) { int numberAlerts = 0; - if (!getNumberTableRows(dbHandle, ALERTS_V2_TABLE_NAME, &numberAlerts)) { + if (!getNumberTableRows(db, ALERTS_V2_TABLE_NAME, &numberAlerts)) { ACSDK_ERROR(LX("printOneLineSummaryFailed").m("could not read number of alerts.")); return; } @@ -1194,15 +1120,15 @@ static void printOneLineSummary(sqlite3* dbHandle) { /** * Utility diagnostic function to print the details of all the alerts stored in the database. * - * @param dbHandle The database handle. + * @param db The database object. * @param shouldPrintEverything If @c true, then all details of an alert will be printed. If @c false, then * summary information will be printed instead. */ static void printAlertsSummary( - sqlite3* dbHandle, + SQLiteDatabase* db, const std::vector>& alerts, bool shouldPrintEverything = false) { - printOneLineSummary(dbHandle); + printOneLineSummary(db); for (auto& alert : alerts) { alert->printDiagnostic(); @@ -1214,13 +1140,13 @@ void SQLiteAlertStorage::printStats(StatLevel level) { load(&alerts); switch (level) { case StatLevel::ONE_LINE: - printOneLineSummary(m_dbHandle); + printOneLineSummary(&m_db); break; case StatLevel::ALERTS_SUMMARY: - printAlertsSummary(m_dbHandle, alerts, false); + printAlertsSummary(&m_db, alerts, false); break; case StatLevel::EVERYTHING: - printAlertsSummary(m_dbHandle, alerts, true); + printAlertsSummary(&m_db, alerts, true); break; } } diff --git a/CapabilityAgents/Alerts/test/AlertSchedulerTest.cpp b/CapabilityAgents/Alerts/test/AlertSchedulerTest.cpp index a844fef7ba..deab781505 100644 --- a/CapabilityAgents/Alerts/test/AlertSchedulerTest.cpp +++ b/CapabilityAgents/Alerts/test/AlertSchedulerTest.cpp @@ -23,9 +23,6 @@ namespace capabilityAgents { namespace alerts { namespace test { -/// File path where the database for alerts storage will exist. -static const std::string STORAGE_FILE_PATH = "test/file/path"; - /// Tokens for alerts. static const std::string ALERT1_TOKEN = "token1"; static const std::string ALERT2_TOKEN = "token2"; @@ -121,10 +118,10 @@ class MockAlertStorage : public storage::AlertStorageInterface { m_eraseRetVal = true; } - bool createDatabase(const std::string& filePath) { + bool createDatabase() { return m_createDatabaseRetVal; } - bool open(const std::string& filePath) { + bool open() { return m_openRetVal; } bool isOpen() { @@ -150,9 +147,6 @@ class MockAlertStorage : public storage::AlertStorageInterface { bool erase(const std::vector& alertDbIds) { return m_eraseRetVal; } - void printStats(StatLevel level) { - } - void setCreateDatabaseRetVal(bool retVal) { m_createDatabaseRetVal = retVal; } @@ -258,11 +252,11 @@ std::shared_ptr AlertSchedulerTest::doSimpleTestSetup(bool activateAl m_alertStorage->setAlerts(alertToAdd); if (initWithAlertObserver) { - m_alertScheduler->initialize(STORAGE_FILE_PATH, m_testAlertObserver); + m_alertScheduler->initialize(m_testAlertObserver); } else { std::shared_ptr alertSchedulerObs{ std::make_shared(m_alertStorage, m_alertRenderer, m_alertPastDueTimeLimit)}; - m_alertScheduler->initialize(STORAGE_FILE_PATH, alertSchedulerObs); + m_alertScheduler->initialize(alertSchedulerObs); } if (activateAlert) { @@ -278,12 +272,12 @@ std::shared_ptr AlertSchedulerTest::doSimpleTestSetup(bool activateAl */ TEST_F(AlertSchedulerTest, initialize) { /// check if init fails if scheduler is not available - ASSERT_FALSE(m_alertScheduler->initialize(STORAGE_FILE_PATH, nullptr)); + ASSERT_FALSE(m_alertScheduler->initialize(nullptr)); /// check if init fails if a database for alerts cant be created m_alertStorage->setOpenRetVal(false); m_alertStorage->setCreateDatabaseRetVal(false); - ASSERT_FALSE(m_alertScheduler->initialize(STORAGE_FILE_PATH, m_alertScheduler)); + ASSERT_FALSE(m_alertScheduler->initialize(m_alertScheduler)); /// check if init succeeds. Pass in 3 alerts of which 1 is expired. Only 2 should actually remain in the end. std::shared_ptr alertSchedulerObs{ @@ -312,7 +306,7 @@ TEST_F(AlertSchedulerTest, initialize) { /// active alert should get modified EXPECT_CALL(*(m_alertStorage.get()), modify(testing::_)).Times(1); - ASSERT_TRUE(m_alertScheduler->initialize(STORAGE_FILE_PATH, alertSchedulerObs)); + ASSERT_TRUE(m_alertScheduler->initialize(alertSchedulerObs)); const unsigned int expectedRemainingAlerts = 2; @@ -382,7 +376,7 @@ TEST_F(AlertSchedulerTest, deleteAlert) { std::shared_ptr alert1 = std::make_shared(ALERT1_TOKEN, FUTURE_INSTANT); alertsToAdd.push_back(alert1); m_alertStorage->setAlerts(alertsToAdd); - m_alertScheduler->initialize(STORAGE_FILE_PATH, alertSchedulerObs); + m_alertScheduler->initialize(alertSchedulerObs); m_alertScheduler->updateFocus(avsCommon::avs::FocusState::BACKGROUND); /// if active alert and the token matches, ensure that we dont delete it (we deactivate the alert actually) @@ -396,7 +390,7 @@ TEST_F(AlertSchedulerTest, deleteAlert) { std::shared_ptr alert2 = std::make_shared(ALERT2_TOKEN, FUTURE_INSTANT); alertsToAdd.push_back(alert2); m_alertStorage->setAlerts(alertsToAdd); - m_alertScheduler->initialize(STORAGE_FILE_PATH, alertSchedulerObs); + m_alertScheduler->initialize(alertSchedulerObs); EXPECT_CALL(*(m_alertStorage.get()), erase(testing::_)).Times(1); ASSERT_TRUE(m_alertScheduler->deleteAlert(ALERT2_TOKEN)); } @@ -413,7 +407,7 @@ TEST_F(AlertSchedulerTest, isAlertActive) { std::shared_ptr alert1 = std::make_shared(ALERT1_TOKEN, FUTURE_INSTANT); alertsToAdd.push_back(alert1); m_alertStorage->setAlerts(alertsToAdd); - m_alertScheduler->initialize(STORAGE_FILE_PATH, alertSchedulerObs); + m_alertScheduler->initialize(alertSchedulerObs); m_alertScheduler->updateFocus(avsCommon::avs::FocusState::BACKGROUND); /// inactive alert @@ -442,7 +436,7 @@ TEST_F(AlertSchedulerTest, getContextInfo) { std::shared_ptr alert2 = std::make_shared(ALERT2_TOKEN, FUTURE_INSTANT); alertsToAdd.push_back(alert2); m_alertStorage->setAlerts(alertsToAdd); - m_alertScheduler->initialize(STORAGE_FILE_PATH, alertSchedulerObs); + m_alertScheduler->initialize(alertSchedulerObs); m_alertScheduler->updateFocus(avsCommon::avs::FocusState::BACKGROUND); AlertScheduler::AlertsContextInfo resultContextInfo = m_alertScheduler->getContextInfo(); @@ -480,6 +474,19 @@ TEST_F(AlertSchedulerTest, clearData) { ASSERT_EQ(alert->getStopReason(), Alert::StopReason::SHUTDOWN); } +/** + * Test if AlertScheduler clears data + */ +TEST_F(AlertSchedulerTest, clearDataLogout) { + std::shared_ptr alert = doSimpleTestSetup(true); + EXPECT_CALL(*(m_alertStorage.get()), clearDatabase()).Times(1); + + m_alertScheduler->clearData(Alert::StopReason::LOG_OUT); + + ASSERT_EQ(alert->getState(), Alert::State::STOPPING); + ASSERT_EQ(alert->getStopReason(), Alert::StopReason::LOG_OUT); +} + /** * Test if AlertScheduler shuts down appropriately */ diff --git a/CapabilityAgents/Alerts/test/AlertTest.cpp b/CapabilityAgents/Alerts/test/AlertTest.cpp index ec24d3bb94..814f8250d2 100644 --- a/CapabilityAgents/Alerts/test/AlertTest.cpp +++ b/CapabilityAgents/Alerts/test/AlertTest.cpp @@ -233,10 +233,11 @@ TEST_F(AlertTest, testDeactivate) { } TEST_F(AlertTest, testSetTimeISO8601) { + avsCommon::utils::timing::TimeUtils timeUtils; std::string schedTime{"2030-02-02T12:56:34+0000"}; m_alert->setTime_ISO_8601(schedTime); int64_t unixTime = 0; - avsCommon::utils::timing::convert8601TimeStringToUnix(schedTime, &unixTime); + timeUtils.convert8601TimeStringToUnix(schedTime, &unixTime); ASSERT_EQ(m_alert->getScheduledTime_ISO_8601(), schedTime); ASSERT_EQ(m_alert->getScheduledTime_Unix(), unixTime); @@ -279,8 +280,9 @@ TEST_F(AlertTest, testSetBackgroundAssetId) { } TEST_F(AlertTest, testIsPastDue) { + avsCommon::utils::timing::TimeUtils timeUtils; int64_t currentUnixTime = 0; - avsCommon::utils::timing::getCurrentUnixTime(¤tUnixTime); + timeUtils.getCurrentUnixTime(¤tUnixTime); m_alert->setTime_ISO_8601(TEST_DATE_IN_THE_FUTURE); ASSERT_FALSE(m_alert->isPastDue(currentUnixTime, std::chrono::seconds{1})); m_alert->setTime_ISO_8601(TEST_DATE_IN_THE_PAST); diff --git a/CapabilityAgents/Alerts/test/Renderer/RendererTest.cpp b/CapabilityAgents/Alerts/test/Renderer/RendererTest.cpp index f974e862cc..2a27c946a1 100644 --- a/CapabilityAgents/Alerts/test/Renderer/RendererTest.cpp +++ b/CapabilityAgents/Alerts/test/Renderer/RendererTest.cpp @@ -122,7 +122,7 @@ class RendererTest : public ::testing::Test { std::shared_ptr m_renderer; static std::unique_ptr audioFactoryFunc() { - return std::unique_ptr(); + return std::unique_ptr(new std::stringstream()); } }; diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h index f86f68e58c..e2ddfa85d7 100644 --- a/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioItem.h @@ -16,6 +16,12 @@ #ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_AUDIOPLAYER_INCLUDE_AUDIOPLAYER_AUDIOITEM_H_ #define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_AUDIOPLAYER_INCLUDE_AUDIOPLAYER_AUDIOITEM_H_ +#include +#include +#include + +#include + #include "StreamFormat.h" namespace alexaClientSDK { diff --git a/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h index f8b6d3495f..0773b22ae3 100644 --- a/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h +++ b/CapabilityAgents/AudioPlayer/include/AudioPlayer/AudioPlayer.h @@ -31,6 +31,7 @@ #include #include #include +#include #include "AudioItem.h" #include "ClearBehavior.h" @@ -363,6 +364,9 @@ class AudioPlayer /// @} + /// This is used to safely access the time utilities. + avsCommon::utils::timing::TimeUtils m_timeUtils; + /// MediaPlayerInterface instance to send audio attachments to. std::shared_ptr m_mediaPlayer; diff --git a/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp b/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp index 027b620cc1..0ea793d97c 100644 --- a/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp +++ b/CapabilityAgents/AudioPlayer/src/AudioPlayer.cpp @@ -22,7 +22,6 @@ #include #include -#include #include "AudioPlayer/IntervalCalculator.h" @@ -54,9 +53,6 @@ static const AudioPlayer::SourceId ERROR_SOURCE_ID = MediaPlayerInterface::ERROR /// The name of the @c FocusManager channel used by @c AudioPlayer. static const std::string CHANNEL_NAME = avsCommon::sdkInterfaces::FocusManagerInterface::CONTENT_CHANNEL_NAME; -/// The activityId string used with @c FocusManager by @c AudioPlayer. -static const std::string ACTIVITY_ID = "AudioPlayer.Play"; - /// The namespace for this capability agent. static const std::string NAMESPACE = "AudioPlayer"; @@ -471,9 +467,9 @@ void AudioPlayer::handlePlayDirective(std::shared_ptr info) { audioItem.stream.expiryTime = std::chrono::steady_clock::time_point::max(); if (jsonUtils::retrieveValue(stream->value, "expiryTime", &expiryTimeString)) { int64_t unixTime; - if (timing::convert8601TimeStringToUnix(expiryTimeString, &unixTime)) { + if (m_timeUtils.convert8601TimeStringToUnix(expiryTimeString, &unixTime)) { int64_t currentTime; - if (timing::getCurrentUnixTime(¤tTime)) { + if (m_timeUtils.getCurrentUnixTime(¤tTime)) { std::chrono::seconds timeToExpiry(unixTime - currentTime); audioItem.stream.expiryTime = std::chrono::steady_clock::now() + timeToExpiry; } @@ -558,7 +554,9 @@ void AudioPlayer::executeProvideState(bool sendToken, unsigned int stateRequestT rapidjson::Document state(rapidjson::kObjectType); state.AddMember(TOKEN_KEY, m_token, state.GetAllocator()); state.AddMember( - OFFSET_KEY, std::chrono::duration_cast(getOffset()).count(), state.GetAllocator()); + OFFSET_KEY, + (int64_t)std::chrono::duration_cast(getOffset()).count(), + state.GetAllocator()); state.AddMember(ACTIVITY_KEY, playerActivityToString(m_currentActivity), state.GetAllocator()); rapidjson::StringBuffer buffer; @@ -945,12 +943,12 @@ void AudioPlayer::executePlay(PlayBehavior playBehavior, const AudioItem& audioI if (FocusState::NONE == m_focus) { // If we don't currently have focus, acquire it now; playback will start when focus changes to // FOREGROUND. - if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), ACTIVITY_ID)) { + if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), NAMESPACE)) { ACSDK_ERROR(LX("executePlayFailed").d("reason", "CouldNotAcquireChannel")); sendPlaybackFailedEvent( m_token, ErrorType::MEDIA_ERROR_INTERNAL_DEVICE_ERROR, - std::string("Could not acquire ") + CHANNEL_NAME + " for " + ACTIVITY_ID); + std::string("Could not acquire ") + CHANNEL_NAME + " for " + NAMESPACE); return; } } @@ -1092,7 +1090,9 @@ void AudioPlayer::sendEventWithTokenAndOffset(const std::string& eventName, std: offset = getOffset(); } payload.AddMember( - OFFSET_KEY, std::chrono::duration_cast(offset).count(), payload.GetAllocator()); + OFFSET_KEY, + (int64_t)std::chrono::duration_cast(offset).count(), + payload.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); @@ -1130,11 +1130,13 @@ void AudioPlayer::sendPlaybackStutterFinishedEvent() { rapidjson::Document payload(rapidjson::kObjectType); payload.AddMember(TOKEN_KEY, m_token, payload.GetAllocator()); payload.AddMember( - OFFSET_KEY, std::chrono::duration_cast(getOffset()).count(), payload.GetAllocator()); + OFFSET_KEY, + (int64_t)std::chrono::duration_cast(getOffset()).count(), + payload.GetAllocator()); auto stutterDuration = std::chrono::steady_clock::now() - m_bufferUnderrunTimestamp; payload.AddMember( STUTTER_DURATION_KEY, - std::chrono::duration_cast(stutterDuration).count(), + (int64_t)std::chrono::duration_cast(stutterDuration).count(), payload.GetAllocator()); rapidjson::StringBuffer buffer; @@ -1163,7 +1165,9 @@ void AudioPlayer::sendPlaybackFailedEvent( rapidjson::Value currentPlaybackState(rapidjson::kObjectType); currentPlaybackState.AddMember(TOKEN_KEY, m_token, payload.GetAllocator()); currentPlaybackState.AddMember( - OFFSET_KEY, std::chrono::duration_cast(getOffset()).count(), payload.GetAllocator()); + OFFSET_KEY, + (int64_t)std::chrono::duration_cast(getOffset()).count(), + payload.GetAllocator()); currentPlaybackState.AddMember(ACTIVITY_KEY, playerActivityToString(m_currentActivity), payload.GetAllocator()); payload.AddMember("currentPlaybackState", currentPlaybackState, payload.GetAllocator()); diff --git a/CapabilityAgents/AudioPlayer/test/AudioPlayerTest.cpp b/CapabilityAgents/AudioPlayer/test/AudioPlayerTest.cpp index 35a0fab843..4bf0432949 100644 --- a/CapabilityAgents/AudioPlayer/test/AudioPlayerTest.cpp +++ b/CapabilityAgents/AudioPlayer/test/AudioPlayerTest.cpp @@ -66,10 +66,7 @@ using namespace rapidjson; static std::chrono::milliseconds WAIT_TIMEOUT(1000); /// The name of the @c FocusManager channel used by the @c AudioPlayer. -static const std::string CHANNEL_NAME("Content"); - -/// The activity Id used with the @c FocusManager by @c AudioPlayer. -static const std::string FOCUS_MANAGER_ACTIVITY_ID("AudioPlayer.Play"); +static const std::string CHANNEL_NAME(avsCommon::sdkInterfaces::FocusManagerInterface::CONTENT_CHANNEL_NAME); /// Namespace for AudioPlayer. static const std::string NAMESPACE_AUDIO_PLAYER("AudioPlayer"); @@ -586,7 +583,7 @@ void AudioPlayerTest::sendPlayDirective(long offsetInMilliseconds) { std::shared_ptr playDirective = AVSDirective::create( "", avsMessageHeader, createEnqueuePayloadTest(offsetInMilliseconds), m_attachmentManager, CONTEXT_ID_TEST); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_AUDIO_PLAYER)) .Times(1) .WillOnce(InvokeWithoutArgs(this, &AudioPlayerTest::wakeOnAcquireChannel)); @@ -849,7 +846,7 @@ TEST_F(AudioPlayerTest, testTransitionFromStoppedToPlaying) { EXPECT_CALL(*(m_mockMediaPlayer.get()), play(_)).Times(AtLeast(1)); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_AUDIO_PLAYER)) .Times(1) .WillOnce(Return(true)); @@ -889,7 +886,6 @@ TEST_F(AudioPlayerTest, testTransitionFromPlayingToPaused) { /** * Test transition from Paused to Stopped on ClearQueue.CLEAR_ALL directive */ - TEST_F(AudioPlayerTest, testTransitionFromPausedToStopped) { sendPlayDirective(); @@ -1367,7 +1363,6 @@ TEST_F(AudioPlayerTest, testFocusChangesInStoppedState) { * Expect to resume when switching to FOREGROUND, expect nothing when switching to BACKGROUND, expect stop when * switching to NONE */ - TEST_F(AudioPlayerTest, testFocusChangesInPausedState) { sendPlayDirective(); @@ -1448,7 +1443,7 @@ TEST_F(AudioPlayerTest, testFocusChangeToBackgroundBeforeOnPlaybackStarted) { m_audioPlayer->onFocusChanged(FocusState::NONE); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_AUDIO_PLAYER)) .Times(1) .WillOnce(Return(true)); @@ -1502,7 +1497,7 @@ TEST_F(AudioPlayerTest, testPlayAfterOnPlaybackError) { m_wakeAcquireChannelPromise = std::promise(); m_wakeAcquireChannelFuture = m_wakeAcquireChannelPromise.get_future(); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_AUDIO_PLAYER)) .Times(1) .WillOnce(InvokeWithoutArgs(this, &AudioPlayerTest::wakeOnAcquireChannel)); m_audioPlayer->CapabilityAgent::preHandleDirective(playDirective, std::move(m_mockDirectiveHandlerResult)); @@ -1647,3 +1642,14 @@ TEST_F(AudioPlayerTest, testProgressReportIntervalElapsedIntervalLessThanOffset) } // namespace audioPlayer } // namespace capabilityAgents } // namespace alexaClientSDK + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + +// ACSDK-1216 - AudioPlayer tests sometimes fail in Windows +#if !defined(_WIN32) || defined(RESOLVED_ACSDK_1216) + return RUN_ALL_TESTS(); +#else + return 0; +#endif +} diff --git a/CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/ExternalMediaPlayer.h b/CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/ExternalMediaPlayer.h index 79c51391d8..fa38e70a09 100644 --- a/CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/ExternalMediaPlayer.h +++ b/CapabilityAgents/ExternalMediaPlayer/include/ExternalMediaPlayer/ExternalMediaPlayer.h @@ -20,12 +20,11 @@ #include #include -#include "ExternalMediaPlayer/AdapterUtils.h" - #include #include #include #include +#include #include #include #include @@ -50,18 +49,34 @@ namespace externalMediaPlayer { class ExternalMediaPlayer : public avsCommon::avs::CapabilityAgent , public avsCommon::utils::RequiresShutdown + , public avsCommon::sdkInterfaces::ExternalMediaPlayerInterface , public avsCommon::sdkInterfaces::SpeakerInterface , public avsCommon::sdkInterfaces::PlaybackHandlerInterface , public std::enable_shared_from_this { public: + // Map of adapter business names to their mediaPlayers. using AdapterMediaPlayerMap = std::unordered_map>; + // Signature of functions to create an ExternalMediaAdapter. + using AdapterCreateFunction = + std::shared_ptr (*)( + std::shared_ptr mediaPlayer, + std::shared_ptr speakerManager, + std::shared_ptr messageSender, + std::shared_ptr focusManager, + std::shared_ptr contextManager, + std::shared_ptr externalMediaPlayer); + + // Map of adapter business names to their creation method. + using AdapterCreationMap = std::unordered_map; + /** * Creates a new @c ExternalMediaPlayer instance. * * @param mediaPlayers The map of to be used to find the mediaPlayer to use for this * adapter. + * @param adapterCreationMap The map of to be used to create the adapters. * @param speakerManager A @c SpeakerManagerInterface to perform volume changes requested by adapters. * @param messageSender The object to use for sending events. * @param focusManager The object used to manage focus for the adapter managed by the EMP. @@ -72,6 +87,7 @@ class ExternalMediaPlayer */ static std::shared_ptr create( const AdapterMediaPlayerMap& mediaPlayers, + const AdapterCreationMap& adapterCreationMap, std::shared_ptr speakerManager, std::shared_ptr messageSender, std::shared_ptr focusManager, @@ -109,46 +125,10 @@ class ExternalMediaPlayer virtual void onButtonPressed(avsCommon::avs::PlaybackButton button) override; /// @} - /** - * Method to set the player in focus after an adapter has acquired the channel. - * - * @param playerInFocus The business name of the adapter that has currently acquired focus. - */ - void setPlayerInFocus(const std::string& playerInFocus); - - /** - * Method to reset the player in focus after an adapter has acquired the channel. - * - * @param playerInFocus The business name of the adapter that has currently released focus. - * If the currently playerInFocus is the one releasing focus we reset the playerInFocus to - * a null string. - */ - void resetPlayerInFocus(const std::string& playerInFocus); - - // Signature of functions to create an ExternalMediaAdapter. - using AdapterCreateFunction = - std::shared_ptr (*)( - std::shared_ptr mediaPlayer, - std::shared_ptr speakerManager, - std::shared_ptr messageSender, - std::shared_ptr focusManager, - std::shared_ptr contextManager, - std::shared_ptr externalMediaPlayer); - - /** - * Instances of this class register ExternalMediaAdapters. Each adapter registers itself by instantiating - * a static instance of the below class supplying their business name and creator method. - */ - class AdapterRegistration { - public: - /** - * Register an @c ExternalMediaAdapter for use by @c ExternalMediaPlayer. - * - * @param playerId The @c playerId identifying the @c ExternalMediaAdapter to register. - * @param createFunction The function to use to create instances of the specified @c ExternalMediaAdapter. - */ - AdapterRegistration(const std::string& playerId, AdapterCreateFunction createFunction); - }; + /// @name Overridden ExternalMediaPlayerInterface methods. + /// @{ + virtual void setPlayerInFocus(const std::string& playerInFocus) override; + /// @} private: /** @@ -197,25 +177,19 @@ class ExternalMediaPlayer void doShutdown() override; /// @} - /** - * Method to create all the adapters registered. - * - * @param playerId The @c playerId identifying the @c ExternalMediaAdapter to register. - * @param createFunction The function to use to create instances of the specified @c ExternalMediaAdapter. - */ - static bool registerAdapter(const std::string& playerId, AdapterCreateFunction createFunction); - /** * Method to create all the adapters registered. * * @param mediaPlayers The map of to be used to find the mediaPlayer to use for this * adapter. + * @param adapterCreationMap The map of to be used to create the adapters. * @param messageSender The messager sender of the adapter. * @param focusManager The focus manager to be used by the adapter to acquire/release channel. * @param contextManager The context manager of the ExternalMediaPlayer and adapters. */ void createAdapters( const AdapterMediaPlayerMap& mediaPlayers, + const AdapterCreationMap& adapterCreationMap, std::shared_ptr messageSender, std::shared_ptr focusManager, std::shared_ptr contextManager); @@ -373,9 +347,6 @@ class ExternalMediaPlayer avsCommon::avs::NamespaceAndName, std::pair> m_directiveToHandlerMap; - - /// The singleton map from @c playerId to @c ExternalMediaAdapter creation functions. - static std::unordered_map m_adapterToCreateFuncMap; }; } // namespace externalMediaPlayer diff --git a/CapabilityAgents/ExternalMediaPlayer/src/CMakeLists.txt b/CapabilityAgents/ExternalMediaPlayer/src/CMakeLists.txt index 919a62c4c2..a707642c81 100644 --- a/CapabilityAgents/ExternalMediaPlayer/src/CMakeLists.txt +++ b/CapabilityAgents/ExternalMediaPlayer/src/CMakeLists.txt @@ -1,7 +1,10 @@ add_definitions("-DACSDK_LOG_MODULE=ExternalMediaPlayer") set(ExternalMediaPlayer_SOURCES ) list(APPEND ExternalMediaPlayer_SOURCES ExternalMediaPlayer.cpp) -list(APPEND ExternalMediaPlayer_SOURCES AdapterUtils.cpp) + +if(HAS_EXTERNAL_MEDIA_PLAYER_ADAPTERS) + add_subdirectory("ExternalMediaPlayerAdapters") +endif() add_library(ExternalMediaPlayer SHARED ${ExternalMediaPlayer_SOURCES}) @@ -13,5 +16,10 @@ target_include_directories(ExternalMediaPlayer PUBLIC target_link_libraries(ExternalMediaPlayer AVSCommon) +if(HAS_EXTERNAL_MEDIA_PLAYER_ADAPTERS) + target_link_libraries(ExternalMediaPlayer ExternalMediaPlayerAdapters) +endif() + + # install target asdk_install() diff --git a/CapabilityAgents/ExternalMediaPlayer/src/ExternalMediaPlayer.cpp b/CapabilityAgents/ExternalMediaPlayer/src/ExternalMediaPlayer.cpp index d0b8276fc6..f0c6dd77fa 100644 --- a/CapabilityAgents/ExternalMediaPlayer/src/ExternalMediaPlayer.cpp +++ b/CapabilityAgents/ExternalMediaPlayer/src/ExternalMediaPlayer.cpp @@ -15,12 +15,12 @@ /// @file ExternalMediaPlayer.cpp #include "ExternalMediaPlayer/ExternalMediaPlayer.h" -#include "ExternalMediaPlayer/AdapterUtils.h" #include #include #include +#include #include #include #include @@ -30,6 +30,7 @@ namespace capabilityAgents { namespace externalMediaPlayer { using namespace avsCommon::avs; +using namespace avsCommon::avs::externalMediaPlayer; using namespace avsCommon::sdkInterfaces; using namespace avsCommon::sdkInterfaces::externalMediaPlayer; using namespace avsCommon::avs::attachment; @@ -107,10 +108,6 @@ static const int64_t MAX_PAST_OFFSET = -86400000; /// The max relative time in the past that we can seek to in milliseconds(+12 hours in ms). static const int64_t MAX_FUTURE_OFFSET = 86400000; -/// The @c m_adapterToCreateFuncMap Map of the adapter to their create methods. -std::unordered_map - ExternalMediaPlayer::m_adapterToCreateFuncMap; - /// The @c m_directiveToHandlerMap Map of the directives to their handlers. std::unordered_map> ExternalMediaPlayer::m_directiveToHandlerMap = { @@ -161,6 +158,7 @@ static std::unordered_map g_buttonT std::shared_ptr ExternalMediaPlayer::create( const AdapterMediaPlayerMap& mediaPlayers, + const AdapterCreationMap& adapterCreationMap, std::shared_ptr speakerManager, std::shared_ptr messageSender, std::shared_ptr focusManager, @@ -194,7 +192,7 @@ std::shared_ptr ExternalMediaPlayer::create( contextManager->setStateProvider(SESSION_STATE, externalMediaPlayer); contextManager->setStateProvider(PLAYBACK_STATE, externalMediaPlayer); - externalMediaPlayer->createAdapters(mediaPlayers, messageSender, focusManager, contextManager); + externalMediaPlayer->createAdapters(mediaPlayers, adapterCreationMap, messageSender, focusManager, contextManager); return externalMediaPlayer; } @@ -456,13 +454,6 @@ void ExternalMediaPlayer::setPlayerInFocus(const std::string& playerInFocus) { m_playbackRouter->setHandler(shared_from_this()); } -void ExternalMediaPlayer::resetPlayerInFocus(const std::string& playerInFocus) { - ACSDK_DEBUG9(LX("resetPlayerInFocus")); - if (m_playerInFocus == playerInFocus) { - m_playerInFocus.clear(); - } -} - void ExternalMediaPlayer::onButtonPressed(PlaybackButton button) { if (!m_playerInFocus.empty()) { auto adapterIt = m_adapters.find(m_playerInFocus); @@ -502,6 +493,7 @@ void ExternalMediaPlayer::doShutdown() { m_exceptionEncounteredSender.reset(); m_contextManager.reset(); m_playbackRouter.reset(); + m_speakerManager.reset(); } void ExternalMediaPlayer::removeDirective(std::shared_ptr info) { @@ -693,31 +685,14 @@ avsCommon::sdkInterfaces::SpeakerInterface::Type ExternalMediaPlayer::getSpeaker return SpeakerInterface::Type::AVS_SYNCED; } -ExternalMediaPlayer::AdapterRegistration::AdapterRegistration( - const std::string& playerId, - AdapterCreateFunction createFunction) { - if (!ExternalMediaPlayer::registerAdapter(playerId, createFunction)) { - ACSDK_ERROR(LX("AdapterRegistrationFailed").d("playerId", playerId)); - } -} - -bool ExternalMediaPlayer::registerAdapter(const std::string& playerId, AdapterCreateFunction createFunction) { - ACSDK_DEBUG0(LX("registerAdapter").d("playerId", playerId)); - if (m_adapterToCreateFuncMap.find(playerId) != m_adapterToCreateFuncMap.end()) { - ACSDK_ERROR(LX("registerAdapterFailed").d("reason", "playerIdAlreadyRegistered").d("playerId", playerId)); - return false; - } - m_adapterToCreateFuncMap[playerId] = createFunction; - return true; -} - void ExternalMediaPlayer::createAdapters( const AdapterMediaPlayerMap& mediaPlayers, + const AdapterCreationMap& adapterCreationMap, std::shared_ptr messageSender, std::shared_ptr focusManager, std::shared_ptr contextManager) { ACSDK_DEBUG0(LX("createAdapters")); - for (auto& entry : m_adapterToCreateFuncMap) { + for (auto& entry : adapterCreationMap) { auto mediaPlayerIt = mediaPlayers.find(entry.first); if (mediaPlayerIt == mediaPlayers.end()) { diff --git a/CapabilityAgents/GadgetManager/src/CMakeLists.txt b/CapabilityAgents/GadgetManager/src/CMakeLists.txt new file mode 100644 index 0000000000..eacfd4617e --- /dev/null +++ b/CapabilityAgents/GadgetManager/src/CMakeLists.txt @@ -0,0 +1,26 @@ +add_definitions("-DACSDK_LOG_MODULE=gadgetManager") + +add_library(GadgetManager SHARED + BinaryRepresentation.cpp + FinalizeGadgetEvent.cpp + FrameParser.cpp + GadgetEventConstants.cpp + GadgetEventHeader.cpp + GadgetEventProcessor.cpp + GadgetInfoEvent.cpp + GadgetManager.cpp + GadgetProtocol.cpp + GadgetProtocolConstants.cpp) + +target_include_directories(GadgetManager PUBLIC + "${ContextManager_INCLUDE_DIRS}" + "${GadgetManager_SOURCE_DIR}/include") + +if (WIN32) + target_link_libraries(GadgetManager AVSCommon ws2_32) +else() + target_link_libraries(GadgetManager AVSCommon) +endif() + +# install target +asdk_install() diff --git a/CapabilityAgents/GadgetManager/src/GadgetProtocol.cpp b/CapabilityAgents/GadgetManager/src/GadgetProtocol.cpp new file mode 100644 index 0000000000..6283dcdc7e --- /dev/null +++ b/CapabilityAgents/GadgetManager/src/GadgetProtocol.cpp @@ -0,0 +1,333 @@ +/* + * Copyright 2017-2018 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. + */ + +#include "GadgetManager/GadgetProtocol.h" +#ifdef _WIN32 +#include +#else +#include +#endif + +#include + +#include "GadgetManager/GadgetProtocolConstants.h" + +/// String to identify log entries originating from this file. +static const std::string TAG{"GadgetProtocol"}; + +/** + * 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) + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace gadgetManager { + +/// Location of Command ID value in a properly formed GadgetProtocol packet. +static const size_t COMMAND_ID_INDEX = 1; +/// Location of Error ID value in a properly formed GadgetProtocol packet. +static const size_t ERROR_ID_INDEX = 2; +/// Location of Sequence ID value in a properly formed GadgetProtocol packet. +static const size_t SEQUENCE_ID_INDEX = 3; + +/** + * @param byte The character the needs determination if it is a special character within the Gadget Protocol. + * @return true if byte is a special character. + */ +static bool isSpecialCharacter(uint8_t byte) { + return GadgetProtocolConstants::PACKET_BEGIN == byte || GadgetProtocolConstants::PACKET_END == byte || + GadgetProtocolConstants::ESCAPE_BYTE == byte; +} + +/** + * The checksum is the penultimate 16 bits (in big-endian form), unless the checksum contains one of the special + * characters. If that is the case, the special characters are escaped as they would be in the payload. This makes for + * a bit of ugliness in determining the actual bytes of the checksum. Starting at the end of the packet, we search for + * a four-byte checksum, then a three-byte checksum and if neither of those match, it's a two byte checksum. This + * function returns a 2, 3 or 4 byte-vector with the segment that represents the checksum. This function requires a + * properly formatted Gadget Protocol packet. + * + * @param packet a properly formatted Gadget Protocol packet + * @return a vector of the checksum portion of the packet. + */ +static std::vector getChecksumSection(const std::vector& packet) { + // the end of the packet of a 4 byte checksum looks like this: + // begin to (end-6): ... header of packet ... + // end-5 : ESCAPE_BYTE + // end-4 : escaped byte (low-byte) + // end-3 : ESCAPE_BYTE + // end-2 : escaped byte (high-byte) + // end-1 : PACKET_END + const std::vector fourByteChecksum( + std::end(packet) - (4 + sizeof(GadgetProtocolConstants::PACKET_END)), + std::end(packet) - sizeof(GadgetProtocolConstants::PACKET_END)); + + // check the 4 byte checksum. Look for {ESCAPE_BYTE, escaped byte, ESCAPE_BYTE, escaped byte} + if ((fourByteChecksum[0] == GadgetProtocolConstants::ESCAPE_BYTE) && + (isSpecialCharacter(GadgetProtocolConstants::ESCAPE_BYTE ^ fourByteChecksum[1])) && + (fourByteChecksum[2] == GadgetProtocolConstants::ESCAPE_BYTE) && + (isSpecialCharacter(GadgetProtocolConstants::ESCAPE_BYTE ^ fourByteChecksum[3]))) { + return fourByteChecksum; + } + + // the end of the packet of a 3 byte checksum takes on two forms: + // begin to (end-5): ... header of packet ... + // end-4 : unescaped byte (low-byte) + // end-3 : ESCAPE_BYTE + // end-2 : escaped byte (high-byte) + // end-1 : PACKET_END + // OR + // begin to (end-5): ... header of packet ... + // end-4 : ESCAPE_BYTE + // end-3 : escaped byte (low-byte) + // end-2 : unescaped byte (high-byte) + // end-1 : PACKET_END + const std::vector threeByteChecksum( + std::end(packet) - (3 + sizeof(GadgetProtocolConstants::PACKET_END)), + std::end(packet) - sizeof(GadgetProtocolConstants::PACKET_END)); + // check the 3 byte checksum. Look for {ESCAPE_BYTE, escaped byte, unescaped byte} or {unescaped byte, ESCAPE_BYTE, + // escaped byte} + if (((threeByteChecksum[0] == GadgetProtocolConstants::ESCAPE_BYTE) && + (isSpecialCharacter(GadgetProtocolConstants::ESCAPE_BYTE ^ threeByteChecksum[1])) && + (!isSpecialCharacter(threeByteChecksum[2]))) || + ((!isSpecialCharacter(threeByteChecksum[0])) && + (threeByteChecksum[1] == GadgetProtocolConstants::ESCAPE_BYTE) && + (isSpecialCharacter(GadgetProtocolConstants::ESCAPE_BYTE ^ threeByteChecksum[2])))) { + return threeByteChecksum; + } + + // It must be the two byte checksum + // begin to (end-4): ... header of packet ... + // end-3 : unescaped byte (low-byte) + // end-2 : unescaped byte (high-byte) + // end-1 : PACKET_END + return std::vector( + std::end(packet) - (2 + sizeof(GadgetProtocolConstants::PACKET_END)), + std::end(packet) - sizeof(GadgetProtocolConstants::PACKET_END)); +} + +/** + * This returns the 16-bit checksum of what is stored in the packet (not what is calculated). If necessary, it is + * byte-swapped, as it is in big-endian form in the packet. + * + * @param packet a properly formatted Gadget Protocol packet + * @return the checksum of the packet + */ +static uint16_t readChecksum(const std::vector& packet) { + auto checksumSection = getChecksumSection(packet); + + union { + uint8_t bytes[2]; + uint16_t value; + } bigEndianChecksum; + + switch (checksumSection.size()) { + case 4: + // 4 byte checksum {ESCAPE_BYTE, escaped byte, ESCAPE_BYTE, escaped byte}; + bigEndianChecksum.bytes[0] = (GadgetProtocolConstants::ESCAPE_BYTE ^ checksumSection[1]); + bigEndianChecksum.bytes[1] = (GadgetProtocolConstants::ESCAPE_BYTE ^ checksumSection[3]); + break; + case 3: + // 3 byte checksum + if (checksumSection[0] == GadgetProtocolConstants::ESCAPE_BYTE) { + // this is the case {ESCAPE_BYTE, escaped byte, normal byte}; + bigEndianChecksum.bytes[0] = (GadgetProtocolConstants::ESCAPE_BYTE ^ checksumSection[1]); + bigEndianChecksum.bytes[1] = checksumSection[2]; + } else { + // this is the case {normal byte, ESCAPE_BYTE, escaped byte}; + bigEndianChecksum.bytes[0] = checksumSection[0]; + bigEndianChecksum.bytes[1] = (GadgetProtocolConstants::ESCAPE_BYTE ^ checksumSection[2]); + } + break; + default: + // two byte checksum + bigEndianChecksum.value = *(reinterpret_cast(checksumSection.data())); + break; + } + return ntohs(bigEndianChecksum.value); +} + +/** + * Cut off the head and footer of a Gadget Protocol packet. + * + * @param packet a properly formatted Gadget Protocol packet + * @return the still-escaped payload of the Gadget Protocol packet. + */ +static std::vector getPayloadSection(const std::vector& packet) { + // the header is 4 bytes (start of frame, Command ID, Error ID, Sequence ID). + const int numberOfHeaderBytes = 4; + + // see how many characters trail the payload. + const int numberOfTrailingBytes = getChecksumSection(packet).size() + sizeof(GadgetProtocolConstants::PACKET_END); + + return std::vector( + packet.begin() + numberOfHeaderBytes, packet.begin() + (packet.size() - numberOfTrailingBytes)); +} + +bool GadgetProtocol::decode(const std::vector& packet, std::vector* unpackedPayload) { + if (!unpackedPayload) { + ACSDK_ERROR(LX(__func__).m("null unpackedPayload")); + return false; + } + + /// The smallest packet possible, for use in determining if a packet to decode is large enough. + static const auto minimumSizePacket = encode(std::vector(), 0).second; + + if (packet.size() < minimumSizePacket.size()) { + ACSDK_ERROR(LX(__func__).d("packet.size()", packet.size()).m("packet is too short")); + return false; + } + + // The following checks are safe because we already determined that packet is big enough. + if (GadgetProtocolConstants::PACKET_BEGIN != packet.front()) { + ACSDK_ERROR(LX(__func__).d("invalid StartOfFrame", static_cast(packet.front()))); + return false; + } + + const uint8_t commandId = packet[COMMAND_ID_INDEX]; + if (GadgetProtocolConstants::DEFAULT_COMMAND_ID != commandId) { + ACSDK_ERROR(LX(__func__).d("invalid CommandId", static_cast(commandId))); + return false; + } + + const uint8_t errorId = packet[ERROR_ID_INDEX]; + if (GadgetProtocolConstants::DEFAULT_ERROR_ID != errorId) { + ACSDK_ERROR(LX(__func__).d("invalid ErrorId", static_cast(errorId))); + return false; + } + + if (isSpecialCharacter(packet[SEQUENCE_ID_INDEX])) { + ACSDK_ERROR(LX(__func__).d("invalid SequenceId", static_cast(packet[SEQUENCE_ID_INDEX]))); + return false; + } + + if (GadgetProtocolConstants::PACKET_END != packet.back()) { + ACSDK_ERROR(LX(__func__).d("invalid EndOfFrame", static_cast(packet.back()))); + return false; + } + + auto packedPayload = getPayloadSection(packet); + + uint16_t checksum = (commandId + errorId); + + // the unpacked payload is at most the same size as the packedPayload. + unpackedPayload->reserve(packedPayload.size()); + unpackedPayload->clear(); + + bool escaping = false; + for (const auto& byte : packedPayload) { + if (escaping) { + const uint8_t unescaped = GadgetProtocolConstants::ESCAPE_BYTE ^ byte; + checksum += (unescaped); + unpackedPayload->push_back(unescaped); + escaping = false; + } else { + if (isSpecialCharacter(byte)) { + escaping = true; + } else { + checksum += byte; + unpackedPayload->push_back(byte); + } + } + } + + if (checksum != readChecksum(packet)) { + ACSDK_ERROR(LX(__func__).d("received", readChecksum(packet)).d("calculated", checksum).m("invalid checksum")); + return false; + } + + return true; +} + +/** + * Properly escape (if required) and append a byte onto a vector + * + * @param byte the byte to append + * @param v the vector to append to + */ +static void append(uint8_t byte, std::vector* v) { + if (isSpecialCharacter(byte)) { + v->push_back(GadgetProtocolConstants::ESCAPE_BYTE); + v->push_back(GadgetProtocolConstants::ESCAPE_BYTE ^ byte); + } else { + v->push_back(byte); + } +} + +/** + * Function to simplify the handling of the sequenceId. The sequenceId is incremented, rolling over from 0xFF->0x00. + * Special characters are skipped. + * + * @param sequenceId the previous sequence ID + * @return the next sequence ID + */ +static uint8_t getNextSequenceId(uint8_t sequenceId) { + ++sequenceId; + + // step over the special characters, as those are not valid sequence IDs + while (isSpecialCharacter(sequenceId)) { + ++sequenceId; + } + + return sequenceId; +} + +std::pair> GadgetProtocol::encode( + const std::vector& command, + uint8_t prevSequenceId) { + const uint8_t sequenceId = getNextSequenceId(prevSequenceId); + const uint8_t commandId = GadgetProtocolConstants::DEFAULT_COMMAND_ID; + const uint8_t errorId = GadgetProtocolConstants::DEFAULT_ERROR_ID; + + // the checksum is commandId + errorId + each (unescaped) byte in the payload. It is calculated later in the + // function, but is included here for the packet reservation. + uint16_t checksum = commandId + errorId; + + std::vector packet; + // reserve the maximum size that could be needed. The 2 * command.size() and @ * sizeof(checksum) is because each + // byte might need to be escaped. + packet.reserve( + sizeof(GadgetProtocolConstants::PACKET_BEGIN) + sizeof(commandId) + sizeof(errorId) + sizeof(sequenceId) + + 2 * command.size() + 2 * sizeof(checksum) + sizeof(GadgetProtocolConstants::PACKET_END)); + + // HEADER + packet.push_back(GadgetProtocolConstants::PACKET_BEGIN); + // The spec says that these are specific values; it also says that they should be escaped if they are special + // characters. Play it safe and use a common insertion interface. + append(commandId, &packet); + append(errorId, &packet); + append(sequenceId, &packet); + + // PAYLOAD + for (const auto& byte : command) { + checksum += byte; + append(byte, &packet); + } + + // The packet is big-endian, so insert the higher order byte first. + append((checksum >> 8) & 0xFF, &packet); + append(checksum & 0xFF, &packet); + + packet.push_back(GadgetProtocolConstants::PACKET_END); + + return std::make_pair(sequenceId, packet); +} + +} // namespace gadgetManager +} // namespace capabilityAgents +} // namespace alexaClientSDK diff --git a/CapabilityAgents/GadgetManager/test/GadgetProtocolTest.cpp b/CapabilityAgents/GadgetManager/test/GadgetProtocolTest.cpp new file mode 100644 index 0000000000..9affc128eb --- /dev/null +++ b/CapabilityAgents/GadgetManager/test/GadgetProtocolTest.cpp @@ -0,0 +1,343 @@ +/* + * Copyright 2017-2018 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. + */ + +#include + +#include +#include + +#ifdef _WIN32 +#include +#else +#include +#endif + +#include "GadgetManager/GadgetProtocol.h" +#include "GadgetManager/GadgetProtocolConstants.h" +#include "GenerateRandomVector.h" + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace gadgetManager { +namespace test { + +/// Utility function to determine if the packet is properly formed +static bool isValid(const std::vector& packet) { + std::vector dummyPayload; + return GadgetProtocol::decode(packet, &dummyPayload); +} + +/// Verify proper error handling on bad parameters. +TEST(GadgetProtocolTest, DecodeNullOutputParameter) { + const auto valid = GadgetProtocol::encode(std::vector(), 0).second; + ASSERT_TRUE(isValid(valid)); + ASSERT_FALSE(GadgetProtocol::decode(valid, nullptr)); +} + +/// Check that packets that are too short are rejected. +TEST(GadgetProtocolTest, InvalidLength) { + const auto smallestValid = GadgetProtocol::encode(std::vector(), 0).second; + ASSERT_TRUE(isValid(smallestValid)); + + // all lengths under the valid packet are illegal + for (size_t length = 0; length < smallestValid.size(); ++length) { + ASSERT_FALSE(isValid(std::vector(std::begin(smallestValid), std::begin(smallestValid) + length))); + } +} + +/// utility function that creates a packet, modifies the byte at the specificed index and checks to see if the value +/// it's being changed to is in the set of possible correct bytes. The packet should be invalid if the byte is changes +/// to something outside of the correct bytes. +static bool modifyThenCheckForValidity(const std::unordered_set& possibleCorrectBytes, size_t indexToChange) { + auto packet = GadgetProtocol::encode(std::vector(), 0).second; + for (int i = 0; i < 0x100; ++i) { + const uint8_t byteToChangeTo = i & 0xFF; + if (indexToChange > packet.size()) { + return false; + } + packet[indexToChange] = byteToChangeTo; + + if ((possibleCorrectBytes.find(byteToChangeTo) != possibleCorrectBytes.end()) != isValid(packet)) { + // isValid should return true when byteToChangeTo is in possibleCorrectBytes + return false; + } + } + + return true; +} + +/// Test to change the 1st byte. +TEST(GadgetProtocolTest, InvalidStartOfFrame) { + // the SOF is at index 0 + ASSERT_TRUE(modifyThenCheckForValidity(std::unordered_set{GadgetProtocolConstants::PACKET_BEGIN}, 0)); +} + +/// Test to change the 2nd byte. +TEST(GadgetProtocolTest, InvalidCommandId) { + // the commandID is at index 1 + ASSERT_TRUE( + modifyThenCheckForValidity(std::unordered_set{GadgetProtocolConstants::DEFAULT_COMMAND_ID}, 1)); +} + +/// Test to change the 3rd byte. +TEST(GadgetProtocolTest, InvalidErrorId) { + // the errorID is at index 2 + ASSERT_TRUE(modifyThenCheckForValidity(std::unordered_set{GadgetProtocolConstants::DEFAULT_ERROR_ID}, 2)); +} + +/// Test to change the 4th byte. +TEST(GadgetProtocolTest, InvalidSequenceId) { + // create a set of all legitimate sequenceId values (i.e. everything except SOF, EOF and the ESCAPE_BYTE byte) + std::unordered_set goodSequenceIdValues; + for (int i = 0; i < 0x100; ++i) { + if ((GadgetProtocolConstants::PACKET_BEGIN != i) && (GadgetProtocolConstants::PACKET_END != i) && + (GadgetProtocolConstants::ESCAPE_BYTE != i)) { + goodSequenceIdValues.insert(i); + } + } + + // the sequenceID is at index 3 + ASSERT_TRUE(modifyThenCheckForValidity(goodSequenceIdValues, 3)); +} + +/// Utility function to determine if a byte is a special character. +static bool isSpecial(uint8_t byte) { + return GadgetProtocolConstants::PACKET_BEGIN == byte || GadgetProtocolConstants::PACKET_END == byte || + GadgetProtocolConstants::ESCAPE_BYTE == byte; +} + +/// Counts the number of escaped bytes in a checksum +static size_t numEscapedBytes(uint16_t checksum) { + uint8_t* bytePtr = reinterpret_cast(&checksum); + return isSpecial(*bytePtr) + isSpecial(*(bytePtr + 1)); +} + +/// Given a 16 bit checksum, create a vector that escapes the bytes properly. The checksum vector is big-endian. +static std::vector createEscapedChecksum(uint16_t checksum) { + std::vector checksumVector; + for (unsigned int i = sizeof(checksum); i > 0; --i) { + const uint8_t workingByte = (checksum >> (8 * (i - 1))) & 0xFF; + if (isSpecial(workingByte)) { + checksumVector.push_back(GadgetProtocolConstants::ESCAPE_BYTE); + checksumVector.push_back(workingByte ^ GadgetProtocolConstants::ESCAPE_BYTE); + } else { + checksumVector.push_back(workingByte); + } + } + + return checksumVector; +} + +/// Test for cases where the checksum has no escape bytes. It checks to see that if the checksum is incorrect it fails +/// to decode and if it's correct, it decodes correctly. +TEST(GadgetProtocolTest, ChecksumNoEscapeBytes) { + // zero length payload results in checksum that is CommandID + ErrorID + const uint16_t expectedChecksum = + GadgetProtocolConstants::DEFAULT_COMMAND_ID + GadgetProtocolConstants::DEFAULT_ERROR_ID; + for (int i = 0; i < 0x10000; ++i) { + const size_t CHECKSUM_INDEX = 4; + const uint16_t checksum = i & 0xFFFF; + if (0 == numEscapedBytes(checksum)) { + auto packet = GadgetProtocol::encode(std::vector(), 0).second; + ASSERT_GE( + packet.size(), + CHECKSUM_INDEX + sizeof(checksum)); // verfiy the packet is long enough for the next statement + + // overwrite the checksum with 'checksum' + uint16_t* checksumPtr = reinterpret_cast(packet.data() + CHECKSUM_INDEX); + *checksumPtr = htons(checksum); + + // it should be invalid + ASSERT_EQ(checksum == expectedChecksum, isValid(packet)); + } + } +} + +/// This function is for testing checksums that have at least one escaped byte. A packet is created and all of the 3 or +/// 4 byte checksums are substituted into the Gadget Protocol packet (depending on whether the expected checksum needs 1 +/// or 2 escaped bytes). The test checks to see wheter or not the packet is valid with the substituted checksum. If +/// the substituted checksum is the same as the expected checksum, the packet should be valid, otherwise it is invalid. +static void checkEscapedByteChecksum(uint16_t expectedChecksum) { + // First step, generate a payload so the expected checksum is correct. + uint16_t currentChecksum = + (GadgetProtocolConstants::DEFAULT_COMMAND_ID + GadgetProtocolConstants::DEFAULT_ERROR_ID); + std::vector payload; + // keep the step below 0xF0 so we don't get any escape bytes in the payload. + const uint8_t step = 0xEE; + while (currentChecksum + step < expectedChecksum) { + payload.push_back(step); + currentChecksum += step; + } + + const int finalPayloadByteToGenerateExpectedChecksum = expectedChecksum - currentChecksum; + ASSERT_LE(finalPayloadByteToGenerateExpectedChecksum, step); + payload.push_back(finalPayloadByteToGenerateExpectedChecksum); + + // Exhaustively search the checksum space for all checksums that are the same length as the expected checksum. This + // is so they can be substituted into the Gadget Protocol packet without any packet size modification. + for (int i = 0; i < 0x10000; ++i) { + const uint16_t checksum = i & 0xFFFF; + + const size_t expectedChecksumVectorSize = 2 + numEscapedBytes(checksum); + if (expectedChecksumVectorSize == (numEscapedBytes(expectedChecksum) + 2)) { + auto packet = GadgetProtocol::encode(payload, 0).second; + const size_t CHECKSUM_INDEX = 4 + payload.size(); + + auto checksumVector = createEscapedChecksum(checksum); + ASSERT_EQ(expectedChecksumVectorSize, checksumVector.size()); + + memcpy(packet.data() + CHECKSUM_INDEX, checksumVector.data(), checksumVector.size()); + + ASSERT_EQ(checksum == expectedChecksum, isValid(packet)); + } + } +} + +/// Test where the checksum has a leading escaped byte (e.g. 0xF200) +TEST(GadgetProtocolTest, ChecksumLeadingEscapeByte) { + for (const auto& leadingEscapeByte : std::set{GadgetProtocolConstants::PACKET_BEGIN, + GadgetProtocolConstants::PACKET_END, + GadgetProtocolConstants::ESCAPE_BYTE}) { + checkEscapedByteChecksum(leadingEscapeByte << 8); + } +} + +/// Test where the checksum has a trailing escaped byte (e.g. 0x00F1) +TEST(GadgetProtocolTest, ChecksumTrailingEscapeByte) { + for (const auto& leadingEscapeByte : std::set{GadgetProtocolConstants::PACKET_BEGIN, + GadgetProtocolConstants::PACKET_END, + GadgetProtocolConstants::ESCAPE_BYTE}) { + checkEscapedByteChecksum(leadingEscapeByte); + } +} + +/// Test where the checksum has both a leading and trailing escaped byte (e.g. 0xF1F1) +TEST(GadgetProtocolTest, ChecksumLeadingAndTrailingEscapeByte) { + std::set escapeBytes = {GadgetProtocolConstants::PACKET_BEGIN, + GadgetProtocolConstants::PACKET_END, + GadgetProtocolConstants::ESCAPE_BYTE}; + for (const auto& leadingByte : escapeBytes) { + for (const auto& trailingByte : escapeBytes) { + const uint16_t expectedChecksum = (leadingByte << 8 | trailingByte); + checkEscapedByteChecksum(expectedChecksum); + } + } +} + +/// Test where the end of frame byte is replaced and packet validity is checked. +TEST(GadgetProtocolTest, InvalidEndOfFrame) { + for (int i = 0; i < 0x100; ++i) { + const uint8_t endOfFrame = i & 0xFF; + auto packet = GadgetProtocol::encode(std::vector(), 0).second; + ASSERT_GE(packet.size(), 1u); // verfiy the packet is long enough for the next statement + packet[packet.size() - 1] = endOfFrame; + + // all values for the end byte except PACKET_END shall fail + ASSERT_EQ(endOfFrame == GadgetProtocolConstants::PACKET_END, isValid(packet)); + } +} + +/// Insert special characters into the payload to verify that they are escaped correctly. +TEST(GadgetProtocolTest, EscapeBytesInPayload) { + for (const auto& escapeByte : std::set{GadgetProtocolConstants::PACKET_BEGIN, + GadgetProtocolConstants::PACKET_END, + GadgetProtocolConstants::ESCAPE_BYTE}) { + std::vector payload{escapeByte}; + auto packet = GadgetProtocol::encode(payload, 0).second; + ASSERT_GE(packet.size(), 6u); + ASSERT_EQ(GadgetProtocolConstants::ESCAPE_BYTE, packet[4]); + ASSERT_EQ(GadgetProtocolConstants::ESCAPE_BYTE ^ escapeByte, packet[5]); + } +} + +/// Insert normal bytes into the payload to verify that they are never escapedescaped correctly. +TEST(GadgetProtocolTest, NonEscapeBytesInPayload) { + for (const auto& normalByte : std::set{0, 1, 2, 3, 4, 5, 6, 0xF3}) { + std::vector payload{normalByte}; + auto packet = GadgetProtocol::encode(payload, 0).second; + ASSERT_GE(packet.size(), 5u); + ASSERT_EQ(normalByte, packet[4]); + } +} + +// Test normal sequenceId operation +TEST(GadgetProtocolTest, UnescapedSequenceId) { + for (int i = 0; i < 0x100; ++i) { + uint8_t prevSequenceId = (0xFF & i); + if (!isSpecial(prevSequenceId + 1)) { + auto encodeResult = GadgetProtocol::encode(std::vector(), prevSequenceId); + ASSERT_EQ((prevSequenceId + 1) & 0xFF, encodeResult.first); + ASSERT_EQ(encodeResult.second[3], encodeResult.first); + } + } +} + +/// Test skipping of special byte SequenceId's. +TEST(GadgetProtocolTest, EscapedSequenceId) { + for (const auto& escapeByte : std::set{GadgetProtocolConstants::PACKET_BEGIN, + GadgetProtocolConstants::PACKET_END, + GadgetProtocolConstants::ESCAPE_BYTE}) { + auto encodeResult = GadgetProtocol::encode(std::vector(), escapeByte); + ASSERT_EQ(0xF3, encodeResult.first); + ASSERT_EQ(encodeResult.second[3], encodeResult.first); + } +} + +/// Test the SequenceId wrapping around from 0xFF to 0x00. +TEST(GadgetProtocolTest, WrappedSequenceId) { + auto encodeResult = GadgetProtocol::encode(std::vector(), 0xFF); + ASSERT_EQ(0x00, encodeResult.first); + ASSERT_EQ(encodeResult.second[3], encodeResult.first); +} + +static void encodeAndDecode(const std::vector originalPayload) { + const auto packet = GadgetProtocol::encode(originalPayload, 0).second; + std::vector unpackedPayload; + ASSERT_TRUE(GadgetProtocol::decode(packet, &unpackedPayload)); + ASSERT_EQ(originalPayload, unpackedPayload); +} + +/// Test the encoding and decoding of some representative payloads. +TEST(GadgetProtocolTest, InterestingPayloads) { + const std::vector> payloads = {{}, + {0}, + {0xF0}, + {0xF1}, + {0xF2}, + {0xF3}, + {0x00, 0xF0}, + {0xF0, 0x00}, + {0x01, 0xF0, 0x02}, + {0xF0, 0xF1}, + {0xF0, 0xF1, 0xF2}, + {0xF2, 0x11, 0xF1}}; + + for (const auto& payload : payloads) { + encodeAndDecode(payload); + } +} + +/// Generate random vectors and see if an encode and decode results in the same original payload. +TEST(GadgetProtocolTest, FuzzTest) { + for (int i = 0; i < 1024; ++i) { + auto payload = generateRandomVector(251); + encodeAndDecode(payload); + } +} + +} // namespace test +} // namespace gadgetManager +} // namespace capabilityAgents +} // namespace alexaClientSDK diff --git a/CapabilityAgents/GadgetManager/test/GenerateRandomVector.h b/CapabilityAgents/GadgetManager/test/GenerateRandomVector.h new file mode 100644 index 0000000000..bada2b7704 --- /dev/null +++ b/CapabilityAgents/GadgetManager/test/GenerateRandomVector.h @@ -0,0 +1,52 @@ +/* + * Copyright 2017-2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_GADGETMANAGER_TEST_GENERATERANDOMVECTOR_H_ +#define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_GADGETMANAGER_TEST_GENERATERANDOMVECTOR_H_ + +#include +#include +#include +#include +#include + +namespace alexaClientSDK { +namespace capabilityAgents { +namespace gadgetManager { +namespace test { + +/** + * Utility function that is used in multiple tests for generating random vectors of bytes + */ +std::vector generateRandomVector(size_t length) { + // First create an instance of an engine. + std::random_device rnd_device; + // Specify the engine and distribution. + std::mt19937 mersenne_engine(rnd_device()); + std::uniform_int_distribution dist(0, 0xFF); + + auto gen = std::bind(dist, mersenne_engine); + + std::vector vec(length); + std::generate(std::begin(vec), std::end(vec), gen); + return vec; +} + +} // namespace test +} // namespace gadgetManager +} // namespace capabilityAgents +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_CAPABILITYAGENTS_GADGETMANAGER_TEST_GENERATERANDOMVECTOR_H_ diff --git a/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgent.h b/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgent.h index f7ab9a4134..4f04a92b5c 100644 --- a/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgent.h +++ b/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgent.h @@ -25,6 +25,7 @@ #include #include #include +#include #include "NotificationIndicator.h" #include "NotificationRendererInterface.h" @@ -49,6 +50,7 @@ class NotificationsCapabilityAgent : public NotificationRendererObserverInterface , public avsCommon::avs::CapabilityAgent , public avsCommon::utils::RequiresShutdown + , public registrationManager::CustomerDataHandler , public std::enable_shared_from_this { public: /** @@ -61,6 +63,7 @@ class NotificationsCapabilityAgent * @param exceptionSender The object to use for sending AVS Exception messages. * @param notificationsAudioFactory The audio factory object to produce the default notification sound. * @param observers The set of observers that will be notified of IndicatorState changes. + * @param dataManager A dataManager object that will track the CustomerDataHandler. * @return A @c std::shared_ptr to the new @c NotificationsCapabilityAgent instance. */ static std::shared_ptr create( @@ -68,7 +71,8 @@ class NotificationsCapabilityAgent std::shared_ptr renderer, std::shared_ptr contextManager, std::shared_ptr exceptionSender, - std::shared_ptr notificationsAudioFactory); + std::shared_ptr notificationsAudioFactory, + std::shared_ptr dataManager); /** * Adds a NotificationsObserver to the set of observers. This observer will be notified when a SetIndicator @@ -108,6 +112,11 @@ class NotificationsCapabilityAgent void onNotificationRenderingFinished() override; /// @} + /** + * Clear all notifications saved in the device + */ + void clearData() override; + private: /** * Constructor. @@ -119,13 +128,15 @@ class NotificationsCapabilityAgent * @param exceptionSender The object to use for sending AVS Exception messages. * @param notificationsAudioFactory The audio factory object to produce the default notification sound. * @param observers The set of observers that will be notified of IndicatorState changes. + * @param dataManager A dataManager object that will track the CustomerDataHandler. */ NotificationsCapabilityAgent( std::shared_ptr notificationsStorage, std::shared_ptr renderer, std::shared_ptr contextManager, std::shared_ptr exceptionSender, - std::shared_ptr notificationsAudioFactory); + std::shared_ptr notificationsAudioFactory, + std::shared_ptr dataManager); /** * Utility to set some member variables and setup the database. diff --git a/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgentState.h b/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgentState.h index 64eb316ef5..062c5ad3c9 100644 --- a/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgentState.h +++ b/CapabilityAgents/Notifications/include/Notifications/NotificationsCapabilityAgentState.h @@ -20,6 +20,9 @@ namespace alexaClientSDK { namespace capabilityAgents { namespace notifications { +#include +#include + enum class NotificationsCapabilityAgentState { // The capability agent is awaiting directives. IDLE, diff --git a/CapabilityAgents/Notifications/include/Notifications/NotificationsStorageInterface.h b/CapabilityAgents/Notifications/include/Notifications/NotificationsStorageInterface.h index 8fa6f10ccb..11f9fd2a41 100644 --- a/CapabilityAgents/Notifications/include/Notifications/NotificationsStorageInterface.h +++ b/CapabilityAgents/Notifications/include/Notifications/NotificationsStorageInterface.h @@ -39,34 +39,23 @@ class NotificationsStorageInterface { virtual ~NotificationsStorageInterface() = default; /** - * Creates a new database with the given filepath. - * If the file specified already exists, or if a database is already being handled by this object, then - * this function returns false. + * Creates a new database. + * If a database is already being handled by this object or there is another internal error, then this function + * returns false. * - * If the necessary tables cannot be created, this will return false. - * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is created ok, or @c false if either the file exists or a database is already - * being handled by this object. - */ - virtual bool createDatabase(const std::string& filePath) = 0; - - /** - * Open a database with the given filepath. If this object is already managing an open database, or the file - * does not exist, or there is a problem opening the database, this function returns false. - * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is opened ok, @c false if either the file does not exist, if this object is - * already managing an open database, or if there is another internal reason the database could not be opened. + * @return @c true If the database is created ok, or @c false if a database is already being handled by this object + * or there is a problem creating the database. */ - virtual bool open(const std::string& filePath) = 0; + virtual bool createDatabase() = 0; /** - * Query if this object is currently managing an open database. + * Open an existing database. If this object is already managing an open database, or there is a problem opening + * the database, this function returns false. * - * @return @c true If a database is being currently managed by this object, @c false otherwise. + * @return @c true If the database is opened ok, @c false if this object is already managing an open database, or if + * there is another internal reason the database could not be opened. */ - virtual bool isOpen() const = 0; + virtual bool open() = 0; /** * Close the currently open database, if one is open. @@ -110,7 +99,7 @@ class NotificationsStorageInterface { * @return Whether the get operation was successful. * @note The default IndicatorState for a new database is IndicatorState::OFF. */ - virtual bool getIndicatorState(IndicatorState* state) const = 0; + virtual bool getIndicatorState(IndicatorState* state) = 0; /** * Checks if there are any NotificationIndicator records in the database. @@ -118,7 +107,7 @@ class NotificationsStorageInterface { * @param [out] empty Whether there were any records in the database. * @return Whether the operation was successful. */ - virtual bool checkForEmptyQueue(bool* empty) const = 0; + virtual bool checkForEmptyQueue(bool* empty) = 0; /** * Clears the database of all @c NotificationIndicators. @@ -133,7 +122,7 @@ class NotificationsStorageInterface { * @param [out] size A pointer to receive the calculated size. * @return Whether the size operation was successful. */ - virtual bool getQueueSize(int* size) const = 0; + virtual bool getQueueSize(int* size) = 0; }; } // namespace notifications diff --git a/CapabilityAgents/Notifications/include/Notifications/SQLiteNotificationsStorage.h b/CapabilityAgents/Notifications/include/Notifications/SQLiteNotificationsStorage.h index 7f331a8d9a..75d91ca07d 100644 --- a/CapabilityAgents/Notifications/include/Notifications/SQLiteNotificationsStorage.h +++ b/CapabilityAgents/Notifications/include/Notifications/SQLiteNotificationsStorage.h @@ -16,10 +16,12 @@ #ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_NOTIFICATIONS_INCLUDE_NOTIFICATIONS_SQLITENOTIFICATIONSSTORAGE_H_ #define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_NOTIFICATIONS_INCLUDE_NOTIFICATIONS_SQLITENOTIFICATIONSSTORAGE_H_ +#include "Notifications/NotificationsStorageInterface.h" + #include -#include -#include "Notifications/NotificationsStorageInterface.h" +#include +#include namespace alexaClientSDK { namespace capabilityAgents { @@ -31,19 +33,27 @@ namespace notifications { */ class SQLiteNotificationsStorage : public NotificationsStorageInterface { public: + /** + * Factory method for creating a storage object for Notifications based on an SQLite database. + * + * @param configurationRoot The global config object. + * @return Pointer to the SQLiteMessagetStorge object, nullptr if there's an error creating it. + */ + static std::unique_ptr create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot); + /** * Constructor. - * Initializes m_dbHandle to nullptr. + * + * @param dbFilePath The location of the SQLite database file. */ - SQLiteNotificationsStorage(); + SQLiteNotificationsStorage(const std::string& databaseFilePath); ~SQLiteNotificationsStorage(); - bool createDatabase(const std::string& filePath) override; - - bool open(const std::string& filePath) override; + bool createDatabase() override; - bool isOpen() const override; + bool open() override; void close() override; @@ -55,13 +65,13 @@ class SQLiteNotificationsStorage : public NotificationsStorageInterface { bool setIndicatorState(IndicatorState state) override; - bool getIndicatorState(IndicatorState* state) const override; + bool getIndicatorState(IndicatorState* state) override; - bool checkForEmptyQueue(bool* empty) const override; + bool checkForEmptyQueue(bool* empty) override; bool clearNotificationIndicators() override; - bool getQueueSize(int* size) const override; + bool getQueueSize(int* size) override; private: /** @@ -71,11 +81,11 @@ class SQLiteNotificationsStorage : public NotificationsStorageInterface { */ bool getNextNotificationIndicatorLocked(NotificationIndicator* notificationIndicator); - /// The sqlite database handle. - sqlite3* m_dbHandle; - /// A mutex to protect database access. std::mutex m_databaseMutex; + + /// The underlying database class. + alexaClientSDK::storage::sqliteStorage::SQLiteDatabase m_database; }; } // namespace notifications diff --git a/CapabilityAgents/Notifications/src/CMakeLists.txt b/CapabilityAgents/Notifications/src/CMakeLists.txt index a6faa4f596..4f1f7d10d1 100644 --- a/CapabilityAgents/Notifications/src/CMakeLists.txt +++ b/CapabilityAgents/Notifications/src/CMakeLists.txt @@ -9,9 +9,10 @@ add_library(Notifications SHARED target_include_directories(Notifications PUBLIC "${AudioResources_SOURCE_DIR}/include" "${Notifications_SOURCE_DIR}/include" - "${SQLiteStorage_SOURCE_DIR}/include") + "${SQLiteStorage_SOURCE_DIR}/include" + "${RegistrationManager_SOURCE_DIR}/include") -target_link_libraries(Notifications AudioResources AVSCommon SQLiteStorage) +target_link_libraries(Notifications AudioResources AVSCommon SQLiteStorage RegistrationManager) # install target asdk_install() \ No newline at end of file diff --git a/CapabilityAgents/Notifications/src/NotificationsCapabilityAgent.cpp b/CapabilityAgents/Notifications/src/NotificationsCapabilityAgent.cpp index a5607ec2fa..5cd1fcdb1e 100644 --- a/CapabilityAgents/Notifications/src/NotificationsCapabilityAgent.cpp +++ b/CapabilityAgents/Notifications/src/NotificationsCapabilityAgent.cpp @@ -42,12 +42,6 @@ static const std::string TAG("NotificationsCapabilityAgent"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) -/// The key in our config file to find the root of settings. -static const std::string NOTIFICATIONS_CONFIGURATION_ROOT_KEY = "notifications"; - -/// The key in our config file to find the database file path. -static const std::string NOTIFICATIONS_DB_FILE_PATH_KEY = "databaseFilePath"; - /// The namespace for this capability agent. static const std::string NAMESPACE = "Notifications"; @@ -80,7 +74,8 @@ std::shared_ptr NotificationsCapabilityAgent::crea std::shared_ptr renderer, std::shared_ptr contextManager, std::shared_ptr exceptionSender, - std::shared_ptr notificationsAudioFactory) { + std::shared_ptr notificationsAudioFactory, + std::shared_ptr dataManager) { if (nullptr == notificationsStorage) { ACSDK_ERROR(LX("createFailed").d("reason", "nullNotificationsStorage")); return nullptr; @@ -101,9 +96,13 @@ std::shared_ptr NotificationsCapabilityAgent::crea ACSDK_ERROR(LX("createFailed").d("reason", "nullNotificationsAudioFactory")); return nullptr; } + if (nullptr == dataManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullDataManager")); + return nullptr; + } auto notificationsCapabilityAgent = std::shared_ptr(new NotificationsCapabilityAgent( - notificationsStorage, renderer, contextManager, exceptionSender, notificationsAudioFactory)); + notificationsStorage, renderer, contextManager, exceptionSender, notificationsAudioFactory, dataManager)); if (!notificationsCapabilityAgent->init()) { ACSDK_ERROR(LX("createFailed").d("reason", "initFailed")); @@ -117,9 +116,11 @@ NotificationsCapabilityAgent::NotificationsCapabilityAgent( std::shared_ptr renderer, std::shared_ptr contextManager, std::shared_ptr exceptionSender, - std::shared_ptr notificationsAudioFactory) : + std::shared_ptr notificationsAudioFactory, + std::shared_ptr dataManager) : CapabilityAgent{NAMESPACE, exceptionSender}, RequiresShutdown{"NotificationsCapabilityAgent"}, + CustomerDataHandler{dataManager}, m_notificationsStorage{notificationsStorage}, m_contextManager{contextManager}, m_renderer{renderer}, @@ -134,22 +135,9 @@ bool NotificationsCapabilityAgent::init() { m_renderer->addObserver(shared_from_this()); m_contextManager->setStateProvider(INDICATOR_STATE_CONTEXT_KEY, shared_from_this()); - // initialize database - auto configurationRoot = ConfigurationNode::getRoot()[NOTIFICATIONS_CONFIGURATION_ROOT_KEY]; - if (!configurationRoot) { - ACSDK_ERROR(LX("initFailed").d("reason", "NotificationsConfigurationRootNotFound.")); - return false; - } - - std::string databaseFilePath; - if (!configurationRoot.getString(NOTIFICATIONS_DB_FILE_PATH_KEY, &databaseFilePath) || databaseFilePath.empty()) { - ACSDK_ERROR(LX("initFailed").d("reason", "DatabaseFilePathNotFound")); - return false; - } - - if (!m_notificationsStorage->open(databaseFilePath)) { + if (!m_notificationsStorage->open()) { ACSDK_INFO(LX(__func__).m("database file does not exist. Creating.")); - if (!m_notificationsStorage->createDatabase(databaseFilePath)) { + if (!m_notificationsStorage->createDatabase()) { ACSDK_ERROR(LX("initFailed").d("reason", "NotificationIndicatorDatabaseCreationFailed")); return false; } @@ -733,6 +721,31 @@ void NotificationsCapabilityAgent::executeShutdown() { } } +void NotificationsCapabilityAgent::clearData() { + ACSDK_DEBUG5(LX(__func__)); + std::unique_lock lock(m_shutdownMutex); + + if (m_currentState == NotificationsCapabilityAgentState::PLAYING) { + if (!m_renderer->cancelNotificationRendering()) { + ACSDK_ERROR(LX(__func__).m("failed to cancel notification rendering during clearData")); + } + } + + if (m_currentState == NotificationsCapabilityAgentState::SHUTDOWN || + m_currentState == NotificationsCapabilityAgentState::SHUTTING_DOWN) { + ACSDK_WARN(LX(__func__).m("should not be trying to clear data during shutdown.")); + } else { + executeSetState(NotificationsCapabilityAgentState::IDLE); + } + + auto result = m_executor.submit([this]() { + m_notificationsStorage->clearNotificationIndicators(); + m_notificationsStorage->setIndicatorState(IndicatorState::OFF); + }); + + result.wait(); +} + } // namespace notifications } // namespace capabilityAgents } // namespace alexaClientSDK diff --git a/CapabilityAgents/Notifications/src/SQLiteNotificationsStorage.cpp b/CapabilityAgents/Notifications/src/SQLiteNotificationsStorage.cpp index 023ccb67bc..9b4cb1fc94 100644 --- a/CapabilityAgents/Notifications/src/SQLiteNotificationsStorage.cpp +++ b/CapabilityAgents/Notifications/src/SQLiteNotificationsStorage.cpp @@ -39,6 +39,12 @@ static const std::string TAG("SQLiteNotificationsStorage"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) +/// The key in our config file to find the root of settings. +static const std::string NOTIFICATIONS_CONFIGURATION_ROOT_KEY = "notifications"; + +/// The key in our config file to find the database file path. +static const std::string NOTIFICATIONS_DB_FILE_PATH_KEY = "databaseFilePath"; + static const std::string NOTIFICATION_INDICATOR_TABLE_NAME = "notificationIndicators"; static const std::string DATABASE_COLUMN_PERSIST_VISUAL_INDICATOR_NAME = "persistVisualIndicator"; @@ -61,28 +67,39 @@ static const std::string INDICATOR_STATE_NAME = "indicatorState"; static const std::string CREATE_INDICATOR_STATE_TABLE_SQL_STRING = std::string("CREATE TABLE ") + INDICATOR_STATE_NAME + " (" + INDICATOR_STATE_NAME + " INT NOT NULL);"; -SQLiteNotificationsStorage::SQLiteNotificationsStorage() : m_dbHandle{nullptr} { -} - -bool SQLiteNotificationsStorage::createDatabase(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "DatabaseHandleAlreadyOpen")); - return false; +std::unique_ptr SQLiteNotificationsStorage::create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot) { + auto notificationConfigurationRoot = configurationRoot[NOTIFICATIONS_CONFIGURATION_ROOT_KEY]; + if (!notificationConfigurationRoot) { + ACSDK_ERROR(LX("createFailed") + .d("reason", "Could not load config for the Notification Storage database") + .d("key", NOTIFICATIONS_CONFIGURATION_ROOT_KEY)); + return nullptr; } - if (fileExists(filePath)) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "FileAlreadyExists").d("FilePath", filePath)); - return false; + std::string notificationDatabaseFilePath; + if (!notificationConfigurationRoot.getString(NOTIFICATIONS_DB_FILE_PATH_KEY, ¬ificationDatabaseFilePath) || + notificationDatabaseFilePath.empty()) { + ACSDK_ERROR( + LX("createFailed").d("reason", "Could not load config value").d("key", NOTIFICATIONS_DB_FILE_PATH_KEY)); + return nullptr; } - m_dbHandle = createSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "SQLiteCreateDatabaseFailed").d("file path", filePath)); + return std::unique_ptr(new SQLiteNotificationsStorage(notificationDatabaseFilePath)); +} + +SQLiteNotificationsStorage::SQLiteNotificationsStorage(const std::string& databaseFilePath) : + m_database{databaseFilePath} { +} + +bool SQLiteNotificationsStorage::createDatabase() { + if (!m_database.initialize()) { + ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "SQLiteCreateDatabaseFailed")); return false; } // Create NotificationIndicator table - if (!performQuery(m_dbHandle, CREATE_NOTIFICATION_INDICATOR_TABLE_SQL_STRING)) { + if (!m_database.performQuery(CREATE_NOTIFICATION_INDICATOR_TABLE_SQL_STRING)) { ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "failed to create notification indicator table")); close(); return false; @@ -92,7 +109,7 @@ bool SQLiteNotificationsStorage::createDatabase(const std::string& filePath) { // the database will be in an inconsistent state and will require deletion or another call to createDatabase(). // Create IndicatorState table - if (!performQuery(m_dbHandle, CREATE_INDICATOR_STATE_TABLE_SQL_STRING)) { + if (!m_database.performQuery(CREATE_INDICATOR_STATE_TABLE_SQL_STRING)) { ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "failed to create indicator state table")); close(); return false; @@ -108,30 +125,19 @@ bool SQLiteNotificationsStorage::createDatabase(const std::string& filePath) { return true; } -bool SQLiteNotificationsStorage::open(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("openFailed").d("reason", "DatabaseHandleAlreadyOpen")); +bool SQLiteNotificationsStorage::open() { + if (!m_database.open()) { + ACSDK_ERROR(LX("openFailed").d("reason", "openSQLiteDatabaseFailed")); return false; } - if (!fileExists(filePath)) { - ACSDK_DEBUG(LX("openFailed").d("reason", "FileDoesNotExist").d("FilePath", filePath)); - return false; - } - - m_dbHandle = openSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("openFailed").d("reason", "openSQLiteDatabaseFailed").d("FilePath", filePath)); - return false; - } - - if (!tableExists(m_dbHandle, NOTIFICATION_INDICATOR_TABLE_NAME)) { + if (!m_database.tableExists(NOTIFICATION_INDICATOR_TABLE_NAME)) { ACSDK_ERROR( LX("openFailed").d("reason", "table doesn't exist").d("TableName", NOTIFICATION_INDICATOR_TABLE_NAME)); return false; } - if (!tableExists(m_dbHandle, INDICATOR_STATE_NAME)) { + if (!m_database.tableExists(INDICATOR_STATE_NAME)) { ACSDK_ERROR(LX("openFailed").d("reason", "table doesn't exist").d("TableName", INDICATOR_STATE_NAME)); return false; } @@ -139,15 +145,8 @@ bool SQLiteNotificationsStorage::open(const std::string& filePath) { return true; } -bool SQLiteNotificationsStorage::isOpen() const { - return (nullptr != m_dbHandle); -} - void SQLiteNotificationsStorage::close() { - if (m_dbHandle) { - closeSQLiteDatabase(m_dbHandle); - m_dbHandle = nullptr; - } + m_database.close(); } bool SQLiteNotificationsStorage::enqueue(const NotificationIndicator& notificationIndicator) { @@ -162,32 +161,27 @@ bool SQLiteNotificationsStorage::enqueue(const NotificationIndicator& notificati // lock here to bind the id generation and the enqueue operations std::lock_guard lock{m_databaseMutex}; - if (!m_dbHandle) { - ACSDK_ERROR(LX("enqueueFailed").m("Database handle is not open")); - return false; - } - - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("enqueueFailed").m("Could not create statement")); return false; } int boundParam = 1; - if (!statement.bindIntParameter(boundParam++, notificationIndicator.persistVisualIndicator) || - !statement.bindIntParameter(boundParam++, notificationIndicator.playAudioIndicator) || - !statement.bindStringParameter(boundParam++, notificationIndicator.asset.assetId) || - !statement.bindStringParameter(boundParam++, notificationIndicator.asset.url)) { + if (!statement->bindIntParameter(boundParam++, notificationIndicator.persistVisualIndicator) || + !statement->bindIntParameter(boundParam++, notificationIndicator.playAudioIndicator) || + !statement->bindStringParameter(boundParam++, notificationIndicator.asset.assetId) || + !statement->bindStringParameter(boundParam++, notificationIndicator.asset.url)) { ACSDK_ERROR(LX("enqueueFailed").m("Could not bind parameter")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("enqueueFailed").m("Could not perform step")); return false; } - statement.finalize(); + statement->finalize(); return true; } @@ -195,11 +189,16 @@ bool SQLiteNotificationsStorage::enqueue(const NotificationIndicator& notificati /** * A utility function to pop the next notificationIndicator from the database. * - * @param dbHandle The database handle. + * @param database Pounter to the database. * @return Whether the delete operation was successful. * @note This function should only be called when holding m_databaseMutex. */ -static bool popNotificationIndicatorLocked(sqlite3* dbHandle) { +static bool popNotificationIndicatorLocked(SQLiteDatabase* database) { + if (!database) { + ACSDK_ERROR(LX("popNotificationIndicatorLockedFailed").m("null database")); + return false; + } + // the next notificationIndicator in the queue corresponds to the minimum id const std::string minTableId = "(SELECT ROWID FROM " + NOTIFICATION_INDICATOR_TABLE_NAME + " order by ROWID limit 1)"; @@ -207,14 +206,14 @@ static bool popNotificationIndicatorLocked(sqlite3* dbHandle) { const std::string sqlString = "DELETE FROM " + NOTIFICATION_INDICATOR_TABLE_NAME + " WHERE ROWID=" + minTableId + ";"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = database->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("popNotificationIndicatorLockedFailed").m("Could not create statement.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("popNotificationIndicatorLockedFailed").m("Could not perform step.")); return false; } @@ -225,11 +224,6 @@ static bool popNotificationIndicatorLocked(sqlite3* dbHandle) { bool SQLiteNotificationsStorage::dequeue() { std::lock_guard lock{m_databaseMutex}; - if (!m_dbHandle) { - ACSDK_ERROR(LX("dequeueFailed").m("Database handle is not open")); - return false; - } - // need to check if there are NotificationIndicators left to dequeue, but the indicator itself doesn't matter NotificationIndicator dummyIndicator; @@ -238,7 +232,7 @@ bool SQLiteNotificationsStorage::dequeue() { return false; } - if (!popNotificationIndicatorLocked(m_dbHandle)) { + if (!popNotificationIndicatorLocked(&m_database)) { ACSDK_ERROR(LX("dequeueFailed").m("Could not pop notificationIndicator from table")); return false; } @@ -258,124 +252,109 @@ bool SQLiteNotificationsStorage::peek(NotificationIndicator* notificationIndicat bool SQLiteNotificationsStorage::setIndicatorState(IndicatorState state) { std::lock_guard lock{m_databaseMutex}; - if (!m_dbHandle) { - ACSDK_ERROR(LX("setIndicatorStateFailed").m("Database handle is not open")); - return false; - } - // first delete the old record, we only need to maintain one record of IndicatorState at a time. std::string sqlString = "DELETE FROM " + INDICATOR_STATE_NAME + " WHERE ROWID IN (SELECT ROWID FROM " + INDICATOR_STATE_NAME + " limit 1);"; - SQLiteStatement deleteStatement(m_dbHandle, sqlString); + auto deleteStatement = m_database.createStatement(sqlString); - if (!deleteStatement.isValid()) { + if (!deleteStatement) { ACSDK_ERROR(LX("setIndicatorStateFailed").m("Could not create deleteStatement")); return false; } - if (!deleteStatement.step()) { + if (!deleteStatement->step()) { ACSDK_ERROR(LX("setIndicatorStateFailed").m("Could not perform step")); return false; } - deleteStatement.finalize(); + deleteStatement->finalize(); // we should only be storing one record in this table at any given time sqlString = "INSERT INTO " + INDICATOR_STATE_NAME + " (" + INDICATOR_STATE_NAME + ") VALUES (?);"; - SQLiteStatement insertStatement(m_dbHandle, sqlString); + auto insertStatement = m_database.createStatement(sqlString); - if (!insertStatement.isValid()) { + if (!insertStatement) { ACSDK_ERROR(LX("setIndicatorStateFailed").m("Could not create insertStatement")); return false; } - if (!insertStatement.bindIntParameter(1, indicatorStateToInt(state))) { + if (!insertStatement->bindIntParameter(1, indicatorStateToInt(state))) { ACSDK_ERROR(LX("setIndicatorStateFailed").m("Could not bind parameter")); return false; } - if (!insertStatement.step()) { + if (!insertStatement->step()) { ACSDK_ERROR(LX("setIndicatorStateFailed").m("Could not perform step")); return false; } - insertStatement.finalize(); + insertStatement->finalize(); return true; } -bool SQLiteNotificationsStorage::getIndicatorState(IndicatorState* state) const { +bool SQLiteNotificationsStorage::getIndicatorState(IndicatorState* state) { if (!state) { ACSDK_ERROR(LX("getIndicatorStateFailed").m("State parameter was nullptr")); return false; } - if (!m_dbHandle) { - ACSDK_ERROR(LX("getIndicatorStateFailed").m("Database handle is not open")); - return false; - } - std::string sqlString = "SELECT * FROM " + INDICATOR_STATE_NAME; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("getIndicatorStateFailed").m("Could not create statement")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("getIndicatorStateFailed").m("Could not perform step")); return false; } - int stepResult = statement.getStepResult(); + int stepResult = statement->getStepResult(); if (stepResult != SQLITE_ROW) { ACSDK_ERROR(LX("getIndicatorStateFailed").m("No records left in table")); return false; } - *state = avsCommon::avs::intToIndicatorState(statement.getColumnInt(0)); + *state = avsCommon::avs::intToIndicatorState(statement->getColumnInt(0)); if (IndicatorState::UNDEFINED == *state) { ACSDK_ERROR(LX("getIndicatorStateFailed").m("Unknown indicator state retrieved from table")); return false; } - statement.finalize(); + statement->finalize(); return true; } -bool SQLiteNotificationsStorage::checkForEmptyQueue(bool* empty) const { +bool SQLiteNotificationsStorage::checkForEmptyQueue(bool* empty) { if (!empty) { ACSDK_ERROR(LX("checkForEmptyQueueFailed").m("empty parameter was nullptr")); return false; } - if (!m_dbHandle) { - ACSDK_ERROR(LX("checkForEmptyQueueFailed").m("Database handle is not open")); - return false; - } - std::string sqlString = "SELECT * FROM " + NOTIFICATION_INDICATOR_TABLE_NAME; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("checkForEmptyQueueFailed").m("Could not create statement")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("checkForEmptyQueueFailed").m("Could not perform step")); return false; } - int stepResult = statement.getStepResult(); + int stepResult = statement->getStepResult(); if (stepResult != SQLITE_ROW) { *empty = true; @@ -391,19 +370,14 @@ bool SQLiteNotificationsStorage::clearNotificationIndicators() { std::lock_guard lock{m_databaseMutex}; - if (!m_dbHandle) { - ACSDK_ERROR(LX("clearNotificationIndicatorsFailed").m("Database handle is not open")); - return false; - } - - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("clearNotificationIndicatorsFailed").m("Could not create statement.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("clearNotificationIndicatorsFailed").m("Could not perform step.")); return false; } @@ -418,26 +392,26 @@ bool SQLiteNotificationsStorage::getNextNotificationIndicatorLocked(Notification // the minimum id is the next NotificationIndicator in the queue std::string sqlString = "SELECT * FROM " + NOTIFICATION_INDICATOR_TABLE_NAME + " ORDER BY ROWID ASC LIMIT 1;"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("getNextNotificationIndicatorLockedFailed").m("Could not create statement")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("getNextNotificationIndicatorLockedFailed").m("Could not perform step")); return false; } - int stepResult = statement.getStepResult(); + int stepResult = statement->getStepResult(); if (stepResult != SQLITE_ROW) { ACSDK_ERROR(LX("getNextNotificationIndicatorLockedFailed").m("No records left in table")); return false; } - int numberColumns = statement.getColumnCount(); + int numberColumns = statement->getColumnCount(); bool persistVisualIndicator = false; bool playAudioIndicator = false; @@ -446,19 +420,19 @@ bool SQLiteNotificationsStorage::getNextNotificationIndicatorLocked(Notification // loop through columns to get all pertinent data for the out parameter for (int i = 0; i < numberColumns; i++) { - std::string columnName = statement.getColumnName(i); + std::string columnName = statement->getColumnName(i); if (DATABASE_COLUMN_PERSIST_VISUAL_INDICATOR_NAME == columnName) { - persistVisualIndicator = statement.getColumnInt(i); + persistVisualIndicator = statement->getColumnInt(i); } else if (DATABASE_COLUMN_PLAY_AUDIO_INDICATOR_NAME == columnName) { - playAudioIndicator = statement.getColumnInt(i); + playAudioIndicator = statement->getColumnInt(i); } else if (DATABASE_COLUMN_ASSET_ID_NAME == columnName) { - assetId = statement.getColumnText(i); + assetId = statement->getColumnText(i); } else if (DATABASE_COLUMN_ASSET_URL_NAME == columnName) { - assetUrl = statement.getColumnText(i); + assetUrl = statement->getColumnText(i); } } @@ -467,18 +441,13 @@ bool SQLiteNotificationsStorage::getNextNotificationIndicatorLocked(Notification return true; } -bool SQLiteNotificationsStorage::getQueueSize(int* size) const { +bool SQLiteNotificationsStorage::getQueueSize(int* size) { if (!size) { ACSDK_ERROR(LX("getQueueSizeFailed").m("size parameter was nullptr")); return false; } - if (!m_dbHandle) { - ACSDK_ERROR(LX("getQueueSizeFailed").m("Database handle is not open")); - return false; - } - - if (!getNumberTableRows(m_dbHandle, NOTIFICATION_INDICATOR_TABLE_NAME, size)) { + if (!getNumberTableRows(&m_database, NOTIFICATION_INDICATOR_TABLE_NAME, size)) { ACSDK_ERROR(LX("getQueueSizeFailed").m("Failed to count rows in table")); return false; } diff --git a/CapabilityAgents/Notifications/test/NotificationsCapabilityAgentTest.cpp b/CapabilityAgents/Notifications/test/NotificationsCapabilityAgentTest.cpp index a896f63a22..1b20905a6e 100644 --- a/CapabilityAgents/Notifications/test/NotificationsCapabilityAgentTest.cpp +++ b/CapabilityAgents/Notifications/test/NotificationsCapabilityAgentTest.cpp @@ -31,6 +31,7 @@ #include #include #include +#include #include "Notifications/NotificationsCapabilityAgent.h" #include "Notifications/NotificationIndicator.h" @@ -169,11 +170,9 @@ void TestNotificationsObserver::onSetIndicator(IndicatorState state) { */ class TestNotificationsStorage : public NotificationsStorageInterface { public: - bool createDatabase(const std::string& filePath) override; + bool createDatabase() override; - bool open(const std::string& filePath) override; - - bool isOpen() const override; + bool open() override; void close() override; @@ -185,13 +184,13 @@ class TestNotificationsStorage : public NotificationsStorageInterface { bool setIndicatorState(IndicatorState state) override; - bool getIndicatorState(IndicatorState* state) const override; + bool getIndicatorState(IndicatorState* state) override; - bool checkForEmptyQueue(bool* empty) const override; + bool checkForEmptyQueue(bool* empty) override; bool clearNotificationIndicators() override; - bool getQueueSize(int* size) const override; + bool getQueueSize(int* size) override; /** * Waits until the queue is a particular size. @@ -213,7 +212,7 @@ class TestNotificationsStorage : public NotificationsStorageInterface { std::condition_variable m_conditionVariable; }; -bool TestNotificationsStorage::createDatabase(const std::string& filePath) { +bool TestNotificationsStorage::createDatabase() { if (!setIndicatorState(IndicatorState::OFF)) { ACSDK_ERROR(LX("createTestDatabaseFailed").d("reason", "failed to set default indicator state")); return false; @@ -221,11 +220,7 @@ bool TestNotificationsStorage::createDatabase(const std::string& filePath) { return true; } -bool TestNotificationsStorage::open(const std::string& filePath) { - return true; -} - -bool TestNotificationsStorage::isOpen() const { +bool TestNotificationsStorage::open() { return true; } @@ -259,7 +254,7 @@ bool TestNotificationsStorage::setIndicatorState(IndicatorState state) { return true; } -bool TestNotificationsStorage::getIndicatorState(IndicatorState* state) const { +bool TestNotificationsStorage::getIndicatorState(IndicatorState* state) { if (!state) { return false; } @@ -267,7 +262,7 @@ bool TestNotificationsStorage::getIndicatorState(IndicatorState* state) const { return true; } -bool TestNotificationsStorage::checkForEmptyQueue(bool* empty) const { +bool TestNotificationsStorage::checkForEmptyQueue(bool* empty) { *empty = m_notificationQueue.empty(); return true; } @@ -277,7 +272,7 @@ bool TestNotificationsStorage::clearNotificationIndicators() { return true; } -bool TestNotificationsStorage::getQueueSize(int* size) const { +bool TestNotificationsStorage::getQueueSize(int* size) { if (!size) { return false; } @@ -555,6 +550,9 @@ class NotificationsCapabilityAgentTest : public ::testing::Test { /// Triggers threads waiting on a SetIndicator directive to be processed. std::condition_variable m_setIndicatorTrigger; + /// Shared pointer to @c CustomerDataManager + std::shared_ptr m_dataManager; + /// A count of how many SetIndicator directives have been processed. unsigned int m_numSetIndicatorsProcessed; }; @@ -565,7 +563,8 @@ void NotificationsCapabilityAgentTest::initializeCapabilityAgent() { m_renderer, m_mockContextManager, m_mockExceptionSender, - m_testNotificationsAudioFactory); + m_testNotificationsAudioFactory, + m_dataManager); ASSERT_TRUE(m_notificationsCapabilityAgent); m_notificationsCapabilityAgent->addObserver(m_testNotificationsObserver); m_renderer->addObserver(m_notificationsCapabilityAgent); @@ -586,6 +585,7 @@ void NotificationsCapabilityAgentTest::SetUp() { m_mockDirectiveHandlerResult = std::unique_ptr(new MockDirectiveHandlerResult); m_numSetIndicatorsProcessed = 0; + m_dataManager = std::make_shared(); } void NotificationsCapabilityAgentTest::TearDown() { @@ -661,23 +661,43 @@ TEST_F(NotificationsCapabilityAgentTest, testCreate) { std::shared_ptr testNotificationsCapabilityAgent; testNotificationsCapabilityAgent = NotificationsCapabilityAgent::create( - nullptr, m_renderer, m_mockContextManager, m_mockExceptionSender, m_testNotificationsAudioFactory); + nullptr, + m_renderer, + m_mockContextManager, + m_mockExceptionSender, + m_testNotificationsAudioFactory, + m_dataManager); EXPECT_EQ(testNotificationsCapabilityAgent, nullptr); testNotificationsCapabilityAgent = NotificationsCapabilityAgent::create( - m_notificationsStorage, nullptr, m_mockContextManager, m_mockExceptionSender, m_testNotificationsAudioFactory); + m_notificationsStorage, + nullptr, + m_mockContextManager, + m_mockExceptionSender, + m_testNotificationsAudioFactory, + m_dataManager); EXPECT_EQ(testNotificationsCapabilityAgent, nullptr); testNotificationsCapabilityAgent = NotificationsCapabilityAgent::create( - m_notificationsStorage, m_renderer, nullptr, m_mockExceptionSender, m_testNotificationsAudioFactory); + m_notificationsStorage, + m_renderer, + nullptr, + m_mockExceptionSender, + m_testNotificationsAudioFactory, + m_dataManager); EXPECT_EQ(testNotificationsCapabilityAgent, nullptr); testNotificationsCapabilityAgent = NotificationsCapabilityAgent::create( - m_notificationsStorage, m_renderer, m_mockContextManager, nullptr, m_testNotificationsAudioFactory); + m_notificationsStorage, + m_renderer, + m_mockContextManager, + nullptr, + m_testNotificationsAudioFactory, + m_dataManager); EXPECT_EQ(testNotificationsCapabilityAgent, nullptr); testNotificationsCapabilityAgent = NotificationsCapabilityAgent::create( - m_notificationsStorage, m_renderer, m_mockContextManager, m_mockExceptionSender, nullptr); + m_notificationsStorage, m_renderer, m_mockContextManager, m_mockExceptionSender, nullptr, m_dataManager); EXPECT_EQ(testNotificationsCapabilityAgent, nullptr); } @@ -870,6 +890,32 @@ TEST_F(NotificationsCapabilityAgentTest, testMultipleSetIndicators) { ASSERT_TRUE(m_renderer->waitUntilRenderingFinished()); } +/** + * Test that @c clearData() removes all notifications and sets the indicator to OFF. + */ +TEST_F(NotificationsCapabilityAgentTest, testClearData) { + initializeCapabilityAgent(); + sendSetIndicatorDirective(generatePayload(true, true, "assetId1"), "firstIndicatorMessageId"); + ASSERT_TRUE(m_renderer->waitUntilRenderingStarted()); + ASSERT_TRUE(m_renderer->waitUntilRenderingFinished()); + + // Check that indicator is ON + IndicatorState state = IndicatorState::UNDEFINED; + m_notificationsStorage->getIndicatorState(&state); + ASSERT_EQ(state, IndicatorState::ON); + + // Check that the notification queue is not empty + int queueSize; + m_notificationsStorage->getQueueSize(&queueSize); + ASSERT_GT(queueSize, 0); + + m_notificationsCapabilityAgent->clearData(); + ASSERT_TRUE(m_notificationsStorage->waitForQueueSizeToBe(0)); + + m_notificationsStorage->getIndicatorState(&state); + ASSERT_EQ(state, IndicatorState::OFF); +} + } // namespace test } // namespace notifications } // namespace capabilityAgents diff --git a/CapabilityAgents/Notifications/test/NotificationsStorageTest.cpp b/CapabilityAgents/Notifications/test/NotificationsStorageTest.cpp index 78f7844daa..a26a09c801 100644 --- a/CapabilityAgents/Notifications/test/NotificationsStorageTest.cpp +++ b/CapabilityAgents/Notifications/test/NotificationsStorageTest.cpp @@ -57,6 +57,17 @@ static const unsigned int NOTIFICATION_INDICATOR_SEED = 1; /// The max random number to generate. static const unsigned int MAX_RANDOM_INT = 100; +/** + * Utility function to determine if the storage component is opened. + * + * @param storage The storage component to check. + * @return True if the storage component's underlying database is opened, false otherwise. + */ +static bool isOpen(const std::shared_ptr& storage) { + int dummySize; + return storage->getQueueSize(&dummySize); +} + class NotificationsStorageTest : public ::testing::Test { public: NotificationsStorageTest(); @@ -82,7 +93,8 @@ class NotificationsStorageTest : public ::testing::Test { std::shared_ptr m_storage; }; -NotificationsStorageTest::NotificationsStorageTest() : m_storage{std::make_shared()} { +NotificationsStorageTest::NotificationsStorageTest() : + m_storage{std::make_shared(TEST_DATABASE_FILE_PATH)} { cleanupLocalDbFile(); } @@ -92,7 +104,7 @@ NotificationsStorageTest::~NotificationsStorageTest() { } void NotificationsStorageTest::createDatabase() { - m_storage->createDatabase(TEST_DATABASE_FILE_PATH); + m_storage->createDatabase(); } void NotificationsStorageTest::cleanupLocalDbFile() { @@ -114,31 +126,31 @@ void NotificationsStorageTest::checkNotificationIndicatorsEquality( * Test basic construction. Database should not be open. */ TEST_F(NotificationsStorageTest, testConstructionAndDestruction) { - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); } /** * Test database creation. */ TEST_F(NotificationsStorageTest, testDatabaseCreation) { - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); } /** * Test opening and closing a database. */ TEST_F(NotificationsStorageTest, testOpenAndCloseDatabase) { - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); m_storage->close(); - ASSERT_FALSE(m_storage->isOpen()); - ASSERT_TRUE(m_storage->open(TEST_DATABASE_FILE_PATH)); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); + ASSERT_TRUE(m_storage->open()); + ASSERT_TRUE(isOpen(m_storage)); m_storage->close(); - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); } /** @@ -153,7 +165,7 @@ TEST_F(NotificationsStorageTest, testDatabaseEnqueueAndDequeue) { ASSERT_FALSE(m_storage->dequeue()); createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); ASSERT_TRUE(m_storage->enqueue(firstIndicator)); ASSERT_TRUE(m_storage->enqueue(secondIndicator)); @@ -186,7 +198,7 @@ TEST_F(NotificationsStorageTest, testSettingAndGettingIndicatorState) { ASSERT_FALSE(m_storage->getIndicatorState(&state)); createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); ASSERT_TRUE(m_storage->setIndicatorState(IndicatorState::ON)); ASSERT_TRUE(m_storage->getIndicatorState(&state)); @@ -204,7 +216,7 @@ TEST_F(NotificationsStorageTest, testSettingAndGettingIndicatorState) { */ TEST_F(NotificationsStorageTest, testClearingNotificationIndicators) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); NotificationIndicator firstIndicator(true, false, TEST_ASSET_ID1, TEST_ASSET_URL1); NotificationIndicator secondIndicator(false, true, TEST_ASSET_ID2, TEST_ASSET_URL2); @@ -221,7 +233,7 @@ TEST_F(NotificationsStorageTest, testClearingNotificationIndicators) { */ TEST_F(NotificationsStorageTest, testCheckingEmptyQueue) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); bool empty = false; @@ -257,7 +269,7 @@ TEST_F(NotificationsStorageTest, testCheckingEmptyQueue) { */ TEST_F(NotificationsStorageTest, testDatabasePersistence) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); NotificationIndicator firstIndicator(true, false, TEST_ASSET_ID1, TEST_ASSET_URL1); NotificationIndicator secondIndicator(false, true, TEST_ASSET_ID2, TEST_ASSET_URL2); @@ -267,9 +279,9 @@ TEST_F(NotificationsStorageTest, testDatabasePersistence) { m_storage->close(); - ASSERT_FALSE(m_storage->isOpen()); - ASSERT_TRUE(m_storage->open(TEST_DATABASE_FILE_PATH)); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); + ASSERT_TRUE(m_storage->open()); + ASSERT_TRUE(isOpen(m_storage)); NotificationIndicator firstDequeue; ASSERT_TRUE(m_storage->peek(&firstDequeue)); @@ -280,9 +292,9 @@ TEST_F(NotificationsStorageTest, testDatabasePersistence) { // let's try closing again before the second dequeue m_storage->close(); - ASSERT_FALSE(m_storage->isOpen()); - ASSERT_TRUE(m_storage->open(TEST_DATABASE_FILE_PATH)); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); + ASSERT_TRUE(m_storage->open()); + ASSERT_TRUE(isOpen(m_storage)); NotificationIndicator secondDequeue; ASSERT_TRUE(m_storage->peek(&secondDequeue)); @@ -296,7 +308,7 @@ TEST_F(NotificationsStorageTest, testDatabasePersistence) { */ TEST_F(NotificationsStorageTest, testQueueOrder) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); // generate ints with a random generator, this will be used to produce random bool values std::default_random_engine intGenerator; @@ -334,7 +346,7 @@ TEST_F(NotificationsStorageTest, testQueueOrder) { */ TEST_F(NotificationsStorageTest, testPeek) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); NotificationIndicator firstIndicator(true, false, TEST_ASSET_ID1, TEST_ASSET_URL1); NotificationIndicator secondIndicator(false, true, TEST_ASSET_ID2, TEST_ASSET_URL2); @@ -361,7 +373,7 @@ TEST_F(NotificationsStorageTest, testPeek) { TEST_F(NotificationsStorageTest, testSize) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); int size = 0; ASSERT_TRUE(m_storage->getQueueSize(&size)); diff --git a/CapabilityAgents/Settings/include/Settings/SQLiteSettingStorage.h b/CapabilityAgents/Settings/include/Settings/SQLiteSettingStorage.h index 72c0ea99b3..fa2a8817fa 100644 --- a/CapabilityAgents/Settings/include/Settings/SQLiteSettingStorage.h +++ b/CapabilityAgents/Settings/include/Settings/SQLiteSettingStorage.h @@ -16,10 +16,11 @@ #ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_SETTINGS_INCLUDE_SETTINGS_SQLITESETTINGSTORAGE_H_ #define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_SETTINGS_INCLUDE_SETTINGS_SQLITESETTINGSTORAGE_H_ -#include - #include "Settings/SettingsStorageInterface.h" +#include +#include + namespace alexaClientSDK { namespace capabilityAgents { namespace settings { @@ -32,15 +33,24 @@ namespace settings { class SQLiteSettingStorage : public SettingsStorageInterface { public: /** - * Constructor. + * Factory method for creating a storage object for Settings based on an SQLite database. + * + * @param configurationRoot The global config object. + * @return Pointer to the SQLiteSettingStorage object, nullptr if there's an error creating it. */ - SQLiteSettingStorage(); + static std::unique_ptr create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot); - bool createDatabase(const std::string& filePath) override; + /** + * Constructor. + * + * @param dbFilePath The location of the SQLite database file. + */ + SQLiteSettingStorage(const std::string& databaseFilePath); - bool open(const std::string& filePath) override; + bool createDatabase() override; - bool isOpen() override; + bool open() override; void close() override; @@ -59,8 +69,8 @@ class SQLiteSettingStorage : public SettingsStorageInterface { ~SQLiteSettingStorage(); private: - /// The sqlite database handle. - sqlite3* m_dbHandle; + /// The underlying database class. + alexaClientSDK::storage::sqliteStorage::SQLiteDatabase m_database; }; } // namespace settings diff --git a/CapabilityAgents/Settings/include/Settings/Settings.h b/CapabilityAgents/Settings/include/Settings/Settings.h index 50fdcaef49..de985eb1ad 100644 --- a/CapabilityAgents/Settings/include/Settings/Settings.h +++ b/CapabilityAgents/Settings/include/Settings/Settings.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_SETTINGS_INCLUDE_SETTINGS_SETTINGS_H_ #define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_SETTINGS_INCLUDE_SETTINGS_SETTINGS_H_ +#include #include #include #include @@ -24,6 +25,8 @@ #include #include #include +#include +#include #include "Settings/SettingsStorageInterface.h" namespace alexaClientSDK { @@ -36,19 +39,20 @@ namespace settings { * This class writes the Setting change to database and notifies the observers of the setting. * @see https://developer.amazon.com/public/solutions/alexa/alexa-voice-service/reference/settings */ -class Settings { +class Settings : public registrationManager::CustomerDataHandler { public: /** * Creates a new @c Settings instance. * @param settingsStorage An interface to store, load, modify and delete Settings. * @param globalSettingsObserver A set of SettingsGlobalObserver which are notified when all the settings are * changed. + * @param dataManager A dataManager object that will track the CustomerDataHandler. * @return An instance of Settings if construction is successful or nullptr if construction fails. */ static std::shared_ptr create( std::shared_ptr settingsStorage, - std::unordered_set> - globalSettingsObserver); + std::unordered_set> observers, + std::shared_ptr dataManager); /** * Add an observer for a single setting mapped to the setting key. @@ -98,6 +102,11 @@ class Settings { */ void sendDefaultSettings(); + /** + * Clears the settings storage and attributes + */ + void clearData() override; + private: /** * The structure to hold all the data of a single setting. @@ -118,11 +127,16 @@ class Settings { /** * Constructor + * + * @param settingsStorage An interface to store, load, modify and delete Settings. + * @param globalSettingsObserver A set of SettingsGlobalObserver which are notified when all the settings are + * changed. + * @param dataManager A dataManager object that will track the CustomerDataHandler. */ Settings( std::shared_ptr settingsStorage, - std::unordered_set> - globalSettingsObserver); + std::unordered_set> observers, + std::shared_ptr dataManager); /** * Function which implements the setting change. The function writes to the database and notifies the diff --git a/CapabilityAgents/Settings/include/Settings/SettingsStorageInterface.h b/CapabilityAgents/Settings/include/Settings/SettingsStorageInterface.h index eaced31cfc..969a49ed66 100644 --- a/CapabilityAgents/Settings/include/Settings/SettingsStorageInterface.h +++ b/CapabilityAgents/Settings/include/Settings/SettingsStorageInterface.h @@ -38,32 +38,23 @@ class SettingsStorageInterface { virtual ~SettingsStorageInterface() = default; /** - * Creates a new database with the given filepath. - * If the file specified already exists, or if a database is already being handled by this object, then - * this function returns false. + * Creates a new database. + * If a database is already being handled by this object or there is another internal error, then this function + * returns false. * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is created ok, or @c false if either the file exists or a database is already - * being handled by this object. + * @return @c true If the database is created ok, or @c false if a database is already being handled by this object + * or there is a problem creating the database. */ - virtual bool createDatabase(const std::string& filePath) = 0; + virtual bool createDatabase() = 0; /** - * Open a database with the given filepath. If this object is already managing an open database, or the file - * does not exist, or there is a problem opening the database, this function returns false. + * Open an existing database. If this object is already managing an open database, or there is a problem opening + * the database, this function returns false. * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is opened ok, @c false if either the file does not exist, if this object is - * already managing an open database, or if there is another internal reason the database could not be opened. + * @return @c true If the database is opened ok, @c false if this object is already managing an open database, or if + * there is another internal reason the database could not be opened. */ - virtual bool open(const std::string& filePath) = 0; - - /** - * Query if this object is currently managing an open database. - * - * @return @c true If a database is being currently managed by this object, @c false otherwise. - */ - virtual bool isOpen() = 0; + virtual bool open() = 0; /** * Close the currently open database, if one is open. diff --git a/CapabilityAgents/Settings/src/CMakeLists.txt b/CapabilityAgents/Settings/src/CMakeLists.txt index 4022dcd6da..0ceaef59d0 100644 --- a/CapabilityAgents/Settings/src/CMakeLists.txt +++ b/CapabilityAgents/Settings/src/CMakeLists.txt @@ -8,9 +8,10 @@ add_library(Settings SHARED target_include_directories(Settings PUBLIC "${Settings_SOURCE_DIR}/include" "${SpeechSynthesizer_SOURCE_DIR}/include" - "${SQLiteStorage_SOURCE_DIR}/include") + "${SQLiteStorage_SOURCE_DIR}/include" + "{RegistrationManager_SOURCE_DIR}/include") -target_link_libraries(Settings AVSCommon SQLiteStorage) +target_link_libraries(Settings AVSCommon SQLiteStorage RegistrationManager) # install target asdk_install() \ No newline at end of file diff --git a/CapabilityAgents/Settings/src/SQLiteSettingStorage.cpp b/CapabilityAgents/Settings/src/SQLiteSettingStorage.cpp index 6e1199d0d2..b1839bec05 100644 --- a/CapabilityAgents/Settings/src/SQLiteSettingStorage.cpp +++ b/CapabilityAgents/Settings/src/SQLiteSettingStorage.cpp @@ -38,6 +38,11 @@ static const std::string TAG("SQLiteSettingsStorage"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) +/// The key in our config file to find the root of settings. +static const std::string SETTINGS_CONFIGURATION_ROOT_KEY = "settings"; +/// The key in our config file to find the database file path. +static const std::string SETTINGS_DB_FILE_PATH_KEY = "databaseFilePath"; + /// The name of the settings table. static const std::string SETTINGS_TABLE_NAME = "settings"; /// The setting key. @@ -49,99 +54,75 @@ static const std::string CREATE_SETTINGS_TABLE_SQL_STRING = std::string("CREATE SETTING_KEY + " TEXT PRIMARY KEY NOT NULL," + SETTING_VALUE + " TEXT NOT NULL);"; -SQLiteSettingStorage::SQLiteSettingStorage() : m_dbHandle{nullptr} { -} - -/** - * A small utility function to help determine if a file exists. - * - * @param filePath The path to the file being queried about. - * @return Whether the file exists and is accessible. - */ -static bool fileExists(const std::string& filePath) { - std::ifstream is(filePath); - return is.good(); -} - -bool SQLiteSettingStorage::createDatabase(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "DatabaseHandleAlreadyOpen")); - return false; - } - - if (fileExists(filePath)) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "FileAlreadyExists").d("FilePath", filePath)); - return false; +std::unique_ptr SQLiteSettingStorage::create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot) { + auto settingsConfigurationRoot = configurationRoot[SETTINGS_CONFIGURATION_ROOT_KEY]; + if (!settingsConfigurationRoot) { + ACSDK_ERROR(LX("createFailed") + .d("reason", "Could not load config for the Settings capability agent") + .d("key", SETTINGS_CONFIGURATION_ROOT_KEY)); + return nullptr; } - m_dbHandle = createSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "SQLiteCreateDatabaseFailed").d("file path", filePath)); - return false; + std::string settingDbFilePath; + if (!settingsConfigurationRoot.getString(SETTINGS_DB_FILE_PATH_KEY, &settingDbFilePath) || + settingDbFilePath.empty()) { + ACSDK_ERROR(LX("createFailed").d("reason", "Could not load config value").d("key", SETTINGS_DB_FILE_PATH_KEY)); + return nullptr; } - if (!performQuery(m_dbHandle, CREATE_SETTINGS_TABLE_SQL_STRING)) { - ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "PerformQueryFailed")); - close(); - return false; - } + return std::unique_ptr(new SQLiteSettingStorage(settingDbFilePath)); +} - return true; +SQLiteSettingStorage::SQLiteSettingStorage(const std::string& databaseFilePath) : m_database{databaseFilePath} { } -bool SQLiteSettingStorage::open(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("openFailed").d("reason", "DatabaseHandleAlreadyOpen")); +bool SQLiteSettingStorage::createDatabase() { + if (!m_database.initialize()) { + ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "SQLiteCreateDatabaseFailed")); return false; } - if (!fileExists(filePath)) { - ACSDK_DEBUG(LX("openFailed").d("reason", "FileDoesNotExist").d("FilePath", filePath)); + if (!m_database.performQuery(CREATE_SETTINGS_TABLE_SQL_STRING)) { + ACSDK_ERROR(LX("createDatabaseFailed").d("reason", "PerformQueryFailed")); + close(); return false; } - m_dbHandle = openSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("openFailed").d("reason", "openSQLiteDatabaseFailed").d("FilePath", filePath)); - return false; - } return true; } -bool SQLiteSettingStorage::isOpen() { - return (nullptr != m_dbHandle); +bool SQLiteSettingStorage::open() { + return m_database.open(); } void SQLiteSettingStorage::close() { - if (m_dbHandle) { - closeSQLiteDatabase(m_dbHandle); - m_dbHandle = nullptr; - } + m_database.close(); } bool SQLiteSettingStorage::settingExists(const std::string& key) { std::string sqlString = "SELECT COUNT(*) FROM " + SETTINGS_TABLE_NAME + " WHERE " + SETTING_KEY + "=?;"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("settingExistsFailed").d("reason", "SQliteStatementInvalid")); return false; } int boundParam = 1; - if (!statement.bindStringParameter(boundParam, key)) { + if (!statement->bindStringParameter(boundParam, key)) { ACSDK_ERROR(LX("settingExistsFailed").d("reason", "BindParameterFailed")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("settingExistsFailed").d("reason", "StepToRowFailed")); return false; } const int RESULT_COLUMN_POSITION = 0; - std::string rowValue = statement.getColumnText(RESULT_COLUMN_POSITION); + std::string rowValue = statement->getColumnText(RESULT_COLUMN_POSITION); int countValue = 0; if (!stringToInt(rowValue.c_str(), &countValue)) { @@ -149,16 +130,11 @@ bool SQLiteSettingStorage::settingExists(const std::string& key) { return false; } - statement.finalize(); + statement->finalize(); return countValue > 0; } bool SQLiteSettingStorage::store(const std::string& key, const std::string& value) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("storeFailed").d("reason", "DatabaseHandleNotOpen")); - return false; - } - if (value.empty()) { ACSDK_ERROR(LX("storeFailed").d("reason", "SettingValueisEmpty")); return false; @@ -172,39 +148,34 @@ bool SQLiteSettingStorage::store(const std::string& key, const std::string& valu std::string sqlString = std::string("INSERT INTO " + SETTINGS_TABLE_NAME + " (") + SETTING_KEY + ", " + SETTING_VALUE + ") VALUES (" + "?, ?" + ");"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("storeFailed").d("reason", "SQliteStatementInvalid")); return false; } int boundParam = 1; - if (!statement.bindStringParameter(boundParam++, key) || !statement.bindStringParameter(boundParam, value)) { + if (!statement->bindStringParameter(boundParam++, key) || !statement->bindStringParameter(boundParam, value)) { ACSDK_ERROR(LX("storeFailed").d("reason", "BindParameterFailed")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("storeFailed").d("reason", "StepToRowFailed")); return false; } - statement.finalize(); + statement->finalize(); return true; } bool SQLiteSettingStorage::load(std::unordered_map* mapOfSettings) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("loadFailed").d("reason", "DatabaseHandleNotOpen")); - return false; - } - std::string sqlString = "SELECT * FROM " + SETTINGS_TABLE_NAME + ";"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("loadFailed").d("reason", "SQliteStatementInvalid")); return false; } @@ -212,40 +183,35 @@ bool SQLiteSettingStorage::load(std::unordered_map* ma std::string key; std::string value; - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("loadFailed").d("reason", "StepToRowFailed")); return false; } - while (SQLITE_ROW == statement.getStepResult()) { - int numberColumns = statement.getColumnCount(); + while (SQLITE_ROW == statement->getStepResult()) { + int numberColumns = statement->getColumnCount(); // SQLite cannot guarantee the order of the columns in a given row, so this logic is required. for (int i = 0; i < numberColumns; i++) { - std::string columnName = statement.getColumnName(i); + std::string columnName = statement->getColumnName(i); if (SETTING_KEY == columnName) { - key = statement.getColumnText(i); + key = statement->getColumnText(i); } else if (SETTING_VALUE == columnName) { - value = statement.getColumnText(i); + value = statement->getColumnText(i); } } mapOfSettings->insert(make_pair(key, value)); - statement.step(); + statement->step(); } - statement.finalize(); + statement->finalize(); return true; } bool SQLiteSettingStorage::modify(const std::string& key, const std::string& value) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("modifyFailed").d("reason", "DatabaseHandleNotOpen")); - return false; - } - if (value.empty()) { ACSDK_ERROR(LX("modifyFailed").d("reason", "SettingValueisEmpty")); return false; @@ -259,34 +225,29 @@ bool SQLiteSettingStorage::modify(const std::string& key, const std::string& val std::string sqlString = std::string("UPDATE " + SETTINGS_TABLE_NAME + " SET ") + "value=?" + "WHERE " + SETTING_KEY + "=?;"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("modifyFailed").d("reason", "SQliteStatementInvalid")); return false; } int boundParam = 1; - if (!statement.bindStringParameter(boundParam++, value) || !statement.bindStringParameter(boundParam, key)) { + if (!statement->bindStringParameter(boundParam++, value) || !statement->bindStringParameter(boundParam, key)) { ACSDK_ERROR(LX("modifyFailed").d("reason", "BindParameterFailed")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("modifyFailed").d("reason", "StepToRowFailed")); return false; } - statement.finalize(); + statement->finalize(); return true; } bool SQLiteSettingStorage::erase(const std::string& key) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("eraseFailed").d("reason", "DatabaseHandleNotOpen")); - return false; - } - if (key.empty()) { ACSDK_ERROR(LX("eraseFailed").d("reason", "SettingKeyEmpty")); return false; @@ -299,30 +260,30 @@ bool SQLiteSettingStorage::erase(const std::string& key) { std::string sqlString = "DELETE FROM " + SETTINGS_TABLE_NAME + " WHERE " + SETTING_KEY + "=?;"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("eraseFailed").d("reason", "SQliteStatementInvalid")); return false; } int boundParam = 1; - if (!statement.bindStringParameter(boundParam, key)) { + if (!statement->bindStringParameter(boundParam, key)) { ACSDK_ERROR(LX("eraseFailed").d("reason", "BindParameterFailed")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("eraseFailed").d("reason", "StepToRowFailed")); return false; } - statement.finalize(); + statement->finalize(); return true; } bool SQLiteSettingStorage::clearDatabase() { - if (!clearTable(m_dbHandle, SETTINGS_TABLE_NAME)) { + if (!m_database.clearTable(SETTINGS_TABLE_NAME)) { ACSDK_ERROR(LX("clearDatabaseFailed").d("reason", "SqliteClearTableFailed")); return false; } diff --git a/CapabilityAgents/Settings/src/Settings.cpp b/CapabilityAgents/Settings/src/Settings.cpp index ad268a2dd4..9b051fe788 100644 --- a/CapabilityAgents/Settings/src/Settings.cpp +++ b/CapabilityAgents/Settings/src/Settings.cpp @@ -36,15 +36,14 @@ static const std::string TAG{"Settings"}; /// The key in our config file to find the root of settings. static const std::string SETTINGS_CONFIGURATION_ROOT_KEY = "settings"; /// The key in our config file to find the database file path. -static const std::string SETTINGS_DB_FILE_PATH_KEY = "databaseFilePath"; -/// The key in our config file to find the default setting root. static const std::string SETTINGS_DEFAULT_SETTINGS_ROOT_KEY = "defaultAVSClientSettings"; /// The acceptable setting keys to find in our config file. static const std::unordered_set SETTINGS_ACCEPTED_KEYS = {"locale"}; std::shared_ptr Settings::create( std::shared_ptr settingsStorage, - std::unordered_set> globalSettingsObserver) { + std::unordered_set> globalSettingsObserver, + std::shared_ptr dataManager) { if (!settingsStorage) { ACSDK_ERROR(LX("createFailed").d("reason", "settingsStorageNullReference").d("return", "nullptr")); return nullptr; @@ -62,7 +61,7 @@ std::shared_ptr Settings::create( } } - auto settingsObject = std::shared_ptr(new Settings(settingsStorage, globalSettingsObserver)); + auto settingsObject = std::shared_ptr(new Settings(settingsStorage, globalSettingsObserver, dataManager)); if (!settingsObject->initialize()) { ACSDK_ERROR(LX("createFailed").d("reason", "Initialization error.")); @@ -182,21 +181,9 @@ bool Settings::executeChangeSetting(const std::string& key, const std::string& v } bool Settings::initialize() { - auto configurationRoot = ConfigurationNode::getRoot()[SETTINGS_CONFIGURATION_ROOT_KEY]; - if (!configurationRoot) { - ACSDK_ERROR(LX("initializeFailed").d("reason", "SettingsConfigurationRootNotFound.")); - return false; - } - - std::string databaseFilePath; - if (!configurationRoot.getString(SETTINGS_DB_FILE_PATH_KEY, &databaseFilePath) || databaseFilePath.empty()) { - ACSDK_ERROR(LX("initializeFailed").d("reason", "SqliteFilePathNotFound")); - return false; - } - - if (!m_settingsStorage->open(databaseFilePath)) { + if (!m_settingsStorage->open()) { ACSDK_INFO(LX("initialize").m("database file does not exist. Creating.")); - if (!m_settingsStorage->createDatabase(databaseFilePath)) { + if (!m_settingsStorage->createDatabase()) { ACSDK_ERROR(LX("initializeFailed").d("reason", "SettingsDatabaseCreationFailed")); return false; } @@ -210,8 +197,13 @@ bool Settings::initialize() { return false; } - auto defaultSettingRoot = - ConfigurationNode::getRoot()[SETTINGS_CONFIGURATION_ROOT_KEY][SETTINGS_DEFAULT_SETTINGS_ROOT_KEY]; + auto configurationRoot = ConfigurationNode::getRoot()[SETTINGS_CONFIGURATION_ROOT_KEY]; + if (!configurationRoot) { + ACSDK_ERROR(LX("initializeFailed").d("reason", "SettingsConfigurationRootNotFound.")); + return false; + } + + auto defaultSettingRoot = configurationRoot[SETTINGS_DEFAULT_SETTINGS_ROOT_KEY]; if (!defaultSettingRoot) { ACSDK_ERROR(LX("initializeFailed").d("reason", "DefaultSettingsRootNotFound")); @@ -246,9 +238,20 @@ bool Settings::initialize() { return true; } +void Settings::clearData() { + auto result = m_executor.submit([this]() { + // Notify the observers of the single settting with value of setting. + m_settingsStorage->clearDatabase(); + m_mapOfSettingsAttributes.clear(); + }); + result.wait(); +} + Settings::Settings( std::shared_ptr settingsStorage, - std::unordered_set> globalSettingsObserver) : + std::unordered_set> globalSettingsObserver, + std::shared_ptr dataManager) : + CustomerDataHandler{dataManager}, m_settingsStorage{settingsStorage}, m_globalSettingsObserver{globalSettingsObserver}, m_sendDefaultSettings{false} { diff --git a/CapabilityAgents/Settings/test/SettingsTest.cpp b/CapabilityAgents/Settings/test/SettingsTest.cpp index 2fc3976c07..0a0cff7ced 100644 --- a/CapabilityAgents/Settings/test/SettingsTest.cpp +++ b/CapabilityAgents/Settings/test/SettingsTest.cpp @@ -91,6 +91,17 @@ static const std::string SETTINGS_CONFIG_JSON = "}"; // clang-format on +/** + * Utility function to determine if the storage component is opened. + * + * @param storage The storage component to check. + * @return True if the storage component's underlying database is opened, false otherwise. + */ +static bool isOpen(const std::shared_ptr& storage) { + std::unordered_map dummyMapOfSettings; + return storage->load(&dummyMapOfSettings); +} + /** * This class allows us to test SingleSettingObserver interaction. */ @@ -209,16 +220,19 @@ class SettingsTest : public ::testing::Test { std::shared_ptr m_settingsVerifyObject; /// The map which stores all the settings for the object. std::unordered_map m_mapOfSettings; + /// The data manager required to build the base object + std::shared_ptr m_dataManager; }; void SettingsTest::SetUp() { + m_dataManager = std::make_shared(); std::istringstream inString(SETTINGS_CONFIG_JSON); ASSERT_TRUE(AlexaClientSDKInit::initialize({&inString})); m_mockMessageSender = std::make_shared(); m_settingsEventSender = SettingsUpdatedEventSender::create(m_mockMessageSender); ASSERT_NE(m_settingsEventSender, nullptr); - m_storage = std::make_shared(); - m_settingsObject = Settings::create(m_storage, {m_settingsEventSender}); + m_storage = std::make_shared("settingsUnitTest.db"); + m_settingsObject = Settings::create(m_storage, {m_settingsEventSender}, m_dataManager); ASSERT_NE(m_settingsObject, nullptr); ASSERT_TRUE(m_storage->load(&m_mapOfSettings)); } @@ -257,9 +271,10 @@ bool SettingsTest::testChangeSettingSucceeds(const std::string& key, const std:: TEST_F(SettingsTest, createTest) { ASSERT_EQ( nullptr, - m_settingsObject->create(m_storage, std::unordered_set>())); - ASSERT_EQ(nullptr, m_settingsObject->create(nullptr, {m_settingsEventSender})); - ASSERT_EQ(nullptr, m_settingsObject->create(m_storage, {nullptr})); + m_settingsObject->create( + m_storage, std::unordered_set>(), m_dataManager)); + ASSERT_EQ(nullptr, m_settingsObject->create(nullptr, {m_settingsEventSender}, m_dataManager)); + ASSERT_EQ(nullptr, m_settingsObject->create(m_storage, {nullptr}, m_dataManager)); } /** @@ -351,6 +366,18 @@ TEST_F(SettingsTest, defaultSettingsCorrect) { } } +/** + * Test to check that @c clearData() removes any setting stored in the database. + */ +TEST_F(SettingsTest, clearDataTest) { + ASSERT_TRUE(testChangeSettingSucceeds("locale", "en-CA")); + m_settingsObject->clearData(); + + std::unordered_map tempMap; + ASSERT_TRUE(m_storage->load(&tempMap)); + ASSERT_TRUE(tempMap.empty()); +} + /** * Test to check clear database works as expected. */ @@ -399,18 +426,18 @@ TEST_F(SettingsTest, eraseTest) { */ TEST_F(SettingsTest, createDatabaseTest) { m_storage->close(); - ASSERT_FALSE(m_storage->createDatabase("settingsUnitTest.db")); + ASSERT_FALSE(m_storage->createDatabase()); } /** * Test to check the open and close functions of SQLiteSettingStorage class. */ TEST_F(SettingsTest, openAndCloseDatabaseTest) { - ASSERT_FALSE(m_storage->open("settingsUnitTest.db")); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_FALSE(m_storage->open()); + ASSERT_TRUE(isOpen(m_storage)); m_storage->close(); - ASSERT_TRUE(m_storage->open("settingsUnitTest.db")); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(m_storage->open()); + ASSERT_TRUE(isOpen(m_storage)); } } // namespace test } // namespace settings diff --git a/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp b/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp index a0c0569730..1b6bdaf957 100644 --- a/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp +++ b/CapabilityAgents/SpeechSynthesizer/src/SpeechSynthesizer.cpp @@ -56,9 +56,6 @@ static const NamespaceAndName CONTEXT_MANAGER_SPEECH_STATE{NAMESPACE, "SpeechSta /// The name of the @c FocusManager channel used by the @c SpeechSynthesizer. static const std::string CHANNEL_NAME = FocusManagerInterface::DIALOG_CHANNEL_NAME; -/// The activity Id used with the @c FocusManager by @c SpeechSynthesizer. -static const std::string FOCUS_MANAGER_ACTIVITY_ID{"SpeechSynthesizer.Speak"}; - /// The name of the event to send to the AVS server once audio starting playing. static std::string SPEECH_STARTED_EVENT_NAME{"SpeechStarted"}; @@ -442,9 +439,8 @@ void SpeechSynthesizer::executePreHandleAfterValidation(std::shared_ptr speakInfo) { m_currentInfo = speakInfo; - if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), FOCUS_MANAGER_ACTIVITY_ID)) { - static const std::string message = - std::string("Could not acquire ") + CHANNEL_NAME + " for " + FOCUS_MANAGER_ACTIVITY_ID; + if (!m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), NAMESPACE)) { + static const std::string message = std::string("Could not acquire ") + CHANNEL_NAME + " for " + NAMESPACE; ACSDK_ERROR(LX("executeHandleFailed") .d("reason", "CouldNotAcquireChannel") .d("messageId", m_currentInfo->directive->getMessageId())); diff --git a/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp b/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp index d05d99002e..c70ad557ed 100644 --- a/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp +++ b/CapabilityAgents/SpeechSynthesizer/test/SpeechSynthesizerTest.cpp @@ -55,10 +55,7 @@ static const std::chrono::milliseconds WAIT_TIMEOUT(1000); static const std::chrono::milliseconds STATE_CHANGE_TIMEOUT(10000); /// The name of the @c FocusManager channel used by the @c SpeechSynthesizer. -static const std::string CHANNEL_NAME("Dialog"); - -/// The activity Id used with the @c FocusManager by @c SpeechSynthesizer. -static const std::string FOCUS_MANAGER_ACTIVITY_ID("SpeechSynthesizer.Speak"); +static const std::string CHANNEL_NAME(avsCommon::sdkInterfaces::FocusManagerInterface::DIALOG_CHANNEL_NAME); /// Namespace for SpeechSynthesizer. static const std::string NAMESPACE_SPEECH_SYNTHESIZER("SpeechSynthesizer"); @@ -373,7 +370,7 @@ TEST_F(SpeechSynthesizerTest, testCallingHandleImmediately) { std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(1) .WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -413,7 +410,7 @@ TEST_F(SpeechSynthesizerTest, testCallingHandle) { std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(1) .WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -476,7 +473,7 @@ TEST_F(SpeechSynthesizerTest, testCallingCancelAfterHandle) { std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(1) .WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -548,7 +545,7 @@ TEST_F(SpeechSynthesizerTest, testCallingProvideStateWhenPlaying) { std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(1) .WillOnce(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -613,7 +610,7 @@ TEST_F(SpeechSynthesizerTest, testBargeInWhilePlaying) { std::shared_ptr directive2 = AVSDirective::create("", avsMessageHeader2, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST_2); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(AtLeast(1)) .WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -680,7 +677,7 @@ TEST_F(SpeechSynthesizerTest, testNotCallStopTwice) { std::shared_ptr directive2 = AVSDirective::create("", avsMessageHeader2, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST_2); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(AtLeast(1)) .WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -779,7 +776,7 @@ TEST_F(SpeechSynthesizerTest, testMediaPlayerFailedToStop) { std::shared_ptr directive2 = AVSDirective::create("", avsMessageHeader2, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST_2); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(AtLeast(1)) .WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( @@ -869,7 +866,7 @@ TEST_F(SpeechSynthesizerTest, testSetStateTimeout) { std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PAYLOAD_TEST, m_attachmentManager, CONTEXT_ID_TEST); - EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, FOCUS_MANAGER_ACTIVITY_ID)) + EXPECT_CALL(*(m_mockFocusManager.get()), acquireChannel(CHANNEL_NAME, _, NAMESPACE_SPEECH_SYNTHESIZER)) .Times(AtLeast(1)) .WillRepeatedly(InvokeWithoutArgs(this, &SpeechSynthesizerTest::wakeOnAcquireChannel)); EXPECT_CALL( diff --git a/CapabilityAgents/System/src/UserInactivityMonitor.cpp b/CapabilityAgents/System/src/UserInactivityMonitor.cpp index 2a1d6958a1..705f0d9922 100644 --- a/CapabilityAgents/System/src/UserInactivityMonitor.cpp +++ b/CapabilityAgents/System/src/UserInactivityMonitor.cpp @@ -121,7 +121,7 @@ void UserInactivityMonitor::sendInactivityReport() { const Pointer::Token payloadKey[] = {{INACTIVITY_EVENT_PAYLOAD_KEY.c_str(), payloadKeySize, kPointerInvalidIndex}}; auto inactiveTime = std::chrono::duration_cast(std::chrono::steady_clock::now() - lastTimeActive); - Pointer(payloadKey, 1).Set(inactivityPayload, inactiveTime.count()); + Pointer(payloadKey, 1).Set(inactivityPayload, static_cast(inactiveTime.count())); std::string inactivityPayloadString; jsonUtils::convertToValue(inactivityPayload, &inactivityPayloadString); diff --git a/CapabilityAgents/TemplateRuntime/include/TemplateRuntime/TemplateRuntime.h b/CapabilityAgents/TemplateRuntime/include/TemplateRuntime/TemplateRuntime.h index 0b4cacb76f..862cdfdd7f 100644 --- a/CapabilityAgents/TemplateRuntime/include/TemplateRuntime/TemplateRuntime.h +++ b/CapabilityAgents/TemplateRuntime/include/TemplateRuntime/TemplateRuntime.h @@ -16,6 +16,7 @@ #ifndef ALEXA_CLIENT_SDK_CAPABILITYAGENTS_TEMPLATERUNTIME_INCLUDE_TEMPLATERUNTIME_TEMPLATERUNTIME_H_ #define ALEXA_CLIENT_SDK_CAPABILITYAGENTS_TEMPLATERUNTIME_INCLUDE_TEMPLATERUNTIME_TEMPLATERUNTIME_H_ +#include #include #include #include @@ -24,9 +25,12 @@ #include #include #include +#include +#include #include #include #include +#include namespace alexaClientSDK { namespace capabilityAgents { @@ -39,6 +43,9 @@ namespace templateRuntime { * CA is an observer to the AudioPlayer and will be synchronizing the @c RenderPlayerInfo directives with the * corresponding @c AudioItem being handled in the @c AudioPlayer. * + * The @c TemplateRuntime CA is also an observer to the @c DialogUXState to determine the end of a interaction so + * that it would know when to clear a @c RenderTemplate displayCard. + * * The clients who are interested in any TemplateRuntime directives can subscribe themselves as an observer, and the * clients will be notified via the TemplateRuntimeObserverInterface. */ @@ -46,6 +53,7 @@ class TemplateRuntime : public avsCommon::avs::CapabilityAgent , public avsCommon::utils::RequiresShutdown , public avsCommon::sdkInterfaces::AudioPlayerObserverInterface + , public avsCommon::sdkInterfaces::DialogUXStateObserverInterface , public std::enable_shared_from_this { public: /** @@ -58,6 +66,7 @@ class TemplateRuntime */ static std::shared_ptr create( std::shared_ptr audioPlayerInterface, + std::shared_ptr focusManager, std::shared_ptr exceptionSender); /** @@ -74,11 +83,22 @@ class TemplateRuntime avsCommon::avs::DirectiveHandlerConfiguration getConfiguration() const override; /// @} + /// @name ChannelObserverInterface Functions + /// @{ + void onFocusChanged(avsCommon::avs::FocusState newFocus) override; + /// @} + /// @name AudioPlayerObserverInterface Functions /// @{ void onPlayerActivityChanged(avsCommon::avs::PlayerActivity state, const Context& context) override; /// @} + /// @name DialogUXStateObserverInterface Functions + /// @{ + void onDialogUXStateChanged( + avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState newState) override; + /// @} + /** * This function adds an observer to @c TemplateRuntime so that it will get notified for renderTemplateCard or * renderPlayerInfoCard. @@ -95,7 +115,46 @@ class TemplateRuntime */ void removeObserver(std::shared_ptr observer); + /** + * This function notifies the @c TemplateRuntime that a displayCard has been cleared from the screen. Upon getting + * this notification, the @c TemplateRuntime will release the visual channel. + */ + void displayCardCleared(); + private: + /** + * This enum provides the state of the @c TemplateRuntime. + */ + enum class State { + /// The @c TemplateRuntime is idle. + IDLE, + + /* + * The @c TemplateRuntime has received a displayCard event is acquiring the visual channel from @c + * FocusManager. + */ + ACQUIRING, + + /* + * The @c TemplateRuntime has focus, either background or foreground, of the channel and has + * notified its observers of a displayCard. @TemplateRuntime will remain in this state until there is a + * timeout, clearCard, or focusChanged(NONE) event. + */ + DISPLAYING, + + /* + * The @c TemplateRuntime has received a timeout or a clearCard event and is releasing the + * channel and has notified its observers to clear the display. + */ + RELEASING, + + /* + * The @c TemplateRuntime has received a displayCard event during releasing of the channel and is trying to + * acquire the visual channel again. + */ + REACQUIRING + }; + /** * Utility structure to correspond a directive with its audioItemId. */ @@ -133,6 +192,7 @@ class TemplateRuntime */ TemplateRuntime( std::shared_ptr audioPlayerInterface, + std::shared_ptr focusManager, std::shared_ptr exceptionSender); // @name RequiresShutdown Functions @@ -185,11 +245,78 @@ class TemplateRuntime */ void executeAudioPlayerInfoUpdates(avsCommon::avs::PlayerActivity state, const Context& context); + /** + * This is an internal function that start or stop the @c m_clearDisplayTimer based on the @c PlayerActivity + * reported by the @c AudioPlayer. + * + * @param state The @c PlayerActivity of the @c AudioPlayer. + */ + void executeAudioPlayerStartTimer(avsCommon::avs::PlayerActivity state); + /** * This function handles the notification of the renderPlayerInfoCard callbacks to all the observers. This function * is intended to be used in the context of @c m_executor worker thread. */ - void executeRenderPlayerInfoCallbacks(); + void executeRenderPlayerInfoCallbacks(bool isClearCard); + + /** + * This function handles the notification of the renderTemplateCard callbacks to all the observers. This function + * is intended to be used in the context of @c m_executor worker thread. + */ + void executeRenderTemplateCallbacks(bool isClearCard); + + /** + * This is an internal function that is called when the state machine is ready to notify the @TemplateRuntime + * observers to display a card. + */ + void executeDisplayCard(); + + /** + * This is an internal function that is called when the state machine is ready to notify the @TemplateRuntime + * observers to clear a card. + */ + void executeClearCard(); + + /** + * This is an internal function to start the @c m_clearDisplayTimer. + * + * @param timeout The period of the timer. + */ + void executeStartTimer(std::chrono::milliseconds timeout); + + /** + * This is an internal function to stop the @c m_clearDisplayTimer. + */ + void executeStopTimer(); + + /** + * This is an internal function to convert the @c State to a string. + */ + std::string stateToString(const TemplateRuntime::State state); + + /** + * This is a state machine function to handle the timer event. + */ + void executeTimerEvent(); + + /** + * This is a state machine function to handle the focus change event. + */ + void executeOnFocusChangedEvent(avsCommon::avs::FocusState newFocus); + + /** + * This is a state machine function to handle the displayCard event. + */ + void executeDisplayCardEvent( + const std::shared_ptr info); + + /** + * This is a state machine function to handle the clearCard event. + */ + void executeCardClearedEvent(); + + /// Timer that is responsible for clearing the display. + avsCommon::utils::timing::Timer m_clearDisplayTimer; /** * @name Executor Thread Variables @@ -198,7 +325,7 @@ class TemplateRuntime * synchronization. */ /// @{ - /// A set of observers to be notified when a @c RenderTemplate or @c RenderPlayerInfo direective is recevied + /// A set of observers to be notified when a @c RenderTemplate or @c RenderPlayerInfo directive is received std::unordered_set> m_observers; /* @@ -216,8 +343,17 @@ class TemplateRuntime /// This is to store the @c AudioPlayerInfo to be passed to the observers in the renderPlayerInfoCard callback. avsCommon::sdkInterfaces::TemplateRuntimeObserverInterface::AudioPlayerInfo m_audioPlayerInfo; + /// The directive corresponding to the RenderTemplate directive. + std::shared_ptr m_lastDisplayedDirective; + /// A flag to check if @c RenderTemplate is the last directive received. bool m_isRenderTemplateLastReceived; + + /// The current focus state of the @c TemplateRuntime on the visual channel. + avsCommon::avs::FocusState m_focus; + + /// The state of the @c TemplateRuntime state machine. + State m_state; /// @} /* @@ -227,6 +363,9 @@ class TemplateRuntime */ std::shared_ptr m_audioPlayerInterface; + /// The @c FocusManager used to manage usage of the visual channel. + std::shared_ptr m_focusManager; + /// This is the worker thread for the @c TemplateRuntime CA. avsCommon::utils::threading::Executor m_executor; }; diff --git a/CapabilityAgents/TemplateRuntime/src/TemplateRuntime.cpp b/CapabilityAgents/TemplateRuntime/src/TemplateRuntime.cpp index 9cabc8c773..1928baec1c 100644 --- a/CapabilityAgents/TemplateRuntime/src/TemplateRuntime.cpp +++ b/CapabilityAgents/TemplateRuntime/src/TemplateRuntime.cpp @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +#include + #include #include @@ -39,14 +41,23 @@ static const std::string TAG{"TemplateRuntime"}; */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) +/// The name of the @c FocusManager channel used by @c TemplateRuntime. +static const std::string CHANNEL_NAME = avsCommon::sdkInterfaces::FocusManagerInterface::VISUAL_CHANNEL_NAME; + /// The namespace for this capability agent. static const std::string NAMESPACE{"TemplateRuntime"}; +/// The name for RenderTemplate directive. +static const std::string RENDER_TEMPLATE{"RenderTemplate"}; + +/// The name for RenderPlayerInfo directive. +static const std::string RENDER_PLAYER_INFO{"RenderPlayerInfo"}; + /// The RenderTemplate directive signature. -static const NamespaceAndName TEMPLATE{NAMESPACE, "RenderTemplate"}; +static const NamespaceAndName TEMPLATE{NAMESPACE, RENDER_TEMPLATE}; /// The RenderPlayerInfo directive signature. -static const NamespaceAndName PLAYER_INFO{NAMESPACE, "RenderPlayerInfo"}; +static const NamespaceAndName PLAYER_INFO{NAMESPACE, RENDER_PLAYER_INFO}; /// Tag for find the AudioItemId in the payload of the RenderPlayerInfo directive static const std::string AUDIO_ITEM_ID_TAG{"audioItemId"}; @@ -54,30 +65,46 @@ static const std::string AUDIO_ITEM_ID_TAG{"audioItemId"}; /// Maximum queue size allowed for m_audioItems. static const size_t MAXIMUM_QUEUE_SIZE{100}; +/// Timeout for clearing the RenderTemplate display card when SpeechSynthesizer is in FINISHED state. +static const std::chrono::milliseconds TTS_FINISHED_TIMEOUT_MS{2000}; + +/// Timeout for clearing the RenderPlayerInfo display card when AudioPlayer is in FINISHED state. +static const std::chrono::milliseconds AUDIO_FINISHED_TIMEOUT_MS{2000}; + +/// Timeout for clearing the RenderPlayerInfo display card when AudioPlayer is in STOPPED/PAUSED state. +static const std::chrono::milliseconds AUDIO_STOPPED_PAUSED_TIMEOUT_MS{60000}; + std::shared_ptr TemplateRuntime::create( std::shared_ptr audioPlayerInterface, + std::shared_ptr focusManager, std::shared_ptr exceptionSender) { if (!audioPlayerInterface) { ACSDK_ERROR(LX("createFailed").d("reason", "nullAudioPlayerInterface")); return nullptr; } + if (!focusManager) { + ACSDK_ERROR(LX("createFailed").d("reason", "nullFocusManager")); + return nullptr; + } + if (!exceptionSender) { ACSDK_ERROR(LX("createFailed").d("reason", "nullExceptionSender")); return nullptr; } - std::shared_ptr templateRuntime(new TemplateRuntime(audioPlayerInterface, exceptionSender)); + std::shared_ptr templateRuntime( + new TemplateRuntime(audioPlayerInterface, focusManager, exceptionSender)); audioPlayerInterface->addObserver(templateRuntime); return templateRuntime; } void TemplateRuntime::handleDirectiveImmediately(std::shared_ptr directive) { - ACSDK_DEBUG9(LX("handleDirectiveImmediately")); + ACSDK_DEBUG5(LX("handleDirectiveImmediately")); preHandleDirective(std::make_shared(directive, nullptr)); } void TemplateRuntime::preHandleDirective(std::shared_ptr info) { - ACSDK_DEBUG9(LX("preHandleDirective")); + ACSDK_DEBUG5(LX("preHandleDirective")); if (!info || !info->directive) { ACSDK_ERROR(LX("preHandleDirectiveFailed").d("reason", "nullDirectiveInfo")); return; @@ -92,7 +119,7 @@ void TemplateRuntime::preHandleDirective(std::shared_ptr info) { } void TemplateRuntime::handleDirective(std::shared_ptr info) { - ACSDK_DEBUG9(LX("handleDirective")); + ACSDK_DEBUG5(LX("handleDirective")); // Do nothing here as directives are handled in the preHandle stage. } @@ -101,30 +128,47 @@ void TemplateRuntime::cancelDirective(std::shared_ptr info) { } DirectiveHandlerConfiguration TemplateRuntime::getConfiguration() const { - ACSDK_DEBUG9(LX("getConfiguration")); + ACSDK_DEBUG5(LX("getConfiguration")); DirectiveHandlerConfiguration configuration; configuration[TEMPLATE] = BlockingPolicy::HANDLE_IMMEDIATELY; configuration[PLAYER_INFO] = BlockingPolicy::HANDLE_IMMEDIATELY; return configuration; } +void TemplateRuntime::onFocusChanged(avsCommon::avs::FocusState newFocus) { + m_executor.submit([this, newFocus]() { executeOnFocusChangedEvent(newFocus); }); +} + void TemplateRuntime::onPlayerActivityChanged(avsCommon::avs::PlayerActivity state, const Context& context) { - ACSDK_DEBUG9(LX("onPlayerActivityChanged")); + ACSDK_DEBUG5(LX("onPlayerActivityChanged")); m_executor.submit([this, state, context]() { - ACSDK_DEBUG0(LX("onPlayerActivityChangedInExecutor")); + ACSDK_DEBUG5(LX("onPlayerActivityChangedInExecutor")); executeAudioPlayerInfoUpdates(state, context); }); } +void TemplateRuntime::onDialogUXStateChanged( + avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState newState) { + ACSDK_DEBUG5(LX("onDialogUXStateChanged").d("state", newState)); + m_executor.submit([this, newState]() { + if (avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState::IDLE == newState && + TemplateRuntime::State::DISPLAYING == m_state) { + if (m_lastDisplayedDirective && m_lastDisplayedDirective->directive->getName() == RENDER_TEMPLATE) { + executeStartTimer(TTS_FINISHED_TIMEOUT_MS); + } + } + }); +} + void TemplateRuntime::addObserver( std::shared_ptr observer) { - ACSDK_DEBUG9(LX("addObserver")); + ACSDK_DEBUG5(LX("addObserver")); if (!observer) { ACSDK_ERROR(LX("addObserver").m("Observer is null.")); return; } m_executor.submit([this, observer]() { - ACSDK_DEBUG0(LX("addObserverInExecutor")); + ACSDK_DEBUG5(LX("addObserverInExecutor")); if (!m_observers.insert(observer).second) { ACSDK_ERROR(LX("addObserverInExecutor").m("Duplicate observer.")); } @@ -133,13 +177,13 @@ void TemplateRuntime::addObserver( void TemplateRuntime::removeObserver( std::shared_ptr observer) { - ACSDK_DEBUG9(LX("removeObserver")); + ACSDK_DEBUG5(LX("removeObserver")); if (!observer) { ACSDK_ERROR(LX("removeObserver").m("Observer is null.")); return; } m_executor.submit([this, observer]() { - ACSDK_DEBUG0(LX("removeObserverInExecutor")); + ACSDK_DEBUG5(LX("removeObserverInExecutor")); if (m_observers.erase(observer) == 0) { ACSDK_WARN(LX("removeObserverInExecutor").m("Nonexistent observer.")); } @@ -148,15 +192,20 @@ void TemplateRuntime::removeObserver( TemplateRuntime::TemplateRuntime( std::shared_ptr audioPlayerInterface, + std::shared_ptr focusManager, std::shared_ptr exceptionSender) : CapabilityAgent{NAMESPACE, exceptionSender}, RequiresShutdown{"TemplateRuntime"}, m_isRenderTemplateLastReceived{false}, - m_audioPlayerInterface{audioPlayerInterface} { + m_focus{FocusState::NONE}, + m_state{TemplateRuntime::State::IDLE}, + m_audioPlayerInterface{audioPlayerInterface}, + m_focusManager{focusManager} { } void TemplateRuntime::doShutdown() { m_executor.shutdown(); + m_focusManager.reset(); m_observers.clear(); m_audioPlayerInterface->removeObserver(shared_from_this()); m_audioPlayerInterface.reset(); @@ -172,6 +221,10 @@ void TemplateRuntime::removeDirective(std::shared_ptr info) { } } +void TemplateRuntime::displayCardCleared() { + m_executor.submit([this]() { executeCardClearedEvent(); }); +} + void TemplateRuntime::setHandlingCompleted(std::shared_ptr info) { if (info && info->result) { info->result->setCompleted(); @@ -180,23 +233,21 @@ void TemplateRuntime::setHandlingCompleted(std::shared_ptr info) } void TemplateRuntime::handleRenderTemplateDirective(std::shared_ptr info) { - ACSDK_DEBUG9(LX("handleRenderTemplateDirective")); + ACSDK_DEBUG5(LX("handleRenderTemplateDirective")); m_executor.submit([this, info]() { - ACSDK_DEBUG0(LX("handleRenderTemplateDirectiveInExecutor")); + ACSDK_DEBUG5(LX("handleRenderTemplateDirectiveInExecutor")); m_isRenderTemplateLastReceived = true; - for (auto observer : m_observers) { - observer->renderTemplateCard(info->directive->getPayload()); - } + executeDisplayCardEvent(info); setHandlingCompleted(info); }); } void TemplateRuntime::handleRenderPlayerInfoDirective(std::shared_ptr info) { - ACSDK_DEBUG9(LX("handleRenderPlayerInfoDirective")); + ACSDK_DEBUG5(LX("handleRenderPlayerInfoDirective")); m_executor.submit([this, info]() { - ACSDK_DEBUG0(LX("handleRenderPlayerInfoDirectiveInExecutor")); + ACSDK_DEBUG5(LX("handleRenderPlayerInfoDirectiveInExecutor")); m_isRenderTemplateLastReceived = false; rapidjson::Document payload; @@ -221,7 +272,7 @@ void TemplateRuntime::handleRenderPlayerInfoDirective(std::shared_ptrgetAudioItemOffset(); - executeRenderPlayerInfoCallbacks(); + executeStopTimer(); + executeDisplayCardEvent(info); } setHandlingCompleted(info); }); @@ -262,7 +314,7 @@ void TemplateRuntime::handleUnknownDirective(std::shared_ptr info } void TemplateRuntime::executeAudioPlayerInfoUpdates(avsCommon::avs::PlayerActivity state, const Context& context) { - ACSDK_DEBUG0(LX("executeAudioPlayerInfoUpdates") + ACSDK_DEBUG5(LX("executeAudioPlayerInfoUpdates") .d("audioItemId", context.audioItemId) .d("offset", context.offset.count()) .d("audioPlayerState", state) @@ -285,6 +337,7 @@ void TemplateRuntime::executeAudioPlayerInfoUpdates(avsCommon::avs::PlayerActivi return; } + auto isStateUpdated = (m_audioPlayerInfo.audioPlayerState != state); m_audioPlayerInfo.audioPlayerState = state; m_audioPlayerInfo.offset = context.offset; if (m_audioItemInExecution.audioItemId != context.audioItemId) { @@ -294,13 +347,13 @@ void TemplateRuntime::executeAudioPlayerInfoUpdates(avsCommon::avs::PlayerActivi auto audioItem = m_audioItems.front(); m_audioItems.pop(); if (audioItem.audioItemId == context.audioItemId) { - ACSDK_DEBUG0(LX("executeAudioPlayerInfoUpdates") + ACSDK_DEBUG3(LX("executeAudioPlayerInfoUpdates") .d("audioItemId", context.audioItemId) .m("Found matching audioItemId in queue.")); m_audioItemInExecution.directive = audioItem.directive; break; } else { - ACSDK_DEBUG0(LX("executeAudioPlayerInfoUpdates") + ACSDK_DEBUG3(LX("executeAudioPlayerInfoUpdates") .d("audioItemId", audioItem.audioItemId) .m("Dropping out-dated audioItemId in queue.")); } @@ -318,22 +371,295 @@ void TemplateRuntime::executeAudioPlayerInfoUpdates(avsCommon::avs::PlayerActivi /* * If the AudioPlayer notifies a PLAYING state before the RenderPlayerInfo with the corresponding * audioItemId is received, this function will also be called but the m_audioItemInExecution.directive - * will be set to nullptr and the callback to the observers will be filtered out by the nullptr - * check in executeRenderPlayerInfoCallbacks(). + * will be set to nullptr. So we need to do a nullptr check here to make sure there is a RenderPlayerInfo + * displayCard to display.. */ - executeRenderPlayerInfoCallbacks(); + if (m_audioItemInExecution.directive) { + if (isStateUpdated) { + executeAudioPlayerStartTimer(state); + } + executeDisplayCardEvent(m_audioItemInExecution.directive); + } } -void TemplateRuntime::executeRenderPlayerInfoCallbacks() { - ACSDK_DEBUG0(LX("executeRenderPlayerInfoCallbacks")); - if (m_audioItemInExecution.directive) { - for (auto& observer : m_observers) { +void TemplateRuntime::executeAudioPlayerStartTimer(avsCommon::avs::PlayerActivity state) { + if (avsCommon::avs::PlayerActivity::PLAYING == state) { + executeStopTimer(); + } else if (avsCommon::avs::PlayerActivity::PAUSED == state || avsCommon::avs::PlayerActivity::STOPPED == state) { + executeStartTimer(AUDIO_STOPPED_PAUSED_TIMEOUT_MS); + } else if (avsCommon::avs::PlayerActivity::FINISHED == state) { + executeStartTimer(AUDIO_FINISHED_TIMEOUT_MS); + } +} + +void TemplateRuntime::executeRenderPlayerInfoCallbacks(bool isClearCard) { + ACSDK_DEBUG3(LX("executeRenderPlayerInfoCallbacks").d("isClearCard", isClearCard ? "True" : "False")); + for (auto& observer : m_observers) { + if (isClearCard) { + observer->clearPlayerInfoCard(); + } else { observer->renderPlayerInfoCard( - m_audioItemInExecution.directive->directive->getPayload(), m_audioPlayerInfo); + m_audioItemInExecution.directive->directive->getPayload(), m_audioPlayerInfo, m_focus); + } + } +} + +void TemplateRuntime::executeRenderTemplateCallbacks(bool isClearCard) { + ACSDK_DEBUG3(LX("executeRenderTemplateCallbacks").d("isClear", isClearCard ? "True" : "False")); + for (auto& observer : m_observers) { + if (isClearCard) { + observer->clearTemplateCard(); + } else { + observer->renderTemplateCard(m_lastDisplayedDirective->directive->getPayload(), m_focus); + } + } +} + +void TemplateRuntime::executeDisplayCard() { + if (m_lastDisplayedDirective) { + if (m_lastDisplayedDirective->directive->getName() == RENDER_TEMPLATE) { + executeStopTimer(); + executeRenderTemplateCallbacks(false); + } else { + executeRenderPlayerInfoCallbacks(false); + } + } +} + +void TemplateRuntime::executeClearCard() { + if (m_lastDisplayedDirective) { + if (m_lastDisplayedDirective->directive->getName() == RENDER_TEMPLATE) { + executeRenderTemplateCallbacks(true); + } else { + executeRenderPlayerInfoCallbacks(true); } } } +void TemplateRuntime::executeStartTimer(std::chrono::milliseconds timeout) { + if (TemplateRuntime::State::DISPLAYING == m_state) { + ACSDK_DEBUG3(LX("executeStartTimer").d("timeoutInMilliseconds", timeout.count())); + m_clearDisplayTimer.start(timeout, [this] { m_executor.submit([this] { executeTimerEvent(); }); }); + } +} + +void TemplateRuntime::executeStopTimer() { + ACSDK_DEBUG3(LX("executeStopTimer")); + m_clearDisplayTimer.stop(); +} + +/* + * A state machine is used to acquire and release the visual channel from the visual @c FocusManager. The state machine + * has five @c State, and four events as listed below: + * + * displayCard - This event happens when the TempateRuntime is ready to notify its observers to display a + * displayCard. + * + * focusChanged - This event happens when the @c FocusManager notifies a change in @c FocusState in the visual + * channel. + * + * timer - This event happens when m_clearDisplayTimer expires and needs to notify its observers to clear the + * displayCard. + * + * cardCleared - This event happens when @c displayCardCleared() is called to notify @c TemplateRuntime the device has + * cleared the screen. + * + * Each state transition may result in one or more of the following actions: + * (A) Acquire channel + * (B) Release channel + * (C) Notify observers to display displayCard + * (D) Notify observers to clear displayCard + * (E) Log error about unexpected focusChanged event. + * + * Below is the state table illustrating the state transition and its action. NC means no change in state. + * + * E V E N T S + * ----------------------------------------------------------------------------------------- + * Current State | displayCard | timer | focusChanged::NONE | focusChanged::FG/BG | cardCleared + * -------------------------------------------------------------------------------------------------------- + * | IDLE | ACQUIRING(A) | NC | NC | RELEASING(B&E) | NC + * | ACQUIRING | NC | NC | IDLE(E) | DISPLAYING(C) | NC + * | DISPLAYING | NC(C) | RELEASING(B&D) | IDLE(D) | DISPLAYING(C) | RELEASING(B) + * | RELEASING | REACQUIRING | NC | IDLE | NC(B&E) | NC + * | REACQUIRING | NC | NC | ACQUIRING(A) | RELEASING(B&E) | NC + * -------------------------------------------------------------------------------------------------------- + * + */ + +std::string TemplateRuntime::stateToString(const TemplateRuntime::State state) { + switch (state) { + case TemplateRuntime::State::IDLE: + return "IDLE"; + case TemplateRuntime::State::ACQUIRING: + return "ACQUIRING"; + case TemplateRuntime::State::DISPLAYING: + return "DISPLAYING"; + case TemplateRuntime::State::RELEASING: + return "RELEASING"; + case TemplateRuntime::State::REACQUIRING: + return "REACQUIRING"; + } + return "UNKNOWN"; +} + +void TemplateRuntime::executeTimerEvent() { + State nextState = m_state; + + switch (m_state) { + case TemplateRuntime::State::DISPLAYING: + executeClearCard(); + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + nextState = TemplateRuntime::State::RELEASING; + break; + + case TemplateRuntime::State::IDLE: + case TemplateRuntime::State::ACQUIRING: + case TemplateRuntime::State::RELEASING: + case TemplateRuntime::State::REACQUIRING: + // Do Nothing. + break; + } + ACSDK_DEBUG3( + LX("executeTimerEvent").d("prevState", stateToString(m_state)).d("nextState", stateToString(nextState))); + m_state = nextState; +} + +void TemplateRuntime::executeOnFocusChangedEvent(avsCommon::avs::FocusState newFocus) { + ACSDK_DEBUG5(LX("executeOnFocusChangedEvent").d("prevFocus", m_focus).d("newFocus", newFocus)); + + bool weirdFocusState = false; + State nextState = m_state; + m_focus = newFocus; + + switch (m_state) { + case TemplateRuntime::State::IDLE: + // This is weird. We shouldn't be getting any focus updates in Idle. + switch (newFocus) { + case FocusState::FOREGROUND: + case FocusState::BACKGROUND: + weirdFocusState = true; + break; + case FocusState::NONE: + // Do nothing. + break; + } + break; + case TemplateRuntime::State::ACQUIRING: + switch (newFocus) { + case FocusState::FOREGROUND: + case FocusState::BACKGROUND: + executeDisplayCard(); + nextState = TemplateRuntime::State::DISPLAYING; + break; + case FocusState::NONE: + ACSDK_ERROR(LX("executeOnFocusChangedEvent") + .d("prevState", stateToString(m_state)) + .d("nextFocus", newFocus) + .m("Unexpected focus state event.")); + nextState = TemplateRuntime::State::IDLE; + break; + } + break; + case TemplateRuntime::State::DISPLAYING: + switch (newFocus) { + case FocusState::FOREGROUND: + case FocusState::BACKGROUND: + executeDisplayCard(); + break; + case FocusState::NONE: + executeClearCard(); + nextState = TemplateRuntime::State::IDLE; + break; + } + break; + case TemplateRuntime::State::RELEASING: + switch (newFocus) { + case FocusState::FOREGROUND: + case FocusState::BACKGROUND: + weirdFocusState = true; + break; + case FocusState::NONE: + nextState = TemplateRuntime::State::IDLE; + break; + } + break; + case TemplateRuntime::State::REACQUIRING: + switch (newFocus) { + case FocusState::FOREGROUND: + case FocusState::BACKGROUND: + weirdFocusState = true; + break; + case FocusState::NONE: + m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), NAMESPACE); + nextState = TemplateRuntime::State::ACQUIRING; + break; + } + break; + } + if (weirdFocusState) { + ACSDK_ERROR(LX("executeOnFocusChangedEvent") + .d("prevState", stateToString(m_state)) + .d("nextFocus", newFocus) + .m("Unexpected focus state event.")); + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + nextState = TemplateRuntime::State::RELEASING; + } + ACSDK_DEBUG3(LX("executeOnFocusChangedEvent") + .d("prevState", stateToString(m_state)) + .d("nextState", stateToString(nextState))); + m_state = nextState; +} + +void TemplateRuntime::executeDisplayCardEvent( + const std::shared_ptr info) { + State nextState = m_state; + m_lastDisplayedDirective = info; + + switch (m_state) { + case TemplateRuntime::State::IDLE: + m_focusManager->acquireChannel(CHANNEL_NAME, shared_from_this(), NAMESPACE); + nextState = TemplateRuntime::State::ACQUIRING; + break; + case TemplateRuntime::State::ACQUIRING: + // Do Nothing. + break; + case TemplateRuntime::State::DISPLAYING: + executeDisplayCard(); + nextState = TemplateRuntime::State::DISPLAYING; + break; + case TemplateRuntime::State::RELEASING: + nextState = TemplateRuntime::State::REACQUIRING; + break; + case TemplateRuntime::State::REACQUIRING: + // Do Nothing. + break; + } + ACSDK_DEBUG3( + LX("executeDisplayCardEvent").d("prevState", stateToString(m_state)).d("nextState", stateToString(nextState))); + m_state = nextState; +} + +void TemplateRuntime::executeCardClearedEvent() { + State nextState = m_state; + switch (m_state) { + case TemplateRuntime::State::IDLE: + case TemplateRuntime::State::ACQUIRING: + // Do Nothing. + break; + case TemplateRuntime::State::DISPLAYING: + m_focusManager->releaseChannel(CHANNEL_NAME, shared_from_this()); + nextState = TemplateRuntime::State::RELEASING; + break; + case TemplateRuntime::State::RELEASING: + case TemplateRuntime::State::REACQUIRING: + // Do Nothing. + break; + } + ACSDK_DEBUG3( + LX("executeCardClearedEvent").d("prevState", stateToString(m_state)).d("nextState", stateToString(nextState))); + m_state = nextState; +} + } // namespace templateRuntime } // namespace capabilityAgents } // namespace alexaClientSDK diff --git a/CapabilityAgents/TemplateRuntime/test/TemplateRuntimeTest.cpp b/CapabilityAgents/TemplateRuntime/test/TemplateRuntimeTest.cpp index e20fa92f56..fb3bad7eae 100644 --- a/CapabilityAgents/TemplateRuntime/test/TemplateRuntimeTest.cpp +++ b/CapabilityAgents/TemplateRuntime/test/TemplateRuntimeTest.cpp @@ -14,6 +14,7 @@ */ /// @file TemplateRuntimeTest +#include #include #include @@ -25,6 +26,7 @@ #include #include #include +#include #include #include #include @@ -47,6 +49,12 @@ using namespace ::testing; /// Timeout when waiting for futures to be set. static std::chrono::milliseconds TIMEOUT(1000); +/// Timeout when waiting for clearTemplateCard. +static std::chrono::milliseconds TEMPLATE_TIMEOUT(5000); + +/// Timeout when waiting for clearTemplateCard. +static std::chrono::milliseconds PLAYER_FINISHED_TIMEOUT(5000); + /// The namespace for this capability agent. static const std::string NAMESPACE{"TemplateRuntime"}; @@ -113,10 +121,15 @@ class MockAudioPlayer : public AudioPlayerInterface { class MockGui : public TemplateRuntimeObserverInterface { public: - MOCK_METHOD1(renderTemplateCard, void(const std::string& jsonPayload)); - MOCK_METHOD2( + MOCK_METHOD2(renderTemplateCard, void(const std::string& jsonPayload, avsCommon::avs::FocusState focusState)); + MOCK_METHOD0(clearTemplateCard, void()); + MOCK_METHOD3( renderPlayerInfoCard, - void(const std::string& jsonPayload, TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo)); + void( + const std::string& jsonPayload, + TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo, + avsCommon::avs::FocusState focusState)); + MOCK_METHOD0(clearPlayerInfoCard, void()); }; /// Test harness for @c TemplateRuntime class. @@ -137,6 +150,15 @@ class TemplateRuntimeTest : public ::testing::Test { /// Function to set the promise and wake @c m_wakeRenderPlayerInfoCardFuture. void wakeOnRenderPlayerInfoCard(); + /// Function to set the promise and wake @c m_wakeClearTemplateCardFuture. + void wakeOnClearTemplateCard(); + + /// Function to set the promise and wake @c m_wakeClearPlayerInfoCardFuture. + void wakeOnClearPlayerInfoCard(); + + /// Function to set the promise and wake @c m_wakeReleaseChannelFuture. + void wakeOnReleaseChannel(); + /// A constructor which initializes the promises and futures needed for the test class. TemplateRuntimeTest() : m_wakeSetCompletedPromise{}, @@ -144,7 +166,13 @@ class TemplateRuntimeTest : public ::testing::Test { m_wakeRenderTemplateCardPromise{}, m_wakeRenderTemplateCardFuture{m_wakeRenderTemplateCardPromise.get_future()}, m_wakeRenderPlayerInfoCardPromise{}, - m_wakeRenderPlayerInfoCardFuture{m_wakeRenderPlayerInfoCardPromise.get_future()} { + m_wakeRenderPlayerInfoCardFuture{m_wakeRenderPlayerInfoCardPromise.get_future()}, + m_wakeClearTemplateCardPromise{}, + m_wakeClearTemplateCardFuture{m_wakeClearTemplateCardPromise.get_future()}, + m_wakeClearPlayerInfoCardPromise{}, + m_wakeClearPlayerInfoCardFuture{m_wakeClearPlayerInfoCardPromise.get_future()}, + m_wakeReleaseChannelPromise{}, + m_wakeReleaseChannelFuture{m_wakeReleaseChannelPromise.get_future()} { } protected: @@ -166,6 +194,24 @@ class TemplateRuntimeTest : public ::testing::Test { /// Future to synchronize directive handling with RenderPlayerInfoCard callback. std::future m_wakeRenderPlayerInfoCardFuture; + /// Promise to synchronize ClearTemplateCard callback. + std::promise m_wakeClearTemplateCardPromise; + + /// Future to synchronize ClearTemplateCard callback. + std::future m_wakeClearTemplateCardFuture; + + /// Promise to synchronize ClearPlayerInfoCard callback. + std::promise m_wakeClearPlayerInfoCardPromise; + + /// Future to synchronize ClearPlayerInfoCard callback. + std::future m_wakeClearPlayerInfoCardFuture; + + /// Promise to synchronize releaseChannel calls. + std::promise m_wakeReleaseChannelPromise; + + /// Future to synchronize releaseChannel calls. + std::future m_wakeReleaseChannelFuture; + /// A nice mock for the AudioPlayerInterface calls. std::shared_ptr> m_mockAudioPlayerInterface; @@ -175,6 +221,9 @@ class TemplateRuntimeTest : public ::testing::Test { /// A strict mock that allows the test to strictly monitor the handling of directives. std::unique_ptr> m_mockDirectiveHandlerResult; + /// @c FocusManager to request focus to the Visual channel. + std::shared_ptr m_mockFocusManager; + /// A strict mock to allow testing of the observer callback. std::shared_ptr> m_mockGui; @@ -185,8 +234,24 @@ class TemplateRuntimeTest : public ::testing::Test { void TemplateRuntimeTest::SetUp() { m_mockExceptionSender = std::make_shared>(); m_mockDirectiveHandlerResult = make_unique>(); + m_mockFocusManager = std::make_shared>(); m_mockAudioPlayerInterface = std::make_shared>(); m_mockGui = std::make_shared>(); + m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockFocusManager, m_mockExceptionSender); + m_templateRuntime->addObserver(m_mockGui); + + ON_CALL(*m_mockFocusManager, acquireChannel(_, _, _)).WillByDefault(InvokeWithoutArgs([this] { + m_templateRuntime->onFocusChanged(avsCommon::avs::FocusState::FOREGROUND); + return true; + })); + + ON_CALL(*m_mockFocusManager, releaseChannel(_, _)).WillByDefault(InvokeWithoutArgs([this] { + auto releaseChannelSuccess = std::make_shared>(); + std::future returnValue = releaseChannelSuccess->get_future(); + m_templateRuntime->onFocusChanged(avsCommon::avs::FocusState::NONE); + releaseChannelSuccess->set_value(true); + return returnValue; + })); } void TemplateRuntimeTest::TearDown() { @@ -208,20 +273,40 @@ void TemplateRuntimeTest::wakeOnRenderPlayerInfoCard() { m_wakeRenderPlayerInfoCardPromise.set_value(); } +void TemplateRuntimeTest::wakeOnClearTemplateCard() { + m_wakeClearTemplateCardPromise.set_value(); +} + +void TemplateRuntimeTest::wakeOnClearPlayerInfoCard() { + m_wakeClearPlayerInfoCardPromise.set_value(); +} + +void TemplateRuntimeTest::wakeOnReleaseChannel() { + m_wakeReleaseChannelPromise.set_value(); +} + /** * Tests creating the TemplateRuntime with a null audioPlayerInterface. */ TEST_F(TemplateRuntimeTest, testNullAudioPlayerInterface) { - m_templateRuntime = TemplateRuntime::create(nullptr, m_mockExceptionSender); - ASSERT_EQ(m_templateRuntime, nullptr); + auto templateRuntime = TemplateRuntime::create(nullptr, m_mockFocusManager, m_mockExceptionSender); + ASSERT_EQ(templateRuntime, nullptr); +} + +/** + * Tests creating the TemplateRuntime with a null focusManagerInterface. + */ +TEST_F(TemplateRuntimeTest, testNullFocusManagerInterface) { + auto templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, nullptr, m_mockExceptionSender); + ASSERT_EQ(templateRuntime, nullptr); } /** * Tests creating the TemplateRuntime with a null exceptionSender. */ TEST_F(TemplateRuntimeTest, testNullExceptionSender) { - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, nullptr); - ASSERT_EQ(m_templateRuntime, nullptr); + auto templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockFocusManager, nullptr); + ASSERT_EQ(templateRuntime, nullptr); } /** @@ -229,19 +314,19 @@ TEST_F(TemplateRuntimeTest, testNullExceptionSender) { * successfully remove itself with the AudioPlayer during shutdown. */ TEST_F(TemplateRuntimeTest, testAudioPlayerAddRemoveObserver) { - EXPECT_CALL(*m_mockAudioPlayerInterface, addObserver(NotNull())).Times(Exactly(1)); - EXPECT_CALL(*m_mockAudioPlayerInterface, removeObserver(NotNull())).Times(Exactly(1)); - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); + auto mockAudioPlayerInterface = std::make_shared>(); + auto mockExceptionSender = std::make_shared>(); + auto mockFocusManager = std::make_shared>(); + EXPECT_CALL(*mockAudioPlayerInterface, addObserver(NotNull())).Times(Exactly(1)); + EXPECT_CALL(*mockAudioPlayerInterface, removeObserver(NotNull())).Times(Exactly(1)); + auto templateRuntime = TemplateRuntime::create(mockAudioPlayerInterface, mockFocusManager, mockExceptionSender); + templateRuntime->shutdown(); } /** * Tests unknown Directive. Expect that the sendExceptionEncountered and setFailed will be called. */ TEST_F(TemplateRuntimeTest, testUnknownDirective) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(NAMESPACE, UNKNOWN_DIRECTIVE, MESSAGE_ID); @@ -257,27 +342,33 @@ TEST_F(TemplateRuntimeTest, testUnknownDirective) { } /** - * Tests RenderTemplate Directive. Expect that the renderTemplateCard callback will be called. + * Tests RenderTemplate Directive. Expect that the renderTemplateCard callback will be called and clearTemplateCard will + * be called after 2s after DialogUXState is changed to IDLE state. */ TEST_F(TemplateRuntimeTest, testRenderTemplateDirective) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(TEMPLATE.nameSpace, TEMPLATE.name, MESSAGE_ID); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, TEMPLATE_PAYLOAD, attachmentManager, ""); - EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD)).Times(Exactly(1)); + EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD, _)) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderTemplateCard)); EXPECT_CALL(*m_mockDirectiveHandlerResult, setCompleted()) .Times(Exactly(1)) .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnSetCompleted)); + EXPECT_CALL(*m_mockGui, clearTemplateCard()) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnClearTemplateCard)); m_templateRuntime->CapabilityAgent::preHandleDirective(directive, std::move(m_mockDirectiveHandlerResult)); m_templateRuntime->CapabilityAgent::handleDirective(MESSAGE_ID); m_wakeSetCompletedFuture.wait_for(TIMEOUT); + m_wakeRenderTemplateCardFuture.wait_for(TIMEOUT); + m_templateRuntime->onDialogUXStateChanged( + avsCommon::sdkInterfaces::DialogUXStateObserverInterface::DialogUXState::IDLE); + m_wakeClearTemplateCardFuture.wait_for(TEMPLATE_TIMEOUT); } /** @@ -285,17 +376,13 @@ TEST_F(TemplateRuntimeTest, testRenderTemplateDirective) { * callback will be called. */ TEST_F(TemplateRuntimeTest, testHandleDirectiveImmediately) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(TEMPLATE.nameSpace, TEMPLATE.name, MESSAGE_ID); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, TEMPLATE_PAYLOAD, attachmentManager, ""); - EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD)) + EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD, _)) .Times(Exactly(1)) .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderTemplateCard)); @@ -305,13 +392,10 @@ TEST_F(TemplateRuntimeTest, testHandleDirectiveImmediately) { /** * Tests RenderTemplate Directive received before the corresponding AudioPlayer call. Expect - * that the renderTemplateCard callback will be called. + * that the renderTemplateCard callback will be called and clearPlayerInfoCard will be called after 2s after Audio State + * is changed to FINISHED state. */ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveBefore) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); @@ -322,18 +406,19 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveBefore) { EXPECT_CALL(*m_mockDirectiveHandlerResult, setCompleted()) .Times(Exactly(1)) .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnSetCompleted)); - EXPECT_CALL(*m_mockGui, renderTemplateCard(_)).Times(Exactly(0)); + EXPECT_CALL(*m_mockGui, renderTemplateCard(_, _)).Times(Exactly(0)); // do not expect renderPlayerInfo card call until AudioPlayer notify with the correct audioItemId - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(_, _)).Times(Exactly(0)); + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(_, _, _)).Times(Exactly(0)); m_templateRuntime->CapabilityAgent::preHandleDirective(directive, std::move(m_mockDirectiveHandlerResult)); m_templateRuntime->CapabilityAgent::handleDirective(MESSAGE_ID); m_wakeSetCompletedFuture.wait_for(TIMEOUT); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) - .Times(Exactly(1)) - .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderPlayerInfoCard)); + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) + .Times(Exactly(2)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderPlayerInfoCard)) + .WillOnce(InvokeWithoutArgs([] {})); AudioPlayerObserverInterface::Context context; context.audioItemId = AUDIO_ITEM_ID; @@ -341,6 +426,13 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveBefore) { m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::PLAYING, context); m_wakeRenderPlayerInfoCardFuture.wait_for(TIMEOUT); + + EXPECT_CALL(*m_mockGui, clearPlayerInfoCard()) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnClearPlayerInfoCard)); + + m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::FINISHED, context); + m_wakeClearPlayerInfoCardFuture.wait_for(PLAYER_FINISHED_TIMEOUT); } /** @@ -348,17 +440,13 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveBefore) { * that the renderTemplateCard callback will be called. */ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAfter) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PLAYERINFO_PAYLOAD, attachmentManager, ""); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) .Times(Exactly(1)) .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderPlayerInfoCard)); EXPECT_CALL(*m_mockDirectiveHandlerResult, setCompleted()) @@ -381,10 +469,6 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAfter) { * sendExceptionEncountered and setFailed will be called. */ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveWithoutAudioItemId) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); @@ -405,10 +489,6 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveWithoutAudioItemId) { * sendExceptionEncountered and setFailed will be called. */ TEST_F(TemplateRuntimeTest, testMalformedRenderPlayerInfoDirective) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); @@ -430,17 +510,13 @@ TEST_F(TemplateRuntimeTest, testMalformedRenderPlayerInfoDirective) { * the AudioPlayer notified the handling of AUDIO_ITEM_ID later. */ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveDifferentAudioItemId) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); std::shared_ptr directive = AVSDirective::create("", avsMessageHeader, PLAYERINFO_PAYLOAD, attachmentManager, ""); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)).Times(Exactly(0)); + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)).Times(Exactly(0)); EXPECT_CALL(*m_mockDirectiveHandlerResult, setCompleted()) .Times(Exactly(1)) .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnSetCompleted)); @@ -453,7 +529,7 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveDifferentAudioItemId) { m_templateRuntime->CapabilityAgent::handleDirective(MESSAGE_ID); m_wakeSetCompletedFuture.wait_for(TIMEOUT); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) .Times(Exactly(1)) .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderPlayerInfoCard)); @@ -469,10 +545,6 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveDifferentAudioItemId) { * AudioPlayerObserverInterface. */ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAudioStateUpdate) { - // Create TemplateRuntime and add m_mockGui and its observer. - m_templateRuntime = TemplateRuntime::create(m_mockAudioPlayerInterface, m_mockExceptionSender); - m_templateRuntime->addObserver(m_mockGui); - // Create Directive. auto attachmentManager = std::make_shared>(); auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); @@ -495,15 +567,16 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAudioStateUpdate) { std::promise wakePlayPromise; std::future wakePlayFuture = wakePlayPromise.get_future(); context.offset = std::chrono::milliseconds(100); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) .Times(Exactly(1)) - .WillOnce(Invoke( - [&wakePlayPromise, context]( - const std::string& jsonPayload, TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo) { - EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::PLAYING); - EXPECT_EQ(audioPlayerInfo.offset, context.offset); - wakePlayPromise.set_value(); - })); + .WillOnce(Invoke([&wakePlayPromise, context]( + const std::string& jsonPayload, + TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo, + avsCommon::avs::FocusState focusState) { + EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::PLAYING); + EXPECT_EQ(audioPlayerInfo.offset, context.offset); + wakePlayPromise.set_value(); + })); m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::PLAYING, context); wakePlayFuture.wait_for(TIMEOUT); @@ -511,15 +584,16 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAudioStateUpdate) { std::promise wakePausePromise; std::future wakePauseFuture = wakePausePromise.get_future(); context.offset = std::chrono::milliseconds(200); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) .Times(Exactly(1)) - .WillOnce(Invoke( - [&wakePausePromise, context]( - const std::string& jsonPayload, TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo) { - EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::PAUSED); - EXPECT_EQ(audioPlayerInfo.offset, context.offset); - wakePausePromise.set_value(); - })); + .WillOnce(Invoke([&wakePausePromise, context]( + const std::string& jsonPayload, + TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo, + avsCommon::avs::FocusState focusState) { + EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::PAUSED); + EXPECT_EQ(audioPlayerInfo.offset, context.offset); + wakePausePromise.set_value(); + })); m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::PAUSED, context); wakePauseFuture.wait_for(TIMEOUT); @@ -527,15 +601,16 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAudioStateUpdate) { std::promise wakeStopPromise; std::future wakeStopFuture = wakeStopPromise.get_future(); context.offset = std::chrono::milliseconds(300); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) .Times(Exactly(1)) - .WillOnce(Invoke( - [&wakeStopPromise, context]( - const std::string& jsonPayload, TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo) { - EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::STOPPED); - EXPECT_EQ(audioPlayerInfo.offset, context.offset); - wakeStopPromise.set_value(); - })); + .WillOnce(Invoke([&wakeStopPromise, context]( + const std::string& jsonPayload, + TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo, + avsCommon::avs::FocusState focusState) { + EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::STOPPED); + EXPECT_EQ(audioPlayerInfo.offset, context.offset); + wakeStopPromise.set_value(); + })); m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::STOPPED, context); wakeStopFuture.wait_for(TIMEOUT); @@ -543,19 +618,129 @@ TEST_F(TemplateRuntimeTest, testRenderPlayerInfoDirectiveAudioStateUpdate) { std::promise wakeFinishPromise; std::future wakeFinishFuture = wakeFinishPromise.get_future(); context.offset = std::chrono::milliseconds(400); - EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _)) + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) .Times(Exactly(1)) - .WillOnce(Invoke( - [&wakeFinishPromise, context]( - const std::string& jsonPayload, TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo) { - EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::FINISHED); - EXPECT_EQ(audioPlayerInfo.offset, context.offset); - wakeFinishPromise.set_value(); - })); + .WillOnce(Invoke([&wakeFinishPromise, context]( + const std::string& jsonPayload, + TemplateRuntimeObserverInterface::AudioPlayerInfo audioPlayerInfo, + avsCommon::avs::FocusState focusState) { + EXPECT_EQ(audioPlayerInfo.audioPlayerState, avsCommon::avs::PlayerActivity::FINISHED); + EXPECT_EQ(audioPlayerInfo.offset, context.offset); + wakeFinishPromise.set_value(); + })); m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::FINISHED, context); wakeFinishFuture.wait_for(TIMEOUT); } +/** + * Tests that if focus is changed to none, the clearTemplateCard() will be called. + */ +TEST_F(TemplateRuntimeTest, testFocusNone) { + // Create Directive. + auto attachmentManager = std::make_shared>(); + auto avsMessageHeader = std::make_shared(TEMPLATE.nameSpace, TEMPLATE.name, MESSAGE_ID); + std::shared_ptr directive = + AVSDirective::create("", avsMessageHeader, TEMPLATE_PAYLOAD, attachmentManager, ""); + + EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD, _)) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderTemplateCard)); + EXPECT_CALL(*m_mockDirectiveHandlerResult, setCompleted()) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnSetCompleted)); + EXPECT_CALL(*m_mockGui, clearTemplateCard()) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnClearTemplateCard)); + + m_templateRuntime->CapabilityAgent::preHandleDirective(directive, std::move(m_mockDirectiveHandlerResult)); + m_templateRuntime->CapabilityAgent::handleDirective(MESSAGE_ID); + m_wakeSetCompletedFuture.wait_for(TIMEOUT); + m_wakeRenderTemplateCardFuture.wait_for(TIMEOUT); + m_templateRuntime->onFocusChanged(FocusState::NONE); + m_wakeClearTemplateCardFuture.wait_for(TIMEOUT); +} + +/** + * Tests that if displayCardCleared() is called, the clearTemplateCard() will not be called. + */ +TEST_F(TemplateRuntimeTest, testDisplayCardCleared) { + // Create Directive. + auto attachmentManager = std::make_shared>(); + auto avsMessageHeader = std::make_shared(TEMPLATE.nameSpace, TEMPLATE.name, MESSAGE_ID); + std::shared_ptr directive = + AVSDirective::create("", avsMessageHeader, TEMPLATE_PAYLOAD, attachmentManager, ""); + + EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD, _)) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderTemplateCard)); + EXPECT_CALL(*m_mockDirectiveHandlerResult, setCompleted()) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnSetCompleted)); + EXPECT_CALL(*m_mockGui, clearTemplateCard()).Times(Exactly(0)); + EXPECT_CALL(*m_mockFocusManager, releaseChannel(_, _)).Times(Exactly(1)).WillOnce(InvokeWithoutArgs([this] { + auto releaseChannelSuccess = std::make_shared>(); + std::future returnValue = releaseChannelSuccess->get_future(); + m_templateRuntime->onFocusChanged(avsCommon::avs::FocusState::NONE); + releaseChannelSuccess->set_value(true); + wakeOnReleaseChannel(); + return returnValue; + })); + + m_templateRuntime->CapabilityAgent::preHandleDirective(directive, std::move(m_mockDirectiveHandlerResult)); + m_templateRuntime->CapabilityAgent::handleDirective(MESSAGE_ID); + m_wakeSetCompletedFuture.wait_for(TIMEOUT); + m_wakeRenderTemplateCardFuture.wait_for(TIMEOUT); + m_templateRuntime->displayCardCleared(); + m_wakeReleaseChannelFuture.wait_for(TIMEOUT); +} + +/** + * Tests that if another displayCard event is sent before channel's focus is set to none, the state machine would + * transition to REACQUIRING state and acquireChannel again to display the card. + */ +TEST_F(TemplateRuntimeTest, testReacquireChannel) { + // Create RenderPlayerInfo Directive and wait until PlayerInfo card is displayed. + auto attachmentManager = std::make_shared>(); + auto avsMessageHeader = std::make_shared(PLAYER_INFO.nameSpace, PLAYER_INFO.name, MESSAGE_ID); + std::shared_ptr directive = + AVSDirective::create("", avsMessageHeader, PLAYERINFO_PAYLOAD, attachmentManager, ""); + + EXPECT_CALL(*m_mockGui, renderPlayerInfoCard(PLAYERINFO_PAYLOAD, _, _)) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderPlayerInfoCard)); + + AudioPlayerObserverInterface::Context context; + context.audioItemId = AUDIO_ITEM_ID; + context.offset = TIMEOUT; + m_templateRuntime->onPlayerActivityChanged(avsCommon::avs::PlayerActivity::PLAYING, context); + m_templateRuntime->handleDirectiveImmediately(directive); + m_wakeRenderPlayerInfoCardFuture.wait_for(TIMEOUT); + + // Send displayCardCleared() to clear card, before setting focus to NONE, send another TemplateCard. + EXPECT_CALL(*m_mockFocusManager, releaseChannel(_, _)).Times(Exactly(1)).WillOnce(InvokeWithoutArgs([this] { + auto releaseChannelSuccess = std::make_shared>(); + std::future returnValue = releaseChannelSuccess->get_future(); + releaseChannelSuccess->set_value(true); + wakeOnReleaseChannel(); + return returnValue; + })); + m_templateRuntime->displayCardCleared(); + m_wakeReleaseChannelFuture.wait_for(TIMEOUT); + + // Create RenderTemplate Directive and see if channel is reacquire correctly. + auto avsMessageHeader1 = std::make_shared(TEMPLATE.nameSpace, TEMPLATE.name, MESSAGE_ID); + std::shared_ptr directive1 = + AVSDirective::create("", avsMessageHeader1, TEMPLATE_PAYLOAD, attachmentManager, ""); + + EXPECT_CALL(*m_mockGui, renderTemplateCard(TEMPLATE_PAYLOAD, _)) + .Times(Exactly(1)) + .WillOnce(InvokeWithoutArgs(this, &TemplateRuntimeTest::wakeOnRenderTemplateCard)); + + m_templateRuntime->handleDirectiveImmediately(directive1); + m_templateRuntime->onFocusChanged(avsCommon::avs::FocusState::NONE); + m_wakeRenderTemplateCardFuture.wait_for(TIMEOUT); +} + } // namespace test } // namespace templateRuntime } // namespace capabilityAgents diff --git a/CertifiedSender/include/CertifiedSender/CertifiedSender.h b/CertifiedSender/include/CertifiedSender/CertifiedSender.h index b99dfd52a8..570819b698 100644 --- a/CertifiedSender/include/CertifiedSender/CertifiedSender.h +++ b/CertifiedSender/include/CertifiedSender/CertifiedSender.h @@ -24,6 +24,8 @@ #include #include #include +#include +#include #include #include @@ -53,7 +55,8 @@ static const int CERTIFIED_SENDER_QUEUE_SIZE_HARD_LIMIT = 50; class CertifiedSender : public avsCommon::utils::RequiresShutdown , public avsCommon::sdkInterfaces::ConnectionStatusObserverInterface - , public std::enable_shared_from_this { + , public std::enable_shared_from_this + , public registrationManager::CustomerDataHandler { public: /** * This function creates a new instance of a @c CertifiedSender. If it fails for any reason, @c nullptr is returned. @@ -61,12 +64,14 @@ class CertifiedSender * @param messageSender The entity which is able to send @c MessageRequests to AVS. * @param connection The connection which may be observed to determine connection status. * @param storage The object which manages persistent storage of messages to be sent. + * @param dataManager A dataManager object that will track the CustomerDataHandler. * @return A @c CertifiedSender object, or @c nullptr if there is any problem. */ static std::shared_ptr create( std::shared_ptr messageSender, std::shared_ptr connection, - std::shared_ptr storage); + std::shared_ptr storage, + std::shared_ptr dataManager); /** * Destructor. @@ -85,6 +90,11 @@ class CertifiedSender */ std::future sendJSONMessage(const std::string& jsonMessage); + /** + * Clear all messages that we are currently storing + */ + void clearData() override; + private: /** * A utility class to manage interaction with the MessageSender. @@ -144,6 +154,7 @@ class CertifiedSender * @param messageSender The entity which is able to send @c MessageRequests to AVS. * @param connection The connection which may be observed to determine connection status. * @param storage The object which manages persistent storage of messages to be sent. + * @param dataManager A dataManager object that will track the CustomerDataHandler. * @param queueSizeWarnLimit The number of items we can store for sending without emitting a warning. * @param queueSizeHardLimit The maximum number of items we can store for sending. */ @@ -151,6 +162,7 @@ class CertifiedSender std::shared_ptr messageSender, std::shared_ptr connection, std::shared_ptr storage, + std::shared_ptr dataManager, int queueSizeWarnLimit = CERTIFIED_SENDER_QUEUE_SIZE_WARN_LIMIT, int queueSizeHardLimit = CERTIFIED_SENDER_QUEUE_SIZE_HARD_LIMIT); diff --git a/CertifiedSender/include/CertifiedSender/MessageStorageInterface.h b/CertifiedSender/include/CertifiedSender/MessageStorageInterface.h index 7d57122624..e58f2839cb 100644 --- a/CertifiedSender/include/CertifiedSender/MessageStorageInterface.h +++ b/CertifiedSender/include/CertifiedSender/MessageStorageInterface.h @@ -65,31 +65,22 @@ class MessageStorageInterface { /** * Creates a new database with the given filePath. - * If the file specified already exists, or if a database is already being handled by this object, then - * this function returns false. + * If a database is already being handled by this object, or there is are other errors creating the database, this + * function returns false. * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is created ok, or @c false if either the file exists or a database is already - * being handled by this object. + * @return @c true If the database is created ok, or @c false if a database is already being handled by this object + * or there is an internal error creating the database. */ - virtual bool createDatabase(const std::string& filePath) = 0; + virtual bool createDatabase() = 0; /** - * Open a database with the given filePath. If this object is already managing an open database, or the file - * does not exist, or there is a problem opening the database, this function returns false. + * Open an existing database. If this object is already managing an open database, or there is a problem opening + * the database, this function returns false. * - * @param filePath The path to the file which will be used to contain the database. - * @return @c true If the database is opened ok, @c false if either the file does not exist, if this object is - * already managing an open database, or if there is another internal reason the database could not be opened. + * @return @c true If the database is opened ok, @c false if this object is already managing an open database, or if + * there is another internal reason the database could not be opened. */ - virtual bool open(const std::string& filePath) = 0; - - /** - * Query if this object is currently managing an open database. - * - * @return @c true If a database is being currently managed by this object, @c false otherwise. - */ - virtual bool isOpen() = 0; + virtual bool open() = 0; /** * Close the currently open database, if one is open. diff --git a/CertifiedSender/include/CertifiedSender/SQLiteMessageStorage.h b/CertifiedSender/include/CertifiedSender/SQLiteMessageStorage.h index 17778c81c0..9d80ab0b46 100644 --- a/CertifiedSender/include/CertifiedSender/SQLiteMessageStorage.h +++ b/CertifiedSender/include/CertifiedSender/SQLiteMessageStorage.h @@ -18,7 +18,8 @@ #include "CertifiedSender/MessageStorageInterface.h" -#include +#include +#include namespace alexaClientSDK { namespace certifiedSender { @@ -30,18 +31,27 @@ namespace certifiedSender { */ class SQLiteMessageStorage : public MessageStorageInterface { public: + /** + * Factory method for creating a storage object for Messages based on an SQLite database. + * + * @param configurationRoot The global config object. + * @return Pointer to the SQLiteMessagetStorge object, nullptr if there's an error creating it. + */ + static std::unique_ptr create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot); + /** * Constructor. + * + * @param dbFilePath The location of the SQLite database file. */ - SQLiteMessageStorage(); + SQLiteMessageStorage(const std::string& databaseFilePath); ~SQLiteMessageStorage(); - bool createDatabase(const std::string& filePath) override; - - bool open(const std::string& filePath) override; + bool createDatabase() override; - bool isOpen() override; + bool open() override; void close() override; @@ -53,15 +63,9 @@ class SQLiteMessageStorage : public MessageStorageInterface { bool clearDatabase() override; -protected: - /** - * A non-virtual function that may be called to clean up resources managed by this class. - */ - void doClose(); - private: - /// The sqlite database handle. - sqlite3* m_dbHandle; + /// The underlying database class. + alexaClientSDK::storage::sqliteStorage::SQLiteDatabase m_database; }; } // namespace certifiedSender diff --git a/CertifiedSender/src/CMakeLists.txt b/CertifiedSender/src/CMakeLists.txt index 8d7d69ecdf..6a4c206a1c 100644 --- a/CertifiedSender/src/CMakeLists.txt +++ b/CertifiedSender/src/CMakeLists.txt @@ -6,9 +6,10 @@ add_library(CertifiedSender SHARED target_include_directories(CertifiedSender PUBLIC "${AVSCommon_INCLUDE_DIRS}" "${CertifiedSender_SOURCE_DIR}/include" + "${RegistrationManager_SOURCE_DIR}/include" "${SQLiteStorage_SOURCE_DIR}/include") -target_link_libraries(CertifiedSender AVSCommon SQLiteStorage) +target_link_libraries(CertifiedSender AVSCommon RegistrationManager SQLiteStorage) # install target asdk_install() \ No newline at end of file diff --git a/CertifiedSender/src/CertifiedSender.cpp b/CertifiedSender/src/CertifiedSender.cpp index c86ff6d077..76a3efdc4f 100644 --- a/CertifiedSender/src/CertifiedSender.cpp +++ b/CertifiedSender/src/CertifiedSender.cpp @@ -18,6 +18,7 @@ #include #include #include +#include namespace alexaClientSDK { namespace certifiedSender { @@ -27,11 +28,6 @@ using namespace avsCommon::sdkInterfaces; using namespace avsCommon::avs; using namespace avsCommon::utils::configuration; -/// The key in our config file to find the root of settings for this Capability Agent. -static const std::string CERTIFIED_SENDER_CONFIGURATION_ROOT_KEY = "certifiedSender"; -/// The key in our config file to find the database file path. -static const std::string CERTIFIED_SENDER_DB_FILE_PATH_KEY = "databaseFilePath"; - /// String to identify log entries originating from this file. static const std::string TAG("CertifiedSender"); @@ -90,8 +86,10 @@ void CertifiedSender::CertifiedMessageRequest::shutdown() { std::shared_ptr CertifiedSender::create( std::shared_ptr messageSender, std::shared_ptr connection, - std::shared_ptr storage) { - auto certifiedSender = std::shared_ptr(new CertifiedSender(messageSender, connection, storage)); + std::shared_ptr storage, + std::shared_ptr dataManager) { + auto certifiedSender = + std::shared_ptr(new CertifiedSender(messageSender, connection, storage, dataManager)); if (!certifiedSender->init()) { ACSDK_ERROR(LX("createFailed").m("Could not initialize certifiedSender.")); @@ -107,9 +105,11 @@ CertifiedSender::CertifiedSender( std::shared_ptr messageSender, std::shared_ptr connection, std::shared_ptr storage, + std::shared_ptr dataManager, int queueSizeWarnLimit, int queueSizeHardLimit) : RequiresShutdown("CertifiedSender"), + CustomerDataHandler(dataManager), m_queueSizeWarnLimit{queueSizeWarnLimit}, m_queueSizeHardLimit{queueSizeHardLimit}, m_isShuttingDown{false}, @@ -143,17 +143,9 @@ bool CertifiedSender::init() { return false; } - auto configurationRoot = ConfigurationNode::getRoot()[CERTIFIED_SENDER_CONFIGURATION_ROOT_KEY]; - - std::string dbFilePath; - if (!configurationRoot.getString(CERTIFIED_SENDER_DB_FILE_PATH_KEY, &dbFilePath) || dbFilePath.empty()) { - ACSDK_ERROR(LX("initFailed").m("Could not load db file path.")); - return false; - } - - if (!m_storage->open(dbFilePath)) { + if (!m_storage->open()) { ACSDK_INFO(LX("init : Database file does not exist. Creating.")); - if (!m_storage->createDatabase(dbFilePath)) { + if (!m_storage->createDatabase()) { ACSDK_ERROR(LX("initFailed").m("Could not create database file.")); return false; } @@ -256,5 +248,14 @@ void CertifiedSender::doShutdown() { m_connection->removeConnectionStatusObserver(shared_from_this()); } +void CertifiedSender::clearData() { + auto result = m_executor.submit([this]() { + std::unique_lock lock(m_mutex); + m_messagesToSend.clear(); + m_storage->clearDatabase(); + }); + result.wait(); +} + } // namespace certifiedSender } // namespace alexaClientSDK diff --git a/CertifiedSender/src/SQLiteMessageStorage.cpp b/CertifiedSender/src/SQLiteMessageStorage.cpp index 631072e0c6..6d30c90e8e 100644 --- a/CertifiedSender/src/SQLiteMessageStorage.cpp +++ b/CertifiedSender/src/SQLiteMessageStorage.cpp @@ -40,6 +40,11 @@ static const std::string TAG("SQLiteMessageStorage"); */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) +/// The key in our config file to find the root of settings for this Capability Agent. +static const std::string CERTIFIED_SENDER_CONFIGURATION_ROOT_KEY = "certifiedSender"; +/// The key in our config file to find the database file path. +static const std::string CERTIFIED_SENDER_DB_FILE_PATH_KEY = "databaseFilePath"; + /// The name of the alerts table. static const std::string MESSAGES_TABLE_NAME = "messages"; /// The name of the 'id' field we will use as the primary key in our tables. @@ -51,74 +56,57 @@ static const std::string CREATE_MESSAGES_TABLE_SQL_STRING = std::string("CREATE DATABASE_COLUMN_ID_NAME + " INT PRIMARY KEY NOT NULL," + DATABASE_COLUMN_MESSAGE_TEXT_NAME + " TEXT NOT NULL);"; -SQLiteMessageStorage::SQLiteMessageStorage() : m_dbHandle{nullptr} { -} - -SQLiteMessageStorage::~SQLiteMessageStorage() { - doClose(); -} - -bool SQLiteMessageStorage::createDatabase(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").m("Database handle is already open.")); - return false; +std::unique_ptr SQLiteMessageStorage::create( + const avsCommon::utils::configuration::ConfigurationNode& configurationRoot) { + auto certifiedSenderConfigurationRoot = configurationRoot[CERTIFIED_SENDER_CONFIGURATION_ROOT_KEY]; + if (!certifiedSenderConfigurationRoot) { + ACSDK_ERROR(LX("createFailed") + .d("reason", "Could not load config for the Message Storage database") + .d("key", CERTIFIED_SENDER_CONFIGURATION_ROOT_KEY)); + return nullptr; } - if (fileExists(filePath)) { - ACSDK_ERROR(LX("createDatabaseFailed").m("File specified already exists.").d("file path", filePath)); - return false; + std::string certifiedSenderDatabaseFilePath; + if (!certifiedSenderConfigurationRoot.getString( + CERTIFIED_SENDER_DB_FILE_PATH_KEY, &certifiedSenderDatabaseFilePath) || + certifiedSenderDatabaseFilePath.empty()) { + ACSDK_ERROR( + LX("createFailed").d("reason", "Could not load config value").d("key", CERTIFIED_SENDER_DB_FILE_PATH_KEY)); + return nullptr; } - m_dbHandle = createSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("createDatabaseFailed").m("Database could not be created.").d("file path", filePath)); - return false; - } - - if (!performQuery(m_dbHandle, CREATE_MESSAGES_TABLE_SQL_STRING)) { - ACSDK_ERROR(LX("createDatabaseFailed").m("Table could not be created.")); - close(); - return false; - } + return std::unique_ptr(new SQLiteMessageStorage(certifiedSenderDatabaseFilePath)); +} - return true; +SQLiteMessageStorage::SQLiteMessageStorage(const std::string& certifiedSenderDatabaseFilePath) : + m_database{certifiedSenderDatabaseFilePath} { } -bool SQLiteMessageStorage::open(const std::string& filePath) { - if (m_dbHandle) { - ACSDK_ERROR(LX("openFailed").m("Database handle is already open.")); - return false; - } +SQLiteMessageStorage::~SQLiteMessageStorage() { + close(); +} - if (!fileExists(filePath)) { - ACSDK_ERROR(LX("openFailed").m("File specified does not exist.").d("file path", filePath)); +bool SQLiteMessageStorage::createDatabase() { + if (!m_database.initialize()) { + ACSDK_ERROR(LX("createDatabaseFailed")); return false; } - m_dbHandle = openSQLiteDatabase(filePath); - if (!m_dbHandle) { - ACSDK_ERROR(LX("openFailed").m("Database could not be opened.").d("file path", filePath)); + if (!m_database.performQuery(CREATE_MESSAGES_TABLE_SQL_STRING)) { + ACSDK_ERROR(LX("createDatabaseFailed").m("Table could not be created.")); + close(); return false; } return true; } -bool SQLiteMessageStorage::isOpen() { - return (nullptr != m_dbHandle); +bool SQLiteMessageStorage::open() { + return m_database.open(); } void SQLiteMessageStorage::close() { - doClose(); -} - -void SQLiteMessageStorage::doClose() { - if (isOpen()) { - if (!closeSQLiteDatabase(m_dbHandle)) { - ACSDK_ERROR(LX("closeFailed").m("Could not close the database.")); - } - m_dbHandle = nullptr; - } + m_database.close(); } bool SQLiteMessageStorage::store(const std::string& message, int* id) { @@ -126,16 +114,12 @@ bool SQLiteMessageStorage::store(const std::string& message, int* id) { ACSDK_ERROR(LX("storeFailed").m("id parameter was nullptr.")); return false; } - if (!m_dbHandle) { - ACSDK_ERROR(LX("storeFailed").m("Database handle is not open.")); - return false; - } std::string sqlString = std::string("INSERT INTO " + MESSAGES_TABLE_NAME + " (") + DATABASE_COLUMN_ID_NAME + ", " + DATABASE_COLUMN_MESSAGE_TEXT_NAME + ") VALUES (" + "?, ?" + ");"; int nextId = 0; - if (!getTableMaxIntValue(m_dbHandle, MESSAGES_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &nextId)) { + if (!getTableMaxIntValue(&m_database, MESSAGES_TABLE_NAME, DATABASE_COLUMN_ID_NAME, &nextId)) { ACSDK_ERROR(LX("storeFailed").m("Cannot generate message id.")); return false; } @@ -146,20 +130,20 @@ bool SQLiteMessageStorage::store(const std::string& message, int* id) { return false; } - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("storeFailed").m("Could not create statement.")); return false; } int boundParam = 1; - if (!statement.bindIntParameter(boundParam++, nextId) || !statement.bindStringParameter(boundParam, message)) { + if (!statement->bindIntParameter(boundParam++, nextId) || !statement->bindStringParameter(boundParam, message)) { ACSDK_ERROR(LX("storeFailed").m("Could not bind parameter.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("storeFailed").m("Could not perform step.")); return false; } @@ -170,11 +154,6 @@ bool SQLiteMessageStorage::store(const std::string& message, int* id) { } bool SQLiteMessageStorage::load(std::queue* messageContainer) { - if (!m_dbHandle) { - ACSDK_ERROR(LX("loadFailed").m("Database handle is not open.")); - return false; - } - if (!messageContainer) { ACSDK_ERROR(LX("loadFailed").m("Alert container parameter is nullptr.")); return false; @@ -182,9 +161,9 @@ bool SQLiteMessageStorage::load(std::queue* messageContainer) { std::string sqlString = "SELECT * FROM " + MESSAGES_TABLE_NAME + " ORDER BY id;"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("loadFailed").m("Could not create statement.")); return false; } @@ -193,22 +172,22 @@ bool SQLiteMessageStorage::load(std::queue* messageContainer) { int id = 0; std::string message; - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("loadFailed").m("Could not perform step.")); return false; } - while (SQLITE_ROW == statement.getStepResult()) { - int numberColumns = statement.getColumnCount(); + while (SQLITE_ROW == statement->getStepResult()) { + int numberColumns = statement->getColumnCount(); // SQLite cannot guarantee the order of the columns in a given row, so this logic is required. for (int i = 0; i < numberColumns; i++) { - std::string columnName = statement.getColumnName(i); + std::string columnName = statement->getColumnName(i); if (DATABASE_COLUMN_ID_NAME == columnName) { - id = statement.getColumnInt(i); + id = statement->getColumnInt(i); } else if (DATABASE_COLUMN_MESSAGE_TEXT_NAME == columnName) { - message = statement.getColumnText(i); + message = statement->getColumnText(i); } } @@ -216,31 +195,29 @@ bool SQLiteMessageStorage::load(std::queue* messageContainer) { messageContainer->push(storedMessage); - statement.step(); + statement->step(); } - statement.finalize(); - return true; } bool SQLiteMessageStorage::erase(int messageId) { std::string sqlString = "DELETE FROM " + MESSAGES_TABLE_NAME + " WHERE id=?;"; - SQLiteStatement statement(m_dbHandle, sqlString); + auto statement = m_database.createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("eraseFailed").m("Could not create statement.")); return false; } int boundParam = 1; - if (!statement.bindIntParameter(boundParam, messageId)) { + if (!statement->bindIntParameter(boundParam, messageId)) { ACSDK_ERROR(LX("eraseFailed").m("Could not bind messageId.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("eraseFailed").m("Could not perform step.")); return false; } @@ -249,7 +226,7 @@ bool SQLiteMessageStorage::erase(int messageId) { } bool SQLiteMessageStorage::clearDatabase() { - if (!clearTable(m_dbHandle, MESSAGES_TABLE_NAME)) { + if (!m_database.clearTable(MESSAGES_TABLE_NAME)) { ACSDK_ERROR(LX("clearDatabaseFailed").m("could not clear messages table.")); return false; } diff --git a/CertifiedSender/test/CMakeLists.txt b/CertifiedSender/test/CMakeLists.txt index fa1c868a3e..16aaf30d48 100644 --- a/CertifiedSender/test/CMakeLists.txt +++ b/CertifiedSender/test/CMakeLists.txt @@ -1,6 +1,7 @@ set(INCLUDE_PATH - "${AVSCommon_INCLUDE_DIRS}" - "${CertifiedSender_INCLUDE_DIRS}") + "${AVSCommon_INCLUDE_DIRS}" + "${CertifiedSender_INCLUDE_DIRS}" + "${AVSCommon_SOURCE_DIR}/SDKInterfaces/test") set(TEST_FOLDER "${CertifiedSender_SOURCE_DIR}/test") diff --git a/CertifiedSender/test/CertifiedSenderTest.cpp b/CertifiedSender/test/CertifiedSenderTest.cpp new file mode 100644 index 0000000000..1413c457a1 --- /dev/null +++ b/CertifiedSender/test/CertifiedSenderTest.cpp @@ -0,0 +1,98 @@ +/* + * Copyright 2018 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. + */ + +#include + +#include + +#include "RegistrationManager/CustomerDataManager.h" +#include "AVSCommon/AVS/Initialization/AlexaClientSDKInit.h" +#include "AVSCommon/SDKInterfaces/MockMessageSender.h" + +#include "CertifiedSender/CertifiedSender.h" + +using namespace ::testing; + +namespace alexaClientSDK { +namespace certifiedSender { +namespace test { + +class MockConnection : public avsCommon::avs::AbstractConnection { + MOCK_CONST_METHOD0(isConnected, bool()); +}; + +class MockMessageStorage : public MessageStorageInterface { +public: + MOCK_METHOD0(createDatabase, bool()); + MOCK_METHOD0(open, bool()); + MOCK_METHOD0(close, void()); + MOCK_METHOD2(store, bool(const std::string& message, int* id)); + MOCK_METHOD1(load, bool(std::queue* messageContainer)); + MOCK_METHOD1(erase, bool(int messageId)); + MOCK_METHOD0(clearDatabase, bool()); + virtual ~MockMessageStorage() = default; +}; + +class CertifiedSenderTest : public ::testing::Test { +public: +protected: + void SetUp() override { + static const std::string CONFIGURATION = R"({ + "certifiedSender" : { + "databaseFilePath":"database.db" + } + })"; + std::stringstream configuration; + configuration << CONFIGURATION; + ASSERT_TRUE(avsCommon::avs::initialization::AlexaClientSDKInit::initialize({&configuration})); + + auto customerDataManager = std::make_shared(); + auto msgSender = std::make_shared(); + m_connection = std::make_shared(); + m_storage = std::make_shared(); + + EXPECT_CALL(*m_storage, open()).Times(1).WillOnce(Return(true)); + m_certifiedSender = CertifiedSender::create(msgSender, m_connection, m_storage, customerDataManager); + } + + void TearDown() override { + if (avsCommon::avs::initialization::AlexaClientSDKInit::isInitialized()) { + avsCommon::avs::initialization::AlexaClientSDKInit::uninitialize(); + } + m_connection->removeConnectionStatusObserver(m_certifiedSender); + } + + /// Class under test. + std::shared_ptr m_certifiedSender; + + /// Mock message storage layer. + std::shared_ptr m_storage; + + /// Pointer to connection. We need to remove certifiedSender as a connection observer or both objects will never + /// be deleted. + std::shared_ptr m_connection; +}; + +/** + * Check that @c clearData() method clears the persistent message storage and the current msg queue + */ +TEST_F(CertifiedSenderTest, clearDataTest) { + EXPECT_CALL(*m_storage, clearDatabase()).Times(1); + m_certifiedSender->clearData(); +} + +} // namespace test +} // namespace certifiedSender +} // namespace alexaClientSDK diff --git a/CertifiedSender/test/MessageStorageTest.cpp b/CertifiedSender/test/MessageStorageTest.cpp index ebd14d59b7..db13767d8f 100644 --- a/CertifiedSender/test/MessageStorageTest.cpp +++ b/CertifiedSender/test/MessageStorageTest.cpp @@ -22,6 +22,7 @@ #include #include +#include using namespace ::testing; @@ -54,7 +55,7 @@ class MessageStorageTest : public ::testing::Test { /** * Constructor. */ - MessageStorageTest() : m_storage{std::make_shared()} { + MessageStorageTest() : m_storage{std::make_shared(g_dbTestFilePath)} { cleanupLocalDbFile(); } @@ -64,18 +65,13 @@ class MessageStorageTest : public ::testing::Test { ~MessageStorageTest() { m_storage->close(); cleanupLocalDbFile(); - - // test } /** * Utility function to create the database, using the global filename. */ void createDatabase() { - if (g_dbTestFilePath.empty()) { - return; - } - m_storage->createDatabase(g_dbTestFilePath); + m_storage->createDatabase(); } /** @@ -96,35 +92,46 @@ class MessageStorageTest : public ::testing::Test { std::shared_ptr m_storage; }; +/** + * Utility function to determine if the storage component is opened. + * + * @param storage The storage component to check. + * @return True if the storage component's underlying database is opened, false otherwise. + */ +static bool isOpen(const std::shared_ptr& storage) { + std::queue dummyMessages; + return storage->load(&dummyMessages); +} + /** * Test basic construction. Database should not be open. */ TEST_F(MessageStorageTest, testConstructionAndDestruction) { - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); } /** * Test database creation. */ TEST_F(MessageStorageTest, testDatabaseCreation) { - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); } /** * Test opening and closing a database. */ TEST_F(MessageStorageTest, testOpenAndCloseDatabase) { - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); m_storage->close(); - ASSERT_FALSE(m_storage->isOpen()); - ASSERT_TRUE(m_storage->open(g_dbTestFilePath)); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); + ASSERT_TRUE(m_storage->open()); + ASSERT_TRUE(isOpen(m_storage)); m_storage->close(); - ASSERT_FALSE(m_storage->isOpen()); + ASSERT_FALSE(isOpen(m_storage)); } /** @@ -132,7 +139,7 @@ TEST_F(MessageStorageTest, testOpenAndCloseDatabase) { */ TEST_F(MessageStorageTest, testDatabaseStoreAndLoad) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); std::queue dbMessages; ASSERT_TRUE(m_storage->load(&dbMessages)); @@ -167,7 +174,7 @@ TEST_F(MessageStorageTest, testDatabaseStoreAndLoad) { */ TEST_F(MessageStorageTest, testDatabaseErase) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); // add three messages, and verify int dbId = 0; @@ -199,7 +206,7 @@ TEST_F(MessageStorageTest, testDatabaseErase) { */ TEST_F(MessageStorageTest, testDatabaseClear) { createDatabase(); - ASSERT_TRUE(m_storage->isOpen()); + ASSERT_TRUE(isOpen(m_storage)); int dbId = 0; ASSERT_TRUE(m_storage->store(TEST_MESSAGE_ONE, &dbId)); diff --git a/ContextManager/src/ContextManager.cpp b/ContextManager/src/ContextManager.cpp index c0bfe36721..defbaf7daa 100644 --- a/ContextManager/src/ContextManager.cpp +++ b/ContextManager/src/ContextManager.cpp @@ -164,7 +164,7 @@ SetStateResult ContextManager::updateStateLocked( const StateRefreshPolicy& refreshPolicy) { auto stateInfoMappingIt = m_namespaceNameToStateInfo.find(stateProviderName); if (m_namespaceNameToStateInfo.end() == stateInfoMappingIt) { - if (StateRefreshPolicy::ALWAYS == refreshPolicy) { + if (StateRefreshPolicy::ALWAYS == refreshPolicy || StateRefreshPolicy::SOMETIMES == refreshPolicy) { ACSDK_ERROR(LX("updateStateLockedFailed") .d("reason", "unregisteredStateProvider") .d("namespace", stateProviderName.nameSpace) @@ -197,7 +197,8 @@ void ContextManager::requestStatesLocked(std::unique_lock& stateProv for (auto it = m_namespaceNameToStateInfo.begin(); it != m_namespaceNameToStateInfo.end(); ++it) { auto& stateInfo = it->second; - if (StateRefreshPolicy::ALWAYS == stateInfo->refreshPolicy) { + if (StateRefreshPolicy::ALWAYS == stateInfo->refreshPolicy || + StateRefreshPolicy::SOMETIMES == stateInfo->refreshPolicy) { m_pendingOnStateProviders.insert(it->first); stateProviderLock.unlock(); stateInfo->stateProvider->provideState(it->first, curStateReqToken); @@ -298,6 +299,14 @@ void ContextManager::sendContextToRequesters() { std::unique_lock stateProviderLock(m_stateProviderMutex); for (auto it = m_namespaceNameToStateInfo.begin(); it != m_namespaceNameToStateInfo.end(); ++it) { auto& stateInfo = it->second; + if (stateInfo->jsonState.empty() && StateRefreshPolicy::SOMETIMES == stateInfo->refreshPolicy) { + /* + * If jsonState supplied by the state provider is empty and it has a refreshPolicy of SOMETIMES, it means + * that it doesn't want to provide state. + */ + ACSDK_DEBUG9(LX("buildContextIgnored").d("namespace", it->first.nameSpace).d("name", it->first.name)); + continue; + } Value jsonState = buildState(it->first, stateInfo->jsonState, allocator); if (jsonState.ObjectEmpty()) { ACSDK_ERROR(LX("buildContextFailed").d("reason", "buildStateFailed")); diff --git a/ContextManager/test/ContextManagerTest.cpp b/ContextManager/test/ContextManagerTest.cpp index 4bcfff0959..f8cb1f1752 100644 --- a/ContextManager/test/ContextManagerTest.cpp +++ b/ContextManager/test/ContextManagerTest.cpp @@ -141,6 +141,9 @@ static const NamespaceAndName AUDIO_PLAYER(NAMESPACE_AUDIO_PLAYER, NAME_PLAYBACK /// Alerts namespace and name static const NamespaceAndName ALERTS(NAMESPACE_ALERTS, NAME_ALERTS_STATE); +/// Dummy provider namespace and name +static const NamespaceAndName DUMMY_PROVIDER("Dummy", "DummyName"); + /** * @c MockContextRequester used to verify @c ContextManager behavior. */ @@ -645,6 +648,47 @@ TEST_F(ContextManagerTest, testIncorrectToken) { m_speechSynthesizer->getCurrentstateRequestToken() + 1)); } +/** + * Set the states with a @c StateRefreshPolicy @c ALWAYS for @c StateProviderInterfaces that are registered with the + * @c ContextManager. Request for context by calling @c getContext. Expect that the context is returned within the + * timeout period. + * + * There's a dummyProvider with StateRefreshPolicy @c SOMETIMES that returns an empty context. Check ContextManager is + * okay with it and would include the context provided by the dummyProvider. + * + * Check the context that is returned by the @c ContextManager. Expect it should match the test value. + */ +// ACSDK-1217 - ContextManagerTest::testEmptyProvider fails on Windows +#if !defined(_WIN32) || defined(RESOLVED_ACSDK_1217) +TEST_F(ContextManagerTest, testEmptyProvider) { + auto dummyProvider = MockStateProvider::create( + m_contextManager, DUMMY_PROVIDER, "", StateRefreshPolicy::SOMETIMES, DEFAULT_SLEEP_TIME); + m_contextManager->setStateProvider(DUMMY_PROVIDER, dummyProvider); + + ASSERT_EQ( + SetStateResult::SUCCESS, + m_contextManager->setState( + SPEECH_SYNTHESIZER, + SPEECH_SYNTHESIZER_PAYLOAD_PLAYING, + StateRefreshPolicy::ALWAYS, + m_speechSynthesizer->getCurrentstateRequestToken())); + ASSERT_EQ( + SetStateResult::SUCCESS, + m_contextManager->setState( + AUDIO_PLAYER, + AUDIO_PLAYER_PAYLOAD, + StateRefreshPolicy::ALWAYS, + m_audioPlayer->getCurrentstateRequestToken())); + ASSERT_EQ( + SetStateResult::SUCCESS, + m_contextManager->setState( + DUMMY_PROVIDER, "", StateRefreshPolicy::ALWAYS, dummyProvider->getCurrentstateRequestToken())); + m_contextManager->getContext(m_contextRequester); + ASSERT_TRUE(m_contextRequester->waitForContext(DEFAULT_TIMEOUT)); + ASSERT_EQ(CONTEXT_TEST, m_contextRequester->getContextString()); +} +#endif + } // namespace test } // namespace contextManager } // namespace alexaClientSDK diff --git a/ESP/CMakeLists.txt b/ESP/CMakeLists.txt new file mode 100644 index 0000000000..6606e39d9a --- /dev/null +++ b/ESP/CMakeLists.txt @@ -0,0 +1,6 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +project(ESP LANGUAGES CXX C) + +include(../build/BuildDefaults.cmake) + +add_subdirectory("src") diff --git a/ESP/include/ESP/DummyESPDataProvider.h b/ESP/include/ESP/DummyESPDataProvider.h new file mode 100644 index 0000000000..b38fc90cdb --- /dev/null +++ b/ESP/include/ESP/DummyESPDataProvider.h @@ -0,0 +1,62 @@ +/* + * Copyright 2018 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. + */ +#ifndef ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_DUMMYESPDATAPROVIDER_H_ +#define ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_DUMMYESPDATAPROVIDER_H_ + +#include + +#include +#include +#include + +namespace alexaClientSDK { +namespace esp { + +/** + * This is a dummy provider that allows customer to manually test ESP or just to keep it disabled. + */ +class DummyESPDataProvider + : public ESPDataProviderInterface + , public ESPDataModifierInterface { +public: + /** + * DummyESPDataProvider Constructor. + */ + DummyESPDataProvider(); + + /// @name Overridden ESPDataProviderInterface methods. + /// @{ + capabilityAgents::aip::ESPData getESPData() override; + bool isEnabled() const override; + void disable() override; + void enable() override; + /// @} + + /// @name Overridden ESPDataModifierInterface methods. + /// @{ + void setVoiceEnergy(const std::string& voiceEnergy) override; + void setAmbientEnergy(const std::string& ambientEnergy) override; + /// @} + +private: + std::string m_voiceEnergy; + std::string m_ambientEnergy; + bool m_enabled; +}; + +} // namespace esp +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_DUMMYESPDATAPROVIDER_H_ diff --git a/ESP/include/ESP/ESPDataModifierInterface.h b/ESP/include/ESP/ESPDataModifierInterface.h new file mode 100644 index 0000000000..a7792c6e33 --- /dev/null +++ b/ESP/include/ESP/ESPDataModifierInterface.h @@ -0,0 +1,53 @@ +/* + * Copyright 2018 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. + */ +#ifndef ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAMODIFIERINTERFACE_H_ +#define ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAMODIFIERINTERFACE_H_ + +#include + +#include + +namespace alexaClientSDK { +namespace esp { + +/** + * The ESPDataModifierInterface is a debugging interface to allow modifying the ESPData in the DummyESPDataProvider. + */ +class ESPDataModifierInterface { +public: + /** + * Destructor + */ + virtual ~ESPDataModifierInterface() = default; + + /** + * Set new voice energy. + * + * @param voiceEnergy New voice energy value. + */ + virtual void setVoiceEnergy(const std::string& voiceEnergy) = 0; + + /** + * Set new ambient energy. + * + * @param ambientEnergy New ambient energy value. + */ + virtual void setAmbientEnergy(const std::string& ambientEnergy) = 0; +}; + +} // namespace esp +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAMODIFIERINTERFACE_H_ diff --git a/ESP/include/ESP/ESPDataProvider.h b/ESP/include/ESP/ESPDataProvider.h new file mode 100644 index 0000000000..4c53b0ecaa --- /dev/null +++ b/ESP/include/ESP/ESPDataProvider.h @@ -0,0 +1,118 @@ +/* + * Copyright 2018 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. + */ +#ifndef ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAPROVIDER_H_ +#define ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAPROVIDER_H_ + +#include +#include +#include +#include + +#include "DA_Metrics/FrameEnergyComputing.h" +#include "VAD_Features/VAD_class.h" + +#include +#include +#include + +namespace alexaClientSDK { +namespace esp { + +/** + * The ESPDataProvider is used to connect the sample app with the ESP library. When enabled, the ESPDataProvider object + * feeds the ESP Library constantly using its own thread. + */ +class ESPDataProvider : public ESPDataProviderInterface { +public: + /** + * Create a unique pointer for an ESPDataProvider. + * + * @param audioProvider Should have the audio input stream used by the wakeword engine and the input parameters. + * @return A valid ESPDataProvider pointer if creation succeeds and a empty pointer if it fails. + */ + static std::unique_ptr create(const capabilityAgents::aip::AudioProvider& audioProvider); + + /** + * ESPDataProvider Destructor. + */ + ~ESPDataProvider(); + + /// @name Overridden ESPDataProviderInterface methods. + /// @{ + capabilityAgents::aip::ESPData getESPData() override; + bool isEnabled() const override; + void disable() override; + void enable() override; + /// @} + + /** + * Delete ESPDataProvider default constructor. + */ + ESPDataProvider() = delete; + + /** + * Delete ESPDataProvider copy constructor. + */ + ESPDataProvider(const ESPDataProvider&) = delete; + + /** + * Delete ESPDataProvider copy operator. + */ + ESPDataProvider operator=(const ESPDataProvider&) = delete; + +private: + /** + * ESP processing loop. This method will feed the ESP library with the audio input until the ESPDataProvider is + * disabled. + */ + void espLoop(); + + /** + * ESPDataProvider constructor. + * + * @param reader Audio input stream that should be used to feed the ESP library. + * @param frameSize The audio frame size per ms in bits. + */ + ESPDataProvider(std::unique_ptr reader, unsigned int frameSize); + + // Unique pointer to a valid stream reader. + std::unique_ptr m_reader; + + /// Object responsible for VAD algorithm. + VADClass m_vad; + + /// Object used to calculate the frame energy. The access to this variable is guarded by @c m_mutex. + FrameEnergyClass m_frameEnergyCompute; + + /// Thread that keeps feeding audio to ESP library. + std::thread m_thread; + + /// Serializes access to m_FrameEnergyCompute. + std::mutex m_mutex; + + /// Indicates if ESP data is provided or not. The access to this variable is guarded by @c m_mutex. + bool m_isEnabled; + + /// Indicates whether the internal main loop should keep running. + std::atomic m_isShuttingDown; + + /// Keeps the frame size. + unsigned int m_frameSize; +}; + +} // namespace esp +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAPROVIDER_H_ diff --git a/ESP/include/ESP/ESPDataProviderInterface.h b/ESP/include/ESP/ESPDataProviderInterface.h new file mode 100644 index 0000000000..213fb504ad --- /dev/null +++ b/ESP/include/ESP/ESPDataProviderInterface.h @@ -0,0 +1,61 @@ +/* + * Copyright 2018 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. + */ +#ifndef ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAPROVIDERINTERFACE_H_ +#define ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAPROVIDERINTERFACE_H_ + +#include + +namespace alexaClientSDK { +namespace esp { + +/** + * The ESPDataProviderInterface should be used to provide ESPData. + */ +class ESPDataProviderInterface { +public: + /** + * Destructor + */ + virtual ~ESPDataProviderInterface() = default; + + /** + * Retrieve the ESPData from the ESP Library. + * + * @return Collected ESPData if ESP is enabled, otherwise it returns ESPData::EMPTY_ESP_DATA. + */ + virtual capabilityAgents::aip::ESPData getESPData() = 0; + + /** + * Return whether the ESP is enabled or not. + * + * @return @c true if ESP is enabled, else @c false. + */ + virtual bool isEnabled() const = 0; + + /** + * Disable ESP and stop the processing thread. + */ + virtual void disable() = 0; + + /** + * Enable ESP and starts the processing thread if it wasn't running yet. + */ + virtual void enable() = 0; +}; + +} // namespace esp +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_ESP_INCLUDE_ESP_ESPDATAPROVIDERINTERFACE_H_ diff --git a/ESP/src/CMakeLists.txt b/ESP/src/CMakeLists.txt new file mode 100644 index 0000000000..3d040021bc --- /dev/null +++ b/ESP/src/CMakeLists.txt @@ -0,0 +1,19 @@ +add_definitions("-DACSDK_LOG_MODULE=esp") + +if (ESP_PROVIDER) + add_library(ESP SHARED ESPDataProvider.cpp) + target_link_libraries(ESP "${ESP_LIB_PATH}") + target_include_directories(ESP PUBLIC "${ESP_INCLUDE_DIR}") +else() + add_library(ESP SHARED DummyESPDataProvider.cpp) +endif() + +target_include_directories(ESP PUBLIC + "${ESP_SOURCE_DIR}/include" + "${AIP_SOURCE_DIR}/include" + "${AVSCommon_SOURCE_DIR}/include") + +target_link_libraries(ESP AIP AVSCommon) + +# install target +asdk_install() diff --git a/ESP/src/DummyESPDataProvider.cpp b/ESP/src/DummyESPDataProvider.cpp new file mode 100644 index 0000000000..7b51faf57e --- /dev/null +++ b/ESP/src/DummyESPDataProvider.cpp @@ -0,0 +1,51 @@ +/* + * Copyright 2018 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. + */ + +#include + +namespace alexaClientSDK { +namespace esp { + +DummyESPDataProvider::DummyESPDataProvider() : m_enabled{false} { +} + +capabilityAgents::aip::ESPData DummyESPDataProvider::getESPData() { + return capabilityAgents::aip::ESPData(m_voiceEnergy, m_ambientEnergy); +} + +bool DummyESPDataProvider::isEnabled() const { + return m_enabled; +} + +void DummyESPDataProvider::disable() { + m_enabled = false; + m_ambientEnergy.clear(); + m_voiceEnergy.clear(); +} + +void DummyESPDataProvider::enable() { + m_enabled = true; +} + +void DummyESPDataProvider::setVoiceEnergy(const std::string& voiceEnergy) { + m_voiceEnergy = voiceEnergy; +} + +void DummyESPDataProvider::setAmbientEnergy(const std::string& ambientEnergy) { + m_ambientEnergy = ambientEnergy; +} + +} // namespace esp +} // namespace alexaClientSDK diff --git a/ESP/src/ESPDataProvider.cpp b/ESP/src/ESPDataProvider.cpp new file mode 100644 index 0000000000..b45a3aa11f --- /dev/null +++ b/ESP/src/ESPDataProvider.cpp @@ -0,0 +1,166 @@ +/* + * Copyright 2018 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. + */ +#include +#include +#include +#include +#include + +#include "ESP/ESPDataProvider.h" + +/// String to identify log entries originating from this file. +static const std::string TAG{"ESPDataProvider"}; + +/** + * 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 ESP compatible AVS sample rate of 16 kHz. +static const unsigned int ESP_COMPATIBLE_SAMPLE_RATE = 16000; + +/// The ESP compatible bits per sample of 16. +static const unsigned int ESP_COMPATIBLE_SAMPLE_SIZE_IN_BITS = 16; + +/// The ESP frame size in ms. The ESP library supports 8ms, 15ms and 16ms +static const unsigned int ESP_FRAMES_IN_MILLISECONDS = 16; + +namespace alexaClientSDK { +namespace esp { + +static const auto TIMEOUT = std::chrono::seconds(1); + +using AudioInputStream = avsCommon::avs::AudioInputStream; +using ESPData = alexaClientSDK::capabilityAgents::aip::ESPData; + +std::unique_ptr ESPDataProvider::create(const capabilityAgents::aip::AudioProvider& audioProvider) { + if ((ESP_COMPATIBLE_SAMPLE_RATE != audioProvider.format.sampleRateHz) || + (ESP_COMPATIBLE_SAMPLE_SIZE_IN_BITS != audioProvider.format.sampleSizeInBits)) { + ACSDK_ERROR(LX(__func__) + .d("reason", "unsupportedFormat") + .d("sampleSize", audioProvider.format.sampleSizeInBits) + .d("sampleRateHz", audioProvider.format.sampleRateHz)); + return nullptr; + } + + unsigned int frameSize = (audioProvider.format.sampleRateHz / 1000) * ESP_FRAMES_IN_MILLISECONDS; + + auto reader = audioProvider.stream->createReader(avsCommon::avs::AudioInputStream::Reader::Policy::BLOCKING); + if (!reader) { + ACSDK_ERROR(LX(__func__).d("reason", "createReaderFailed")); + return nullptr; + } + + auto connector = std::unique_ptr(new ESPDataProvider(std::move(reader), frameSize)); + connector->enable(); + return connector; +} + +ESPDataProvider::~ESPDataProvider() { + m_isShuttingDown = true; + if (m_thread.joinable()) { + m_thread.join(); + } +} + +ESPData ESPDataProvider::getESPData() { + std::lock_guard lock{m_mutex}; + if (m_isEnabled) { + return ESPData{std::to_string(m_frameEnergyCompute.getVoicedEnergy()), + std::to_string(m_frameEnergyCompute.getAmbientEnergy())}; + } + return ESPData::EMPTY_ESP_DATA; +} + +bool ESPDataProvider::isEnabled() const { + return m_isEnabled; +} + +void ESPDataProvider::disable() { + std::lock_guard lock{m_mutex}; + m_isEnabled = false; +} + +void ESPDataProvider::enable() { + std::lock_guard lock{m_mutex}; + m_isEnabled = true; +} + +void ESPDataProvider::espLoop() { + Word64 currentFrameEnergy = 0; + short procBuff[m_frameSize]; + const auto numWords = sizeof(procBuff) / m_reader->getWordSize(); + bool GVAD = false; + bool hasErrorOccurred = false; + + while (!m_isShuttingDown) { + auto words = m_reader->read(&procBuff, numWords, TIMEOUT); + + if (words > 0) { + // Call VAD and get frame energy + m_vad.process(procBuff, GVAD, currentFrameEnergy); + { + std::lock_guard lock{m_mutex}; + m_frameEnergyCompute.process(GVAD, currentFrameEnergy); + } + } else { + switch (words) { + case AudioInputStream::Reader::Error::CLOSED: + ACSDK_CRITICAL(LX("espLoopFailed").d("reason", "streamClosed")); + hasErrorOccurred = true; + break; + case AudioInputStream::Reader::Error::OVERRUN: + ACSDK_ERROR(LX("espLoopFailed").d("reason", "streamOverrun")); + m_reader->seek(0, AudioInputStream::Reader::Reference::BEFORE_WRITER); + break; + case AudioInputStream::Reader::Error::TIMEDOUT: + ACSDK_INFO(LX("espLoopFailed").d("reason", "readerTimeOut")); + break; + default: + // We should never get this since we are using a Blocking Reader. + ACSDK_CRITICAL(LX("espLoopFailed") + .d("reason", "unexpectedError") + // Leave as ssize_t to avoid messiness of casting to enum. + .d("error", words)); + hasErrorOccurred = true; + break; + } + } + if (hasErrorOccurred) { + ACSDK_CRITICAL(LX("espLoop").m("An error has occurred, exiting loop.")); + break; + } + } + m_reader->close(); +} + +ESPDataProvider::ESPDataProvider( + std::unique_ptr reader, + unsigned int frameSize) : + m_reader{std::move(reader)}, + m_vad{frameSize}, + m_frameEnergyCompute{frameSize}, + m_isEnabled{true}, + m_isShuttingDown{false}, + m_frameSize{frameSize} { + m_vad.blkReset(); + m_frameEnergyCompute.blkReset(); + m_thread = std::thread(&ESPDataProvider::espLoop, this); +} + +} // namespace esp +} // namespace alexaClientSDK diff --git a/Integration/AlexaClientSDKConfig.json b/Integration/AlexaClientSDKConfig.json index 6c2e46e906..819947e41f 100644 --- a/Integration/AlexaClientSDKConfig.json +++ b/Integration/AlexaClientSDKConfig.json @@ -53,7 +53,30 @@ // The default endpoint to connect to. // See https://developer.amazon.com/docs/alexa-voice-service/api-overview.html#endpoints for regions and values // e.g. "endpoint": "https://avs-alexa-na.amazon.com" + + // Example of specifying suggested latency in seconds when openning PortAudio stream. By default, + // when this paramater isn't specified, SampleApp calls Pa_OpenDefaultStream to use the default value. + // See http://portaudio.com/docs/v19-doxydocs/structPaStreamParameters.html for further explanation + // on this parameter. + //"portAudio":{ + // "suggestedLatency": 0.150 + //} } + + // Example of specifying the output format for the gstreamer-based MediaPlayer bundled with the SDK. Many platforms + // will automatically set the output format correctly, but in some cases where the hardware requires a specific + // format and the software stack is not automatically setting it correctly, these parameters can be used to manually + // specify the output format. Supported rate/format/channels values are documented in detail here: + // https://gstreamer.freedesktop.org/documentation/design/mediatype-audio-raw.html + // + // "gstreamerMediaPlayer":{ + // "outputConversion":{ + // "rate":16000, + // "format":"S16LE", + // "channels":1 + // } + // }, + // Example of specifying a default log level for all ModuleLoggers. If not specified, ModuleLoggers get // their log level from the sink logger. // "logging":{ diff --git a/Integration/include/Integration/AuthObserver.h b/Integration/include/Integration/AuthObserver.h index 6c4740dd9d..f92118f35d 100644 --- a/Integration/include/Integration/AuthObserver.h +++ b/Integration/include/Integration/AuthObserver.h @@ -31,7 +31,7 @@ class AuthObserver : public avsCommon::sdkInterfaces::AuthObserverInterface { void onAuthStateChange( const avsCommon::sdkInterfaces::AuthObserverInterface::State, const avsCommon::sdkInterfaces::AuthObserverInterface::Error = - avsCommon::sdkInterfaces::AuthObserverInterface::Error::NO_ERROR) override; + avsCommon::sdkInterfaces::AuthObserverInterface::Error::SUCCESS) override; AuthObserverInterface::State getAuthState() const; bool waitFor( const avsCommon::sdkInterfaces::AuthObserverInterface::State, diff --git a/Integration/include/Integration/JsonHeader.h b/Integration/include/Integration/JsonHeader.h index 7564a1bbee..2bc4efdd1a 100644 --- a/Integration/include/Integration/JsonHeader.h +++ b/Integration/include/Integration/JsonHeader.h @@ -16,6 +16,8 @@ #ifndef ALEXA_CLIENT_SDK_INTEGRATION_INCLUDE_INTEGRATION_JSONHEADER_H_ #define ALEXA_CLIENT_SDK_INTEGRATION_INCLUDE_INTEGRATION_JSONHEADER_H_ +#include + // Todo ACSDK-443: Move the JSON to text file /// This is a basic synchronize JSON message which may be used to initiate a connection with AVS. // clang-format off diff --git a/Integration/inputs/recognize_long_timer_test.wav b/Integration/inputs/recognize_long_timer_test.wav index b7bb85746da312554758c3b05dfda3ca7631af5e..d4a57db96f588aa17ac99435721c9ac8711831cf 100644 GIT binary patch literal 307328 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

M7~v$Slixs+%~hx;woy;vc6^B&fhV;e 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

wEUUMDp?$hdu)np@)?oASD!%++rMu}> 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 z0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq z5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH* zAOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8} z0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq z5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH* zAOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8} z0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq z5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH* zAOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8} z0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq z5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH* zAOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8} z0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq z5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH*AOR8}0TLhq5+DH* LAORBi|1R)k^H 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 z01-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$ z1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0 zL;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1 zKm-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG z0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF0 z5kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e0 z5CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p4 z01-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$ z1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0 zL;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1 zKm-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG z0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF0 z5kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e0 z5CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p4 z01-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$ z1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0 xL;w*$1P}p401-e05CKF05kLeG0Ym^1Km-s0L;w*$1P}p401-e0{(mR%{{ZBqpnL!T diff --git a/Integration/inputs/recognize_very_long_timer_test.wav b/Integration/inputs/recognize_very_long_timer_test.wav index f9f6b4d94c6caaac93d9331e12d5b400ae15981c..0deaf82aa4bf15bd1ca0205bc8efc2afee8daa19 100644 GIT binary patch literal 314852 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 z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs W0RjXF5FkK+009C72oU%m3H%3hf7)CC literal 277036 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ç|V0Z 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 P7W(^O$U*O+mH8t7cbF<-^MSV9(@B7nU|J8B~%} zW~z}mTJkDsfMHuGAAT`3E1*nWB%iOqHJO34BX`GY;ej@TSHO#MVgN-L$SR+?e9cuDje_=Egl&pxyV{Rihgssx?@b@f8z|^_z0&uQa7(N-lf=xoz;9HmtMzh}h zLE#8+of??-C*#FjPqqZyaTb{GHVjQi{(&iN>$$CL7W)JYOb1|(IG^jz63iNZAAh>< zn6Ibrig%+I<~DiOyH~rHy7#%Bx_-FcIafP7J5{cH*JoF>N8&jT*KEIMv}cd|p8J<4 z&^OoLg-P>Q`!v1+I777sgHD}49EkCL%tj!m1ul{A2m7t5unNCNPT_ZnQt}HehRHYc zMS9UwQHIDRekScLyCmPPD3a^touujFf#NpeiIPf*NSZ7Omf+$Mv>W)tBzh&ik(x(7 zBPWvmiT{Y##2)-P9z$60kI)k;Cw3FT#1vvQ@i%du=tk}&;_$_o7W)sm4J-dEWGW^j zrsBIW2e6vu!16YPoyG-VS!=kF;OJTbOy)?~85VQ@KO>+$pASB&G(Jlh1}3guEa(3T za};`VBHqKPSwOTsXCYl)0eQM(+;VoMkML~tw(+-Sm%#OB*(<(vUa9x1uL;|XEn|kj zDPt77pY6j8@aHjRwwMp)2e50|{r=^?kG={&4U;b_xj|eT7Gb*rrMDKcr>$q9$ zA7I-0lYa_swY$O%zEt=k;J^jn7v`gD!G4g%cN7L74ZvhJOZbml!*7H=(;?w2o5s%N z2Esm|6uy50a)l2Es*2~H!(MYSWHQ~tB-mki;r;)DEf4}>LdOUs3_T6R>qV3T<5y?M z0Ve}DoQnJpXxsyc9chNXMHhgV?gTL3=Qxb_a+~;T$ZWJ43BcE*djvn21?=2^d=X+n z|HWRQBk@r5wa^>U11}Q=JHQ8sl(>SuM$^D^whLJa!z;H4P(?&c=oX-J2{ayC3sWUc zd;@L^ykTRoOL#ZJj$DBBoet(I{6xp|QbCFy0Lzjbs!6vIf*2y~B2;=AQ65 z{8_vVosZX|cJLFvXHvMq$QWb{UW{eXvygFIss9xl1#80|>@Kh$D`9uih>J%mVQOU+ zsf9NZ?mx)PX5Vv*u!ZPGG(y;gT;}#NKGuRX79_Zc>cjU3hH*61H!h*yh(x%{E+eUg z61@qe{5uwbI>`iXBT**oz&@gh{1KGl`uY@jdvr4Q410v4R2VP9)5%V}jBmk|AzOJ8 zs?)n*Pqu@oNZ;&(K07&{kH9R(T<=%! zZBC12OYVY2Z40&%Y-vY1ph1O~#4av{6XTuG=|T{iMrdH7+;(OdyN>UMS7I}SSadw{ zjX{_i;E8*nDYy)q4fk0+-wyUFHWuqi4MU!Sk?%KO%B|vSfqUPN)G{*GE)-)|@xI6m zbUbF`mxEU*4xa$omY-NZu(c&2F+w%BpNm4*f`8*Hq}3k78N4SL5>}vg^d#3GGFona z1tNv4OC53>c1p1VkW+XgWDvZ=kH7$;PeN0ECb%r*NF08FUCHDjeYrE(cIqk)jtiKU zb{5Qs$zYFwXNC`e8L4`hdAJBE;ydDV=q}_aA`{sNp15{=A=u^mkh}RR{|&aY?+;GL z&m?=|{jq1leMS$-7dsgJz6nKa605{QkhRz&d^M-XUkQ19A1p=yVw<>$Y-hjm?~nn) zIrtvWaMSq~=w`Mh_pdMs-vbjY*I)~v#*)fiMTTK*xBx5@Qa(;tV|w$K`R#ZX-054f z$H;o_Uw%A#k-skt##;)F&?01|kR_zB5omX$5>4kjz|*{%zvI6Fci9Z~GG-L^BZt_f z+(TgCDK3&6gwBL}5V!!QFaLySEnJ4Z&mUk{EP;1Dg?(Y``0a!V)}KVIjNirog=pC2 zd~3Fka2;tNydyV|Be?BCXQ*RB-2*a98~Lf&V%RG!;a7QMU~1}HZYqx=WAPL0HRLij zo_ooy7T(}>V7z#ST}2ZR3&(?JB>@@D{{kQB6Y#2x;ewf9!HXUj2H+3T(a1o40s9N> zf)2qFApiRn>MQnL@}8Du#-CiRhK(puDB?C$EuJUXaGrr zyV8BaC$1Z^7f(l;qmR+%@JxlE>F{3J3-69}L?ZH5&;f^k4jIg@;x7RuHw~4*yu{7?Gd>*K0#6aaz2@HVUVaz-NpV+lie69W z5C?cEQwj$CGhjw*!Zrn$YD2s&dIK%N2H`V_HOOkD8Pbn`$82MgnMD72rVF=`--+=8 zCFr>R%x2_o94BhfZQy_1f(!=djURmsHdiHgj%^Fon0oL@nSif+jm#Ggf^qRTm^S}p z|HpmhJ0h3SE--6(4f-8+DJk%$96nujC?k}J$62=We|L8L*aZ#I#PM6ed$Wu^~I4sI#R z6VH^^iE%uIAI`)uZl4+~K=s)cLIk(kHv(p<&125Pxq2sbPiU~x++{biMsW7f{9CY& zz*`q88C|gHXbI8>-$3*twd7W?qCZD33CUpB{hM=f&tP{yf@{f9On|@2H{6eN$Aw)& zE#FfZ2ky^GsQ&~B3*qhSBI2n=!~j^si@6)T8c_=)!CyKXd?#y!3+!m_f>4HwfMGa> zBPgU2`K7!R`fqmZ4m@v3SS@}S_8i~Q)@V1ZA*lk&%}2I`p6FDp9-ODnLauf`)*FnH zQ}A2=e^uCY-%v<$q%ya-o3LMg1C zGG_!Mmk7?-6?QhRgd@+BLzY9-b5~6@N?n(IGE(np}3&kNrIhprD@8(b8I2wab!tUU5 zs8%3YG<1-*fw_kQtIuJSL2se^phocuvYcA>o!{@T@K1xeCo?@$JXT)_J4%pYp>PW0 zX-rZDl@p_clF8((l#*tdVZXzyy~s&Wi=ymd@;2Dth-w>ZzatKCQ3M_e(GBaCw|aUF9Uw9m4)hkjf$ zdmrmnAfRk@hm7{>HdT$P+t+Tdol;BG9x`?@FSaGPHn^SM?%ZL_L{qYE>PrEG!n;M? zj2>S9S_5t4%*Ku4XT+as+^Jz+TE``f?QR$RBW5 zO@z7DW$+pN5;jBs{ViBv!;r~fI7)*j@(p?r3@-6d$rz0MhEKKz=B4##FZgD;Pdftb z-E2QB2h1}~FO7=YpxW&tLqeXs~`zwsajXHpn62Dt?rklp401_ zfzA~VR}}{wj3}+&BYs@dY0d2|9<{7!(W+Ua#-r*>SIft8wz@}FRi+=}l*UT}|@ z`AlD!YEj5!`LFsrGd-9(|8ak?zZe{$y?on!DUjHBC~QZU;iIXE;%~C2$`+bLU5Ngn zVZULWUZTrYTa}Fz5z-#?MdB1j!F;zyOs0?at@qXQjq(djIP4W03Tu%CXcAt4_k^cC zh#UZ2vVZaY_z~D_mz9f3d=f|v!&@J zQ%kCg#}&USI$hMd%viikM9=E*H7E3tPX-j8QN1{XL*0!eI z8-_&h3Jo+gP#&Y-qYpW=|2EkF(_B!B^gQ>5`kj7)UB+d?+T0fWSVr(?P3Ko~>q zR}ZQ^ShvsI(6-e5n|XlzN2;Vs)dAhjz@;IR!umukjj)7`3hf^>(6C$+CSN2H!^Arp zR$0dFfV}4&d$@h9&1n4-TsBhs1$ZXXz(se{R$yCdE4TVA4=sBwaTe0r!y0W}Y8`2t zXS)O$!`b!^_WAJl-r06pms*yXFV?lGJyUhM;#JwAl7&S%h350W{i*>Gdu`Z|kvG=Ec5*QfnzzK1Z5RFX0!pLg6Sh88ZLzS;h zH;f887JMssUC@Dmv-&ogN_nOTazWfCPiy-u)0&!y%Hw5YN@ta>D!Wz@T{FN`YrE=h z0n-8Ri`pulYd#rNA*S%_QETgMi5e7H6^@483gUH)@{?o;)elSJhxm=|4$fD$o0i_@ z3ez;R)!f7~+fr$f+5WPJJAT{a?00NGY@O`Q?El%8+s4@L*kq7B?c`YL_~yuQEQX$e z#qPAdv0gV{uDeq^qdK_qNZFE-e~OM2Nb*PL<>al*|FghUc&%hhMMSOG`VMANFA)z{ zy8?Gct!VV5rL05u&i9gjCjFW8x^t6+n$}2DMcmzphXIFFnB)$zN67Y1^)z?p0mbXG z?}W)92OTe*ZQUb1H^Fz9?VacA>YoWu(@epDbs>j{4oQ0}Ett$1DWzOqmCvzpaLrCDJ+?mFRrDEv(jqTaF$ z)i2%1Kuhr3u*?W5a!!OP>~(14kg~vdy+);%&Z3%PZtkv+aJ%hutvk(4P42pLrYGhf zmTfSN@0{(l{hFh@GXlCo>wvQE;aK3f<%n@Ea{h3BboO>`ckHyUw>5&KdZ{Velu%b# zdle?#{HSbKak2$9?kl>V?poD(bo*<~2gJ3H2nx_E>c|JeW~Qs>x+5OUiw)tL4RZE(b#i@o_4h=1 zUwB%0&Uk{n5#F2L(f(-ejIbD=AgYvwsE_IY4PF;+h>VV06frF9L(mhQPvNJ3BNP3Y z^P72i?U*WG#excBMQl~`n$N~<=H<4Nj-#$So>bp!_JE+l-%@)d7lD(A2$&vnE8=YQ z(fVm|WZa^dDN&)}LeNIt1$hdUfLMJ?o%5`1O*3ofSMRI3S2d$LrZ&G$12)`7w%+#R zj$_V?u14-~_kNepdDi)db11y!hByy{HFBRV!@Ak}#j*$zQ*&xRR`;zcs_>QPmCY@U zD|u6tQ1~mqZT=2O4_5;BIJ3B@;z`{o=Pz~y6{%K-42xmn@3o;j+qxd^5zymJmoFXr zwJm8@6o*HQGb~Z8r#qwIF7iL~cJ)?xCV1vS@^P1Ep0~C4yZeyqU#A)9MUgw$+uHw& zZGarbFVg#EU(|^PJg9r{hTv~OzaSrb2v`w9l1jW60+?2wV#i1LbpJCKm`0lpni$h| zu&*wF&-$$?+kD1aZ1=c2`k~@Pu8{53+JgR$7!i{ex4dEFh7B6buivuXhtQvf64hAA zT6_$j?(5(#x4VJp+H3s{dw|_8k*AesruVQ<=~wtSc-woQyU)V?G{@f4F0nVZKd`T` zU$&>)5yxzBz@E0Mz^+nW+pYS2<=l#p@&~0%@%f_H1+x4hc_(tI-1#{Ja&G6e$ZeBX zSMYD?i<(0=oB^^-`)B0x#$DS5b-&+xNZtMUUyN{Xh_7bgw9DBa|?EN1H9p$>)Xj? zhu;cY8~QY`i(#SKC^OOh!B&v!Z|Ep@teDLh_^a7Sp>dt|R^+{J#n3=W973u2h z`sPx*Kf615tZvMG%;~Z}vwk#B0wU;2ZA6W=>TKo1ihnDHS6rzWRdK!CTY9f}Kw)iO zqugPTSbUlNF?(HhTF&bH&n3re9ywWjh<0kdqNeu}GP;fId8OyrZqGXZ)8=X8`}GC} zCa4sW=hO;tJ{Ux0Fe&r{Z6uP>Wo%o|Uk;1)jwRGmX>IH5;d>(-q;Pq;`ltR^z!zXv zj;W0DMCpFf0=hr_ik<=h(N* z5C1kMfqlj~A$eCQoZ}nwJGlSYhfJ|w#*FjxzSF+5zBNAHJH&U{JKa+OEZz~Ai8{`h zQhlIuV0l7mP)S8mqoVpn?~4|dXv!LvZ-7K|Qb|;yKJRz7E-NwXVPHgt*av( zd$4cXx6%7rw@O;pV_lE(q{kgHnx)qtA8{(MgSJ+&Q8rofQOrrU$R5hKfw|(R*n@jG zg_m=zv-P#dx)1n^1rsqvY>_^Zjgv>o3#Dhom%%f18z?jIRAMUfis+o|U!_siN0lw# zB7R7S&{8gm>Ff*ew)14TYMh6h+0KtHk9#QOlsVrqzZxvH6kmcQ5GUw(sb29wRjvKc zKn8^b-wE0nRAlgI_A8??(c(X-OsoV}xkuPW_8lZ*baFf^ z0blO9CYecM`fR#Se}>(@AFF5Uxx?r<@vk6Syihh?In8{BrM>lO>oThpiz4MnS%1+G zvM=Tb?qp%C4dj(9ha{97sJNe`^GuAfQ?Jxb)fB5%cHZr{-af9~t7A@w5A1kOb=s(^ zIv1$A!+JHclkBi+-`e`NC9K7>*`Zn6l-*R&ENY2saczlc@ddA(SuNLF7PN%4G&kor z4QcwT@l(_7);+3B(@p%d)Z5C}A>O&jIm&5=ox63sc^_pr;3>XTTmrRQtKx?8q!O8J zF^e$!SDCCRmfewTO^^8m*o59(S$0LYMkiD}Ev%=y~Bsl9XfZe|jd}zQ;cRcvLCO{el3F!}4 z0m1!^ae&k<1zo^k!WF-b8o4p-cffGnfPwLa@nZjC3h6yi(QsiE>^*Q0z64HY{g`S~ zk@2lD+4zrfqe%+BLe((tEa^!47@b0|rr*-HnG~jizD}=UwlL*1P5;a2X&LRu>;*sa zA&|MjFqdhGNkY#De~uz}IxRB!8PA$*m^a3F!$NQ}W(+?~yVyDOX`?GBU#%bsYZ!XM zJF}KheG&r~`Uq6Y_H#=9IJOu?b7!G)w;fSXqv?&cgYRt_)H~mzV<;4Vh-YKDXaY8q zAPHyi{+&d8CJMlxtvh7PEX2+5I(#6}oe<&|fg$i5u*p(>B^m^oX4><@@%2pAH_yhcJd@Cmb=HBAyDu_N{$Ws0LX+pFfSURDm()F z$Vs^|+)h54U&i5lDm$3z#-OLz>DLsP{G}R8qKD&d-yJV6nma^1=dImdy5?eEij+)ptqU=7@iZNR(Owe z7)J;l1%A>3;CL^DG4PHrWvv0J5@!QxksO>%CU$Ji)D>Q+;hX+E=&jKtPeFDGqx6FH92%fa}2peKH z-%cypDNyyVA(rA@@xNgde&TPy`3wd%PX>&SCNvZ<)j^P{vDrSko0iT@(zSV>I7uYx$p%;kpkd5~n+MpEBBpeXf zKA?URf_rmM&{deQ0`3F5~DqTj#??>Bz}&Sf5dfVs^a0|wDzlta+`21vtt3c8jZ z*dSQ*h=~qxAO8$|RF+>4YngSB7UqKnvCAOMuakX&>ha^CK1k#1AO+9FwF9a}vm22E zQZOFO89=35xm<9K%p%|5d7zik!an>QIA1@37VZH_K|KJmc?24!bNDbM<0i5T`B_k< z{(;;vM>daZ$4c>ZeljECzp+!08>R+)ErHDfwEi-`88oG_fMU<#-@uqs@vhu;m}R`d z8Dc$e0poc%mcaGo?eS5NB{vA}F99C|m0ov#7gUi$`1RnPhUzdw~c0W=NX)!mnYyxj(@(d?WS+c>ljS zM^1^A17a!%@Be9-k?qOZnP?=}pC1U3e0WhHM^AmgCBymfiLv}K7-AK`%s8KVWsS3Y%yRqAF)Gd6X%RQ!>zINTqa;RU9b#( zGjLG@*}bR^R&sNJ(tQ;679&im7{8DT@ zRF2<6Uyp{};Yq-9euCfoNE89SNIRiM-yN@IW^!AxQtl?Mq>4b1dAIdjldZw3ay8sJc@ zLBsJa;6VHpPlB8nIiCl!az9|;nn5pwbE8lnek6AR@X&?W5;Py$ZUCDIGjc57740W9 zFw43FPE?JphIEB;;ymhPpK%P#^nv_8vnma}V(VG#nd-I@m|Vd*p&;vV({=$wJ~2VDO(2!E|B+ zh$q+_xRdReJ4{dDn2l$ffk}0NeMe7+M2r4RZ{T&CVLe$JrYH7|4}~#19(Z`8*q-2b zvkz4O=lGx?RB#uc$@D{>*eizRW!RsD6wt&+SP>*g95mek2cE@TS8%Z(!H;9#F?Q$^ z?S}nL7T{K#2QwBt>MsH(x|ypW#$q!e&!LSu%lrv5Nndbq{fz~pVZa!S;Zrb#LFyy% z3K)gE7;}6hsH;B;cMze1U67nr4KC0-A@Q%C6~kCjF~=Y?W(Zn@J9G7P5BdnK1;g+h z@M-7Ry|fzm)8=5)5zn6mFY$$#ifBhS!3p#r^;9sJFfu`W8+M0z1~}|-;w6{D2jeA> zSoYQ=V7J4ZTF6g=RZS%u#Ka;2BS1%iM_bl3RhT z1N7l2Ab=~FldO^318Y12%O^@;1>?q!L1og3(m|p^Vk^Z6MPwDQ7EJ7Q?i{2{d2xH` zd3t~CCc`1-EDB)NeVZW(-zZjy-eJQG zQqVXttR1-WF2T2RuIwaJG+T^)#Ydo1$eQScXZ`~2D-%jf*?;*c;PVV5-r%PJ`F4YB z^l!L==#A@519gnS9ulF(Gc7Q7yU_>O4&x;wLA%gi+)(T^d7m&xiJ;n92%77y!VvK) zY6muw>q9rtw!EHLMYMAnOo8btY=F;kuleo3J=%yLWf?Bxzx)#X7U+jR;5tYw%-{>K zD}+7aEqF%FCLZufkb{_lJ%BZR1ExSv+5Jo_9DI_hPzE!CpABh2^9UQt zPxy$$@MCBamP>sUuLYm%PJAcN;pfT6f_AbOJbe_9H`&bV*|#Qxkz{@WZehWe7_J$z zO!JvP;l8WGCSqx9J9e8`L)8eg2oG3kkHiu|k<-pBGR-p0VfzDKldYQt>eBu?iK)Vv zuRYrt-EyMML3LTvrroOEt!hz~YMOKk$QM2X*>~yuRM;(nvnTI}IpdyGyeLl?ME-zP z-$tkysfY~x7cQsP!@lGL_zM{%gkrq8pXGe3LDq|GS^FHvT&G?xxNE*kzS9|p9*#jy zxen7Dde{xOwzLwN4NwsBMrp2;k_;4ClO5c8c0BO!s^Dqv$t*Q>WzNzO;H|t#_eI}V zzfgBr6WbZu64>Ie9{WqkX>hLyM=%N?|xQ z$W*Ugt=XYFra!9gY^`dzQ7fy_)&(?nLRFD$_HAhTeW3YRJF6P4iPN@fPpDUSCUwqM z2X>0v^V+N1eOr^jC074yciGQhOi^v_pXuLHzI?0udiCqQq@jt`AO3y+_=EA|>U8&_ z;>Kk1tJ2==1xcb==AmBGqrwOJ56p>u7#rS! z;Pbv#Lo)g%M!gP85B)prc*K*43*qG<*+KfQt#%4|Z(%O^llmwcD0oTxw#HO#tXy3; zt0hq#s3}sps_^#6j*BmdEXz9eX>p+S=LocHZkX+5b_1rGI0W2TqN)Z)``| z`PrFSRVvnq-v9?F3KAJEYbWS;8ZPQLYl}KNT2Hr3@2JxVO)-2n>|d*xf4HuAJ{cn3 zA>AQ6ArF*qlk^atq^6Q(kVfmt-Pg`(*;@a+)~&i8|BY&y7w?6n_gADQ#R!qD;f(qqYyY7cnAW zW_L}{-Qa%%M+VgSC3=3h+pe%8o#>CoEwGaN#*S3E)^8}E`)g*++U7l4mNprSGzQHF zqlz~QUYo@`+PcX+cDTp5EOg3u?CD&CBe|D^b6tbS$2E$=UkJ%4NnHAv7ERmJTW-Dzi7h3kPe4wZk&7jsm z#y~RJADeI7*EXZxvSw+GXU)0FwsMUweH`nrDj8wyXC{ALE<_}jLA(o8nLQRNB>q)Te0ew`bOuXR(V@X z`*xMNaRrw`>>$5VEkdE>v7*>4)KYCT!p_>R(<;WSM#>Ap`CoEs(kG=&PFwNaDWhlRweLX8ysp%>3_^>kb- z%R`feUY1FI&4U_8y7c!BF6c7Q=STOafewBz{2uy!^R9I?DRV>{*fc{K*PWP$aXN8R zL*>NE*!sRL5_J!#u#M5YYIoG%;@)8=gqy7Txw0Oc+|D}!|u8-bBXs3 z_8ZkT)8}QE1lP}2V(Dy&mT%FN>D&1a_*&EY*6ix7<=v}N8va%Brf72eY3*lGn7}sgqV_2q} z1(|y~Z4h`a6l!;9agCWutE2ekpd$H6)Icu$FTq>MLFE#2sl`^a_wr@Z4B>5Z93GCZ zLv5xyb+6Wz`p~KiV5$sUT*JPR{M1LzV^B2lkMf^Vr_9;d{TUatyQlv zJcLBUP(x*t96aOls(hNH+L2J(4%IAbTdMwLx`wiZ3oI5nk}kzgmG%bEmuno~dCcl2 z@$GbXx0`P{Ksi@dC9Ri#myH(Gqj>tA;SW&dR~r@^HW!mYfu`;7=AEMF%hoE zUJ%{dym+JVB+(BlK|$;b{nO6TEw+v6zw7IKYpLqJrcDLD6Ycz(CMjrs2R?9ahJe&pC>?@AXWf6hiGAH`*Jv1B&$2XM0LrSrSq?a?bH z#rL5@xSi77&1;-{dzUZXYdp;yKU@8rOHY_*T(NQLYex7MBdxwvJL>>{;W;r~HXr&%W{*WCN-6JcY zihLh9rJu0HydSO9Cu=6Crgn^OJJJ-{G_bj$VMRSvmsIniW>bxCo!jrAx>;3tg{k6p zt$Y3J>QU9Jt1nbC#}y}$15sDeZ{Vd%Vyur-dc6D z8E;!;wG+HQv(48kvt@De#pX#CYNfll2X&mdg*T9D@k?2iz!Tq!jm3h<7IGE%K}``q zkc<+g3jBrD!oP$_bU@q)9)^zM)xsMBTR|qZlSsoWAj{_zKa#bDI$aUe<{!}~=^fxN zP-(KGGhhe*n)$-LM{#&1R4whW+pt@|2}uP$tO9wjTNj!$Eh`H1@H|@Rc4Uwkgr;&j(|IC zzk0X&nfir#gl3ZFyT(eJukE3qYt+#HvhmzHNQH+SIlPQ)7G#S?0_yl$R4%fSxJrt| zO`*auFz8CE^HT! z6qv}7-~sdpRO6Rm^FdoP32G??+;#2jAZkc?Oj)!h%kqVEFLbqnCo2SR0b1Jq^v0WbP9)S}}dRWS+hmb2U@s4cDM zTER1}KhJV8++#M8od>ynL7a#yhw6wA=)2uH6YR2@|NoORs1#L!^XEWt+qnoHn5%*5 z-Iu+z~HgLk;x= zs2QRmA#pYM<~`v~puey!Xgl8yyTP+iEfGMDTn|uLH$$~M64ZcD|A#tv79_!KgXF_5 z;HQ+x?t?a z6dHpK!!?jPc$-{HPKMM+D{v403_fw0XfG&n9r?q|vH$P6r?CH+#HbLznWD- z(rhoT0ADM3fOX@p(>LjVxNU&SsX!gjLEmF?cq?)Q`1ySU=4>PCi`DY=fWRH+(m2pQ zb2;oFxC6IB=KLMtaYph&z8ZS;4Di46`ElHNHkEzI-^A7*u3CG6)3%7zD!L7kU!7FJQ@I{C5 zW!MDJ7xy7HLra9AYjCtHP^0pvN{I=;{LcBHst5*M z6U!)}+IEhp#=3KHPyv4iwf|t=gFDJ!!7g&^A>HyiHUd;Y=dle~99GUna--n=KXQk# z0)8Vi9G)bQsfc-_o7_Kui~I$dmX+WO=YnY$UkDO5Ca|{kC6cB&>7I6#e+6y zKeSaOm&FG8nN_GJOrw@cj5jRhO9vk&azoh zsea3?XDpC46^p&3w*YVYDc=XU`cL`GtR0b!9BE)&P&-hFt_v?04P;Lm=b{nBV7`^% znLbc|+Q+59XcB_HWE?+|Y2)mP9#G?Vh8bWJKa~3i#?)Ibi2uz=usX=i&Bs!Kk-i$( z$ui%!vdQ3Y&}elSP0ajKH#BJ{bvG!52{VnhCjKe*hQW18NU)&>x(_ zqOkKYlMVy?)EfIQNoy5gSv!#jB&99@#kLA~{8m^hD3Po{9XJmt`)C^4EcsPGMn6q{R9z^mF_>X)bC>HwxQ#}s6fTzJ<_yT@s z58BnEfRx(-LcSel{>^ZdeUNOq6tpO(;k%{q`;Q=#^CkS=0??elfg_%Q8KfAFm;g=* zpJ2{y10UiP(7?$7-#gELfoDVuXe6^ihm-^u^xW8lkf93UjF%#*ZzaGC^?l8aU!`sIA$+i17uKFbWWep~xKp7X1I5PhU_= zh9M>V%o9GlAXmsewFT5t0mrk4*Z*=!75{&&gLkinbE$%N`2iWH%>UC(dBA_b5D1Q8 z4X^+GB|;=1Q5>8D3uoI1Pm@}>(tLR4C&BfkgW9tQo_;m(0X~P2n))BcEQj~$fTtk_ z3IQXuMl)Q!2#y0321s5N0~T!u$7gscy#AM^Y7bXUK!T|Uyi*wJ2HKsGC=#A2gF$!G zAHE8Ks~Qd8_ky4Nm*yG+|JMz!$Ql058d|~{u9$%|R2O6p|Kt4s_Mrg14~87r!T zFtkJ%pd5C9?4ti)mo54YS;w`|UnFXPig)3Ejug-i^^mx#hZ*G+T&)gT=f7vVCA73T z{B8i8i5VQ#68eaMv!&pbgfo8*{ayne4r#m*`e!>-y~|-NB;h86e<7pvxHUh5970!Pmy!(I2t^)wk ziiTDQgPyj95kNs7J3^}o5DjPP3@xjGKJO0gY!Bxc11;7I4S~P%0DrS66aWqz70|n4 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 z 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 z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk z1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs z0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZ zfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&U zAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C7 z2oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N z0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+ z009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBly zK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF z5FkK+009C72oNAZfB*pk1PBlyK!5-N0t5&UAV7cs0RjXF5FkK+009C72oNAZfB*pk a1PBlyK!5-N0t5&UAV7cs0RsPLfxiJl5xExt diff --git a/Integration/inputs/utterance_time_success.opus b/Integration/inputs/utterance_time_success.opus new file mode 100644 index 0000000000000000000000000000000000000000..9c1198ba203b2a85315d474d86c82914912d55fe GIT binary patch literal 28880 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%0Dea?U%^M;}P8yB)LB_zgYt27n}FTcp%Qrhj^G{) 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 + m_focusManager = std::make_shared(FocusManager::DEFAULT_AUDIO_CHANNELS); m_testContentClient = std::make_shared(); ASSERT_TRUE(m_focusManager->acquireChannel( FocusManager::CONTENT_CHANNEL_NAME, m_testContentClient, CONTENT_ACTIVITY_ID)); @@ -367,14 +368,21 @@ class AlertsTest : public ::testing::Test { auto alertsAudioFactory = std::make_shared(); - m_alertStorage = std::make_shared(alertsAudioFactory); + m_alertStorage = capabilityAgents::alerts::storage::SQLiteAlertStorage::create( + avsCommon::utils::configuration::ConfigurationNode::getRoot(), alertsAudioFactory); m_alertObserver = std::make_shared(); - auto messageStorage = std::make_shared(); + auto messageStorage = + SQLiteMessageStorage::create(avsCommon::utils::configuration::ConfigurationNode::getRoot()); + + m_customerDataManager = std::make_shared(); m_certifiedSender = CertifiedSender::create( - m_avsConnectionManager, m_avsConnectionManager->getConnectionManager(), messageStorage); + m_avsConnectionManager, + m_avsConnectionManager->getConnectionManager(), + std::move(messageStorage), + m_customerDataManager); m_alertsAgent = AlertsCapabilityAgent::create( m_avsConnectionManager, @@ -384,7 +392,8 @@ class AlertsTest : public ::testing::Test { m_exceptionEncounteredSender, m_alertStorage, alertsAudioFactory, - m_alertRenderer); + m_alertRenderer, + m_customerDataManager); ASSERT_NE(m_alertsAgent, nullptr); m_alertsAgent->addObserver(m_alertObserver); m_alertsAgent->onLocalStop(); @@ -529,7 +538,7 @@ class AlertsTest : public ::testing::Test { std::shared_ptr m_speechSynthesizer; std::shared_ptr m_alertsAgent; std::shared_ptr m_speechSynthesizerObserver; - std::shared_ptr m_alertStorage; + std::shared_ptr m_alertStorage; std::shared_ptr m_alertRenderer; std::shared_ptr m_alertObserver; std::shared_ptr m_holdToTalkButton; @@ -539,6 +548,7 @@ class AlertsTest : public ::testing::Test { std::shared_ptr m_AudioBuffer; std::shared_ptr m_AudioInputProcessor; std::shared_ptr m_userInactivityMonitor; + std::shared_ptr m_customerDataManager; FocusState m_focusState; std::mutex m_mutex; diff --git a/Integration/test/AudioInputProcessorIntegrationTest.cpp b/Integration/test/AudioInputProcessorIntegrationTest.cpp index b6348f3caf..334d3f704b 100644 --- a/Integration/test/AudioInputProcessorIntegrationTest.cpp +++ b/Integration/test/AudioInputProcessorIntegrationTest.cpp @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -/// @file AudioInputProcessorTest.cpp +/// @file AudioInputProcessorIntegrationTest.cpp #include #include #include @@ -99,7 +99,8 @@ static const std::string ALEXA_JOKE_AUDIO_FILE = "/alexa_recognize_joke_test.wav static const std::string ALEXA_WIKI_AUDIO_FILE = "/alexa_recognize_wiki_test.wav"; // This is a 16 bit 16 kHz little endian linear PCM audio file of "Alexa" then silence to be recognized. static const std::string ALEXA_SILENCE_AUDIO_FILE = "/alexa_recognize_silence_test.wav"; - +// This is a 32KHz little endian OPUS audio file with Constant Bit rate of "What time is it?" to be recognized. +static const std::string TIME_AUDIO_FILE_OPUS = "/utterance_time_success.opus"; // This string to be used for Speak Directives which use the NAMESPACE_SPEECH_SYNTHESIZER namespace. static const std::string NAME_VOLUME_STATE = "VolumeState"; // This string to be used for Speak Directives which use the NAMESPACE_SPEECH_SYNTHESIZER namespace. @@ -149,7 +150,10 @@ static const std::chrono::seconds LONG_TIMEOUT_DURATION(10); static const std::chrono::seconds SHORT_TIMEOUT_DURATION(2); // This Integer to be used when no timeout is desired. static const std::chrono::seconds NO_TIMEOUT_DURATION(0); - +// The length of RIFF container format which is the header of a wav file. +static const int RIFF_HEADER_SIZE = 44; +/// The compatible sample rate for OPUS 32KHz. +static const unsigned int COMPATIBLE_SAMPLE_RATE_OPUS_32 = 32000; #ifdef KWD_KITTAI /// The name of the resource file required for Kitt.ai. static const std::string RESOURCE_FILE = "/KittAiModels/common.res"; @@ -230,7 +234,7 @@ class holdToTalkButton { } }; -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) class wakeWordTrigger : public KeyWordObserverInterface { public: wakeWordTrigger(AudioFormat compatibleAudioFormat, std::shared_ptr aip) { @@ -438,7 +442,7 @@ class AudioInputProcessorTest : public ::testing::Test { m_tapToTalkButton = std::make_shared(); m_holdToTalkButton = std::make_shared(); - m_focusManager = std::make_shared(); + m_focusManager = std::make_shared(FocusManager::DEFAULT_AUDIO_CHANNELS); m_dialogUXStateAggregator = std::make_shared(); m_contextManager = ContextManager::create(); @@ -476,7 +480,7 @@ class AudioInputProcessorTest : public ::testing::Test { ASSERT_TRUE(m_directiveSequencer->addDirectiveHandler(m_AudioInputProcessor)); -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) m_wakeWordTrigger = std::make_shared(m_compatibleAudioFormat, m_AudioInputProcessor); #ifdef KWD_KITTAI @@ -581,7 +585,7 @@ class AudioInputProcessorTest : public ::testing::Test { std::shared_ptr m_TapToTalkAudioProvider; std::shared_ptr m_HoldToTalkAudioProvider; avsCommon::utils::AudioFormat m_compatibleAudioFormat; -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) std::shared_ptr m_wakeWordTrigger; #ifdef KWD_KITTAI std::unique_ptr m_detector; @@ -591,9 +595,8 @@ class AudioInputProcessorTest : public ::testing::Test { #endif }; -std::vector readAudioFromFile(const std::string& fileName, bool* errorOccurred) { - const int RIFF_HEADER_SIZE = 44; - +template +std::vector readAudioFromFile(const std::string& fileName, const int& headerPosition, bool* errorOccurred) { std::ifstream inputFile(fileName.c_str(), std::ifstream::binary); if (!inputFile.good()) { std::cout << "Couldn't open audio file!" << std::endl; @@ -604,23 +607,24 @@ std::vector readAudioFromFile(const std::string& fileName, bool* errorO } inputFile.seekg(0, std::ios::end); int fileLengthInBytes = inputFile.tellg(); - if (fileLengthInBytes <= RIFF_HEADER_SIZE) { - std::cout << "File should be larger than 44 bytes, which is the size of the RIFF header" << std::endl; + + if (fileLengthInBytes <= headerPosition) { + std::cout << "File should be larger than header position" << std::endl; if (errorOccurred) { *errorOccurred = true; } return {}; } - inputFile.seekg(RIFF_HEADER_SIZE, std::ios::beg); + inputFile.seekg(headerPosition, std::ios::beg); - int numSamples = (fileLengthInBytes - RIFF_HEADER_SIZE) / 2; + int numSamples = (fileLengthInBytes - headerPosition) / sizeof(T); - std::vector retVal(numSamples, 0); + std::vector retVal(numSamples, 0); - inputFile.read((char*)&retVal[0], numSamples * 2); + inputFile.read((char*)&retVal[0], numSamples * sizeof(T)); - if (inputFile.gcount() != numSamples * 2) { + if (static_cast(inputFile.gcount()) != numSamples * sizeof(T)) { std::cout << "Error reading audio file" << std::endl; if (errorOccurred) { *errorOccurred = true; @@ -642,12 +646,12 @@ std::vector readAudioFromFile(const std::string& fileName, bool* errorO * AudioInputProcessor is then observed to send a Recognize event to AVS which responds with a SetMute and Speak * directive. */ -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) TEST_F(AudioInputProcessorTest, wakeWordJoke) { // Put audio onto the SDS saying "Alexa, Tell me a joke". bool error; std::string file = inputPath + ALEXA_JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -693,12 +697,12 @@ TEST_F(AudioInputProcessorTest, wakeWordJoke) { * To do this, audio of "Alexa, ........." is fed into a stream that is being read by a wake word engine. The * AudioInputProcessor is then observed to send a Recognize event to AVS which responds with no directives. */ -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) TEST_F(AudioInputProcessorTest, wakeWordSilence) { // Put audio onto the SDS saying "Alexa ......". bool error; std::string file = inputPath + ALEXA_SILENCE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -738,12 +742,12 @@ TEST_F(AudioInputProcessorTest, wakeWordSilence) { * AudioInputProcessor is then observed to send a Recognize event to AVS which responds with a SetMute, Speak, * and ExpectSpeech directive. Audio of "Lions" is then fed into the stream and another recognize event is sent. */ -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) TEST_F(AudioInputProcessorTest, wakeWordMultiturn) { // Put audio onto the SDS saying "Alexa, wikipedia". bool error; std::string file = inputPath + ALEXA_WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -792,7 +796,7 @@ TEST_F(AudioInputProcessorTest, wakeWordMultiturn) { // Put audio onto the SDS saying "Lions". bool secondError; std::string secondFile = inputPath + LIONS_AUDIO_FILE; - std::vector secondAudioData = readAudioFromFile(secondFile, &secondError); + std::vector secondAudioData = readAudioFromFile(secondFile, RIFF_HEADER_SIZE, &secondError); ASSERT_FALSE(secondError); m_AudioBufferWriter->write(secondAudioData.data(), secondAudioData.size()); @@ -834,12 +838,12 @@ TEST_F(AudioInputProcessorTest, wakeWordMultiturn) { * and ExpectSpeech directive. Audio of "...." is then fed into the stream and another recognize event is sent * but no directives are given in response. */ -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) TEST_F(AudioInputProcessorTest, wakeWordMultiturnWithoutUserResponse) { // Put audio onto the SDS saying "Alexa, wikipedia". bool error; std::string file = inputPath + ALEXA_WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -886,7 +890,7 @@ TEST_F(AudioInputProcessorTest, wakeWordMultiturnWithoutUserResponse) { // Put audio onto the SDS saying ".......". bool secondError; std::string secondFile = inputPath + SILENCE_AUDIO_FILE; - std::vector secondAudioData = readAudioFromFile(secondFile, &secondError); + std::vector secondAudioData = readAudioFromFile(secondFile, RIFF_HEADER_SIZE, &secondError); ASSERT_FALSE(secondError); m_AudioBufferWriter->write(secondAudioData.data(), secondAudioData.size()); @@ -941,7 +945,7 @@ TEST_F(AudioInputProcessorTest, DISABLED_tapToTalkJoke) { // Put audio onto the SDS saying "Tell me a joke". bool error; std::string file = inputPath + JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -975,6 +979,52 @@ TEST_F(AudioInputProcessorTest, DISABLED_tapToTalkJoke) { } } +TEST_F(AudioInputProcessorTest, tapToTalkTimeOpus) { + m_compatibleAudioFormat.sampleRateHz = COMPATIBLE_SAMPLE_RATE_OPUS_32; + m_compatibleAudioFormat.numChannels = COMPATIBLE_NUM_CHANNELS; + m_compatibleAudioFormat.endianness = COMPATIBLE_ENDIANNESS; + m_compatibleAudioFormat.encoding = avsCommon::utils::AudioFormat::Encoding::OPUS; + + bool alwaysReadable = true; + bool canOverride = true; + bool canBeOverridden = true; + std::shared_ptr tapToTalkAudioProvider; + tapToTalkAudioProvider = std::make_shared( + m_AudioBuffer, m_compatibleAudioFormat, ASRProfile::NEAR_FIELD, alwaysReadable, canOverride, !canBeOverridden); + // Signal to the AIP to start recognizing. + ASSERT_TRUE(m_tapToTalkButton->startRecognizing(m_AudioInputProcessor, tapToTalkAudioProvider)); + + // Check that AIP is now in RECOGNIZING state. + ASSERT_TRUE( + m_StateObserver->checkState(AudioInputProcessorObserverInterface::State::RECOGNIZING, LONG_TIMEOUT_DURATION)); + + // Put audio onto the SDS saying "What time is it?". + bool error; + std::string file = inputPath + TIME_AUDIO_FILE_OPUS; + int headerSize = 0; + std::vector audioData = readAudioFromFile(file, headerSize, &error); + ASSERT_FALSE(audioData.empty()); + m_AudioBufferWriter->write(audioData.data(), audioData.size()); + + // The test channel client has been notified the alarm channel has been backgrounded. + ASSERT_EQ(m_testClient->waitForFocusChange(LONG_TIMEOUT_DURATION), FocusState::BACKGROUND); + + // Check that AIP is in BUSY state. + ASSERT_TRUE(m_StateObserver->checkState(AudioInputProcessorObserverInterface::State::BUSY, LONG_TIMEOUT_DURATION)); + + // Check that AIP is in an IDLE state. + ASSERT_TRUE(m_StateObserver->checkState(AudioInputProcessorObserverInterface::State::IDLE, LONG_TIMEOUT_DURATION)); + + // Check that the test context provider was asked to provide context for the event. + ASSERT_TRUE(m_stateProvider->checkStateRequested()); + + // The test channel client has been notified the alarm channel has been foregrounded. + ASSERT_EQ(m_testClient->waitForFocusChange(LONG_TIMEOUT_DURATION), FocusState::FOREGROUND); + + // Check that a recognize event was sent. + ASSERT_TRUE(checkSentEventName(m_avsConnectionManager, NAME_RECOGNIZE)); +} + /** * Test AudioInputProcessor's ability to handle a silent interation triggered by a tap to talk button. * @@ -992,7 +1042,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkSilence) { // Put audio onto the SDS saying ".......". bool error; std::string file = inputPath + SILENCE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1060,7 +1110,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkNoAudio) { * AudioInputProcessor. The AudioInputProcessor is then observed to send only one Recognize event to AVS which responds * with a SetMute and Speak directive. */ -#ifdef KWD +#if defined(KWD_KITTAI) || defined(KWD_SENSORY) TEST_F(AudioInputProcessorTest, tapToTalkWithWakeWordConflict) { // Signal to the AIP to start recognizing. ASSERT_TRUE(m_tapToTalkButton->startRecognizing(m_AudioInputProcessor, m_TapToTalkAudioProvider)); @@ -1072,7 +1122,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkWithWakeWordConflict) { // Put audio onto the SDS saying "Alexa, Tell me a joke". bool error; std::string file = inputPath + ALEXA_JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1125,7 +1175,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkMultiturn) { // Put audio onto the SDS saying "Wikipedia". bool error; std::string file = inputPath + WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1170,7 +1220,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkMultiturn) { // Put audio onto the SDS saying "Lions". bool secondError; std::string secondFile = inputPath + LIONS_AUDIO_FILE; - std::vector secondAudioData = readAudioFromFile(secondFile, &secondError); + std::vector secondAudioData = readAudioFromFile(secondFile, RIFF_HEADER_SIZE, &secondError); ASSERT_FALSE(secondError); m_AudioBufferWriter->write(secondAudioData.data(), secondAudioData.size()); @@ -1220,7 +1270,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkMultiturnWithoutUserResponse) { // Put audio onto the SDS saying "Wikipedia". bool error; std::string file = inputPath + WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1265,7 +1315,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkMultiturnWithoutUserResponse) { // Put audio onto the SDS saying ".......". bool secondError; std::string secondFile = inputPath + SILENCE_AUDIO_FILE; - std::vector secondAudioData = readAudioFromFile(secondFile, &secondError); + std::vector secondAudioData = readAudioFromFile(secondFile, RIFF_HEADER_SIZE, &secondError); ASSERT_FALSE(secondError); m_AudioBufferWriter->write(secondAudioData.data(), secondAudioData.size()); @@ -1330,7 +1380,7 @@ TEST_F(AudioInputProcessorTest, tapToTalkCancel) { // Put audio onto the SDS saying "Tell me a joke". bool error; std::string file = inputPath + JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1362,7 +1412,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkJoke) { // Put audio onto the SDS saying "Tell me a joke". bool error; std::string file = inputPath + JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1417,7 +1467,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkMultiturn) { // Put audio onto the SDS saying "Wikipedia". bool error; std::string file = inputPath + WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1464,7 +1514,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkMultiturn) { // Put audio onto the SDS of "Lions". bool secondError; file = inputPath + LIONS_AUDIO_FILE; - std::vector secondAudioData = readAudioFromFile(file, &secondError); + std::vector secondAudioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &secondError); ASSERT_FALSE(secondError); m_AudioBufferWriter->write(secondAudioData.data(), secondAudioData.size()); @@ -1522,7 +1572,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkMultiTurnWithSilence) { // Put audio onto the SDS saying "Wikipedia". bool error; std::string file = inputPath + WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1575,7 +1625,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkMultiTurnWithSilence) { // Put audio onto the SDS saying ".......". bool secondError; std::string secondFile = inputPath + SILENCE_AUDIO_FILE; - std::vector secondAudioData = readAudioFromFile(secondFile, &secondError); + std::vector secondAudioData = readAudioFromFile(secondFile, RIFF_HEADER_SIZE, &secondError); ASSERT_FALSE(secondError); m_AudioBufferWriter->write(secondAudioData.data(), secondAudioData.size()); @@ -1643,7 +1693,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkMultiturnWithTimeOut) { // Put audio onto the SDS saying "Wikipedia". bool error; std::string file = inputPath + WIKI_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1759,7 +1809,7 @@ TEST_F(AudioInputProcessorTest, holdToTalkCancel) { // Put audio onto the SDS saying "Tell me a joke". bool error; std::string file = inputPath + JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); ASSERT_FALSE(audioData.empty()); m_AudioBufferWriter->write(audioData.data(), audioData.size()); @@ -1791,7 +1841,7 @@ TEST_F(AudioInputProcessorTest, audioWithoutAnyTrigger) { // Put audio onto the SDS saying "Tell me a joke" without a trigger. bool error; std::string file = inputPath + JOKE_AUDIO_FILE; - std::vector audioData = readAudioFromFile(file, &error); + std::vector audioData = readAudioFromFile(file, RIFF_HEADER_SIZE, &error); ASSERT_FALSE(error); m_AudioBufferWriter->write(audioData.data(), audioData.size()); diff --git a/Integration/test/SpeechSynthesizerIntegrationTest.cpp b/Integration/test/SpeechSynthesizerIntegrationTest.cpp index 748b77f842..11a4808495 100644 --- a/Integration/test/SpeechSynthesizerIntegrationTest.cpp +++ b/Integration/test/SpeechSynthesizerIntegrationTest.cpp @@ -340,7 +340,7 @@ class SpeechSynthesizerTest : public ::testing::Test { ASSERT_NE(nullptr, m_avsConnectionManager); connect(); - m_focusManager = std::make_shared(); + m_focusManager = std::make_shared(FocusManager::DEFAULT_AUDIO_CHANNELS); m_testClient = std::make_shared(); ASSERT_TRUE( m_focusManager->acquireChannel(FocusManager::ALERTS_CHANNEL_NAME, m_testClient, ALERTS_ACTIVITY_ID)); diff --git a/KWD/CMakeLists.txt b/KWD/CMakeLists.txt index 19d6fd1430..4d5e16e20e 100644 --- a/KWD/CMakeLists.txt +++ b/KWD/CMakeLists.txt @@ -7,6 +7,9 @@ acsdk_add_test_subdirectory_if_allowed() if(AMAZON_KEY_WORD_DETECTOR) add_subdirectory("Amazon") endif() +if(AMAZONLITE_KEY_WORD_DETECTOR) + add_subdirectory("AmazonLite") +endif() if(KITTAI_KEY_WORD_DETECTOR) add_subdirectory("KittAi") endif() diff --git a/MediaPlayer/include/MediaPlayer/MediaPlayer.h b/MediaPlayer/include/MediaPlayer/MediaPlayer.h index 8288075c52..8b143b171a 100644 --- a/MediaPlayer/include/MediaPlayer/MediaPlayer.h +++ b/MediaPlayer/include/MediaPlayer/MediaPlayer.h @@ -496,6 +496,11 @@ class MediaPlayer */ void saveOffsetBeforeTeardown(); + /** + * Destructs the @c m_source with proper steps. + */ + void cleanUpSource(); + /// The volume to restore to when exiting muted state. Used in GStreamer crash fix for zero volume on PCM data. gdouble m_lastVolume; diff --git a/MediaPlayer/src/BaseStreamSource.cpp b/MediaPlayer/src/BaseStreamSource.cpp index dd9cc8dbe0..bd31fcfcbc 100644 --- a/MediaPlayer/src/BaseStreamSource.cpp +++ b/MediaPlayer/src/BaseStreamSource.cpp @@ -51,6 +51,10 @@ static std::string getCapsString(const AudioFormat& audioFormat) { case AudioFormat::Encoding::LPCM: caps << "audio/x-raw"; break; + case AudioFormat::Encoding::OPUS: + ACSDK_ERROR(LX("MediaPlayer does not handle OPUS data")); + caps << " "; + break; } switch (audioFormat.endianness) { @@ -98,6 +102,17 @@ BaseStreamSource::~BaseStreamSource() { g_signal_handler_disconnect(m_pipeline->getAppSrc(), m_needDataHandlerId); g_signal_handler_disconnect(m_pipeline->getAppSrc(), m_enoughDataHandlerId); g_signal_handler_disconnect(m_pipeline->getAppSrc(), m_seekDataHandlerId); + if (m_pipeline->getPipeline()) { + if (m_pipeline->getAppSrc()) { + gst_bin_remove(GST_BIN(m_pipeline->getPipeline()), GST_ELEMENT(m_pipeline->getAppSrc())); + } + m_pipeline->setAppSrc(nullptr); + + if (m_pipeline->getDecoder()) { + gst_bin_remove(GST_BIN(m_pipeline->getPipeline()), GST_ELEMENT(m_pipeline->getDecoder())); + } + m_pipeline->setDecoder(nullptr); + } { std::lock_guard lock(m_callbackIdMutex); if (m_needDataCallbackId && !g_source_remove(m_needDataCallbackId)) { @@ -217,7 +232,6 @@ void BaseStreamSource::signalEndOfData() { .d("result", gst_flow_get_name(flowRet))); } ACSDK_DEBUG9(LX("gstAppSrcEndOfStreamSuccess")); - close(); clearOnReadDataHandler(); } diff --git a/MediaPlayer/src/MediaPlayer.cpp b/MediaPlayer/src/MediaPlayer.cpp index b58cb50080..84d97ca38b 100644 --- a/MediaPlayer/src/MediaPlayer.cpp +++ b/MediaPlayer/src/MediaPlayer.cpp @@ -157,11 +157,7 @@ std::shared_ptr MediaPlayer::create( MediaPlayer::~MediaPlayer() { ACSDK_DEBUG9(LX("~MediaPlayerCalled")); - gst_element_set_state(m_pipeline.pipeline, GST_STATE_NULL); - if (m_source) { - m_source->shutdown(); - } - m_source.reset(); + cleanUpSource(); g_main_loop_quit(m_mainLoop); if (m_mainLoopThread.joinable()) { m_mainLoopThread.join(); @@ -727,22 +723,7 @@ void MediaPlayer::tearDownTransientPipelineElements() { sendPlaybackStopped(); } m_currentId = ERROR_SOURCE_ID; - if (m_source) { - m_source->shutdown(); - } - m_source.reset(); - if (m_pipeline.pipeline) { - gst_element_set_state(m_pipeline.pipeline, GST_STATE_NULL); - if (m_pipeline.appsrc) { - gst_bin_remove(GST_BIN(m_pipeline.pipeline), GST_ELEMENT(m_pipeline.appsrc)); - } - m_pipeline.appsrc = nullptr; - - if (m_pipeline.decoder) { - gst_bin_remove(GST_BIN(m_pipeline.pipeline), GST_ELEMENT(m_pipeline.decoder)); - } - m_pipeline.decoder = nullptr; - } + cleanUpSource(); m_offsetManager.clear(); m_playPending = false; m_pausePending = false; @@ -1459,10 +1440,7 @@ void MediaPlayer::sendPlaybackFinished() { if (m_currentId == ERROR_SOURCE_ID) { return; } - if (m_source) { - m_source->shutdown(); - } - m_source.reset(); + cleanUpSource(); m_isPaused = false; m_playbackStartedSent = false; if (!m_playbackFinishedSent) { @@ -1567,5 +1545,14 @@ gboolean MediaPlayer::onErrorCallback(gpointer pointer) { return false; } +void MediaPlayer::cleanUpSource() { + if (m_pipeline.pipeline) { + gst_element_set_state(m_pipeline.pipeline, GST_STATE_NULL); + } + if (m_source) { + m_source->shutdown(); + } + m_source.reset(); +} } // namespace mediaPlayer } // namespace alexaClientSDK diff --git a/MediaPlayer/test/MediaPlayerTest.cpp b/MediaPlayer/test/MediaPlayerTest.cpp index 06fed4d415..fbdf0f1659 100644 --- a/MediaPlayer/test/MediaPlayerTest.cpp +++ b/MediaPlayer/test/MediaPlayerTest.cpp @@ -97,7 +97,9 @@ class MockContentFetcher : public avsCommon::sdkInterfaces::HTTPContentFetcherIn MockContentFetcher(const std::string& url) : m_url{url} { } - std::unique_ptr getContent(FetchOptions fetchOption) override { + std::unique_ptr getContent( + FetchOptions fetchOption, + std::shared_ptr writer) override { if (fetchOption == FetchOptions::CONTENT_TYPE) { auto urlAndContentType = urlsToContentTypes.find(m_url); if (urlAndContentType == urlsToContentTypes.end()) { @@ -123,7 +125,7 @@ class MockContentFetcher : public avsCommon::sdkInterfaces::HTTPContentFetcherIn std::promise contentTypePromise; auto contentTypeFuture = contentTypePromise.get_future(); contentTypePromise.set_value(""); - auto attachment = writeStringIntoAttachment(urlAndContent->second); + auto attachment = writeStringIntoAttachment(urlAndContent->second, writer); if (!attachment) { return nullptr; } @@ -134,16 +136,17 @@ class MockContentFetcher : public avsCommon::sdkInterfaces::HTTPContentFetcherIn private: std::shared_ptr writeStringIntoAttachment( - const std::string& string) { + const std::string& string, + std::shared_ptr writer) { static int id = 0; std::shared_ptr stream = std::make_shared(std::to_string(id++)); if (!stream) { return nullptr; } - auto writer = stream->createWriter(); + if (!writer) { - return nullptr; + writer = stream->createWriter(); } avsCommon::avs::attachment::AttachmentWriter::WriteStatus writeStatus; writer->write(string.data(), string.size(), &writeStatus); @@ -1412,6 +1415,10 @@ int main(int argc, char** argv) { alexaClientSDK::mediaPlayer::test::urlsToContent.insert( {alexaClientSDK::mediaPlayer::test::TEST_M3U_PLAYLIST_URL, alexaClientSDK::mediaPlayer::test::TEST_M3U_PLAYLIST_CONTENT}); +// ACSDK-1141 - Some tests fail on Windows. +#if defined(_WIN32) && !defined(RESOLVED_ACSDK_1141) + ::testing::GTEST_FLAG(filter) = "-MediaPlayerTest*"; +#endif return RUN_ALL_TESTS(); } } diff --git a/NOTICE.txt b/NOTICE.txt index 40bd06693b..04bbb6ae30 100644 --- a/NOTICE.txt +++ b/NOTICE.txt @@ -1,5 +1,5 @@ AVS Device SDK -Copyright 2016-2017 Amazon.com, Inc. or its affiliates. All Rights Reserved. +Copyright 2016-2018 Amazon.com, Inc. or its affiliates. All Rights Reserved. *************************** AVS DEVICE SDK COMPONENTS @@ -17,14 +17,17 @@ License, Version 2.0 (the "License"): - Certified Sender - Context Manager - Default Client +- ExternalMediaPlayer Capability Agent - Key Word Detector (KWD) - Media Player +- Notifications Capability Agent - Playback Controller Capability Agent - Playlist Parser +- RegistrationManager - Sample App - Settings Capability Agent -- Speaker Manager Capability Agent -- Speech Synthesizer Capability Agent +- SpeakerManager Capability Agent +- SpeechSynthesizer Capability Agent - Storage - System Capability Agent - Template Runtime Capability Agent @@ -43,7 +46,7 @@ limitations under the License. ALEXA AUDIO ASSETS ****************** -Copyright 2017 Amazon.com, Inc. or its affiliates (“Amazon”). +Copyright 2018 Amazon.com, Inc. or its affiliates (“Amazon”). All Rights Reserved. These materials are licensed to you as "Alexa Materials" under the Alexa Voice diff --git a/PlaylistParser/include/PlaylistParser/UrlContentToAttachmentConverter.h b/PlaylistParser/include/PlaylistParser/UrlContentToAttachmentConverter.h index e71ed21973..cef52530a8 100644 --- a/PlaylistParser/include/PlaylistParser/UrlContentToAttachmentConverter.h +++ b/PlaylistParser/include/PlaylistParser/UrlContentToAttachmentConverter.h @@ -50,13 +50,13 @@ class UrlContentToAttachmentConverter * Creates a converter object. Note that calling this function will commence the parsing and streaming of the URL * into the internal attachment. If a desired start time is specified, this function will attempt to start streaming * at that offset, based on available metadata if the URL points to a playlist file. If no such information is - * available, stremaing will begin from the beginning. It is up to the caller of this function to make a call to + * available, streaming will begin from the beginning. It is up to the caller of this function to make a call to * @c getStartStreamingPoint() to find out the actual offset from which streaming began. * * @param contentFetcherFactory Used to create @c HTTPContentFetchers. * @param url The URL to stream from. * @param observer An observer to be notified of any errors that may happen during streaming. - * @param desiredStartTime The desired time to attempt to start streaming from. Note that this will only succeed + * @param startTime The desired time to attempt to start streaming from. Note that this will only succeed * in cases where the URL points to a playlist with metadata about individual chunks within it. If none are found, * streaming will begin from the beginning. * @return A @c std::shared_ptr to the new @c UrlContentToAttachmentConverter object or @c nullptr on failure. @@ -131,15 +131,6 @@ class UrlContentToAttachmentConverter */ bool writeUrlContentIntoStream(std::string url); - /** - * Writes the given data into the internal stream. - * - * @param buffer The data to write. - * @param numBytes The number of bytes to write. - * @return @c true if the data was successully written or @c false otherwise - */ - bool writeDataIntoStream(const std::vector& buffer, size_t numBytes); - /// @} /// The initial desired offset from which streaming should begin. @@ -151,9 +142,10 @@ class UrlContentToAttachmentConverter /// Used to parse URLS that point to playlists. std::shared_ptr m_playlistParser; + /// The stream that will hold downloaded data. std::shared_ptr m_stream; - /// The writer used to write data into the internal master stream. + /// The writer used to write data into the stream. std::shared_ptr m_streamWriter; /// The observer to be notified of errors. diff --git a/PlaylistParser/src/UrlContentToAttachmentConverter.cpp b/PlaylistParser/src/UrlContentToAttachmentConverter.cpp index e6b56f22df..b6c38a959e 100644 --- a/PlaylistParser/src/UrlContentToAttachmentConverter.cpp +++ b/PlaylistParser/src/UrlContentToAttachmentConverter.cpp @@ -23,12 +23,6 @@ namespace playlistParser { /// String to identify log entries originating from this file. static const std::string TAG("UrlContentToAttachmentConverter"); -/** - * The timeout for a blocking write call to an @c AttachmentWriter. This value may be increased to decrease wakeups but - * may also increase latency. - */ -static const std::chrono::milliseconds TIMEOUT_FOR_BLOCKING_WRITE = std::chrono::milliseconds(100); - /** * Create a LogEntry using this file's TAG and the specified event string. * @@ -36,11 +30,6 @@ static const std::chrono::milliseconds TIMEOUT_FOR_BLOCKING_WRITE = std::chrono: */ #define LX(event) alexaClientSDK::avsCommon::utils::logger::LogEntry(TAG, event) -/// The number of bytes read from the attachment with each read in the read loop. -// Just smaller than the default megabyte size of an Attachment to allow for maximum possible write size at at time -static const size_t CHUNK_SIZE{avsCommon::avs::attachment::InProcessAttachment::SDS_BUFFER_DEFAULT_SIZE_IN_BYTES - - avsCommon::avs::attachment::InProcessAttachment::SDS_BUFFER_DEFAULT_SIZE_IN_BYTES / 4}; - static const std::chrono::milliseconds UNVALID_DURATION = avsCommon::utils::playlistParser::PlaylistParserObserverInterface::INVALID_DURATION; @@ -116,9 +105,9 @@ void UrlContentToAttachmentConverter::onPlaylistEntryParsed( } } m_startedStreaming = true; + ACSDK_DEBUG3(LX("onPlaylistEntryParsed").d("status", parseResult)); switch (parseResult) { case avsCommon::utils::playlistParser::PlaylistParseResult::ERROR: - ACSDK_DEBUG9(LX("onPlaylistEntryParsed").d("status", parseResult)); m_executor.submit([this]() { ACSDK_DEBUG9(LX("closingWriter")); m_streamWriter->close(); @@ -132,7 +121,6 @@ void UrlContentToAttachmentConverter::onPlaylistEntryParsed( }); break; case avsCommon::utils::playlistParser::PlaylistParseResult::SUCCESS: - ACSDK_DEBUG9(LX("onPlaylistEntryParsed").d("status", parseResult)); m_executor.submit([this, url]() { if (!m_streamWriterClosed && !writeUrlContentIntoStream(url)) { ACSDK_ERROR(LX("writeUrlContentToStreamFailed")); @@ -149,7 +137,6 @@ void UrlContentToAttachmentConverter::onPlaylistEntryParsed( }); break; case avsCommon::utils::playlistParser::PlaylistParseResult::STILL_ONGOING: - ACSDK_DEBUG9(LX("onPlaylistEntryParsed").d("status", parseResult)); m_executor.submit([this, url]() { if (!m_streamWriterClosed && !writeUrlContentIntoStream(url)) { ACSDK_ERROR(LX("writeUrlContentToStreamFailed").d("info", "closingWriter")); @@ -170,14 +157,11 @@ void UrlContentToAttachmentConverter::onPlaylistEntryParsed( } bool UrlContentToAttachmentConverter::writeUrlContentIntoStream(std::string url) { - /* - * TODO: ACSDK-826 We currently copy from one SDS with the individual URL data into a master SDS. We could probably - * optimize this to avoid the extra copy. - */ ACSDK_DEBUG9(LX("writeUrlContentIntoStream").d("info", "beginning")); + auto contentFetcher = m_contentFetcherFactory->create(url); - auto httpContent = - contentFetcher->getContent(avsCommon::sdkInterfaces::HTTPContentFetcherInterface::FetchOptions::ENTIRE_BODY); + auto httpContent = contentFetcher->getContent( + avsCommon::sdkInterfaces::HTTPContentFetcherInterface::FetchOptions::ENTIRE_BODY, m_streamWriter); if (!httpContent) { ACSDK_ERROR(LX("getContentFailed").d("reason", "nullHTTPContentReceived")); return false; @@ -186,44 +170,6 @@ bool UrlContentToAttachmentConverter::writeUrlContentIntoStream(std::string url) ACSDK_ERROR(LX("getContentFailed").d("reason", "badHTTPContentReceived")); return false; } - if (!httpContent->dataStream) { - ACSDK_ERROR(LX("getContentFailed").d("reason", "badDataStream")); - return false; - } - auto reader = httpContent->dataStream->createReader(avsCommon::utils::sds::ReaderPolicy::BLOCKING); - if (!reader) { - ACSDK_ERROR(LX("getContentFailed").d("reason", "failedToCreateStreamReader")); - return false; - } - avsCommon::avs::attachment::AttachmentReader::ReadStatus readStatus = - avsCommon::avs::attachment::AttachmentReader::ReadStatus::OK; - std::vector buffer(CHUNK_SIZE, 0); - bool streamClosed = false; - while (!streamClosed && !m_shuttingDown) { - auto bytesRead = reader->read(buffer.data(), buffer.size(), &readStatus); - switch (readStatus) { - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::CLOSED: - streamClosed = true; - if (bytesRead == 0) { - ACSDK_INFO(LX("readFinished").d("reason", "CLOSED")); - break; - } - /* FALL THROUGH - to add any data received even if closed */ - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::OK: - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::OK_WOULDBLOCK: - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::OK_TIMEDOUT: - if (!writeDataIntoStream(buffer, bytesRead)) { - ACSDK_ERROR(LX("writeDataIntoStreamFailed").d("reason", "writeError")); - return false; - } - break; - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::ERROR_OVERRUN: - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::ERROR_BYTES_LESS_THAN_WORD_SIZE: - case avsCommon::avs::attachment::AttachmentReader::ReadStatus::ERROR_INTERNAL: - ACSDK_ERROR(LX("readFailed").d("status", "readError")); - return false; - } - } if (m_shuttingDown) { return false; } @@ -231,49 +177,9 @@ bool UrlContentToAttachmentConverter::writeUrlContentIntoStream(std::string url) return true; } -bool UrlContentToAttachmentConverter::writeDataIntoStream(const std::vector& buffer, size_t numBytes) { - avsCommon::avs::attachment::AttachmentWriter::WriteStatus writeStatus = - avsCommon::avs::attachment::AttachmentWriter::WriteStatus::OK; - - size_t totalBytesWritten = 0; - auto bufferStart = buffer.data(); - while (totalBytesWritten < numBytes && !m_shuttingDown) { - // because we use a BLOCKING writer, we have to keep track of how many bytes are written per write() - // and update the buffer pointer accordingly - size_t bytesWritten = - m_streamWriter->write(bufferStart, numBytes - totalBytesWritten, &writeStatus, TIMEOUT_FOR_BLOCKING_WRITE); - bufferStart += bytesWritten; - totalBytesWritten += bytesWritten; - switch (writeStatus) { - case avsCommon::avs::attachment::AttachmentWriter::WriteStatus::CLOSED: - // TODO: ACSDK-827 Replace with just the writeStatus once the << operator is added - ACSDK_ERROR(LX("writeContentFailed").d("reason", "writeStatusCLOSED")); - return false; - case avsCommon::avs::attachment::AttachmentWriter::WriteStatus::ERROR_BYTES_LESS_THAN_WORD_SIZE: - ACSDK_ERROR(LX("writeContentFailed").d("reason", "writeStatusBYTESLESSTHANWORDSIZE")); - return false; - case avsCommon::avs::attachment::AttachmentWriter::WriteStatus::ERROR_INTERNAL: - ACSDK_ERROR(LX("writeContentFailed").d("reason", "writeStatusERRORINTERNAL")); - return false; - case avsCommon::avs::attachment::AttachmentWriter::WriteStatus::TIMEDOUT: - case avsCommon::avs::attachment::AttachmentWriter::WriteStatus::OK: - // might still have bytes to write - continue; - case avsCommon::avs::attachment::AttachmentWriter::WriteStatus::OK_BUFFER_FULL: - ACSDK_ERROR(LX("writeContentFailed").d("unexpected return code", "writeStatusOK_BUFFER_FULL")); - return false; - default: - ACSDK_ERROR(LX("writeContentFailed").d("reason", "unknownWriteStatus")); - return false; - } - } - if (m_shuttingDown) { - return false; - } - return true; -} - void UrlContentToAttachmentConverter::doShutdown() { + m_streamWriter->close(); + { std::lock_guard lock{m_mutex}; m_observer.reset(); diff --git a/PlaylistParser/test/PlaylistParserTest.cpp b/PlaylistParser/test/PlaylistParserTest.cpp index c77d32b27d..a8f651304a 100644 --- a/PlaylistParser/test/PlaylistParserTest.cpp +++ b/PlaylistParser/test/PlaylistParserTest.cpp @@ -260,7 +260,9 @@ class MockContentFetcher : public avsCommon::sdkInterfaces::HTTPContentFetcherIn MockContentFetcher(const std::string& url) : m_url{url} { } - std::unique_ptr getContent(FetchOptions fetchOption) { + std::unique_ptr getContent( + FetchOptions fetchOption, + std::shared_ptr writer) { if (fetchOption == FetchOptions::CONTENT_TYPE) { auto it1 = urlsToContentTypes.find(m_url); if (it1 == urlsToContentTypes.end()) { diff --git a/README.md b/README.md index e578d6cdef..728e01b690 100644 --- a/README.md +++ b/README.md @@ -1,5 +1,5 @@ ### IMPORTANT NOTE -If you are updating from v1.3 or earlier to v1.5, you must update your `AlexaClientSDKConfig.json` to include a Notifications database. An updated sample is available in the quickstart guides for Ubuntu Linux, Raspberry Pi, macOS, and Generic Linux. +If you are updating from v1.3 or earlier to v1.6, you must update your `AlexaClientSDKConfig.json` to include a Notifications database. An updated sample is available in the quickstart guides for Ubuntu Linux, Raspberry Pi, macOS, and Generic Linux. ### What is the Alexa Voice Service (AVS)? @@ -17,6 +17,7 @@ You can set up the SDK on the following platforms: * [Ubuntu Linux](https://github.com/alexa/avs-device-sdk/wiki/Ubuntu-Linux-Quick-Start-Guide) * [Raspberry Pi](https://github.com/alexa/avs-device-sdk/wiki/Raspberry-Pi-Quick-Start-Guide-with-Script) (Raspbian Stretch) * [macOS](https://github.com/alexa/avs-device-sdk/wiki/macOS-Quick-Start-Guide) +* [Windows 64-bit](https://github.com/alexa/avs-device-sdk/wiki/Windows-Quick-Start-Guide-with-Script) * [Generic Linux](https://github.com/alexa/avs-device-sdk/wiki/Linux-Reference-Guide) You can also prototype with a third party development kit: @@ -94,26 +95,25 @@ Focus management is not specific to Capability Agents or Directive Handlers, and **Note**: Feature enhancements, 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.5.0 released 02/12/2018: +v1.6.0 released 03/08/2018: **Enhancements** -* Added the `ExternalMediaPlayer` Capability Agent. This allows playback from music providers that control their own playback queue. Example: Spotify. -* Added support for AU and NZ to the `SampleApp`. -* Firmware version can now be sent to Alexa via the `SoftwareInfo` event. The firmware version is specified in the config file under the `sampleApp` object as an integer value named [`firmwareVersion`](https://github.com/alexa/avs-device-sdk/blob/master/Integration/AlexaClientSDKConfig.json#L52). -* The new `f` command was added to the `SampleApp` which allows the firmware version to be updated at run-time. -* Optional configuration changes have been introduced. Now a [default log level](https://github.com/alexa/avs-device-sdk/blob/master/Integration/AlexaClientSDKConfig.json#L93) can be set for `ACSDK_LOG_MODULE` components, globally or individually. This value is specified under a new root level configuration object called `logger`, and the value itself is named `logLevel`. This allows you to limit the degree of logging to that default value, such as `ERROR`or `INFO`. +* `rapidJson` is now included with "make install". +* Updated the `TemplateRuntimeObserverInterface` to support clearing of `displayCards`. +* Added Windows SDK support, along with an installation script (MinGW-w64). +* Updated `ContextManager` to ignore context reported by a state provider. +* The `SharedDataStream` object is now associated by playlist, rather than by URL. +* Added the `RegistrationManager` component. Now, when a user logs out all persistent user-specific data is cleared from the SDK. The log out functionality can be exercised in the sample app with the new command: `k`. **Bug Fixes** -* Fixed bug where `AudioPlayer` progress reports were not being sent, or were being sent incorrectly. -* [Issue 408](https://github.com/alexa/avs-device-sdk/issues/408) - Irrelevant code related to `UrlSource` was removed from the `GStreamer-based MediaPlayer` implementation. -* The `TZ` variable no longer needs to be set to `UTC` when running `SampleApp`. -* Fixed a bug where `CurlEasyHandleWrapper` logged unwanted data on failure conditions. -* Fixed a bug to improve `SIGPIPE` handling. -* Fixed a bug where the filename and classname were mismatched. Changed `UrlToAttachmentConverter.h` to `UrlContentToAttachmentConverter.h`,and `UrlToAttachmentConverter.cpp` to `UrlContentToAttachmentConverter.cpp`. -* Fixed a bug where after muting and then un-muting the GStreamer-based `MediaPlayer` implementation, the next item in queue would play instead of continuing playback of the originally muted item. +* [Issue 400](https://github.com/alexa/avs-device-sdk/issues/400) Fixed a bug where the alert reminder did not iterate as intended after loss of network connection. +* [Issue 477](https://github.com/alexa/avs-device-sdk/issues/477) Fixed a bug in which Alexa's weather response was being truncated. +* Fixed an issue in which there were reports of instability related to the Sensory engine. To correct this, the `portAudio` [`suggestedLatency`](https://github.com/alexa/avs-device-sdk/blob/master/Integration/AlexaClientSDKConfig.json#L62) value can now be configured. **Known Issues** * The `ACL` may encounter issues if audio attachments are received but not consumed. -* Display Cards for Kindle don't render. * `SpeechSynthesizerState` currently uses `GAINING_FOCUS` and `LOSING_FOCUS` as a workaround for handling intermediate state. These states may be removed in a future release. * Music playback doesn't immediately stop when a user barges-in on iHeartRadio. +* The Windows sample app hangs on exit. +* GDB receives a `SIGTRAP` when troubleshooting the Windows sample app. +* `make integration` doesn't work on Windows. Integration tests will need to be run individually. diff --git a/RegistrationManager/CMakeLists.txt b/RegistrationManager/CMakeLists.txt new file mode 100644 index 0000000000..d1cc10e2be --- /dev/null +++ b/RegistrationManager/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) +project(RegistrationManager LANGUAGES CXX) + +include(../build/BuildDefaults.cmake) + +add_subdirectory("src") +acsdk_add_test_subdirectory_if_allowed() \ No newline at end of file diff --git a/RegistrationManager/include/RegistrationManager/CustomerDataHandler.h b/RegistrationManager/include/RegistrationManager/CustomerDataHandler.h new file mode 100644 index 0000000000..d0f3011dd3 --- /dev/null +++ b/RegistrationManager/include/RegistrationManager/CustomerDataHandler.h @@ -0,0 +1,72 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_CUSTOMERDATAHANDLER_H_ +#define ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_CUSTOMERDATAHANDLER_H_ + +#include + +namespace alexaClientSDK { +namespace registrationManager { + +class CustomerDataManager; + +/** + * Abstract base class which requires the derived class to implement a @c clearData() function. + * + * For changes in the device registration, it is extremely important to remove any customer data saved in the device. + * Classes that have any data related to the currently logged user must extend this class to guarantee that their data + * will be wiped out during logout. + */ +class CustomerDataHandler { +public: + /** + * Build and register the new object with the CustomerDataManager. + * + * @param customerDataManager The CustomerDataManager that will keep track of the new data handler. + * @note The customerDataManager must have a valid pointer to a manager instance. + */ + CustomerDataHandler(std::shared_ptr customerDataManager); + + /** + * CustomerDataHandler destructor. + * + * Deregister the handler with the CustomerDataManager. + */ + virtual ~CustomerDataHandler(); + + /** + * Reset any internal state that may be associated with a particular user. + * + * @warning Object must succeed in deleting any customer data. + * @warning This method is called while CustomerDataManager is in a locked state. Do not call or wait for any + * CustomerDataManager operation. + */ + virtual void clearData() = 0; + +private: + /** + * Keep a constant pointer to CustomerDataManager so that the CustomerDataHandler object can auto remove itself. + * + * @note The goal is to guarantee that all customerDataHandlers are properly managed. The trade-off is that we have + * to keep a shared_pointer to its manager and the manager has to keep a raw pointer to each handler. + */ + const std::shared_ptr m_dataManager; +}; + +} // namespace registrationManager +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_CUSTOMERDATAHANDLER_H_ diff --git a/RegistrationManager/include/RegistrationManager/CustomerDataManager.h b/RegistrationManager/include/RegistrationManager/CustomerDataManager.h new file mode 100644 index 0000000000..0af83079f1 --- /dev/null +++ b/RegistrationManager/include/RegistrationManager/CustomerDataManager.h @@ -0,0 +1,67 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_CUSTOMERDATAMANAGER_H_ +#define ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_CUSTOMERDATAMANAGER_H_ + +#include +#include +#include + +#include "RegistrationManager/CustomerDataHandler.h" + +namespace alexaClientSDK { +namespace registrationManager { + +/** + * The @c CustomerDataManager is an object responsible for managing customer data and to ensure that one + * customer will not have access to another customer's data. + */ +class CustomerDataManager { +public: + /** + * CustomerDataManager destructor. + */ + virtual ~CustomerDataManager(); + + /** + * Add object that tracks any sort of customer data. + */ + void addDataHandler(CustomerDataHandler* handler); + + /** + * Remove object that tracks customer data. + */ + void removeDataHandler(CustomerDataHandler* handler); + + /** + * Clear every customer data kept in the device. + * + * @note We do not guarantee the order that the CustomerDataHandlers are called. + */ + void clearData(); + +private: + /// List of all data handlers. + std::unordered_set m_dataHandlers; + + /// Mutex used to synchronize m_dataHandlers variable access. + std::mutex m_mutex; +}; + +} // namespace registrationManager +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_CUSTOMERDATAMANAGER_H_ diff --git a/RegistrationManager/include/RegistrationManager/RegistrationManager.h b/RegistrationManager/include/RegistrationManager/RegistrationManager.h new file mode 100644 index 0000000000..8d880a49f3 --- /dev/null +++ b/RegistrationManager/include/RegistrationManager/RegistrationManager.h @@ -0,0 +1,101 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_REGISTRATIONMANAGER_H_ +#define ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_REGISTRATIONMANAGER_H_ + +#include +#include + +#include +#include +#include + +#include "RegistrationManager/CustomerDataManager.h" +#include "RegistrationManager/RegistrationObserverInterface.h" + +namespace alexaClientSDK { +namespace registrationManager { + +/** + * The @c RegistrationManager is responsible for logout and deregister actions. + * + * When a user is logging out of the device, the registration manager will close down the AVS connection, + * cancel ongoing directives and delete any customer data saved in the device. + */ +class RegistrationManager { +public: + /** + * RegistrationManager constructor. + * + * @param directiveSequencer Object used to clear directives during logout process. + * @param connectionManager Connection manager must be disabled during customer logout. + * @param dataManager Object that manages customer data, which must be cleared during logout. + */ + RegistrationManager( + std::shared_ptr& directiveSequencer, + std::shared_ptr& connectionManager, + std::shared_ptr dataManager); + + /** + * RegistrationManager destructor + */ + virtual ~RegistrationManager() = default; + + /** + * Log out current customer. This will clear any persistent data. + */ + void logout(); + + /** + * Add a new registration observer object which will get notified after the registration state has changed. + * + * @param observer Object to be notified of any registration event. + */ + void addObserver(std::shared_ptr observer); + + /** + * Remove the given observer object which will no longer get any registration notification. + * + * @param observer Object to be removed from observers set. + */ + void removeObserver(std::shared_ptr observer); + +private: + /** + * Notify all observers that a new registration even has happened. + */ + void notifyObservers(); + + /// Used to cancel all directives. + std::shared_ptr m_directiveSequencer; + + // Used to enable / disable connection during logout to avoid any interruption. + std::shared_ptr m_connectionManager; + + // Used to clear customer data to ensure that a future login will not have access to previous customer data + std::shared_ptr m_dataManager; + + /// Mutex for registration observers. + std::mutex m_observersMutex; + + // Observers + std::unordered_set > m_observers; +}; + +} // namespace registrationManager +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_REGISTRATIONMANAGER_H_ diff --git a/RegistrationManager/include/RegistrationManager/RegistrationObserverInterface.h b/RegistrationManager/include/RegistrationManager/RegistrationObserverInterface.h new file mode 100644 index 0000000000..c8ac74b5b6 --- /dev/null +++ b/RegistrationManager/include/RegistrationManager/RegistrationObserverInterface.h @@ -0,0 +1,44 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_REGISTRATIONOBSERVERINTERFACE_H_ +#define ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_REGISTRATIONOBSERVERINTERFACE_H_ + +namespace alexaClientSDK { +namespace registrationManager { + +/** + * This interface is used to observe changes to the device registration, such as user logout. + */ +class RegistrationObserverInterface { +public: + /** + * Virtual destructor to assure proper cleanup of derived types. + */ + virtual ~RegistrationObserverInterface() = default; + + /** + * Notification that the current customer has logged out. + * + * @warning This method is called while RegistrationManager is in a locked state. The callback must not block on + * calls to RegistrationManager methods either. + */ + virtual void onLogout() = 0; +}; + +} // namespace registrationManager +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_REGISTRATIONMANAGER_INCLUDE_REGISTRATIONMANAGER_REGISTRATIONOBSERVERINTERFACE_H_ diff --git a/RegistrationManager/src/CMakeLists.txt b/RegistrationManager/src/CMakeLists.txt new file mode 100644 index 0000000000..586a4ba2ce --- /dev/null +++ b/RegistrationManager/src/CMakeLists.txt @@ -0,0 +1,12 @@ +add_definitions("-DACSDK_LOG_MODULE=registrationManager") + +add_library(RegistrationManager SHARED RegistrationManager.cpp CustomerDataManager.cpp CustomerDataHandler.cpp) + +target_include_directories(RegistrationManager PUBLIC + "${RegistrationManager_SOURCE_DIR}/include" + "${ACL_SOURCE_DIR}/include") + +target_link_libraries(RegistrationManager ADSL ACL) + +# install target +asdk_install() diff --git a/RegistrationManager/src/CustomerDataHandler.cpp b/RegistrationManager/src/CustomerDataHandler.cpp new file mode 100644 index 0000000000..c6b37d4a4c --- /dev/null +++ b/RegistrationManager/src/CustomerDataHandler.cpp @@ -0,0 +1,52 @@ +/* + * Copyright 2018 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. + */ + +#include "AVSCommon/Utils/Logger/Logger.h" +#include "RegistrationManager/CustomerDataManager.h" +#include "RegistrationManager/CustomerDataHandler.h" + +/// String to identify log entries originating from this file. +static const std::string TAG("CustomerDataHandler"); + +/** + * 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) + +namespace alexaClientSDK { +namespace registrationManager { + +CustomerDataHandler::CustomerDataHandler(std::shared_ptr manager) : m_dataManager{manager} { + if (!manager) { + ACSDK_ERROR( + LX(__func__).m("Failed to register CustomerDataHandler. The customer data manager provided is " + "invalid.")); + } else { + m_dataManager->addDataHandler(this); + } +} + +CustomerDataHandler::~CustomerDataHandler() { + if (!m_dataManager) { + ACSDK_ERROR(LX(__func__).m("Failed to remove CustomerDataHandler. Customer data manager is invalid.")); + } else { + m_dataManager->removeDataHandler(this); + } +} + +} // namespace registrationManager +} // namespace alexaClientSDK diff --git a/RegistrationManager/src/CustomerDataManager.cpp b/RegistrationManager/src/CustomerDataManager.cpp new file mode 100644 index 0000000000..06bea84e24 --- /dev/null +++ b/RegistrationManager/src/CustomerDataManager.cpp @@ -0,0 +1,63 @@ +/* + * Copyright 2018 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. + */ + +#include "RegistrationManager/CustomerDataManager.h" + +#include + +#include "AVSCommon/Utils/Logger/Logger.h" + +/// String to identify log entries originating from this file. +static const std::string TAG("CustomerDataManager"); + +/** + * 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) + +namespace alexaClientSDK { +namespace registrationManager { + +void CustomerDataManager::addDataHandler(CustomerDataHandler* handler) { + if (handler == nullptr) { + ACSDK_ERROR(LX("addDataHandlerFailed").m("Cannot register a NULL handler.")); + } else { + std::lock_guard lock{m_mutex}; + m_dataHandlers.insert(handler); + } +} + +void CustomerDataManager::removeDataHandler(CustomerDataHandler* handler) { + std::lock_guard lock{m_mutex}; + m_dataHandlers.erase(handler); +} + +void CustomerDataManager::clearData() { + std::lock_guard lock{m_mutex}; + for (auto handler : m_dataHandlers) { + handler->clearData(); + } +} + +CustomerDataManager::~CustomerDataManager() { + if (!m_dataHandlers.empty()) { + ACSDK_ERROR(LX(__func__).m("All CustomerDataHandlers should be removed before deleting their manager.")); + } +} + +} // namespace registrationManager +} // namespace alexaClientSDK diff --git a/RegistrationManager/src/RegistrationManager.cpp b/RegistrationManager/src/RegistrationManager.cpp new file mode 100644 index 0000000000..72baaba7b9 --- /dev/null +++ b/RegistrationManager/src/RegistrationManager.cpp @@ -0,0 +1,78 @@ +/* + * Copyright 2018 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. + */ + +#include "RegistrationManager/RegistrationManager.h" + +#include "AVSCommon/Utils/Logger/Logger.h" +#include "RegistrationManager/CustomerDataManager.h" + +/// String to identify log entries originating from this file. +static const std::string TAG("RegistrationManager"); + +/** + * 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) + +namespace alexaClientSDK { +namespace registrationManager { + +void RegistrationManager::logout() { + ACSDK_DEBUG(LX("logout")); + m_directiveSequencer->disable(); + m_connectionManager->disable(); + m_dataManager->clearData(); + notifyObservers(); +} + +RegistrationManager::RegistrationManager( + std::shared_ptr& directiveSequencer, + std::shared_ptr& connectionManager, + std::shared_ptr dataManager) : + m_directiveSequencer{directiveSequencer}, + m_connectionManager{connectionManager}, + m_dataManager{dataManager} { + if (!directiveSequencer) { + ACSDK_ERROR(LX("RegistrationManagerFailed").m("Invalid directiveSequencer.")); + } + if (!connectionManager) { + ACSDK_ERROR(LX("RegistrationManagerFailed").m("Invalid connectionManager.")); + } + if (!dataManager) { + ACSDK_ERROR(LX("RegistrationManagerFailed").m("Invalid dataManager.")); + } +} + +void RegistrationManager::addObserver(std::shared_ptr observer) { + std::lock_guard lock{m_observersMutex}; + m_observers.insert(observer); +} + +void RegistrationManager::removeObserver(std::shared_ptr observer) { + std::lock_guard lock{m_observersMutex}; + m_observers.erase(observer); +} + +void RegistrationManager::notifyObservers() { + std::lock_guard lock{m_observersMutex}; + for (auto& observer : m_observers) { + observer->onLogout(); + } +} + +} // namespace registrationManager +} // namespace alexaClientSDK diff --git a/RegistrationManager/test/CMakeLists.txt b/RegistrationManager/test/CMakeLists.txt new file mode 100644 index 0000000000..4eff83442c --- /dev/null +++ b/RegistrationManager/test/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) + +set(INCLUDE_PATH + "${RegistrationManager_INCLUDE_DIRS}" + "${AVSCommon_SOURCE_DIR}/AVS/test" + "${AVSCommon_SOURCE_DIR}/SDKInterfaces/test") + +discover_unit_tests("${INCLUDE_PATH}" RegistrationManager) diff --git a/RegistrationManager/test/CustomerDataManagerTest.cpp b/RegistrationManager/test/CustomerDataManagerTest.cpp new file mode 100644 index 0000000000..cb832b64fa --- /dev/null +++ b/RegistrationManager/test/CustomerDataManagerTest.cpp @@ -0,0 +1,79 @@ +/* + * Copyright 2018 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. + */ + +#include +#include + +#include + +#include "RegistrationManager/CustomerDataHandler.h" +#include "RegistrationManager/CustomerDataManager.h" + +namespace alexaClientSDK { +namespace registrationManager { +namespace test { + +class MockCustomerDataHandler : public CustomerDataHandler { +public: + MockCustomerDataHandler(std::shared_ptr dataManager) : CustomerDataHandler(dataManager) { + } + MOCK_METHOD0(clearData, void()); +}; + +class CustomerDataManagerTest : public ::testing::Test { +protected: + void SetUp() override { + m_dataManager = std::make_shared(); + } + + void TearDown() override { + m_dataManager.reset(); + } + + std::shared_ptr m_dataManager; +}; + +TEST_F(CustomerDataManagerTest, testEmptyManager) { + m_dataManager->clearData(); +} + +/** + * Test that all data handlers are cleared. + */ +TEST_F(CustomerDataManagerTest, testClearData) { + MockCustomerDataHandler handler1{m_dataManager}; + MockCustomerDataHandler handler2{m_dataManager}; + EXPECT_CALL(handler1, clearData()).Times(1); + EXPECT_CALL(handler2, clearData()).Times(1); + m_dataManager->clearData(); +} + +/** + * Test that removing a data handler does not leave any dangling reference inside @c CustomerDataManager. + */ +TEST_F(CustomerDataManagerTest, testClearDataAfterHandlerDeletion) { + { + // CustomerDataHandler will register and deregister with CustomerDataManager during ctor and dtor, respectively. + MockCustomerDataHandler handler1{m_dataManager}; + EXPECT_CALL(handler1, clearData()).Times(0); + } + MockCustomerDataHandler handler2{m_dataManager}; + EXPECT_CALL(handler2, clearData()).Times(1); + m_dataManager->clearData(); +} + +} // namespace test +} // namespace registrationManager +} // namespace alexaClientSDK diff --git a/RegistrationManager/test/RegistrationManagerTest.cpp b/RegistrationManager/test/RegistrationManagerTest.cpp new file mode 100644 index 0000000000..e17ae46f6e --- /dev/null +++ b/RegistrationManager/test/RegistrationManagerTest.cpp @@ -0,0 +1,134 @@ +/* + * Copyright 2018 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. + */ + +#include +#include +#include + +#include +#include + +#include "AVSCommon/AVS/Attachment/MockAttachmentManager.h" +#include "AVSCommon/AVS/Initialization/AlexaClientSDKInit.h" +#include "AVSCommon/SDKInterfaces/MockExceptionEncounteredSender.h" +#include "AVSCommon/Utils/Memory/Memory.h" +#include "RegistrationManager/RegistrationManager.h" +#include "RegistrationManager/CustomerDataManager.h" + +namespace alexaClientSDK { +namespace registrationManager { +namespace test { + +using avsCommon::utils::memory::make_unique; + +class MockMessageRouter : public acl::MessageRouterInterface { +public: + MockMessageRouter() : MessageRouterInterface{"MockMessageRouter"} { + } + + MOCK_METHOD0(enable, void()); + MOCK_METHOD0(disable, void()); + MOCK_METHOD0(doShutdown, void()); + MOCK_METHOD0(getConnectionStatus, acl::MessageRouterInterface::ConnectionStatus()); + MOCK_METHOD1(sendMessage, void(std::shared_ptr request)); + MOCK_METHOD1(setAVSEndpoint, void(const std::string& avsEndpoint)); + MOCK_METHOD1(setObserver, void(std::shared_ptr observer)); +}; + +class MockRegistrationObserver : public RegistrationObserverInterface { +public: + MOCK_METHOD0(onLogout, void()); +}; + +class MockCustomerDataHandler : public CustomerDataHandler { +public: + MockCustomerDataHandler(std::shared_ptr manager) : CustomerDataHandler{manager} { + } + MOCK_METHOD0(clearData, void()); +}; + +class RegistrationManagerTest : public ::testing::Test { +protected: + void SetUp() override { + avsCommon::avs::initialization::AlexaClientSDKInit::initialize(std::vector()); + + m_messageRouter = std::make_shared(); + EXPECT_CALL(*m_messageRouter, setObserver(testing::_)); + EXPECT_CALL(*m_messageRouter, enable()); + m_avsConnectionManager = acl::AVSConnectionManager::create(m_messageRouter, true); + + auto exceptionEncounteredSender = + std::make_shared(); + m_directiveSequencer = adsl::DirectiveSequencer::create(exceptionEncounteredSender); + + m_dataManager = std::make_shared(); + m_dataHandler = make_unique(m_dataManager); + + m_registrationManager = + make_unique(m_directiveSequencer, m_avsConnectionManager, m_dataManager); + m_registrationObserver = std::make_shared(); + m_registrationManager->addObserver(m_registrationObserver); + } + + void TearDown() override { + if (m_directiveSequencer) { + m_directiveSequencer->shutdown(); + } + } + + /// Connection manager used during logout. + std::shared_ptr m_avsConnectionManager; + /// Mock message router. + std::shared_ptr m_messageRouter; + /// Used to check if logout disabled the directive sequencer. + std::shared_ptr m_directiveSequencer; + /// Mock data handler to ensure that @c clearData() method is called during logout. + std::unique_ptr m_dataHandler; + /// Data manager is used to call @c clearData() on every dataHandler. + std::shared_ptr m_dataManager; + /// Object under test. It is responsible for implementing logout. + std::unique_ptr m_registrationManager; + /// Mock registration observer used to check if RegistrationObserver is notified after logout. + std::shared_ptr m_registrationObserver; +}; + +/** + * Test that logout performsa all the following actions: + * - disable connection manager + * - disable directive sequencer + * - clear data handler's data + * - notify registration observer + */ +TEST_F(RegistrationManagerTest, testLogout) { + EXPECT_CALL(*m_messageRouter, disable()); + EXPECT_CALL(*m_registrationObserver, onLogout()); + EXPECT_CALL(*m_dataHandler, clearData()); + + m_registrationManager->logout(); + + ASSERT_FALSE(m_avsConnectionManager->isEnabled()); + + // Check that directive sequencer is not processing directives + std::string context{"context"}; + auto header = std::make_shared("namespace", "name", "messageid", "requestid"); + auto attachmentManager = std::make_shared(); + std::shared_ptr directive = + avsCommon::avs::AVSDirective::create("unparsed", header, "payload", attachmentManager, context); + ASSERT_FALSE(m_directiveSequencer->onDirective(directive)); +} + +} // namespace test +} // namespace registrationManager +} // namespace alexaClientSDK diff --git a/SampleApp/include/SampleApp/ConsolePrinter.h b/SampleApp/include/SampleApp/ConsolePrinter.h index 7224409102..394c10bbb9 100644 --- a/SampleApp/include/SampleApp/ConsolePrinter.h +++ b/SampleApp/include/SampleApp/ConsolePrinter.h @@ -20,6 +20,7 @@ #include #include +#include namespace alexaClientSDK { namespace sampleApp { @@ -64,6 +65,11 @@ class ConsolePrinter : public avsCommon::utils::logger::Logger { * when called from global's destructor */ std::shared_ptr m_mutex; + + /** + * Object used to format strings for log messages. + */ + avsCommon::utils::logger::LogStringFormatter m_logFormatter; }; } // namespace sampleApp diff --git a/SampleApp/include/SampleApp/GuiRenderer.h b/SampleApp/include/SampleApp/GuiRenderer.h index 9a10c32ce9..f8510e934f 100644 --- a/SampleApp/include/SampleApp/GuiRenderer.h +++ b/SampleApp/include/SampleApp/GuiRenderer.h @@ -33,9 +33,13 @@ class GuiRenderer : public avsCommon::sdkInterfaces::TemplateRuntimeObserverInte public: /// @name TemplateRuntimeObserverInterface Functions /// @{ - void renderTemplateCard(const std::string& jsonPayload) override; - void renderPlayerInfoCard(const std::string& jsonPayload, TemplateRuntimeObserverInterface::AudioPlayerInfo info) - override; + void renderTemplateCard(const std::string& jsonPayload, avsCommon::avs::FocusState focusState) override; + void clearTemplateCard() override; + void renderPlayerInfoCard( + const std::string& jsonPayload, + TemplateRuntimeObserverInterface::AudioPlayerInfo info, + avsCommon::avs::FocusState focusState) override; + void clearPlayerInfoCard() override; /// @} }; diff --git a/SampleApp/include/SampleApp/InteractionManager.h b/SampleApp/include/SampleApp/InteractionManager.h index a7c1038da6..ac4638546d 100644 --- a/SampleApp/include/SampleApp/InteractionManager.h +++ b/SampleApp/include/SampleApp/InteractionManager.h @@ -22,6 +22,8 @@ #include #include #include +#include +#include #include "KeywordObserver.h" #include "PortAudioMicrophoneWrapper.h" @@ -48,7 +50,8 @@ class InteractionManager capabilityAgents::aip::AudioProvider holdToTalkAudioProvider, capabilityAgents::aip::AudioProvider tapToTalkAudioProvider, capabilityAgents::aip::AudioProvider wakeWordAudioProvider = capabilityAgents::aip::AudioProvider::null(), - std::shared_ptr keywordObserver = nullptr); + std::shared_ptr espProvider = nullptr, + std::shared_ptr espModifier = nullptr); /** * Begins the interaction between the Sample App and the user. This should only be called at startup. @@ -156,6 +159,16 @@ class InteractionManager */ void setMute(avsCommon::sdkInterfaces::SpeakerInterface::Type type, bool mute); + /** + * Reset the device and remove any customer data. + */ + void resetDevice(); + + /** + * Prompts the user to confirm the intent to reset the device. + */ + void confirmResetDevice(); + /** * Should be called whenever a user requests for ESP control. */ @@ -197,8 +210,11 @@ class InteractionManager /// The user interface manager. std::shared_ptr m_userInterface; - /// The keyword observer instance passed to the keyword detector. - std::shared_ptr m_keywordObserver; + /// The ESP provider. + std::shared_ptr m_espProvider; + + /// The ESP modifier. + std::shared_ptr m_espModifier; /// The hold to talk audio provider. capabilityAgents::aip::AudioProvider m_holdToTalkAudioProvider; diff --git a/SampleApp/include/SampleApp/KeywordObserver.h b/SampleApp/include/SampleApp/KeywordObserver.h index 21a194dc81..4be0d50ad9 100644 --- a/SampleApp/include/SampleApp/KeywordObserver.h +++ b/SampleApp/include/SampleApp/KeywordObserver.h @@ -22,6 +22,7 @@ #include #include #include +#include namespace alexaClientSDK { namespace sampleApp { @@ -36,10 +37,12 @@ class KeywordObserver : public avsCommon::sdkInterfaces::KeyWordObserverInterfac * * @param client The default SDK client. * @param audioProvider The audio provider from which to stream audio data from. + * @parm espProvider The ESP provider to calculate the Ambient and Voice energy from the audio stream. */ KeywordObserver( std::shared_ptr client, - capabilityAgents::aip::AudioProvider audioProvider); + capabilityAgents::aip::AudioProvider audioProvider, + std::shared_ptr espProvider = nullptr); void onKeyWordDetected( std::shared_ptr stream, @@ -54,17 +57,8 @@ class KeywordObserver : public avsCommon::sdkInterfaces::KeyWordObserverInterfac /// The audio provider. capabilityAgents::aip::AudioProvider m_audioProvider; - /// Flag to indicate if report of Echo Spatial Perception (ESP) is supported. - bool m_espSupport; - - /// String representation of voice energy ESP measurement in float. - std::string m_voiceEnergy; - - /// String representation of ambient energy ESP measurement in float. - std::string m_ambientEnergy; - - /// Having InterfactionManager as a friend so that it can adjust the ESP related settings. - friend class InteractionManager; + /// Echo Spatial Perception (ESP) provider. + std::shared_ptr m_espProvider; }; } // namespace sampleApp diff --git a/SampleApp/include/SampleApp/PortAudioMicrophoneWrapper.h b/SampleApp/include/SampleApp/PortAudioMicrophoneWrapper.h index 3d5f72a697..315c44b0e0 100644 --- a/SampleApp/include/SampleApp/PortAudioMicrophoneWrapper.h +++ b/SampleApp/include/SampleApp/PortAudioMicrophoneWrapper.h @@ -86,6 +86,15 @@ class PortAudioMicrophoneWrapper { /// Initializes PortAudio bool initialize(); + /** + * Get the optional config parameter from @c AlexaClientSDKConfig.json + * for setting the PortAudio stream's suggested latency. + * + * @param[out] suggestedLatency The latency as it is configured in the file. + * @return @c true if the suggestedLatency is defined in the config file, @c false otherwise. + */ + bool getConfigSuggestedLatency(PaTime& suggestedLatency); + /// The stream of audio data. const std::shared_ptr m_audioInputStream; diff --git a/SampleApp/include/SampleApp/SampleApplication.h b/SampleApp/include/SampleApp/SampleApplication.h index b5b256fc19..905aaa8848 100644 --- a/SampleApp/include/SampleApp/SampleApplication.h +++ b/SampleApp/include/SampleApp/SampleApplication.h @@ -16,17 +16,18 @@ #ifndef ALEXA_CLIENT_SDK_SAMPLEAPP_INCLUDE_SAMPLEAPP_SAMPLEAPPLICATION_H_ #define ALEXA_CLIENT_SDK_SAMPLEAPP_INCLUDE_SAMPLEAPP_SAMPLEAPPLICATION_H_ +#include #include + #include "ConsolePrinter.h" #include "UserInputManager.h" #ifdef KWD #include #endif +#include #include -#include - namespace alexaClientSDK { namespace sampleApp { @@ -53,6 +54,67 @@ class SampleApplication { /// Destructor which manages the @c SampleApplication shutdown sequence. ~SampleApplication(); + /** + * Method to create mediaPlayers for the optional music provider adapters plugged into the SDK. + * + * @param httpContentFetcherFactory The HTTPContentFetcherFactory to be used while creating the mediaPlayers. + * @param additionalSpeakers The speakerInterface to add the created mediaPlayer. + * @return @c true if the mediaPlayer of all the registered adapters could be created @c false otherwise. + */ + bool createMediaPlayersForAdapters( + std::shared_ptr httpContentFetcherFactory, + std::vector>& additionalSpeakers); + + /** + * Instances of this class register ExternalMediaAdapters. Each adapter registers itself by instantiating + * a static instance of the below class supplying their business name and creator method. + */ + class AdapterRegistration { + public: + /** + * Register an @c ExternalMediaAdapter for use by @c ExternalMediaPlayer. + * + * @param playerId The @c playerId identifying the @c ExternalMediaAdapter to register. + * @param createFunction The function to use to create instances of the specified @c ExternalMediaAdapter. + */ + AdapterRegistration( + const std::string& playerId, + capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::AdapterCreateFunction createFunction); + }; + + /** + * Signature of functions to create a MediaPlayer. + * + * @param httpContentFetcherFactory The HTTPContentFetcherFactory to be used while creating the mediaPlayers. + * @param type The type of the SpeakerInterface. + * @param name The name of the MediaPlayer instance. + * @return Return shared pointer to the created MediaPlayer instance. + */ + using MediaPlayerCreateFunction = std::shared_ptr (*)( + std::shared_ptr contentFetcherFactory, + avsCommon::sdkInterfaces::SpeakerInterface::Type type, + std::string name); + + /** + * Instances of this class register MediaPlayers to be created. Each third-party adapter registers a mediaPlayer + * for itself by instantiating a static instance of the below class supplying their business name, speaker interface + * type and creator method. + */ + class MediaPlayerRegistration { + public: + /** + * Register a @c MediaPlayer for use by a music provider adapter. + * + * @param playerId The @c playerId identifying the @c ExternalMediaAdapter to register. + * @speakerType The SpeakerType of the mediaPlayer to be created. + * @param createFunction The function to use to create instances of the mediaPlayer to use for the player. + */ + MediaPlayerRegistration( + const std::string& playerId, + avsCommon::sdkInterfaces::SpeakerInterface::Type speakerType, + MediaPlayerCreateFunction createFunction); + }; + private: /** * Initialize a SampleApplication. @@ -68,8 +130,12 @@ class SampleApplication { /// The @c UserInputManager which controls the client. std::unique_ptr m_userInputManager; - /// The vector of mediaPlayers used by the adapters. - std::vector> m_externalMusicProviderMediaPlayers; + /// The map of the adapters and their mediaPlayers. + std::unordered_map> + m_externalMusicProviderMediaPlayersMap; + + /// The vector of mediaPlayers for the adapters. + std::vector> m_adapterMediaPlayers; /// The @c MediaPlayer used by @c SpeechSynthesizer. std::shared_ptr m_speakMediaPlayer; @@ -83,6 +149,15 @@ class SampleApplication { /// The @c MediaPlayer used by @c NotificationsCapabilityAgent. std::shared_ptr m_notificationsMediaPlayer; + using SpeakerTypeAndCreateFunc = + std::pair; + + /// The singleton map from @c playerId to @c MediaPlayerCreateFunction. + static std::unordered_map m_playerToMediaPlayerMap; + + /// The singleton map from @c playerId to @c ExternalMediaAdapter creation functions. + static capabilityAgents::externalMediaPlayer::ExternalMediaPlayer::AdapterCreationMap m_adapterToCreateFuncMap; + #ifdef KWD /// The Wakeword Detector which can wake up the client using audio input. std::unique_ptr m_keywordDetector; diff --git a/SampleApp/include/SampleApp/UIManager.h b/SampleApp/include/SampleApp/UIManager.h index f6ff325a75..1977eeabe9 100644 --- a/SampleApp/include/SampleApp/UIManager.h +++ b/SampleApp/include/SampleApp/UIManager.h @@ -113,6 +113,26 @@ class UIManager */ void microphoneOn(); + /** + * Prints a warning that the customer still has to manually deregister the device. + */ + void printResetWarning(); + + /** + * Prints a confirmation message prompting the user to confirm their intent. + */ + void printResetConfirmation(); + + /** + * Prints an error message while trying to configure ESP in a device where ESP is not supported. + */ + void printESPNotSupported(); + + /** + * Prints an error message while trying to override ESP Data in a device that do not support manual override. + */ + void printESPDataOverrideNotSupported(); + private: /** * Prints the current state of Alexa after checking what the appropriate message to display is based on the current diff --git a/SampleApp/src/CMakeLists.txt b/SampleApp/src/CMakeLists.txt index e8decf5633..f5170a185d 100644 --- a/SampleApp/src/CMakeLists.txt +++ b/SampleApp/src/CMakeLists.txt @@ -1,26 +1,38 @@ -add_executable(SampleApp - main.cpp - PortAudioMicrophoneWrapper.cpp - InteractionManager.cpp - UserInputManager.cpp +set(SampleApp_SOURCES) +list(APPEND SampleApp_SOURCES + ConnectionObserver.cpp ConsolePrinter.cpp GuiRenderer.cpp - UIManager.cpp + InteractionManager.cpp KeywordObserver.cpp - ConnectionObserver.cpp - SampleApplication.cpp) + PortAudioMicrophoneWrapper.cpp + UIManager.cpp + UserInputManager.cpp + SampleApplication.cpp + main.cpp) + +IF (HAS_EXTERNAL_MEDIA_PLAYER_ADAPTERS) + file(GLOB_RECURSE SRC_FILE ${CMAKE_CURRENT_SOURCE_DIR}/ExternalMediaAdapterRegistration/*.cpp) + foreach(myfile ${SRC_FILE}) + list(APPEND SampleApp_SOURCES ${myfile}) + endforeach(myfile) +ENDIF() + +add_executable(SampleApp ${SampleApp_SOURCES}) target_include_directories(SampleApp PUBLIC "${SampleApp_SOURCE_DIR}/include" "${MediaPlayer_SOURCE_DIR}/include" - "${DefaultClient_SOURCE_DIR}/include" "${AudioResources_SOURCE_DIR}/include" + "${RegistrationManager_SOURCE_DIR}/include" + "${ESP_SOURCE_DIR}/include" "${PORTAUDIO_INCLUDE_DIR}") target_link_libraries(SampleApp DefaultClient - AuthDelegate - MediaPlayer + AuthDelegate + MediaPlayer + ESP "${PORTAUDIO_LIB_PATH}") if(KITTAI_KEY_WORD_DETECTOR) @@ -42,7 +54,5 @@ if (${CMAKE_SYSTEM_NAME} MATCHES "Darwin") elseif(${CMAKE_SYSTEM_NAME} MATCHES "Linux") target_link_libraries(SampleApp rt m pthread asound) -elseif(${CMAKE_SYSTEM_NAME} MATCHES "Windows") - message(FATAL_ERROR "Windows is unsupported at the moment") endif() diff --git a/SampleApp/src/ConsolePrinter.cpp b/SampleApp/src/ConsolePrinter.cpp index 63ddb1a1aa..d233a9b856 100644 --- a/SampleApp/src/ConsolePrinter.cpp +++ b/SampleApp/src/ConsolePrinter.cpp @@ -17,8 +17,6 @@ #include -#include - namespace alexaClientSDK { namespace sampleApp { @@ -55,7 +53,7 @@ void ConsolePrinter::emit( const char* threadMoniker, const char* text) { std::lock_guard lock{*m_mutex}; - std::cout << avsCommon::utils::logger::formatLogString(level, time, threadMoniker, text) << std::endl; + std::cout << m_logFormatter.format(level, time, threadMoniker, text) << std::endl; } } // namespace sampleApp diff --git a/SampleApp/src/GuiRenderer.cpp b/SampleApp/src/GuiRenderer.cpp index c0fb160c4c..55af81302e 100644 --- a/SampleApp/src/GuiRenderer.cpp +++ b/SampleApp/src/GuiRenderer.cpp @@ -40,6 +40,11 @@ static const std::string RENDER_TEMPLATE_HEADER = "# RenderTemplateCard \n" "#-----------------------------------------------------------------------------\n"; +static const std::string RENDER_TEMPLATE_CLEARED = + "##############################################################################\n" + "# RenderTemplateCard - Cleared \n" + "##############################################################################\n"; + static const std::string RENDER_FOOTER = "##############################################################################\n"; @@ -48,7 +53,12 @@ static const std::string RENDER_PLAYER_INFO_HEADER = "# RenderPlayerInfoCard \n" "#-----------------------------------------------------------------------------\n"; -void GuiRenderer::renderTemplateCard(const std::string& jsonPayload) { +static const std::string RENDER_PLAYER_INFO_CLEARED = + "##############################################################################\n" + "# RenderPlayerInfoCard - Cleared \n" + "##############################################################################\n"; + +void GuiRenderer::renderTemplateCard(const std::string& jsonPayload, avsCommon::avs::FocusState focusState) { rapidjson::Document payload; rapidjson::ParseResult result = payload.Parse(jsonPayload); if (!result) { @@ -72,8 +82,8 @@ void GuiRenderer::renderTemplateCard(const std::string& jsonPayload) { // Storing the output in a single buffer so that the display is continuous. std::string buffer; - buffer += RENDER_TEMPLATE_HEADER; + buffer += "# Focus State : " + focusStateToString(focusState) + "\n"; buffer += "# Template Type : " + templateType + "\n"; buffer += "# Main Title : " + mainTitle + "\n"; buffer += RENDER_FOOTER; @@ -84,9 +94,14 @@ void GuiRenderer::renderTemplateCard(const std::string& jsonPayload) { ConsolePrinter::simplePrint(buffer); } +void GuiRenderer::clearTemplateCard() { + ConsolePrinter::simplePrint(RENDER_TEMPLATE_CLEARED); +} + void GuiRenderer::renderPlayerInfoCard( const std::string& jsonPayload, - TemplateRuntimeObserverInterface::AudioPlayerInfo info) { + TemplateRuntimeObserverInterface::AudioPlayerInfo info, + avsCommon::avs::FocusState focusState) { rapidjson::Document payload; rapidjson::ParseResult result = payload.Parse(jsonPayload); if (!result) { @@ -100,8 +115,8 @@ void GuiRenderer::renderPlayerInfoCard( // Storing the output in a single buffer so that the display is continuous. std::string buffer; - buffer += RENDER_PLAYER_INFO_HEADER; + buffer += "# Focus State : " + focusStateToString(focusState) + "\n"; buffer += "# AudioItemId : " + audioItemId + "\n"; buffer += "# Audio state : " + playerActivityToString(info.audioPlayerState) + "\n"; buffer += "# Offset milliseconds : " + std::to_string(info.offset.count()) + "\n"; @@ -111,5 +126,9 @@ void GuiRenderer::renderPlayerInfoCard( ConsolePrinter::simplePrint(buffer); } +void GuiRenderer::clearPlayerInfoCard() { + ConsolePrinter::simplePrint(RENDER_PLAYER_INFO_CLEARED); +} + } // namespace sampleApp } // namespace alexaClientSDK diff --git a/SampleApp/src/InteractionManager.cpp b/SampleApp/src/InteractionManager.cpp index 09e6d0853f..1f42119a1b 100644 --- a/SampleApp/src/InteractionManager.cpp +++ b/SampleApp/src/InteractionManager.cpp @@ -13,7 +13,9 @@ * permissions and limitations under the License. */ +#include "ESP/ESPDataProviderInterface.h" #include "SampleApp/InteractionManager.h" +#include "RegistrationManager/CustomerDataManager.h" namespace alexaClientSDK { namespace sampleApp { @@ -25,12 +27,14 @@ InteractionManager::InteractionManager( capabilityAgents::aip::AudioProvider holdToTalkAudioProvider, capabilityAgents::aip::AudioProvider tapToTalkAudioProvider, capabilityAgents::aip::AudioProvider wakeWordAudioProvider, - std::shared_ptr keywordObserver) : + std::shared_ptr espProvider, + std::shared_ptr espModifier) : RequiresShutdown{"InteractionManager"}, m_client{client}, m_micWrapper{micWrapper}, m_userInterface{userInterface}, - m_keywordObserver{keywordObserver}, + m_espProvider{espProvider}, + m_espModifier{espModifier}, m_holdToTalkAudioProvider{holdToTalkAudioProvider}, m_tapToTalkAudioProvider{tapToTalkAudioProvider}, m_wakeWordAudioProvider{wakeWordAudioProvider}, @@ -180,23 +184,67 @@ void InteractionManager::setMute(avsCommon::sdkInterfaces::SpeakerInterface::Typ }); } +void InteractionManager::confirmResetDevice() { + m_executor.submit([this]() { m_userInterface->printResetConfirmation(); }); +} + +void InteractionManager::resetDevice() { + // This is a blocking operation. No interaction will be allowed during / after resetDevice + auto result = m_executor.submit([this]() { + m_client->getRegistrationManager()->logout(); + m_userInterface->printResetWarning(); + }); + result.wait(); +} + void InteractionManager::espControl() { m_executor.submit([this]() { - m_userInterface->printESPControlScreen( - m_keywordObserver->m_espSupport, m_keywordObserver->m_voiceEnergy, m_keywordObserver->m_ambientEnergy); + if (m_espProvider) { + auto espData = m_espProvider->getESPData(); + m_userInterface->printESPControlScreen( + m_espProvider->isEnabled(), espData.getVoiceEnergy(), espData.getAmbientEnergy()); + } else { + m_userInterface->printESPNotSupported(); + } }); } void InteractionManager::toggleESPSupport() { - m_executor.submit([this]() { m_keywordObserver->m_espSupport = !m_keywordObserver->m_espSupport; }); + m_executor.submit([this]() { + if (m_espProvider) { + m_espProvider->isEnabled() ? m_espProvider->disable() : m_espProvider->enable(); + } else { + m_userInterface->printESPNotSupported(); + } + }); } void InteractionManager::setESPVoiceEnergy(const std::string& voiceEnergy) { - m_executor.submit([this, voiceEnergy]() { m_keywordObserver->m_voiceEnergy = voiceEnergy; }); + m_executor.submit([this, voiceEnergy]() { + if (m_espProvider) { + if (m_espModifier) { + m_espModifier->setVoiceEnergy(voiceEnergy); + } else { + m_userInterface->printESPDataOverrideNotSupported(); + } + } else { + m_userInterface->printESPNotSupported(); + } + }); } void InteractionManager::setESPAmbientEnergy(const std::string& ambientEnergy) { - m_executor.submit([this, ambientEnergy]() { m_keywordObserver->m_ambientEnergy = ambientEnergy; }); + m_executor.submit([this, ambientEnergy]() { + if (m_espProvider) { + if (m_espModifier) { + m_espModifier->setAmbientEnergy(ambientEnergy); + } else { + m_userInterface->printESPDataOverrideNotSupported(); + } + } else { + m_userInterface->printESPNotSupported(); + } + }); } void InteractionManager::onDialogUXStateChanged(DialogUXState state) { @@ -207,7 +255,6 @@ void InteractionManager::onDialogUXStateChanged(DialogUXState state) { } void InteractionManager::doShutdown() { - m_keywordObserver.reset(); m_client.reset(); } diff --git a/SampleApp/src/KeywordObserver.cpp b/SampleApp/src/KeywordObserver.cpp index 74815ea8e4..d273d6f763 100644 --- a/SampleApp/src/KeywordObserver.cpp +++ b/SampleApp/src/KeywordObserver.cpp @@ -20,12 +20,11 @@ namespace sampleApp { KeywordObserver::KeywordObserver( std::shared_ptr client, - capabilityAgents::aip::AudioProvider audioProvider) : + capabilityAgents::aip::AudioProvider audioProvider, + std::shared_ptr espProvider) : m_client{client}, m_audioProvider{audioProvider}, - m_espSupport{false}, - m_voiceEnergy{""}, - m_ambientEnergy{""} { + m_espProvider{espProvider} { } void KeywordObserver::onKeyWordDetected( @@ -42,8 +41,8 @@ void KeywordObserver::onKeyWordDetected( endIndex != avsCommon::sdkInterfaces::KeyWordObserverInterface::UNSPECIFIED_INDEX && beginIndex != avsCommon::sdkInterfaces::KeyWordObserverInterface::UNSPECIFIED_INDEX) { if (m_client) { - if (m_espSupport) { - capabilityAgents::aip::ESPData espData{m_voiceEnergy, m_ambientEnergy}; + if (m_espProvider) { + auto espData = m_espProvider->getESPData(); m_client->notifyOfWakeWord(m_audioProvider, beginIndex, endIndex, keyword, espData); } else { m_client->notifyOfWakeWord(m_audioProvider, beginIndex, endIndex, keyword); diff --git a/SampleApp/src/PortAudioMicrophoneWrapper.cpp b/SampleApp/src/PortAudioMicrophoneWrapper.cpp index 9f97b012f3..01fb3bee45 100644 --- a/SampleApp/src/PortAudioMicrophoneWrapper.cpp +++ b/SampleApp/src/PortAudioMicrophoneWrapper.cpp @@ -13,6 +13,12 @@ * permissions and limitations under the License. */ +#include +#include + +#include + +#include #include "SampleApp/PortAudioMicrophoneWrapper.h" #include "SampleApp/ConsolePrinter.h" @@ -26,6 +32,10 @@ static const int NUM_OUTPUT_CHANNELS = 0; static const double SAMPLE_RATE = 16000; static const unsigned long PREFERRED_SAMPLES_PER_CALLBACK = paFramesPerBufferUnspecified; +static const std::string SAMPLE_APP_CONFIG_ROOT_KEY("sampleApp"); +static const std::string PORTAUDIO_CONFIG_ROOT_KEY("portAudio"); +static const std::string PORTAUDIO_CONFIG_SUGGESTED_LATENCY_KEY("suggestedLatency"); + std::unique_ptr PortAudioMicrophoneWrapper::create( std::shared_ptr stream) { if (!stream) { @@ -63,15 +73,43 @@ bool PortAudioMicrophoneWrapper::initialize() { ConsolePrinter::simplePrint("Failed to initialize PortAudio"); return false; } - err = Pa_OpenDefaultStream( - &m_paStream, - NUM_INPUT_CHANNELS, - NUM_OUTPUT_CHANNELS, - paInt16, - SAMPLE_RATE, - PREFERRED_SAMPLES_PER_CALLBACK, - PortAudioCallback, - this); + + PaTime suggestedLatency; + bool latencyInConfig = getConfigSuggestedLatency(suggestedLatency); + + if (!latencyInConfig) { + err = Pa_OpenDefaultStream( + &m_paStream, + NUM_INPUT_CHANNELS, + NUM_OUTPUT_CHANNELS, + paInt16, + SAMPLE_RATE, + PREFERRED_SAMPLES_PER_CALLBACK, + PortAudioCallback, + this); + } else { + ConsolePrinter::simplePrint( + "PortAudio suggestedLatency has been configured to " + std::to_string(suggestedLatency) + " Seconds"); + + PaStreamParameters inputParameters; + std::memset(&inputParameters, 0, sizeof(inputParameters)); + inputParameters.device = Pa_GetDefaultInputDevice(); + inputParameters.channelCount = NUM_INPUT_CHANNELS; + inputParameters.sampleFormat = paInt16; + inputParameters.suggestedLatency = suggestedLatency; + inputParameters.hostApiSpecificStreamInfo = nullptr; + + err = Pa_OpenStream( + &m_paStream, + &inputParameters, + nullptr, + SAMPLE_RATE, + PREFERRED_SAMPLES_PER_CALLBACK, + paNoFlag, + PortAudioCallback, + this); + } + if (err != paNoError) { ConsolePrinter::simplePrint("Failed to open PortAudio default stream"); return false; @@ -115,5 +153,21 @@ int PortAudioMicrophoneWrapper::PortAudioCallback( return paContinue; } +bool PortAudioMicrophoneWrapper::getConfigSuggestedLatency(PaTime& suggestedLatency) { + bool latencyInConfig = false; + auto config = avsCommon::utils::configuration::ConfigurationNode::getRoot()[SAMPLE_APP_CONFIG_ROOT_KEY] + [PORTAUDIO_CONFIG_ROOT_KEY]; + if (config) { + latencyInConfig = config.getValue( + PORTAUDIO_CONFIG_SUGGESTED_LATENCY_KEY, + &suggestedLatency, + suggestedLatency, + &rapidjson::Value::IsDouble, + &rapidjson::Value::GetDouble); + } + + return latencyInConfig; +} + } // namespace sampleApp } // namespace alexaClientSDK diff --git a/SampleApp/src/SampleApplication.cpp b/SampleApp/src/SampleApplication.cpp index 03cda10c01..2ad79ecd9f 100644 --- a/SampleApp/src/SampleApplication.cpp +++ b/SampleApp/src/SampleApplication.cpp @@ -24,6 +24,12 @@ #include #endif +#ifdef ENABLE_ESP +#include +#else +#include +#endif + #include #include #include @@ -73,6 +79,15 @@ static const std::string ENDPOINT_KEY("endpoint"); /// Key for setting if display cards are supported or not under the @c SAMPLE_APP_CONFIG_KEY configuration node. static const std::string DISPLAY_CARD_KEY("displayCardsSupported"); +using namespace capabilityAgents::externalMediaPlayer; + +/// The @c m_playerToMediaPlayerMap Map of the adapter to their speaker-type and MediaPlayer creation methods. +std::unordered_map + SampleApplication::m_playerToMediaPlayerMap; + +/// The singleton map from @c playerId to @c ExternalMediaAdapter creation functions. +std::unordered_map SampleApplication::m_adapterToCreateFuncMap; + #ifdef KWD_KITTAI /// The sensitivity of the Kitt.ai engine. static const double KITT_AI_SENSITIVITY = 0.6; @@ -121,9 +136,11 @@ static alexaClientSDK::avsCommon::utils::logger::Level getLogLevelFromUserInput( * @return true if the action for handling SIGPIPEs was correctly set to ignore, else false. */ static bool ignoreSigpipeSignals() { +#ifndef NO_SIGPIPE if (std::signal(SIGPIPE, SIG_IGN) == SIG_ERR) { return false; } +#endif return true; } @@ -144,6 +161,31 @@ std::unique_ptr SampleApplication::create( return clientApplication; } +SampleApplication::AdapterRegistration::AdapterRegistration( + const std::string& playerId, + ExternalMediaPlayer::AdapterCreateFunction createFunction) { + if (m_adapterToCreateFuncMap.find(playerId) != m_adapterToCreateFuncMap.end()) { + std::string errorStr = "WARNING:Adapter already exists for playerId " + playerId; + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint(errorStr); + } + + m_adapterToCreateFuncMap[playerId] = createFunction; +} + +SampleApplication::MediaPlayerRegistration::MediaPlayerRegistration( + const std::string& playerId, + avsCommon::sdkInterfaces::SpeakerInterface::Type speakerType, + MediaPlayerCreateFunction createFunction) { + if (m_playerToMediaPlayerMap.find(playerId) != m_playerToMediaPlayerMap.end()) { + std::string errorStr = "WARNING:MediaPlayer already exists for playerId " + playerId; + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint(errorStr); + } + + m_playerToMediaPlayerMap[playerId] = + std::pair( + speakerType, createFunction); +} + void SampleApplication::run() { m_userInputManager->run(); } @@ -151,9 +193,10 @@ void SampleApplication::run() { SampleApplication::~SampleApplication() { // First clean up anything that depends on the the MediaPlayers. m_userInputManager.reset(); + m_externalMusicProviderMediaPlayersMap.clear(); // Now it's safe to shut down the MediaPlayers. - for (auto mediaPlayer : m_externalMusicProviderMediaPlayers) { + for (auto& mediaPlayer : m_adapterMediaPlayers) { mediaPlayer->shutdown(); } if (m_speakMediaPlayer) { @@ -170,6 +213,27 @@ SampleApplication::~SampleApplication() { } } +bool SampleApplication::createMediaPlayersForAdapters( + std::shared_ptr httpContentFetcherFactory, + std::vector>& additionalSpeakers) { + for (auto& entry : m_playerToMediaPlayerMap) { + auto mediaPlayer = + entry.second.second(httpContentFetcherFactory, entry.second.first, entry.first + "MediaPlayer"); + if (mediaPlayer) { + m_externalMusicProviderMediaPlayersMap[entry.first] = mediaPlayer; + additionalSpeakers.push_back( + std::static_pointer_cast(mediaPlayer)); + m_adapterMediaPlayers.push_back(mediaPlayer); + } else { + std::string errorStr = "ERROR:Failed to create mediaPlayer for playerId " + entry.first; + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint(errorStr); + return false; + } + } + + return true; +} + bool SampleApplication::initialize( const std::string& pathToConfig, const std::string& pathToInputFolder, @@ -219,9 +283,6 @@ bool SampleApplication::initialize( auto httpContentFetcherFactory = std::make_shared(); - std::unordered_map> - externalMusicProviderMediaPlayersMap; - m_speakMediaPlayer = alexaClientSDK::mediaPlayer::MediaPlayer::create( httpContentFetcherFactory, avsCommon::sdkInterfaces::SpeakerInterface::Type::AVS_SYNCED, "SpeakMediaPlayer"); if (!m_speakMediaPlayer) { @@ -275,22 +336,30 @@ bool SampleApplication::initialize( std::vector> additionalSpeakers; + if (!createMediaPlayersForAdapters(httpContentFetcherFactory, additionalSpeakers)) { + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("ERROR: Could not create mediaPlayers for adapters"); + return false; + } + auto audioFactory = std::make_shared(); // Creating the alert storage object to be used for rendering and storing alerts. auto alertStorage = - std::make_shared(audioFactory->alerts()); + alexaClientSDK::capabilityAgents::alerts::storage::SQLiteAlertStorage::create(config, audioFactory->alerts()); + + // Creating the message storage object to be used for storing message to be sent later. + auto messageStorage = alexaClientSDK::certifiedSender::SQLiteMessageStorage::create(config); /* * Creating notifications storage object to be used for storing notification indicators. */ auto notificationsStorage = - std::make_shared(); + alexaClientSDK::capabilityAgents::notifications::SQLiteNotificationsStorage::create(config); /* * Creating settings storage object to be used for storing pairs of AVS Settings. */ - auto settingsStorage = std::make_shared(); + auto settingsStorage = alexaClientSDK::capabilityAgents::settings::SQLiteSettingStorage::create(config); /* * Creating the UI component that observes various components and prints to the console accordingly. @@ -310,19 +379,32 @@ bool SampleApplication::initialize( std::shared_ptr authDelegate = alexaClientSDK::authDelegate::AuthDelegate::create(); + if (!authDelegate) { + alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Creation of AuthDelegate failed!"); + return false; + } + authDelegate->addAuthObserver(connectionObserver); // INVALID_FIRMWARE_VERSION is passed to @c getInt() as a default in case FIRMWARE_VERSION_KEY is not found. int firmwareVersion = static_cast(avsCommon::sdkInterfaces::softwareInfo::INVALID_FIRMWARE_VERSION); sampleAppConfig.getInt(FIRMWARE_VERSION_KEY, &firmwareVersion, firmwareVersion); + /* + * Check to see if displayCards is supported on the device. The default is supported unless specified otherwise in + * the configuration. + */ + bool displayCardsSupported; + config[SAMPLE_APP_CONFIG_KEY].getBool(DISPLAY_CARD_KEY, &displayCardsSupported, true); + /* * Creating the DefaultClient - this component serves as an out-of-box default object that instantiates and "glues" * together all the modules. */ std::shared_ptr client = alexaClientSDK::defaultClient::DefaultClient::create( - externalMusicProviderMediaPlayersMap, + m_externalMusicProviderMediaPlayersMap, + m_adapterToCreateFuncMap, m_speakMediaPlayer, m_audioMediaPlayer, m_alertsMediaPlayer, @@ -334,11 +416,13 @@ bool SampleApplication::initialize( additionalSpeakers, audioFactory, authDelegate, - alertStorage, - notificationsStorage, - settingsStorage, + std::move(alertStorage), + std::move(messageStorage), + std::move(notificationsStorage), + std::move(settingsStorage), {userInterfaceManager}, {connectionObserver, userInterfaceManager}, + displayCardsSupported, firmwareVersion, true, nullptr); @@ -377,11 +461,8 @@ bool SampleApplication::initialize( client->addNotificationsObserver(userInterfaceManager); /* - * Add GUI Renderer as an observer if display cards are supported. The default is supported unless specified - * otherwise in the configuration. + * Add GUI Renderer as an observer if display cards are supported. */ - bool displayCardsSupported; - config[SAMPLE_APP_CONFIG_KEY].getBool(DISPLAY_CARD_KEY, &displayCardsSupported, true); if (displayCardsSupported) { auto guiRenderer = std::make_shared(); client->addTemplateRuntimeObserver(guiRenderer); @@ -446,7 +527,6 @@ bool SampleApplication::initialize( alexaClientSDK::sampleApp::ConsolePrinter::simplePrint("Failed to create PortAudioMicrophoneWrapper!"); return false; } - // Creating wake word audio provider, if necessary #ifdef KWD bool wakeAlwaysReadable = true; @@ -461,8 +541,20 @@ bool SampleApplication::initialize( wakeCanOverride, wakeCanBeOverridden); +#ifdef ENABLE_ESP + // Creating ESP connector + std::shared_ptr espProvider = esp::ESPDataProvider::create(wakeWordAudioProvider); + std::shared_ptr espModifier = nullptr; +#else + // Create dummy ESP connector + auto dummyEspProvider = std::make_shared(); + std::shared_ptr espProvider = dummyEspProvider; + std::shared_ptr espModifier = dummyEspProvider; +#endif + // This observer is notified any time a keyword is detected and notifies the DefaultClient to start recognizing. - auto keywordObserver = std::make_shared(client, wakeWordAudioProvider); + auto keywordObserver = + std::make_shared(client, wakeWordAudioProvider, espProvider); #if defined(KWD_KITTAI) m_keywordDetector = alexaClientSDK::kwd::KittAiKeyWordDetector::create( @@ -501,7 +593,8 @@ bool SampleApplication::initialize( holdToTalkAudioProvider, tapToTalkAudioProvider, wakeWordAudioProvider, - keywordObserver); + espProvider, + espModifier); #else // If wake word is not enabled, then creating the interaction manager without a wake word audio provider. diff --git a/SampleApp/src/UIManager.cpp b/SampleApp/src/UIManager.cpp index 1e03cb999a..8f4d55d172 100644 --- a/SampleApp/src/UIManager.cpp +++ b/SampleApp/src/UIManager.cpp @@ -66,7 +66,7 @@ static const std::string HELP_MESSAGE = #ifdef KWD "| Privacy mode (microphone off): |\n" "| Press 'm' and Enter to turn on and off the microphone. |\n" - "| Echo Spatial Perception (ESP): This is only for testing purpose only! |\n" + "| Echo Spatial Perception (ESP): This is for testing purpose only! |\n" "| Press 'e' followed by Enter at any time to adjust ESP settings. |\n" #endif "| Playback Controls: |\n" @@ -83,6 +83,11 @@ static const std::string HELP_MESSAGE = "| firmware version. |\n" "| Info: |\n" "| Press 'i' followed by Enter at any time to see the help screen. |\n" + "| Reset device: |\n" + "| Press 'k' followed by Enter at any time to reset your device. This |\n" + "| will erase any data stored in the device and you will have to |\n" + "| register your device with another account. |\n" + "| This will kill the application since we don't support login yet. |\n" "| Quit: |\n" "| Press 'q' followed by Enter at any time to quit the application. |\n" "+----------------------------------------------------------------------------+\n"; @@ -148,6 +153,21 @@ static const std::string ESP_CONTROL_MESSAGE = "| Press '3' followed by Enter to enter the ambient energy. |\n" "| Press 'q' to exit ESP Control Mode. |\n"; +static const std::string RESET_CONFIRMATION = + "+----------------------------------------------------------------------------+\n" + "| Device Reset Confirmation: |\n" + "| |\n" + "| This operation will remove all your personal information, device settings, |\n" + "| and downloaded content. Are you sure you want to reset your device? |\n" + "| |\n" + "| Press 'Y' followed by Enter to reset the device. |\n" + "| Press 'N' followed by Enter to cancel the device reset operation. |\n" + "+----------------------------------------------------------------------------+\n"; + +static const std::string RESET_WARNING = + "Device was reset! Please don't forget to deregister it. For more details " + "visit https://www.amazon.com/gp/help/customer/display.html?nodeId=201357520"; + void UIManager::onDialogUXStateChanged(DialogUXState state) { m_executor.submit([this, state]() { if (state == m_dialogState) { @@ -244,6 +264,14 @@ void UIManager::microphoneOff() { m_executor.submit([]() { ConsolePrinter::prettyPrint("Microphone Off!"); }); } +void UIManager::printResetConfirmation() { + m_executor.submit([]() { ConsolePrinter::simplePrint(RESET_CONFIRMATION); }); +} + +void UIManager::printResetWarning() { + m_executor.submit([]() { ConsolePrinter::prettyPrint(RESET_WARNING); }); +} + void UIManager::microphoneOn() { m_executor.submit([this]() { printState(); }); } @@ -279,5 +307,13 @@ void UIManager::printState() { } } +void UIManager::printESPDataOverrideNotSupported() { + m_executor.submit([]() { ConsolePrinter::simplePrint("Cannot override ESP Value in this device."); }); +} + +void UIManager::printESPNotSupported() { + m_executor.submit([]() { ConsolePrinter::simplePrint("ESP is not supported in this device."); }); +} + } // namespace sampleApp } // namespace alexaClientSDK diff --git a/SampleApp/src/UserInputManager.cpp b/SampleApp/src/UserInputManager.cpp index f96fb440c0..a99a30184e 100644 --- a/SampleApp/src/UserInputManager.cpp +++ b/SampleApp/src/UserInputManager.cpp @@ -40,6 +40,7 @@ static const char SETTINGS = 'c'; static const char SPEAKER_CONTROL = 'p'; static const char FIRMWARE_VERSION = 'f'; static const char ESP_CONTROL = 'e'; +static const char RESET = 'k'; enum class SettingsValues : char { LOCALE = '1' }; @@ -196,6 +197,29 @@ void UserInputManager::run() { break; } } + } else if (x == RESET) { + m_interactionManager->confirmResetDevice(); + char y; + bool cancelReset = false; + do { + std::cin >> y; + // Check the Setting which has to be changed. + switch (y) { + case 'Y': + case 'y': + // Login experience is not provided yet. Exit sample app for now. + m_interactionManager->resetDevice(); + return; + case 'N': + case 'n': + cancelReset = true; + break; + default: + m_interactionManager->errorValue(); + m_interactionManager->confirmResetDevice(); + break; + } + } while (!cancelReset); } else { m_interactionManager->errorValue(); } diff --git a/Storage/SQLiteStorage/CMakeLists.txt b/Storage/SQLiteStorage/CMakeLists.txt index 844e6bdbd2..d51789deaf 100644 --- a/Storage/SQLiteStorage/CMakeLists.txt +++ b/Storage/SQLiteStorage/CMakeLists.txt @@ -4,3 +4,4 @@ project(SQLiteStorage LANGUAGES CXX) include(../../build/BuildDefaults.cmake) add_subdirectory("src") +add_subdirectory("test") diff --git a/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteDatabase.h b/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteDatabase.h new file mode 100644 index 0000000000..130a86e281 --- /dev/null +++ b/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteDatabase.h @@ -0,0 +1,121 @@ +/* + * Copyright 2018 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. + */ + +#ifndef ALEXA_CLIENT_SDK_STORAGE_SQLITESTORAGE_INCLUDE_SQLITESTORAGE_SQLITEDATABASE_H_ +#define ALEXA_CLIENT_SDK_STORAGE_SQLITESTORAGE_INCLUDE_SQLITESTORAGE_SQLITEDATABASE_H_ + +#include +#include +#include + +#include + +#include + +namespace alexaClientSDK { +namespace storage { +namespace sqliteStorage { + +/** + * A basic class for performing basic SQLite database operations. This the boilerplate code used to manage the + * SQLiteDatabase. This database is not thread-safe, and must be protected before being used in a mutlithreaded + * fashion. + */ +class SQLiteDatabase { +public: + /** + * Constructor. The internal variables are initialized. + * + * @param filePath The location of the file that the SQLite DB will use as it's backing storage when initialize or + * open are called. + */ + SQLiteDatabase(const std::string& filePath); + + /** + * Destructor. + * + * On destruction, the DB is checked to see if it is closed. It must be closed before the SQLiteDatabase object is + * destroyed as there is no handle to the DB to close it after this object is gone. + */ + ~SQLiteDatabase(); + + /** + * The internal SQLite DB is created. + * + * @return true, if the (empty) DB is created successfully. If there is already a DB at the path specified, this + * function fails and returns false. It also returns false if the database is already opened or if there is an + * internal failure in creating the DB. + */ + bool initialize(); + + /** + * The internal SQLite DB is opened. + * + * @return true, if the DB is opened successfully. If there is no DB at the path specified, this function fails and + * returns false. It also returns false if the database is already opened or if there is an internal failure in + * creating the DB. + */ + bool open(); + + /** + * Run a SQL query on the database. + * + * @param sqlString A valid SQL query. + * @return true if successful, false if there is a problem. + */ + bool performQuery(const std::string& sqlString); + + /** + * Check to see if a specified table exists. + * + * @param tableName The name of the table to check. + * @return true if the table exists, flase if it doesn't or there is an error. + */ + bool tableExists(const std::string& tableName); + + /** + * Remove all the rows from the specified table. + * + * @param tableName The name of the table to clear. + * @return true if successful, false if there was an error. + */ + bool clearTable(const std::string& tableName); + + /** + * If open, close the internal SQLite DB. Do nothing if there is no DB open. + */ + void close(); + + /** + * Create an SQLiteStatement object to execute the provided string. + * + * @param sqlString The SQL command to execute. + * @return A unique_ptr to the SQLiteStatement that represents the sqlString. + */ + std::unique_ptr createStatement(const std::string& sqlString); + +private: + /// The path to use when creating/opening the internal SQLite DB. + const std::string m_storageFilePath; + + /// The sqlite database handle. + sqlite3* m_dbHandle; +}; + +} // namespace sqliteStorage +} // namespace storage +} // namespace alexaClientSDK + +#endif // ALEXA_CLIENT_SDK_STORAGE_SQLITESTORAGE_INCLUDE_SQLITESTORAGE_SQLITEDATABASE_H_ diff --git a/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteStatement.h b/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteStatement.h index 23522f41e0..412ffbdaec 100644 --- a/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteStatement.h +++ b/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteStatement.h @@ -100,13 +100,6 @@ class SQLiteStatement { */ bool bindStringParameter(int index, const std::string& value); - /** - * Returns the managed statement handle. - * - * @return The managed statement handle. - */ - sqlite3_stmt* getHandle(); - /** * Returns the SQLite result for the last step operation performed. * diff --git a/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteUtils.h b/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteUtils.h index 971fd7bb01..47d5916a5e 100644 --- a/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteUtils.h +++ b/Storage/SQLiteStorage/include/SQLiteStorage/SQLiteUtils.h @@ -16,9 +16,12 @@ #ifndef ALEXA_CLIENT_SDK_STORAGE_SQLITESTORAGE_INCLUDE_SQLITESTORAGE_SQLITEUTILS_H_ #define ALEXA_CLIENT_SDK_STORAGE_SQLITESTORAGE_INCLUDE_SQLITESTORAGE_SQLITEUTILS_H_ -#include #include +#include + +#include + namespace alexaClientSDK { namespace storage { namespace sqliteStorage { @@ -45,7 +48,7 @@ sqlite3* openSQLiteDatabase(const std::string& filePath); * Closes a SQLite database. * * @param The handle to sqlite database. - * @return Whether the operation was successul. + * @return Whether the operation was successful. */ bool closeSQLiteDatabase(sqlite3* dbHandle); @@ -62,24 +65,24 @@ bool performQuery(sqlite3* dbHandle, const std::string& sqlString); /** * Acquires the number of rows in a table within an open database. * - * @param dbHandle A SQLite handle to an open database. + * @param db A SQLite database object. * @param tableName The name of the table to be queried. * @param[out] numberRows Where the number of rows will be stored on a successful lookup. * @return Whether the lookup was successful or not. */ -bool getNumberTableRows(sqlite3* dbHandle, const std::string& tableName, int* numberRows); +bool getNumberTableRows(SQLiteDatabase* db, const std::string& tableName, int* numberRows); /** * Queries a specified column in a SQLite table, and identifies the highest value across all rows. * This function requires that the table and column exists, and that the column is of integer type. * - * @param dbHandle A SQLite handle to an open database. + * @param db A SQLite database object. * @param tableName The name of the table to be queried. * @param columnName The name of the column in the table to be queried. * @param[out] maxId Where the maximum id will be stored on a successful lookup. * @return Whether the lookup was successful or not. */ -bool getTableMaxIntValue(sqlite3* dbHandle, const std::string& tableName, const std::string& columnName, int* maxId); +bool getTableMaxIntValue(SQLiteDatabase* db, const std::string& tableName, const std::string& columnName, int* maxId); /** * Queries if a table exists within a given open database. diff --git a/Storage/SQLiteStorage/src/CMakeLists.txt b/Storage/SQLiteStorage/src/CMakeLists.txt index f14feb15e6..573d0ce22c 100644 --- a/Storage/SQLiteStorage/src/CMakeLists.txt +++ b/Storage/SQLiteStorage/src/CMakeLists.txt @@ -1,5 +1,6 @@ add_definitions("-DACSDK_LOG_MODULE=sqliteStorage") add_library(SQLiteStorage SHARED + SQLiteDatabase.cpp SQLiteStatement.cpp SQLiteUtils.cpp) @@ -15,4 +16,4 @@ target_include_directories(SQLiteStorage PUBLIC target_link_libraries(SQLiteStorage AVSCommon "${SQLITE_LDFLAGS}") # install target -asdk_install() \ No newline at end of file +asdk_install() diff --git a/Storage/SQLiteStorage/src/SQLiteDatabase.cpp b/Storage/SQLiteStorage/src/SQLiteDatabase.cpp new file mode 100644 index 0000000000..205002e8ed --- /dev/null +++ b/Storage/SQLiteStorage/src/SQLiteDatabase.cpp @@ -0,0 +1,142 @@ +/* + * Copyright 2018 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. + */ + +#include "SQLiteStorage/SQLiteDatabase.h" + +#include +#include + +#include "SQLiteStorage/SQLiteUtils.h" + +namespace alexaClientSDK { +namespace storage { +namespace sqliteStorage { + +/// String to identify log entries originating from this file. +static const std::string TAG("SQLiteDatabase"); + +/** + * 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) + +SQLiteDatabase::SQLiteDatabase(const std::string& storageFilePath) : + m_storageFilePath{storageFilePath}, + m_dbHandle{nullptr} { +} + +SQLiteDatabase::~SQLiteDatabase() { + if (m_dbHandle) { + ACSDK_WARN( + LX(__func__).m("DB wasn't closed before destruction of SQLiteDatabase").d("file path", m_storageFilePath)); + + // TODO: The DB should just be closed by the destructor, but currently some of the tests call + // SQLiteDatabase::close(). It doesn't happen outside of the test code. This JIRA is filed to take care of it + // later: ACSDK-1094. + close(); + } +} + +bool SQLiteDatabase::initialize() { + if (m_dbHandle) { + ACSDK_ERROR(LX(__func__).m("Database is already open.")); + return false; + } + + if (avsCommon::utils::file::fileExists(m_storageFilePath)) { + ACSDK_ERROR(LX(__func__).m("File specified already exists.").d("file path", m_storageFilePath)); + return false; + } + + m_dbHandle = createSQLiteDatabase(m_storageFilePath); + if (!m_dbHandle) { + ACSDK_ERROR(LX(__func__).m("Database could not be created.").d("file path", m_storageFilePath)); + return false; + } + + return true; +} + +bool SQLiteDatabase::open() { + if (m_dbHandle) { + ACSDK_ERROR(LX(__func__).m("Database is already open.")); + return false; + } + + if (!avsCommon::utils::file::fileExists(m_storageFilePath)) { + ACSDK_ERROR(LX(__func__).m("File specified does not exist.").d("file path", m_storageFilePath)); + return false; + } + + m_dbHandle = openSQLiteDatabase(m_storageFilePath); + if (!m_dbHandle) { + ACSDK_ERROR(LX(__func__).m("Database could not be opened.").d("file path", m_storageFilePath)); + return false; + } + + return true; +} + +bool SQLiteDatabase::performQuery(const std::string& sqlString) { + if (!alexaClientSDK::storage::sqliteStorage::performQuery(m_dbHandle, sqlString)) { + ACSDK_ERROR(LX(__func__).m("Table could not be created.").d("SQL string", sqlString)); + return false; + } + + return true; +} + +bool SQLiteDatabase::tableExists(const std::string& tableName) { + if (!alexaClientSDK::storage::sqliteStorage::tableExists(m_dbHandle, tableName)) { + ACSDK_ERROR( + LX(__func__).d("reason", "table doesn't exist or there was an error checking").d("table", tableName)); + return false; + } + return true; +} + +bool SQLiteDatabase::clearTable(const std::string& tableName) { + if (!alexaClientSDK::storage::sqliteStorage::clearTable(m_dbHandle, tableName)) { + ACSDK_ERROR(LX(__func__).d("could not clear table", tableName)); + return false; + } + + return true; +} + +void SQLiteDatabase::close() { + if (m_dbHandle) { + closeSQLiteDatabase(m_dbHandle); + m_dbHandle = nullptr; + } +} + +std::unique_ptr SQLiteDatabase::createStatement( + const std::string& sqlString) { + std::unique_ptr statement( + new SQLiteStatement(m_dbHandle, sqlString)); + if (!statement->isValid()) { + ACSDK_ERROR(LX("createStatementFailed").d("sqlString", sqlString)); + statement = nullptr; + } + + return statement; +} + +} // namespace sqliteStorage +} // namespace storage +} // namespace alexaClientSDK diff --git a/Storage/SQLiteStorage/src/SQLiteStatement.cpp b/Storage/SQLiteStorage/src/SQLiteStatement.cpp index 40601677b9..9642c42965 100644 --- a/Storage/SQLiteStorage/src/SQLiteStatement.cpp +++ b/Storage/SQLiteStorage/src/SQLiteStatement.cpp @@ -59,7 +59,7 @@ SQLiteStatement::~SQLiteStatement() { } bool SQLiteStatement::isValid() { - return (getHandle() != nullptr); + return nullptr != m_handle; } bool SQLiteStatement::step() { @@ -155,10 +155,6 @@ bool SQLiteStatement::bindStringParameter(int index, const std::string& value) { return true; } -sqlite3_stmt* SQLiteStatement::getHandle() { - return m_handle; -} - int SQLiteStatement::getStepResult() const { return m_stepResult; } diff --git a/Storage/SQLiteStorage/src/SQLiteUtils.cpp b/Storage/SQLiteStorage/src/SQLiteUtils.cpp index 82b1b28a52..e614d83b9f 100644 --- a/Storage/SQLiteStorage/src/SQLiteUtils.cpp +++ b/Storage/SQLiteStorage/src/SQLiteUtils.cpp @@ -151,32 +151,32 @@ bool performQuery(sqlite3* dbHandle, const std::string& sqlString) { return true; } -bool getNumberTableRows(sqlite3* dbHandle, const std::string& tableName, int* numberRows) { - if (!dbHandle) { - ACSDK_ERROR(LX("getNumberTableRowsFailed").m("dbHandle was nullptr.")); +bool getNumberTableRows(SQLiteDatabase* db, const std::string& tableName, int* numberRows) { + if (!db) { + ACSDK_ERROR(LX("getNumberTableRowsFailed").m("db was nullptr.")); return false; } if (!numberRows) { - ACSDK_ERROR(LX("getNumberTableRowsFailed").m("dbHandle was nullptr.")); + ACSDK_ERROR(LX("getNumberTableRowsFailed").m("numberRows was nullptr.")); return false; } std::string sqlString = "SELECT COUNT(*) FROM " + tableName + ";"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("getNumberTableRowsFailed").m("Could not create statement.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("getNumberTableRowsFailed").m("Could not step to next row.")); return false; } const int RESULT_COLUMN_POSITION = 0; - std::string rowValue = statement.getColumnText(RESULT_COLUMN_POSITION); + std::string rowValue = statement->getColumnText(RESULT_COLUMN_POSITION); if (!stringToInt(rowValue.c_str(), numberRows)) { ACSDK_ERROR(LX("getNumberTableRowsFailed").d("Could not convert string to integer", rowValue)); @@ -186,28 +186,33 @@ bool getNumberTableRows(sqlite3* dbHandle, const std::string& tableName, int* nu return true; } -bool getTableMaxIntValue(sqlite3* dbHandle, const std::string& tableName, const std::string& columnName, int* maxId) { - if (!maxId) { - ACSDK_ERROR(LX("getMaxIdFailed").m("dbHandle was nullptr.")); +bool getTableMaxIntValue(SQLiteDatabase* db, const std::string& tableName, const std::string& columnName, int* maxInt) { + if (!db) { + ACSDK_ERROR(LX("getTableMaxIntValue").m("db was nullptr.")); + return false; + } + + if (!maxInt) { + ACSDK_ERROR(LX("getMaxIntFailed").m("maxInt was nullptr.")); return false; } std::string sqlString = "SELECT " + columnName + " FROM " + tableName + " ORDER BY " + columnName + " DESC LIMIT 1;"; - SQLiteStatement statement(dbHandle, sqlString); + auto statement = db->createStatement(sqlString); - if (!statement.isValid()) { + if (!statement) { ACSDK_ERROR(LX("getTableMaxIntValueFailed").m("Could not create statement.")); return false; } - if (!statement.step()) { + if (!statement->step()) { ACSDK_ERROR(LX("getTableMaxIntValueFailed").m("Could not step to next row.")); return false; } - int stepResult = statement.getStepResult(); + int stepResult = statement->getStepResult(); if (stepResult != SQLITE_ROW && stepResult != SQLITE_DONE) { ACSDK_ERROR(LX("getTableMaxIntValueFailed").m("Step did not evaluate to either row or completion.")); @@ -216,15 +221,15 @@ bool getTableMaxIntValue(sqlite3* dbHandle, const std::string& tableName, const // No entries were found in database - set to zero as the current max id. if (SQLITE_DONE == stepResult) { - *maxId = 0; + *maxInt = 0; } // Entries were found - let's get the value. if (SQLITE_ROW == stepResult) { const int RESULT_COLUMN_POSITION = 0; - std::string rowValue = statement.getColumnText(RESULT_COLUMN_POSITION); + std::string rowValue = statement->getColumnText(RESULT_COLUMN_POSITION); - if (!stringToInt(rowValue.c_str(), maxId)) { + if (!stringToInt(rowValue.c_str(), maxInt)) { ACSDK_ERROR(LX("getTableMaxIntValueFailed").d("Could not convert string to integer", rowValue)); return false; } diff --git a/Storage/SQLiteStorage/test/CMakeLists.txt b/Storage/SQLiteStorage/test/CMakeLists.txt new file mode 100644 index 0000000000..37190283eb --- /dev/null +++ b/Storage/SQLiteStorage/test/CMakeLists.txt @@ -0,0 +1,7 @@ +cmake_minimum_required(VERSION 3.1 FATAL_ERROR) + +set(INCLUDE_PATH + "${SQLiteStorage_SOURCE_DIR}/include") + +discover_unit_tests("${INCLUDE_PATH}" SQLiteStorage ".") + diff --git a/Storage/SQLiteStorage/test/SQLiteDatabaseTest.cpp b/Storage/SQLiteStorage/test/SQLiteDatabaseTest.cpp new file mode 100644 index 0000000000..282099ca7a --- /dev/null +++ b/Storage/SQLiteStorage/test/SQLiteDatabaseTest.cpp @@ -0,0 +1,148 @@ +/* + * Copyright 2018 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. + */ + +#include + +#include + +#include + +#include +#include + +namespace alexaClientSDK { +namespace storage { +namespace sqliteStorage { +namespace test { + +/// Variable for storing the working directory. This is where all of the test databases will be created. +static std::string g_workingDirectory; + +/// An example of a path that doesn't exist in a system. +static const std::string BAD_PATH = + "_/_/_/there/is/no/way/this/path/should/exist/,/so/it/should/cause/an/error/when/creating/the/db"; + +/** + * Helper function that generates a unique filepath using the passed in g_workingDirectory. + * + * @return A unique filepath. + */ +static std::string generateDbFilePath() { + struct timeval t; + gettimeofday(&t, nullptr); + std::string filePath = g_workingDirectory + "/SQLiteDatabaseTest-" + std::to_string(t.tv_sec) + + std::to_string(t.tv_usec) + std::to_string(rand()); + EXPECT_FALSE(avsCommon::utils::file::fileExists(filePath)); + return filePath; +} + +/// Test to close DB then open it. +TEST(SQLiteDatabaseTest, CloseThenOpen) { + auto dbFilePath = generateDbFilePath(); + SQLiteDatabase db(dbFilePath); + ASSERT_TRUE(db.initialize()); + db.close(); + ASSERT_TRUE(db.open()); +} + +/// Test to initialize already existing DB. +TEST(SQLiteDatabaseTest, InitializeAlreadyExisting) { + auto dbFilePath = generateDbFilePath(); + SQLiteDatabase db1(dbFilePath); + ASSERT_TRUE(db1.initialize()); + + SQLiteDatabase db2(dbFilePath); + ASSERT_FALSE(db2.initialize()); + + db2.close(); + db1.close(); +} + +/// Test to initialize a bad path. +TEST(SQLiteDatabaseTest, InitializeBadPath) { + SQLiteDatabase db(BAD_PATH); + ASSERT_FALSE(db.initialize()); +} + +/// Test to initialize a directory. +TEST(SQLiteDatabaseTest, InitializeOnDirectory) { + SQLiteDatabase db(g_workingDirectory); + ASSERT_FALSE(db.initialize()); +} + +/// Test to initialize DB twice. +TEST(SQLiteDatabaseTest, InitializeTwice) { + SQLiteDatabase db(generateDbFilePath()); + + ASSERT_TRUE(db.initialize()); + ASSERT_FALSE(db.initialize()); + db.close(); +} + +/// Test to open already existing DB. +TEST(SQLiteDatabaseTest, OpenAlreadyExisting) { + auto dbFilePath = generateDbFilePath(); + SQLiteDatabase db1(dbFilePath); + ASSERT_TRUE(db1.initialize()); + + SQLiteDatabase db2(dbFilePath); + ASSERT_TRUE(db2.open()); + + db2.close(); + db1.close(); +} + +/// Test to open a bad path. +TEST(SQLiteDatabaseTest, OpenBadPath) { + SQLiteDatabase db(BAD_PATH); + ASSERT_FALSE(db.open()); +} + +/// Test to open directory. +TEST(SQLiteDatabaseTest, OpenDirectory) { + SQLiteDatabase db(g_workingDirectory); + ASSERT_FALSE(db.open()); +} + +/// Test to open DB twice. +TEST(SQLiteDatabaseTest, OpenTwice) { + auto dbFilePath = generateDbFilePath(); + SQLiteDatabase db1(dbFilePath); + ASSERT_TRUE(db1.initialize()); + + SQLiteDatabase db2(dbFilePath); + ASSERT_TRUE(db2.open()); + ASSERT_FALSE(db2.open()); + + db2.close(); + db1.close(); +} + +} // namespace test +} // namespace sqliteStorage +} // namespace storage +} // namespace alexaClientSDK + +int main(int argc, char** argv) { + ::testing::InitGoogleTest(&argc, argv); + + if (argc < 2) { + std::cerr << "Usage: " << std::string(argv[0]) << " " << std::endl; + return -1; + } else { + alexaClientSDK::storage::sqliteStorage::test::g_workingDirectory = std::string(argv[1]); + return RUN_ALL_TESTS(); + } +} diff --git a/ThirdParty/rapidjson/CMakeLists.txt b/ThirdParty/rapidjson/CMakeLists.txt index 9e5dd70823..7f5f545d86 100644 --- a/ThirdParty/rapidjson/CMakeLists.txt +++ b/ThirdParty/rapidjson/CMakeLists.txt @@ -2,3 +2,4 @@ cmake_minimum_required(VERSION 3.1 FATAL_ERROR) project(RapidJSON LANGUAGES CXX) set(RAPIDJSON_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/rapidjson-1.1.0/include" CACHE INTERNAL "") +install(DIRECTORY "${RAPIDJSON_INCLUDE_DIR}" DESTINATION "${ASDK_INCLUDE_INSTALL_DIR}") diff --git a/build/BuildDefaults.cmake b/build/BuildDefaults.cmake index 5545c98f72..90dcfe978a 100644 --- a/build/BuildDefaults.cmake +++ b/build/BuildDefaults.cmake @@ -32,3 +32,13 @@ include(PortAudio) # Setup Test Options variables. include(TestOptions) + +# Setup platform dependant variables. +include (Platforms) + +# Setup ESP variables. +include (ESP) + +if (HAS_EXTERNAL_MEDIA_PLAYER_ADAPTERS) + include (ExternalMediaPlayerAdapters) +endif() diff --git a/build/cmake/BuildOptions.cmake b/build/cmake/BuildOptions.cmake index 672d161169..a33737f2bf 100644 --- a/build/cmake/BuildOptions.cmake +++ b/build/cmake/BuildOptions.cmake @@ -46,7 +46,7 @@ set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_CXX_EXTENSIONS OFF) # Determine the platform and compiler dependent flags. -if (UNIX) +if (UNIX OR CMAKE_COMPILER_IS_GNUCXX) set(CXX_PLATFORM_DEPENDENT_FLAGS_DEBUG "-DDEBUG -DACSDK_DEBUG_LOG_ENABLED -Wall -Werror -Wsign-compare -g") set(CXX_PLATFORM_DEPENDENT_FLAGS_RELEASE "-DNDEBUG -Wall -Werror -O2") set(CXX_PLATFORM_DEPENDENT_FLAGS_MINSIZEREL "-DNDEBUG -Wall -Werror -Os") diff --git a/build/cmake/ESP.cmake b/build/cmake/ESP.cmake new file mode 100644 index 0000000000..8620f034da --- /dev/null +++ b/build/cmake/ESP.cmake @@ -0,0 +1,22 @@ +# +# Setup the ESP compiler options. +# +# To build with ESP support, include the following option on the cmake command line. +# cmake +# -DESP_PROVIDER=ON +# -DESP_LIB_PATH= +# -DESP_INCLUDE_DIR= +# + +option(ESP_PROVIDER "Enable Echo Spatial Perception (ESP)." OFF) + +if(ESP_PROVIDER) + if(NOT ESP_LIB_PATH) + message(FATAL_ERROR "Must pass library path of ESP to enable it.") + endif() + if(NOT ESP_INCLUDE_DIR) + message(FATAL_ERROR "Must pass include dir path of ESP to enable it.") + endif() + message("Creating ${PROJECT_NAME} with Echo Spatial Perception (ESP)") + add_definitions(-DENABLE_ESP) +endif() diff --git a/build/cmake/KeywordDetector.cmake b/build/cmake/KeywordDetector.cmake index 10aa0fa0aa..08bcd32720 100644 --- a/build/cmake/KeywordDetector.cmake +++ b/build/cmake/KeywordDetector.cmake @@ -2,11 +2,16 @@ # Setup the Keyword Detector type and compiler options. # # To build with a Keyword Detector, run the following command with a keyword detector type of AMAZON_KEY_WORD_DETECTOR, -# KITTAI_KEY_WORD_DETECTOR, or SENSORY_KEY_WORD_DETECTOR: +# AMAZONLITE_KEY_WORD_DETECTOR, KITTAI_KEY_WORD_DETECTOR, or SENSORY_KEY_WORD_DETECTOR: # cmake # -DAMAZON_KEY_WORD_DETECTOR=ON # -DAMAZON_KEY_WORD_DETECTOR_LIB_PATH= # -DAMAZON_KEY_WORD_DETECTOR_INCLUDE_DIR= +# -DAMAZONLITE_KEY_WORD_DETECTOR=ON +# -DAMAZONLITE_KEY_WORD_DETECTOR_LIB_PATH= +# -DAMAZONLITE_KEY_WORD_DETECTOR_INCLUDE_DIR= +# -DAMAZONLITE_KEY_WORD_DETECTOR_DYNAMIC_MODEL_LOADING= +# -DAMAZONLITE_KEY_WORD_DETECTOR_MODEL_CPP_PATH= # -DKITTAI_KEY_WORD_DETECTOR=ON # -DKITTAI_KEY_WORD_DETECTOR_LIB_PATH= # -DKITTAI_KEY_WORD_DETECTOR_INCLUDE_DIR= @@ -16,10 +21,12 @@ # option(AMAZON_KEY_WORD_DETECTOR "Enable Amazon keyword detector." OFF) +option(AMAZONLITE_KEY_WORD_DETECTOR "Enable AmazonLite keyword detector." OFF) +option(AMAZONLITE_KEY_WORD_DETECTOR_DYNAMIC_MODEL_LOADING "Enable AmazonLite keyword detector dynamic model loading." OFF) option(KITTAI_KEY_WORD_DETECTOR "Enable KittAi keyword detector." OFF) option(SENSORY_KEY_WORD_DETECTOR "Enable Sensory keyword detector." OFF) -if(NOT AMAZON_KEY_WORD_DETECTOR AND NOT KITTAI_KEY_WORD_DETECTOR AND NOT SENSORY_KEY_WORD_DETECTOR) +if(NOT AMAZON_KEY_WORD_DETECTOR AND NOT AMAZONLITE_KEY_WORD_DETECTOR AND NOT KITTAI_KEY_WORD_DETECTOR AND NOT SENSORY_KEY_WORD_DETECTOR) message("No keyword detector type specified, skipping build of keyword detector.") return() endif() @@ -36,6 +43,25 @@ if(AMAZON_KEY_WORD_DETECTOR) add_definitions(-DKWD_AMAZON) endif() +if(AMAZONLITE_KEY_WORD_DETECTOR) + message("Creating ${PROJECT_NAME} with keyword detector type: AmazonLite") + if(NOT AMAZONLITE_KEY_WORD_DETECTOR_LIB_PATH) + message(FATAL_ERROR "Must pass library path of AmazonLite KeywordDetector!") + endif() + if(NOT AMAZONLITE_KEY_WORD_DETECTOR_INCLUDE_DIR) + message(FATAL_ERROR "Must pass include dir path of AmazonLite KeywordDetector!") + endif() + if(NOT AMAZONLITE_KEY_WORD_DETECTOR_DYNAMIC_MODEL_LOADING) + if(NOT AMAZONLITE_KEY_WORD_DETECTOR_MODEL_CPP_PATH) + message(FATAL_ERROR "Must pass the path of the desired model .cpp file for the AmazonLite Keyword Detector if dynamic loading of model is disabled!") + endif() + else() + add_definitions(-DKWD_AMAZONLITE_DYNAMIC_MODEL_LOADING) + endif() + add_definitions(-DKWD) + add_definitions(-DKWD_AMAZONLITE) +endif() + if(KITTAI_KEY_WORD_DETECTOR) message("Creating ${PROJECT_NAME} with keyword detector type: KittAi") if(NOT KITTAI_KEY_WORD_DETECTOR_LIB_PATH) diff --git a/build/cmake/Platforms.cmake b/build/cmake/Platforms.cmake new file mode 100644 index 0000000000..df379c20fc --- /dev/null +++ b/build/cmake/Platforms.cmake @@ -0,0 +1,21 @@ +# +# Setup the platform dependent Cmake options. +# + +# Build for Windows is supported +# with MSYS2-MinGW64 shell +# Run cmake with -G"MSYS Makefiles" +if (WIN32) + add_definitions( + # Wingdi.h inclusion causes build error + -DNOGDI + # Gtest has open issue with pthread on MinGW + # See: https://github.com/google/googletest/issues/606 + -Dgtest_disable_pthreads=ON + -DNO_SIGPIPE + ) + + # Windows doesn't have rpath so shared libraries should either be + # in the same directory or add them to the path variable. + set(CMAKE_RUNTIME_OUTPUT_DIRECTORY ${CMAKE_BINARY_DIR}/bin) +endif() diff --git a/tools/RaspberryPi/config.txt b/tools/Install/config.txt similarity index 100% rename from tools/RaspberryPi/config.txt rename to tools/Install/config.txt diff --git a/tools/Install/mingw.sh b/tools/Install/mingw.sh new file mode 100644 index 0000000000..027eb5239d --- /dev/null +++ b/tools/Install/mingw.sh @@ -0,0 +1,67 @@ +# +# Copyright 2018 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. +# + +if [ -z "$PLATFORM" ]; then + echo "You should run the main script." + exit 1 +fi + +LIB_SUFFIX="dll.a" +START_SCRIPT="$INSTALL_BASE/startsample.bat" +CMAKE_PLATFORM_SPECIFIC=(-G 'MSYS Makefiles' -Dgtest_disable_pthreads=ON) +CONFIG_DB_PATH=`cygpath.exe -m $DB_PATH` + +install_dependencies() { + + PACMAN_ARGS="--noconfirm --needed" + + # Build tools and make (mingw32-make fails building portAudio) + pacman -S ${PACMAN_ARGS} git mingw-w64-x86_64-toolchain mingw-w64-x86_64-cmake msys/tar msys/make + + # pip required for installing flask and commentjson + pacman -S ${PACMAN_ARGS} mingw64/mingw-w64-x86_64-python2-pip + + # Requirements for AuthServer + pip install flask requests commentjson + + # required by the SDK + pacman -S ${PACMAN_ARGS} mingw-w64-x86_64-sqlite3 + + # MediaPlayer reference Implementation + pacman -S ${PACMAN_ARGS} mingw64/mingw-w64-x86_64-gstreamer + + # MediaPlayer reference Implementation + pacman -S ${PACMAN_ARGS} mingw64/mingw-w64-x86_64-gst-plugins-good mingw64/mingw-w64-x86_64-gst-plugins-base mingw64/mingw-w64-x86_64-gst-plugins-ugly + + # Music providers requirements + pacman -S ${PACMAN_ARGS} mingw64/mingw-w64-x86_64-gst-plugins-bad mingw64/mingw-w64-x86_64-faad2 + + # Install Portaudio + pacman -S ${PACMAN_ARGS} mingw64/mingw-w64-x86_64-portaudio + +} + +run_os_specifics() { + : +} + +generate_start_script() { + cat << EOF > "$START_SCRIPT" + set path=`cygpath.exe -m $MSYSTEM_PREFIX/bin`;%path%; + cd `cygpath.exe -m $BUILD_PATH/bin` + SampleApp.exe `cygpath.exe -m $CONFIG_FILE` DEBUG9 + pause +EOF +} \ No newline at end of file diff --git a/tools/Install/pi.sh b/tools/Install/pi.sh new file mode 100644 index 0000000000..32e6a37497 --- /dev/null +++ b/tools/Install/pi.sh @@ -0,0 +1,76 @@ +# +# Copyright 2018 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. +# + +if [ -z "$PLATFORM" ]; then + echo "You should run the setup.sh script." + exit 1 +fi + +SOUND_CONFIG="$HOME/.asoundrc" +START_SCRIPT="$INSTALL_BASE/startsample.sh" +CMAKE_PLATFORM_SPECIFIC=(-DSENSORY_KEY_WORD_DETECTOR=ON \ + -DSENSORY_KEY_WORD_DETECTOR_LIB_PATH=$THIRD_PARTY_PATH/alexa-rpi/lib/libsnsr.a \ + -DSENSORY_KEY_WORD_DETECTOR_INCLUDE_DIR=$THIRD_PARTY_PATH/alexa-rpi/include) + + +install_dependencies() { + sudo apt-get update + sudo apt-get -y install git gcc cmake build-essential libsqlite3-dev libcurl4-openssl-dev libfaad-dev libsoup2.4-dev libgcrypt20-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-good libasound2-dev sox gedit vim python3-pip + pip install flask commentjson +} + +run_os_specifics() { + build_kwd_engine + configure_sound +} + +configure_sound() { + echo + echo "==============> SAVING AUDIO CONFIGURATION FILE ==============" + echo + + cat << EOF > "$SOUND_CONFIG" + pcm.!default { + type asym + playback.pcm { + type plug + slave.pcm "hw:0,0" + } + capture.pcm { + type plug + slave.pcm "hw:1,0" + } + } +EOF +} + +build_kwd_engine() { + #get sensory and build + echo + echo "==============> CLONING AND BUILDING SENSORY ==============" + echo + + cd $THIRD_PARTY_PATH + git clone git://github.com/Sensory/alexa-rpi.git + bash ./alexa-rpi/bin/license.sh +} + +generate_start_script() { + cat << EOF > "$START_SCRIPT" + cd "$BUILD_PATH/SampleApp/src" + + ./SampleApp "$CONFIG_FILE" "$THIRD_PARTY_PATH/alexa-rpi/models" DEBUG9 +EOF +} \ No newline at end of file diff --git a/tools/RaspberryPi/setup.sh b/tools/Install/setup.sh similarity index 81% rename from tools/RaspberryPi/setup.sh rename to tools/Install/setup.sh index 9d8903519f..bedd38e2ad 100644 --- a/tools/RaspberryPi/setup.sh +++ b/tools/Install/setup.sh @@ -15,6 +15,65 @@ # permissions and limitations under the License. # + +LOCALE=${LOCALE:-'en-US'} + +PORT_AUDIO_FILE="pa_stable_v190600_20161030.tgz" +PORT_AUDIO_DOWNLOAD_URL="http://www.portaudio.com/archives/$PORT_AUDIO_FILE" + +TEST_MODEL_DOWNLOAD="https://github.com/Sensory/alexa-rpi/blob/master/models/spot-alexa-rpi-31000.snsr" + +BUILD_TESTS=${BUILD_TESTS:-'true'} + +CURRENT_DIR="$(pwd)" +INSTALL_BASE=${INSTALL_BASE:-"$CURRENT_DIR"} +SOURCE_FOLDER=${SDK_LOC:-''} +THIRD_PARTY_FOLDER=${THIRD_PARTY_LOC:-'third-party'} +BUILD_FOLDER=${BUILD_FOLDER:-'build'} +SOUNDS_FOLDER=${SOUNDS_FOLDER:-'sounds'} +DB_FOLDER=${DB_FOLDER:-'db'} + +SOURCE_PATH="$INSTALL_BASE/$SOURCE_FOLDER" +THIRD_PARTY_PATH="$INSTALL_BASE/$THIRD_PARTY_FOLDER" +BUILD_PATH="$INSTALL_BASE/$BUILD_FOLDER" +SOUNDS_PATH="$INSTALL_BASE/$SOUNDS_FOLDER" +DB_PATH="$INSTALL_BASE/$DB_FOLDER" +CONFIG_DB_PATH="$DB_PATH" +UNIT_TEST_MODEL_PATH="$INSTALL_BASE/avs-device-sdk/KWD/inputs/SensoryModels/" +UNIT_TEST_MODEL="$THIRD_PARTY_PATH/alexa-rpi/models/spot-alexa-rpi-31000.snsr" +CONFIG_FILE="$BUILD_PATH/Integration/AlexaClientSDKConfig.json" +START_AUTH_SCRIPT="$INSTALL_BASE/startauth.sh" +TEST_SCRIPT="$INSTALL_BASE/test.sh" +LIB_SUFFIX="a" + +get_platform() { + uname_str=`uname -a` + + if [[ "$uname_str" == "Linux raspberrypi"* ]] + then + result="pi" + elif [[ "$uname_str" == "MINGW64"* ]] + then + result="mingw64" + else + result="" + fi +} + +get_platform +PLATFORM=$result + +if [ "$PLATFORM" == "pi" ] +then + source pi.sh +elif [ "$PLATFORM" == "mingw64" ] +then + source mingw.sh +else + echo "The installation script doesn't support current system. (System: $(uname -a))" + exit 1 +fi + echo "################################################################################" echo "################################################################################" echo "" @@ -55,10 +114,6 @@ else exit 1 fi - - - - if [ $# -eq 0 ] then echo 'bash setup.sh ' @@ -67,6 +122,8 @@ then echo ' CLIENT_SECRET=' echo ' PRODUCT_NAME=' echo ' DEVICE_SERIAL_NUMBER=' + + exit 1 fi source $1 @@ -97,37 +154,6 @@ then exit 1 fi -LOCALE=${LOCALE:-'en-US'} - -PORT_AUDIO_FILE="pa_stable_v190600_20161030.tgz" -PORT_AUDIO_DOWNLOAD_URL="http://www.portaudio.com/archives/$PORT_AUDIO_FILE" - -TEST_MODEL_DOWNLOAD="https://github.com/Sensory/alexa-rpi/blob/master/models/spot-alexa-rpi-31000.snsr" - -BUILD_TESTS=${BUILD_TESTS:-'true'} - -CURRENT_DIR="$(pwd)" -INSTALL_BASE=${INSTALL_BASE:-"$CURRENT_DIR"} -SOURCE_FOLDER=${SDK_LOC:-''} -THIRD_PARTY_FOLDER=${THIRD_PARTY_LOC:-'third-party'} -BUILD_FOLDER=${BUILD_FOLDER:-'build'} -SOUNDS_FOLDER=${SOUNDS_FOLDER:-'sounds'} -DB_FOLDER=${DB_FOLDER:-'db'} - -SOURCE_PATH="$INSTALL_BASE/$SOURCE_FOLDER" -THIRD_PARTY_PATH="$INSTALL_BASE/$THIRD_PARTY_FOLDER" -BUILD_PATH="$INSTALL_BASE/$BUILD_FOLDER" -SOUNDS_PATH="$INSTALL_BASE/$SOUNDS_FOLDER" -DB_PATH="$INSTALL_BASE/$DB_FOLDER" -UNIT_TEST_MODEL_PATH="$INSTALL_BASE/avs-device-sdk/KWD/inputs/SensoryModels/" -UNIT_TEST_MODEL="$THIRD_PARTY_PATH/alexa-rpi/models/spot-alexa-rpi-31000.snsr" - -CONFIG_FILE="$BUILD_PATH/Integration/AlexaClientSDKConfig.json" -SOUND_CONFIG="$HOME/.asoundrc" -START_SCRIPT="$INSTALL_BASE/startsample.sh" -START_AUTH_SCRIPT="$INSTALL_BASE/startauth.sh" -TEST_SCRIPT="$INSTALL_BASE/test.sh" - if [ ! -d "$BUILD_PATH" ] then @@ -135,9 +161,7 @@ then echo "==============> INSTALLING REQUIRED TOOLS AND PACKAGE ============" echo - sudo apt-get update - sudo apt-get -y install git gcc cmake build-essential libsqlite3-dev libcurl4-openssl-dev libfaad-dev libsoup2.4-dev libgcrypt20-dev libgstreamer-plugins-bad1.0-dev gstreamer1.0-plugins-good libasound2-dev sox gedit vim python3-pip - pip install flask commentjson + install_dependencies # create / paths echo @@ -163,6 +187,8 @@ then ./configure --without-jack make + run_os_specifics + #get sdk echo echo "==============> CLONING SDK ==============" @@ -171,15 +197,6 @@ then cd $SOURCE_PATH git clone git://github.com/alexa/avs-device-sdk.git - #get sensory and build - echo - echo "==============> CLONING AND BUILDING SENSORY ==============" - echo - - cd $THIRD_PARTY_PATH - git clone git://github.com/Sensory/alexa-rpi.git - bash ./alexa-rpi/bin/license.sh - # make the SDK echo echo "==============> BUILDING SDK ==============" @@ -187,13 +204,10 @@ then cd $BUILD_PATH cmake "$SOURCE_PATH/avs-device-sdk" \ - -DSENSORY_KEY_WORD_DETECTOR=ON \ - -DSENSORY_KEY_WORD_DETECTOR_LIB_PATH="$THIRD_PARTY_PATH/alexa-rpi/lib/libsnsr.a" \ - -DSENSORY_KEY_WORD_DETECTOR_INCLUDE_DIR="$THIRD_PARTY_PATH/alexa-rpi/include" \ + "${CMAKE_PLATFORM_SPECIFIC[@]}" \ -DGSTREAMER_MEDIA_PLAYER=ON -DPORTAUDIO=ON \ - -DPORTAUDIO_LIB_PATH="$THIRD_PARTY_PATH/portaudio/lib/.libs/libportaudio.a" \ + -DPORTAUDIO_LIB_PATH="$THIRD_PARTY_PATH/portaudio/lib/.libs/libportaudio.$LIB_SUFFIX" \ -DPORTAUDIO_INCLUDE_DIR="$THIRD_PARTY_PATH/portaudio/include" \ - -DACSDK_EMIT_SENSITIVE_LOGS=ON \ -DCMAKE_BUILD_TYPE=DEBUG cd $BUILD_PATH @@ -211,19 +225,19 @@ echo cat << EOF > "$CONFIG_FILE" { "alertsCapabilityAgent":{ - "databaseFilePath":"$DB_PATH/alerts.db" + "databaseFilePath":"$CONFIG_DB_PATH/alerts.db" }, "certifiedSender":{ - "databaseFilePath":"$DB_PATH/certifiedSender.db" + "databaseFilePath":"$CONFIG_DB_PATH/certifiedSender.db" }, "settings":{ - "databaseFilePath":"$DB_PATH/settings.db", + "databaseFilePath":"$CONFIG_DB_PATH/settings.db", "defaultAVSClientSettings":{ "locale":"$LOCALE" } }, "notifications":{ - "databaseFilePath":"$DB_PATH/notifications.db" + "databaseFilePath":"$CONFIG_DB_PATH/notifications.db" }, "authDelegate":{ "clientId":"$CLIENT_ID", @@ -240,35 +254,13 @@ echo "==============> FINAL CONFIGURATION ==============" echo cat $CONFIG_FILE -echo -echo "==============> SAVING AUDIO CONFIGURATION FILE ==============" -echo - -cat << EOF > "$SOUND_CONFIG" -pcm.!default { - type asym - playback.pcm { - type plug - slave.pcm "hw:0,0" - } - capture.pcm { - type plug - slave.pcm "hw:1,0" - } -} -EOF - -cat << EOF > "$START_SCRIPT" -cd "$BUILD_PATH/SampleApp/src" - -./SampleApp "$CONFIG_FILE" "$THIRD_PARTY_PATH/alexa-rpi/models" DEBUG9 -EOF - cat << EOF > "$START_AUTH_SCRIPT" cd "$BUILD_PATH" python AuthServer/AuthServer.py EOF +generate_start_script + cat << EOF > "$TEST_SCRIPT" echo echo "==============> BUILDING Tests =============="