diff --git a/CHANGELOG.md b/CHANGELOG.md index da8cdce..c7d31f0 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,22 @@ # Changelog +## [2023.2] + +This release adds support for version 2023.2 of the APL specification. + +### Added + +- Deferred evaluation +- SeekTo command for video +- Alpha feature: Host component +- Alpha feature: Support for viewport autosize +- Alpha feature: InsertItem and RemoveItem command + +### Changed + +- Bug fixes +- Performance improvements. + ## [2023.1] ### Added diff --git a/CMakeLists.txt b/CMakeLists.txt index 17bb4dd..421c312 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -19,6 +19,11 @@ cmake_minimum_required(VERSION 3.11) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) +if(POLICY CMP0135) + # If available, use the new timestamp policy for FetchContent + cmake_policy(SET CMP0135 NEW) +endif() + project(APLCoreEngine VERSION 1.0.0 LANGUAGES CXX) @@ -32,3 +37,13 @@ include(options.cmake) set(APL_PATCH_DIR "${CMAKE_CURRENT_SOURCE_DIR}/patches") include(components.cmake) + +if (BUILD_UNIT_TESTS) + include(CTest) + + set(MEMCHECK_OPTIONS "--tool=memcheck --leak-check=full --show-reachable=no --error-exitcode=1 --errors-for-leak-kinds=definite,possible") + add_custom_target(unittest_memcheck + COMMAND ${CMAKE_CTEST_COMMAND} -VV + --overwrite MemoryCheckCommandOptions=${MEMCHECK_OPTIONS} + -T memcheck) +endif() diff --git a/README.md b/README.md index 5e75167..67d5c52 100644 --- a/README.md +++ b/README.md @@ -263,12 +263,6 @@ To build lib with memory debugging support use: $ cmake -DDEBUG_MEMORY_USE=ON ``` -## Tools -In order to build the tools use: -``` -$ cmake -DTOOLS=ON -``` - ## Paranoid build In order to build library with -Werror use: ``` diff --git a/apl-dev-env.sh b/apl-dev-env.sh index d8e0374..c782583 100644 --- a/apl-dev-env.sh +++ b/apl-dev-env.sh @@ -130,7 +130,9 @@ function apl-test-core { # Run unit tests in the core build apl-switch-to-build-directory build $@ && \ $CMAKE -DBUILD_TESTS=ON -DCOVERAGE=OFF .. && \ make -j$APL_BUILD_PROCS && \ - unit/unittest + aplcore/unit/unittest && \ + tools/unit/tools-unittest && \ + extensions/unit/alexaext-unittest ) } @@ -139,7 +141,9 @@ function apl-memcheck-core { # Run unit tests in the core build apl-switch-to-build-directory build $@ && \ $CMAKE -DBUILD_TESTS=ON -DCOVERAGE=OFF .. && \ make -j$APL_BUILD_PROCS && \ - valgrind --tool=memcheck --gen-suppressions=all --track-origins=yes --leak-check=full --num-callers=50 ./unit/unittest + valgrind --tool=memcheck --gen-suppressions=all --track-origins=yes --leak-check=full --num-callers=50 ./aplcore/unit/unittest && \ + valgrind --tool=memcheck --gen-suppressions=all --track-origins=yes --leak-check=full --num-callers=50 ./tools/unit/tools-unittest && \ + valgrind --tool=memcheck --gen-suppressions=all --track-origins=yes --leak-check=full --num-callers=50 ./extensions/unit/alexaext-unittest ) } diff --git a/aplcore/CMakeLists.txt b/aplcore/CMakeLists.txt index 3e706bd..d83005c 100644 --- a/aplcore/CMakeLists.txt +++ b/aplcore/CMakeLists.txt @@ -57,13 +57,12 @@ if (TARGET yoga) add_dependencies(apl yoga) endif() -if (NOT HAS_FETCH_CONTENT) - add_dependencies(apl pegtl-build) +if (NOT USE_PROVIDED_YOGA_INLINE) + target_link_libraries(apl PUBLIC $) endif() -# When not using the system rapidjson build the library, add a dependency on the build step -if (NOT USE_SYSTEM_RAPIDJSON AND NOT HAS_FETCH_CONTENT) - add_dependencies(apl rapidjson-build) +if (NOT HAS_FETCH_CONTENT) + add_dependencies(apl pegtl-build) endif() add_subdirectory(src/action) @@ -75,6 +74,7 @@ add_subdirectory(src/content) add_subdirectory(src/datagrammar) add_subdirectory(src/datasource) add_subdirectory(src/document) +add_subdirectory(src/embed) add_subdirectory(src/engine) add_subdirectory(src/extension) add_subdirectory(src/focus) @@ -100,7 +100,9 @@ set_target_properties(apl PROPERTIES VERSION "${PROJECT_VERSION}" SOVERSION 1 - PUBLIC_HEADER "${PUBLIC_HEADER_LIST}") + PUBLIC_HEADER "${PUBLIC_HEADER_LIST}" + EXPORT_NAME "core" +) if (ENABLE_PIC) set_target_properties(apl @@ -116,19 +118,16 @@ target_include_directories(apl PUBLIC $ $ - $ $ PRIVATE $ $ ) +target_link_libraries(apl PUBLIC rapidjson-apl) + if (USE_PROVIDED_YOGA_INLINE) target_sources(apl PRIVATE ${YOGA_SRC}) -else() - target_link_libraries(apl - PRIVATE - libyoga) endif() # include the alexa extensions library @@ -159,6 +158,12 @@ target_link_libraries(apl PRIVATE ${log-lib}) endif(ANDROID) +# Test cases are built conditionally. Only affect core do not build them for everything else. +if (BUILD_UNIT_TESTS) + include_directories(${GTEST_INCLUDE}) + add_subdirectory(unit) +endif (BUILD_UNIT_TESTS) + install(TARGETS apl EXPORT apl-targets ARCHIVE DESTINATION lib @@ -173,12 +178,6 @@ install(DIRECTORY ${PROJECT_SOURCE_DIR}/aplcore/include/apl install(FILES ${CMAKE_CURRENT_BINARY_DIR}/apl.pc DESTINATION lib/pkgconfig) -if (NOT USE_SYSTEM_RAPIDJSON) -install(DIRECTORY ${RAPIDJSON_INCLUDE}/rapidjson - DESTINATION include - FILES_MATCHING PATTERN "*.h") -endif() - if (USE_PROVIDED_YOGA_AS_LIB) # We built the bundled yoga lib, install it install(FILES ${YOGA_LIB} @@ -189,16 +188,6 @@ if (USE_PROVIDED_YOGA_AS_LIB) set(YOGA_EXTERNAL_LIB ${YOGA_LIB}) # used by aplcoreConfig.cmake.in endif() -if (NOT USE_PROVIDED_YOGA_INLINE) -set_target_properties(apl PROPERTIES - EXPORT_NAME - core - INTERFACE_LINK_LIBRARIES - # Only set this for builds, the find module will handle the other cases - $ -) -endif() - export( EXPORT apl-targets @@ -227,4 +216,4 @@ install( ${CMAKE_CURRENT_BINARY_DIR}/aplcoreConfig.cmake DESTINATION lib/cmake/aplcore -) +) \ No newline at end of file diff --git a/aplcore/aplcoreConfig.cmake.in b/aplcore/aplcoreConfig.cmake.in index db2d4e6..92d8a1e 100644 --- a/aplcore/aplcoreConfig.cmake.in +++ b/aplcore/aplcoreConfig.cmake.in @@ -1,30 +1,51 @@ @PACKAGE_INIT@ +set(USE_PROVIDED_YOGA_INLINE @USE_PROVIDED_YOGA_INLINE@) set(YOGA_EXTERNAL_LIB @YOGA_EXTERNAL_LIB@) - -if(YOGA_EXTERNAL_LIB) - set_and_check(aplcore_yoga_LIBRARY "${YOGA_EXTERNAL_LIB}") -else() - # This file gets installed at ${APL_CORE_INSTALL_DIR}/lib/cmake/aplcore/aplcoreConfig.cmake, so go up 3 directories - # to find the root - get_filename_component(APL_CORE_INSTALL_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE) - - set_and_check(aplcore_yoga_LIBRARY "${APL_CORE_INSTALL_DIR}/lib/@YOGA_LIB_NAME@") - -endif() +set(USE_SYSTEM_RAPIDJSON @USE_SYSTEM_RAPIDJSON@) set(ENABLE_ALEXAEXTENSIONS @ENABLE_ALEXAEXTENSIONS@) -set(USE_INTERNAL_ALEXAEXT @BUILD_ALEXAEXTENSIONS@) if(ENABLE_ALEXAEXTENSIONS) - if(NOT USE_INTERNAL_ALEXAEXT) - find_package(alexaext REQUIRED) - endif() + find_package(alexaext REQUIRED) endif(ENABLE_ALEXAEXTENSIONS) +# For backwards-compatibility with the old build logic, try to locate RapidJSON on the system if the +# new CMake package is not found +if (NOT TARGET rapidjson-apl) + if (USE_SYSTEM_RAPIDJSON) + find_package(aplrapidjson QUIET) + if (NOT aplrapidjson_FOUND) + # Try to locate RapidJSON on the system + find_package(RapidJSON QUIET) + + if (NOT RapidJSON_FOUND) + # Try to find the headers directly on the system + find_path(RAPIDJSON_INCLUDE_DIRS + NAMES rapidjson/document.h + REQUIRED) + endif() + + add_library(rapidjson-apl INTERFACE IMPORTED) + target_include_directories(rapidjson-apl INTERFACE ${RAPIDJSON_INCLUDE_DIRS}) + endif() + else() + find_package(aplrapidjson REQUIRED) + endif() +endif() + include("${CMAKE_CURRENT_LIST_DIR}/aplcoreTargets.cmake") -set_target_properties(apl::core - PROPERTIES - INTERFACE_LINK_LIBRARIES "${aplcore_yoga_LIBRARY}" -) \ No newline at end of file +if (NOT USE_PROVIDED_YOGA_INLINE) + # Yoga is not built into core, so add the dependency here + if(YOGA_EXTERNAL_LIB) + set_and_check(aplcore_yoga_LIBRARY "${YOGA_EXTERNAL_LIB}") + else() + # This file gets installed at ${APL_CORE_INSTALL_DIR}/lib/cmake/aplcore/aplcoreConfig.cmake, so go up 3 directories + # to find the root + get_filename_component(APL_CORE_INSTALL_DIR "${CMAKE_CURRENT_LIST_DIR}/../../../" ABSOLUTE) + + set_and_check(aplcore_yoga_LIBRARY "${APL_CORE_INSTALL_DIR}/lib/@YOGA_LIB_NAME@") + endif() + target_link_libraries(apl::core INTERFACE "${aplcore_yoga_LIBRARY}") +endif() diff --git a/aplcore/buildTimeConstants.cpp.in b/aplcore/buildTimeConstants.cpp.in index 994bd74..4f5f9f7 100644 --- a/aplcore/buildTimeConstants.cpp.in +++ b/aplcore/buildTimeConstants.cpp.in @@ -17,4 +17,6 @@ namespace apl { const char *sCoreRepositoryVersion = "@CORE_REPOSITORY_VERSION@"; +int kEvaluationDepthLimit = @EVALUATION_DEPTH_LIMIT@; + } // namespace apl diff --git a/aplcore/include/apl/action/action.h b/aplcore/include/apl/action/action.h index 56707bb..fafcc26 100644 --- a/aplcore/include/apl/action/action.h +++ b/aplcore/include/apl/action/action.h @@ -16,18 +16,13 @@ #ifndef _APL_ACTION_H #define _APL_ACTION_H -#include -#include -#include -#include - #include "apl/common.h" +#include "apl/primitives/rect.h" #include "apl/time/timers.h" #include "apl/utils/counter.h" #include "apl/utils/noncopyable.h" #include "apl/utils/streamer.h" #include "apl/utils/userdata.h" -#include "apl/primitives/rect.h" namespace apl { @@ -200,10 +195,10 @@ class Action : public std::enable_shared_from_this, /** * Revive an action in the new context. - * @param context new RootContext. + * @param context new DocumentContext. * @return true if successful, false otherwise. */ - virtual bool rehydrate(const RootContext& context); + virtual bool rehydrate(const CoreDocumentContext& context); protected: virtual void onFinish() {} diff --git a/aplcore/include/apl/action/animatedscrollaction.h b/aplcore/include/apl/action/animatedscrollaction.h index 33a555a..bf0e8c4 100644 --- a/aplcore/include/apl/action/animatedscrollaction.h +++ b/aplcore/include/apl/action/animatedscrollaction.h @@ -30,7 +30,7 @@ class AnimatedScrollAction : public ResourceHoldingAction { void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; protected: AnimatedScrollAction(const TimersPtr& timers, diff --git a/aplcore/include/apl/action/animateitemaction.h b/aplcore/include/apl/action/animateitemaction.h index 225fc32..f1abd25 100644 --- a/aplcore/include/apl/action/animateitemaction.h +++ b/aplcore/include/apl/action/animateitemaction.h @@ -38,7 +38,7 @@ class AnimateItemAction : public ResourceHoldingAction { bool fastMode); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void start(); diff --git a/aplcore/include/apl/action/arrayaction.h b/aplcore/include/apl/action/arrayaction.h index 22360e8..a61c7f4 100644 --- a/aplcore/include/apl/action/arrayaction.h +++ b/aplcore/include/apl/action/arrayaction.h @@ -25,14 +25,14 @@ namespace apl { class ArrayAction : public Action { public: static std::shared_ptr make(const TimersPtr& timers, - std::shared_ptr command, + std::shared_ptr&& command, bool fastMode) { - auto ptr = std::make_shared(timers, command, fastMode); + auto ptr = std::make_shared(timers, std::move(command), fastMode); ptr->advance(); return ptr; } - ArrayAction(const TimersPtr& timers, std::shared_ptr command, bool fastMode); + ArrayAction(const TimersPtr& timers, std::shared_ptr&& command, bool fastMode); private: void advance(); diff --git a/aplcore/include/apl/action/autopageaction.h b/aplcore/include/apl/action/autopageaction.h index 09f071d..9abbeee 100644 --- a/aplcore/include/apl/action/autopageaction.h +++ b/aplcore/include/apl/action/autopageaction.h @@ -16,8 +16,8 @@ #ifndef _APL_AUTO_PAGE_ACTION_H #define _APL_AUTO_PAGE_ACTION_H -#include "apl/action/resourceholdingaction.h" #include "apl/common.h" +#include "apl/action/resourceholdingaction.h" namespace apl { @@ -40,7 +40,7 @@ class AutoPageAction : public ResourceHoldingAction { apl_time_t duration); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void advance(); diff --git a/aplcore/include/apl/action/delayaction.h b/aplcore/include/apl/action/delayaction.h index 42547da..1894d9d 100644 --- a/aplcore/include/apl/action/delayaction.h +++ b/aplcore/include/apl/action/delayaction.h @@ -36,7 +36,7 @@ class DelayAction : public Action { DelayAction(const TimersPtr& timers, const CommandPtr& command, bool fastMode); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: /** diff --git a/aplcore/include/apl/action/playmediaaction.h b/aplcore/include/apl/action/playmediaaction.h index 242cf7b..bb8176b 100644 --- a/aplcore/include/apl/action/playmediaaction.h +++ b/aplcore/include/apl/action/playmediaaction.h @@ -40,7 +40,7 @@ class PlayMediaAction : public ResourceHoldingAction { const ComponentPtr& target); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void start(); diff --git a/aplcore/include/apl/action/resourceholdingaction.h b/aplcore/include/apl/action/resourceholdingaction.h index 8b82747..fed24dd 100644 --- a/aplcore/include/apl/action/resourceholdingaction.h +++ b/aplcore/include/apl/action/resourceholdingaction.h @@ -29,7 +29,7 @@ class ResourceHoldingAction : public Action { const ContextPtr& context); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; protected: void onFinish() override; diff --git a/aplcore/include/apl/action/scrollaction.h b/aplcore/include/apl/action/scrollaction.h index 5053a1a..c8d963b 100644 --- a/aplcore/include/apl/action/scrollaction.h +++ b/aplcore/include/apl/action/scrollaction.h @@ -58,7 +58,7 @@ class ScrollAction : public AnimatedScrollAction { apl_duration_t duration); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void start(); diff --git a/aplcore/include/apl/action/scrolltoaction.h b/aplcore/include/apl/action/scrolltoaction.h index e6d04ae..c6b0be7 100644 --- a/aplcore/include/apl/action/scrolltoaction.h +++ b/aplcore/include/apl/action/scrolltoaction.h @@ -108,7 +108,7 @@ class ScrollToAction : public AnimatedScrollAction { apl_duration_t duration); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void start(); diff --git a/aplcore/include/apl/action/sequentialaction.h b/aplcore/include/apl/action/sequentialaction.h index f61c711..2148947 100644 --- a/aplcore/include/apl/action/sequentialaction.h +++ b/aplcore/include/apl/action/sequentialaction.h @@ -32,11 +32,11 @@ class SequentialAction : public Action { bool fastMode); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void advance(); - bool doCommand(const Object& command); + bool doCommand(CommandData&& commandData); private: std::shared_ptr mCommand; diff --git a/aplcore/include/apl/action/setpageaction.h b/aplcore/include/apl/action/setpageaction.h index b5c5091..f5d9baf 100644 --- a/aplcore/include/apl/action/setpageaction.h +++ b/aplcore/include/apl/action/setpageaction.h @@ -36,7 +36,7 @@ class SetPageAction : public ResourceHoldingAction { const CoreComponentPtr& target); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void start(); diff --git a/aplcore/include/apl/action/speakitemaction.h b/aplcore/include/apl/action/speakitemaction.h index 4f33bad..9ff647c 100644 --- a/aplcore/include/apl/action/speakitemaction.h +++ b/aplcore/include/apl/action/speakitemaction.h @@ -53,7 +53,7 @@ class SpeakItemAction : public ResourceHoldingAction { const CoreComponentPtr& target); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void start(); diff --git a/aplcore/include/apl/action/speaklistaction.h b/aplcore/include/apl/action/speaklistaction.h index ed9d1eb..583dae0 100644 --- a/aplcore/include/apl/action/speaklistaction.h +++ b/aplcore/include/apl/action/speaklistaction.h @@ -37,7 +37,7 @@ class SpeakListAction : public Action { size_t startIndex, size_t endIndex); void freeze() override; - bool rehydrate(const RootContext& context) override; + bool rehydrate(const CoreDocumentContext& context) override; private: void advance(); diff --git a/aplcore/include/apl/animation/easing.h b/aplcore/include/apl/animation/easing.h index a6631e9..241e96d 100644 --- a/aplcore/include/apl/animation/easing.h +++ b/aplcore/include/apl/animation/easing.h @@ -72,10 +72,6 @@ class Easing : public ObjectData { public: bool isCallable() const override { return true; } - bool equals(const Object::DataHolder& lhs, const Object::DataHolder& rhs) const override { - return *lhs.data == *rhs.data; - } - Object call(const Object::DataHolder& dataHolder, const ObjectArray& args) const override { return dataHolder.data->call(args); } diff --git a/aplcore/include/apl/apl.h b/aplcore/include/apl/apl.h index 76204d3..2714d26 100644 --- a/aplcore/include/apl/apl.h +++ b/aplcore/include/apl/apl.h @@ -25,7 +25,7 @@ #include "rapidjson/document.h" #include "apl/buildTimeConstants.h" -#include "apl/common.h" +#include "apl/apl_config.h" #include "apl/action/action.h" #include "apl/audio/audioplayer.h" @@ -43,6 +43,9 @@ #include "apl/content/rootconfig.h" #include "apl/datasource/datasourceconnection.h" #include "apl/datasource/datasourceprovider.h" +#include "apl/document/documentcontext.h" +#include "apl/embed/documentmanager.h" +#include "apl/embed/embedrequest.h" #include "apl/engine/event.h" #include "apl/engine/rootcontext.h" #include "apl/extension/extensionclient.h" @@ -67,6 +70,7 @@ #include "apl/primitives/roundedrect.h" #include "apl/primitives/styledtext.h" #include "apl/primitives/transform2d.h" +#include "apl/primitives/urlrequest.h" #include "apl/scaling/metricstransform.h" #include "apl/touch/pointerevent.h" #include "apl/utils/localemethods.h" diff --git a/aplcore/include/apl/buildTimeConstants.h b/aplcore/include/apl/buildTimeConstants.h index abc569c..ffce1b9 100644 --- a/aplcore/include/apl/buildTimeConstants.h +++ b/aplcore/include/apl/buildTimeConstants.h @@ -18,8 +18,12 @@ namespace apl { +/// The string version of the current git HEAD extern const char *sCoreRepositoryVersion; +/// Compile-time constant for how many nested eval() statements will be evaluated +extern int kEvaluationDepthLimit; + } // namespace apl #endif // _APL_BUILD_TIME_CONSTANTS_H diff --git a/aplcore/include/apl/command/animateitemcommand.h b/aplcore/include/apl/command/animateitemcommand.h index 067d85e..a1b60c1 100644 --- a/aplcore/include/apl/command/animateitemcommand.h +++ b/aplcore/include/apl/command/animateitemcommand.h @@ -20,20 +20,9 @@ namespace apl { -class AnimateItemCommand : public CoreCommand { +class AnimateItemCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - AnimateItemCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - { - } + COMMAND_CONSTRUCTOR(AnimateItemCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/arraycommand.h b/aplcore/include/apl/command/arraycommand.h index 0b5e010..cc9e6e7 100644 --- a/aplcore/include/apl/command/arraycommand.h +++ b/aplcore/include/apl/command/arraycommand.h @@ -18,6 +18,7 @@ #include "apl/command/corecommand.h" #include "apl/engine/properties.h" +#include "apl/primitives/commanddata.h" namespace apl { @@ -27,9 +28,9 @@ namespace apl { class ArrayCommand : public CoreCommand { public: static CommandPtr create(const ContextPtr& context, - const Object& commands, + CommandData&& commands, const CoreComponentPtr& base, - const Properties& properties, + Properties&& properties, const std::string& parentSequencer = "", bool finishAllOnTerminate = false); @@ -42,7 +43,7 @@ class ArrayCommand : public CoreCommand { * @param properties Additional properties to apply to each command. */ ArrayCommand(const ContextPtr& context, - const Object& commands, + CommandData&& commands, const CoreComponentPtr& base, Properties&& properties, const std::string& parentSequencer, @@ -53,11 +54,9 @@ class ArrayCommand : public CoreCommand { std::string name() const override { return "ArrayCommand"; } ActionPtr execute(const TimersPtr& timers, bool fastMode) override; - const std::vector& commands() const { return mCommands.getArray(); } bool finishAllOnTerminate() const { return mFinishAllOnTerminate; } private: - const Object mCommands; bool mFinishAllOnTerminate; }; diff --git a/aplcore/include/apl/command/autopagecommand.h b/aplcore/include/apl/command/autopagecommand.h index 590ec01..846661a 100644 --- a/aplcore/include/apl/command/autopagecommand.h +++ b/aplcore/include/apl/command/autopagecommand.h @@ -20,20 +20,9 @@ namespace apl { -class AutoPageCommand : public CoreCommand { +class AutoPageCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - AutoPageCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(AutoPageCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/clearfocuscommand.h b/aplcore/include/apl/command/clearfocuscommand.h index 5e88925..f075e29 100644 --- a/aplcore/include/apl/command/clearfocuscommand.h +++ b/aplcore/include/apl/command/clearfocuscommand.h @@ -20,21 +20,9 @@ namespace apl { -class ClearFocusCommand : public CoreCommand { +class ClearFocusCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ClearFocusCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - { - } + COMMAND_CONSTRUCTOR(ClearFocusCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/command.h b/aplcore/include/apl/command/command.h index 7ded0a4..38aae64 100644 --- a/aplcore/include/apl/command/command.h +++ b/aplcore/include/apl/command/command.h @@ -16,10 +16,10 @@ #ifndef _APL_COMMAND_H #define _APL_COMMAND_H -#include "apl/utils/counter.h" -#include "apl/utils/noncopyable.h" #include "apl/action/action.h" #include "apl/command/commandproperties.h" +#include "apl/utils/counter.h" +#include "apl/utils/noncopyable.h" namespace apl { @@ -92,7 +92,7 @@ class Command : public std::enable_shared_from_this, * @param context new RootContext. * @return true if successful, false otherwise. */ - virtual bool rehydrate(const RootContext& context) { return false; } + virtual bool rehydrate(const CoreDocumentContext& context) { return false; } }; } // namespace apl diff --git a/aplcore/include/apl/command/commandfactory.h b/aplcore/include/apl/command/commandfactory.h index e6370c0..5bba881 100644 --- a/aplcore/include/apl/command/commandfactory.h +++ b/aplcore/include/apl/command/commandfactory.h @@ -23,10 +23,12 @@ #include "apl/engine/context.h" #include "apl/component/corecomponent.h" #include "apl/primitives/object.h" +#include "apl/primitives/commanddata.h" namespace apl { -using CommandFunc = std::function; +using CommandFunc = std::function; class CoreCommand; @@ -35,47 +37,40 @@ class CoreCommand; * * This class is used by ArrayAction and SequentialAction and ultimately invoked by the sequencer. */ -class CommandFactory { +class CommandFactory : public NonCopyable { public: static CommandFactory& instance(); - ActionPtr execute(const TimersPtr& timers, - const ContextPtr& context, - const Object& command, - const CoreComponentPtr& base, - bool fastMode); - void reset(); CommandFactory& set(const char *name, CommandFunc func); CommandFunc get(const char *name) const; - CommandPtr inflate(const ContextPtr& context, const Object& command, const Properties& properties, - const CoreComponentPtr& base, const std::string& parentSequencer = ""); - - CommandPtr inflate(const ContextPtr& context, const Object& command, - const CoreComponentPtr& base); + CommandPtr inflate(const ContextPtr& context, CommandData&& commandData, const CoreComponentPtr& base); - CommandPtr inflate(const Object& command, const std::shared_ptr& parent); + CommandPtr inflate(CommandData&& commandData, const std::shared_ptr& parent); protected: - CommandFactory() {}; + CommandFactory() = default;; private: - CommandFactory(const CommandFactory&) = delete; - CommandFactory& operator=(const CommandFactory&) = delete; - CommandPtr expandMacro(const ContextPtr& context, - Properties& properties, - const rapidjson::Value& definition, - const CoreComponentPtr& base, - const std::string& parentSequencer); + CommandData&& commandData, + Properties&& properties, + const rapidjson::Value& definition, + const CoreComponentPtr& base, + const std::string& parentSequencer); + CommandPtr inflate(const ContextPtr& context, + CommandData&& commandData, + Properties&& properties, + const CoreComponentPtr& base, + const std::string& parentSequencer = ""); +private: static CommandFactory *sInstance; std::map mCommandMap; - }; diff --git a/aplcore/include/apl/command/commandproperties.h b/aplcore/include/apl/command/commandproperties.h index 2ba3917..2a8698c 100644 --- a/aplcore/include/apl/command/commandproperties.h +++ b/aplcore/include/apl/command/commandproperties.h @@ -46,6 +46,8 @@ enum CommandType { kCommandTypeFinish, kCommandTypeReinflate, kCommandTypeCustomEvent, + kCommandTypeInsertItem, + kCommandTypeRemoveItem, }; enum CommandScrollAlign { @@ -78,6 +80,7 @@ enum CommandControlMedia { kCommandControlMediaPrevious, kCommandControlMediaRewind, kCommandControlMediaSeek, + kCommandControlMediaSeekTo, kCommandControlMediaSetTrack }; @@ -94,6 +97,7 @@ enum CommandReason { enum CommandPropertyKey { kCommandPropertyAlign, kCommandPropertyArguments, + kCommandPropertyAt, kCommandPropertyAudioTrack, kCommandPropertyCatch, kCommandPropertyCommand, @@ -111,6 +115,7 @@ enum CommandPropertyKey { kCommandPropertyFlags, kCommandPropertyHighlightMode, kCommandPropertyIndex, + kCommandPropertyItem, kCommandPropertyMinimumDwellTime, kCommandPropertyOnFail, kCommandPropertyOtherwise, diff --git a/aplcore/include/apl/command/configchangecommand.h b/aplcore/include/apl/command/configchangecommand.h index a716575..573ad27 100644 --- a/aplcore/include/apl/command/configchangecommand.h +++ b/aplcore/include/apl/command/configchangecommand.h @@ -30,14 +30,14 @@ class ConfigChangeCommand : public Command { /// Sequencer reserved for executing the onConfigChange command static const char *SEQUENCER; - static CommandPtr create(const RootContextPtr& rootContext, + static CommandPtr create(const CoreDocumentContextPtr& documentContext, ObjectMap&& properties) { - return std::make_shared(rootContext, std::move(properties)); + return std::make_shared(documentContext, std::move(properties)); } - ConfigChangeCommand(const RootContextPtr& rootContext, + ConfigChangeCommand(const CoreDocumentContextPtr& documentContext, ObjectMap&& properties) - : mRootContext(rootContext), + : mDocumentContext(documentContext), mProperties(std::move(properties)) { } @@ -47,7 +47,7 @@ class ConfigChangeCommand : public Command { ActionPtr execute(const TimersPtr& timers, bool fastMode) override; private: - const std::weak_ptr mRootContext; + const std::weak_ptr mDocumentContext; ObjectMap mProperties; }; diff --git a/aplcore/include/apl/command/controlmediacommand.h b/aplcore/include/apl/command/controlmediacommand.h index 96b7acb..77811b9 100644 --- a/aplcore/include/apl/command/controlmediacommand.h +++ b/aplcore/include/apl/command/controlmediacommand.h @@ -20,20 +20,9 @@ namespace apl { -class ControlMediaCommand : public CoreCommand { +class ControlMediaCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ControlMediaCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(ControlMediaCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/corecommand.h b/aplcore/include/apl/command/corecommand.h index 56bce08..ff43871 100644 --- a/aplcore/include/apl/command/corecommand.h +++ b/aplcore/include/apl/command/corecommand.h @@ -18,9 +18,11 @@ #include "apl/command/command.h" #include "apl/component/corecomponent.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/context.h" #include "apl/engine/event.h" #include "apl/engine/propdef.h" +#include "apl/primitives/commanddata.h" #include "apl/primitives/objectbag.h" #include "apl/utils/bimap.h" @@ -28,7 +30,8 @@ namespace apl { extern Bimap sCommandNameBimap; -using CommandCreateFunc = std::function; +using CommandCreateFunc = std::function; extern std::map sCommandCreatorMap; class CoreCommand; @@ -73,6 +76,7 @@ class CoreCommand : public Command { public: CoreCommand(const ContextPtr& context, + CommandData&& commandData, Properties&& properties, const CoreComponentPtr& base, const std::string& parentSequencer); @@ -94,7 +98,9 @@ class CoreCommand : public Command { virtual const CommandPropDefSet& propDefSet() const; void freeze() final; - bool rehydrate(const RootContext& context) final; + bool rehydrate(const CoreDocumentContext& context) final; + + const CommandData& data() const { return mCommandData; } protected: bool validate(); @@ -102,6 +108,8 @@ class CoreCommand : public Command { protected: ContextPtr mContext; + // Data that this command was created from and requires to operate properly. + CommandData mCommandData; Properties mProperties; CoreComponentPtr mBase; CommandBag mValues; @@ -118,6 +126,48 @@ class CoreCommand : public Command { bool mMissingTargetId = false; }; +/** + * Template for command definition in order to avoid copying creation and constructor primitives. + * @tparam Name Command name. + */ +template +class TemplatedCommand : public CoreCommand { +public: + static CommandPtr create(const ContextPtr& context, + CommandData&& commandData, + Properties&& properties, + const CoreComponentPtr& base, + const std::string& parentSequencer) { + auto ptr = std::make_shared(context, std::move(commandData), std::move(properties), base, parentSequencer); + return ptr->validate() ? ptr : nullptr; + } + + TemplatedCommand( + const ContextPtr& context, + CommandData&& commandData, + Properties&& properties, + const CoreComponentPtr& base, + const std::string& parentSequencer) + : CoreCommand(context, std::move(commandData), std::move(properties), base, parentSequencer) + {} +}; + +/// Constructor helper to use in conjunction with template above (templates can't properly define +/// constructors in C++11). +#define COMMAND_CONSTRUCTOR(NAME) \ + NAME( \ + const ContextPtr& context, \ + CommandData&& commandData, \ + Properties&& properties, \ + const CoreComponentPtr& base, \ + const std::string& parentSequencer) \ + : TemplatedCommand( \ + context, \ + std::move(commandData), \ + std::move(properties), \ + base, \ + parentSequencer) \ + {} } // namespace apl #endif //_APL_COMMAND_CORE_COMMAND_H diff --git a/aplcore/include/apl/command/displaystatechangecommand.h b/aplcore/include/apl/command/displaystatechangecommand.h index 875a588..9aa6148 100644 --- a/aplcore/include/apl/command/displaystatechangecommand.h +++ b/aplcore/include/apl/command/displaystatechangecommand.h @@ -30,24 +30,24 @@ class DisplayStateChangeCommand : public Command { // Sequencer reserved for executing the onDisplayStateChange command static const char *SEQUENCER; - static CommandPtr create(const RootContextPtr& rootContext, + static CommandPtr create(const CoreDocumentContextPtr& document, ObjectMap&& properties) { - return std::make_shared(rootContext, std::move(properties)); + return std::make_shared(document, std::move(properties)); } - DisplayStateChangeCommand(const RootContextPtr& rootContext, - ObjectMap&& properties) - : mRootContext(rootContext), + DisplayStateChangeCommand( + const CoreDocumentContextPtr& document, + ObjectMap&& properties) + : mDocument(document), mProperties(std::move(properties)) - { - } + {} unsigned long delay() const override { return 0; } std::string name() const override { return "DisplayStateChangeCommand"; } ActionPtr execute(const TimersPtr& timers, bool fastMode) override; private: - const std::weak_ptr mRootContext; + const std::weak_ptr mDocument; ObjectMap mProperties; }; diff --git a/aplcore/include/apl/command/documentcommand.h b/aplcore/include/apl/command/documentcommand.h index a506ccc..9760c91 100644 --- a/aplcore/include/apl/command/documentcommand.h +++ b/aplcore/include/apl/command/documentcommand.h @@ -22,7 +22,6 @@ namespace apl { class Content; -class RootContext; /** * Run a large number of commands in parallel with a "finally" clause. @@ -31,16 +30,16 @@ class DocumentCommand : public Command { public: static CommandPtr create(PropertyKey propertyKey, const std::string& handler, - const RootContextPtr& rootContext) { - return std::make_shared(propertyKey, handler, rootContext); + const CoreDocumentContextPtr& documentContext) { + return std::make_shared(propertyKey, handler, documentContext); } DocumentCommand(PropertyKey propertyKey, const std::string& handler, - const RootContextPtr& rootContext) + const CoreDocumentContextPtr& documentContext) : mPropertyKey(propertyKey), mHandler(handler), - mRootContext(rootContext) + mDocumentContext(documentContext) { } @@ -59,7 +58,7 @@ class DocumentCommand : public Command { private: PropertyKey mPropertyKey; // Which property we will extract std::string mHandler; - const std::weak_ptr mRootContext; + const std::weak_ptr mDocumentContext; }; using TransitionCommandPtr = std::shared_ptr; diff --git a/aplcore/include/apl/command/extensioneventcommand.h b/aplcore/include/apl/command/extensioneventcommand.h index 6940284..257b59a 100644 --- a/aplcore/include/apl/command/extensioneventcommand.h +++ b/aplcore/include/apl/command/extensioneventcommand.h @@ -31,18 +31,21 @@ class ExtensionEventCommand : public CoreCommand { public: static CommandPtr create(const ExtensionCommandDefinition& def, const ContextPtr& context, + CommandData&& commandData, Properties&& properties, const CoreComponentPtr& base, const std::string& parentSequencer) { - return std::make_shared(def, context, std::move(properties), base, parentSequencer); + return std::make_shared(def, context, std::move(commandData), + std::move(properties), base, parentSequencer); } ExtensionEventCommand(const ExtensionCommandDefinition& def, const ContextPtr& context, + CommandData&& commandData, Properties&& properties, const CoreComponentPtr& base, const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer), + : CoreCommand(context, std::move(commandData), std::move(properties), base, parentSequencer), mDefinition(def) {} const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/finishcommand.h b/aplcore/include/apl/command/finishcommand.h index 9c9473e..9a6a349 100644 --- a/aplcore/include/apl/command/finishcommand.h +++ b/aplcore/include/apl/command/finishcommand.h @@ -32,20 +32,9 @@ namespace apl { * Executing the finish command stops all other processing in APL, including any commands that are still executing. * The finish command executes in both normal and fast mode. */ -class FinishCommand : public CoreCommand { +class FinishCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - FinishCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(FinishCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/idlecommand.h b/aplcore/include/apl/command/idlecommand.h index a6d9463..d5db3d0 100644 --- a/aplcore/include/apl/command/idlecommand.h +++ b/aplcore/include/apl/command/idlecommand.h @@ -20,20 +20,9 @@ namespace apl { -class IdleCommand : public CoreCommand { +class IdleCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - IdleCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(IdleCommand); CommandType type() const override { return kCommandTypeIdle; } diff --git a/aplcore/include/apl/command/insertitemcommand.h b/aplcore/include/apl/command/insertitemcommand.h new file mode 100644 index 0000000..748ea18 --- /dev/null +++ b/aplcore/include/apl/command/insertitemcommand.h @@ -0,0 +1,36 @@ +/** + * Copyright 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 _APL_INSERT_ITEM_COMMAND_H +#define _APL_INSERT_ITEM_COMMAND_H + +#include "apl/command/corecommand.h" + +namespace apl { + +class InsertItemCommand : public TemplatedCommand { +public: + COMMAND_CONSTRUCTOR(InsertItemCommand); + + const CommandPropDefSet& propDefSet() const override; + + CommandType type() const override { return kCommandTypeInsertItem; } + + ActionPtr execute(const TimersPtr& timers, bool fastMode) override; +}; + +} // namespace apl + +#endif // _APL_INSERT_ITEM_H diff --git a/aplcore/include/apl/command/openurlcommand.h b/aplcore/include/apl/command/openurlcommand.h index 449975d..eb8577f 100644 --- a/aplcore/include/apl/command/openurlcommand.h +++ b/aplcore/include/apl/command/openurlcommand.h @@ -20,20 +20,9 @@ namespace apl { -class OpenURLCommand : public CoreCommand { +class OpenURLCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - OpenURLCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(OpenURLCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/parallelcommand.h b/aplcore/include/apl/command/parallelcommand.h index bf3d593..dad3434 100644 --- a/aplcore/include/apl/command/parallelcommand.h +++ b/aplcore/include/apl/command/parallelcommand.h @@ -20,20 +20,9 @@ namespace apl { -class ParallelCommand : public CoreCommand { +class ParallelCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ParallelCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(ParallelCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/playmediacommand.h b/aplcore/include/apl/command/playmediacommand.h index 17f26c3..360336d 100644 --- a/aplcore/include/apl/command/playmediacommand.h +++ b/aplcore/include/apl/command/playmediacommand.h @@ -20,20 +20,9 @@ namespace apl { -class PlayMediaCommand : public CoreCommand { +class PlayMediaCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - PlayMediaCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(PlayMediaCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/reinflatecommand.h b/aplcore/include/apl/command/reinflatecommand.h index 8f4385a..d7eae82 100644 --- a/aplcore/include/apl/command/reinflatecommand.h +++ b/aplcore/include/apl/command/reinflatecommand.h @@ -20,20 +20,9 @@ namespace apl { -class ReinflateCommand : public CoreCommand { +class ReinflateCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ReinflateCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(ReinflateCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/removeitemcommand.h b/aplcore/include/apl/command/removeitemcommand.h new file mode 100644 index 0000000..a34f626 --- /dev/null +++ b/aplcore/include/apl/command/removeitemcommand.h @@ -0,0 +1,36 @@ +/** + * Copyright 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 _APL_REMOVE_ITEM_COMMAND_H +#define _APL_REMOVE_ITEM_COMMAND_H + +#include "apl/command/corecommand.h" + +namespace apl { + +class RemoveItemCommand : public TemplatedCommand { +public: + COMMAND_CONSTRUCTOR(RemoveItemCommand); + + const CommandPropDefSet& propDefSet() const override; + + CommandType type() const override { return kCommandTypeRemoveItem; } + + ActionPtr execute(const TimersPtr& timers, bool fastMode) override; +}; + +} // namespace apl + +#endif // _APL_REMOVE_ITEM_COMMAND_H diff --git a/aplcore/include/apl/command/scrollcommand.h b/aplcore/include/apl/command/scrollcommand.h index 4206808..d24d169 100644 --- a/aplcore/include/apl/command/scrollcommand.h +++ b/aplcore/include/apl/command/scrollcommand.h @@ -20,20 +20,9 @@ namespace apl { -class ScrollCommand : public CoreCommand { +class ScrollCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ScrollCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(ScrollCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/scrolltocomponentcommand.h b/aplcore/include/apl/command/scrolltocomponentcommand.h index f38f367..55fc780 100644 --- a/aplcore/include/apl/command/scrolltocomponentcommand.h +++ b/aplcore/include/apl/command/scrolltocomponentcommand.h @@ -20,20 +20,9 @@ namespace apl { -class ScrollToComponentCommand : public CoreCommand { +class ScrollToComponentCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ScrollToComponentCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(ScrollToComponentCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/scrolltoindexcommand.h b/aplcore/include/apl/command/scrolltoindexcommand.h index b96106e..75b0995 100644 --- a/aplcore/include/apl/command/scrolltoindexcommand.h +++ b/aplcore/include/apl/command/scrolltoindexcommand.h @@ -21,20 +21,9 @@ namespace apl { -class ScrollToIndexCommand : public CoreCommand { +class ScrollToIndexCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - ScrollToIndexCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(ScrollToIndexCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/selectcommand.h b/aplcore/include/apl/command/selectcommand.h index be41913..0218115 100644 --- a/aplcore/include/apl/command/selectcommand.h +++ b/aplcore/include/apl/command/selectcommand.h @@ -20,20 +20,9 @@ namespace apl { -class SelectCommand : public CoreCommand { +class SelectCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SelectCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SelectCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/sendeventcommand.h b/aplcore/include/apl/command/sendeventcommand.h index b2ab10f..573bd48 100644 --- a/aplcore/include/apl/command/sendeventcommand.h +++ b/aplcore/include/apl/command/sendeventcommand.h @@ -20,15 +20,9 @@ namespace apl { -class SendEventCommand : public CoreCommand { +class SendEventCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer); - - SendEventCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer); + COMMAND_CONSTRUCTOR(SendEventCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/sequentialcommand.h b/aplcore/include/apl/command/sequentialcommand.h index 194a2ad..1f070c3 100644 --- a/aplcore/include/apl/command/sequentialcommand.h +++ b/aplcore/include/apl/command/sequentialcommand.h @@ -21,20 +21,9 @@ namespace apl { -class SequentialCommand : public CoreCommand { +class SequentialCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SequentialCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SequentialCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/setfocuscommand.h b/aplcore/include/apl/command/setfocuscommand.h index 71fcfb6..50a85bf 100644 --- a/aplcore/include/apl/command/setfocuscommand.h +++ b/aplcore/include/apl/command/setfocuscommand.h @@ -20,21 +20,9 @@ namespace apl { -class SetFocusCommand : public CoreCommand { +class SetFocusCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SetFocusCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - { - } + COMMAND_CONSTRUCTOR(SetFocusCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/setpagecommand.h b/aplcore/include/apl/command/setpagecommand.h index c34f1b7..8c8f2e8 100644 --- a/aplcore/include/apl/command/setpagecommand.h +++ b/aplcore/include/apl/command/setpagecommand.h @@ -20,20 +20,9 @@ namespace apl { -class SetPageCommand : public CoreCommand { +class SetPageCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SetPageCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SetPageCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/setstatecommand.h b/aplcore/include/apl/command/setstatecommand.h index 6556f00..094504c 100644 --- a/aplcore/include/apl/command/setstatecommand.h +++ b/aplcore/include/apl/command/setstatecommand.h @@ -20,20 +20,9 @@ namespace apl { -class SetStateCommand : public CoreCommand { +class SetStateCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SetStateCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SetStateCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/setvaluecommand.h b/aplcore/include/apl/command/setvaluecommand.h index c6653e2..fa9f295 100644 --- a/aplcore/include/apl/command/setvaluecommand.h +++ b/aplcore/include/apl/command/setvaluecommand.h @@ -20,21 +20,9 @@ namespace apl { -class SetValueCommand : public CoreCommand { - +class SetValueCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SetValueCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SetValueCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/speakitemcommand.h b/aplcore/include/apl/command/speakitemcommand.h index d0010e2..805d60d 100644 --- a/aplcore/include/apl/command/speakitemcommand.h +++ b/aplcore/include/apl/command/speakitemcommand.h @@ -20,20 +20,9 @@ namespace apl { -class SpeakItemCommand : public CoreCommand { +class SpeakItemCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SpeakItemCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SpeakItemCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/command/speaklistcommand.h b/aplcore/include/apl/command/speaklistcommand.h index dba02cc..eea6e95 100644 --- a/aplcore/include/apl/command/speaklistcommand.h +++ b/aplcore/include/apl/command/speaklistcommand.h @@ -20,20 +20,9 @@ namespace apl { -class SpeakListCommand : public CoreCommand { +class SpeakListCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - SpeakListCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(SpeakListCommand); const CommandPropDefSet& propDefSet() const override; diff --git a/aplcore/include/apl/common.h b/aplcore/include/apl/common.h index 7c51621..02bc95a 100644 --- a/aplcore/include/apl/common.h +++ b/aplcore/include/apl/common.h @@ -25,7 +25,7 @@ namespace apl { /** - * Standard type for unique IDs in components + * Standard type for unique IDs in components and dependents */ using id_type = unsigned int; @@ -55,11 +55,20 @@ class Command; class Component; class Content; class Context; +class ContextData; class CoreComponent; +class CoreDocumentContext; +class CoreRootContext; class DataSource; class DataSourceProvider; +class Dependant; +class DependantManager; +class DocumentConfig; +class DocumentContext; +class DocumentContextData; +class DocumentManager; class Easing; -class EventManager; +class EmbedRequest; class ExtensionClient; class ExtensionCommandDefinition; class ExtensionComponent; @@ -80,6 +89,7 @@ class RootConfig; class RootContext; class Session; class Settings; +class SharedContextData; class StyleDefinition; class StyleInstance; class TextMeasurement; @@ -95,10 +105,20 @@ using ConstCommandPtr = std::shared_ptr; using ConstContextPtr = std::shared_ptr; using ContentPtr = std::shared_ptr; using ContextPtr = std::shared_ptr; +using ContextDataPtr = std::shared_ptr; using CoreComponentPtr = std::shared_ptr; +using CoreDocumentContextPtr = std::shared_ptr; +using CoreRootContextPtr = std::shared_ptr; using DataSourceProviderPtr = std::shared_ptr; using DataSourcePtr = std::shared_ptr; +using DependantPtr = std::shared_ptr; +using DocumentConfigPtr = std::shared_ptr; +using DocumentContextDataPtr = std::shared_ptr; +using DocumentContextPtr = std::shared_ptr; +using DocumentContextWeakPtr = std::weak_ptr; +using DocumentManagerPtr = std::shared_ptr; using EasingPtr = std::shared_ptr; +using EmbedRequestPtr = std::shared_ptr; using ExtensionClientPtr = std::shared_ptr; using ExtensionCommandDefinitionPtr = std::shared_ptr; using ExtensionComponentPtr = std::shared_ptr; @@ -119,6 +139,7 @@ using RootConfigPtr = std::shared_ptr; using RootContextPtr = std::shared_ptr; using SessionPtr = std::shared_ptr; using SettingsPtr = std::shared_ptr; +using SharedContextDataPtr = std::shared_ptr; using StyleDefinitionPtr = std::shared_ptr; using StyleInstancePtr = std::shared_ptr; using TextMeasurementPtr = std::shared_ptr; diff --git a/aplcore/include/apl/component/component.h b/aplcore/include/apl/component/component.h index be32689..8af5a3f 100644 --- a/aplcore/include/apl/component/component.h +++ b/aplcore/include/apl/component/component.h @@ -29,7 +29,6 @@ #include "apl/primitives/rect.h" #include "apl/utils/counter.h" #include "apl/utils/deprecated.h" -#include "apl/utils/noncopyable.h" #include "apl/utils/userdata.h" #include "apl/utils/visitor.h" @@ -71,16 +70,19 @@ enum UpdateType { * Update the current scroll position. The argument is the updated scroll position in dp. * Scroll positions are non-negative. */ + ///@deprecated kUpdateScrollPosition, /** * A pager has been moved by the user. The argument is the new page number (0-based index). */ + ///@deprecated kUpdatePagerPosition, /** * A pager has been moved in response to a SetPage event. The argument is the new page number (0-based index). */ + ///@deprecated kUpdatePagerByEvent, /** @@ -138,7 +140,6 @@ enum PageDirection { class Component : public UIDObject, public Counter, public UserData, - public NonCopyable, public std::enable_shared_from_this { public: @@ -466,8 +467,9 @@ class Component : public UIDObject, * Find a component at or below this point in the hierarchy with the given id or uniqueId. * @param id The id or uniqueId to search for. * @return The component or nullptr if it is not found. + * @deprecated Should not be used. Refer to RootContext::findComponentById */ - virtual ComponentPtr findComponentById(const std::string& id) const = 0; + APL_DEPRECATED virtual ComponentPtr findComponentById(const std::string& id) const = 0; /** * Find a visible component at or below this point in the hierarchy containing the given position. diff --git a/aplcore/include/apl/component/componentproperties.h b/aplcore/include/apl/component/componentproperties.h index c0f6853..8752016 100644 --- a/aplcore/include/apl/component/componentproperties.h +++ b/aplcore/include/apl/component/componentproperties.h @@ -429,10 +429,14 @@ enum PropertyKey { kPropertyDisplay, /// FrameComponent | EditTextComponent drawn border width (output only) kPropertyDrawnBorderWidth, + /// Embedded document state + kPropertyEmbeddedDocument, /// ContainerComponent child absolute right position for LTR layout or left position for RTL layout kPropertyEnd, /// Component array of opaque entity data kPropertyEntities, + /// HostComponent Environment overrides applicable to the embedded document + kPropertyEnvironment, /// SequenceComponent fast scroll scaling setting kPropertyFastScrollScale, /// ImageComponent array of filters @@ -724,6 +728,7 @@ enum ComponentType { kComponentTypeExtension, kComponentTypeFrame, kComponentTypeGridSequence, + kComponentTypeHost, kComponentTypeImage, kComponentTypePager, kComponentTypeScrollView, diff --git a/aplcore/include/apl/component/corecomponent.h b/aplcore/include/apl/component/corecomponent.h index fd72c41..c6bdf78 100644 --- a/aplcore/include/apl/component/corecomponent.h +++ b/aplcore/include/apl/component/corecomponent.h @@ -107,6 +107,11 @@ class CoreComponent : public Component, */ void release() override; + /** + * Clear any active component state. This may include animations/timers/caches/etc. + */ + void clearActiveState(); + /** * Visitor pattern for walking the component hierarchy. We are interested in the components * that the user can see/interact with. Overrides that have knowledge about which children are off screen or otherwise @@ -127,10 +132,22 @@ class CoreComponent : public Component, /** * Find a component at or below this point in the hierarchy with the given id or uniqueId. + * This variant of findComponentById must only be used by clients of Core, and not Core itself. + * Core code must use the variant accepting 'traverseHost', setting 'traverseHost' to false. * @param id The id or uniqueId to search for. * @return The component or nullptr if it is not found. + * @deprecated Use @see CoreComponent::findComponentById(const std::string&, bool) */ - ComponentPtr findComponentById(const std::string& id) const override; + APL_DEPRECATED ComponentPtr findComponentById(const std::string& id) const override; + + /** + * Special variant of findComponentId providing a signal to HostComponent indicating whether or + * not the 'child' of the HostComponent should be included in the search. + * @param id The id or uniqueId to search for. + * @param traverseHost the 'child' of HostComponent will be included iff true + * @return The component or nullptr if it is not found. + */ + virtual ComponentPtr findComponentById(const std::string& id, bool traverseHost) const; /** * Find a visible component at or below this point in the hierarchy containing the given position. @@ -149,7 +166,7 @@ class CoreComponent : public Component, * @param index The zero-based index of the child. * @return The child. */ - ComponentPtr getChildAt(size_t index) const override { return mChildren.at(index); } + ComponentPtr getChildAt(size_t index) const override { return std::static_pointer_cast(mChildren.at(index)); } /** * Return the index of a particular child. @@ -190,6 +207,7 @@ class CoreComponent : public Component, bool insertChild(const ComponentPtr& child, size_t index) override; bool appendChild(const ComponentPtr& child) override; bool remove() override; + bool remove(bool useDirtyFlag); bool canInsertChild() const override { // Child insertion is permitted if (a) there isn't a layout rebuilder and (b) there is space for a child. return !mRebuilder && ((singleChild() && mChildren.empty()) || multiChild()); @@ -271,13 +289,7 @@ class CoreComponent : public Component, */ void markProperty(PropertyKey key); - /** - * A context that this property depends upon has changed value. Update the - * value of the property and set the dirty flag if it changes. - * @param key The property to recalculate - * @param value The new value to assign - */ - void updateProperty(PropertyKey key, const Object& value); + void setValue(PropertyKey key, const Object& value, bool useDirtyFlag) override; /** * Change the state of the component. This may trigger a style change in @@ -311,7 +323,12 @@ class CoreComponent : public Component, /** * @return The current parent of this component. May be nullptr. */ - ComponentPtr getParent() const override { return mParent; } + ComponentPtr getParent() const override { return std::static_pointer_cast(mParent); } + + /** + * @return The current parent of this component if it is in the same document. May be nullptr. + */ + CoreComponentPtr getParentIfInDocument() const; /** * Guarantees that this component has been laid out, so that layout bounds are fully calculated. @@ -535,7 +552,7 @@ class CoreComponent : public Component, * @param keyboard The keyboard update. * @return The event data-binding context. */ - ContextPtr createKeyboardEventContext(const std::string& handler, const ObjectMapPtr& keyboard) const; + ContextPtr createKeyEventContext(const std::string& handler, const ObjectMapPtr& keyboard) const; virtual const ComponentPropDefSet& propDefSet() const; @@ -711,6 +728,8 @@ class CoreComponent : public Component, bool isAttached() const; /** + * Determines whether a component is laid out. This cannot be reliably used before the initial layout pass. + * * @return True if the component is attached to the yoga hierarchy and there are no dirty ancestor nodes */ bool isLaidOut() const; @@ -841,9 +860,10 @@ class CoreComponent : public Component, * Defer pointer processing to component. * @param event pointer event. * @param timestamp event timestamp. + * @param onlyProcessGestures specify whether we only process gestures. * @return the status of the pointer following processing by this component */ - virtual PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp); + virtual PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures); /** * @return The root configuration provided by the viewhost @@ -1040,12 +1060,22 @@ class CoreComponent : public Component, template void traverse(const Pre& pre) { traverse(pre, [](CoreComponent& c) {}); }; + /** + * Operation to perform before actual component release. + */ + virtual void preRelease() {}; + /** * Release this component. This component may still be in its parent's child list. This does * not release children of this component, nor does it clear this component's list of children. */ virtual void releaseSelf(); + /** + * Clear any component specific delayed processing (timers/animations/etc) + */ + virtual void clearActiveStateSelf(); + virtual void removeChildAfterMarkedRemoved(const CoreComponentPtr& child, size_t index, bool useDirtyFlag); #ifdef SCENEGRAPH @@ -1085,6 +1115,13 @@ class CoreComponent : public Component, return { Object::NULL_OBJECT(), false }; } +protected: + /** + * @return true if children of this component should be included in the visual context, false + * otherwise. + */ + virtual bool includeChildrenInVisualContext() const { return true; } + private: friend streamer& operator<<(streamer&, const Component&); @@ -1092,6 +1129,7 @@ class CoreComponent : public Component, friend class LayoutRebuilder; friend class LayoutManager; friend class ChildWalker; + friend class HostComponent; // for access to attachedToParent bool appendChild(const ComponentPtr& child, bool useDirtyFlag); @@ -1122,8 +1160,9 @@ class CoreComponent : public Component, virtual const ComponentPropDefSet* layoutPropDefSet() const { return nullptr; }; - void serializeVisualContextInternal(rapidjson::Value& outArray, rapidjson::Document::AllocatorType& allocator, - float realOpacity, float visibility, const Rect& visibleRect, int visualLayer); + void serializeVisualContextInternal( + rapidjson::Value& outArray, rapidjson::Document::AllocatorType& allocator, float realOpacity, + float visibility, const Rect& visibleRect, int visualLayer); void attachRebuilder(const std::shared_ptr& rebuilder) { mRebuilder = rebuilder; } @@ -1131,6 +1170,10 @@ class CoreComponent : public Component, void notifyChildChanged(size_t index, const std::string& uid, const std::string& action); + /** + * The default behavior of the child insertion is to attach the child when it happens. + * Override this function for cases when such behavior is not required. + */ virtual void attachYogaNodeIfRequired(const CoreComponentPtr& coreChild, int index); void scheduleTickHandler(const Object& handler, double delay); @@ -1172,6 +1215,7 @@ class CoreComponent : public Component, bool mTextMeasurementHashStale; bool mVisualHashStale; std::string mTextMeasurementHash; + timeout_id mTickHandlerId = 0; }; } // namespace apl diff --git a/aplcore/include/apl/component/edittextcomponent.h b/aplcore/include/apl/component/edittextcomponent.h index afbaed1..41e2ba5 100644 --- a/aplcore/include/apl/component/edittextcomponent.h +++ b/aplcore/include/apl/component/edittextcomponent.h @@ -51,7 +51,7 @@ class EditTextComponent : public ActionableComponent { protected: const ComponentPropDefSet& propDefSet() const override; const EventPropertyMap& eventPropertyMap() const override; - PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp) override; + PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) override; void executeOnBlur() override; void executeOnFocus() override; diff --git a/aplcore/include/apl/component/hostcomponent.h b/aplcore/include/apl/component/hostcomponent.h new file mode 100644 index 0000000..1aee548 --- /dev/null +++ b/aplcore/include/apl/component/hostcomponent.h @@ -0,0 +1,103 @@ +/** +* Copyright 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 APL_HOST_COMPONENT_H +#define APL_HOST_COMPONENT_H + +#include + +#include "apl/common.h" +#include "apl/component/actionablecomponent.h" +#include "apl/component/componentpropdef.h" +#include "apl/component/componentproperties.h" +#include "apl/embed/documentmanager.h" +#include "apl/engine/properties.h" +#include "apl/utils/path.h" + +namespace apl { + +class HostComponent : public ActionableComponent { +public: + static CoreComponentPtr create(const ContextPtr& context, Properties&& properties, const Path& path); + + HostComponent(const ContextPtr& context, + Properties&& properties, + const Path& path); + + ComponentType getType() const override { return kComponentTypeHost; }; + + ComponentPtr findComponentById(const std::string& id, bool traverseHost) const override; + + bool singleChild() const override { return true; } + + void processLayoutChanges(bool useDirtyFlag, bool first) override; + + void postProcessLayoutChanges() override; + + /** + * Reinflate contained document. + */ + void reinflate(); + +protected: + const ComponentPropDefSet& propDefSet() const override; + + void preRelease() override; + + void releaseSelf() override; + + void attachYogaNodeIfRequired(const CoreComponentPtr& coreChild, int index) override {} + + bool includeChildrenInVisualContext() const override; + + std::string getVisualContextType() const override; + + bool executeKeyHandlers(KeyHandlerType type, const Keyboard &keyboard) override; + +private: + DocumentContextPtr onLoad(const EmbeddedRequestSuccessResponse&& response); + + void onLoadHandler(); + + void onFail(const EmbeddedRequestFailureResponse&& response); + + void onFailHandler(const URLRequest& url, const std::string& failure); + + DocumentContextPtr initializeEmbedded(const EmbeddedRequestSuccessResponse&& response); + + void detachEmbedded(); + + void releaseEmbedded(); + + void requestEmbedded(); + + void resolvePendingParameters(const ContentPtr& content); + + /** + * @return Owned document ID, 0 if none. + */ + int getDocumentId() const; + + void setDocument(int id, bool connectedVC); + +private: + EmbedRequestPtr mRequest; + bool mOnLoadOnFailReported = false; + bool mNeedToRequestDocument = true; +}; + +} // namespace apl + +#endif // APL_HOST_COMPONENT_H diff --git a/aplcore/include/apl/component/multichildscrollablecomponent.h b/aplcore/include/apl/component/multichildscrollablecomponent.h index 09e5e1b..17d36c1 100644 --- a/aplcore/include/apl/component/multichildscrollablecomponent.h +++ b/aplcore/include/apl/component/multichildscrollablecomponent.h @@ -120,18 +120,16 @@ class MultiChildScrollableComponent : public ScrollableComponent { float maxScroll() const override; bool shouldAttachChildYogaNode(int index) const override { return false; } bool shouldBeFullyInflated(int index) const override; - const EventPropertyMap & eventPropertyMap() const override; void handlePropertyChange(const ComponentPropDef& def, const Object& value) override; - void onScrollPositionUpdated() override; + void attachYogaNode(const CoreComponentPtr& child) override; + void clearActiveStateSelf() override; virtual size_t getItemsPerCourse() const { return 1; } virtual void ensureChildAttached(const CoreComponentPtr& child, int targetIdx); - void attachYogaNode(const CoreComponentPtr& child) override; - /** * Estimate number of children required to cover provided distance based on parameters of child provided. */ diff --git a/aplcore/include/apl/component/pagercomponent.h b/aplcore/include/apl/component/pagercomponent.h index 96d6283..0cc62ba 100644 --- a/aplcore/include/apl/component/pagercomponent.h +++ b/aplcore/include/apl/component/pagercomponent.h @@ -104,6 +104,7 @@ class PagerComponent : public ActionableComponent { void finalizePopulate() override; void ensureDisplayedChildren() override; void releaseSelf() override; + void clearActiveStateSelf() override; private: bool multiChild() const override { return true; } diff --git a/aplcore/include/apl/component/selector.h b/aplcore/include/apl/component/selector.h index cf87b5d..0ed9d51 100644 --- a/aplcore/include/apl/component/selector.h +++ b/aplcore/include/apl/component/selector.h @@ -16,6 +16,8 @@ #ifndef _APL_SELECTOR_H #define _APL_SELECTOR_H +#include + #include "apl/common.h" namespace apl { diff --git a/aplcore/include/apl/component/touchablecomponent.h b/aplcore/include/apl/component/touchablecomponent.h index db0dadb..ea76bf7 100644 --- a/aplcore/include/apl/component/touchablecomponent.h +++ b/aplcore/include/apl/component/touchablecomponent.h @@ -51,10 +51,10 @@ class TouchableComponent : public ActionableComponent { const ComponentPropDefSet &propDefSet() const override; bool getTags(rapidjson::Value &outMap, rapidjson::Document::AllocatorType &allocator) override; bool isTouchable() const override { return true; } - // TODO: override to be removed once we support handing intrinsic/reserved keys + // TODO: override to be removed once we support handling intrinsic/reserved keys void update(UpdateType type, float value) override; void setGestureHandlers(); - PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp) override; + PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) override; void invokeStandardAccessibilityAction(const std::string& name) override; private: diff --git a/aplcore/include/apl/component/videocomponent.h b/aplcore/include/apl/component/videocomponent.h index 76e1fa4..77139a9 100644 --- a/aplcore/include/apl/component/videocomponent.h +++ b/aplcore/include/apl/component/videocomponent.h @@ -28,7 +28,9 @@ class VideoComponent : public CoreComponent { VideoComponent(const ContextPtr& context, Properties&& properties, const Path& path); virtual ~VideoComponent() noexcept; - ComponentType getType() const override { return kComponentTypeVideo; }; + ComponentType getType() const override { return kComponentTypeVideo; } + + void releaseSelf() override; bool remove() override; void updateMediaState(const MediaState& state, bool fromEvent) override; diff --git a/aplcore/include/apl/content/aplversion.h b/aplcore/include/apl/content/aplversion.h index ac383e6..b8514d7 100644 --- a/aplcore/include/apl/content/aplversion.h +++ b/aplcore/include/apl/content/aplversion.h @@ -37,6 +37,7 @@ class APLVersion { kAPLVersion20221 = 0x1U << 10, /// Support version 2022.1 kAPLVersion20222 = 0x1U << 11, /// Support version 2022.2 kAPLVersion20231 = 0x1U << 12, /// Support version 2023.1 + kAPLVersion20232 = 0x1U << 13, /// Support version 2023.2 kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, @@ -49,9 +50,10 @@ class APLVersion { kAPLVersion10to20221 = kAPLVersion10to19 | kAPLVersion20221, kAPLVersion20221to20222 = kAPLVersion10to20221 | kAPLVersion20222, kAPLVersion20222to20231 = kAPLVersion20221to20222 | kAPLVersion20231, - kAPLVersionLatest = kAPLVersion20222to20231, /// Support the most recent engine version - kAPLVersionDefault = kAPLVersion20222to20231, /// Default value - kAPLVersionReported = kAPLVersion20231, /// Default reported version + kAPLVersion20231to20232 = kAPLVersion20222to20231 | kAPLVersion20232, + kAPLVersionLatest = kAPLVersion20231to20232, /// Support the most recent engine version + kAPLVersionDefault = kAPLVersion20231to20232, /// Default value + kAPLVersionReported = kAPLVersion20232, /// Default reported version kAPLVersionAny = 0xffffffff, /// Support any versions in the list }; diff --git a/aplcore/include/apl/content/configurationchange.h b/aplcore/include/apl/content/configurationchange.h index ed02f31..096e647 100644 --- a/aplcore/include/apl/content/configurationchange.h +++ b/aplcore/include/apl/content/configurationchange.h @@ -21,6 +21,7 @@ #include "apl/content/rootconfig.h" #include "apl/content/metrics.h" #include "apl/primitives/size.h" +#include "apl/utils/log.h" namespace apl { @@ -82,6 +83,22 @@ class ConfigurationChange { return *this; } + /** + * Set the viewport mode. + * @param viewportMode The viewport mode + * @return This object for chaining + */ + ConfigurationChange& mode(const std::string &viewportMode) + { + auto it = sViewportModeBimap.find(viewportMode); + if (it != sViewportModeBimap.endBtoA()) { + return mode(static_cast(it->second)); + } + + LOG(LogLevel::kWarn) << "Ignoring invalid viewport mode for configuration change: " << viewportMode; + return *this; + } + /** * Set the requested font scaling factor for the document. * @param scale The scaling factor. Default is 1.0 @@ -115,6 +132,21 @@ class ConfigurationChange { return *this; } + /** + * Set the screen display mode for accessibility (normal or high-contrast) + * @param mode The screen display mode + * @return This object for chaining + */ + ConfigurationChange& screenMode(const std::string &mode) { + auto it = sScreenModeBimap.find(mode); + if (it != sScreenModeBimap.endBtoA()) { + return screenMode(static_cast(it->second)); + } + + LOG(LogLevel::kWarn) << "Ignoring invalid screen mode for configuration change: " << mode; + return *this; + } + /** * Inform that a screen reader is turned on. * @param enabled True if the screen reader is enabled @@ -155,13 +187,6 @@ class ConfigurationChange { */ RootConfig mergeRootConfig(const RootConfig& oldRootConfig) const; - /** - * Merge this configuration change into a new size object - * @param oldSize The old size to merge with this change - * @return A new size object with these changes - */ - Size mergeSize(const Size& oldSize) const; - /** * Merge a new configuration change into this one. * @param other The old configuration change to merge with this change @@ -176,6 +201,16 @@ class ConfigurationChange { */ ObjectMap asEventProperties(const RootConfig& rootConfig, const Metrics& metrics) const; + /** + * @return True if configuration change contains size change, false otherwise. + */ + bool hasSizeChange() const { return (mFlags & kConfigurationChangeSize); } + + /** + * @return New pixel size from this change. + */ + Size getSize() const { return { static_cast(mPixelWidth), static_cast(mPixelHeight) }; } + /** * @return True if the configuration change is empty */ diff --git a/aplcore/include/apl/content/content.h b/aplcore/include/apl/content/content.h index 5fe1a5c..f28c691 100644 --- a/aplcore/include/apl/content/content.h +++ b/aplcore/include/apl/content/content.h @@ -16,10 +16,8 @@ #ifndef _APL_CONTENT_H #define _APL_CONTENT_H -#include -#include - #include "apl/common.h" +#include "apl/content/extensionrequest.h" #include "apl/content/package.h" #include "apl/content/settings.h" #include "apl/engine/properties.h" @@ -130,6 +128,13 @@ class Content : public Counter { */ void addData(const std::string& name, JsonData&& data); + /** + * Add data + * @param name The name of the data source + * @param data The raw data source + */ + void addObjectData(const std::string& name, const Object& data); + /** * @return The number of parameters */ @@ -181,6 +186,11 @@ class Content : public Counter { */ std::set getExtensionRequests() const; + /** + * @return The ordered collection of extension requests + */ + const std::vector& getExtensionRequestsV2() const; + /** * Retrieve the settings associated with an extension request. * @param uri The uri of the extension. @@ -204,7 +214,7 @@ class Content : public Counter { std::set getPendingParameters() const { return mPendingParameters; } private: // Non-public methods used by other classes - friend class RootContext; + friend class CoreDocumentContext; const std::vector& ordered() const {return mOrderedDependencies;}; const rapidjson::Value& getMainTemplate() const { return mMainTemplate; } @@ -217,8 +227,8 @@ class Content : public Counter { * @param mainPackagePtr The main package * @param mainTemplate The RapidJSON main template object */ - Content(SessionPtr session, - PackagePtr mainPackagePtr, + Content(const SessionPtr& session, + const PackagePtr& mainPackagePtr, const rapidjson::Value& mainTemplate); private: // Private internal methods @@ -229,6 +239,7 @@ class Content : public Counter { void loadExtensionSettings(); bool orderDependencyList(); bool addToDependencyList(std::vector& ordered, std::set& inProgress, const PackagePtr& package); + bool allowAdd(const std::string& name); private: enum State { @@ -241,7 +252,7 @@ class Content : public Counter { SessionPtr mSession; PackagePtr mMainPackage; - std::vector> mExtensionRequests; // ordered + std::vector mExtensionRequests; ObjectMapPtr mExtensionSettings; // Map Name -> may be null State mState; @@ -252,7 +263,7 @@ class Content : public Counter { std::map mLoaded; std::vector mOrderedDependencies; - std::map mParameterValues; + std::map mParameterValues; std::vector mMainParameters; // Requested by the main template std::vector mEnvironmentParameters; // Requested by the environment block std::set mPendingParameters; // Union of main and environment parameters diff --git a/aplcore/include/apl/content/documentconfig.h b/aplcore/include/apl/content/documentconfig.h new file mode 100644 index 0000000..be117fa --- /dev/null +++ b/aplcore/include/apl/content/documentconfig.h @@ -0,0 +1,87 @@ +/** + * Copyright 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 _APL_DOCUMENT_CONFIG_H +#define _APL_DOCUMENT_CONFIG_H + +#include "apl/common.h" + +#ifdef ALEXAEXTENSIONS +#include +#endif + +namespace apl { + +/** + * Configuration at the document level, applicable to embedded as well as primary documents. + */ +class DocumentConfig { +public: + /** + * @return DocumentConfig instance. + */ + static DocumentConfigPtr create() { return std::make_shared(); } + + /** + * Default constructor. Use create() instead. + */ + DocumentConfig() {} + +#ifdef ALEXAEXTENSIONS + /** + * Assign a Alexa Extension mediator. + * + * @param extensionMediator Message mediator manages messages between Extension and APL engine. + * and the APL engine. + * @return This object for chaining + */ + DocumentConfig& extensionMediator(const ExtensionMediatorPtr &extensionMediator) { + mExtensionMediator = extensionMediator; + return *this; + } + + /** + * @return The extension mediator. + */ + const ExtensionMediatorPtr& getExtensionMediator() const { + return mExtensionMediator; + } +#endif + + /** + * Add DataSource provider implementation. + * @param dataSourceProvider provider implementation. + * @return This object for chaining. + */ + DocumentConfig& dataSourceProvider(const DataSourceProviderPtr& dataSourceProvider) { + mDataSources.emplace(dataSourceProvider); + return *this; + } + + /** + * @return Set of DataSource providers. + */ + const std::set& getDataSourceProviders() const { return mDataSources; } + +private: +#ifdef ALEXAEXTENSIONS + ExtensionMediatorPtr mExtensionMediator; +#endif + std::set mDataSources; +}; + +} + +#endif //_APL_DOCUMENT_CONFIG_H diff --git a/aplcore/include/apl/content/extensionrequest.h b/aplcore/include/apl/content/extensionrequest.h new file mode 100644 index 0000000..8595e46 --- /dev/null +++ b/aplcore/include/apl/content/extensionrequest.h @@ -0,0 +1,34 @@ +/** + * Copyright 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 _APL_EXTENSION_REQUEST_H +#define _APL_EXTENSION_REQUEST_H + +#include + +namespace apl { + +/** + * Extension request definition. + */ +struct ExtensionRequest { + std::string name; + std::string uri; + bool required; +}; + +} // namespace apl + +#endif // _APL_EXTENSION_REQUEST_H \ No newline at end of file diff --git a/aplcore/include/apl/content/importrequest.h b/aplcore/include/apl/content/importrequest.h index 76c963b..2f5294f 100644 --- a/aplcore/include/apl/content/importrequest.h +++ b/aplcore/include/apl/content/importrequest.h @@ -46,8 +46,7 @@ class ImportRequest { bool operator<(const ImportRequest& other) const { return this->compare(other) < 0; } int compare(const ImportRequest& other) const { - int result = mReference.compare(other.reference()); - return result == 0 ? mSource.compare(other.mSource) : result; + return mReference.compare(other.reference()); } uint32_t getUniqueId() const { return mUniqueId; } const std::string& source() const { return mSource; } diff --git a/aplcore/include/apl/content/jsondata.h b/aplcore/include/apl/content/jsondata.h index d0130f7..83f4762 100644 --- a/aplcore/include/apl/content/jsondata.h +++ b/aplcore/include/apl/content/jsondata.h @@ -23,6 +23,8 @@ namespace apl { +class Object; + /** * Wrapper class for holding JSON data. * @@ -131,6 +133,12 @@ class JsonData { } } + /** + * Move the rapidjson state of this to a new Object and return that Object. + * @return The Object + */ + Object moveToObject(); + /** * @return A reference to the top-level rapidjson Value. */ diff --git a/aplcore/include/apl/content/metrics.h b/aplcore/include/apl/content/metrics.h index 1eb5e78..e5b6a16 100644 --- a/aplcore/include/apl/content/metrics.h +++ b/aplcore/include/apl/content/metrics.h @@ -20,6 +20,7 @@ #include "apl/utils/streamer.h" #include "apl/utils/bimap.h" +#include "apl/utils/log.h" #include "apl/utils/userdata.h" namespace apl { @@ -58,21 +59,12 @@ extern Bimap sViewportModeBimap; */ class Metrics : public UserData { public: - static constexpr float CORE_DPI = 160.0f; /** * Construct default metrics. */ - Metrics() : - mTheme("dark"), - mPixelWidth(1024), - mPixelHeight(800), - mDpi(CORE_DPI), - mShape(RECTANGLE), - mMode(kViewportModeHub) - { - } + Metrics() = default; /** * Set the color theme. @@ -85,17 +77,39 @@ class Metrics : public UserData { } /** - * Set the pixel dimensions of the screen. + * Set the pixel dimensions of the screen or view. When using auto-sizing, this + * should be set to the nominal or target dimension of the view. * @param pixelWidth The width of the screen, in pixels. * @param pixelHeight The height of the screen, in pixels. * @return This object for chaining. */ Metrics& size(int pixelWidth, int pixelHeight) { + assert(pixelWidth > 0 && pixelHeight > 0); mPixelWidth = pixelWidth; mPixelHeight = pixelHeight; return *this; } + /** + * Set if the width of the view can be automatically sized by the APL document + * @param value True if the view width can be auto-sized. + * @return This object for chaining + */ + Metrics& autoSizeWidth(bool value) { + mAutoSizeWidth = value; + return *this; + } + + /** + * Set if the height of the view can be automatically sized by the APL document + * @param value True if the view height can be auto-sized. + * @return This object for chaining + */ + Metrics& autoSizeHeight(bool value) { + mAutoSizeHeight = value; + return *this; + } + /** * Set the dpi of the screen (display-independent pixel resolution). * @param dpi The dpi to set. @@ -117,6 +131,21 @@ class Metrics : public UserData { return *this; } + /** + * Set the shape of the screen. + * @param screenShape The screen shape. + * @return This object, for chaining. + */ + Metrics& shape(const std::string& screenShape) { + auto it = sScreenShapeBimap.find(screenShape); + if (it != sScreenShapeBimap.endBtoA()) { + return shape(static_cast(it->second)); + } + + LOG(LogLevel::kWarn) << "Ignoring invalid screen shape for metrics: " << screenShape; + return *this; + } + /** * Set the operating mode of the viewport * @param mode The viewport mode. @@ -127,6 +156,21 @@ class Metrics : public UserData { return *this; } + /** + * Set the operating mode of the viewport + * @param viewportMode The viewport mode. + * @return This object, for chaining. + */ + Metrics& mode(const std::string& viewportMode) { + auto it = sViewportModeBimap.find(viewportMode); + if (it != sViewportModeBimap.endBtoA()) { + return mode(static_cast(it->second)); + } + + LOG(LogLevel::kWarn) << "Ignoring invalid viewport mode for metrics: " << viewportMode; + return *this; + } + /** * @return The dpi of the screen. */ @@ -142,6 +186,16 @@ class Metrics : public UserData { */ float getWidth() const { return pxToDp(mPixelWidth); } + /** + * @return True if the width should auto-size + */ + bool getAutoWidth() const { return mAutoSizeWidth; } + + /** + * @return True if the height should auto-size + */ + bool getAutoHeight() const { return mAutoSizeHeight; } + /** * Convert Display Pixels to Pixels * @param dp Display Pixels @@ -194,12 +248,14 @@ class Metrics : public UserData { std::string toDebugString() const; private: - std::string mTheme; - int mPixelWidth; - int mPixelHeight; - int mDpi; - ScreenShape mShape; - ViewportMode mMode; + std::string mTheme = "dark"; + int mPixelWidth = 1024; + int mPixelHeight = 800; + int mDpi = CORE_DPI; + ScreenShape mShape = RECTANGLE; + ViewportMode mMode = kViewportModeHub; + bool mAutoSizeWidth = false; + bool mAutoSizeHeight = false; }; } // namespace apl diff --git a/aplcore/include/apl/content/package.h b/aplcore/include/apl/content/package.h index f3e1799..da490e3 100644 --- a/aplcore/include/apl/content/package.h +++ b/aplcore/include/apl/content/package.h @@ -58,12 +58,12 @@ class Package : public Counter, /** * @return package APL spec version. */ - const std::string version(); + std::string version(); /** * @return package type field */ - const std::string type(); + std::string type(); Package(const std::string& name, JsonData&& json) : mName(name), diff --git a/aplcore/include/apl/content/rootconfig.h b/aplcore/include/apl/content/rootconfig.h index 17828b3..517e5bc 100644 --- a/aplcore/include/apl/content/rootconfig.h +++ b/aplcore/include/apl/content/rootconfig.h @@ -150,6 +150,16 @@ class RootConfig { return *this; } + /** + * Specify the document manager used for loading embedded documents. + * @param documentManager The document manager object. + * @return This object for chaining. + */ + RootConfig& documentManager(const DocumentManagerPtr& documentManager) { + mDocumentManager = documentManager; + return *this; + } + /** * Specify the media manager used for loading images, videos, and vector graphics. * @param mediaManager The media manager object. @@ -270,8 +280,9 @@ class RootConfig { * Set the session * @param session The session * @return This object for chaining. + * @deprecated Session used for content creation will be used instead. */ - RootConfig& session(const SessionPtr& session); + APL_DEPRECATED RootConfig& session(const SessionPtr& session); /** * Assign a LiveObject to the top-level context @@ -341,12 +352,8 @@ class RootConfig { * Assign a Alexa Extension mediator. * Requires kExperimentalFeatureExtensionProvider feature be enabled. * - * IMPORTANT: ExtensionMediator is a class that is expected to be eliminated. It - * can only be used with a single document/RootContext. It is expected the viewhost call ExtensionMediator.loadExtensions() - * prior to calling RootContext::create(). RootContext will bind to the mediator obtained from this assignment. - * * @param extensionMediator Message mediator manages messages between Extension and APL engine. - * and the APL engine. + * and the APL engine. * @return This object for chaining */ RootConfig& extensionMediator(const ExtensionMediatorPtr &extensionMediator) { @@ -362,6 +369,7 @@ class RootConfig { * This method will also register the extension as a supported extension. * @param handler The name of the handler to support. * @return This object for chaining. + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionEventHandler(ExtensionEventHandler handler) { auto uri = handler.getURI(); @@ -377,6 +385,7 @@ class RootConfig { * This method will also register the extension as a supported extension. * @param commandDef The definition of the custom command (includes the name, URI, etc). * @return This object for chaining + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionCommand(ExtensionCommandDefinition commandDef) { auto uri = commandDef.getURI(); @@ -392,6 +401,7 @@ class RootConfig { * This method will also register the extension as a supported extension. * @param filterDef The definition of the custom filter (includes the name, URI, properties) * @return This object for chaining + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionFilter(ExtensionFilterDefinition filterDef) { const auto& uri = filterDef.getURI(); @@ -407,6 +417,7 @@ class RootConfig { * This method will also register the extension as a supported extension. * @param componentDef The definition of the custom component (includes the name, URI, etc). * @return This object for chaining + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionComponent(ExtensionComponentDefinition componentDef) { auto uri = componentDef.getURI(); @@ -426,6 +437,7 @@ class RootConfig { * @param uri The URI of the extension * @param environment values * @return This object for chaining + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionEnvironment(const std::string& uri, const Object& environment) { registerExtension(uri, environment); @@ -437,6 +449,7 @@ class RootConfig { * @param uri The URI of the extension * @param config Configuration value(s) supported by this extension. * @return This object for chaining + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtension(const std::string& uri, const Object& config = Object::TRUE_OBJECT()) { if (!config.truthy()) { @@ -454,6 +467,7 @@ class RootConfig { * @param uri The URI of the extension * @param flags The extension flags * @return This object for chaining + * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionFlags(const std::string& uri, const Object& flags) { if (mSupportedExtensions.find(uri) == mSupportedExtensions.end()) { @@ -903,6 +917,11 @@ class RootConfig { */ TextMeasurementPtr getMeasure() const { return mTextMeasurement; } + /** + * @return The configured document manager object + */ + DocumentManagerPtr getDocumentManager() const { return mDocumentManager; } + /** * @return The configured media manager object */ @@ -1059,8 +1078,9 @@ class RootConfig { /** * @return The current session pointer + * @deprecated Always null */ - SessionPtr getSession() const { return mSession; } + APL_DEPRECATED SessionPtr getSession() const { return nullptr; } /** * @return The starting UTC time in milliseconds past the epoch. @@ -1112,14 +1132,6 @@ class RootConfig { return mHeaderFilters; } - /** - * @param type DataSource type. - * @return true if registered, false otherwise. - */ - bool isDataSource(const std::string& type) const { - return (mDataSources.find(type) != mDataSources.end()); - } - #ifdef ALEXAEXTENSIONS /** * Requires kExperimentalExtensionProvider. @@ -1138,8 +1150,31 @@ class RootConfig { } #endif + /** + * Extension clients should only be created by the mediator, but if they are created + * independently (legacy pathway), they need to be registered with RootConfig. + * + * Note: Self-registration happens automatically in the legacy ExtensionClient::create() + * + * @return The root config + * @deprecated Extensions should be managed via ExtensionMediator + */ + RootConfig& registerLegacyExtensionClient(const std::string& uri, const ExtensionClientPtr& client) { + mLegacyExtensionClients.emplace(uri, client); + return *this; + } + + /** + * @return Any extension clients registered with the RootConfig for legacy support. + * @deprecated Extensions should be managed via ExtensionMediator + */ + const std::map> getLegacyExtensionClients() const { + return mLegacyExtensionClients; + } + /** * @return The registered extension commands + * @deprecated Extensions should be managed via ExtensionMediator */ const std::vector& getExtensionCommands() const { return mExtensionCommands; @@ -1147,6 +1182,7 @@ class RootConfig { /** * @return The registered extension events + * @deprecated Extensions should be managed via ExtensionMediator */ const std::vector& getExtensionEventHandlers() const { return mExtensionHandlers; @@ -1154,6 +1190,7 @@ class RootConfig { /** * @return The registered extension filters + * @deprecated Extensions should be managed via ExtensionMediator */ const std::vector& getExtensionFilters() const { return mExtensionFilters; @@ -1161,6 +1198,7 @@ class RootConfig { /** * @return The registered extension components + * @deprecated Extensions should be managed via ExtensionMediator */ const std::vector& getExtensionComponentDefinitions() const { return mExtensionComponentDefinitions; @@ -1169,6 +1207,7 @@ class RootConfig { /** * @return The collection of supported extensions and their config values. These are the extensions that have * been marked in the root config as "supported". + * @deprecated Extensions should be managed via ExtensionMediator */ const ObjectMap& getSupportedExtensions() const { return mSupportedExtensions; @@ -1177,6 +1216,7 @@ class RootConfig { /** * @param uri The URI of the extension. * @return The environment Object for a supported extensions, Object::NULL_OBJECT if no environment exists. + * @deprecated Extensions should be managed via ExtensionMediator */ const Object& getExtensionEnvironment(const std::string& uri) const { auto it = mSupportedExtensions.find(uri); @@ -1188,6 +1228,7 @@ class RootConfig { /** * @param uri The URI of the extension. * @return The registered extension flags, NULL_OBJECT if no flags are registered. + * @deprecated Extensions should be managed via ExtensionMediator */ const Object& getExtensionFlags(const std::string& uri) const { auto it = mExtensionFlags.find(uri); @@ -1196,6 +1237,14 @@ class RootConfig { return Object::NULL_OBJECT(); } + /** + * @return All extension flags known to RootConfig as an object map (uri->flags) + * @deprecated Extensions should be managed via ExtensionMediator + */ + const ObjectMap& getExtensionFlags() const { + return mExtensionFlags; + } + /** * @return A map of values to be reported in the data-binding environment context */ @@ -1377,13 +1426,21 @@ class RootConfig { return mEnabledExperimentalFeatures.count(feature) > 0; } + /** + * Create a new RootConfig instance, copying all non-document-specific state from this instance. + * @return the copy + */ + RootConfigPtr copy() const; + private: const RootPropDefSet& propDefSet() const; + bool isAllowedEnvironmentName(const std::string &name) const; private: ContextPtr mContext; TextMeasurementPtr mTextMeasurement; + DocumentManagerPtr mDocumentManager; MediaManagerPtr mMediaManager; MediaPlayerFactoryPtr mMediaPlayerFactory; AudioPlayerFactoryPtr mAudioPlayerFactory; @@ -1394,7 +1451,7 @@ class RootConfig { std::shared_ptr mLocaleMethods; std::map, std::pair> mDefaultComponentSize; - SessionPtr mSession; + SessionPtr mConfigSession; ObjectMap mEnvironmentValues; // Data sources and live maps @@ -1407,6 +1464,11 @@ class RootConfig { alexaext::ExtensionProviderPtr mExtensionProvider; ExtensionMediatorPtr mExtensionMediator; #endif + // Clients should only be created by the mediator, but for legacy reasons clients can be created + // on their own. So, we need to keep track of these "standalone" clients so that we can extract + // registration information from them later. + std::map> mLegacyExtensionClients; + ObjectMap mSupportedExtensions; // URI -> config ObjectMap mExtensionFlags; // URI -> opaque flags std::vector mExtensionHandlers; diff --git a/aplcore/include/apl/content/rootproperties.h b/aplcore/include/apl/content/rootproperties.h index 1338702..6949c13 100644 --- a/aplcore/include/apl/content/rootproperties.h +++ b/aplcore/include/apl/content/rootproperties.h @@ -22,6 +22,7 @@ namespace apl { +// Note: If any per-document properties added here, also update sCopyableConfigProperties enum RootProperty { /// Agent name kAgentName, diff --git a/aplcore/include/apl/content/settings.h b/aplcore/include/apl/content/settings.h index dab02df..c9e7d1d 100644 --- a/aplcore/include/apl/content/settings.h +++ b/aplcore/include/apl/content/settings.h @@ -20,9 +20,9 @@ #include "rapidjson/document.h" -#include "apl/primitives/object.h" -#include "apl/content/rootconfig.h" #include "apl/content/package.h" +#include "apl/content/rootconfig.h" +#include "apl/primitives/object.h" #include "apl/utils/deprecated.h" namespace apl { @@ -32,7 +32,7 @@ namespace apl { * override device values. */ class Settings { - friend class RootContext; + friend class CoreDocumentContext; friend class Content; public: diff --git a/aplcore/include/apl/datagrammar/bytecode.h b/aplcore/include/apl/datagrammar/bytecode.h index e4dc08a..3bfc07a 100644 --- a/aplcore/include/apl/datagrammar/bytecode.h +++ b/aplcore/include/apl/datagrammar/bytecode.h @@ -19,12 +19,14 @@ #include #include "apl/datagrammar/functions.h" +#include "apl/primitives/boundsymbolset.h" #include "apl/primitives/objecttype.h" - namespace apl { namespace datagrammar { +class Disassembly; + using bciValueType = std::int32_t; const unsigned OPCODE_BITS = 8; const unsigned BCI_BITS = 24; @@ -78,6 +80,7 @@ enum ByteCodeOpcode { BC_OPCODE_MERGE_STRING, // TOS = TOS_n + ... + TOS where n = value - 1 BC_OPCODE_APPEND_ARRAY, // TOS = TOS_1.append(TOS) BC_OPCODE_APPEND_MAP, // TOS = TOS_2.append(TOS_1, TOS) + BC_OPCODE_EVALUATE, // TOS = eval(TOS) }; /** @@ -118,7 +121,7 @@ inline Object getConstant(ByteCodeConstant value) { case BC_CONSTANT_TRUE: return Object::TRUE_OBJECT(); case BC_CONSTANT_EMPTY_STRING: - return Object(""); + return {""}; case BC_CONSTANT_EMPTY_ARRAY: return Object::EMPTY_MUTABLE_ARRAY(); case BC_CONSTANT_EMPTY_MAP: @@ -172,6 +175,46 @@ struct ByteCodeInstruction { static_assert(sizeof(ByteCodeInstruction) == 4, "Wrong size of ByteCodeInstruction"); + +/** + * The Disassembly class is a convenience class for ByteCode that provides an iterator + * to output the disassembly of the bytecode into one string per line. + * @code + * for (const auto& m : byteCode.disassemble()) + * std::cout << m << std::endl; + * @endcode + */ +class Disassembly { +public: + class Iterator { + public: + Iterator(const ByteCode& byteCode, size_t lineNumber); + + using iterator_category = std::forward_iterator_tag; + using difference_type = size_t; + using value_type = std::string; + using pointer = value_type*; + using reference = std::string; + + reference operator*() const; + + Iterator& operator++(); + friend bool operator== (const Iterator& lhs, const Iterator& rhs); + friend bool operator!= (const Iterator& lhs, const Iterator& rhs); + + private: + const ByteCode& mByteCode; + size_t mLineNumber = 0; + }; + + explicit Disassembly(const ByteCode& byteCode) : mByteCode(byteCode) {} + Iterator begin() const; + Iterator end() const; + +private: + const ByteCode& mByteCode; +}; + /** * Store an expression that has been compiled into byte code. */ @@ -189,22 +232,17 @@ class ByteCode : public ObjectData, public std::enable_shared_from_this { public: - rapidjson::Value serialize( - const Object::DataHolder&, - rapidjson::Document::AllocatorType& allocator) const override - { - return rapidjson::Value("COMPILED BYTE CODE", allocator); + rapidjson::Value serialize(const Object::DataHolder&, + rapidjson::Document::AllocatorType& allocator) const override { + return {"COMPILED BYTE CODE", allocator}; } }; + Disassembly disassemble() const { + return Disassembly(*this); + } + private: std::weak_ptr mContext; std::vector mInstructions; @@ -252,6 +303,8 @@ class ByteCode : public ObjectData, public std::enable_shared_from_this 0; } ContextPtr context() const { return mContext; } @@ -161,6 +170,8 @@ class ByteCodeAssembler { std::vector* mInstructionRef; std::vector* mDataRef; std::vector* mOperatorsRef; + int mDeferredDepth = 0; // Track the stack of nested deferred evaluation #{..#{..${...}..}..} + bool mCanDeferAndEval = false; // Set to true if this assembler supports deferred evaluation (2023.2 or later) }; } // namespace datagrammar diff --git a/aplcore/include/apl/datagrammar/bytecodeevaluator.h b/aplcore/include/apl/datagrammar/bytecodeevaluator.h index 4d92cf8..7ca5b89 100644 --- a/aplcore/include/apl/datagrammar/bytecodeevaluator.h +++ b/aplcore/include/apl/datagrammar/bytecodeevaluator.h @@ -20,6 +20,7 @@ #include #include "apl/datagrammar/bytecode.h" +#include "apl/primitives/boundsymbolset.h" namespace apl { namespace datagrammar { @@ -30,7 +31,7 @@ namespace datagrammar { */ class ByteCodeEvaluator { public: - explicit ByteCodeEvaluator(const ByteCode& byteCode); + ByteCodeEvaluator(const ByteCode& byteCode, BoundSymbolSet *symbols, int depth); /** * Start or continue executing the byte code. @@ -42,17 +43,6 @@ class ByteCodeEvaluator { */ bool isDone() const { return mState == DONE; } - /** - * @return True if the byte code is an error state - */ - bool isError() const { return mState == ERROR; } - - /** - * Only valid after the byte code has run to completion. - * @return True if the byte code does not depend on any mutable methods or data. - */ - bool isConstant() const { assert(mState == DONE); return mIsConstant; } - /** * @return The result of executing the byte code. If the return type is void, this * method will return null. @@ -64,9 +54,10 @@ class ByteCodeEvaluator { const ByteCode& mByteCode; std::vector mStack; + BoundSymbolSet *mSymbols; int mProgramCounter = 0; + int mEvaluationDepth; State mState = INIT; - bool mIsConstant = true; }; } // namespace datagrammar diff --git a/aplcore/include/apl/datagrammar/databindingerrors.h b/aplcore/include/apl/datagrammar/databindingerrors.h index d707e1d..890e9cc 100644 --- a/aplcore/include/apl/datagrammar/databindingerrors.h +++ b/aplcore/include/apl/datagrammar/databindingerrors.h @@ -179,6 +179,7 @@ template<> const GrammarError error_control::error_value = static_cast const GrammarError error_control::error_value = static_cast(122); template<> const GrammarError error_control::error_value = static_cast(123); template<> const GrammarError error_control::error_value = static_cast(127); +template<> const GrammarError error_control::error_value = static_cast(128); template<> const GrammarError error_control::error_value = static_cast(131); template<> const GrammarError error_control>::error_value = static_cast(141); diff --git a/aplcore/include/apl/datagrammar/databindinggrammar.h b/aplcore/include/apl/datagrammar/databindinggrammar.h index 71bf55c..d94c8f1 100644 --- a/aplcore/include/apl/datagrammar/databindinggrammar.h +++ b/aplcore/include/apl/datagrammar/databindinggrammar.h @@ -70,6 +70,28 @@ struct sym_attribute : one<'.'> {}; struct sym_array_access_start : one<'['> {}; struct sym_array_access_end : one<']'> {}; +// The "#{...}" starting symbol is only available in 2023.2 and higher +// struct sym_delaystart : string<'#', '{'>{}; +// template +struct sym_delaystart +{ + template< apply_mode A, + rewind_mode M, + template< typename... > class Action, + template< typename... > class Control, + typename Input > + static bool match( Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { + if (assembler.canDeferAndEval() && in.size(2) >= 2) { + const auto *ptr = in.current(); + if (ptr[0] == '#' && ptr[1] == '{') { + in.bump(2); + return true; + } + } + return false; + } +}; + struct sep : space {}; struct ws : star {}; @@ -136,12 +158,36 @@ struct group_start : one<'('> {}; struct group_end : one<')'> {}; struct grouping : if_must {}; +// The "eval()" built-in function is only available in 2023.2 and higher. +// struct eval_start : string<'e','v','a','l','('> {}; +struct eval_start +{ + template< apply_mode A, + rewind_mode M, + template< typename... > class Action, + template< typename... > class Control, + typename Input > + static bool match( Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { + if (assembler.canDeferAndEval() && in.size(5) >= 5) { + const auto *ptr = in.current(); + if (ptr[0] == 'e' && ptr[1] == 'v' && ptr[2] == 'a' && ptr[3] == 'l' && ptr[4] == '(') { + in.bump(5); + return true; + } + } + return false; + } +}; +struct eval_end : one<')'> {}; +struct evaluation : if_must {}; + struct factor : sor< grouping, key_true, key_false, key_null, dimension, + evaluation, postfix_expression, number, ss_string, @@ -164,7 +210,8 @@ struct expression : ternary_expression {}; struct db_empty : success {}; // No expression - by default we insert an empty string struct db_body : pad_opt, sep> {}; -struct db : if_must {}; +struct db_head : sor {}; +struct db : if_must {}; // TODO: This assumes UTF-8 encoding. We're relying on RapidJSON to return UTF-8 struct char_ : utf8::any {}; @@ -172,7 +219,7 @@ struct char_ : utf8::any {}; // Double-quoted string. E.g.: ${"foo"} struct sym_double_quote : one<'"'> {}; struct ds_char : char_ {}; -struct ds_raw : until, at >, must > {}; +struct ds_raw : until, at, at >, must > {}; struct ds_start : sym_double_quote {}; struct ds_end : sym_double_quote {}; struct ds_body : list {}; @@ -181,15 +228,15 @@ struct ds_string : if_must {}; // Single-quoted string. E.g.: ${'foo'} struct sym_single_quote : one<'\''> {}; struct ss_char : char_ {}; -struct ss_raw : until, at >, must > {}; +struct ss_raw : until, at, at >, must > {}; struct ss_start : sym_single_quote {}; struct ss_end : sym_single_quote {}; struct ss_body : list {}; struct ss_string : if_must {}; -// NOTE: Probably can change this to until< at, char_ > {}; +// NOTE: Probably can change this to until< sor, at> >, char_ > {}; // Open-string: E.g. "this is a ${1+3} generic string" -struct os_raw : until, at >, must > {}; +struct os_raw : until, at, at >, must > {}; struct os_start : success {}; struct os_string : seq> {}; diff --git a/aplcore/include/apl/datagrammar/databindingrules.h b/aplcore/include/apl/datagrammar/databindingrules.h index 74c1d3d..49e3ad7 100644 --- a/aplcore/include/apl/datagrammar/databindingrules.h +++ b/aplcore/include/apl/datagrammar/databindingrules.h @@ -54,7 +54,7 @@ template<> struct action< number > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; double value = sutil::stod(in.string()); if (fitsInBCI(value)) assembler.loadImmediate(asBCI(value)); @@ -67,7 +67,7 @@ template<> struct action< key_null > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.loadConstant(BC_CONSTANT_NULL); } }; @@ -76,7 +76,7 @@ template<> struct action< key_true > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.loadConstant(BC_CONSTANT_TRUE); } }; @@ -85,7 +85,7 @@ template<> struct action< key_false > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.loadConstant(BC_CONSTANT_FALSE); } }; @@ -96,7 +96,7 @@ template<> struct action< dimension > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.loadOperand(Object(Dimension(*(assembler.context()), in.string()))); } }; @@ -106,7 +106,7 @@ template<> struct action< sym_unary > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushUnaryOperator(in.string()[0]); } }; @@ -115,7 +115,7 @@ template<> struct action< unary_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceUnary(); } }; @@ -124,7 +124,7 @@ template<> struct action< unary_expression > template<> struct action< sym_multiplicative > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushBinaryOperator(in.string()); } }; @@ -133,7 +133,7 @@ template<> struct action< multiplicative_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceBinary(BC_ORDER_MULTIPLICATIVE); } }; @@ -142,7 +142,7 @@ template<> struct action< multiplicative_expression > template<> struct action< sym_additive > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushBinaryOperator(in.string()); } }; @@ -151,7 +151,7 @@ template<> struct action< additive_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceBinary(BC_ORDER_ADDITIVE); } }; @@ -160,7 +160,7 @@ template<> struct action< additive_expression > template<> struct action< sym_compare > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushBinaryOperator(in.string()); } }; @@ -169,7 +169,7 @@ template<> struct action< comparison_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceBinary(BC_ORDER_COMPARISON); } }; @@ -178,7 +178,7 @@ template<> struct action< comparison_expression > template<> struct action< sym_equal > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushBinaryOperator(in.string()); } }; @@ -187,7 +187,7 @@ template<> struct action< equality_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceBinary(BC_ORDER_EQUALITY); } }; @@ -196,7 +196,7 @@ template<> struct action< equality_expression > template<> struct action< sym_and > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushAnd(); } }; @@ -205,7 +205,7 @@ template<> struct action< logical_and_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceJumps(BC_ORDER_LOGICAL_AND); } }; @@ -214,7 +214,7 @@ template<> struct action< logical_and_expression > template<> struct action< sym_or > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushOr(); } }; @@ -223,7 +223,7 @@ template<> struct action< logical_or_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceJumps(BC_ORDER_LOGICAL_OR); } }; @@ -232,7 +232,7 @@ template<> struct action< logical_or_expression > template<> struct action< sym_nullc > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushNullC(); } }; @@ -241,7 +241,7 @@ template<> struct action< nullc_expression > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceJumps(BC_ORDER_NULLC); } }; @@ -251,7 +251,7 @@ template<> struct action < sym_question > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushTernaryIf(); } }; @@ -260,7 +260,7 @@ template<> struct action < sym_colon > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushTernaryElse(); } }; @@ -269,7 +269,7 @@ template<> struct action< ternary_tail > { template< typename Input > static void apply(const Input& in, fail_state& failState, ByteCodeAssembler& assembler) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.reduceOneJump(BC_ORDER_TERNARY_ELSE); } }; @@ -279,7 +279,7 @@ template<> struct action< group_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushGroup(); } }; @@ -289,17 +289,37 @@ template<> struct action< grouping > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.popGroup(); } }; +// ************* Starting evaluation "eval(" ************* +template<> struct action< eval_start > +{ + template< typename Input > + static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { + if (failState.failed || assembler.deferred()) return; + assembler.pushGroup(); + } +}; + +template<> struct action< evaluation > +{ + template< typename Input > + static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { + if (failState.failed || assembler.deferred()) return; + assembler.popGroup(); + assembler.evaluate(); + } +}; + // ************* Resource lookup ************* template<> struct action< resource > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.loadGlobal(in.string()); // TODO: Should this be treated differently? } }; @@ -309,7 +329,7 @@ template<> struct action< plain_symbol > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.loadGlobal(in.string()); } }; @@ -319,7 +339,7 @@ template<> struct action< array_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushInlineArrayStart(); } }; @@ -328,7 +348,7 @@ template<> struct action< array_end > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushInlineArrayEnd(); } }; @@ -337,7 +357,7 @@ template<> struct action< array_comma > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.appendInlineArrayArgument(); } }; @@ -346,7 +366,7 @@ template<> struct action< array_list > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.appendInlineArrayArgument(); // Insert a fake comma } }; @@ -356,7 +376,7 @@ template<> struct action< map_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushInlineMapStart(); } }; @@ -365,7 +385,7 @@ template<> struct action< map_end > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushInlineMapEnd(); } }; @@ -374,7 +394,7 @@ template<> struct action< map_comma > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.appendInlineMapArgument(); } }; @@ -383,7 +403,7 @@ template<> struct action< map_list > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.appendInlineMapArgument(); // Insert a fake comma } }; @@ -393,7 +413,7 @@ template<> struct action< postfix_identifier > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushAttributeName(in.string()); assembler.loadAttribute(); } @@ -404,7 +424,7 @@ template<> struct action< sym_array_access_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushArrayAccessStart(); } }; @@ -413,7 +433,7 @@ template<> struct action< postfix_array_access > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushArrayAccessEnd(); } }; @@ -423,7 +443,7 @@ template<> struct action { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushFunctionStart(); } }; @@ -432,7 +452,7 @@ template<> struct action< sym_comma > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushComma(); } }; @@ -441,7 +461,7 @@ template<> struct action< argument_list > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushComma(); // Insert a fake comma } }; @@ -450,7 +470,7 @@ template<> struct action< postfix_right_paren > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.pushFunctionEnd(); } }; @@ -462,15 +482,24 @@ template<> struct action< sym_dbstart > template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { if (failState.failed) return; - assembler.pushDBGroup(); + assembler.pushDBGroup(false); } }; -template<> struct action +template<> struct action< sym_delaystart > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { if (failState.failed) return; + assembler.pushDBGroup(true); + } +}; + +template<> struct action +{ + template< typename Input > + static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { + if (failState.failed || assembler.deferred()) return; assembler.loadConstant(BC_CONSTANT_EMPTY_STRING); } }; @@ -480,7 +509,13 @@ template<> struct action< db > template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { if (failState.failed) return; - assembler.popDBGroup(); + if (assembler.popDBGroup()) { + // Copy over the entire delayed string for later evaluation. Change from #{} to ${}. + auto s = in.string(); + assert(s.at(0) == '#'); + s[0] = '$'; + assembler.addString(s); + } } }; @@ -490,7 +525,7 @@ template<> struct action< ds_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.startString(); } }; @@ -499,7 +534,7 @@ template<> struct action< ss_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.startString(); } }; @@ -508,7 +543,7 @@ template<> struct action< os_start > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.startString(); } }; @@ -517,7 +552,7 @@ template<> struct action< ss_raw > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; auto s = in.string(); if (s.length() > 0) assembler.addString(s); @@ -528,7 +563,7 @@ template<> struct action< ds_raw > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; auto s = in.string(); if (s.length() > 0) assembler.addString(s); @@ -539,7 +574,7 @@ template<> struct action< os_raw > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; auto s = in.string(); if (s.length() > 0) assembler.addString(s); @@ -551,7 +586,7 @@ template<> struct action< os_string > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.endString(); } }; @@ -560,7 +595,7 @@ template<> struct action< ss_string > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.endString(); } }; @@ -569,7 +604,7 @@ template<> struct action< ds_string > { template< typename Input > static void apply( const Input& in, fail_state& failState, ByteCodeAssembler& assembler ) { - if (failState.failed) return; + if (failState.failed || assembler.deferred()) return; assembler.endString(); } }; diff --git a/aplcore/include/apl/datagrammar/databindingstack.h b/aplcore/include/apl/datagrammar/databindingstack.h deleted file mode 100644 index d5ab05b..0000000 --- a/aplcore/include/apl/datagrammar/databindingstack.h +++ /dev/null @@ -1,369 +0,0 @@ -/* - * Copyright 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. - * - * Data-binding rules - */ - -#ifndef _APL_DATA_BINDING_STACK_H -#define _APL_DATA_BINDING_STACK_H - -#include -#include -#include "databindinggrammar.h" -#include "functions.h" -#include "node.h" -#include "apl/engine/context.h" -#include "apl/primitives/object.h" -#include "apl/utils/log.h" -#include "apl/utils/streamer.h" -#include "apl/utils/throw.h" - -namespace apl { -namespace datagrammar { - -static const bool DEBUG_STATE = false; - -// Operator precedence -enum { - OP_FIELD_OR_FUNCTION = 1, - OP_UNARY = 2, - OP_TERM = 3, - OP_EXPRESSION = 4, - OP_COMPARISON = 5, - OP_EQUALITY = 6, - OP_LOGICAL_AND = 7, - OP_LOGICAL_OR = 8, - OP_NULLC = 9, // Note: associates right-to-left - OP_TERNARY = 10, - OP_GROUP = 20, - OP_DB = 21 -}; - -using CreateFunction = Object (*)(std::vector&&); - -struct Operator -{ - int order; - CreateFunction func; - std::string name; -}; - -static const std::map sTermOperators = { - {"*", {OP_TERM, Multiply, "*"}}, - {"/", {OP_TERM, Divide, "/"}}, - {"%", {OP_TERM, Remainder, "%"}} -}; - -static const std::map sExpressionOperators = { - {"+", {OP_EXPRESSION, Add, "+"}}, - {"-", {OP_EXPRESSION, Subtract, "-"}} -}; - -static const std::map sCompareOperators = { - {"<", {OP_COMPARISON, LessThan, "<"}}, - {">", {OP_COMPARISON, GreaterThan, ">"}}, - {"<=", {OP_COMPARISON, LessEqual, "<="}}, - {">=", {OP_COMPARISON, GreaterEqual, ">="}} -}; - -static const std::map sEqualityOperators = { - {"==", {OP_EQUALITY, Equal, "=="}}, - {"!=", {OP_EQUALITY, NotEqual, "!="}} -}; - -static const std::map< char, Operator > sUnaryOperators = { - {'!', {OP_UNARY, UnaryNot, "!"}}, - {'+', {OP_UNARY, UnaryPlus, "+"}}, - {'-', {OP_UNARY, UnaryMinus, "-"}} -}; - -static const Operator FIELD_ACCESS_OPERATOR = {OP_FIELD_OR_FUNCTION, FieldAccess, "."}; -static const Operator ARRAY_ACCESS_OPERATOR = {OP_FIELD_OR_FUNCTION, ArrayAccess, "[" }; -static const Operator TERNARY_OPERATOR = { OP_TERNARY, Ternary, "?:"}; -static const Operator FUNCTION_OPERATOR = { OP_FIELD_OR_FUNCTION, FunctionCall, "function" }; -static const Operator GROUP_OPERATOR = { OP_GROUP, nullptr, "("}; -static const Operator NULLC_OPERATOR = { OP_NULLC, Nullc, "nullc"}; -static const Operator AND_OPERATOR = {OP_LOGICAL_AND, And, "and" }; -static const Operator OR_OPERATOR = {OP_LOGICAL_OR, Or, "or" }; -static const Operator DB_OPERATOR = {OP_DB, nullptr, "${"}; - -// State within parsing a single string -enum CombineType { - /** The top-level string holding the expression */ - kCombineTopString, - - /** An embedded string such as ${...' '...} */ - kCombineEmbeddedString, - - /** A comma-separated list */ - kCombineVector, - - /** A single argument - for example, an atribute inside of brackets [] */ - kCombineSingle -}; - -class Stack { -public: - Stack(int depth) : mDepth(depth) {} - - void push(const Object& object) - { - LOG_IF(DEBUG_STATE) << "Stack[" << mDepth << "].push_object " << object; - mObjects.push_back(object); - } - - void push(const Operator& op) - { - LOG_IF(DEBUG_STATE) << "Stack[" << mDepth << "].push( " << op.name << " )"; - mOps.push_back(op); - } - - void pop(const Operator& op) { - LOG_IF(DEBUG_STATE) << "Stack[" << mDepth << "].pop(" << op.name << " ) " << toString(); - assert(!mOps.empty()); - assert(mOps.back().order == op.order); - mOps.pop_back(); - } - - double popNumber() { - assert(!mObjects.empty()); - auto result = mObjects.back().getDouble(); - mObjects.pop_back(); - return result; - } - - // Reduce left-to-right a series of binary operations - void reduceLR(int order) - { - // Search from the back to the starting position with the first operator to combine - auto backIter = mOps.rbegin(); - while ( backIter != mOps.rend() && backIter->order == order) - backIter++; - - auto opIter = backIter.base(); // Points to the first valid operator - auto objectIter = mObjects.end() - (mOps.end() - opIter + 1); // Points to the starting object - - while (opIter != mOps.end()) { - LOG_IF(DEBUG_STATE) << "Reducing " << opIter->name; - auto node = opIter->func(std::vector(objectIter, objectIter+2)); - *objectIter = node; - mObjects.erase( objectIter + 1, objectIter + 2); - opIter = mOps.erase(opIter); - } - } - - // Reduce a unary operation. Return true if we found a unary operation to reduce - bool reduceUnary(int order) { - auto back = mOps.rbegin(); - if (back == mOps.rend() || back->order != order) - return false; - - auto node = back->func(std::vector(mObjects.end() - 1, mObjects.end())); - mObjects.pop_back(); - mObjects.emplace_back(std::move(node)); - mOps.pop_back(); - return true; - } - - // Reduce a single binary operation at the end - void reduceBinary(int order) { - auto back = mOps.rbegin(); - if (back == mOps.rend() || back->order != order) - return; - - assert(mObjects.size() >= 2); - auto node = back->func(std::vector(mObjects.end() - 2, mObjects.end())); - mObjects.pop_back(); - mObjects.pop_back(); - mOps.pop_back(); - mObjects.emplace_back(std::move(node)); - } - - // Reduce a ternary operation at the end - void reduceTernary(int order) { - auto back = mOps.rbegin(); - if (back == mOps.rend() || back->order != order) - return; - - back++; - assert(back != mOps.rend() && back->order == order); - auto node = back->func(std::vector(mObjects.end() - 3, mObjects.end())); - mObjects.erase(mObjects.end() - 3, mObjects.end()); - mOps.erase(mOps.end() - 2, mOps.end()); - mObjects.emplace_back(std::move(node)); - } - - Object combine(CombineType combineType) - { - LOG_IF(DEBUG_STATE) << "[" << mDepth << "] Stack.combine"; - - switch (combineType) { - case kCombineEmbeddedString: - // If there's nothing, we started with an empty string - if (mObjects.empty()) - return Object(""); - - if (mObjects.size() == 1) - return mObjects.back(); - - return Combine(std::vector(mObjects.begin(), mObjects.end())); - - case kCombineTopString: - // If there's nothing, we started with an empty string - if (mObjects.empty()) - return Object(""); - - if (mObjects.size() == 1) - return mObjects.back(); - - return Combine(std::vector(mObjects.begin(), mObjects.end())); - - case kCombineVector: - // TODO: Do we have move assignment? - return Object(std::make_shared >(mObjects)); - - case kCombineSingle: - assert(mObjects.size() == 1); - return mObjects.back(); - } - - aplThrow("Illegal combination"); - } - - void dump() - { - printf(" stack %s", toString().c_str()); - } - - std::string toString() { - streamer buf; - - buf << "["; - auto it = mOps.begin(); - if (it != mOps.end()) { - buf << it->name; - it++; - } - - while (it != mOps.end()) { - buf << "," << it->name; - it++; - } - - buf << "]["; - auto it2 = mObjects.begin(); - if (it2 != mObjects.end()) { - buf << *it2; - it2++; - } - - while (it2 != mObjects.end()) { - buf << "," << *it2; - it2++; - } - - buf << "]"; - return buf.str(); - } - -private: - int mDepth; - std::vector mObjects; - std::vector mOps; -}; - -class Stacks -{ -public: - // Start with an initial stack that is handling the outer string context - Stacks(const Context& context) : mContext(context) { - open(); - } - - // Call this when you start processing a new string region or list of arguments - void open() - { - LOG_IF(DEBUG_STATE) << "Stacks.open"; - mStack.emplace_back(Stack(mStack.size() + 1)); - } - - // Call this when you stop processing a region (string, parenthesis, arglist) - void close(CombineType combineType) - { - LOG_IF(DEBUG_STATE) << "Stacks.close"; - auto object = mStack.back().combine(combineType); - mStack.pop_back(); - mStack.back().push(object); - } - - // TODO: Change this to emplace_back - void push(const Object& object) { mStack.back().push(object); } - void push(const Operator& op) { mStack.back().push(op); } - void pop(const Operator& op) { mStack.back().pop(op); } - - double popNumber() { return mStack.back().popNumber(); } - - /** - * Reduce any number of operators with the same order, following a left-to-right - * strategy. For example, "1 - 3 + 4 - 5" will be resolved as (((1-3)+4)-5). - * @param order The operator order (see the operator precedence enumeration) - */ - void reduceLR(int order) { mStack.back().reduceLR(order); } - - /** - * Reduce any number of unary operators with the given order. If the top operator - * on the stack does not match "order", this method does nothing. - * @param order The order of the operator - */ - void reduceUnary(int order) { while (mStack.back().reduceUnary(order)) ; } - - /** - * Reduce a single binary operator with the given order. If the top operator - * on the stack does not match "order", this method does nothing. - * @param order The order of the operator - */ - void reduceBinary(int order) { mStack.back().reduceBinary(order); } - - /** - * Reduce a single ternary operator with the given order. If the top operator - * on the stack does not match "order", this method does nothing. If the - * top TWO operators on the stack don't match "order", we throw an exception. - * @param order The order of the operator. - */ - void reduceTernary(int order) { mStack.back().reduceTernary(order); } - - Object finish() - { - LOG_IF(DEBUG_STATE) << "Stacks.finish"; - assert(mStack.size() == 1); - return mStack.back().combine(kCombineTopString); - } - - void dump() - { - LOG(LogLevel::kDebug).session(mContext) << "Stacks=" << mStack.size(); - for (auto& m : mStack) - m.dump(); - } - - const Context& context() const { return mContext; } - -private: - std::vector mStack; - const Context& mContext; -}; -} // namespace datagrammar -} // namespace apl - -#endif // _APL_DATA_BINDING_STACK_H \ No newline at end of file diff --git a/aplcore/include/apl/datasource/datasource.h b/aplcore/include/apl/datasource/datasource.h index e91b148..abb8166 100644 --- a/aplcore/include/apl/datasource/datasource.h +++ b/aplcore/include/apl/datasource/datasource.h @@ -31,10 +31,12 @@ class DataSource : public LiveArrayObject { /** * Create DataSource object from provided object. * @param context data binding context. + * @param provider DDS provider to use. * @param object object to create from. * @return DataSource object or NULL_OBJECT if failed. */ - static Object create(const ContextPtr& context, const Object& object, const std::string& name); + static Object create(const ContextPtr& context, const DataSourceProviderPtr& provider, + const Object& object, const std::string& name); /// LiveArrayObject override void ensure(size_t idx) override; diff --git a/aplcore/include/apl/datasource/datasourceprovider.h b/aplcore/include/apl/datasource/datasourceprovider.h index facb8ed..4a2d3da 100644 --- a/aplcore/include/apl/datasource/datasourceprovider.h +++ b/aplcore/include/apl/datasource/datasourceprovider.h @@ -24,6 +24,17 @@ namespace apl { +/** + * Document errors structure. + * @param documentContext document context weak pointer. + * @param Object of error + * @deprecated Providers are per document. + */ +struct DocumentError { + DocumentContextWeakPtr documentContext; + Object error; +}; + class DataSourceConnection; class DataSourceProvider : public NonCopyable { @@ -42,6 +53,11 @@ class DataSourceProvider : public NonCopyable { std::weak_ptr context, std::weak_ptr liveArray) = 0; + /** + * @return DataSource type name. + */ + virtual std::string getType() const { return ""; } + /** * Parse update payload and pass it to relevant connection. * @param payload update payload. @@ -55,6 +71,15 @@ class DataSourceProvider : public NonCopyable { * */ virtual Object getPendingErrors() { return Object::EMPTY_ARRAY(); } + + /** + * Retrieve any errors pending. + * @return vector of errors. + * @deprecated Providers are per document + */ + APL_DEPRECATED virtual std::vector getPendingDocumentErrors() { + return {}; + } }; } // namespace apl diff --git a/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h b/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h index 21973b6..4e9705d 100644 --- a/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h +++ b/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h @@ -115,7 +115,7 @@ class DynamicListDataSourceConnection : public OffsetIndexDataSourceConnection, void clearTimeouts(const ContextPtr& context, const std::string& correlationToken); timeout_id scheduleUpdateExpiry(int version); void reportUpdateExpired(int version); - void constructAndReportError(const SessionPtr& session, const std::string& reason, const Object& operationIndex, + void constructAndReportError(const ContextPtr& context, const std::string& reason, const Object& operationIndex, const std::string& message); std::weak_ptr mContext; @@ -167,25 +167,26 @@ class DynamicListDataSourceProvider : public DataSourceProvider { std::shared_ptr create(const Object& sourceDefinition, std::weak_ptr context, std::weak_ptr liveArray) override; bool processUpdate(const Object& payload) override; + std::string getType() const override { return mConfiguration.type; } protected: DLConnectionPtr getConnection(const std::string& listId); DLConnectionPtr getConnection(const std::string& listId, const Object& correlationToken); void constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const std::string& listId, const Object& listVersion, const Object& operationIndex, const std::string& message); void constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const std::string& listId, const std::string& message); void constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const DLConnectionPtr& connection, const Object& operationIndex, diff --git a/aplcore/include/apl/document/contextdata.h b/aplcore/include/apl/document/contextdata.h new file mode 100644 index 0000000..03ebd6c --- /dev/null +++ b/aplcore/include/apl/document/contextdata.h @@ -0,0 +1,90 @@ +/** + * Copyright 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 _APL_CONTEXT_DATA_H +#define _APL_CONTEXT_DATA_H + +#include "apl/common.h" +#include "apl/component/componentproperties.h" +#include "apl/content/metrics.h" +#include "apl/content/rootconfig.h" +#include "apl/engine/runtimestate.h" +#include "apl/primitives/size.h" +#include "apl/utils/counter.h" + +namespace apl { + +class ContextData : public Counter { +public: + explicit ContextData(const RootConfig& config, + RuntimeState&& runtimeState, + const SettingsPtr& settings, + const std::string& lang, + LayoutDirection layoutDirection) + : mConfig(config), + mRuntimeState(std::move(runtimeState)), + mSettings(settings), + mLang(lang), + mLayoutDirection(layoutDirection) + {} + + std::string getRequestedAPLVersion() const { return mRuntimeState.getRequestedAPLVersion(); } + + const RootConfig& rootConfig() const { return mConfig; } + + ContextData& lang(const std::string& lang) { + mLang = lang; + return *this; + } + + ContextData& layoutDirection(LayoutDirection layoutDirection) { + mLayoutDirection = layoutDirection; + return *this; + } + + std::string getLang() const { return mLang; } + LayoutDirection getLayoutDirection() const { return mLayoutDirection; } + bool getReinflationFlag() const { return mRuntimeState.getReinflation(); } + virtual std::string getTheme() const { return mRuntimeState.getTheme(); } + + /** + * @return true if represents full data binding context, false otherwise. + */ + virtual bool fullContext() const { return false; } + + /** + * @return Console log session owned by this context. + */ + virtual const SessionPtr& session() const = 0; + + /** + * @return true if context is in embedded document, false otherwise. + */ + virtual bool embedded() const = 0; + + virtual ~ContextData() = default; + +protected: + const RootConfig mConfig; + RuntimeState mRuntimeState; + SettingsPtr mSettings; + std::string mLang; + LayoutDirection mLayoutDirection = LayoutDirection::kLayoutDirectionInherit; +}; + + +} // namespace apl + +#endif //_APL_CONTEXT_DATA_H diff --git a/aplcore/include/apl/document/coredocumentcontext.h b/aplcore/include/apl/document/coredocumentcontext.h new file mode 100644 index 0000000..f4917f0 --- /dev/null +++ b/aplcore/include/apl/document/coredocumentcontext.h @@ -0,0 +1,294 @@ +/** + * Copyright 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 _APL_CORE_DOCUMENT_CONTEXT_H +#define _APL_CORE_DOCUMENT_CONTEXT_H + +#include "apl/content/configurationchange.h" +#include "apl/content/documentconfig.h" +#include "apl/document/displaystate.h" +#include "apl/document/documentcontext.h" +#include "apl/document/documentcontextdata.h" + +#ifdef ALEXAEXTENSIONS +#include "apl/extension/extensionmediator.h" +#endif + +namespace apl { + +class EventManager; +class Metrics; +class RootConfig; +class Sequencer; +struct PointerEvent; + +using LayoutCallbackFunc = std::function; + +/** + * Core implementation of rendered document API. + */ +class CoreDocumentContext : public DocumentContext, public std::enable_shared_from_this { +public: + /** + * Construct a document context. + * @param shared Pointer to the SharedContextData. + * @param metrics Display metrics + * @param content Content to display + * @param config Configuration information + * @return A pointer to the document context. + */ + static CoreDocumentContextPtr create( + const SharedContextDataPtr& shared, + const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config); + + /** + * Create a DocumentContext for the given Content with the given environment and size. + * + * @param context The Context in which the DocumentContext is created; ...not the Context of the DocumentContext + * @param content Represents the document rendered within the created DocumentContext + * @param env May contain overrides for context to be applied within the created DocumentContext + * @param size Specifies the height and width in display-independent pixels of the DocumentContext + * @param documentConfig Document configuration + * @return The DocumentContext + */ + static CoreDocumentContextPtr create(const ContextPtr& context, + const ContentPtr& content, + const Object& env, + const Size& size, + const DocumentConfigPtr& documentConfig); + + /** + * Notify core of a configuration change. Internally this method will trigger the "onConfigChange" + * event handler in the APL document. A common behavior in the onConfigChange event handler is to + * send a Reinflate (kEventTypeReinflate) event. + * @see RootContext::configurationChange + * + * @param change Configuration change information + */ + void configurationChange(const ConfigurationChange& change); + + /** + * Update the display state of the document. Internally this method will trigger the + * "onDisplayStateChange" event handler in the APL document, if the display state changed. + * @see RootContext::updateDisplayState + * + * @param displayState The new display state + */ + void updateDisplayState(DisplayState displayState); + + /** + * Reinflate this context using the internally cached configuration changes. This will terminate any + * existing animations, remove any events on the queue, clear the dirty components, and create a new + * component hierarchy. After calling this method the view host should rebuild its visual hierarchy. + * @see RootContext::reinflate + * @param layoutCallback Callback executed when reinflation process finished. + * + * @return true if successful, false otherwise. + */ + bool reinflate(const LayoutCallbackFunc& layoutCallback); + + /** + * Trigger a resize based on stored configuration changes. + * @see RootContext::resize + */ + void resize(); + + /** + * Clear any pending onMount handlers and extension handlers. + */ + void clearPending() const; + + /** + * Public constructor. Use the ::create method instead. + * @param content Processed APL content data + * @param config Configuration information + */ + CoreDocumentContext( + const ContentPtr& content, + const RootConfig& config); + + ~CoreDocumentContext() override; + + /** + * @return The top-level context. + */ + Context& context() const { return *mContext; } + + /** + * @return The top-level context as a shared pointer + */ + ContextPtr contextPtr() const { return mContext; } + + /** + * @return The top-level component for this document + */ + ComponentPtr topComponent(); + + /** + * @return The top-level context with payload binding. This context is used when executing document-level + * commands. + */ + ContextPtr payloadContext() const; + + /** + * Invoke an extension event handler. + * @param uri The URI of the custom document handler + * @param name The name of the handler to invoke + * @param data The data to associate with the handler + * @param fastMode If true, this handler will be invoked in fast mode + * @param resourceId handle associated with extension component if present + * @return An ActionPtr + */ + ActionPtr invokeExtensionEventHandler(const std::string& uri, const std::string& name, + const ObjectMap& data, bool fastMode, + std::string resourceId = ""); + + /** + * Update context time-related variables. + * @param elapsedTime The time to move forward to + * @param utcTime The current UTC time on your system + */ + void updateTime(apl_time_t utcTime, apl_duration_t localTimeAdjustment); + + /** + * @return current time. + */ + apl_time_t currentTime(); + + /** + * @return the RootConfig used to initialize this context. + */ + const RootConfig& rootConfig(); + + /** + * @return The content + */ + const ContentPtr& content() const { return mContent; } + + /** + * Create a suitable document-level data-binding context for evaluating a document-level + * event. + * @param handler The name of the handler. + * @param optional optional data to add to the event. + * @return The document-level data-binding context. + */ + ContextPtr createDocumentContext(const std::string& handler, const ObjectMap& optional = {}); + + /** + * Create a suitable document-level data-binding context for evaluating a document-level + * keyboard event. + * @param handler The name of the handler. + * @param keyboard The keyboard event. + * @return The document-level data-binding context. + */ + ContextPtr createKeyEventContext(const std::string& handler, const ObjectMapPtr& keyboard); + + /** + * @return The current logging session + */ + const SessionPtr& getSession() const; + + /** + * @return The root configuration used to create this context. + */ + const RootConfig& getRootConfig() const; + + /** + * @return The current theme + */ + std::string getTheme() const; + + /** + * @return Text measurement pointer reference + */ + const TextMeasurementPtr& measure() const; + + /** + * Find a component somewhere in the DOM with the given id or uniqueId. + * @param id The id or uniqueID to search for. + * @return The component or nullptr if it is not found. + */ + ComponentPtr findComponentById(const std::string& id) const; + + /** + * Find a UID object + * @param uid The uniqueId to search for. + * @return The object or nullptr if it is not found. + */ + UIDObject* findByUniqueId(const std::string& uid) const; + + /** + * @return true if corresponds to embedded document, false otherwise. + */ + bool isEmbedded() const { return mCore->embedded(); } + + friend streamer& operator<<(streamer& os, const CoreDocumentContext& root); + + bool setup(const CoreComponentPtr& top); + + void processOnMounts(); + + void flushDataUpdates(); + + /** + * @param documentContext Pointer to cast. + * @return Casted pointer to this type + */ + static CoreDocumentContextPtr cast(const DocumentContextPtr& documentContext); + + /// DocumentContext overrides + bool isVisualContextDirty() const override; + void clearVisualContextDirty() override; + rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) override; + bool isDataSourceContextDirty() const override; + void clearDataSourceContextDirty() override; + rapidjson::Value serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) override; + rapidjson::Value serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) override; + rapidjson::Value serializeContext(rapidjson::Document::AllocatorType& allocator) override; + ActionPtr executeCommands(const Object& commands, bool fastMode) override; + +private: + #ifdef ALEXAEXTENSIONS + friend class ExtensionMediator; + #endif + + void init(const Metrics& metrics, + const RootConfig& config, + const SharedContextDataPtr& sharedData, + bool reinflation); + + bool verifyAPLVersionCompatibility(const std::vector>& ordered, + const APLVersion& compatibilityVersion); + bool verifyTypeField(const std::vector>& ordered, bool enforce); + ObjectMapPtr createDocumentEventProperties(const std::string& handler) const; + +private: + friend class CoreRootContext; + + ContentPtr mContent; + ContextPtr mContext; + DocumentContextDataPtr mCore; // When you die, make sure to tell the data to terminate itself. + ConfigurationChange mActiveConfigurationChanges; + DisplayState mDisplayState; + + apl_time_t mUTCTime; + apl_duration_t mLocalTimeAdjustment; +}; + +} // namespace apl + +#endif //_APL_CORE_DOCUMENT_CONTEXT_H diff --git a/aplcore/include/apl/document/documentcontext.h b/aplcore/include/apl/document/documentcontext.h new file mode 100644 index 0000000..f5afb56 --- /dev/null +++ b/aplcore/include/apl/document/documentcontext.h @@ -0,0 +1,96 @@ +/** + * Copyright 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 _APL_DOCUMENT_CONTEXT_H +#define _APL_DOCUMENT_CONTEXT_H + +#include "apl/common.h" +#include "apl/primitives/object.h" +#include "apl/utils/counter.h" +#include "apl/utils/noncopyable.h" + +namespace apl { + +/** + * Representation of the rendered document interface. + */ +class DocumentContext : public NonCopyable, public Counter { +public: + virtual ~DocumentContext() = default; + + /** + * Identifies when the visual context may have changed. A call to serializeVisualContext resets this value to false. + * @return true if the visual context has changed since the last call to serializeVisualContext, false otherwise. + */ + virtual bool isVisualContextDirty() const = 0; + + /** + * Clear the visual context dirty flag + */ + virtual void clearVisualContextDirty() = 0; + + /** + * Retrieve component's visual context as a JSON object. This method also clears the + * visual context dirty flag + * @param allocator Rapidjson allocator + * @return The serialized visual context + */ + virtual rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) = 0; + + /** + * Identifies when the datasource context may have changed. A call to serializeDatasourceContext resets this value to false. + * @return true if the datasource context has changed since the last call to serializeDatasourceContext, false otherwise. + */ + virtual bool isDataSourceContextDirty() const = 0; + + /** + * Clear the datasource context dirty flag + */ + virtual void clearDataSourceContextDirty() = 0; + + /** + * Retrieve datasource context as a JSON array object. This method also clears the + * datasource context dirty flag + * @param allocator Rapidjson allocator + * @return The serialized datasource context + */ + virtual rapidjson::Value serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) = 0; + + /** + * Serialize a complete version of the DOM + * @param extended If true, serialize everything. If false, just serialize external data + * @param allocator Rapidjson allocator + * @return The serialized DOM + */ + virtual rapidjson::Value serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) = 0; + + /** + * Serialize the global values for developer tools + * @param allocator Rapidjson allocator + * @return The serialized global values + */ + virtual rapidjson::Value serializeContext(rapidjson::Document::AllocatorType& allocator) = 0; + + /** + * Execute an externally-driven command + * @param commands The commands to execute + * @param fastMode If true this handler will be invoked in fast mode + */ + virtual ActionPtr executeCommands(const Object& commands, bool fastMode) = 0; +}; + +} // namespace apl + +#endif //_APL_DOCUMENT_CONTEXT_H diff --git a/aplcore/include/apl/document/documentcontextdata.h b/aplcore/include/apl/document/documentcontextdata.h new file mode 100644 index 0000000..1688088 --- /dev/null +++ b/aplcore/include/apl/document/documentcontextdata.h @@ -0,0 +1,199 @@ +/** + * Copyright 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 _APL_DOCUMENT_CONTEXT_DATA_H +#define _APL_DOCUMENT_CONTEXT_DATA_H + +#include "apl/document/contextdata.h" + +#include +#include + +#include "apl/common.h" +#include "apl/content/extensionrequest.h" +#include "apl/engine/eventmanager.h" +#include "apl/engine/jsonresource.h" +#include "apl/primitives/textmeasurerequest.h" +#include "apl/utils/lrucache.h" + +namespace apl { + +class ExtensionManager; +class FocusManager; +class HoverManager; +class LayoutManager; +class LiveDataManager; +class Sequencer; +class Styles; +class UIDManager; +class DataSourceConnection; +using DataSourceConnectionPtr = std::shared_ptr; + +/** + * Data contained in the rendered document. + */ +class DocumentContextData : public ContextData, public std::enable_shared_from_this { + friend class CoreDocumentContext; + +public: + /** + * Stock constructor + * @param document Document this data belongs to. + * @param metrics Display metrics + * @param config Configuration settings + * @param runtimeState Runtime state information (theme, required APL version, re-inflation state) + * @param settings Document settings + * @param session Session information for logging messages and warnings + * @param extensions List of extension requests + * @param sharedContext Shared data. + */ + DocumentContextData(const DocumentContextPtr& document, + const Metrics& metrics, + const RootConfig& config, + RuntimeState runtimeState, + const SettingsPtr& settings, + const SessionPtr& session, + const std::vector& extensions, + const SharedContextDataPtr& sharedContext); + + ~DocumentContextData() override; + + bool fullContext() const override { return true; } + + /** + * Halt the RootContextData and release the component hierarchy.. + */ + void terminate(); + + /** + * This root context data is being replaced by a new one. Terminate all processing + * and return the top component. To release memory, you must call release on the top + * component after you are done with it. Once halted the RootContextData cannot be + * restarted. + */ + CoreComponentPtr halt(); + + std::shared_ptr styles() const { return mStyles; } + LiveDataManager& dataManager() const { return *mDataManager; } + ExtensionManager& extensionManager() const { return *mExtensionManager; } + CoreComponentPtr top() const { return mTop; } + const std::map& layouts() const { return mLayouts; } + const std::map& commands() const { return mCommands; } + const std::map& graphics() const { return mGraphics; } + + Sequencer& sequencer() const { return *mSequencer; } + FocusManager& focusManager() const; + HoverManager& hoverManager() const; + LayoutManager& layoutManager() const; + MediaManager& mediaManager() const; + MediaPlayerFactory& mediaPlayerFactory() const; + UIDManager& uniqueIdManager() const { return *mUniqueIdManager; } + DependantManager& dependantManager() const; + + const YGConfigRef& ygconfig() const; + const SessionPtr& session() const override { return mSession; } + + /** + * @return The installed text measurement for this context. + */ + const TextMeasurementPtr& measure() const; + + const Metrics& metrics() const { return mMetrics; } + + /** + * Acquire the screen lock + */ + void takeScreenLock(); + + /** + * Release the screen lock + */ + void releaseScreenLock(); + + /** + * @return internal text measurement cache. + */ + LruCache& cachedMeasures(); + + /** + * @return internal text measurement baseline cache. + */ + LruCache& cachedBaselines(); + + /** + * @return List of pending onMount handlers for recently inflated components. + */ + WeakPtrSet& pendingOnMounts() { return mPendingOnMounts; } + + /** + * @return Parent DocumentContext. + */ + DocumentContextPtr documentContext() const { return mDocument.lock(); } + +#ifdef SCENEGRAPH + /** + * @return A cache of TextProperties + */ + sg::TextPropertiesCache& textPropertiesCache(); +#endif // SCENEGRAPH + + double getWidth() const { return mMetrics.getWidth(); } + double getHeight() const { return mMetrics.getHeight(); } + double getPxToDp() const { return Metrics::CORE_DPI / mMetrics.getDpi(); } + + ScreenShape getScreenShape() { return mMetrics.getScreenShape(); } + int getDpi() const { return mMetrics.getDpi(); } + ViewportMode getViewportMode() const { return mMetrics.getViewportMode(); } + + SharedContextDataPtr getShared() const { return mSharedData; } + + bool embedded() const override; + void pushEvent(Event&& event); + void setDirty(const ComponentPtr& component); + void clearDirty(const ComponentPtr& component); + + std::set& getDirtyVisualContext() { return mDirtyVisualContext; } + std::set& getDirtyDatasourceContext() { return mDirtyDatasourceContext; } + +#ifdef ALEXAEXTENSIONS + std::queue& getExtensionEvents() { return mExtensionEvents; } +#endif + +private: + SharedContextDataPtr mSharedData; + std::weak_ptr mDocument; + Metrics mMetrics; + std::map mLayouts; + std::map mCommands; + std::map mGraphics; + std::shared_ptr mStyles; + std::unique_ptr mSequencer; + std::unique_ptr mDataManager; + std::unique_ptr mExtensionManager; + std::unique_ptr mUniqueIdManager; + CoreComponentPtr mTop; + SessionPtr mSession; + WeakPtrSet mPendingOnMounts; + std::set mDirtyVisualContext; + std::set mDirtyDatasourceContext; +#ifdef ALEXAEXTENSIONS + std::queue mExtensionEvents; +#endif +}; + + +} // namespace apl + +#endif //_APL_DOCUMENT_CONTEXT_DATA_H diff --git a/aplcore/include/apl/embed/documentmanager.h b/aplcore/include/apl/embed/documentmanager.h new file mode 100644 index 0000000..e44d627 --- /dev/null +++ b/aplcore/include/apl/embed/documentmanager.h @@ -0,0 +1,91 @@ +/** +* Copyright 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 _APL_DOCUMENT_MANAGER_H +#define _APL_DOCUMENT_MANAGER_H + +#include +#include +#include + +#include "apl/common.h" +#include "apl/content/documentconfig.h" +#include "apl/embed/embedrequest.h" + +namespace apl { + +/** + * Successful response structure. + * @param request The request identifying the resolved Content + * @param content The requested Content + * @param connectedVisualContext True for cases when the embedded document's visual context should + * @param documentConfig Configuration for the embedded document + * be stitched into the parent document's visual context. + */ +struct EmbeddedRequestSuccessResponse { + EmbedRequestPtr request; + ContentPtr content; + bool connectedVisualContext; + DocumentConfigPtr documentConfig; +}; + +using EmbedRequestSuccessCallback = std::function; + +/** + * Failed response structure. + * @param request The request that could not be resolved + * @param failure A failure message + */ +struct EmbeddedRequestFailureResponse { + EmbedRequestPtr request; + std::string failure; +}; + +using EmbedRequestFailureCallback = std::function; + +/** + * DocumentManager facilitates embedding APL Documents within other APL Documents by enabling the + * "hosting" RootContext to request APL Document Content via URL. + */ +class DocumentManager { +public: + virtual ~DocumentManager() = default; + + /** + * Request to resolve the given URL to APL Document Content. Once resolved, exactly one of + * success or failure must be invoked, informing the requester of the outcome. If the same Content + * is requested by multiple callers, resolving that request must result in invoking one of success + * or failure for each requester, i.e., invoke each of the success callbacks for each requester, + * or invoke each of the failure callbacks for each requester. + * + * "success" may be invoked prior to the requested Content being "ready" iff the Content has + * one or more pending parameters. + * + * If request is expired then the request is considered cancelled; neither success nor error + * will be invoked once a request is cancelled. + * + * @param request The request identifying the Content to resolve. + * @param success The callback to invoke when the requested Content is available. + * @param failure The callback to invoke if the EmbedRequest will never be available. + */ + virtual void request( + const std::weak_ptr& request, + EmbedRequestSuccessCallback success, + EmbedRequestFailureCallback error) = 0; +}; + +} // namespace apl + +#endif // _APL_DOCUMENT_MANAGER_H diff --git a/aplcore/include/apl/embed/documentregistrar.h b/aplcore/include/apl/embed/documentregistrar.h new file mode 100644 index 0000000..f137756 --- /dev/null +++ b/aplcore/include/apl/embed/documentregistrar.h @@ -0,0 +1,87 @@ +/** +* Copyright 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 APL_DOCUMENT_REGISTRY_H +#define APL_DOCUMENT_REGISTRY_H + +#include +#include +#include + +#include "apl/common.h" +#include "apl/utils/noncopyable.h" + +namespace apl { + +/** + * Allows registration and de-registration of documents. + */ +class DocumentRegistrar final : public NonCopyable +{ +public: + ~DocumentRegistrar() = default; + + /** + * Retrieve the document associated with id in the registry. + * + * @param id identifies the document to retrieve + * @return the document if registered, and nullptr otherwise + */ + CoreDocumentContextPtr get(int id) const; + + /** + * @return All documents in the registrar. + */ + const std::map& list() const; + + /** + * Apply a function to every registered document. + * @param func unary function object + */ + template + Function forEach(Function func) { + std::for_each( + mDocumentMap.begin(), + mDocumentMap.end(), + [func](const std::pair& document) { + return func(document.second); + }); + return func; + } + + /** + * Add document to this registry. + * + * @param document the document to register + * @return unique document ID + */ + int registerDocument(const CoreDocumentContextPtr& document); + + /** + * Removes the document identified by id from this registry. + * + * @param id identifies the document to deregister + */ + void deregisterDocument(int id); + +private: + std::map mDocumentMap; + + int mIdGenerator = 1000; +}; + +} // namespace apl + +#endif // APL_DOCUMENT_REGISTRY_H diff --git a/aplcore/include/apl/embed/embedrequest.h b/aplcore/include/apl/embed/embedrequest.h new file mode 100644 index 0000000..e0c17ca --- /dev/null +++ b/aplcore/include/apl/embed/embedrequest.h @@ -0,0 +1,47 @@ +/** +* Copyright 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 _APL_EMBED_REQUEST_H +#define _APL_EMBED_REQUEST_H + +#include + +#include "apl/common.h" +#include "apl/primitives/urlrequest.h" + +namespace apl { + +/** + * EmbedRequest tracks a request made to resolve a URL to APL Document Content. + */ +class EmbedRequest final { +public: + + static EmbedRequestPtr create(URLRequest url, const DocumentContextPtr& origin); + + EmbedRequest(URLRequest url, const DocumentContextPtr& origin); + + const URLRequest& getUrlRequest() const; + + DocumentContextPtr getOrigin() const; + +private: + const URLRequest mUrl; + std::weak_ptr mOrigin; +}; + +} // namespace apl + +#endif // _APL_EMBED_REQUEST_H diff --git a/aplcore/include/apl/engine/componentdependant.h b/aplcore/include/apl/engine/componentdependant.h deleted file mode 100644 index 9e88b88..0000000 --- a/aplcore/include/apl/engine/componentdependant.h +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 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 _APL_COMPONENT_DEPENDANT_H -#define _APL_COMPONENT_DEPENDANT_H - -#include - -#include "apl/common.h" -#include "apl/engine/dependant.h" -#include "apl/component/componentproperties.h" - -namespace apl { - -class Context; -class CoreComponent; - -/** - * A dependant relationship where a change in the upstream context results in a change in - * a downstream component property. - * - * The downstream component stores the data-binding expression that will be recalculated, so - * all this object has to do is inform the downstream component that the specific property - * should be recalculated. - */ -class ComponentDependant : public Dependant { -public: - /** - * Construct a downstream component dependency - * @param downstreamComponent The downstream or target component - * @param downstreamKey The property that will be modified - * @param equation The expression which will be evaluated to recalculate downstream. - * @param bindingContext The context where the equation will be bound - * @param bindingFunction The binding function that will be applied after evaluating the equation - */ - static void create(const CoreComponentPtr& downstreamComponent, - PropertyKey downstreamKey, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction); - - /** - * Internal constructor: do not call. Use ComponentDependant::create instead. - * - * @param downstreamComponent The downstream or target component - * @param downstreamKey The property that will be modified - * @param equation The expression which will be evaluated to recalculate downstream. - * @param bindingContext The context where the equation will be bound - * @param bindingFunction The binding function that will be applied after evaluating the equation - */ - ComponentDependant(const CoreComponentPtr& downstreamComponent, - PropertyKey downstreamKey, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction) - : Dependant(equation, bindingContext, bindingFunction), - mDownstreamComponent(downstreamComponent), - mDownstreamKey(downstreamKey) - {} - - void recalculate(bool useDirtyFlag) const override; - -private: - std::weak_ptr mDownstreamComponent; - PropertyKey mDownstreamKey; -}; - - -} // namespace apl - -#endif //_APL_COMPONENT_DEPENDANT_H diff --git a/aplcore/include/apl/engine/context.h b/aplcore/include/apl/engine/context.h index 0d5e7b3..eacf397 100644 --- a/aplcore/include/apl/engine/context.h +++ b/aplcore/include/apl/engine/context.h @@ -16,15 +16,11 @@ #ifndef _APL_CONTEXT_H #define _APL_CONTEXT_H -#include -#include -#include -#include #include #include "apl/common.h" - #include "apl/component/componentproperties.h" +#include "apl/content/metrics.h" #include "apl/engine/contextobject.h" #include "apl/engine/jsonresource.h" #include "apl/engine/recalculatesource.h" @@ -44,21 +40,19 @@ namespace apl { -class Metrics; -class Styles; -class RootContextData; -class State; +class DataSourceConnection; class Event; -class Sequencer; class RootConfig; -class DataSourceConnection; +class Sequencer; +class State; +class Styles; +class ExtensionManager; class FocusManager; class HoverManager; class KeyboardManager; -class LiveDataManager; -class ExtensionManager; class LayoutManager; +class LiveDataManager; class UIDManager; using DataSourceConnectionPtr = std::shared_ptr; @@ -99,30 +93,49 @@ class Context : public RecalculateTarget, */ static ContextPtr createTestContext(const Metrics& metrics, const RootConfig& config); + /** + * Create a top-level context for testing. Do not use this for production. + * @param metrics Display metrics. + * @param config Root configuration + * @param session The logging session + * @return The context + */ + static ContextPtr createTestContext(const Metrics& metrics, const RootConfig& config, const SessionPtr& session); + /** * Create a top-level context for the document background extraction. * @param metrics Display metrics. * @param config Root configuration * @param theme Theme + * @param session Session * @return The context */ - static ContextPtr createBackgroundEvaluationContext(const Metrics& metrics, const RootConfig& config, const std::string& theme); + static ContextPtr createBackgroundEvaluationContext( + const Metrics& metrics, + const RootConfig& config, + const std::string& aplVersion, + const std::string& theme, + const SessionPtr& session); /** * Create a top-level context for extension definition. * @param config Root configuration + * @param session Session * @return The context */ - static ContextPtr createTypeEvaluationContext(const RootConfig& config); + static ContextPtr createTypeEvaluationContext( + const RootConfig& config, + const std::string& aplVersion, + const SessionPtr& session); /** * Create a top-level context. Only used by RootContext * @param metrics Display metrics - * @param core Internal core data. + * @param core Internal document data. * @return The context. */ static ContextPtr createRootEvaluationContext(const Metrics& metrics, - const std::shared_ptr& core); + const ContextDataPtr& core); /** * Create a "clean" context. This shares the same root data, but does not contain any @@ -143,19 +156,9 @@ class Context : public RecalculateTarget, /** * Construct a free-standing context. Do not call this directly; use the ::create* method instead * @param metrics The display metrics. - * @param core A pointer to the common core data. + * @param core A pointer to the document data. */ - Context(const Metrics& metrics, const std::shared_ptr& core); - - /** - * Construct a free-standing context with simulated runtime state and document parameters. It should - * only be used for context or type evaluation not in data binding context hierarchy. Do not call - * this directly; use the ::create* method instead - * @param metrics Display metrics. - * @param config Root configuration - * @param theme Theme - */ - Context(const Metrics& metrics, const RootConfig& config, const std::string& theme); + Context(const Metrics& metrics, const ContextDataPtr& core); /** * Standard destructor @@ -270,24 +273,13 @@ class Context : public RecalculateTarget, return cr.context(); } - /** - * Propagate a changed value in the context. This can only be called if the value already exists. Updating - * a value will also cause all dependants of this value to be updated. This method should only be called - * by an upstream dependant - * @param key The string key name - * @param value The new value to assign - * @param useDirtyFlag If true, mark downstream changes as dirty - * @return True if the key name exists in this context; false if there is no binding value with this name. - */ - bool propagate(const std::string& key, const Object& value, bool useDirtyFlag) { + void setValue(std::string key, const Object& value, bool) override { auto it = mMap.find(key); if (it == mMap.end()) - return false; + return; if (it->second.set(value)) - recalculateDownstream(key, useDirtyFlag); - - return true; + enqueueDownstream(key); } /** @@ -506,11 +498,13 @@ class Context : public RecalculateTarget, const JsonResource getGraphic(const std::string& name) const; /** - * Find a component somewhere in the DOM with the given id or uniqueId. + * Find a component with the given id or uniqueId somewhere in the DOM of the document identified by documentId. + * To find a component in the top-level document, use the variant accepting only id. * @param id The id or uniqueID to search for. - * @return The component or nullptr if it is not found. + * @param documentId The document to search in. + * @return The component or nullptr if either documentId is not registered or id is not found within the registered document. */ - ComponentPtr findComponentById(const std::string& id) const; + ComponentPtr findComponentById(const std::string& id, bool traverseHost = true) const; /** * @return The top component @@ -547,6 +541,26 @@ class Context : public RecalculateTarget, */ std::string getRequestedAPLVersion() const; + /** + * @return The dpi of the screen. + */ + int getDpi() const; + + /** + * @return The screen shape + */ + ScreenShape getScreenShape() const; + + /** + * @return The SharedContextData. + */ + SharedContextDataPtr getShared() const; + + /** + * @return The human-readable mode of the viewport + */ + ViewportMode getViewportMode() const; + /** * Internal routine used by components to mark themselves as changed. * @param ptr The component to mark @@ -592,6 +606,16 @@ class Context : public RecalculateTarget, */ WeakPtrSet& pendingOnMounts(); + /** + * @return true if embedded document context, false if host one. + */ + bool embedded() const; + + /** + * @return Relevant DocumentContext. + */ + DocumentContextPtr documentContext() const; + #ifdef SCENEGRAPH /** * @return A cache of TextProperties @@ -609,13 +633,13 @@ class Context : public RecalculateTarget, FocusManager& focusManager() const; HoverManager& hoverManager() const; - KeyboardManager& keyboardManager() const; LiveDataManager& dataManager() const; ExtensionManager& extensionManager() const; LayoutManager& layoutManager() const; MediaManager& mediaManager() const; MediaPlayerFactory& mediaPlayerFactory() const; UIDManager& uniqueIdManager() const; + DependantManager& dependantManager() const; std::shared_ptr styles() const; @@ -641,16 +665,16 @@ class Context : public RecalculateTarget, protected: ContextPtr mParent; ContextPtr mTop; - std::shared_ptr mCore; + ContextDataPtr mCore; std::map mMap; private: /** * Initialize environment parameters for the context * @param metrics The display metrics. - * @param core A pointer to the common core data. + * @param core A pointer to the document data. */ - void init(const Metrics& metrics, const std::shared_ptr& core); + void init(const Metrics& metrics, const ContextDataPtr& core); }; } // namespace apl diff --git a/aplcore/include/apl/engine/contextdependant.h b/aplcore/include/apl/engine/contextdependant.h deleted file mode 100644 index da85af0..0000000 --- a/aplcore/include/apl/engine/contextdependant.h +++ /dev/null @@ -1,83 +0,0 @@ -/** - * Copyright 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 _APL_CONTEXT_DEPENDANT_H -#define _APL_CONTEXT_DEPENDANT_H - -#include - -#include "apl/engine/builder.h" -#include "apl/engine/dependant.h" -#include "apl/primitives/object.h" - -namespace apl { - -class Context; - -/** - * A dependant relationship where a change in the source context results in a change - * in the target context. This occurs when a "bind" relationship in a Component refers - * to a value defined in a data-binding context. - * - * The dependant stores the parsed Node in the child context. When the source - * context value changes, the dependant calculates the new target context value and - * stores it in the target context. This normally triggers additional dependants - * to update their values. - */ -class ContextDependant : public Dependant { -public: - /** - * Construct a dependency between two contexts - * @param downstreamContext The downstream or target context. - * @param downstreamName The name of the symbol in the downstream context which will be recalculated. - * @param equation The expression which will be evaluated to recalculate downstream. - * @param bindingContext The context where the equation will be bound - * @param bindingFunction The binding function that will be applied after evaluating the equation - */ - static void create(const ContextPtr& downstreamContext, - const std::string& downstreamName, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction); - - - /** - * Internal constructor - do not call. Use ContextDependant::create instead. - * @param downstreamContext The downstream or target context. - * @param downstreamName The name of the symbol in the downstream context which will be recalculated. - * @param equation The expression which will be evaluated to recalculate downstream. - * @param bindingContext The context where the equation will be bound - * @param bindingFunction The binding function that will be applied after evaluating the equation - */ - ContextDependant(const ContextPtr& downstreamContext, - const std::string& downstreamName, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction) - : Dependant(equation, bindingContext, bindingFunction), - mDownstreamContext(downstreamContext), - mDownstreamName(downstreamName) - {} - - void recalculate(bool useDirtyFlag) const override; - -private: - std::weak_ptr mDownstreamContext; - std::string mDownstreamName; -}; - -} // namespace apl - -#endif //_APL_CONTEXT_DEPENDANT_H diff --git a/aplcore/include/apl/engine/corerootcontext.h b/aplcore/include/apl/engine/corerootcontext.h new file mode 100644 index 0000000..12e6c65 --- /dev/null +++ b/aplcore/include/apl/engine/corerootcontext.h @@ -0,0 +1,177 @@ +/** + * Copyright 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 _APL_CORE_ROOT_CONTEXT_H +#define _APL_CORE_ROOT_CONTEXT_H + +#include "apl/engine/rootcontext.h" +#include "apl/engine/context.h" + +namespace apl { + +/** + * Core implementation of RootContext API. + */ +class CoreRootContext : public std::enable_shared_from_this, public RootContext { +public: + static RootContextPtr create(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config, + std::function callback); + + /** + * Public constructor. Use the ::create method instead. + * @param metrics Display metrics + * @param content Processed APL content data + * @param config Configuration information + */ + CoreRootContext(const Metrics& metrics, const ContentPtr& content, const RootConfig& config); + + ~CoreRootContext() override; + + + /// RootContext overrides + void configurationChange(const ConfigurationChange& change) override; + void updateDisplayState(DisplayState displayState) override; + void reinflate() override; + void clearPending() const override; + bool hasEvent() const override; + Event popEvent() override; + Context& context() const override; + ContextPtr contextPtr() const override; + ComponentPtr topComponent() const override; + DocumentContextPtr topDocument() const override; + bool isDirty() const override; + const std::set& getDirty() override; + void clearDirty() override; + bool isVisualContextDirty() const override; + void clearVisualContextDirty() override; + rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) override; + bool isDataSourceContextDirty() const override; + void clearDataSourceContextDirty() override; + rapidjson::Value serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) override; + rapidjson::Value serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) override; + rapidjson::Value serializeContext(rapidjson::Document::AllocatorType& allocator) override; + APL_DEPRECATED ActionPtr executeCommands(const Object& commands, bool fastMode) override; + ActionPtr invokeExtensionEventHandler(const std::string& uri, const std::string& name, + const ObjectMap& data, bool fastMode, + std::string resourceId = "") override; + void cancelExecution() override; + void updateTime(apl_time_t elapsedTime) override; + void updateTime(apl_time_t elapsedTime, apl_time_t utcTime) override; + void setLocalTimeAdjustment(apl_duration_t adjustment) override { + mLocalTimeAdjustment = adjustment; + } + void scrollToRectInComponent(const ComponentPtr& component, + const Rect &bounds, + CommandScrollAlign align) override; + apl_time_t nextTime() override; + apl_time_t currentTime() const override; + bool screenLock() const override; + const RootConfig& rootConfig() const override; + const Settings& settings() const override; + const ContentPtr& content() const override; + Info info() const override; + void updateCursorPosition(Point cursorPosition) override; + bool handlePointerEvent(const PointerEvent& pointerEvent) override; + bool handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) override; + const RootConfig& getRootConfig() const override; + std::string getTheme() const override; + ComponentPtr findComponentById(const std::string& id) const override; + UIDObject* findByUniqueId(const std::string& uid) const override; + bool setFocus(FocusDirection direction, const Rect& origin, const std::string& targetId) override; + bool nextFocus(FocusDirection direction, const Rect& origin) override; + bool nextFocus(FocusDirection direction) override; + void clearFocus() override; + std::string getFocused() override; + std::map getFocusableAreas() override; + void mediaLoaded(const std::string& source) override; + void mediaLoadFailed(const std::string& source, + int errorCode = -1, + const std::string& error = std::string()) override; + bool getAutoWidth() const; + bool getAutoHeight() const; + +#ifdef SCENEGRAPH + sg::SceneGraphPtr getSceneGraph() override; +#endif // SCENEGRAPH + + /** + * Create a suitable document-level data-binding context for evaluating a document-level + * event. + * @param handler The name of the handler. + * @param optional optional data to add to the event. + * @return The document-level data-binding context. + */ + ContextPtr createDocumentContext(const std::string& handler, const ObjectMap& optional = {}); + + /** + * @return Text measurement pointer reference + */ + const TextMeasurementPtr& measure() const; + + /** + * @return The top-level context with payload binding. This context is used when executing document-level + * commands. + */ + ContextPtr payloadContext() const; + + /** + * @return Document-used sequencer. + */ + Sequencer& sequencer() const; + + /** + * @return PxToDp conversion for the top document + */ + double getPxToDp() const; + +private: + friend class CoreDocumentContext; + friend class ExtensionClient; // TODO: Required for backwards compatibility with V1 extension interface + #ifdef ALEXAEXTENSIONS + friend class ExtensionMediator; + #endif + + /** + * @return The current display state for this root context. Only exposed internally to friend + * classes. + */ + DisplayState getDisplayState() const { return mDisplayState; } + + void init(const Metrics& metrics, + const RootConfig& config, + const ContentPtr& content); + + bool setup(bool reinflate); + ObjectMapPtr createDocumentEventProperties(const std::string& handler) const; + void clearPendingInternal(bool first) const; + void updateTimeInternal(apl_time_t elapsedTime, apl_time_t utcTime); + +private: + SharedContextDataPtr mShared; + std::shared_ptr mTimeManager; + apl_time_t mUTCTime = 0; // Track the system UTC time + apl_duration_t mLocalTimeAdjustment = 0; + DisplayState mDisplayState; + CoreDocumentContextPtr mTopDocument; +#ifdef SCENEGRAPH + sg::SceneGraphPtr mSceneGraph; +#endif // SCENEGRAPH +}; + +} // namespace apl + +#endif //_APL_CORE_ROOT_CONTEXT_H diff --git a/aplcore/include/apl/engine/dependant.h b/aplcore/include/apl/engine/dependant.h index fc6b880..b0c4dee 100644 --- a/aplcore/include/apl/engine/dependant.h +++ b/aplcore/include/apl/engine/dependant.h @@ -17,12 +17,14 @@ #define _APL_DEPENDANT_H #include +#include #include "apl/common.h" +#include "apl/engine/binding.h" +#include "apl/primitives/boundsymbolset.h" +#include "apl/primitives/object.h" #include "apl/utils/counter.h" #include "apl/utils/noncopyable.h" -#include "apl/primitives/object.h" -#include "apl/engine/binding.h" namespace apl { @@ -38,11 +40,11 @@ class Dependant : public Counter, public NonCopyable, public std::enable_shared_from_this { public: - Dependant(const Object& equation, const ContextPtr& bindingContext, BindingFunction bindingFunction) - : mEquation(equation), - mBindingContext(bindingContext), - mBindingFunction(std::move(bindingFunction)) - {} + Dependant(Object expression, + const ContextPtr& bindingContext, + BindingFunction bindingFunction, + BoundSymbolSet symbols); + virtual ~Dependant() = default; /** @@ -54,12 +56,33 @@ class Dependant : public Counter, * Recalculate the values in the target object. * @param useDirtyFlag If true, mark downstream changes as dirty */ - virtual void recalculate(bool useDirtyFlag) const = 0; + virtual void recalculate(bool useDirtyFlag) = 0; + + /** + * Enqueue this dependency for recalculation + * @return True if the enqueue worked. False if this dependent should be dropped. + */ + bool enqueue(); + + bool friend operator<(const Dependant& lhs, const Dependant& rhs) { + return lhs.mOrder < rhs.mOrder; + } + + virtual std::string toDebugString() const { + return std::string("Dependant<"+std::to_string(mOrder)+">"); + } + +protected: + void attach(); + void detach(); + void reattach(const BoundSymbolSet& symbols); protected: - Object mEquation; // The equation or expression to be evaluated - std::weak_ptr mBindingContext; // The context the BindingFunction will be applied in - BindingFunction mBindingFunction; // The function to be applied after evaluation + Object mExpression; + std::weak_ptr mBindingContext; + BindingFunction mBindingFunction; + BoundSymbolSet mSymbols; + size_t mOrder; }; } // namespace apl diff --git a/aplcore/include/apl/engine/dependantmanager.h b/aplcore/include/apl/engine/dependantmanager.h new file mode 100644 index 0000000..6d29160 --- /dev/null +++ b/aplcore/include/apl/engine/dependantmanager.h @@ -0,0 +1,61 @@ +/* + * Copyright 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 _APL_DEPENDANT_MANAGER_H +#define _APL_DEPENDANT_MANAGER_H + +#include + +#include "apl/common.h" + +namespace apl { + +/** + * Manage topological sorting and dependency propagation for a single root context. All + * documents sharing one view port use the same manager because dependencies can propagate + * across documents. + * + * The manager is responsible for assigning topological sort IDs as the dependencies are + * generated and for processing the dependencies in sort order as they are triggered. + */ +class DependantManager { +public: + DependantManager() = default; + + /** + * @return The next absolute sort order to use for this root context + */ + id_type getNextSortOrder() { return mSortOrderGenerator++; } + + /** + * Add a dependency to the "to-be-processed" list. + * @param dependant The dependant to add to the list + */ + void enqueueDependency(const DependantPtr& dependant); + + /** + * Process the list of dependencies until it is empty. + */ + void processDependencies(bool useDirtyFlag); + +private: + id_type mSortOrderGenerator = 10; // Start at a non-zero value to help debugging + std::vector mProcessList; // Sorted list of dependants. Could be a deque +}; + +} // namespace apl + +#endif \ No newline at end of file diff --git a/aplcore/include/apl/engine/evaluate.h b/aplcore/include/apl/engine/evaluate.h index ce1def8..bd0867e 100644 --- a/aplcore/include/apl/engine/evaluate.h +++ b/aplcore/include/apl/engine/evaluate.h @@ -16,43 +16,13 @@ #ifndef _APL_EVALUATE_H #define _APL_EVALUATE_H -#include "apl/utils/bimap.h" +#include "apl/engine/binding.h" +#include "apl/primitives/boundsymbolset.h" #include "apl/primitives/object.h" +#include "apl/utils/bimap.h" namespace apl { -/** - * Parse a data-binding string and return the parsed expression. The returned object will - * be byte code if the string contained at least one data-binding expression and will be - * a plain string if no data-binding expressions were found - * @param context The data-binding context - * @param value The string value to evaluate - * @return The byte code or a string if no data-binding was found - */ -Object getDataBinding(const Context& context, const std::string& value); - -/** - * Parse a data-binding string and return the parsed expression. If the string contains - * data-binding expressions referring to symbols not defined in the current context - * or symbols that have been marked as mutable, the returned object will be byte code. - * If the parsed expression is constant, the returned object will be the appropriate type. - * - * @param context The data-binding context - * @param value The string value to evaluate - * @return The evaluated object or byte code - */ -Object parseDataBinding(const Context& context, const std::string& value); - -/** - * Parse a data-binding recursively and return the parsed expression tree. If the object contains - * any strings with data-binding expressions referring to symbols not defined in the current context - * or symbols that have been marked as mutable, the returned object will be byte code. - * @param context The data-binding context - * @param object The object to evaluate - * @return The evaluated object or byte code - */ -Object parseDataBindingRecursive(const Context& context, const Object& object); - /** * Evaluate an object applying data-binding. The object or expression will be converted * into byte code if necessary, evaluated, and resources will be substituted. @@ -72,23 +42,80 @@ Object evaluate(const Context& context, const Object& object); */ Object evaluate(const Context& context, const char *expression); -/** - * Re-evaluate an equation that is already marked as evaluable. This method is used - * when propagating dependency changes. - * @param context The binding context of the equation - * @param equation The equation to re-evaluate - * @return The resultant value - */ -Object reevaluate(const Context& context, const Object& equation); - /** * Evaluate an object recursively. Arrays and maps within the object will also * be evaluated for data-binding. * @param context The data-binding context. * @param object The object to evaluate. + * @param symbolSet An optional symbol set to populate with the results of evaluation * @return The result of recursive evaluation. */ -Object evaluateRecursive(const Context& context, const Object& object); +Object +evaluateNested(const Context& context, const Object& object, BoundSymbolSet *symbolSet=nullptr); + +/** + * This method is only used by the byte code evaluator for the "eval(x)" built-in function. + * It is is basically the same as the evaluateNested() method, but it tracks evaluation depth. + * @param context The data-binding context + * @param object The object to evaluate + * @param symbolSet The symbol set to populate with the results of the evaluation (may be nullptr) + * @param depth The current evaluation depth + * @return The result of the evaluation + */ +Object +evaluateInternal(const Context& context, const Object& object, BoundSymbolSet *symbolSet, int depth); + +/** + * The structure returned by parseAndEvaluate(). + */ +struct ParseResult { + Object value; /// The calculated value after all data-binding expressions are evaluated + Object expression; /// The expanded object processed for data-binding expressions + BoundSymbolSet symbols; /// The bound symbols used when calculating "value" from "expression". +}; + +/** + * The structure returned by applyDataBinding(). + */ +struct ApplyResult { + Object value; /// The calculated value after all data-binding expressions are evaluated + BoundSymbolSet symbols; // The bound symbols used when calculating the value. +}; + + +/** + * Recursively parse an object for data-binding and return (a) the result of evaluating the + * object in the assigned context, (b) the same object with byte-code in the places that will + * need to be re-evaluated, and (c) the symbols used to evaluated the nested expression. + * + * Hint: if there are no returned symbols, the object is constant and you can ignore the + * second half of the returned pair. + * + * @param context The data-binding context to use when evaluating. + * @param object The object to recursively parse and evaluate. + * @param optimize If true, optimize the byte code. Defaults to true. + * @return A ParseResult object + */ +ParseResult +parseAndEvaluate(const Context& context, + const Object& object, + bool optimize=true); + +/** + * Apply data-binding to an object that was previously parsed using parseAndEvaluate(). + * The same context that was used for parsing must be used to re-apply the data binding. + * If a SymbolSet pointer is passed, it will be populated with the list of symbols referenced + * when the object is re-evaluated. + * @param context The data-binding context. + * @param object The object to re-evaluate. + * @param bindingFunction A binding function to apply to the evaluated object. + * @return An ApplyResult object containing the calculated value and the symbols referenced. + */ +ApplyResult +applyDataBinding(const Context& context, + const Object& object, + const BindingFunction& bindingFunction); + std::string propertyAsString(const Context& context, const Object& object, const char *name); std::string propertyAsString(const Context& context, const Object& object, const char *name, const std::string& defValue); @@ -98,16 +125,6 @@ int propertyAsInt(const Context& context, const Object& object, const char *name Object propertyAsObject(const Context& context, const Object& object, const char *name); Object propertyAsRecursive(const Context& context, const Object& object, const char *name); -/** - * Retrieve a property from an object and do basic data parsing, but allow a node-tree to - * be returned. - * @param context The data-binding context - * @param item The object that contains the named property - * @param name The name of the property - * @return The value of the property; may be a Node. - */ -Object propertyAsNode(const Context& context, const Object& item, const char *name); - /** * Look up a mapped property. Return (T) -1 if the property is invalid. If the property * is not specified, return defValue. diff --git a/aplcore/include/apl/engine/event.h b/aplcore/include/apl/engine/event.h index fe6df9c..93e918b 100644 --- a/aplcore/include/apl/engine/event.h +++ b/aplcore/include/apl/engine/event.h @@ -320,6 +320,11 @@ class Event : public UserData { */ Event(EventType eventType, EventBag&& bag, const ComponentPtr& component, ActionRef actionRef); + /** + * @return Originating document. Returns null if the underlying document context has been freed. + */ + DocumentContextPtr getDocument() const { return mDocument.lock(); } + /** * @return The type of the event */ @@ -354,16 +359,21 @@ class Event : public UserData { rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; /** - * Equality test. Used primarily by unit testing code; this does not guarantee - * that two events are exactly the same, but does check to make sure they look - * "approximately" the same - * @param rhs The event to compare to - * @return True if the two events match + * Equality test. Does not guarantee that it's the same event object, but contents are equal. */ - bool matches(const Event& rhs) const; + bool operator==(const Event& other) const; private: + friend class DocumentContextData; + + /** + * Tag the event with originating document. Called internally. + * @param document originating document. + */ + void setDocument(const std::weak_ptr& document) { mDocument = document;} + std::shared_ptr mData; + std::weak_ptr mDocument; }; } // namespace apl diff --git a/aplcore/include/apl/engine/eventmanager.h b/aplcore/include/apl/engine/eventmanager.h index d6f0a04..cde504c 100644 --- a/aplcore/include/apl/engine/eventmanager.h +++ b/aplcore/include/apl/engine/eventmanager.h @@ -1,71 +1,32 @@ -/** -* Copyright 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 APL_EVENT_MANAGER_H -#define APL_EVENT_MANAGER_H - -#include - -#include "apl/engine/event.h" -#include "apl/engine/eventpublisher.h" - -namespace apl -{ - -class EventManager; - -using EventManagerPtr = std::shared_ptr; - -/** - * Read-Write interface for publishing and consuming events. - */ -class EventManager : public EventPublisher -{ -public: - virtual ~EventManager() {}; - - /** - * Discard all pending, published events. - */ - virtual void clear() = 0; - - /** - * Determine if any published events are pending. - * - * @return true iff there are no pending events - */ - virtual bool empty() const = 0; - - /** - * Return the next pending published event. Check empty first. - * @return A reference to the next event - */ - virtual Event& front() = 0; - - /** - * Return the next pending published event. Check empty first. - * @return A reference to the next event if it exists; ??? otherwise. - */ - virtual const Event& front() const = 0; - - /** - * Removes the next pending event. Check empty first. - */ - virtual void pop() = 0; -}; - -} // namespace apl - -#endif // APL_EVENT_MANAGER_H +/** + * Copyright 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 APL_EVENT_MANAGER_H +#define APL_EVENT_MANAGER_H + +#include + +#include "apl/utils/scopeddequeue.h" + +#include "apl/engine/event.h" + +namespace apl +{ + +class EventManager : public ScopedDequeue {}; + +} // namespace apl + +#endif // APL_EVENT_MANAGER_H diff --git a/aplcore/include/apl/engine/eventpublisher.h b/aplcore/include/apl/engine/eventpublisher.h index 942d834..83306a7 100644 --- a/aplcore/include/apl/engine/eventpublisher.h +++ b/aplcore/include/apl/engine/eventpublisher.h @@ -1,49 +1,49 @@ -/** -* Copyright 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 APL_EVENT_PUBLISHER_H -#define APL_EVENT_PUBLISHER_H - -#include "apl/engine/event.h" - -namespace apl -{ - -/** - * Write-only interface for publishing events from within a document. - */ -class EventPublisher -{ -public: - virtual ~EventPublisher() {}; - - /** - * Pushes event for publication. - * - * @param event The Event to publish - */ - virtual void push(const Event& event) = 0; - - /** - * Pushes event for publication. - * - * @param event The Event to publish - */ - virtual void push(Event&& event) = 0; -}; - -} // namespace apl - -#endif // APL_EVENT_PUBLISHER_H +/** +* Copyright 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 APL_EVENT_PUBLISHER_H +#define APL_EVENT_PUBLISHER_H + +#include "apl/engine/event.h" + +namespace apl +{ + +/** + * Write-only interface for publishing events from within a document. + */ +class EventPublisher +{ +public: + virtual ~EventPublisher() {}; + + /** + * Pushes event for publication. + * + * @param event The Event to publish + */ + virtual void push(const Event& event) = 0; + + /** + * Pushes event for publication. + * + * @param event The Event to publish + */ + virtual void push(Event&& event) = 0; +}; + +} // namespace apl + +#endif // APL_EVENT_PUBLISHER_H diff --git a/aplcore/include/apl/engine/hovermanager.h b/aplcore/include/apl/engine/hovermanager.h index 2864e18..40c6db7 100644 --- a/aplcore/include/apl/engine/hovermanager.h +++ b/aplcore/include/apl/engine/hovermanager.h @@ -18,12 +18,10 @@ #include -namespace apl { - -class CoreComponent; - -class RootContextData; +#include "apl/common.h" +#include "apl/primitives/point.h" +namespace apl { /// HoveManager is responsible for managing the hover state of a Component and /// executing the OnCursorEnter and OnCursorExit handlers of the component. @@ -58,14 +56,7 @@ class RootContextData; /// class HoverManager { public: - explicit HoverManager(const RootContextData& core) : mCore(core) {} - - /** - * @return The cursor position. - */ - Point cursorPosition() const { - return mCursorPosition; - } + explicit HoverManager(const CoreRootContext& core) : mCore(core) {} /** * Set cursor position @@ -93,7 +84,7 @@ class HoverManager { void componentToggledDisabled(const CoreComponentPtr& component); private: - const RootContextData& mCore; + const CoreRootContext& mCore; std::weak_ptr mHover; Point mCursorPosition; diff --git a/aplcore/include/apl/engine/info.h b/aplcore/include/apl/engine/info.h index 2a59b76..649d8d0 100644 --- a/aplcore/include/apl/engine/info.h +++ b/aplcore/include/apl/engine/info.h @@ -24,7 +24,6 @@ namespace apl { -class RootContextData; class Context; /** @@ -46,7 +45,7 @@ class Info { kInfoTypeStyle }; - Info(const ContextPtr& context, const std::shared_ptr& core) + Info(const ContextPtr& context, const DocumentContextDataPtr& core) : mContext(context), mCore(core) {} /** @@ -71,7 +70,7 @@ class Info { private: ContextPtr mContext; - std::shared_ptr mCore; + DocumentContextDataPtr mCore; }; } // namespace apl diff --git a/aplcore/include/apl/engine/keyboardmanager.h b/aplcore/include/apl/engine/keyboardmanager.h index 4140700..b556feb 100644 --- a/aplcore/include/apl/engine/keyboardmanager.h +++ b/aplcore/include/apl/engine/keyboardmanager.h @@ -19,11 +19,10 @@ #include #include "apl/primitives/keyboard.h" +#include "apl/component/componentproperties.h" namespace apl { -class CoreComponent; - class KeyboardManager { public: @@ -32,10 +31,12 @@ class KeyboardManager { * @param type The keyboard handler type * @param component The component receiving the key press. If null, ignored. * @param keyboard The key press definition. + * @param rootContext pointer to RootContext. + * @result True, if the key was consumed. */ - bool handleKeyboard(KeyHandlerType type, const CoreComponentPtr& component, const Keyboard& keyboard, - const RootContextPtr& rootContext); + bool handleKeyboard(KeyHandlerType type, const CoreComponentPtr& component, + const Keyboard& keyboard, const CoreRootContextPtr& rootContext); /** * @param type The Keyboard handler type. @@ -49,9 +50,16 @@ class KeyboardManager { */ static std::string getHandlerId(KeyHandlerType type); -private: - bool executeDocumentKeyHandlers(const RootContextPtr& rootContext, KeyHandlerType type, const Keyboard& keyboard); - + /** + * Handle a keyboard update on a document. + * @param documentContext pointer to DocumentContext. + * @param type The keyboard handler type + * @param keyboard The key press definition. + * @return True, if the key was consumed. + */ + bool executeDocumentKeyHandlers(const CoreDocumentContextPtr& documentContext, + KeyHandlerType type, + const Keyboard& keyboard); }; } // namespace apl diff --git a/aplcore/include/apl/engine/layoutmanager.h b/aplcore/include/apl/engine/layoutmanager.h index 34d14c2..effd480 100644 --- a/aplcore/include/apl/engine/layoutmanager.h +++ b/aplcore/include/apl/engine/layoutmanager.h @@ -26,7 +26,6 @@ namespace apl { -class RootContextData; class ConfigurationChange; /** @@ -64,13 +63,24 @@ class ConfigurationChange; class LayoutManager { public: - explicit LayoutManager(const RootContextData& core); + /** + * Instantiate a LayoutManager for the given CoreRootContext. + * @param coreRootContext the CoreRootContext for which layouts will be managed + * @param size Initial configured size. + */ + LayoutManager(const CoreRootContext& coreRootContext, const Size& size); /** * Stop all layout processing (and future layout processing) */ void terminate(); + /** + * Set new viewport size + * @param size new viewport size + */ + void setSize(const Size& size); + /** * @return True if there are components that need a layout pass */ @@ -98,8 +108,16 @@ class LayoutManager { * Inform the layout manager of a configuration change. If the configuration change * affects the layout, this will schedule a layout pass. * @param change The configuration change + * @param document The document being reconfigured */ - void configChange(const ConfigurationChange& change); + void configChange(const ConfigurationChange& change, const CoreDocumentContextPtr& document); + + /** + * Determines whether the specified component is a Yoga hierarchy top node. + * + * @return @c true if the node is a top node, @c false otherwise + */ + bool isTopNode(const std::shared_ptr& component) const; /** * Mark this component as the top of a Yoga hierarchy @@ -149,20 +167,31 @@ class LayoutManager { */ void needToReProcessLayoutChanges() { mNeedToReProcessLayoutChanges = true; } + using PPKey = std::pair, PropertyKey>; + class PPKeyLess final { + public: + bool operator()(const PPKey& lhs, const PPKey& rhs) const + { + if (lhs.first.owner_before(rhs.first)) return true; + if (rhs.first.owner_before(lhs.first)) return false; + + // Components are equal, break the tie with the property key + return lhs.second < rhs.second; + } + }; + private: void layoutComponent(const CoreComponentPtr& component, bool useDirtyFlag, bool first); void flushLazyInflationInternal(const CoreComponentPtr& comp); private: - using PPKey = std::pair; - - const RootContextData& mCore; + const CoreRootContext& mRoot; std::set mPendingLayout; Size mConfiguredSize; bool mTerminated = false; bool mInLayout = false; // Guard against recursive calls to layout bool mNeedToReProcessLayoutChanges = false; - std::map mPostProcess; // Collection of elements to post-process + std::map mPostProcess; // Collection of elements to post-process }; } // namespace apl diff --git a/aplcore/include/apl/engine/propdef.h b/aplcore/include/apl/engine/propdef.h index f5c8f66..e700e7c 100644 --- a/aplcore/include/apl/engine/propdef.h +++ b/aplcore/include/apl/engine/propdef.h @@ -19,6 +19,7 @@ #include "apl/engine/arrayify.h" #include "apl/engine/properties.h" #include "apl/primitives/dimension.h" +#include "apl/primitives/urlrequest.h" #include "apl/utils/bimap.h" namespace apl { @@ -168,6 +169,10 @@ inline Object asMapped(const Context& context, const Object& object, return defvalue; } +inline Object asUrlRequest(const Context& context, const Object& object) { + return URLRequest::create(context, object); +} + inline Object asDashArray(const Context& context, const Object& object) { std::vector data = arrayify(context, object); auto size = data.size(); @@ -179,7 +184,7 @@ inline Object asDashArray(const Context& context, const Object& object) { } inline Object asDeepArray(const Context &context, const Object &object) { - return evaluateRecursive(context, arrayify(context, object)); + return evaluateNested(context, arrayify(context, object)); } /** diff --git a/aplcore/include/apl/engine/properties.h b/aplcore/include/apl/engine/properties.h index 13c2d04..7b60555 100644 --- a/aplcore/include/apl/engine/properties.h +++ b/aplcore/include/apl/engine/properties.h @@ -45,7 +45,6 @@ class Properties { std::string asString(const Context& context, const char *name, const char *defvalue); bool asBoolean(const Context& context, const char *name, bool defvalue); double asNumber(const Context& context, const char *name, double defvalue); - Dimension asAbsoluteDimension(const Context& context, const char *name, double defvalue); void emplace(const Object& item); void emplace(const std::string& name, const Object& value) { mProperties.emplace(name, value); } diff --git a/aplcore/include/apl/engine/queueeventmanager.h b/aplcore/include/apl/engine/queueeventmanager.h deleted file mode 100644 index f496f4b..0000000 --- a/aplcore/include/apl/engine/queueeventmanager.h +++ /dev/null @@ -1,49 +0,0 @@ -/** -* Copyright 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 APL_QUEUE_EVENT_MANAGER_H -#define APL_QUEUE_EVENT_MANAGER_H - -#include -#include - -#include "apl/engine/eventmanager.h" - -namespace apl -{ - -/** - * An EventManager that delegates all operations to an encapsulated std::queue. - */ -class QueueEventManager : public EventManager -{ -public: - ~QueueEventManager() override = default; - - void clear() override { events = std::queue(); } - bool empty() const override { return events.empty(); } - const Event& front() const override { return events.front(); } - Event& front() override { return events.front(); } - void pop() override { events.pop(); } - void push(const Event& event) override { events.push(event); } - void push(Event&& event) override { events.push(event); } - -private: - std::queue events; -}; - -} // namespace apl - -#endif // APL_QUEUE_EVENT_MANAGER_H diff --git a/aplcore/include/apl/engine/recalculatesource.h b/aplcore/include/apl/engine/recalculatesource.h index 0f9e6f5..a844725 100644 --- a/aplcore/include/apl/engine/recalculatesource.h +++ b/aplcore/include/apl/engine/recalculatesource.h @@ -81,15 +81,13 @@ class RecalculateSource { /** * The "key" local element has changed. Recalculate all downstream objects that depend on key. * @param key The key that has changed. - * @param useDirtyFlag If true, mark downstream changes with the dirty flag */ - void recalculateDownstream(T key, bool useDirtyFlag) { + void enqueueDownstream(T key) { auto dependants = mDownstream.equal_range(key); auto it = dependants.first; while (it != dependants.second) { auto ptr = it->second.lock(); - if (ptr) { - ptr->recalculate(useDirtyFlag); + if (ptr && ptr->enqueue()) { it++; } else { diff --git a/aplcore/include/apl/engine/recalculatetarget.h b/aplcore/include/apl/engine/recalculatetarget.h index a6ee23d..ece765f 100644 --- a/aplcore/include/apl/engine/recalculatetarget.h +++ b/aplcore/include/apl/engine/recalculatetarget.h @@ -115,6 +115,15 @@ class RecalculateTarget { } } + /** + * Update the value of the key based on a change from an upstream dependency. This method + * should only be called by an upstream dependant. + * @param key The key to modify + * @param value The value to assign + * @param useDirtyFlag If true, mark downstream changes as dirty + */ + virtual void setValue(T key, const Object& value, bool useDirtyFlag) = 0; + private: std::multimap> mUpstream; }; diff --git a/aplcore/include/apl/engine/rootcontext.h b/aplcore/include/apl/engine/rootcontext.h index 6e2a5d5..f4ea477 100644 --- a/aplcore/include/apl/engine/rootcontext.h +++ b/aplcore/include/apl/engine/rootcontext.h @@ -13,11 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _APL_DOCUMENT_H -#define _APL_DOCUMENT_H - -#include -#include +#ifndef _APL_ROOT_CONTEXT_H +#define _APL_ROOT_CONTEXT_H #include "apl/common.h" #include "apl/content/configurationchange.h" @@ -43,7 +40,6 @@ namespace apl { class EventManager; class Metrics; class RootConfig; -class RootContextData; class TimeManager; struct PointerEvent; @@ -85,8 +81,7 @@ struct PointerEvent; * * To cancel any currently running commands, use cancelExecution(). */ -class RootContext : public std::enable_shared_from_this, - public UserData, +class RootContext : public UserData, public NonCopyable { public: /** @@ -109,34 +104,18 @@ class RootContext : public std::enable_shared_from_this, const ContentPtr& content, const RootConfig& config); - /** - * Construct a top-level root context. This static method is mainly for testing - * to support modifying the context before layout inflation. - * @param metrics Display metrics - * @param content Content to display - * @param config Configuration information - * @param callback Pre-layout callback - * @return A pointer to the root context. - */ - static RootContextPtr create(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config, - std::function callback); - /** * Construct a top-level root context. * @param metrics Display metrics * @param content Content to display * @param config Configuration information * @param callback Pre-layout callback - * @param eventManager Manages published events * @return A pointer to the root context. */ static RootContextPtr create(const Metrics& metrics, const ContentPtr& content, const RootConfig& config, - std::function callback, - const std::shared_ptr& eventManager); + std::function callback); /** * Notify core of a configuration change. Internally this method will trigger the "onConfigChange" @@ -145,15 +124,15 @@ class RootContext : public std::enable_shared_from_this, * * @param change Configuration change information */ - void configurationChange(const ConfigurationChange& change); - + virtual void configurationChange(const ConfigurationChange& change) = 0; + /** * Update the display state of the document. Internally this method will trigger the * "onDisplayStateChange" event handler in the APL document, if the display state changed. * * @param displayState The new display state */ - void updateDisplayState(DisplayState displayState); + virtual void updateDisplayState(DisplayState displayState) = 0; /** * Reinflate this context using the internally cached configuration changes. This will terminate any @@ -162,128 +141,105 @@ class RootContext : public std::enable_shared_from_this, * * This method should be called by the view host when it receives a Reinflate (kEventTypeReinflate) event. */ - void reinflate(); - - /** - * Trigger a resize based on stored configuration changes. This is normally not called by the view host; the - * RootContext::configurationChange() method handles resizing automatically. - */ - void resize(); + virtual void reinflate() = 0; /** * Clear any pending timers that need to be processed and execute any layout passes that are required. * This method is called internally by hasEvent(), popEvent(), and isDirty() so you normally don't need * to call this directly. */ - void clearPending() const; + virtual void clearPending() const = 0; /** * @return True if there is at least one queued event to be processed. */ - bool hasEvent() const; + virtual bool hasEvent() const = 0; /** * @return The top event from the event queue. */ - Event popEvent(); + virtual Event popEvent() = 0; - /** - * Public constructor. Use the ::create method instead. - * @param metrics Display metrics - * @param content Processed APL content data - * @param config Configuration information - */ - RootContext(const Metrics& metrics, const ContentPtr& content, const RootConfig& config); - - /** - * Public constructor. Use the ::create method instead. - * @param metrics Display metrics - * @param content Processed APL content data - * @param config Configuration information - * @param eventManager Manages published events - */ - RootContext(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config, - const std::shared_ptr& eventManager); - - ~RootContext() override; + ~RootContext() override = default; /** * @return The top-level context. */ - Context& context() const { return *mContext; } + virtual Context& context() const = 0; /** * @return The top-level context as a shared pointer */ - ContextPtr contextPtr() const { return mContext; } + virtual ContextPtr contextPtr() const = 0; /** * @return The top-level component */ - ComponentPtr topComponent(); + virtual ComponentPtr topComponent() const = 0; /** - * @return The top-level context with payload binding. This context is used when executing document-level - * commands. + * @return Top Document context */ - ContextPtr payloadContext() const; + virtual DocumentContextPtr topDocument() const = 0; /** * @return True if one or more components needs to be updated. */ - bool isDirty() const; + virtual bool isDirty() const = 0; /** * External routine to get the set of components that are dirty. * @return The dirty set. */ - const std::set& getDirty(); + virtual const std::set& getDirty() = 0; /** * Clear all of the dirty flags. This routine will clear all dirty * flags from child components. */ - void clearDirty(); + virtual void clearDirty() {} /** - * Identifies when the visual context may have changed. A call to serializeVisualContext resets this value to false. - * @return true if the visual context has changed since the last call to serializeVisualContext, false otherwise. + * Identifies when the visual context of the top document may have changed. A call to + * serializeVisualContext resets this value to false. + * @return true if the visual context has changed since the last call to serializeVisualContext, + * false otherwise. */ - bool isVisualContextDirty() const; + virtual bool isVisualContextDirty() const = 0; /** - * Clear the visual context dirty flag + * Clear the top document's visual context dirty flag */ - void clearVisualContextDirty(); + virtual void clearVisualContextDirty() = 0; /** - * Retrieve component's visual context as a JSON object. This method also clears the - * visual context dirty flag + * Retrieve top document's visual context as a JSON object. This method also clears + * the visual context dirty flag * @param allocator Rapidjson allocator * @return The serialized visual context */ - rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator); + virtual rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) = 0; /** - * Identifies when the datasource context may have changed. A call to serializeDatasourceContext resets this value to false. - * @return true if the datasource context has changed since the last call to serializeDatasourceContext, false otherwise. + * Identifies when the datasource context for the top document may have changed. A call to + * serializeDatasourceContext resets this value to false. + * @return true if the datasource context has changed since the last call to + * serializeDatasourceContext, false otherwise. */ - bool isDataSourceContextDirty() const; + virtual bool isDataSourceContextDirty() const = 0; /** - * Clear the datasource context dirty flag + * Clear the top document's datasource context dirty flag */ - void clearDataSourceContextDirty(); + virtual void clearDataSourceContextDirty() = 0; /** - * Retrieve datasource context as a JSON array object. This method also clears the + * Retrieve top document's datasource context as a JSON array object. This method also clears the * datasource context dirty flag * @param allocator Rapidjson allocator * @return The serialized datasource context */ - rapidjson::Value serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator); + virtual rapidjson::Value serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) = 0; /** * Serialize a complete version of the DOM @@ -291,21 +247,22 @@ class RootContext : public std::enable_shared_from_this, * @param allocator Rapidjson allocator * @return The serialized DOM */ - rapidjson::Value serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator); + virtual rapidjson::Value serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) = 0; /** * Serialize the global values for developer tools * @param allocator Rapidjson allocator * @return The serialized global values */ - rapidjson::Value serializeContext(rapidjson::Document::AllocatorType& allocator); + virtual rapidjson::Value serializeContext(rapidjson::Document::AllocatorType& allocator) = 0; /** * Execute an externally-driven command * @param commands The commands to execute * @param fastMode If true this handler will be invoked in fast mode + * @deprecated Use corresponding API on top document's DocumentContext. */ - ActionPtr executeCommands(const Object& commands, bool fastMode); + APL_DEPRECATED virtual ActionPtr executeCommands(const Object& commands, bool fastMode) = 0; /** * Invoke an extension event handler. @@ -314,40 +271,41 @@ class RootContext : public std::enable_shared_from_this, * @param data The data to associate with the handler * @param fastMode If true, this handler will be invoked in fast mode * @param resourceId handle associated with extension component if present + * @deprecated Should not be used, consider switching to ExtensionRegistrar/Extension Proxy. * @return An ActionPtr */ - ActionPtr invokeExtensionEventHandler(const std::string& uri, const std::string& name, - const ObjectMap& data, bool fastMode, - std::string resourceId = ""); + APL_DEPRECATED virtual ActionPtr invokeExtensionEventHandler(const std::string& uri, + const std::string& name, + const ObjectMap& data, + bool fastMode, + std::string resourceId = "") = 0; /** * Cancel any current commands in execution. This is typically called * as a result of the user touching on the screen to interrupt. */ - void cancelExecution(); + virtual void cancelExecution() = 0; /** * Move forward in time. This method also advances UTC and Local time by the * same amount. * @param elapsedTime The time to move forward to. */ - void updateTime(apl_time_t elapsedTime); + virtual void updateTime(apl_time_t elapsedTime) = 0; /** * Move forward in time and separately update local/UTC time. * @param elapsedTime The time to move forward to * @param utcTime The current UTC time on your system */ - void updateTime(apl_time_t elapsedTime, apl_time_t utcTime); + virtual void updateTime(apl_time_t elapsedTime, apl_time_t utcTime) = 0; /** * Set the local time zone adjustment. This is the number of milliseconds added to the UTC time * that gives the correct local time including any DST changes. * @param adjustment The adjustment time in milliseconds */ - void setLocalTimeAdjustment(apl_duration_t adjustment) { - mLocalTimeAdjustment = adjustment; - } + virtual void setLocalTimeAdjustment(apl_duration_t adjustment) = 0; /** * Generates a scroll event that will scroll the target component's sub bounds @@ -356,77 +314,60 @@ class RootContext : public std::enable_shared_from_this, * @param bounds The relative bounds within the target to scroll to. * @param align The alignment to scroll to. */ - void scrollToRectInComponent(const ComponentPtr& component, const Rect &bounds, - CommandScrollAlign align); + virtual void scrollToRectInComponent(const ComponentPtr& component, const Rect &bounds, + CommandScrollAlign align) = 0; /** * @return The next time an internal timer is scheduled to fire. This may * be as short as 1 tick past the currentTime(). */ - apl_time_t nextTime(); + virtual apl_time_t nextTime() = 0; /** * @return The current internal time of the system. */ - apl_time_t currentTime(); + virtual apl_time_t currentTime() const = 0; /** * @return True if a command is executing that holds the screen lock. */ - bool screenLock(); + virtual bool screenLock() const = 0; /** * @return the RootConfig used to initialize this context. */ - const RootConfig& rootConfig(); + virtual const RootConfig& rootConfig() const = 0; /** - * @deprecated Use Content->getDocumentSettings() * @return document-wide properties. + * @deprecated Use Content->getDocumentSettings() */ - APL_DEPRECATED const Settings& settings(); + APL_DEPRECATED virtual const Settings& settings() const = 0; /** * @return The content + * @deprecated Use corresponding API on top document's DocumentContext */ - const ContentPtr& content() const { return mContent; } - - /** - * Create a suitable document-level data-binding context for evaluating a document-level - * event. - * @param handler The name of the handler. - * @param optional optional data to add to the event. - * @return The document-level data-binding context. - */ - ContextPtr createDocumentContext(const std::string& handler, const ObjectMap& optional = {}); - - /** - * Create a suitable document-level data-binding context for evaluating a document-level - * keyboard event. - * @param handler The name of the handler. - * @param keyboard The keyboard event. - * @return The document-level data-binding context. - */ - ContextPtr createKeyboardDocumentContext(const std::string& handler, const ObjectMapPtr& keyboard); + APL_DEPRECATED virtual const ContentPtr& content() const = 0; /** * @return Information about the elements defined within the content */ - Info info() const { return Info(mContext, mCore); } + virtual Info info() const = 0; /** * Update cursor position. * @param cursorPosition Cursor positon. * @deprecated use handlePointerEvent instead */ - APL_DEPRECATED void updateCursorPosition(Point cursorPosition); + APL_DEPRECATED virtual void updateCursorPosition(Point cursorPosition) = 0; /** * Handle a given PointerEvent with coordinates relative to the viewport. * @param pointerEvent The pointer event to handle. * @return true if was consumed and should not be passed through any platform handling. */ - bool handlePointerEvent(const PointerEvent& pointerEvent); + virtual bool handlePointerEvent(const PointerEvent& pointerEvent) = 0; /** * An update message from the viewhost called when a key is pressed. The @@ -438,41 +379,31 @@ class RootContext : public std::enable_shared_from_this, * @param keyboard The keyboard message. * @return True, if the key was consumed. */ - virtual bool handleKeyboard(KeyHandlerType type, const Keyboard &keyboard); - - /** - * @return The current logging session - */ - const SessionPtr& getSession() const; + virtual bool handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) = 0; /** * @return The root configuration provided by the viewhost */ - const RootConfig& getRootConfig() const; - - /** - * @return The current theme - */ - std::string getTheme() const; - - /** - * @return Text measurement pointer reference - */ - const TextMeasurementPtr& measure() const; + virtual const RootConfig& getRootConfig() const = 0; /** * Find a component somewhere in the DOM with the given id or uniqueId. * @param id The id or uniqueID to search for. * @return The component or nullptr if it is not found. */ - ComponentPtr findComponentById(const std::string& id) const; + virtual ComponentPtr findComponentById(const std::string& id) const = 0; /** * Find a UID object * @param uid The uniqueId to search for. * @return The object or nullptr if it is not found. */ - UIDObject * findByUniqueId(const std::string& uid) const; + virtual UIDObject* findByUniqueId(const std::string& uid) const = 0; + + /** + * @return The current theme + */ + virtual std::string getTheme() const = 0; /** * Get top level focusable areas available from APL Core. It's up to engine to decide if it needs to pass focus to @@ -480,7 +411,7 @@ class RootContext : public std::enable_shared_from_this, * All dimensions is in APL viewport coordinate space. * @return map from ID to focusable area. */ - std::map getFocusableAreas(); + virtual std::map getFocusableAreas() = 0; /** * Pass focus from runtime to APL Core. @@ -489,7 +420,7 @@ class RootContext : public std::enable_shared_from_this, * @param targetId ID of area selected by runtime from list provided by getFocusableAreas(). * @return true if focus was accepted, false otherwise. */ - bool setFocus(FocusDirection direction, const Rect& origin, const std::string& targetId); + virtual bool setFocus(FocusDirection direction, const Rect& origin, const std::string& targetId) = 0; /** * Request to switch focus in provided direction. Different from setFocus above as actually defers decision on what @@ -498,7 +429,7 @@ class RootContext : public std::enable_shared_from_this, * @param origin previously focused area in APL viewport coordinate space. * @return true if processed successfully, false otherwise. */ - bool nextFocus(FocusDirection direction, const Rect& origin); + virtual bool nextFocus(FocusDirection direction, const Rect& origin) = 0; /** * Request to switch focus in provided direction. If nothing is focused works similarly to @@ -506,24 +437,24 @@ class RootContext : public std::enable_shared_from_this, * @param direction focus movement direction. * @return true if processed successfully, false otherwise. */ - bool nextFocus(FocusDirection direction); + virtual bool nextFocus(FocusDirection direction) = 0; /** * Force APL to release focus. Always succeeds. */ - void clearFocus(); + virtual void clearFocus() = 0; /** * Check if core has anything focused. * @return ID of focused element if something focused, empty if not. */ - std::string getFocused(); + virtual std::string getFocused() = 0; /** * Notify core about requested media being loaded. * @param source requested source. */ - void mediaLoaded(const std::string& source); + virtual void mediaLoaded(const std::string& source) = 0; /** * Notify core about requested media fail to load. @@ -531,54 +462,19 @@ class RootContext : public std::enable_shared_from_this, * @param errorCode integer with the errorValue, to determine by the runtime. * @param error string with the error description. */ - void mediaLoadFailed(const std::string& source, int errorCode = -1, const std::string& error = std::string()); + virtual void mediaLoadFailed(const std::string& source, int errorCode = -1, const std::string& error = std::string()) = 0; #ifdef SCENEGRAPH /** * This method returns the current scene graph. It will clear all dirty properties as well. * @return The current scene graph */ - sg::SceneGraphPtr getSceneGraph(); + virtual sg::SceneGraphPtr getSceneGraph() = 0; #endif // SCENEGRAPH friend streamer& operator<<(streamer& os, const RootContext& root); - -private: - #ifdef ALEXAEXTENSIONS - friend class ExtensionMediator; - #endif - - /** - * @return The current display state for this root context. Only exposed internally to friend - * classes. - */ - DisplayState getDisplayState() const { return mDisplayState; } - - void init(const Metrics& metrics, const RootConfig& config, bool reinflation, const std::shared_ptr& eventManager); - bool setup(const CoreComponentPtr& top); - bool verifyAPLVersionCompatibility(const std::vector>& ordered, - const APLVersion& compatibilityVersion); - bool verifyTypeField(const std::vector>& ordered, bool enforce); - ObjectMapPtr createDocumentEventProperties(const std::string& handler) const; - void scheduleTickHandler(const Object& handler, double delay); - void processTickHandlers(); - void clearPendingInternal(bool first) const; - void updateTimeInternal(apl_time_t elapsedTime, apl_time_t utcTime); - -private: - ContentPtr mContent; - ContextPtr mContext; - std::shared_ptr mCore; // When you die, make sure to tell the data to terminate itself. - std::shared_ptr mTimeManager; - apl_time_t mUTCTime; // Track the system UTC time - apl_duration_t mLocalTimeAdjustment; - ConfigurationChange mActiveConfigurationChanges; - DisplayState mDisplayState; -#ifdef SCENEGRAPH - sg::SceneGraphPtr mSceneGraph; -#endif // SCENEGRAPH }; } // namespace apl -#endif //_APL_DOCUMENT_H +#endif //_APL_ROOT_CONTEXT_H diff --git a/aplcore/include/apl/engine/rootcontextdata.h b/aplcore/include/apl/engine/rootcontextdata.h deleted file mode 100644 index 0ce7447..0000000 --- a/aplcore/include/apl/engine/rootcontextdata.h +++ /dev/null @@ -1,218 +0,0 @@ -/** - * Copyright 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 _APL_ROOT_CONTEXT_DATA_H -#define _APL_ROOT_CONTEXT_DATA_H - -#include -#include - -#include "apl/apl_config.h" - -#include "apl/content/content.h" -#include "apl/content/metrics.h" -#include "apl/content/rootconfig.h" -#include "apl/content/settings.h" -#include "apl/datasource/datasourceconnection.h" -#include "apl/engine/event.h" -#include "apl/engine/queueeventmanager.h" -#include "apl/engine/hovermanager.h" -#include "apl/engine/jsonresource.h" -#include "apl/engine/keyboardmanager.h" -#include "apl/engine/layoutmanager.h" -#include "apl/engine/runtimestate.h" -#include "apl/engine/styles.h" -#include "apl/extension/extensionmanager.h" -#include "apl/focus/focusmanager.h" -#include "apl/livedata/livedatamanager.h" -#include "apl/media/mediamanager.h" -#include "apl/primitives/size.h" -#include "apl/primitives/textmeasurerequest.h" -#include "apl/time/sequencer.h" -#include "apl/touch/pointermanager.h" -#include "apl/utils/counter.h" -#include "apl/utils/lrucache.h" - -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH - -namespace apl { - -class RootContextData : public Counter { - friend class RootContext; - -public: - /** - * Stock constructor - * @param metrics Display metrics - * @param config Configuration settings - * @param runtimeState Runtime state information (theme, required APL version, re-inflation state) - * @param settings Document settings - * @param session Session information for logging messages and warnings - * @param extensions Mapping of requested extensions NAME -> URI - * @param eventManager Responsible for managing all published events. - */ - RootContextData(const Metrics& metrics, - const RootConfig& config, - RuntimeState runtimeState, - const SettingsPtr& settings, - const SessionPtr& session, - const std::vector>& extensions, - const EventManagerPtr& eventManager = std::make_shared()); - - ~RootContextData(); - - /** - * Halt the RootContextData and release the component hierarchy.. - */ - void terminate(); - - /** - * This root context data is being replaced by a new one. Terminate all processing - * and return the top component. To release memory, you must call release on the top - * component after you are done with it. Once halted the RootContextData cannot be - * restarted. - */ - CoreComponentPtr halt(); - - std::shared_ptr styles() const { return mStyles; } - Sequencer& sequencer() const { return *mSequencer; } - FocusManager& focusManager() const { return *mFocusManager; } - HoverManager& hoverManager() const { return *mHoverManager; } - PointerManager& pointerManager() const { return *mPointerManager; } - KeyboardManager& keyboardManager() const { return *mKeyboardManager; } - LiveDataManager& dataManager() const { return *mDataManager; } - ExtensionManager& extensionManager() const { return *mExtensionManager; } - LayoutManager& layoutManager() const { return *mLayoutManager; } - MediaManager& mediaManager() const { return *mConfig.getMediaManager(); } - MediaPlayerFactory& mediaPlayerFactory() const { return *mConfig.getMediaPlayerFactory(); } - UIDManager& uniqueIdManager() const { return *mUniqueIdManager; } - - const YGConfigRef& ygconfig() const { return mYGConfigRef; } - CoreComponentPtr top() const { return mTop; } - const std::map& layouts() const { return mLayouts; } - const std::map& commands() const { return mCommands; } - const std::map& graphics() const { return mGraphics; } - const SessionPtr& session() const { return mSession; } - - RootContextData& lang(std::string lang) { mLang = lang; return *this; } - RootContextData& layoutDirection(LayoutDirection layoutDirection) { - mLayoutDirection = layoutDirection; return *this; - } - - /** - * @return The installed text measurement for this context. - */ - const TextMeasurementPtr& measure() const { return mTextMeasurement; } - - const RootConfig& rootConfig() const { return mConfig; } - - /** - * @return True if the screen lock is currently being held by a command. - */ - bool screenLock() { return mScreenLockCount > 0; } - - /** - * Acquire the screen lock - */ - void takeScreenLock() { mScreenLockCount++; } - - /** - * Release the screen lock - */ - void releaseScreenLock() { mScreenLockCount--; } - - /** - * @return internal text measurement cache. - */ - LruCache& cachedMeasures() { return mCachedMeasures; } - - /** - * @return internal text measurement baseline cache. - */ - LruCache& cachedBaselines() { return mCachedBaselines; } - - /** - * @return List of pending onMount handlers for recently inflated components. - */ - WeakPtrSet& pendingOnMounts() { return mPendingOnMounts; } - -#ifdef SCENEGRAPH - /** - * @return A cache of TextProperties - */ - sg::TextPropertiesCache& textPropertiesCache() { return *mTextPropertiesCache; } -#endif // SCENEGRAPH - -public: - int getPixelWidth() const { return mMetrics.getPixelHeight(); } - int getPixelHeight() const { return mMetrics.getPixelHeight(); } - double getWidth() const { return mMetrics.getWidth(); } - double getHeight() const { return mMetrics.getHeight(); } - Size getSize() const { return { static_cast(mMetrics.getWidth()), static_cast(mMetrics.getHeight())}; } - double getPxToDp() const { return Metrics::CORE_DPI / mMetrics.getDpi(); } - - std::string getTheme() const { return mRuntimeState.getTheme(); } - std::string getRequestedAPLVersion() const { return mRuntimeState.getRequestedAPLVersion(); } - std::string getLang() const { return mLang; } - LayoutDirection getLayoutDirection() const { return mLayoutDirection; } - bool getReinflationFlag() const { return mRuntimeState.getReinflation(); } - - const EventManagerPtr events; -#ifdef ALEXAEXTENSIONS - std::queue extesnionEvents; -#endif - std::set dirty; - std::set dirtyVisualContext; - std::set dirtyDatasourceContext; - -private: - RuntimeState mRuntimeState; - std::map mLayouts; - std::map mCommands; - std::map mGraphics; - Metrics mMetrics; - std::shared_ptr mStyles; - std::unique_ptr mSequencer; - std::unique_ptr mFocusManager; - std::unique_ptr mHoverManager; - std::unique_ptr mPointerManager; - std::unique_ptr mKeyboardManager; - std::unique_ptr mDataManager; - std::unique_ptr mExtensionManager; - std::unique_ptr mLayoutManager; - std::unique_ptr mUniqueIdManager; - YGConfigRef mYGConfigRef; - TextMeasurementPtr mTextMeasurement; - CoreComponentPtr mTop; // The top component - const RootConfig mConfig; - int mScreenLockCount; - SettingsPtr mSettings; - SessionPtr mSession; - std::string mLang; - LayoutDirection mLayoutDirection; - LruCache mCachedMeasures; - LruCache mCachedBaselines; - WeakPtrSet mPendingOnMounts; -#ifdef SCENEGRAPH - std::unique_ptr mTextPropertiesCache; -#endif // SCENEGRAPH -}; - - -} // namespace apl - -#endif //_APL_ROOT_CONTEXT_DATA_H diff --git a/aplcore/include/apl/engine/sharedcontextdata.h b/aplcore/include/apl/engine/sharedcontextdata.h new file mode 100644 index 0000000..86e6dc4 --- /dev/null +++ b/aplcore/include/apl/engine/sharedcontextdata.h @@ -0,0 +1,205 @@ +/** + * Copyright 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 _APL_SHARED_CONTEXT_DATA_H +#define _APL_SHARED_CONTEXT_DATA_H + +#include + +#include "apl/common.h" +#include "apl/content/content.h" +#include "apl/content/metrics.h" +#include "apl/content/settings.h" +#include "apl/engine/event.h" +#include "apl/engine/jsonresource.h" +#include "apl/engine/runtimestate.h" +#include "apl/engine/styles.h" +#include "apl/primitives/size.h" +#include "apl/primitives/textmeasurerequest.h" +#include "apl/utils/counter.h" +#include "apl/utils/lrucache.h" +#include "apl/utils/scopedset.h" + +#ifdef SCENEGRAPH +#include "apl/scenegraph/common.h" +#endif // SCENEGRAPH + +namespace apl { + +class DocumentRegistrar; +class EventManager; +class FocusManager; +class HoverManager; +class KeyboardManager; +class LayoutManager; +class PointerManager; +class TickScheduler; +class UIDGenerator; +class DataSourceConnection; +using DataSourceConnectionPtr = std::shared_ptr; + +class DirtyComponents : public ScopedSet { +public: + void clear() override { + for (auto& component : getAll()) + component->clearDirty(); + + ScopedSet::clear(); + } + + std::set extractScope(const DocumentContextDataPtr& documentData) override { + auto erased = ScopedSet::extractScope(documentData); + for (auto& component : erased) + component->clearDirty(); + + return erased; + } +}; + +/** + * Small utilities to check the pointer before dereference. + */ +template +T& deref(const std::unique_ptr& ptr) { + if (ptr == nullptr) + aplThrow("Can't dereference"); + return *ptr; +} + +template +T& deref(const std::shared_ptr& ptr) { + if (ptr == nullptr) + aplThrow("Can't dereference"); + return *ptr; +} + +/** + * Common data which is shared between rendered documents owned by one Core instance. + */ +class SharedContextData : public NonCopyable, public Counter, + public std::enable_shared_from_this { +public: + /** + * Stock constructor + * @param root RootContext pointer + * @param metrics Display metrics + * @param config Configuration settings + */ + SharedContextData(const CoreRootContextPtr& root, + const Metrics& metrics, + const RootConfig& config); + + /** + * Dummy constructor. Only used internally for test contexts. + * @param config Configuration settings + */ + SharedContextData(const RootConfig& config); + + ~SharedContextData(); + + /** + * Terminate common managers/processing + */ + void halt(); + + DocumentManager& documentManager() const { return deref(mDocumentManager); } + DocumentRegistrar& documentRegistrar() const { return deref(mDocumentRegistrar); } + FocusManager& focusManager() const { return deref(mFocusManager); } + HoverManager& hoverManager() const { return deref(mHoverManager); } + PointerManager& pointerManager() const { return deref(mPointerManager); } + KeyboardManager& keyboardManager() const { return deref(mKeyboardManager); } + LayoutManager& layoutManager() const { return deref(mLayoutManager); } + MediaManager& mediaManager() const { return deref(mMediaManager); } + MediaPlayerFactory& mediaPlayerFactory() const { return deref(mMediaPlayerFactory); } + TickScheduler& tickScheduler() const { return deref(mTickScheduler); } + DirtyComponents& dirtyComponents() const { return deref(mDirtyComponents); } + TimeManager& timeManager() const { return deref(mTimeManager); } + UIDGenerator& uidGenerator() const { return deref(mUniqueIdGenerator); } + EventManager& eventManager() const { return deref(mEventManager); } + DependantManager& dependantManager() const { return deref(mDependantManager); } + + const YGConfigRef& ygconfig() const { return mYGConfigRef; } + + /** + * @return The installed text measurement for this context. + */ + const TextMeasurementPtr& measure() const { return mTextMeasurement; } + + /** + * @return True if the screen lock is currently being held by a command. + */ + bool screenLock() const { return mScreenLockCount > 0; } + + /** + * Acquire the screen lock + */ + void takeScreenLock() { mScreenLockCount++; } + + /** + * Release the screen lock + */ + void releaseScreenLock() { mScreenLockCount--; } + + /** + * @return internal text measurement cache. + */ + LruCache& cachedMeasures() { return mCachedMeasures; } + + /** + * @return internal text measurement baseline cache. + */ + LruCache& cachedBaselines() { return mCachedBaselines; } + +#ifdef SCENEGRAPH + /** + * @return A cache of TextProperties + */ + sg::TextPropertiesCache& textPropertiesCache() { return *mTextPropertiesCache; } +#endif // SCENEGRAPH + +private: + std::string mRequestedVersion; + std::unique_ptr mDocumentRegistrar; + std::unique_ptr mFocusManager; + std::unique_ptr mHoverManager; + std::unique_ptr mPointerManager; + std::unique_ptr mKeyboardManager; + std::unique_ptr mLayoutManager; + std::unique_ptr mTickScheduler; + std::unique_ptr mDirtyComponents; + std::unique_ptr mUniqueIdGenerator; + std::unique_ptr mEventManager; + std::unique_ptr mDependantManager; + + const DocumentManagerPtr mDocumentManager; + std::shared_ptr mTimeManager; + std::shared_ptr mMediaManager; + std::shared_ptr mMediaPlayerFactory; + + YGConfigRef mYGConfigRef; + TextMeasurementPtr mTextMeasurement; + int mScreenLockCount = 0; + LruCache mCachedMeasures; + LruCache mCachedBaselines; + +#ifdef SCENEGRAPH + std::unique_ptr mTextPropertiesCache; +#endif // SCENEGRAPH +}; + + +} // namespace apl + +#endif //_APL_SHARED_CONTEXT_DATA_H diff --git a/aplcore/include/apl/engine/tickscheduler.h b/aplcore/include/apl/engine/tickscheduler.h new file mode 100644 index 0000000..b452769 --- /dev/null +++ b/aplcore/include/apl/engine/tickscheduler.h @@ -0,0 +1,45 @@ +/** +* Copyright 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 APL_TICK_SCHEDULER_H +#define APL_TICK_SCHEDULER_H + +#include + +#include "apl/document/coredocumentcontext.h" +#include "apl/primitives/object.h" +#include "apl/time/timemanager.h" + +namespace apl +{ + +class TickScheduler +{ +public: + explicit TickScheduler(const std::shared_ptr& timeManager); + + void processTickHandlers(const CoreDocumentContextPtr& documentContext) const; + +private: + void scheduleTickHandler(const std::weak_ptr& documentContext, + const Object& handler, + double delay) const; + + const std::shared_ptr mTimeManager; +}; + +} // namespace apl + +#endif // APL_TICK_SCHEDULER_H diff --git a/aplcore/include/apl/engine/typeddependant.h b/aplcore/include/apl/engine/typeddependant.h new file mode 100644 index 0000000..aa10be7 --- /dev/null +++ b/aplcore/include/apl/engine/typeddependant.h @@ -0,0 +1,91 @@ +/* + * Copyright 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 _APL_TYPED_DEPENDANT_H +#define _APL_TYPED_DEPENDANT_H + +#include +#include + +#include "apl/engine/dependant.h" +#include "apl/engine/evaluate.h" + +namespace apl { + +/** + * A template wrapper around Dependant that simplifies creating dependants for data-bound value + * propagation. The Downstream class should support the "setValue" method. + * + * @tparam Downstream The type of the downstream class. + * @tparam Key The type of key used by the downstream class + */ +template +class TypedDependant : public Dependant { +public: + static void create(const std::shared_ptr& downstream, + Key downstreamKey, + Object expression, + const ContextPtr& bindingContext, + BindingFunction bindingFunction, + BoundSymbolSet symbols) { + assert(!symbols.empty()); + assert(downstream); + + auto dependant = std::make_shared>(downstream, + downstreamKey, + std::move(expression), + bindingContext, + std::move(bindingFunction), + std::move(symbols)); + dependant->attach(); + downstream->addUpstream(downstreamKey, dependant); + } + + TypedDependant(const std::shared_ptr& downstream, + Key downstreamKey, + Object expression, + const ContextPtr& bindingContext, + BindingFunction bindingFunction, + BoundSymbolSet symbols) + : Dependant(std::move(expression), + bindingContext, + std::move(bindingFunction), + std::move(symbols)), + mDownstream(downstream), + mDownstreamKey(std::move(downstreamKey)) + {} + + void recalculate(bool useDirtyFlag) override { + auto bindingContext = mBindingContext.lock(); + auto downstream = mDownstream.lock(); + + if (bindingContext && downstream) { + auto result = applyDataBinding(*bindingContext, mExpression, mBindingFunction); + reattach(result.symbols); + downstream->setValue(mDownstreamKey, result.value, useDirtyFlag); + } + } + +private: + std::weak_ptr mDownstream; + Key mDownstreamKey; +}; + +using ContextDependant = TypedDependant; + +} // namespace apl + +#endif //_APL_TYPED_DEPENDANT_H diff --git a/aplcore/include/apl/engine/uidgenerator.h b/aplcore/include/apl/engine/uidgenerator.h new file mode 100644 index 0000000..ed5366b --- /dev/null +++ b/aplcore/include/apl/engine/uidgenerator.h @@ -0,0 +1,36 @@ +/** + * Copyright 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 _APL_UNIQUEIDGENERATOR_H +#define _APL_UNIQUEIDGENERATOR_H + +#include + +namespace apl { + +/** + * Simple UID generator, UIDs unique across same generator users. + */ +class UIDGenerator { +public: + std::string get(); + +private: + int mCurrentId = 1000; +}; + +} // namespace apl + +#endif // _APL_UNIQUEIDGENERATOR_H diff --git a/aplcore/include/apl/engine/uidmanager.h b/aplcore/include/apl/engine/uidmanager.h index 34b0412..048e7a6 100644 --- a/aplcore/include/apl/engine/uidmanager.h +++ b/aplcore/include/apl/engine/uidmanager.h @@ -20,6 +20,8 @@ #include #include "apl/common.h" +#include "apl/engine/uidgenerator.h" +#include "apl/utils/log.h" namespace apl { @@ -32,13 +34,25 @@ namespace apl { */ class UIDManager { public: - std::string create(UIDObject*element); - void remove(const std::string& id, UIDObject* element); + UIDManager(UIDGenerator& generator, const SessionPtr& session) : + mSession(session), mGenerator(generator) {} + + std::string create(UIDObject* element); + UIDObject* find(const std::string& id); - UIDObject*find(const std::string& id); + void terminate() { mTerminated = true; } private: + SessionPtr mSession; + UIDGenerator& mGenerator; std::map mMap; + bool mTerminated = false; + +private: + friend class UIDObject; + + // Only used in UIDObject destructor + void remove(const std::string& id, UIDObject* element); }; } // namespace apl diff --git a/aplcore/include/apl/engine/uidobject.h b/aplcore/include/apl/engine/uidobject.h index 002db1f..46dec52 100644 --- a/aplcore/include/apl/engine/uidobject.h +++ b/aplcore/include/apl/engine/uidobject.h @@ -13,15 +13,16 @@ * permissions and limitations under the License. */ -#ifndef _APL_UNIQUEID_H -#define _APL_UNIQUEID_H +#ifndef _APL_UNIQUEIDOBJECT_H +#define _APL_UNIQUEIDOBJECT_H #include "rapidjson/document.h" -#include "apl/common.h" - #include +#include "apl/common.h" +#include "apl/utils/noncopyable.h" + namespace apl { /** @@ -33,7 +34,7 @@ namespace apl { * The object also contains a unique type field which is used for run-time type identification * handled by the UIDObjectWrapper template. */ -class UIDObject { +class UIDObject : public NonCopyable { public: // Not ideal, but required to distinguish extending class without RTTI. enum class UIDObjectType { @@ -79,4 +80,4 @@ class UIDObject { } // namespace apl -#endif // _APL_UNIQUEID_H +#endif // _APL_UNIQUEIDOBJECT_H diff --git a/aplcore/include/apl/extension/extensionclient.h b/aplcore/include/apl/extension/extensionclient.h index cd8206f..efa137e 100644 --- a/aplcore/include/apl/extension/extensionclient.h +++ b/aplcore/include/apl/extension/extensionclient.h @@ -17,18 +17,21 @@ #define _APL_EXTENSION_CLIENT_H #include "apl/common.h" +#include "apl/content/extensioncommanddefinition.h" #include "apl/content/extensioncomponentdefinition.h" +#include "apl/content/extensioneventhandler.h" +#include "apl/content/extensionfilterdefinition.h" #include "apl/content/extensionproperty.h" #include "apl/content/jsondata.h" #include "apl/engine/event.h" #include "apl/livedata/livedataobjectwatcher.h" +#include "apl/livedata/liveobject.h" #include "apl/utils/counter.h" #include "apl/utils/noncopyable.h" #include "apl/utils/session.h" namespace apl { -class RootConfig; class LiveArrayChange; class LiveMapChange; @@ -115,7 +118,22 @@ class ExtensionEvent { }; /** - * Extension processing client. Refer to unittest_extension.client.cpp for suggested lifecycle. + * Encapsulate schema information that ExtensionClient is responsible for collecting during + * registration. This information can be retrieved via @see ExtensionClient::extensionSchema + */ +struct ParsedExtensionSchema { + Object environment; + std::map types; + std::vector eventHandlers; + std::vector commandDefinitions; + std::vector filterDefinitions; + std::vector componentDefinitions; + std::map liveData; + std::map eventModes; +}; + +/** + * Extension processing client. Refer to unittest_extension_client.cpp for suggested lifecycle. */ class ExtensionClient : public Counter, public LiveDataObjectWatcher, @@ -126,13 +144,24 @@ class ExtensionClient : public Counter, * @param rootConfig rootConfig pointer. * @param uri Requested extension URI. * @return ExtensionClient pointer. + * @deprecated Extensions should be managed via ExtensionMediator + * uri, const SessionPtr& session); */ - static ExtensionClientPtr create(const RootConfigPtr& rootConfig, const std::string& uri); + APL_DEPRECATED static ExtensionClientPtr create(const RootConfigPtr& rootConfig, const std::string& uri); /** - * Constructor. Do not use - use create() instead. + * @param rootConfig rootConfig pointer. + * @param uri Requested extension URI. + * @param session Session + * @return ExtensionClient pointer. + * @deprecated Extensions should be managed via ExtensionMediator */ - ExtensionClient(const RootConfigPtr& rootConfig, const std::string& connectionToken); + static ExtensionClientPtr create(const RootConfigPtr& rootConfig, const std::string& uri, const SessionPtr& session); + + /** + * Constructor. Do not use - let ExtensionMediator create clients instead. + */ + ExtensionClient(const std::string& uri, const SessionPtr& session, const Object& flags); /** * Destructor @@ -173,21 +202,31 @@ class ExtensionClient : public Counter, rapidjson::Value createComponentChange(rapidjson::Document::AllocatorType& allocator, ExtensionComponent& component); + /** + * @return The URI of the extension + */ + std::string getUri() const; + /** * @return True if RegisterSuccess or RegisterFailure was processed. False otherwise. */ - bool registrationMessageProcessed(); + bool registrationMessageProcessed() const; /** * @return True if extension was successfully registered. False otherwise. */ - bool registered(); + bool registered() const; /** * @return True if extension failed to register (i.e. registration was processed but failed). * False otherwise. */ - bool registrationFailed(); + bool registrationFailed() const; + + /** + * @return Extension-related information collected during registration + */ + const ParsedExtensionSchema& extensionSchema() const; /** * @return The assigned connection token. @@ -206,7 +245,7 @@ class ExtensionClient : public Counter, * Associate a RootContext to the mediator for events and live data triggers. * @param rootContext ctx to bind. */ - void bindContext(const RootContextPtr& rootContext); + void bindContext(const CoreRootContextPtr& rootContext); /** * Process extension command into serialized command request. @@ -248,6 +287,9 @@ class ExtensionClient : public Counter, void liveDataObjectFlushed(const std::string& key, LiveDataObject& liveDataObject) override; private: + friend class ExtensionMediator; + friend class ExtensionManager; + // Parse an extension from json bool readExtension(const Context& context, const Object& extension); bool readExtensionTypes(const Context& context, const Object& types); @@ -271,26 +313,30 @@ class ExtensionClient : public Counter, void reportLiveArrayChanges(const LiveDataRef& ref, const std::vector& changes); void sendLiveDataEvent(const std::string& event, const Object& current, const Object& changed); - void flushPendingEvents(const RootContextPtr& rootContext); + void flushPendingEvents(const CoreDocumentContextPtr& rootContext); std::map readPropertyTriggers(const Context& context, const TypePropertiesPtr& type, const Object& triggers); void invokeExtensionHandler(const std::string& uri, const std::string& name, const ObjectMap& data, bool fastMode, std::string resourceId = ""); + void bindContextInternal(const CoreDocumentContextPtr& documentContext); + bool processMessageInternal(const CoreDocumentContextPtr& documentContext, JsonData&& message); + bool handleDisconnectionInternal(const CoreDocumentContextPtr& documentContext, int errorCode, const std::string& message); static id_type sCommandIdGenerator; bool mRegistrationProcessed; bool mRegistered; std::string mUri; - std::weak_ptr mRootConfig; + ParsedExtensionSchema mSchema; + SessionPtr mSession; + Object mFlags; + std::shared_ptr mInternalRootConfig; std::string mConnectionToken; std::map mLiveData; std::map mActionRefs; - std::map mTypes; - std::map mEventModes; - std::weak_ptr mCachedContext; + std::weak_ptr mCachedContext; std::vector mPendingEvents; }; diff --git a/aplcore/include/apl/extension/extensionmanager.h b/aplcore/include/apl/extension/extensionmanager.h index ad953a2..c835d9f 100644 --- a/aplcore/include/apl/extension/extensionmanager.h +++ b/aplcore/include/apl/extension/extensionmanager.h @@ -23,6 +23,7 @@ #include "apl/content/extensioncomponentdefinition.h" #include "apl/content/extensioneventhandler.h" #include "apl/content/extensionfilterdefinition.h" +#include "apl/content/extensionrequest.h" #include "apl/engine/builder.h" #include "apl/extension/extensioncomponent.h" #include "apl/primitives/object.h" @@ -37,12 +38,30 @@ class ExtensionMediator; */ class ExtensionManager { public: - ExtensionManager(const std::vector>& requests, const RootConfig& rootConfig); + ExtensionManager( + const std::vector& requests, + const RootConfig& rootConfig, + const SessionPtr& session); /** - * @return A map of qualified name to the extension event handler definition. + * @return A map of qualified names to event handler definitions. */ - const std::map& qualifiedHandlerMap() const { return mQualifiedEventHandlerMap; } + const std::map& getEventHandlerDefinitions() const { return mEventHandlers; } + + /** + * @return A map of qualified names to command definitions. + */ + const std::map& getCommandDefinitions() const { return mCommandDefinitions; } + + /** + * @return A map of qualified names to component definitions. + */ + const std::map& getComponentDefinitions() const { return mComponentDefinitions; } + + /** + * @return A map of qualified names to filter definitions. + */ + const std::map& getFilterDefinitions() const { return mFilterDefinitions; } /** * Add a document or package-level event handler by name. These are added as @@ -77,7 +96,7 @@ class ExtensionManager { * @param qualifiedName The name of the custom component in the form EXT_NAME:COMPONENT_NAME * @return The component definition or nullptr if it is not found */ - ExtensionComponentDefinition* findComponentDefinition(const std::string& qualifiedName); + ExtensionComponentDefinition* findComponentDefinition(const std::string& qualifiedName); /** * Search the custom filters for one with the given name. @@ -124,7 +143,7 @@ class ExtensionManager { * Returns the map of extension component definitions maintained by the manager. * @return Map of extension component definitions. */ - const std::map& getExtensionComponentDefinitions() const { return mExtensionComponentDefs; } + const std::map& getExtensionComponentDefinitions() const { return mComponentDefinitions; } /** * Notify extensions that the component has changed state or has a property update. @@ -135,11 +154,11 @@ class ExtensionManager { void notifyComponentUpdate(const ExtensionComponentPtr& component, bool resourceNeeded); private: - std::map mQualifiedEventHandlerMap; // Qualified name to extension event handler - std::map mExtensionCommands; // Qualified name to extension command definition - std::map mExtensionComponentDefs; // Qualified name to extension component definition - std::map mExtensionFilters; // Qualified name to extension filter definition - std::map mExtensionEventHandlers; + std::map mEventHandlers; // Qualified name to extension event handler + std::map mCommandDefinitions; // Qualified name to extension command definition + std::map mComponentDefinitions; // Qualified name to extension component definition + std::map mFilterDefinitions; // Qualified name to extension filter definition + std::map mEventHandlerCommandMap; std::map mExtensionComponents; // ResourceId to extension component ObjectMapPtr mEnvironment; // mediator processes extension messages diff --git a/aplcore/include/apl/extension/extensionmediator.h b/aplcore/include/apl/extension/extensionmediator.h index a03f02c..89cba34 100644 --- a/aplcore/include/apl/extension/extensionmediator.h +++ b/aplcore/include/apl/extension/extensionmediator.h @@ -26,17 +26,18 @@ #include "apl/content/content.h" #include "apl/content/rootconfig.h" -#include "apl/engine/rootcontext.h" +#include "apl/document/displaystate.h" #include "apl/extension/extensionclient.h" #include "apl/extension/extensionsession.h" namespace apl { -class RootContext; class ExtensionSessionState; using ExtensionsLoadedCallback = std::function; +using ExtensionsLoadedCallbackV2 = std::function; + /** * This class mediates message passing between "local" alexaext::Extension and the APL engine. It * is intended for internal use by the viewhost. Remote extensions are not supported. @@ -131,36 +132,74 @@ class ExtensionMediator : public std::enable_shared_from_this ExtensionGrantResult deny)>; /** - * Initialize extensions available in provided content. Performance gains can be made - * by initializing extensions as each apl::Content package is loaded. Once Content is ready, - * and all packages have been initialized, @c loadExtensions should be used to register the + * @deprecated Use ExtensionMediator::initializeExtensions(const ObjectMap& flagMap, const + * ContentPtr& content, const ExtensionGrantRequestCallback& grantHandler) instead + */ + void initializeExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + const ExtensionGrantRequestCallback& grantHandler = nullptr); + + /** + * @deprecated Use ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& + * content, ExtensionsLoadedCallbackV2 loaded) instead + */ + void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, ExtensionsLoadedCallback loaded); + + /** + * @deprecated Use ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& + * content, ExtensionsLoadedCallbackV2 loaded) instead + */ + void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + ExtensionsLoadedCallbackV2 loaded); + + /** + * @deprecated Use ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& + * content, const std::set* grantedExtensions) instead + */ + void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + const std::set* grantedExtensions = nullptr); + + /** + * Initialize extensions available in provided content. Performance gains can be made by + * initializing extensions as each apl::Content package is loaded. Once Content is ready, and + * all packages have been initialized, @c loadExtensions should be used to register the * extensions for use. * - * An optional grant request handler is used to grant/deny use of the extension. In the - * absence of the grant handler use of the extension is automatically granted. Calling - * loadExtensions before a grant/deny response results in the extension being unavailable for - * use. + * An optional grant request handler is used to grant/deny use of the extension. In the absence + * of the grant handler use of the extension is automatically granted. Calling loadExtensions + * before a grant/deny response results in the extension being unavailable for use. * - * @param rootConfig The RootConfig receiving the registered extensions. + * @param flagMap A map of runtime-provided flags in the form uri->flags * @param content The document content, contains requested extensions * @param grantHandler Callback that grants use of the extension. */ - void initializeExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + void initializeExtensions(const ObjectMap& flagMap, const ContentPtr& content, const ExtensionGrantRequestCallback& grantHandler = nullptr); /** - * Register the extensions found in the associated alexaext::ExtensionProvider. This method - * should be used in conjunction with @c initializeExtensions. Performance gains can be made - * by initializing extensions as each Content package is loaded. @c initializeExtensions. + * Register the extensions found in the associated alexaext::ExtensionProvider. This method + * should be used in conjunction with @c initializeExtensions. Performance gains can be made by + * initializing extensions as each Content package is loaded. @c initializeExtensions. * * Must be called before RootContext::create(); * - * @param rootConfig The RootConfig receiving the registered extensions. + * @param flagMap A map of runtime-provided flags in the form uri->flags * @param content The document content, contains requested extensions and extension settings. * @param loaded Callback to be called when all extensions required by the doc are loaded. */ - void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, - ExtensionsLoadedCallback loaded); + void loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, ExtensionsLoadedCallback loaded); + + /** + * Register the extensions found in the associated alexaext::ExtensionProvider. This method + * should be used in conjunction with @c initializeExtensions. Performance gains can be made by + * initializing extensions as each Content package is loaded. @c initializeExtensions. + * + * Must be called before RootContext::create(); + * + * @param flagMap A map of runtime-provided flags in the form uri->flags + * @param content The document content, contains requested extensions and extension settings. + * @param loaded Callback to be called when all extensions required by the doc are loaded. + */ + void loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, ExtensionsLoadedCallbackV2 loaded); /** * Register the extensions found in the associated alexaext::ExtensionProvider. This method @@ -173,14 +212,13 @@ class ExtensionMediator : public std::enable_shared_from_this * * Must be called before RootContext::create(); * - * @param rootConfig The RootConfig receiving the registered extensions. + * @param flagMap A map of runtime-provided flags in the form uri->flags * @param content The document content, contains requested extensions and extension settings. * @param grantedExtensions Pre-granted extensions, may be null. */ - void loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, + void loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, const std::set* grantedExtensions = nullptr); - /** * Process an extension event. The extension must be registered in the associated * alexaext::ExtensionProvider. @@ -189,7 +227,7 @@ class ExtensionMediator : public std::enable_shared_from_this * @param root The root context. * @return true if the command was invoked. */ - bool invokeCommand(const Event& even); + bool invokeCommand(const Event& event); /** * Notify the extension that the component has changed. Changes may be a result of document @@ -257,31 +295,32 @@ class ExtensionMediator : public std::enable_shared_from_this void onDisplayStateChanged(DisplayState displayState); private: - friend class RootContext; + friend class CoreDocumentContext; + friend class ExtensionManager; /** * Initialize an extension that was granted approval for use. */ - void grantExtension(const RootConfigPtr& rootConfig, const std::string& uri); + void grantExtension(const Object& flags, const std::string& uri); /** * Stop initialization on a denied extension. */ - void denyExtension(const RootConfigPtr& rootConfig, const std::string& uri); + void denyExtension(const std::string& uri); /** * Perform extension registration requests. */ - void loadExtensionsInternal(const RootConfigPtr& rootConfig, const ContentPtr& content); + void loadExtensionsInternal(const ObjectMap& flagMap, const ContentPtr& content); /** - * Associate a RootContext to the mediator for event and live data updates. + * Associate a CoreDocumentContext to the mediator for event and live data updates. + * @param context Pointer to the DocumentContext. */ - void bindContext(const RootContextPtr& context); + void bindContext(const CoreDocumentContextPtr& context); /** - * Registers the extensions found in the ExtensionProvider by calling - * RootConfig::registerExtensionXXX(). + * Registers an extensions found in the ExtensionProvider */ void registerExtension(const std::string& uri, const alexaext::ExtensionProxyPtr& extension, @@ -312,6 +351,12 @@ class ExtensionMediator : public std::enable_shared_from_this */ ExtensionClientPtr getClient(const std::string& uri); + /** + * Get the clients associated with this mediator + * @return a map of extension URIs to clients + */ + const std::map& getClients(); + /** * Send a resource to an extension. * @param component ExtensionComponent reference. @@ -358,7 +403,7 @@ class ExtensionMediator : public std::enable_shared_from_this */ void unregister(const alexaext::ActivityDescriptorPtr& activity); - private: +private: // access to the extensions std::weak_ptr mProvider; // access to the extension resources @@ -368,9 +413,9 @@ class ExtensionMediator : public std::enable_shared_from_this // Extension session, if provided (nullptr otherwise) ExtensionSessionPtr mExtensionSession; // the context that events and data updates are forwarded to - std::weak_ptr mRootContext; - // reference to associated config - std::weak_ptr mRootConfig; + std::weak_ptr mDocumentContext; + // session extracted from loaded content + SessionPtr mSession; // retro extension wrapper used for message passing std::map> mClients; // Determines whether incoming messages from extensions should be processed. @@ -379,8 +424,12 @@ class ExtensionMediator : public std::enable_shared_from_this std::set mPendingGrants; // Pending Extensions to register. std::set mPendingRegistrations; + // Required extensions list + std::set mRequired; + // Mediator is in fail state if true + bool mFailState = false; // Extensions loaded callback - ExtensionsLoadedCallback mLoadedCallback; + ExtensionsLoadedCallbackV2 mLoadedCallback; std::unordered_map mActivitiesByURI; }; diff --git a/aplcore/include/apl/focus/focusmanager.h b/aplcore/include/apl/focus/focusmanager.h index a54c928..999aec4 100644 --- a/aplcore/include/apl/focus/focusmanager.h +++ b/aplcore/include/apl/focus/focusmanager.h @@ -16,16 +16,12 @@ #ifndef _APL_FOCUS_MANAGER_H #define _APL_FOCUS_MANAGER_H -#include -#include #include "apl/common.h" #include "apl/focus/focusdirection.h" #include "apl/focus/focusfinder.h" namespace apl { -class CoreComponent; -class RootContextData; class Rect; static const std::string FOCUS_SEQUENCER = "__FOCUS_SEQUENCER"; @@ -59,7 +55,7 @@ static const std::string FOCUS_SEQUENCER = "__FOCUS_SEQUENCER"; */ class FocusManager { public: - explicit FocusManager(const RootContextData& core); + explicit FocusManager(const CoreRootContext& core); /** * Focus next available component based on provided parameters. Requires some component to be focused. @@ -145,7 +141,7 @@ class FocusManager { CoreComponentPtr getFocus() { return mFocused.lock(); } private: - const RootContextData& mCore; + const CoreRootContext& mCore; std::unique_ptr mFinder; std::weak_ptr mFocused; diff --git a/aplcore/include/apl/graphic/graphic.h b/aplcore/include/apl/graphic/graphic.h index de0bb86..fe145be 100644 --- a/aplcore/include/apl/graphic/graphic.h +++ b/aplcore/include/apl/graphic/graphic.h @@ -40,7 +40,6 @@ class Graphic : public UIDObject, public UserData, public ObjectData { friend class GraphicElement; - friend class GraphicDependant; friend class VectorGraphicComponent; public: diff --git a/aplcore/include/apl/graphic/graphicdependant.h b/aplcore/include/apl/graphic/graphicdependant.h deleted file mode 100644 index 519833c..0000000 --- a/aplcore/include/apl/graphic/graphicdependant.h +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 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 _APL_GRAPHIC_DEPENDANT_H -#define _APL_GRAPHIC_DEPENDANT_H - -#include "apl/common.h" -#include "apl/engine/dependant.h" -#include "apl/engine/binding.h" -#include "apl/graphic/graphicproperties.h" -#include "apl/primitives/object.h" - -namespace apl { - -/** - * A dependant relationship where a change in the upstream context results in a recalculation - * of a downstream graphic element property. - * - * The dependant stores the parsed Node expression. When the upstream context changes, - * the Node expression is recalculated and the new value is stored in the downstream - * GraphicElement. The GraphicElement will set dirty flags to indicate that it needs - * to be reloaded and redrawn. - */ -class GraphicDependant : public Dependant { -public: - static void create(const GraphicElementPtr& downstreamGraphicElement, - GraphicPropertyKey downstreamKey, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction); - - GraphicDependant(const GraphicElementPtr& downstreamGraphicElement, - GraphicPropertyKey downstreamKey, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction) - : Dependant(equation, bindingContext, bindingFunction), - mDownstreamGraphicElement(downstreamGraphicElement), - mDownstreamKey(downstreamKey) - {} - - void recalculate(bool useDirtyFlag) const override; - -private: - std::weak_ptr mDownstreamGraphicElement; - GraphicPropertyKey mDownstreamKey; -}; - -} // namespace apl - -#endif // _APL_GRAPHIC_DEPENDANT_H diff --git a/aplcore/include/apl/graphic/graphicelement.h b/aplcore/include/apl/graphic/graphicelement.h index 5d76764..3ccd162 100644 --- a/aplcore/include/apl/graphic/graphicelement.h +++ b/aplcore/include/apl/graphic/graphicelement.h @@ -53,10 +53,8 @@ class GraphicElement : public UIDObject, public std::enable_shared_from_this, public RecalculateTarget, public UserData, - public NonCopyable, public Counter { friend class Graphic; - friend class GraphicDependant; friend class GraphicBuilder; public: @@ -177,10 +175,11 @@ class GraphicElement : public UIDObject, static Object asAvgFill(const Context& context, const Object& object); + void setValue(GraphicPropertyKey key, const Object& value, bool useDirtyFlag) override; + protected: virtual bool initialize(const GraphicPtr& graphic, const Object& json); virtual const GraphicPropDefSet& propDefSet() const = 0; - bool setValue(GraphicPropertyKey key, const Object& value, bool useDirtyFlag); StyleInstancePtr getStyle(const GraphicPtr& graphic) const; void updateStyleInternal(const StyleInstancePtr& stylePtr, const GraphicPropDefSet& gds); diff --git a/aplcore/include/apl/media/mediamanager.h b/aplcore/include/apl/media/mediamanager.h index 12f4787..c49d191 100644 --- a/aplcore/include/apl/media/mediamanager.h +++ b/aplcore/include/apl/media/mediamanager.h @@ -42,7 +42,9 @@ class MediaManager { * @param type The type of media requested. This should be removed in the future. * @return the media object */ - virtual MediaObjectPtr request(const std::string& url, EventMediaType type) = 0; + virtual MediaObjectPtr request(const std::string& url, EventMediaType type) { + return request(url, type, HeaderArray()); + } /** * Request a media object diff --git a/aplcore/include/apl/media/mediaplayer.h b/aplcore/include/apl/media/mediaplayer.h index 40e8eb5..d8e90da 100644 --- a/aplcore/include/apl/media/mediaplayer.h +++ b/aplcore/include/apl/media/mediaplayer.h @@ -138,6 +138,18 @@ class MediaPlayer { */ virtual void seek( int offset ) = 0; + /** + * Pause video playback and change the position of the player. The offset is an absolute value. + * If this value is less than the current track offset then we set this value to the current track offset. + * If this value is more than the entire track duration then we set this value to the end of current track. + * The repeat counter is not changed. + * + * Events: onPause, onTimeUpdate + * + * @param offset Offset in milliseconds + */ + virtual void seekTo( int offset ) {} + /** * Pause video playback and change the current track. This command is ignored if the trackIndex * is not valid (but the video will pause). The repeat counter for the track is reloaded even diff --git a/aplcore/include/apl/primitives/accessibilityaction.h b/aplcore/include/apl/primitives/accessibilityaction.h index c301dac..8d6c8a7 100644 --- a/aplcore/include/apl/primitives/accessibilityaction.h +++ b/aplcore/include/apl/primitives/accessibilityaction.h @@ -103,18 +103,15 @@ class AccessibilityAction : public ObjectData, rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override; + void setValue(AccessibilityActionKey key, const Object& value, bool useDirtyFlag) override; + /** * Internal constructor. Do not use directly; instead, use the "create" static method. */ AccessibilityAction(const CoreComponentPtr& component, std::string name, std::string label) : mComponent(component), mName(std::move(name)), mLabel(std::move(label)), mEnabled(true) {} - class ObjectType final : public PointerHolderObjectType { - public: - bool equals(const Object::DataHolder& lhs, const Object::DataHolder& rhs) const override { - return *lhs.data == *rhs.data; - } - }; + class ObjectType final : public PointerHolderObjectType {}; protected: void initialize(const ContextPtr& context, const Object& object); diff --git a/aplcore/include/apl/datagrammar/boundsymbol.h b/aplcore/include/apl/primitives/boundsymbol.h similarity index 62% rename from aplcore/include/apl/datagrammar/boundsymbol.h rename to aplcore/include/apl/primitives/boundsymbol.h index df2c763..d0eda51 100644 --- a/aplcore/include/apl/datagrammar/boundsymbol.h +++ b/aplcore/include/apl/primitives/boundsymbol.h @@ -17,53 +17,43 @@ #define _APL_BOUND_SYMBOL_H #include "apl/primitives/objecttype.h" -#include "apl/primitives/symbolreferencemap.h" namespace apl { -namespace datagrammar { /** * A reference to a symbol in a specific context. Bound symbols are used in equations * to retrieve the current value of a symbol. They hold a weak pointer to the bound - * context to avoid referential loops. Bounds symboles are normallly only used for mutable + * context to avoid referential loops. Bounds symbols are normally only used for mutable * values (immutable values should be directly referenced). */ -class BoundSymbol : public ObjectData +class BoundSymbol { public: BoundSymbol(const ContextPtr& context, std::string name) : mContext(context), mName(std::move(name)) {} - /** - * @return The newly evaluated value of the symbol - */ - Object eval() const override; - - SymbolReference getSymbol() const { return SymbolReference(mName + "/", mContext.lock()); } - - std::string toDebugString() const override; + ContextPtr getContext() const { return mContext.lock(); } + std::string getName() const { return mName; } + // Standard methods for a EvaluableReferenceHolderObjectType + bool truthy() const; + rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; + std::string toDebugString() const; bool operator==(const BoundSymbol& rhs) const; + bool operator<(const BoundSymbol& rhs) const; + bool empty() const; + Object eval() const; friend streamer& operator<<(streamer&, const BoundSymbol&); - class ObjectType final : public EvaluableObjectType { - public: - rapidjson::Value serialize( - const Object::DataHolder&, - rapidjson::Document::AllocatorType& allocator) const override - { - return rapidjson::Value("BOUND SYMBOL", allocator); - } - }; + class ObjectType final : public EvaluableReferenceObjectType {}; private: std::weak_ptr mContext; std::string mName; }; -} // namespace datagrammar } // namespace apl #endif // _APL_BOUND_SYMBOL_H diff --git a/aplcore/include/apl/primitives/boundsymbolset.h b/aplcore/include/apl/primitives/boundsymbolset.h new file mode 100644 index 0000000..9c9f246 --- /dev/null +++ b/aplcore/include/apl/primitives/boundsymbolset.h @@ -0,0 +1,64 @@ +/** + * Copyright 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 _APL_BOUND_SYMBOL_SET_H +#define _APL_BOUND_SYMBOL_SET_H + +#include +#include + +#include "apl/common.h" +#include "apl/primitives/boundsymbol.h" + +namespace apl { + +/** + * A sorted set of unique BoundSymbols. Because the set is expected to be small, we internally + * store this as a sorted std::vector rather than a std::set. + */ +class BoundSymbolSet { +public: + BoundSymbolSet() = default; + BoundSymbolSet(BoundSymbolSet&&) = default; + + BoundSymbolSet& operator=(const BoundSymbolSet& other) { + if (&other == this) + return *this; + mSymbols = other.mSymbols; + return *this; + } + + void emplace(const BoundSymbol& boundSymbol); + + bool empty() const { return mSymbols.empty(); } + size_t size() const { return mSymbols.size(); } + void clear() { mSymbols.clear(); } + + bool operator==(const BoundSymbolSet& other) const { return mSymbols == other.mSymbols; } + bool operator!=(const BoundSymbolSet& other) const { return mSymbols != other.mSymbols; } + +private: + std::vector mSymbols; + +public: + auto begin() ->decltype(mSymbols)::iterator { return mSymbols.begin(); } + auto end() -> decltype(mSymbols)::iterator { return mSymbols.end(); } + auto begin() const ->decltype(mSymbols)::const_iterator { return mSymbols.begin(); } + auto end() const -> decltype(mSymbols)::const_iterator { return mSymbols.end(); } +}; + +} // namespace apl + +#endif // _APL_BOUND_SYMBOL_SET_H diff --git a/aplcore/include/apl/primitives/commanddata.h b/aplcore/include/apl/primitives/commanddata.h new file mode 100644 index 0000000..3b97b5c --- /dev/null +++ b/aplcore/include/apl/primitives/commanddata.h @@ -0,0 +1,58 @@ +/** + * Copyright 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 _APL_COMMAND_DATA +#define _APL_COMMAND_DATA + +#include "apl/primitives/object.h" + +namespace apl { + +/** + * Simple wrapper class for data which commands are inflated from. Main purpose is to maintain + * origin memory alive in cases when down level commands created from references to the top one + * (which is almost always the case). + * This class relies on Object being trivially copyable and actual references been maintained by + * ObjectData (as shared pointers). + */ +class CommandData { +public: + CommandData(const Object& data) : mOrigin(Object::NULL_OBJECT()), mData(data) {} + + CommandData(Object&& data, const CommandData& originData) : + mOrigin(originData.origin()), mData(std::move(data)) {} + + CommandData(const Object& data, const CommandData& originData) : + mOrigin(originData.origin()), mData(data) {} + + const Object& get() const { return mData; } + + size_t size() const { return mData.size(); } + + CommandData at(size_t index) const { + assert(mData.isArray()); + return {mData.at(index), *this}; + } + +private: + const Object& origin() const { return mOrigin.isNull() ? mData : mOrigin; } + + // Origin set in cases when command data derived from other command data. + Object mOrigin; + Object mData; +}; +} + +#endif // _APL_MEDIA_STATE_H \ No newline at end of file diff --git a/aplcore/include/apl/primitives/keyboard.h b/aplcore/include/apl/primitives/keyboard.h index 73c128d..cea7398 100644 --- a/aplcore/include/apl/primitives/keyboard.h +++ b/aplcore/include/apl/primitives/keyboard.h @@ -20,7 +20,8 @@ #include #include "rapidjson/document.h" -#include "object.h" + +#include "apl/primitives/object.h" namespace apl { diff --git a/aplcore/include/apl/primitives/object.h b/aplcore/include/apl/primitives/object.h index 134b6e4..8c12c53 100644 --- a/aplcore/include/apl/primitives/object.h +++ b/aplcore/include/apl/primitives/object.h @@ -56,7 +56,6 @@ namespace apl { namespace datagrammar { - class BoundSymbol; class ByteCode; } @@ -66,7 +65,6 @@ class Dimension; class LiveDataObject; class RangeGenerator; class SliceGenerator; -class SymbolReferenceMap; class ObjectData; class ObjectType; @@ -308,9 +306,6 @@ class Object // BoundSymbol, and compiled ByteCodeInstruction objects bool isPure() const; - // BoundSymbol, ByteCode: Add any symbols defined by this node to the "symbols" set - void symbols(SymbolReferenceMap& symbols) const; - // FUNCTION & Easing objects Object call(const ObjectArray& args) const; diff --git a/aplcore/include/apl/primitives/objectdata.h b/aplcore/include/apl/primitives/objectdata.h index 46834a8..ca36631 100644 --- a/aplcore/include/apl/primitives/objectdata.h +++ b/aplcore/include/apl/primitives/objectdata.h @@ -596,6 +596,7 @@ class JSONDocumentData : public JSONBaseData { * std::string toDebugString() const; * bool operator==( const T& other ) const; * bool empty() const; + * bool truthy() const; * rapidjson::Value serialize(rapidjson::document::AllocatorType& allocator) const; * * @tparam T The type of the stored object. @@ -634,6 +635,50 @@ class DirectObjectData : public ObjectData { T mData; }; +/****************************************************************************/ + +/** + * The EvaluableDirectObjectData is based on DirectObjectData with support + * for the eval() object method. + * + * @tparam T The type of the stored object. + */ +template +class EvaluableDirectObjectData : public ObjectData { +public: + static std::shared_ptr> create(T &&data) { + return std::make_shared>(std::move(data)); + } + + explicit EvaluableDirectObjectData(T &&data) : mData(std::move(data)) {} + + /** + * Internal method for accessing the inner object stored here. + * Eventually we will shift this to return const T& + * @return a pointer to the raw data stored in this object + */ + const void *inner() const override { return &mData; } + + bool empty() const override { return mData.empty(); } + + bool truthy() const override { return mData.truthy(); } + + Object eval() const override { return mData.eval(); } + + std::string toDebugString() const override { return mData.toDebugString(); } + + bool operator==(const ObjectData& other) const override { + return mData == static_cast &>(other).mData; + } + + rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override { + return mData.serialize(allocator); + } + +private: + T mData; +}; + } // namespace apl #endif // _APL_OBJECT_DATA_H diff --git a/aplcore/include/apl/primitives/objecttype.h b/aplcore/include/apl/primitives/objecttype.h index 55c5f72..62340a0 100644 --- a/aplcore/include/apl/primitives/objecttype.h +++ b/aplcore/include/apl/primitives/objecttype.h @@ -143,7 +143,7 @@ class ObjectType : public NonCopyable { * Check if data in 2 objects are equal. * @param lhs DataHolder of object of current type. * @param rhs Other Object's DataHolder. - * @return + * @return true if equal, false otherwise. */ virtual bool equals(const Object::DataHolder& lhs, const Object::DataHolder& rhs) const { return false; } @@ -340,6 +340,25 @@ class EvaluableObjectType : public PointerHolderObjectType { } }; +/*** + * Store a referenced class in Object which supports the "eval" method. + * @tparam T + */ +template +class EvaluableReferenceObjectType : public ReferenceHolderObjectType { +public: + bool isEvaluable() const final { return true; } + + Object eval(const Object::DataHolder& dataHolder) const final { + return dataHolder.data->eval(); + } + + static std::shared_ptr createDirectObjectData(T&& content) { + return EvaluableDirectObjectData::create(std::move(content)); + } +}; + + /// Primitive types class Null { @@ -451,8 +470,20 @@ class Number { const Object::DataHolder& dataHolder, rapidjson::Document::AllocatorType& allocator) const override { - return std::isfinite(dataHolder.value) ? rapidjson::Value(dataHolder.value) - : rapidjson::Value(); + if (!std::isfinite(dataHolder.value)) + return {}; + + // Check to see if this value is an integer + double intPart; + double remainder = std::modf(dataHolder.value, &intPart); + + // 2^53: Largest integer such that it and all smaller integers can + // be stored in a double without losing precision. + if (remainder == 0.0f && std::abs(intPart) <= 2e53) + return rapidjson::Value(static_cast(intPart)); + + // If all else fails, store it as a double value + return rapidjson::Value(dataHolder.value); } std::string toDebugString(const Object::DataHolder& dataHolder) const override { diff --git a/aplcore/include/apl/primitives/symbolreferencemap.h b/aplcore/include/apl/primitives/symbolreferencemap.h deleted file mode 100644 index b7a5376..0000000 --- a/aplcore/include/apl/primitives/symbolreferencemap.h +++ /dev/null @@ -1,59 +0,0 @@ -/** - * Copyright 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 _APL_SYMBOL_REFERENCE_MAP_H -#define _APL_SYMBOL_REFERENCE_MAP_H - -#include -#include - -#include "apl/common.h" - -namespace apl { - -using SymbolReference = std::pair; - -/** - * Collect symbol references. These are JSON paths of bound variables pointing - * to the context where they are defined. We store this information in a custom - * map so that we can simplify the references as they are added. - * - * The path data for symbols are stored as strings with "/" characters separating - * the path elements and terminating the path. For example, the paths extracted - * from the equation ${a.friends[2] + Math.min(b.height, b.weight)}" would be: - * "a/friends/2", "b/height/", and "b/weight/". - */ -class SymbolReferenceMap { -public: - void emplace(const std::string& key, const ContextPtr& value); - void emplace(SymbolReference& ref); - void emplace(SymbolReference&& ref); - - bool empty() const { return mMap.empty(); } - - bool operator==(const SymbolReferenceMap& other) const { - return mMap == other.mMap; - } - - std::string toDebugString() const; - const std::map& get() const { return mMap; } - -private: - std::map mMap; -}; - -} // namespace apl - -#endif // _APL_SYMBOL_REFERENCE_MAP_H diff --git a/aplcore/include/apl/primitives/unicode.h b/aplcore/include/apl/primitives/unicode.h index 9139f78..140919a 100644 --- a/aplcore/include/apl/primitives/unicode.h +++ b/aplcore/include/apl/primitives/unicode.h @@ -28,6 +28,14 @@ namespace apl { */ int utf8StringLength(const std::string& utf8String); +/** + * Count the number of code points in a uint8_t byte range containing UTF-8 data. + * @param utf8StringPtr A pointer to the beginning of the uint8_t byte range where UTF-8 code points should be counted. + * @param count The number of bytes in the uint8_t byte range to check when counting UTF-8 code points. + * @return The number of code points in the string. Return -1 if the string is malformed. + */ +int utf8StringLength(const uint8_t* utf8StringPtr, int count); + /** * Slice a UTF-8 string * @param utf8String A reference to a std::string holding UTF-8 data diff --git a/aplcore/include/apl/scenegraph/edittextconfig.h b/aplcore/include/apl/scenegraph/edittextconfig.h index c2c86ee..044ffa7 100644 --- a/aplcore/include/apl/scenegraph/edittextconfig.h +++ b/aplcore/include/apl/scenegraph/edittextconfig.h @@ -34,7 +34,6 @@ class EditTextConfig : public UserData { static EditTextConfigPtr create(Color textColor, Color highlightColor, KeyboardType keyboardType, - const std::string& language, unsigned int maxLength, bool secureInput, SubmitKeyType submitKeyType, @@ -48,7 +47,6 @@ class EditTextConfig : public UserData { Color textColor() const { return mTextColor; } Color highlightColor() const { return mHighlightColor; } KeyboardType keyboardType() const { return mKeyboardType; } - std::string language() const { return mLanguage; } bool secureInput() const { return mSecureInput; } SubmitKeyType submitKeyType() const { return mSubmitKeyType; } bool selectOnFocus() const { return mSelectOnFocus; } @@ -76,7 +74,6 @@ class EditTextConfig : public UserData { Color mTextColor; Color mHighlightColor; KeyboardType mKeyboardType; - std::string mLanguage; unsigned int mMaxLength; SubmitKeyType mSubmitKeyType; std::string mValidCharacters; diff --git a/aplcore/include/apl/scenegraph/textproperties.h b/aplcore/include/apl/scenegraph/textproperties.h index 1fc16ac..6d93884 100644 --- a/aplcore/include/apl/scenegraph/textproperties.h +++ b/aplcore/include/apl/scenegraph/textproperties.h @@ -35,6 +35,7 @@ class TextProperties : public UserData { std::vector&& fontFamily, float fontSize, FontStyle fontStyle, + const std::string& language, int fontWeight, float letterSpacing = 0, float lineHeight = 1.25f, @@ -47,6 +48,7 @@ class TextProperties : public UserData { const std::vector& fontFamily() const { return mFontFamily; } float fontSize() const { return mFontSize; } FontStyle fontStyle() const { return mFontStyle; } + std::string language() const { return mLanguage; } int fontWeight() const { return mFontWeight; } float letterSpacing() const { return mLetterSpacing; } @@ -64,6 +66,7 @@ class TextProperties : public UserData { std::vector mFontFamily; float mFontSize; FontStyle mFontStyle; + std::string mLanguage; int mFontWeight; float mLetterSpacing; diff --git a/aplcore/include/apl/scenegraph/utilities.h b/aplcore/include/apl/scenegraph/utilities.h index 8ac7dae..2b4354c 100644 --- a/aplcore/include/apl/scenegraph/utilities.h +++ b/aplcore/include/apl/scenegraph/utilities.h @@ -29,7 +29,7 @@ namespace sg { * @param text * @return */ -std::vector splitFontString(const RootConfig& rootConfig, const std::string& text); +std::vector splitFontString(const RootConfig& rootConfig, const SessionPtr& session, const std::string& text); } // namespace sg } // namespace apl diff --git a/aplcore/include/apl/time/executionresourceholder.h b/aplcore/include/apl/time/executionresourceholder.h index a3ec16b..efbbe1b 100644 --- a/aplcore/include/apl/time/executionresourceholder.h +++ b/aplcore/include/apl/time/executionresourceholder.h @@ -17,6 +17,7 @@ #define _APL_EXECUTION_RESOURCE_HOLDER_H #include "apl/time/executionresource.h" +#include "apl/utils/counter.h" #include "apl/utils/noncopyable.h" namespace apl { @@ -30,6 +31,7 @@ class Sequencer; * function is invoked to warn that the resource has been taken away. */ class ExecutionResourceHolder : public NonCopyable, + public Counter, public std::enable_shared_from_this { public: /** diff --git a/aplcore/include/apl/time/sequencer.h b/aplcore/include/apl/time/sequencer.h index 79fa7f1..8222933 100644 --- a/aplcore/include/apl/time/sequencer.h +++ b/aplcore/include/apl/time/sequencer.h @@ -16,12 +16,13 @@ #ifndef _APL_SEQUENCER_H #define _APL_SEQUENCER_H -#include "apl/utils/counter.h" #include "apl/action/action.h" -#include "apl/engine/context.h" #include "apl/command/command.h" +#include "apl/engine/context.h" +#include "apl/primitives/commanddata.h" #include "apl/time/executionresource.h" #include "apl/time/executionresourceholder.h" +#include "apl/utils/counter.h" namespace apl { @@ -48,14 +49,14 @@ class Sequencer : public Counter { * Convenience routine that takes an array object of commands and a data-binding context, * inflates an ArrayCommand, and then executes it. * - * @param commands An array of commands to execute. + * @param commandData An array of commands to execute. * @param context The data-binding context. * @param baseComponent The base component that these commands execute from. * @param fastMode True if the commands should run in fast mode. * @return The action pointer of the command or nullptr if there is nothing to execute. * A nullptr will be returned in fast mode. */ - ActionPtr executeCommands(const Object& commands, + ActionPtr executeCommands(CommandData&& commandData, const ContextPtr& context, const CoreComponentPtr& baseComponent, bool fastMode); @@ -74,14 +75,14 @@ class Sequencer : public Counter { * Convenience routine that takes an array object of commands and a data-binding context, * inflates an ArrayCommand, and then executes it on the named sequencer. * - * @param commands An array of commands to execute. + * @param commandData An array of commands to execute. * @param context The data-binding context. * @param baseComponent The base component that these commands execute from. * @param sequencer Name of the sequencer to use. * @return The action pointer of the command or nullptr if there is nothing to execute. * A nullptr will be returned in fast mode. */ - ActionPtr executeCommandsOnSequencer(const Object& commands, + ActionPtr executeCommandsOnSequencer(CommandData&& commandData, const ContextPtr& context, const CoreComponentPtr& baseComponent, const std::string& sequencer); @@ -178,7 +179,7 @@ class Sequencer : public Counter { * @param root new root context. * @return true if successful, false otherwise. */ - bool reattachSequencer(const std::string& sequencerName, const ActionPtr& action, const RootContext& root); + bool reattachSequencer(const std::string& sequencerName, const ActionPtr& action, const CoreDocumentContext& context); private: friend class ReinflateCommand; diff --git a/aplcore/include/apl/touch/pointermanager.h b/aplcore/include/apl/touch/pointermanager.h index 71950ee..46b2077 100644 --- a/aplcore/include/apl/touch/pointermanager.h +++ b/aplcore/include/apl/touch/pointermanager.h @@ -23,7 +23,7 @@ namespace apl { -class RootContextData; +class HoverManager; /** * PointerManager holds onto all active pointers. For touch pointers, these are ephemeral and come and go as users press @@ -44,9 +44,9 @@ class PointerManager { public: /** * Constructs a PointerManager with no pointers. - * @param core Reference to RootContextData, used to delegate touch events to hover manager when applicable + * @param core Reference to CoreRootContext, used to delegate touch events to hover manager when applicable */ - explicit PointerManager(const RootContextData& core); + explicit PointerManager(const CoreRootContext& core, HoverManager& hover); /** * Handles a PointerEvent. If the pointer has never been seen, it will create a new pointer for the id in the associated @@ -140,7 +140,8 @@ class PointerManager { apl_time_t timestamp); private: - const RootContextData& mCore; + const CoreRootContext& mCore; + HoverManager& mHoverManager; std::shared_ptr mActivePointer; std::shared_ptr mLastActivePointer; }; diff --git a/aplcore/include/apl/touch/utils/pagemovehandler.h b/aplcore/include/apl/touch/utils/pagemovehandler.h index 791ad1e..5c9ced6 100644 --- a/aplcore/include/apl/touch/utils/pagemovehandler.h +++ b/aplcore/include/apl/touch/utils/pagemovehandler.h @@ -97,7 +97,6 @@ class PageMoveHandler { PageMoveDrawOrder getDrawOrder() const { return mDrawOrder; } std::weak_ptr getCurrentPage() const { return mCurrentPage; } - std::weak_ptr getTargetPage() const { return mTargetPage; } SwipeDirection getSwipeDirection() const { return mSwipeDirection; } /** @@ -110,6 +109,26 @@ class PageMoveHandler { */ int getTargetPageIndex(const CoreComponentPtr& component) const; + /** + * Get the current page component, verified to (still) be a child of the specified pager. + * + * @param component parent pager component + * + * @return the current page or nullptr if the is no longer a current page that is a child of the + * specified pager. + */ + CoreComponentPtr getCheckedCurrentPage(const CoreComponentPtr& component) const; + + /** + * Get the target page component, verified to (still) be a child of the specified pager. + * + * @param component parent pager component + * + * @return the target page or nullptr if the is no longer a target page that is a child of the + * specified pager. + */ + CoreComponentPtr getCheckedTargetPage(const CoreComponentPtr& component) const; + private: static ContextPtr createPageMoveContext( float amount, diff --git a/aplcore/include/apl/utils/log.h b/aplcore/include/apl/utils/log.h index 3ee2410..26b654b 100644 --- a/aplcore/include/apl/utils/log.h +++ b/aplcore/include/apl/utils/log.h @@ -88,13 +88,12 @@ class Logger Logger& session(const Context& context); Logger& session(const ConstContextPtr& context); Logger& session(const ContextPtr& context); - Logger& session(const RootConfig& config); - Logger& session(const RootConfigPtr& config); - Logger& session(const RootContextPtr& root); Logger& session(const Component& component); Logger& session(const ComponentPtr& component); Logger& session(const CommandPtr& command); Logger& session(const ConstCommandPtr& command); + Logger& session(const ContentPtr& content); + Logger& session(const CoreDocumentContextPtr& document); template friend Logger& operator<<(Logger& os, T&& value) diff --git a/aplcore/include/apl/utils/path.h b/aplcore/include/apl/utils/path.h index e2204b7..8ecae91 100644 --- a/aplcore/include/apl/utils/path.h +++ b/aplcore/include/apl/utils/path.h @@ -52,9 +52,6 @@ class Path { if (mPath.empty()) return *this; - if (mPath.back() == '/') - LOG(LogLevel::kError) << "Adding string segment to array path " << mPath << " - " << segment; - return Path(mPath + '/' + segment); } @@ -76,9 +73,6 @@ class Path { if (mPath.empty()) return *this; - if (mPath.back() == '/') - LOG(LogLevel::kError) << "Adding array segment to array path " << mPath << " - " << segment; - return Path(mPath + '/' + segment + '/'); } @@ -103,9 +97,6 @@ class Path { if (mPath.back() == '/') return Path(mPath + std::to_string(index)); - if (index != 0) - LOG(LogLevel::kError) << "Expected zero index for '" << mPath << "'"; - return *this; } diff --git a/aplcore/include/apl/utils/random.h b/aplcore/include/apl/utils/random.h index db51d0c..2868b56 100644 --- a/aplcore/include/apl/utils/random.h +++ b/aplcore/include/apl/utils/random.h @@ -18,6 +18,8 @@ #include +#include "apl/utils/streamer.h" + namespace apl { /** diff --git a/aplcore/include/apl/utils/scopedcollection.h b/aplcore/include/apl/utils/scopedcollection.h new file mode 100644 index 0000000..e250815 --- /dev/null +++ b/aplcore/include/apl/utils/scopedcollection.h @@ -0,0 +1,114 @@ +/** + * Copyright 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 _APL_SCOPED_COLLECTION +#define _APL_SCOPED_COLLECTION + +#include "apl/utils/noncopyable.h" + +namespace apl { + +/** + * Simple implementation of collection that can be grouped by a particular key (scope) + * @tparam Scope Type of the scope + * @tparam Type Type of the value + * @tparam Collection Enclosed collection type + * @tparam ReturnType Collection returns + */ +template< + class Scope, + class Type, + class Collection, + class ReturnType = std::vector> +class ScopedCollection : public NonCopyable { +public: + virtual ~ScopedCollection() = default; + + /** + * @return true if collection is empty, false otherwise. + */ + virtual bool empty() const = 0; + + /** + * @return Number of elements in the collection. + */ + virtual size_t size() const = 0; + + /** + * @return Reference to enclosed collection + */ + virtual const Collection& getAll() const = 0; + + /** + * Get set of values related to a requested scope + * @param scope grouping key + * @return Collection of values behind requested scope. + */ + virtual ReturnType getScoped(const Scope& scope) const = 0; + + /** + * @return Reference to first element in the collection. + */ + virtual const Type& front() = 0; + + /** + * Remove first element from the collection and return it. + * @return First element in the collection. + */ + virtual Type pop() = 0; + + /** + * Clear the collection. + */ + virtual void clear() = 0; + + /** + * Extract and remove all values related to provided key. + * @param scope grouping key. + * @return Collection of values that were extracted. + */ + virtual ReturnType extractScope(const Scope& scope) = 0; + + /** + * Remove all values related to provided key. + * @param scope grouping key. + * @return number of elements removed. + */ + virtual size_t eraseScope(const Scope& scope) = 0; + + /** + * Remove particular value from the calculation. Will remove only first occurence of it. + * @param value value to remove. + */ + virtual void eraseValue(const Type& value) = 0; + + /** + * Add value into the collection. + * @param scope grouping key. + * @param value value to add. + */ + virtual void emplace(const Scope& scope, const Type& value) = 0; + + /** + * Add value into the collection. + * @param scope grouping key. + * @param value value to add. + */ + virtual void emplace(const Scope& scope, Type&& value) = 0; +}; + +} // namespace apl + +#endif // _APL_SCOPED_COLLECTION \ No newline at end of file diff --git a/aplcore/include/apl/utils/scopeddequeue.h b/aplcore/include/apl/utils/scopeddequeue.h new file mode 100644 index 0000000..49849cc --- /dev/null +++ b/aplcore/include/apl/utils/scopeddequeue.h @@ -0,0 +1,109 @@ +/** + * Copyright 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 _APL_SCOPED_DEQUEUE +#define _APL_SCOPED_DEQUEUE + +#include "apl/utils/scopedcollection.h" + +#include + +namespace apl { + +/** + * Scoped implementation of the dequeue. + * @tparam Scope Scoping key type. + * @tparam Type Value type. + */ +template +class ScopedDequeue : public ScopedCollection>> { +public: + bool empty() const override { + return mQueue.empty(); + } + + size_t size() const override { + return mQueue.size(); + } + + const std::deque>& getAll() const override { + return mQueue; + } + + const Type& front() override { + return mQueue.front().second; + } + + Type pop() override { + auto result = mQueue.front(); + mQueue.pop_front(); + return result.second; + } + + void clear() override { + mQueue.clear(); + } + + std::vector getScoped(const Scope& scope) const override { + auto result = std::vector(); + for(auto it = mQueue.begin(); it != mQueue.end(); ++it) { + if (it->first == scope) result.emplace_back(it->second); + } + return result; + } + + std::vector extractScope(const Scope& scope) override { + auto erased = std::vector(); + for (auto it = mQueue.begin() ; it != mQueue.end() ; ) { + if (it->first == scope) { + erased.emplace_back(it->second); + it = mQueue.erase(it); + } else { + it++; + } + } + + return erased; + } + + void eraseValue(const Type& value) override {} + + size_t eraseScope(const Scope& scope) override { + auto eraseIt = + std::remove_if(mQueue.begin(), mQueue.end(), [scope](const std::pair& item) { + return std::get<0>(item) == scope; + }); + + auto result = std::distance(eraseIt, mQueue.end()); + mQueue.erase(eraseIt, mQueue.end()); + + return result; + } + + void emplace(const Scope& scope, const Type& value) override { + mQueue.emplace_back(std::make_pair(scope, value)); + } + + void emplace(const Scope& scope, Type&& value) override { + mQueue.emplace_back(std::make_pair(scope, std::forward(value))); + } + +private: + std::deque> mQueue; +}; + +} // namespace apl + +#endif // _APL_SCOPED_DEQUEUE \ No newline at end of file diff --git a/aplcore/include/apl/utils/scopedset.h b/aplcore/include/apl/utils/scopedset.h new file mode 100644 index 0000000..9e27ed5 --- /dev/null +++ b/aplcore/include/apl/utils/scopedset.h @@ -0,0 +1,119 @@ +/** + * Copyright 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 _APL_SCOPED_SET +#define _APL_SCOPED_SET + +#include "apl/utils/scopedcollection.h" + +namespace apl { + +/** + * Scoped implementation of the set. + * @tparam Scope Scoping key type. + * @tparam Type Value type. + */ +template +class ScopedSet : public ScopedCollection, std::set> { +public: + bool empty() const override { + return mSet.empty(); + } + + size_t size() const override { + return mSet.size(); + } + + const std::set& getAll() const override { + return mSet; + } + + const Type& front() override { + return *mSet.begin(); + } + + Type pop() override { + auto result = *mSet.begin(); + mSet.erase(mSet.begin()); + eraseFromScope(result); + return result; + } + + void clear() override { + mSet.clear(); + mScopeToValue.clear(); + } + + std::set getScoped(const Scope& scope) const override { + auto result = std::set(); + auto range = mScopeToValue.equal_range(scope); + for(auto it = range.first; it != range.second; ++it) { + result.emplace(it->second); + } + return result; + } + + std::set extractScope(const Scope& scope) override { + auto erased = getScoped(scope); + for (auto it = mSet.begin() ; it != mSet.end() ; ) { + if (erased.count(*it)) { + it = mSet.erase(it); + } else { + it++; + } + } + mScopeToValue.erase(scope); + + return erased; + } + + size_t eraseScope(const Scope& scope) override { + return extractScope(scope).size(); + } + + void eraseValue(const Type& value) override { + mSet.erase(value); + eraseFromScope(value); + } + + void emplace(const Scope& scope, const Type& value) override { + if (mSet.emplace(value).second) { + mScopeToValue.emplace(scope, value); + } + } + + void emplace(const Scope& scope, Type&& value) override { + if (mSet.emplace(std::forward(value)).second) { + mScopeToValue.emplace(scope, std::forward(value)); + } + } + +private: + void eraseFromScope(const Type& value) { + for (auto it = mScopeToValue.begin(); it != mScopeToValue.end(); it++) { + if (it->second == value) { + mScopeToValue.erase(it); + break; + } + } + } + + std::multimap mScopeToValue{}; + std::set mSet; +}; + +} // namespace apl + +#endif // _APL_SCOPED_SET \ No newline at end of file diff --git a/aplcore/include/apl/utils/session.h b/aplcore/include/apl/utils/session.h index 722dc02..e673c89 100644 --- a/aplcore/include/apl/utils/session.h +++ b/aplcore/include/apl/utils/session.h @@ -97,10 +97,12 @@ class SessionMessage { SessionMessage(const Context& context, const char *filename, const char *function); - SessionMessage(const RootConfigPtr& config, const char *filename, const char *function); - SessionMessage(const std::weak_ptr& contextPtr, const char *filename, const char *function); + SessionMessage(const ContentPtr& content, const char *filename, const char *function); + + SessionMessage(const CoreDocumentContextPtr& document, const char *filename, const char *function); + ~SessionMessage(); template friend SessionMessage& operator<<(SessionMessage&& sm, T&& value) diff --git a/aplcore/src/action/action.cpp b/aplcore/src/action/action.cpp index 28508d6..9d35a30 100644 --- a/aplcore/src/action/action.cpp +++ b/aplcore/src/action/action.cpp @@ -13,9 +13,11 @@ * permissions and limitations under the License. */ +#include "apl/action/action.h" + #include -#include "apl/action/action.h" +#include "apl/document/documentcontext.h" #include "apl/time/timers.h" #include "apl/utils/log.h" @@ -228,7 +230,7 @@ Action::freeze() } bool -Action::rehydrate(const RootContext& context) +Action::rehydrate(const CoreDocumentContext& context) { if (mTimeoutId != 0 && !mTimers->rehydrate(mTimeoutId)) return false; return true; @@ -273,7 +275,7 @@ class Collection : public Action { Action::freeze(); } - bool rehydrate(const RootContext& context) override { + bool rehydrate(const CoreDocumentContext& context) override { if (!Action::rehydrate(context)) return false; auto it = mActions.cbegin(); diff --git a/aplcore/src/action/animatedscrollaction.cpp b/aplcore/src/action/animatedscrollaction.cpp index 0dc90b9..1337584 100644 --- a/aplcore/src/action/animatedscrollaction.cpp +++ b/aplcore/src/action/animatedscrollaction.cpp @@ -16,7 +16,7 @@ #include "apl/action/animatedscrollaction.h" #include "apl/component/scrollablecomponent.h" -#include "apl/engine/rootcontext.h" +#include "apl/document/coredocumentcontext.h" #include "apl/time/sequencer.h" #include "apl/touch/utils/autoscroller.h" @@ -84,7 +84,7 @@ AnimatedScrollAction::advance() void AnimatedScrollAction::freeze() { - mCurrentAction->freeze(); + if (mCurrentAction) mCurrentAction->freeze(); mFrozenContainerId = mContainer->getId(); @@ -92,14 +92,14 @@ AnimatedScrollAction::freeze() } bool -AnimatedScrollAction::rehydrate(const RootContext& context) +AnimatedScrollAction::rehydrate(const CoreDocumentContext& context) { + if (!ResourceHoldingAction::rehydrate(context)) return false; + if (!mCurrentAction) { return true; } - if (!ResourceHoldingAction::rehydrate(context)) return false; - mContainer = CoreComponent::cast(context.findComponentById(mFrozenContainerId)); if (!mContainer) return false; diff --git a/aplcore/src/action/animateitemaction.cpp b/aplcore/src/action/animateitemaction.cpp index 3a8a08a..9e88dcb 100644 --- a/aplcore/src/action/animateitemaction.cpp +++ b/aplcore/src/action/animateitemaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/animateitemaction.h" + #include "apl/animation/animatedproperty.h" #include "apl/command/corecommand.h" #include "apl/content/rootconfig.h" @@ -154,7 +155,7 @@ AnimateItemAction::freeze() } bool -AnimateItemAction::rehydrate(const RootContext& context) +AnimateItemAction::rehydrate(const CoreDocumentContext& context) { if (!ResourceHoldingAction::rehydrate(context)) return false; diff --git a/aplcore/src/action/arrayaction.cpp b/aplcore/src/action/arrayaction.cpp index a239f33..57495ea 100644 --- a/aplcore/src/action/arrayaction.cpp +++ b/aplcore/src/action/arrayaction.cpp @@ -22,9 +22,9 @@ namespace apl { -ArrayAction::ArrayAction(const TimersPtr& timers, std::shared_ptr command, bool fastMode) +ArrayAction::ArrayAction(const TimersPtr& timers, std::shared_ptr&& command, bool fastMode) : Action(timers), - mCommand(command), + mCommand(std::move(command)), mFastMode(fastMode), mNextIndex(0) { @@ -35,13 +35,13 @@ ArrayAction::ArrayAction(const TimersPtr& timers, std::shared_ptrfinishAllOnTerminate()) { - auto& commands = mCommand->commands(); + const auto& commands = mCommand->data().get(); std::vector remaining; for (size_t i = mNextIndex ; i < commands.size() ; i++) remaining.push_back(commands.at(i)); auto context = mCommand->context(); - context->sequencer().executeCommands(Object(std::move(remaining)), context, mCommand->base(), true); + context->sequencer().executeCommands({std::move(remaining), mCommand->data()}, context, mCommand->base(), true); } }); } @@ -55,12 +55,10 @@ ArrayAction::advance() { if (isTerminated()) return; - auto commands = mCommand->commands(); + const auto& commands = mCommand->data(); while (mNextIndex < commands.size()) { - Object command = commands.at(mNextIndex++); - auto commandPtr = CommandFactory::instance().inflate(command, - mCommand); + auto commandPtr = CommandFactory::instance().inflate(commands.at(mNextIndex++), mCommand); if (!commandPtr) continue; diff --git a/aplcore/src/action/autopageaction.cpp b/aplcore/src/action/autopageaction.cpp index 4ec3f80..eab54ee 100644 --- a/aplcore/src/action/autopageaction.cpp +++ b/aplcore/src/action/autopageaction.cpp @@ -18,7 +18,6 @@ #include "apl/command/corecommand.h" #include "apl/component/corecomponent.h" #include "apl/component/pagercomponent.h" -#include "apl/content/rootconfig.h" #include "apl/time/sequencer.h" namespace apl { @@ -116,7 +115,7 @@ AutoPageAction::freeze() } bool -AutoPageAction::rehydrate(const RootContext& context) +AutoPageAction::rehydrate(const CoreDocumentContext& context) { if (!ResourceHoldingAction::rehydrate(context)) return false; diff --git a/aplcore/src/action/controlmediaaction.cpp b/aplcore/src/action/controlmediaaction.cpp index c882f7a..dfec0e3 100644 --- a/aplcore/src/action/controlmediaaction.cpp +++ b/aplcore/src/action/controlmediaaction.cpp @@ -97,6 +97,9 @@ ControlMediaAction::start() case kCommandControlMediaSeek: mediaPlayer->seek(value.getInteger()); break; + case kCommandControlMediaSeekTo: + mediaPlayer->seekTo(value.getInteger()); + break; case kCommandControlMediaSetTrack: mediaPlayer->setTrackIndex(value.getInteger()); break; diff --git a/aplcore/src/action/delayaction.cpp b/aplcore/src/action/delayaction.cpp index 3c15c64..84381a8 100644 --- a/aplcore/src/action/delayaction.cpp +++ b/aplcore/src/action/delayaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/delayaction.h" + #include "apl/command/command.h" namespace apl { @@ -130,7 +131,7 @@ DelayAction::freeze() } bool -DelayAction::rehydrate(const RootContext& context) +DelayAction::rehydrate(const CoreDocumentContext& context) { if (!Action::rehydrate(context)) return false; diff --git a/aplcore/src/action/openurlaction.cpp b/aplcore/src/action/openurlaction.cpp index 71ae1ab..d12628a 100644 --- a/aplcore/src/action/openurlaction.cpp +++ b/aplcore/src/action/openurlaction.cpp @@ -61,9 +61,9 @@ OpenURLAction::handleFailure(int argument) context->putConstant("event", event); auto array = ArrayCommand::create(context, - mCommand->getValue(kCommandPropertyOnFail), + {mCommand->getValue(kCommandPropertyOnFail), mCommand->data()}, mCommand->target(), - mCommand->properties(), + Properties(mCommand->properties()), mCommand->sequencer()); // Run these in normal mode mCurrentAction = array->execute(timers(), false); diff --git a/aplcore/src/action/playmediaaction.cpp b/aplcore/src/action/playmediaaction.cpp index e5bd8ad..c05fe41 100644 --- a/aplcore/src/action/playmediaaction.cpp +++ b/aplcore/src/action/playmediaaction.cpp @@ -108,7 +108,7 @@ PlayMediaAction::freeze() } bool -PlayMediaAction::rehydrate(const RootContext& context) +PlayMediaAction::rehydrate(const CoreDocumentContext& context) { if (!ResourceHoldingAction::rehydrate(context)) return false; diff --git a/aplcore/src/action/resourceholdingaction.cpp b/aplcore/src/action/resourceholdingaction.cpp index 4c0b87c..fc9850a 100644 --- a/aplcore/src/action/resourceholdingaction.cpp +++ b/aplcore/src/action/resourceholdingaction.cpp @@ -14,9 +14,9 @@ */ #include "apl/action/resourceholdingaction.h" + #include "apl/command/corecommand.h" #include "apl/time/sequencer.h" -#include "apl/engine/rootcontext.h" namespace apl { @@ -40,7 +40,7 @@ ResourceHoldingAction::freeze() } bool -ResourceHoldingAction::rehydrate(const RootContext& context) +ResourceHoldingAction::rehydrate(const CoreDocumentContext& context) { if (!Action::rehydrate(context)) return false; mContext = context.contextPtr(); diff --git a/aplcore/src/action/scrollaction.cpp b/aplcore/src/action/scrollaction.cpp index e43b00d..f840758 100644 --- a/aplcore/src/action/scrollaction.cpp +++ b/aplcore/src/action/scrollaction.cpp @@ -14,6 +14,7 @@ */ #include "apl/action/scrollaction.h" + #include "apl/command/corecommand.h" #include "apl/time/sequencer.h" @@ -87,7 +88,7 @@ ScrollAction::freeze() } bool -ScrollAction::rehydrate(const RootContext& context) +ScrollAction::rehydrate(const CoreDocumentContext& context) { if (!AnimatedScrollAction::rehydrate(context)) return false; diff --git a/aplcore/src/action/scrolltoaction.cpp b/aplcore/src/action/scrolltoaction.cpp index 50b62b5..cd04825 100644 --- a/aplcore/src/action/scrolltoaction.cpp +++ b/aplcore/src/action/scrolltoaction.cpp @@ -17,7 +17,6 @@ #include "apl/command/corecommand.h" #include "apl/component/pagercomponent.h" -#include "apl/engine/rootcontext.h" #include "apl/time/sequencer.h" namespace apl { @@ -313,13 +312,13 @@ ScrollToAction::freeze() } // Intentionally higher level, we don't need to freeze underlying scroller - ResourceHoldingAction::freeze(); + AnimatedScrollAction::freeze(); } bool -ScrollToAction::rehydrate(const RootContext& context) +ScrollToAction::rehydrate(const CoreDocumentContext& context) { - if (!ResourceHoldingAction::rehydrate(context)) return false; + if (!AnimatedScrollAction::rehydrate(context)) return false; mTarget = CoreComponent::cast(context.findComponentById(mFrozenTargetId)); @@ -347,7 +346,7 @@ ScrollToAction::rehydrate(const RootContext& context) // TODO: We don't preserve time, so it takes full duration for the remainder of scrolling. // Considering the need to recalculate the target it's fine for now, but we may consider - // improving in future. + // improving it in the future. return true; } diff --git a/aplcore/src/action/sequentialaction.cpp b/aplcore/src/action/sequentialaction.cpp index 1a9daa3..4d3cb90 100644 --- a/aplcore/src/action/sequentialaction.cpp +++ b/aplcore/src/action/sequentialaction.cpp @@ -64,7 +64,7 @@ SequentialAction::SequentialAction(const TimersPtr& timers, commands.push_back(finallyCommands.at(i)); } - context->sequencer().executeCommands(Object(std::move(commands)), context, mCommand->base(), true); + context->sequencer().executeCommands({std::move(commands), mCommand->data()}, context, mCommand->base(), true); }); } @@ -85,8 +85,8 @@ SequentialAction::advance() { while (mRepeatCounter <= repeatCount) { while (mNextIndex < commands.size()) { - Object command = commands.at(mNextIndex++); - if (doCommand(command)) + const auto& command = commands.at(mNextIndex++); + if (doCommand({command, mCommand->data()})) return; // Done advancing until the current action resolves } mRepeatCounter++; @@ -97,8 +97,8 @@ SequentialAction::advance() { auto commands = mCommand->getValue(kCommandPropertyFinally); while (mNextIndex < commands.size()) { - Object command = commands.at(mNextIndex++); - if (doCommand(command)) + const auto& command = commands.at(mNextIndex++); + if (doCommand({command, mCommand->data()})) return; } @@ -106,9 +106,9 @@ SequentialAction::advance() { } bool -SequentialAction::doCommand(const Object& command) +SequentialAction::doCommand(CommandData&& commandData) { - auto commandPtr = CommandFactory::instance().inflate(command, mCommand); + auto commandPtr = CommandFactory::instance().inflate(std::move(commandData), mCommand); if (commandPtr) { auto childSeq = commandPtr->sequencer(); if (childSeq != mCommand->sequencer()) { @@ -149,7 +149,7 @@ SequentialAction::freeze() } bool -SequentialAction::rehydrate(const RootContext& context) +SequentialAction::rehydrate(const CoreDocumentContext& context) { if (!Action::rehydrate(context)) return false; diff --git a/aplcore/src/action/setpageaction.cpp b/aplcore/src/action/setpageaction.cpp index 8b7dc43..f394ae0 100644 --- a/aplcore/src/action/setpageaction.cpp +++ b/aplcore/src/action/setpageaction.cpp @@ -117,7 +117,7 @@ SetPageAction::freeze() } bool -SetPageAction::rehydrate(const RootContext& context) +SetPageAction::rehydrate(const CoreDocumentContext& context) { if (!ResourceHoldingAction::rehydrate(context)) return false; diff --git a/aplcore/src/action/speakitemaction.cpp b/aplcore/src/action/speakitemaction.cpp index a07dc50..85553bb 100644 --- a/aplcore/src/action/speakitemaction.cpp +++ b/aplcore/src/action/speakitemaction.cpp @@ -20,7 +20,6 @@ #include "apl/command/commandproperties.h" #include "apl/command/corecommand.h" #include "apl/component/textcomponent.h" -#include "apl/engine/rootcontext.h" #include "apl/primitives/styledtext.h" #include "apl/time/sequencer.h" #include "apl/utils/make_unique.h" @@ -302,6 +301,11 @@ class SpeakItemActionPrivate { virtual void highlight(SpeakItemAction& action, Range byteRange) = 0; + void freeze() { + mScrollAction = nullptr; + mDwellAction = nullptr; + } + void rehydrate(SpeakItemAction& action) { // Don't need it for line by line - time update will scroll appropriately if (!mText.empty()) return; @@ -311,7 +315,7 @@ class SpeakItemActionPrivate { start(action); } else { auto align = static_cast(action.mCommand->getValue(kCommandPropertyAlign).getInteger()); - ScrollToAction::make( + mScrollAction = ScrollToAction::make( action.timers(), align, Rect(), @@ -769,11 +773,13 @@ SpeakItemAction::freeze() mCommand->freeze(); } + mPrivate->freeze(); + ResourceHoldingAction::freeze(); } bool -SpeakItemAction::rehydrate(const RootContext& context) +SpeakItemAction::rehydrate(const CoreDocumentContext& context) { if (!mPrivate) return false; diff --git a/aplcore/src/action/speaklistaction.cpp b/aplcore/src/action/speaklistaction.cpp index f9f5bff..da38455 100644 --- a/aplcore/src/action/speaklistaction.cpp +++ b/aplcore/src/action/speaklistaction.cpp @@ -14,10 +14,10 @@ */ #include "apl/action/speaklistaction.h" -#include "apl/action/speakitemaction.h" + #include "apl/action/scrolltoaction.h" +#include "apl/action/speakitemaction.h" #include "apl/command/corecommand.h" -#include "apl/engine/rootcontext.h" namespace apl { @@ -105,7 +105,7 @@ SpeakListAction::freeze() } bool -SpeakListAction::rehydrate(const RootContext& context) +SpeakListAction::rehydrate(const CoreDocumentContext& context) { if (!Action::rehydrate(context)) return false; diff --git a/aplcore/src/command/CMakeLists.txt b/aplcore/src/command/CMakeLists.txt index b3f4e4c..7968e77 100644 --- a/aplcore/src/command/CMakeLists.txt +++ b/aplcore/src/command/CMakeLists.txt @@ -26,10 +26,12 @@ target_sources_local(apl documentcommand.cpp extensioneventcommand.cpp finishcommand.cpp + insertitemcommand.cpp openurlcommand.cpp parallelcommand.cpp playmediacommand.cpp reinflatecommand.cpp + removeitemcommand.cpp scrollcommand.cpp scrolltocomponentcommand.cpp scrolltoindexcommand.cpp diff --git a/aplcore/src/command/arraycommand.cpp b/aplcore/src/command/arraycommand.cpp index eee53d4..f373bc7 100644 --- a/aplcore/src/command/arraycommand.cpp +++ b/aplcore/src/command/arraycommand.cpp @@ -18,31 +18,34 @@ namespace apl { -ArrayCommand::ArrayCommand(const ContextPtr& context, const Object& commands, const CoreComponentPtr& base, - Properties&& properties, const std::string& parentSequencer, bool finishAllOnTerminate) - : CoreCommand(context, std::move(properties), base, parentSequencer), - mCommands(commands), +ArrayCommand::ArrayCommand(const ContextPtr& context, + CommandData&& commands, + const CoreComponentPtr& base, + Properties&& properties, + const std::string& parentSequencer, + bool finishAllOnTerminate) + : CoreCommand(context, std::move(commands), std::move(properties), base, parentSequencer), mFinishAllOnTerminate(finishAllOnTerminate) {} CommandPtr ArrayCommand::create(const ContextPtr& context, - const Object& commands, + CommandData&& commands, const CoreComponentPtr& base, - const Properties& properties, + Properties&& properties, const std::string& parentSequencer, bool finishAllOnTerminate) { - return std::make_shared(context, commands, base, Properties(properties), parentSequencer, finishAllOnTerminate); + return std::make_shared(context, std::move(commands), base, std::move(properties), + parentSequencer, finishAllOnTerminate); } ActionPtr ArrayCommand::execute(const TimersPtr& timers, bool fastMode) { - if (mCommands.size() <= 0) + if (mCommandData.size() <= 0) return nullptr; - auto self = std::static_pointer_cast(shared_from_this()); - return ArrayAction::make(timers, self, fastMode); + return ArrayAction::make(timers, std::static_pointer_cast(shared_from_this()), fastMode); } } // namespace apl \ No newline at end of file diff --git a/aplcore/src/command/commandfactory.cpp b/aplcore/src/command/commandfactory.cpp index 8ed4b31..381a9bf 100644 --- a/aplcore/src/command/commandfactory.cpp +++ b/aplcore/src/command/commandfactory.cpp @@ -62,27 +62,6 @@ CommandFactory::get(const char *name) const return nullptr; } -/** - * Evaluate the command and return an action. This may return a nullptr! - * @param context The data-binding context. - * @param command The JSON representation of the command - * @param base The base component that started this action. May be null. - * @param fastMode If the command should run in fast mode. - * @return An action pointer or nullptr if there is nothing to execute. - */ -ActionPtr -CommandFactory::execute(const TimersPtr& timers, - const ContextPtr& context, - const Object& command, - const CoreComponentPtr& base, - bool fastMode) -{ - CommandPtr ptr = inflate(context, command, base); - if (!ptr) - return nullptr; - return ptr->execute(timers, fastMode); -} - /** * Inflate macro command. * @param context The data-binding context. @@ -93,7 +72,8 @@ CommandFactory::execute(const TimersPtr& timers, */ CommandPtr CommandFactory::expandMacro(const ContextPtr& context, - Properties& properties, + CommandData&& commandData, + Properties&& properties, const rapidjson::Value& definition, const CoreComponentPtr& base, const std::string& parentSequencer) { @@ -113,9 +93,9 @@ CommandFactory::expandMacro(const ContextPtr& context, } return ArrayCommand::create(cptr, - arrayifyProperty(*cptr, definition, "command", "commands"), + {arrayifyProperty(*cptr, definition, "command", "commands"), commandData}, base, - properties, + std::move(properties), parentSequencer ); } @@ -130,11 +110,12 @@ CommandFactory::expandMacro(const ContextPtr& context, */ CommandPtr CommandFactory::inflate(const ContextPtr& context, - const Object& command, - const Properties& properties, + CommandData&& commandData, + Properties&& properties, const CoreComponentPtr& base, const std::string& parentSequencer) { + auto command = commandData.get(); if (!command.isMap()) return nullptr; @@ -155,17 +136,18 @@ CommandFactory::inflate(const ContextPtr& context, // If this is a standard command type, use that logic to expand. auto method = mCommandMap.find(type); if (method != mCommandMap.end()) - return method->second(context, std::move(props), base, parentSequencer); + return method->second(context, std::move(commandData), std::move(props), base, parentSequencer); // Check to see if it is an extension command auto extensionCommand = context->extensionManager().findCommandDefinition(type); if (extensionCommand != nullptr) - return ExtensionEventCommand::create(*extensionCommand, context, std::move(props), base, parentSequencer); + return ExtensionEventCommand::create(*extensionCommand, context, std::move(commandData), + std::move(props), base, parentSequencer); // Look up a command macro. const auto& resource = context->getCommand(type); if (!resource.empty()) - return expandMacro(context, props, resource.json(), base, parentSequencer); + return expandMacro(context, std::move(commandData), std::move(props), resource.json(), base, parentSequencer); CONSOLE(context) << "Unable to find command '" << type << "'"; return nullptr; @@ -180,18 +162,16 @@ CommandFactory::inflate(const ContextPtr& context, */ CommandPtr CommandFactory::inflate(const ContextPtr& context, - const Object& command, + CommandData&& commandData, const CoreComponentPtr& base) { - Properties properties; - return inflate(context, command, properties, base); + return inflate(context, std::move(commandData), Properties(), base); } CommandPtr -CommandFactory::inflate(const Object& command, const std::shared_ptr& parent) +CommandFactory::inflate(CommandData&& commandData, const std::shared_ptr& parent) { - Properties properties; - return inflate(parent->context(), command, properties, parent->base(), parent->sequencer()); + return inflate(parent->context(), std::move(commandData), Properties(), parent->base(), parent->sequencer()); } } // namespace apl diff --git a/aplcore/src/command/commandproperties.cpp b/aplcore/src/command/commandproperties.cpp index 6a170f4..1ba1ee6 100644 --- a/aplcore/src/command/commandproperties.cpp +++ b/aplcore/src/command/commandproperties.cpp @@ -41,11 +41,14 @@ Bimap sCommandNameBimap = { {kCommandTypeClearFocus, "ClearFocus"}, {kCommandTypeFinish, "Finish"}, {kCommandTypeReinflate, "Reinflate"}, + {kCommandTypeInsertItem, "InsertItem"}, + {kCommandTypeRemoveItem, "RemoveItem"}, }; Bimap sCommandPropertyBimap = { {kCommandPropertyAlign, "align"}, {kCommandPropertyArguments, "arguments"}, + {kCommandPropertyAt, "at"}, {kCommandPropertyAudioTrack, "audioTrack"}, {kCommandPropertyCatch, "catch"}, {kCommandPropertyCommand, "command"}, @@ -63,6 +66,8 @@ Bimap sCommandPropertyBimap = { {kCommandPropertyFlags, "flags"}, {kCommandPropertyHighlightMode, "highlightMode"}, {kCommandPropertyIndex, "index"}, + {kCommandPropertyItem, "item"}, + {kCommandPropertyItem, "items"}, {kCommandPropertyMinimumDwellTime, "minimumDwellTime"}, {kCommandPropertyOnFail, "onFail"}, {kCommandPropertyOtherwise, "otherwise"}, @@ -112,6 +117,7 @@ Bimap sControlMediaMap = { {kCommandControlMediaPrevious, "previous"}, {kCommandControlMediaRewind, "rewind"}, {kCommandControlMediaSeek, "seek"}, + {kCommandControlMediaSeekTo, "seekTo"}, {kCommandControlMediaSetTrack, "setTrack"} }; diff --git a/aplcore/src/command/configchangecommand.cpp b/aplcore/src/command/configchangecommand.cpp index 849af25..1b0cf1c 100644 --- a/aplcore/src/command/configchangecommand.cpp +++ b/aplcore/src/command/configchangecommand.cpp @@ -13,13 +13,12 @@ * permissions and limitations under the License. */ -#include "apl/command/arraycommand.h" #include "apl/command/configchangecommand.h" + +#include "apl/command/arraycommand.h" #include "apl/content/content.h" #include "apl/document/documentproperties.h" -#include "apl/engine/evaluate.h" -#include "apl/engine/propdef.h" -#include "apl/engine/rootcontext.h" +#include "apl/engine/corerootcontext.h" namespace apl { @@ -28,25 +27,25 @@ const char * ConfigChangeCommand::SEQUENCER = "__CONFIG_CHANGE_SEQUENCER"; ActionPtr ConfigChangeCommand::execute(const TimersPtr& timers, bool fastMode) { - auto root = mRootContext.lock(); - if (!root) + auto document = mDocumentContext.lock(); + if (!document) return nullptr; // Extract the event handler commands. If none exist, we immediately execute the resize and return. - auto& json = root->content()->getDocument()->json(); + auto& json = document->content()->getDocument()->json(); auto it = json.FindMember(sDocumentPropertyBimap.at(kDocumentPropertyOnConfigChange).c_str()); if (it == json.MemberEnd()) { - root->resize(); + document->resize(); return nullptr; } - auto context = root->createDocumentContext("ConfigChange", mProperties); + auto context = document->createDocumentContext("ConfigChange", mProperties); auto commands = asCommand(*context, evaluate(*context, it->value)); auto cmd = ArrayCommand::create(context, commands, nullptr, Properties(), "", true); // The subcommands of the ConfigChangeCommand always run in fast mode auto action = cmd->execute(timers, true); - auto weak = mRootContext; + auto weak = mDocumentContext; // When all commands have finished executing call RootContext::resize(). return Action::wrapWithCallback(timers, action, [weak](bool isResolved, const ActionPtr& actionPtr) { diff --git a/aplcore/src/command/corecommand.cpp b/aplcore/src/command/corecommand.cpp index 93d99eb..0225bd0 100644 --- a/aplcore/src/command/corecommand.cpp +++ b/aplcore/src/command/corecommand.cpp @@ -13,20 +13,22 @@ * permissions and limitations under the License. */ -#include - #include "apl/command/corecommand.h" +#include + #include "apl/command/animateitemcommand.h" #include "apl/command/autopagecommand.h" #include "apl/command/clearfocuscommand.h" #include "apl/command/controlmediacommand.h" #include "apl/command/finishcommand.h" #include "apl/command/idlecommand.h" +#include "apl/command/insertitemcommand.h" #include "apl/command/openurlcommand.h" #include "apl/command/parallelcommand.h" #include "apl/command/playmediacommand.h" #include "apl/command/reinflatecommand.h" +#include "apl/command/removeitemcommand.h" #include "apl/command/scrollcommand.h" #include "apl/command/scrolltocomponentcommand.h" #include "apl/command/scrolltoindexcommand.h" @@ -67,8 +69,7 @@ calculate(const CommandPropDef& def, auto p = properties.find(def.names); if (p != properties.end()) { - Object tmp = (def.flags & kPropEvaluated) != 0 ? - evaluateRecursive(*context, p->second) : + Object tmp = (def.flags & kPropEvaluated) != 0 ? evaluateNested(*context, p->second) : evaluate(*context, p->second); if (def.map) { @@ -86,9 +87,11 @@ calculate(const CommandPropDef& def, /*************************************************************/ -CoreCommand::CoreCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) +CoreCommand::CoreCommand(const ContextPtr& context, CommandData&& commandData, + Properties&& properties, const CoreComponentPtr& base, + const std::string& parentSequencer) : mContext(context), + mCommandData(std::move(commandData)), mProperties(std::move(properties)), mBase(base), mTarget(base), @@ -145,7 +148,7 @@ CoreCommand::freeze() } bool -CoreCommand::rehydrate(const RootContext& context) +CoreCommand::rehydrate(const CoreDocumentContext& context) { if (!mFrozen) return true; @@ -318,6 +321,8 @@ std::map sCommandCreatorMap = { {kCommandTypeClearFocus, ClearFocusCommand::create}, {kCommandTypeFinish, FinishCommand::create}, {kCommandTypeReinflate, ReinflateCommand::create}, + {kCommandTypeInsertItem, InsertItemCommand::create}, + {kCommandTypeRemoveItem, RemoveItemCommand::create}, }; } diff --git a/aplcore/src/command/displaystatechangecommand.cpp b/aplcore/src/command/displaystatechangecommand.cpp index 6b2fa80..ff5cdbf 100644 --- a/aplcore/src/command/displaystatechangecommand.cpp +++ b/aplcore/src/command/displaystatechangecommand.cpp @@ -13,12 +13,13 @@ * permissions and limitations under the License. */ -#include "apl/command/arraycommand.h" #include "apl/command/displaystatechangecommand.h" + +#include "apl/command/arraycommand.h" #include "apl/content/content.h" #include "apl/document/documentproperties.h" +#include "apl/engine/corerootcontext.h" #include "apl/engine/evaluate.h" -#include "apl/engine/rootcontext.h" namespace apl { @@ -27,18 +28,18 @@ const char * DisplayStateChangeCommand::SEQUENCER = "__DISPLAY_STATE_CHANGE_SEQU ActionPtr DisplayStateChangeCommand::execute(const TimersPtr& timers, bool fastMode) { - auto root = mRootContext.lock(); - if (!root) + auto document = mDocument.lock(); + if (!document) return nullptr; // Extract the event handler commands, if provided in the document - auto& json = root->content()->getDocument()->json(); + auto& json = document->content()->getDocument()->json(); auto it = json.FindMember(sDocumentPropertyBimap.at(kDocumentPropertyOnDisplayStateChange).c_str()); if (it == json.MemberEnd()) { return nullptr; } - auto context = root->createDocumentContext("DisplayStateChange", mProperties); + auto context = document->createDocumentContext("DisplayStateChange", mProperties); auto commands = asCommand(*context, evaluate(*context, it->value)); auto cmd = ArrayCommand::create(context, commands, nullptr, Properties(), "", true); diff --git a/aplcore/src/command/documentcommand.cpp b/aplcore/src/command/documentcommand.cpp index a7f83ed..f777f36 100644 --- a/aplcore/src/command/documentcommand.cpp +++ b/aplcore/src/command/documentcommand.cpp @@ -13,14 +13,14 @@ * permissions and limitations under the License. */ -#include "apl/action/documentaction.h" #include "apl/command/documentcommand.h" -#include "apl/component/corecomponent.h" + +#include "apl/action/documentaction.h" #include "apl/command/arraycommand.h" +#include "apl/component/corecomponent.h" #include "apl/content/content.h" #include "apl/engine/evaluate.h" #include "apl/engine/propdef.h" -#include "apl/engine/rootcontext.h" namespace apl { @@ -44,16 +44,16 @@ DocumentCommand::getDocumentCommand() { // NOTE: We make the large assumption that the name of the document property // is the same name as the component property. - auto root = mRootContext.lock(); - if (!root) + auto document = mDocumentContext.lock(); + if (!document) return nullptr; - auto& json = root->content()->getDocument()->json(); + auto& json = document->content()->getDocument()->json(); auto it = json.FindMember(sComponentPropertyBimap.at(mPropertyKey).c_str()); if (it == json.MemberEnd()) return nullptr; - auto context = root->createDocumentContext(mHandler); + auto context = document->createDocumentContext(mHandler); auto commands = asCommand(*context, evaluate(*context, it->value)); auto cmd = ArrayCommand::create(context, commands, nullptr, Properties(), "", true); return cmd; @@ -62,13 +62,13 @@ DocumentCommand::getDocumentCommand() ActionPtr DocumentCommand::getComponentActions(const TimersPtr& timers, bool fastMode) { - auto root = mRootContext.lock(); - if (!root) + auto document = mDocumentContext.lock(); + if (!document) return nullptr; // Extract the commands from the components std::vector parallelCommands; - collectChildCommands(root->topComponent(), parallelCommands); + collectChildCommands(document->topComponent(), parallelCommands); if (parallelCommands.empty()) return nullptr; @@ -89,11 +89,11 @@ DocumentCommand::getComponentActions(const TimersPtr& timers, bool fastMode) ContextPtr DocumentCommand::context() { - auto root = mRootContext.lock(); - if (!root) + auto document = mDocumentContext.lock(); + if (!document) return nullptr; - return root->payloadContext(); + return document->payloadContext(); } /** @@ -106,4 +106,4 @@ DocumentCommand::execute(const TimersPtr& timers, bool fastMode) { return DocumentAction::make(timers, std::static_pointer_cast(shared_from_this()), fastMode); } -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/command/extensioneventcommand.cpp b/aplcore/src/command/extensioneventcommand.cpp index b099964..8f99592 100644 --- a/aplcore/src/command/extensioneventcommand.cpp +++ b/aplcore/src/command/extensioneventcommand.cpp @@ -45,7 +45,7 @@ ExtensionEventCommand::execute(const TimersPtr& timers, bool fastMode) } else { const auto& bfunc = sBindingFunctions.at(m.second.btype); auto raw = m.second.btype == kBindingTypeArray ? arrayify(*mContext, it->second) : it->second; - map->emplace(m.first, bfunc(*mContext, evaluateRecursive(*mContext, raw))); + map->emplace(m.first, bfunc(*mContext, evaluateNested(*mContext, raw))); } } diff --git a/aplcore/src/command/finishcommand.cpp b/aplcore/src/command/finishcommand.cpp index 30b6c4d..94f44ae 100644 --- a/aplcore/src/command/finishcommand.cpp +++ b/aplcore/src/command/finishcommand.cpp @@ -29,6 +29,9 @@ FinishCommand::propDefSet() const { ActionPtr FinishCommand::execute(const TimersPtr& timers, bool fastMode) { + // TODO: Not supported for now. + if (mContext->embedded()) return nullptr; + if (!calculateProperties()) return nullptr; diff --git a/aplcore/src/command/insertitemcommand.cpp b/aplcore/src/command/insertitemcommand.cpp new file mode 100644 index 0000000..20d611f --- /dev/null +++ b/aplcore/src/command/insertitemcommand.cpp @@ -0,0 +1,66 @@ +/** + * Copyright 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 "apl/command/insertitemcommand.h" +#include "apl/engine/builder.h" +#include "apl/utils/session.h" + +namespace { + +int +getClampedIndex(const int childCount, const int requestedIndex) { + return requestedIndex < 0 + ? std::max(0, childCount + requestedIndex) + : std::min(requestedIndex, childCount); +} + +} // namespace unnamed + +namespace apl { + +const CommandPropDefSet& +InsertItemCommand::propDefSet() const { + static CommandPropDefSet sInsertItemCommandProperties(CoreCommand::propDefSet(), { + { kCommandPropertyAt, INT_MAX, asInteger }, + { kCommandPropertyComponentId, "", asString , kPropRequiredId }, + { kCommandPropertyItem, Object::EMPTY_ARRAY(), asArray } + }); + + return sInsertItemCommandProperties; +} + +ActionPtr +InsertItemCommand::execute(const TimersPtr& timers, bool fastMode) { + + if (!calculateProperties()) + return nullptr; + + const int index = getClampedIndex( + (int) target()->getChildCount(), + getValue(kCommandPropertyAt).asInt()); + + auto child = Builder().inflate( + target()->getContext(), + getValue(kCommandPropertyItem)); + + if (!child || !child->isValid()) + CONSOLE(mContext) << "Could not inflate item to be inserted"; + else if (!target()->insertChild(child, index)) + CONSOLE(mContext) << "Could not insert child into '" << target()->getId() << "'"; + + return nullptr; +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/command/parallelcommand.cpp b/aplcore/src/command/parallelcommand.cpp index 6e64f90..002bd06 100644 --- a/aplcore/src/command/parallelcommand.cpp +++ b/aplcore/src/command/parallelcommand.cpp @@ -39,7 +39,7 @@ ParallelCommand::execute(const TimersPtr& timers, bool fastMode) { for (auto& command : commands.getArray()) { auto self = std::static_pointer_cast(shared_from_this()); - auto commandPtr = CommandFactory::instance().inflate(command, self); + auto commandPtr = CommandFactory::instance().inflate({command, data()}, self); if (commandPtr) { auto childSeq = commandPtr->sequencer(); if (childSeq != sequencer()) { diff --git a/aplcore/src/command/reinflatecommand.cpp b/aplcore/src/command/reinflatecommand.cpp index 86a076a..1256f4d 100644 --- a/aplcore/src/command/reinflatecommand.cpp +++ b/aplcore/src/command/reinflatecommand.cpp @@ -14,6 +14,7 @@ */ #include "apl/command/reinflatecommand.h" +#include "apl/component/hostcomponent.h" #include "apl/time/sequencer.h" namespace apl { @@ -45,6 +46,12 @@ ReinflateCommand::execute(const TimersPtr& timers, bool fastMode) mContext->sequencer().setPreservedSequencers(sequencers); } + if (mContext->embedded()) { + // Directly initiate reinflate on relevant document. + std::static_pointer_cast(mContext->topComponent()->getParent())->reinflate(); + return nullptr; + } + // Return a simple action that pushes the event and does nothing else. The view host must // resolve this event to allow further events in the sequencer to execute. return Action::make(timers, [this](ActionRef ref) { diff --git a/aplcore/src/command/removeitemcommand.cpp b/aplcore/src/command/removeitemcommand.cpp new file mode 100644 index 0000000..d278cf8 --- /dev/null +++ b/aplcore/src/command/removeitemcommand.cpp @@ -0,0 +1,46 @@ +/** + * Copyright 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 "apl/command/removeitemcommand.h" +#include "apl/utils/session.h" + +namespace apl { + +const CommandPropDefSet& +RemoveItemCommand::propDefSet() const { + static CommandPropDefSet sRemoveItemCommandProperties(CoreCommand::propDefSet(), { + { kCommandPropertyComponentId, "", asString, kPropRequiredId }, + }); + + return sRemoveItemCommandProperties; +} + +ActionPtr +RemoveItemCommand::execute(const TimersPtr& timers, bool fastMode) { + if (!calculateProperties()) + return nullptr; + + if (auto comp = target()) { + if (comp->remove()) { + comp->release(); + } else { + CONSOLE(mContext) << "Component '" << target()->getId() << "' cannot be removed"; + } + } + + return nullptr; +} + +} // namespace apl diff --git a/aplcore/src/command/selectcommand.cpp b/aplcore/src/command/selectcommand.cpp index 29d17c1..0107031 100644 --- a/aplcore/src/command/selectcommand.cpp +++ b/aplcore/src/command/selectcommand.cpp @@ -44,7 +44,7 @@ SelectCommand::execute(const TimersPtr& timers, bool fastMode) { // If there is no data, we look for the first valid command if (data.empty()) { for (const auto& command : commands) { - auto ptr = CommandFactory::instance().inflate(context(), command, mBase); + auto ptr = CommandFactory::instance().inflate(context(), {command, this->data()}, mBase); if (ptr) return DelayAction::make(timers, ptr, fastMode); } @@ -59,7 +59,7 @@ SelectCommand::execute(const TimersPtr& timers, bool fastMode) { // Look for an executable command for (const auto& command : commands) { - auto ptr = CommandFactory::instance().inflate(childContext, command, mBase); + auto ptr = CommandFactory::instance().inflate(childContext, {command, this->data()}, mBase); if (ptr) return DelayAction::make(timers, ptr, fastMode); } @@ -73,7 +73,7 @@ SelectCommand::execute(const TimersPtr& timers, bool fastMode) { if (otherwise.empty()) return nullptr; - auto arrayCommand = ArrayCommand::create(context(), otherwise, base(), Properties(), sequencer()); + auto arrayCommand = ArrayCommand::create(context(), {otherwise, this->data()}, base(), Properties(), sequencer()); return arrayCommand->execute(timers, fastMode); } diff --git a/aplcore/src/command/sendeventcommand.cpp b/aplcore/src/command/sendeventcommand.cpp index d56625d..2778d46 100644 --- a/aplcore/src/command/sendeventcommand.cpp +++ b/aplcore/src/command/sendeventcommand.cpp @@ -24,22 +24,6 @@ namespace apl { const static bool DEBUG_SEND_EVENT = false; -CommandPtr -SendEventCommand::create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; -} - -SendEventCommand::SendEventCommand(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) -{} - const CommandPropDefSet& SendEventCommand::propDefSet() const { static CommandPropDefSet sSendEventCommandProperties(CoreCommand::propDefSet(), { diff --git a/aplcore/src/component/CMakeLists.txt b/aplcore/src/component/CMakeLists.txt index fa12064..7a115fa 100644 --- a/aplcore/src/component/CMakeLists.txt +++ b/aplcore/src/component/CMakeLists.txt @@ -24,6 +24,7 @@ target_sources_local(apl edittextcomponent.cpp framecomponent.cpp gridsequencecomponent.cpp + hostcomponent.cpp imagecomponent.cpp mediacomponenttrait.cpp multichildscrollablecomponent.cpp diff --git a/aplcore/src/component/actionablecomponent.cpp b/aplcore/src/component/actionablecomponent.cpp index 5f0e37c..45e11d0 100644 --- a/aplcore/src/component/actionablecomponent.cpp +++ b/aplcore/src/component/actionablecomponent.cpp @@ -76,7 +76,7 @@ ActionableComponent::executeKeyHandlers(KeyHandlerType type, const Keyboard& key if (!handlers.isArray()) return false; - ContextPtr eventContext = createKeyboardEventContext(handlerId, keyboard.serialize()); + ContextPtr eventContext = createKeyEventContext(handlerId, keyboard.serialize()); for (const auto& handler: handlers.getArray()) { if (handler.isMap() && propertyAsBoolean(*eventContext, handler, "when", true)) { @@ -216,7 +216,7 @@ ActionableComponent::getUserSpecifiedNextFocus(FocusDirection direction) if (it != focusDirectionToNextProperty().end()) { auto componentId = getCalculated(it->second).getString(); if (!componentId.empty()) { - return CoreComponent::cast(getContext()->findComponentById(componentId)); + return CoreComponent::cast(getContext()->findComponentById(componentId, false)); } } return nullptr; diff --git a/aplcore/src/component/componentproperties.cpp b/aplcore/src/component/componentproperties.cpp index 58cd711..8b3aae3 100644 --- a/aplcore/src/component/componentproperties.cpp +++ b/aplcore/src/component/componentproperties.cpp @@ -324,8 +324,11 @@ Bimap sComponentPropertyBimap = { {kPropertyDisabled, "disabled"}, {kPropertyDisplay, "display"}, {kPropertyDrawnBorderWidth, "_drawnBorderWidth"}, + {kPropertyEmbeddedDocument, "embeddedDocument"}, {kPropertyEnd, "end"}, {kPropertyEntities, "entities"}, + {kPropertyEntities, "entity"}, + {kPropertyEnvironment, "environment"}, {kPropertyFastScrollScale, "-fastScrollScale"}, {kPropertyFilters, "filters"}, {kPropertyFilters, "filter"}, @@ -477,6 +480,7 @@ Bimap sComponentTypeBimap = { {kComponentTypeExtension, "_ExtensionComponent"}, {kComponentTypeFrame, "Frame"}, {kComponentTypeGridSequence, "GridSequence"}, + {kComponentTypeHost, "Host"}, {kComponentTypeImage, "Image"}, {kComponentTypePager, "Pager"}, {kComponentTypeScrollView, "ScrollView"}, diff --git a/aplcore/src/component/corecomponent.cpp b/aplcore/src/component/corecomponent.cpp index 2b4b8e7..80bfb07 100644 --- a/aplcore/src/component/corecomponent.cpp +++ b/aplcore/src/component/corecomponent.cpp @@ -26,11 +26,10 @@ #include "apl/component/yogaproperties.h" #include "apl/content/rootconfig.h" #include "apl/engine/builder.h" -#include "apl/engine/componentdependant.h" #include "apl/engine/contextwrapper.h" #include "apl/engine/hovermanager.h" -#include "apl/engine/keyboardmanager.h" #include "apl/engine/layoutmanager.h" +#include "apl/engine/typeddependant.h" #include "apl/focus/focusmanager.h" #include "apl/livedata/layoutrebuilder.h" #include "apl/livedata/livearray.h" @@ -69,6 +68,7 @@ const static bool DEBUG_LAYOUTDIRECTION = false; const static bool DEBUG_PADDING = false; const static bool DEBUG_MEASUREMENT = false; +using ComponentDependant = TypedDependant; /** * A helper function to zero the insets for when a component's position is change to "sticky" @@ -127,7 +127,8 @@ void CoreComponent::scheduleTickHandler(const Object& handler, double delay) { auto weak_ptr = std::weak_ptr(shared_from_corecomponent()); // Lambda capture takes care of handler here as it's a copy. - mContext->getRootConfig().getTimeManager()->setTimeout([weak_ptr, handler, delay]() { + mTickHandlerId = mContext->getRootConfig() + .getTimeManager()->setTimeout([weak_ptr, handler, delay]() { auto self = weak_ptr.lock(); if (!self) return; @@ -217,7 +218,7 @@ CoreComponent::initialize() void CoreComponent::release() { - traverse([](CoreComponent& c) {}, + traverse([](CoreComponent& c) { c.preRelease(); }, [](CoreComponent& c) { c.releaseSelf(); }); } @@ -229,6 +230,23 @@ CoreComponent::releaseSelf() RecalculateTarget::removeUpstreamDependencies(); mParent = nullptr; mChildren.clear(); + clearActiveStateSelf(); +} + +void +CoreComponent::clearActiveState() +{ + traverse([](CoreComponent& c) {}, + [](CoreComponent& c) { c.clearActiveStateSelf(); }); +} + +void +CoreComponent::clearActiveStateSelf() +{ + if (mTickHandlerId) { + mContext->getRootConfig().getTimeManager()->clearTimeout(mTickHandlerId); + mTickHandlerId = 0; + } } /** @@ -274,6 +292,12 @@ CoreComponent::traverse(const Pre& pre, const Post& post) ComponentPtr CoreComponent::findComponentById(const std::string& id) const +{ + return findComponentById(id, true); +} + +ComponentPtr +CoreComponent::findComponentById(const std::string& id, bool traverseHost) const { if (id.empty()) return nullptr; @@ -282,7 +306,7 @@ CoreComponent::findComponentById(const std::string& id) const return std::const_pointer_cast(shared_from_corecomponent()); for (auto& m : mChildren) { - auto result = m->findComponentById(id); + auto result = m->findComponentById(id, traverseHost); if (result) return result; } @@ -391,8 +415,6 @@ CoreComponent::notifyChildChanged(size_t index, const std::string& uid, const st void CoreComponent::attachYogaNodeIfRequired(const CoreComponentPtr& coreChild, int index) { - // The default behavior is to attach the child. Override this for - // Pager and MultiChildScrollableComponent YGNodeInsertChild(mYGNodeRef, coreChild->getNode(), index); } @@ -547,6 +569,12 @@ CoreComponent::insertChild(const CoreComponentPtr& child, size_t index, bool use bool CoreComponent::remove() +{ + return remove(true); +} + +bool +CoreComponent::remove(bool useDirtyFlag) { if (!mParent || !mParent->canRemoveChild()) return false; @@ -564,7 +592,7 @@ CoreComponent::remove() } } - mParent->removeChild(shared_from_corecomponent(), true); + mParent->removeChild(shared_from_corecomponent(), useDirtyFlag); mParent = nullptr; return true; } @@ -582,7 +610,7 @@ CoreComponent::removeChildAfterMarkedRemoved(const CoreComponentPtr& child, size markDisplayedChildrenStale(useDirtyFlag); mDisplayedChildren.clear(); - setVisualContextDirty(); + if (useDirtyFlag) setVisualContextDirty(); // Update the position: sticky components tree auto p = stickyfunctions::getHorizontalAndVerticalScrollable(shared_from_corecomponent()); @@ -698,6 +726,9 @@ CoreComponent::isLaidOut() const if (YGNodeIsDirty(node)) return false; + if (mContext->layoutManager().isTopNode(component)) + return true; + auto parent = CoreComponent::cast(component->getParent()); if (!parent) return true; @@ -709,7 +740,6 @@ CoreComponent::isLaidOut() const } } - void CoreComponent::updateNodeProperties() { @@ -855,28 +885,23 @@ CoreComponent::assignProperties(const ComponentPropDefSet& propDefSet) // Check for user-defined property auto p = mProperties.find(pd.names); if (p != mProperties.end()) { - // If the user assigned a string, we need to check for data binding - if (p->second.isString()) { - auto tmp = parseDataBinding(*mContext, p->second.getString()); // Expand data-binding - if (tmp.isEvaluable()) { - auto self = shared_from_corecomponent(); - ComponentDependant::create(self, pd.key, tmp, mContext, pd.getBindingFunction()); - } - value = pd.calculate(*mContext, evaluate(*mContext, tmp)); // Calculate the final value - } - else if ((pd.flags & kPropEvaluated) != 0) { - // Explicitly marked for evaluation, so do it. - // Will not attach dependant if no valid symbols. - auto tmp = parseDataBindingRecursive(*mContext, p->second); - auto self = shared_from_corecomponent(); - ComponentDependant::create(self, pd.key, tmp, mContext, pd.getBindingFunction()); - value = pd.calculate(*mContext, p->second); + if (p->second.isString() || (pd.flags & kPropEvaluated) != 0) { + auto result = parseAndEvaluate(*mContext, p->second); + if (!result.symbols.empty()) + ComponentDependant::create(shared_from_corecomponent(), + pd.key, + result.expression, + mContext, + pd.getBindingFunction(), + std::move(result.symbols)); + value = pd.calculate(*mContext, result.value); } else { value = pd.calculate(*mContext, p->second); } mAssigned.emplace(pd.key); - } else { + } + else { // Make sure this wasn't a required property if ((pd.flags & kPropRequired) != 0) { mFlags |= kComponentFlagInvalid; @@ -1217,7 +1242,7 @@ CoreComponent::markProperty(PropertyKey key) } void -CoreComponent::updateProperty(PropertyKey key, const Object& value) +CoreComponent::setValue(PropertyKey key, const Object& value, bool useDirtyFlag) { auto it = mAssigned.find(key); if (it == mAssigned.end()) @@ -1244,7 +1269,7 @@ CoreComponent::updateProperty(PropertyKey key, const Object& value) return; } - // We should not reach this point. Only an assigned equation calls updateProperty + // We should not reach this point. Only an assigned equation calls setValue CONSOLE(mContext) << "Property " << sComponentPropertyBimap.at(key) << " is not dynamic and can't be updated."; } @@ -1400,6 +1425,14 @@ CoreComponent::clearDirty() fixVisualHash(false); } +CoreComponentPtr +CoreComponent::getParentIfInDocument() const +{ + return mParent && mParent->getType() != kComponentTypeHost + ? mParent + : nullptr; +} + void CoreComponent::updateInheritedState() { @@ -1653,7 +1686,7 @@ CoreComponent::createEventContext(const std::string& handler, const ObjectMapPtr } ContextPtr -CoreComponent::createKeyboardEventContext(const std::string& handler, const ObjectMapPtr& keyboard) const +CoreComponent::createKeyEventContext(const std::string& handler, const ObjectMapPtr& keyboard) const { ContextPtr ctx = Context::createFromParent(mContext); auto event = createEventProperties(handler, getValue()); @@ -1708,7 +1741,7 @@ CoreComponent::serializeEvent(rapidjson::Value& out, rapidjson::Document::Alloca out.AddMember(rapidjson::Value(m.first.c_str(), allocator), m.second(this).serialize(allocator).Move(), allocator); } -static const char sHierarchySig[] = "CEXFMIPSQTWGV"; // Must match ComponentType +static const char sHierarchySig[] = "CEXFMHIPSQTWGV"; // Must match ComponentType std::string CoreComponent::getHierarchySignature() const @@ -1788,7 +1821,7 @@ CoreComponent::fixPadding() // That value may be overridden by the specific paddingLeft/Top/Right/Bottom values if (!setPaddingIfKeyFound(edge.first, edge.second, mCalculated, mYGNodeRef, mContext)) { // If edge isn't set directly use padding value assigned by the "padding" property - auto assigned = commonPadding.size() >= i ? commonPadding.at(i) : Dimension(0); + auto assigned = commonPadding.size() > i ? commonPadding.at(i) : Dimension(0); yn::setPadding(mYGNodeRef, edge.second, assigned, *mContext); } } @@ -1940,11 +1973,12 @@ CoreComponent::serializeVisualContext(rapidjson::Document::AllocatorType& alloca } void -CoreComponent::serializeVisualContextInternal(rapidjson::Value &outArray, rapidjson::Document::AllocatorType& allocator, - float realOpacity, float visibility, const Rect& visibleRect, int visualLayer) +CoreComponent::serializeVisualContextInternal( + rapidjson::Value &outArray, rapidjson::Document::AllocatorType& allocator, float realOpacity, + float visibility, const Rect& visibleRect, int visualLayer) { - - if(visibility == 0.0 && mParent) { + auto parentInDocument = getParentIfInDocument(); + if(visibility == 0.0 && parentInDocument) { // Not visible and not viewport component. return; } @@ -1953,12 +1987,19 @@ CoreComponent::serializeVisualContextInternal(rapidjson::Value &outArray, rapidj bool actionable = !getCalculated(kPropertyEntities).empty(); rapidjson::Value tags(rapidjson::kObjectType); actionable |= getTags(tags, allocator); - bool includeInContext = !mParent || (visibility > 0.0 && actionable); + + bool isTopLevelElement = !mParent || !mParent->includeChildrenInVisualContext(); + if (isTopLevelElement) { + tags.AddMember("viewport", rapidjson::Value(rapidjson::kObjectType), allocator); + actionable |= true; + } + + bool includeInContext = !parentInDocument || (visibility > 0.0 && actionable); rapidjson::Value children(rapidjson::kArrayType); // Process children - if (!mChildren.empty() && visibility > 0.0) { + if (!mChildren.empty() && includeChildrenInVisualContext() && visibility > 0.0) { auto visibleIndexes = getChildrenVisibility(realOpacity, visibleRect); auto visualLayers = calculateChildrenVisualLayer(visibleIndexes, visibleRect, visualLayer); for (auto childIdx : visibleIndexes) { @@ -1968,7 +2009,7 @@ CoreComponent::serializeVisualContextInternal(rapidjson::Value &outArray, rapidj auto childVisibility = childIdx.second; auto childVisualLayer = visualLayers.at(childIdx.first); child->serializeVisualContextInternal( - includeInContext? children : outArray, allocator, + includeInContext ? children : outArray, allocator, childRealOpacity, childVisibility, childVisibleRect, childVisualLayer); } } @@ -2112,11 +2153,6 @@ CoreComponent::getTags(rapidjson::Value &outMap, rapidjson::Document::AllocatorT actionable |= true; } - if(!mParent) { - outMap.AddMember("viewport", rapidjson::Value(rapidjson::kObjectType), allocator); - actionable |= true; - } - return actionable; } @@ -2221,12 +2257,13 @@ CoreComponent::fixSpacing(bool reset) { auto spacing = getCalculated(kPropertySpacing).asDimension(*mContext); if (reset) spacing = 0; if (spacing.isAbsolute()) { - YGNodeRef parent = YGNodeGetParent(mYGNodeRef); - if (!parent) + auto parentNode = YGNodeGetParent(mYGNodeRef); + auto parentComponent = getParentIfInDocument(); + if (!parentNode || !parentComponent) return; - auto parentLayoutDirection = getParent()->getCalculated(kPropertyLayoutDirection); - auto dir = YGNodeStyleGetFlexDirection(parent); + auto parentLayoutDirection = parentComponent->getCalculated(kPropertyLayoutDirection); + auto dir = YGNodeStyleGetFlexDirection(parentNode); YGEdge edge = YGEdgeLeft; switch (dir) { case YGFlexDirectionColumn: edge = YGEdgeTop; break; @@ -2396,6 +2433,8 @@ CoreComponent::propDefSet() const { kPropMixedState | kPropVisualContext}, {kPropertyEntities, Object::EMPTY_ARRAY(), asDeepArray, kPropIn | + kPropDynamic | + kPropEvaluated | kPropVisualContext}, {kPropertyFocusable, false, nullptr, kPropOut}, {kPropertyHandleTick, Object::EMPTY_ARRAY(), asArray, kPropIn}, @@ -2541,7 +2580,7 @@ CoreComponent::inParentViewport() const { } PointerCaptureStatus -CoreComponent::processPointerEvent(const PointerEvent& event, apl_time_t timestamp) +CoreComponent::processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) { if (mState.get(kStateDisabled)) return kPointerStatusNotCaptured; @@ -2549,6 +2588,9 @@ CoreComponent::processPointerEvent(const PointerEvent& event, apl_time_t timesta if (processGestures(event, timestamp)) return kPointerStatusCaptured; + if (onlyProcessGestures) + return kPointerStatusNotCaptured; + auto pointInCurrent = toLocalPoint(event.pointerEventPosition); auto it = sEventHandlers.find(event.pointerEventType); if (it != sEventHandlers.end()) @@ -2621,11 +2663,12 @@ CoreComponent::getLayoutDirection() const { auto direction = YGNodeStyleGetDirection(mYGNodeRef); if (direction == YGDirectionInherit) { - if (!mParent) + auto parentInDocument = getParentIfInDocument(); + if (!parentInDocument) // Fallback to document level layoutDirection return mContext->getLayoutDirection() == kLayoutDirectionRTL ? YGDirectionRTL : YGDirectionLTR; - return mParent->getLayoutDirection(); + return parentInDocument->getLayoutDirection(); } return direction; } diff --git a/aplcore/src/component/edittextcomponent.cpp b/aplcore/src/component/edittextcomponent.cpp index 133d037..46eb35f 100644 --- a/aplcore/src/component/edittextcomponent.cpp +++ b/aplcore/src/component/edittextcomponent.cpp @@ -334,9 +334,9 @@ EditTextComponent::isCharacterValid(const wchar_t wc) const } PointerCaptureStatus -EditTextComponent::processPointerEvent(const PointerEvent& event, apl_time_t timestamp) +EditTextComponent::processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) { - auto pointerStatus = ActionableComponent::processPointerEvent(event, timestamp); + auto pointerStatus = ActionableComponent::processPointerEvent(event, timestamp, onlyProcessGestures); if (pointerStatus != kPointerStatusNotCaptured) return pointerStatus; @@ -636,7 +636,6 @@ EditTextComponent::ensureEditConfig() getCalculated(kPropertyColor).getColor(), getCalculated(kPropertyHighlightColor).getColor(), getCalculated(kPropertyKeyboardType).asEnum(), - getCalculated(kPropertyLang).asString(), getCalculated(kPropertyMaxLength).asInt(), getCalculated(kPropertySecureInput).asBoolean(), getCalculated(kPropertySubmitKeyType).asEnum(), @@ -657,9 +656,11 @@ EditTextComponent::ensureEditTextProperties() mEditTextProperties = sg::TextProperties::create( mContext->textPropertiesCache(), sg::splitFontString(mContext->getRootConfig(), + mContext->session(), getCalculated(kPropertyFontFamily).getString()), getCalculated(kPropertyFontSize).asFloat(), getCalculated(kPropertyFontStyle).asEnum(), + getCalculated(kPropertyLang).getString(), getCalculated(kPropertyFontWeight).getInteger()); return true; @@ -685,9 +686,11 @@ EditTextComponent::ensureHintLayout() mHintTextProperties = sg::TextProperties::create( context->textPropertiesCache(), sg::splitFontString(context->getRootConfig(), + context->session(), getCalculated(kPropertyFontFamily).getString()), getCalculated(kPropertyFontSize).asFloat(), getCalculated(kPropertyHintStyle).asEnum(), + getCalculated(kPropertyLang).getString(), getCalculated(kPropertyHintWeight).getInteger(), 0, // Letter spacing 1.25f, // Line height diff --git a/aplcore/src/component/hostcomponent.cpp b/aplcore/src/component/hostcomponent.cpp new file mode 100644 index 0000000..a7392fa --- /dev/null +++ b/aplcore/src/component/hostcomponent.cpp @@ -0,0 +1,421 @@ +/** +* Copyright 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 "apl/component/hostcomponent.h" + +#include +#include +#include + +#include "apl/component/componentproperties.h" +#include "apl/component/corecomponent.h" +#include "apl/component/yogaproperties.h" +#include "apl/content/content.h" +#include "apl/document/coredocumentcontext.h" +#include "apl/embed/documentregistrar.h" +#include "apl/engine/keyboardmanager.h" +#include "apl/engine/layoutmanager.h" +#include "apl/engine/propdef.h" +#include "apl/engine/sharedcontextdata.h" +#include "apl/engine/tickscheduler.h" +#include "apl/primitives/dimension.h" +#include "apl/primitives/object.h" +#include "apl/primitives/rect.h" +#include "apl/time/sequencer.h" + +namespace { + +using namespace apl; + +// copied from corecomponent.cpp (similar to pagercomponent.cpp) +inline Object +defaultWidth(Component& component, const RootConfig& rootConfig) +{ + return rootConfig.getDefaultComponentWidth(component.getType()); +} + +// copied from corecomponent.cpp (similar to pagercomponent.cpp) +inline Object +defaultHeight(Component& component, const RootConfig& rootConfig) +{ + return rootConfig.getDefaultComponentHeight(component.getType()); +} + +} // unnamed namespace + +namespace apl { + +CoreComponentPtr +HostComponent::create(const ContextPtr& context, + Properties&& properties, + const Path& path) +{ + auto ptr = std::make_shared( + context, + std::move(properties), + path); + ptr->initialize(); + return ptr; +} + +HostComponent::HostComponent(const ContextPtr& context, + Properties&& properties, + const Path& path) + : ActionableComponent(context, std::move(properties), path) +{} + +const ComponentPropDefSet& +HostComponent::propDefSet() const +{ + static auto resetOnLoadOnFailFlag = [](Component& component) { + auto& host = ((HostComponent&)component); + host.mOnLoadOnFailReported = false; + host.mNeedToRequestDocument = true; + host.releaseEmbedded(); + host.requestEmbedded(); + }; + + static ComponentPropDefSet sHostComponentProperties(ActionableComponent::propDefSet(), { + {kPropertyHeight, Dimension(100), asNonAutoDimension, kPropIn | kPropDynamic | kPropStyled, yn::setHeight, defaultHeight}, + {kPropertyWidth, Dimension(100), asNonAutoDimension, kPropIn | kPropDynamic | kPropStyled, yn::setWidth, defaultWidth}, + {kPropertyEnvironment, Object::EMPTY_MAP(), asAny, kPropIn}, + {kPropertyOnFail, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnLoad, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertySource, "", asUrlRequest, kPropRequired | kPropIn | kPropDynamic | kPropVisualHash | kPropEvaluated, resetOnLoadOnFailFlag}, + {kPropertyEmbeddedDocument, Object::NULL_OBJECT(), asAny, kPropDynamic | kPropRuntimeState}, + }); + + return sHostComponentProperties; +} + +void +HostComponent::requestEmbedded() +{ + const auto source = URLRequest::asURLRequest(getProperty(kPropertySource)); + mRequest = EmbedRequest::create(source, mContext->documentContext()); + + if (auto oldEmbeddedId = getDocumentId()) { + mNeedToRequestDocument = false; + + auto& registrar = mContext->getShared()->documentRegistrar(); + auto embedded = registrar.get(oldEmbeddedId); + + // We have old document defined. Reuse as a result of the request. + detachEmbedded(); + + registrar.deregisterDocument(oldEmbeddedId); + + auto coreTop = CoreComponent::cast(embedded->topComponent()); + // Can't really fail + assert(CoreComponent::insertChild(coreTop, 0, false)); + + // Update with new registration + setDocument(registrar.registerDocument(embedded), includeChildrenInVisualContext()); + + mContext->layoutManager().setAsTopNode(coreTop); + mContext->layoutManager().requestLayout(coreTop, false); + + return; + } + + std::weak_ptr weakHost = std::static_pointer_cast(shared_from_this()); + + mContext->getShared()->documentManager().request( + mRequest, + [weakHost](EmbeddedRequestSuccessResponse&& response) { + auto host = weakHost.lock(); + return host + ? host->onLoad(std::move(response)) + : nullptr; + }, + [weakHost](EmbeddedRequestFailureResponse&& response) { + auto host = weakHost.lock(); + if (host) + host->onFail(std::move(response)); + }); + + mNeedToRequestDocument = false; +} + +DocumentContextPtr +HostComponent::onLoad(const EmbeddedRequestSuccessResponse&& response) +{ + if (mOnLoadOnFailReported) + return nullptr; + mOnLoadOnFailReported = true; + + onLoadHandler(); + return initializeEmbedded(std::move(response)); +} + +void +HostComponent::onLoadHandler() +{ + auto& commands = getCalculated(kPropertyOnLoad); + mContext->sequencer().executeCommands( + commands, + createEventContext("Load"), + shared_from_corecomponent(), + true); +} + +DocumentContextPtr +HostComponent::initializeEmbedded(const EmbeddedRequestSuccessResponse&& response) +{ + resolvePendingParameters(response.content); + + auto embedded = CoreDocumentContext::create( + mContext, + response.content, + getProperty(kPropertyEnvironment), + getCalculated(kPropertyInnerBounds).get().getSize(), + response.documentConfig); + + const URLRequest& url = response.request->getUrlRequest(); + if (!embedded || !embedded->setup(nullptr)) { + mRequest = nullptr; + onFailHandler(url, "Embedded document failed to inflate"); + return nullptr; + } + + auto coreTop = CoreComponent::cast(embedded->topComponent()); + + bool result = CoreComponent::insertChild(coreTop, 0, true); + if (!result) { + mRequest = nullptr; + onFailHandler(url, "Failed to insert embedded document's top component"); + return nullptr; + } + + mContext->layoutManager().setAsTopNode(coreTop); + mContext->layoutManager().requestLayout(coreTop, false); + + embedded->processOnMounts(); + mContext->getShared()->tickScheduler().processTickHandlers(embedded); + + auto id = mContext->getShared()->documentRegistrar().registerDocument(embedded); + setDocument(id, response.connectedVisualContext); + + return embedded; +} + +void +HostComponent::reinflate() +{ + auto embeddedId = getDocumentId(); + if (!embeddedId) return; + + auto embedded = mContext->getShared()->documentRegistrar().get(embeddedId); + auto coreTop = CoreComponent::cast(embedded->topComponent()); + + mContext->layoutManager().removeAsTopNode(CoreComponent::cast(embedded->topComponent())); + CoreComponent::removeChild(coreTop, false); + + embedded->reinflate([&](){ + auto coreTop = CoreComponent::cast(embedded->topComponent()); + // Can't fail + assert(insertChild(coreTop, 0, true)); + + mContext->layoutManager().setAsTopNode(coreTop); + mContext->layoutManager().requestLayout(coreTop, false); + + embedded->processOnMounts(); + mContext->getShared()->tickScheduler().processTickHandlers(embedded); + }); +} + +bool +HostComponent::includeChildrenInVisualContext() const +{ + auto embeddedDocument = getCalculated(kPropertyEmbeddedDocument); + if (!embeddedDocument.isNull()) { + return embeddedDocument.get("connectedVC").asBoolean(); + } + return false; +} + +void +HostComponent::resolvePendingParameters(const ContentPtr& content) +{ + if (!content->isReady()) { + for (const auto& param : content->getPendingParameters()) { + auto it = mProperties.find(param); + if (it != mProperties.end()) { + auto parameter = it->second; + if (parameter.isString()) { + parameter = evaluate(*mContext, parameter); + } + content->addObjectData(param, parameter); + } + else { + CONSOLE(mContext) << "Missing value for parameter " << param; + } + } + } +} + +void +HostComponent::onFail(const EmbeddedRequestFailureResponse&& response) +{ + if (mOnLoadOnFailReported) + return; + mOnLoadOnFailReported = true; + + onFailHandler(response.request->getUrlRequest(), response.failure); + mRequest = nullptr; +} + +void +HostComponent::onFailHandler(const URLRequest& url, const std::string& failure) +{ + auto& commands = getCalculated(kPropertyOnFail); + auto errorData = std::make_shared(); + errorData->emplace("value", url.getUrl()); + errorData->emplace("error", failure); + auto eventContext = createEventContext("Fail", errorData); + mContext->sequencer().executeCommands( + commands, + eventContext, + shared_from_corecomponent(), + true); +} + +void +HostComponent::detachEmbedded() +{ + auto embeddedId = getDocumentId(); + auto embedded = CoreDocumentContext::cast(mContext->getShared()->documentRegistrar().get(embeddedId)); + if (embedded != nullptr) { + auto coreTop = CoreComponent::cast(embedded->topComponent()); + mContext->getShared()->layoutManager().removeAsTopNode(coreTop); + // Remove without marking dirty - it's no longer useful for detached hierarchy. + coreTop->remove(false); + } +} + +void +HostComponent::preRelease() +{ + // Detach child document hierarchy. Document should handle cleanup itself. + detachEmbedded(); + ActionableComponent::preRelease(); +} + +void +HostComponent::releaseSelf() +{ + releaseEmbedded(); + ActionableComponent::releaseSelf(); +} + +void +HostComponent::releaseEmbedded() +{ + auto embeddedId = getDocumentId(); + auto embedded = CoreDocumentContext::cast(mContext->getShared()->documentRegistrar().get(embeddedId)); + if (embedded != nullptr) { + mContext->getShared()->documentRegistrar().deregisterDocument(embeddedId); + mCalculated.set(kPropertyEmbeddedDocument, Object::NULL_OBJECT()); + } + mRequest = nullptr; +} + +void +HostComponent::setDocument(int id, bool connectedVC) +{ + auto embeddedDocument = std::make_shared(); + embeddedDocument->emplace("id", id); + embeddedDocument->emplace("connectedVC", connectedVC); + mCalculated.set(kPropertyEmbeddedDocument, embeddedDocument); +} + +int +HostComponent::getDocumentId() const +{ + auto embeddedDocument = getCalculated(kPropertyEmbeddedDocument); + if (embeddedDocument.isNull()) return 0; + return embeddedDocument.get("id").asInt(); +} + +ComponentPtr +HostComponent::findComponentById(const std::string& id, const bool traverseHost) const +{ + if (mId == id || mUniqueId == id) + return std::const_pointer_cast(shared_from_this()); + + auto embeddedId = getDocumentId(); + if (!embeddedId || !traverseHost) + return nullptr; + + if (auto embedded = mContext->getShared()->documentRegistrar().get(embeddedId)) + return CoreComponent::cast(embedded->topComponent())->findComponentById(id, traverseHost); + + return nullptr; +} + +std::string +HostComponent::getVisualContextType() const +{ + // Do not leak embedded document type if not linked. + return includeChildrenInVisualContext() + ? ActionableComponent::getVisualContextType() + : VISUAL_CONTEXT_TYPE_EMPTY; +} + +bool +HostComponent::executeKeyHandlers(KeyHandlerType type, const Keyboard& keyboard) +{ + // Embedded document (if exists) should take document handling + if (auto embeddedId = getDocumentId()) { + if (auto embedded = mContext->getShared()->documentRegistrar().get(embeddedId)) { + auto& manager = mContext->getShared()->keyboardManager(); + const auto handledByEmbedded = manager.executeDocumentKeyHandlers( + CoreDocumentContext::cast(embedded), type, keyboard); + if (handledByEmbedded) + return true; + } + } + + return ActionableComponent::executeKeyHandlers(type, keyboard); +} + +void +HostComponent::processLayoutChanges(bool useDirtyFlag, bool first) +{ + auto boundsBeforeLayout = getProperty(kPropertyInnerBounds); + CoreComponent::processLayoutChanges(useDirtyFlag, first); + + auto embeddedId = getDocumentId(); + if (!embeddedId || mNeedToRequestDocument) return; + auto boundsAfterLayout = getProperty(kPropertyInnerBounds); + + auto embedded = mContext->getShared()->documentRegistrar().get(embeddedId); + if (embedded && boundsBeforeLayout != boundsAfterLayout) { + auto size = boundsAfterLayout.get(); + embedded->configurationChange(ConfigurationChange( + // std::lround use copied from Dimension::AbsoluteDimensionObjectType::asInt + std::lround(mContext->dpToPx(size.getWidth())), + std::lround(mContext->dpToPx(size.getHeight())))); + } +} + +void +HostComponent::postProcessLayoutChanges() +{ + ActionableComponent::postProcessLayoutChanges(); + if (mNeedToRequestDocument) requestEmbedded(); +} + +} // namespace apl diff --git a/aplcore/src/component/multichildscrollablecomponent.cpp b/aplcore/src/component/multichildscrollablecomponent.cpp index 78c9189..b3088d5 100644 --- a/aplcore/src/component/multichildscrollablecomponent.cpp +++ b/aplcore/src/component/multichildscrollablecomponent.cpp @@ -71,7 +71,7 @@ void setScrollAlignId(CoreComponent& component, const Object& value) } auto id = value.at(0).asString(); - auto child = CoreComponent::cast(component.findComponentById(id)); + auto child = CoreComponent::cast(component.findComponentById(id, false)); if (!child) { CONSOLE(component.getContext()) << "Unable to find child with id " << id; return; @@ -132,6 +132,13 @@ MultiChildScrollableComponent::propDefSet() const return sSequenceComponentProperties; } +void +MultiChildScrollableComponent::clearActiveStateSelf() +{ + ScrollableComponent::clearActiveStateSelf(); + mDelayLayoutAction = nullptr; +} + bool MultiChildScrollableComponent::allowForward() const { diff --git a/aplcore/src/component/pagercomponent.cpp b/aplcore/src/component/pagercomponent.cpp index a11a19a..be31bea 100644 --- a/aplcore/src/component/pagercomponent.cpp +++ b/aplcore/src/component/pagercomponent.cpp @@ -50,10 +50,19 @@ PagerComponent::PagerComponent(const ContextPtr& context, void PagerComponent::releaseSelf() { - mCurrentAnimation = nullptr; + clearActiveStateSelf(); ActionableComponent::releaseSelf(); } +void +PagerComponent::clearActiveStateSelf() +{ + ActionableComponent::clearActiveStateSelf(); + mPageMoveHandler = nullptr; + mCurrentAnimation = nullptr; + mDelayLayoutAction = nullptr; +} + inline Object defaultWidth(Component& component, const RootConfig& rootConfig) { return rootConfig.getDefaultComponentWidth(component.getType()); @@ -463,7 +472,7 @@ PagerComponent::ensureDisplayedChildren() { // current page is in the viewport CoreComponentPtr currentPage = nullptr; if (mPageMoveHandler) { - currentPage = mPageMoveHandler->getCurrentPage().lock(); + currentPage = mPageMoveHandler->getCheckedCurrentPage(shared_from_corecomponent()); } else { currentPage = mChildren.at(pagePosition()); } @@ -474,7 +483,7 @@ PagerComponent::ensureDisplayedChildren() { // when in a page move transition, add the target next page if (mPageMoveHandler) { // current page - auto nextPage = mPageMoveHandler->getTargetPage().lock(); + auto nextPage = mPageMoveHandler->getCheckedTargetPage(shared_from_corecomponent()); if (nextPage && nextPage->isDisplayable()) { // get the gesture direction by working backwards from swipe direction diff --git a/aplcore/src/component/selector.cpp b/aplcore/src/component/selector.cpp index 5d8a700..d4aec80 100644 --- a/aplcore/src/component/selector.cpp +++ b/aplcore/src/component/selector.cpp @@ -14,13 +14,15 @@ * */ +#include "apl/component/selector.h" + #include #include "apl/component/corecomponent.h" -#include "apl/component/selector.h" #include "apl/datagrammar/grammarpolyfill.h" -#include "apl/engine/rootcontextdata.h" +#include "apl/document/documentcontextdata.h" #include "apl/utils/make_unique.h" +#include "apl/utils/session.h" namespace apl { @@ -55,7 +57,7 @@ static CoreComponentPtr findSibling(const CoreComponentPtr& component, const MatchFunction& matcher, int offset) { assert(component); - auto parent = CoreComponent::cast(component->getParent()); + auto parent = component->getParentIfInDocument(); if (!parent) return nullptr; @@ -88,11 +90,11 @@ static CoreComponentPtr findParent(const CoreComponentPtr& start, const MatchFunction& matcher) { assert(start); - auto parent = CoreComponent::cast(start->getParent()); + auto parent = start->getParentIfInDocument(); while (parent) { if (matcher(parent)) return parent; - parent = CoreComponent::cast(parent->getParent()); + parent = parent->getParentIfInDocument(); } return nullptr; } @@ -277,7 +279,7 @@ template<> struct action< uid > { template< typename Input > static void apply( const Input& in, selector_state& state) { - state.target = CoreComponent::cast(state.context->findComponentById(in.string())); + state.target = CoreComponent::cast(state.context->findComponentById(in.string(), false)); state.somethingMatched = true; LOGF_IF(DEBUG_GRAMMAR, "Find UID: '%s'", in.string().c_str()); } @@ -287,7 +289,7 @@ template<> struct action< top_id > { template< typename Input > static void apply( const Input& in, selector_state& state) { - state.target = CoreComponent::cast(state.context->findComponentById(in.string())); + state.target = CoreComponent::cast(state.context->findComponentById(in.string(), false)); state.somethingMatched = true; LOGF_IF(DEBUG_GRAMMAR, "Find ID: '%s'", in.string().c_str()); } diff --git a/aplcore/src/component/textcomponent.cpp b/aplcore/src/component/textcomponent.cpp index 774956d..b629496 100644 --- a/aplcore/src/component/textcomponent.cpp +++ b/aplcore/src/component/textcomponent.cpp @@ -548,9 +548,11 @@ TextComponent::ensureTextProperties() mTextProperties = sg::TextProperties::create( mContext->textPropertiesCache(), sg::splitFontString(mContext->getRootConfig(), + mContext->session(), getCalculated(kPropertyFontFamily).getString()), getCalculated(kPropertyFontSize).asFloat(), getCalculated(kPropertyFontStyle).asEnum(), + getCalculated(kPropertyLang).getString(), getCalculated(kPropertyFontWeight).getInteger(), getCalculated(kPropertyLetterSpacing).asFloat(), getCalculated(kPropertyLineHeight).asFloat(), diff --git a/aplcore/src/component/touchablecomponent.cpp b/aplcore/src/component/touchablecomponent.cpp index 25e7e7b..d3293dd 100644 --- a/aplcore/src/component/touchablecomponent.cpp +++ b/aplcore/src/component/touchablecomponent.cpp @@ -53,9 +53,9 @@ TouchableComponent::setGestureHandlers() } PointerCaptureStatus -TouchableComponent::processPointerEvent(const PointerEvent& event, apl_time_t timestamp) +TouchableComponent::processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) { - auto pointerStatus = ActionableComponent::processPointerEvent(event, timestamp); + auto pointerStatus = ActionableComponent::processPointerEvent(event, timestamp, onlyProcessGestures); if (pointerStatus != kPointerStatusNotCaptured) return pointerStatus; diff --git a/aplcore/src/component/vectorgraphiccomponent.cpp b/aplcore/src/component/vectorgraphiccomponent.cpp index 93152e5..ecb5845 100644 --- a/aplcore/src/component/vectorgraphiccomponent.cpp +++ b/aplcore/src/component/vectorgraphiccomponent.cpp @@ -401,10 +401,9 @@ VectorGraphicComponent::isFocusable() const // According to the APL specification, a Vector Graphic component should only receive keyboard // focus if at least one of the following handlers are defined: onFocus, onBlur, handleKeyDown, handleKeyUp, // onDown, and onPress. - return !getCalculated(kPropertyOnFocus).empty() || !getCalculated(kPropertyOnBlur).empty() || - !getCalculated(kPropertyHandleKeyDown).empty() || !getCalculated(kPropertyHandleKeyUp).empty() || - !getCalculated(kPropertyOnDown).empty() || !getCalculated(kPropertyOnPress).empty() || - !getCalculated(kPropertyGestures).empty(); + return isTouchable() || + !getCalculated(kPropertyOnFocus).empty() || !getCalculated(kPropertyOnBlur).empty() || + !getCalculated(kPropertyHandleKeyDown).empty() || !getCalculated(kPropertyHandleKeyUp).empty(); } bool @@ -417,8 +416,8 @@ VectorGraphicComponent::isActionable() const bool VectorGraphicComponent::isTouchable() const { - // Same rules as for focus - return isFocusable(); + return !getCalculated(kPropertyOnDown).empty() || !getCalculated(kPropertyOnPress).empty() || + !getCalculated(kPropertyGestures).empty(); } std::vector diff --git a/aplcore/src/component/videocomponent.cpp b/aplcore/src/component/videocomponent.cpp index 6794bf3..15bc63d 100644 --- a/aplcore/src/component/videocomponent.cpp +++ b/aplcore/src/component/videocomponent.cpp @@ -196,6 +196,17 @@ VideoComponent::remove() return CoreComponent::remove(); } +void +VideoComponent::releaseSelf() +{ + if (mMediaPlayer) { + mMediaPlayer->halt(); + mMediaPlayer = nullptr; + } + + CoreComponent::releaseSelf(); +} + const ComponentPropDefSet& VideoComponent::propDefSet() const { diff --git a/aplcore/src/component/yogaproperties.cpp b/aplcore/src/component/yogaproperties.cpp index 565b00e..1d2a42f 100644 --- a/aplcore/src/component/yogaproperties.cpp +++ b/aplcore/src/component/yogaproperties.cpp @@ -177,7 +177,7 @@ void setPosition(YGNodeRef nodeRef, YGEdge edge, const Object& value, const Cont void setFlexDirection(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX).session(context) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << sContainerDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; auto flexDirection = static_cast(value.asInt()); switch (flexDirection) { case kContainerDirectionColumn: diff --git a/aplcore/src/content/aplversion.cpp b/aplcore/src/content/aplversion.cpp index 4018841..56b29a6 100644 --- a/aplcore/src/content/aplversion.cpp +++ b/aplcore/src/content/aplversion.cpp @@ -34,6 +34,7 @@ static const Bimap sVersionMap = { { APLVersion::kAPLVersion20221, "2022.1" }, { APLVersion::kAPLVersion20222, "2022.2" }, { APLVersion::kAPLVersion20231, "2023.1" }, + { APLVersion::kAPLVersion20232, "2023.2" }, }; bool diff --git a/aplcore/src/content/configurationchange.cpp b/aplcore/src/content/configurationchange.cpp index 4e26d17..7fca597 100644 --- a/aplcore/src/content/configurationchange.cpp +++ b/aplcore/src/content/configurationchange.cpp @@ -98,17 +98,6 @@ ConfigurationChange::mergeConfigurationChange(const ConfigurationChange& other) } } -Size -ConfigurationChange::mergeSize(const Size& oldSize) const -{ - auto size = oldSize; - - if ((mFlags & kConfigurationChangeSize)) - size = { static_cast(mPixelWidth), static_cast(mPixelHeight) }; - - return size; -} - ConfigurationChange& ConfigurationChange::environmentValue(const std::string &name, const Object &newValue) { mFlags |= kConfigurationChangeEnvironment; @@ -119,7 +108,7 @@ ConfigurationChange::environmentValue(const std::string &name, const Object &new ObjectMap ConfigurationChange::asEventProperties(const RootConfig& rootConfig, const Metrics& metrics) const { - auto sizeChanged = mPixelHeight != metrics.getPixelHeight() || mPixelWidth != metrics.getPixelWidth(); + auto sizeChanged = hasSizeChange() && (mPixelHeight != metrics.getPixelHeight() || mPixelWidth != metrics.getPixelWidth()); auto rotated = sizeChanged && mPixelWidth == metrics.getPixelHeight() && mPixelHeight == metrics.getPixelWidth(); // Populate the event with any custom env properties from the root config. If a property diff --git a/aplcore/src/content/content.cpp b/aplcore/src/content/content.cpp index 357751c..1417bd2 100644 --- a/aplcore/src/content/content.cpp +++ b/aplcore/src/content/content.cpp @@ -19,6 +19,7 @@ #include "apl/buildTimeConstants.h" #include "apl/command/arraycommand.h" +#include "apl/content/extensionrequest.h" #include "apl/content/importrequest.h" #include "apl/content/jsondata.h" #include "apl/content/metrics.h" @@ -42,12 +43,14 @@ const char* DOCUMENT_LANGUAGE = "lang"; const char* DOCUMENT_LAYOUT_DIRECTION = "layoutDirection"; ContentPtr -Content::create(JsonData&& document) { +Content::create(JsonData&& document) +{ return create(std::move(document), makeDefaultSession()); } ContentPtr -Content::create(JsonData&& document, const SessionPtr& session) { +Content::create(JsonData&& document, const SessionPtr& session) +{ if (!document) { CONSOLE(session).log("Document parse error offset=%u: %s.", document.offset(), document.error()); return nullptr; @@ -67,11 +70,11 @@ Content::create(JsonData&& document, const SessionPtr& session) { return std::make_shared(session, ptr, it->value); } -Content::Content(SessionPtr session, - PackagePtr mainPackagePtr, +Content::Content(const SessionPtr& session, + const PackagePtr& mainPackagePtr, const rapidjson::Value& mainTemplate) - : mSession(std::move(session)), - mMainPackage(std::move(mainPackagePtr)), + : mSession(session), + mMainPackage(mainPackagePtr), mState(LOADING), mMainTemplate(mainTemplate) { @@ -106,7 +109,8 @@ Content::Content(SessionPtr session, } PackagePtr -Content::getPackage(const std::string& name) const { +Content::getPackage(const std::string& name) const +{ if (name == Path::MAIN) return mMainPackage; @@ -118,7 +122,8 @@ Content::getPackage(const std::string& name) const { } std::set -Content::getRequestedPackages() { +Content::getRequestedPackages() +{ mPending.insert(mRequested.begin(), mRequested.end()); auto result = mRequested; mRequested.clear(); @@ -126,7 +131,8 @@ Content::getRequestedPackages() { } void -Content::addPackage(const ImportRequest& request, JsonData&& raw) { +Content::addPackage(const ImportRequest& request, JsonData&& raw) +{ if (mState != LOADING) return; @@ -179,15 +185,10 @@ Content::addPackage(const ImportRequest& request, JsonData&& raw) { updateStatus(); } -void Content::addData(const std::string& name, JsonData&& raw) { - if (mState != LOADING) - return; - - if (!mPendingParameters.erase(name)) { - CONSOLE(mSession).log("Data parameter '%s' does not exist or is already assigned", - name.c_str()); +void Content::addData(const std::string& name, JsonData&& raw) +{ + if (!allowAdd(name)) return; - } // If the data is invalid, set the error state if (!raw) { @@ -197,12 +198,38 @@ void Content::addData(const std::string& name, JsonData&& raw) { return; } - mParameterValues.emplace(name, std::move(raw)); + mParameterValues.emplace(name, raw.moveToObject()); + updateStatus(); +} + +void +Content::addObjectData(const std::string& name, const Object& data) +{ + if (!allowAdd(name)) + return; + + mParameterValues.emplace(name, data); updateStatus(); } +bool +Content::allowAdd(const std::string& name) +{ + if (mState != LOADING) + return false; + + if (!mPendingParameters.erase(name)) { + CONSOLE(mSession).log("Data parameter '%s' does not exist or is already assigned", + name.c_str()); + return false; + } + + return true; +} + std::vector -Content::getLoadedPackageNames() const { +Content::getLoadedPackageNames() const +{ std::vector result; for (const auto& m : mLoaded) result.push_back(m.second->name()); @@ -210,13 +237,14 @@ Content::getLoadedPackageNames() const { } bool -Content::getMainProperties(Properties& out) const { +Content::getMainProperties(Properties& out) const +{ if (!isReady()) return false; ParameterArray params(mMainTemplate); for (const auto& m : params) - out.emplace(m.name, mParameterValues.at(m.name).get()); + out.emplace(m.name, mParameterValues.at(m.name)); if (DEBUG_CONTENT) { LOG(LogLevel::kDebug).session(mSession) << "Main Properties:"; @@ -228,7 +256,8 @@ Content::getMainProperties(Properties& out) const { } void -Content::addImportList(Package& package) { +Content::addImportList(Package& package) +{ LOG_IF(DEBUG_CONTENT).session(mSession) << "addImportList " << &package; const rapidjson::Value& value = package.json(); @@ -246,7 +275,8 @@ Content::addImportList(Package& package) { } void -Content::addImport(Package& package, const rapidjson::Value& value) { +Content::addImport(Package& package, const rapidjson::Value& value) +{ LOG_IF(DEBUG_CONTENT).session(mSession) << "addImport " << &package; if (!value.IsObject()) { @@ -273,21 +303,26 @@ Content::addImport(Package& package, const rapidjson::Value& value) { } void -Content::addExtensions(Package& package) { - +Content::addExtensions(Package& package) +{ const auto features = arrayifyProperty(package.json(), "extension", "extensions"); - for (auto it = features.begin(); it != features.end(); it++) { + for (const auto& feature : features) { std::string uri; std::string name; + bool required = false; - if (it->IsObject()) { // Check for a "uri" property and a "name" property - auto iter = it->FindMember("uri"); - if (iter != it->MemberEnd() && iter->value.IsString()) + if (feature.IsObject()) { + auto iter = feature.FindMember("uri"); + if (iter != feature.MemberEnd() && iter->value.IsString()) uri = iter->value.GetString(); - iter = it->FindMember("name"); - if (iter != it->MemberEnd() && iter->value.IsString()) + iter = feature.FindMember("name"); + if (iter != feature.MemberEnd() && iter->value.IsString()) name = iter->value.GetString(); + + iter = feature.FindMember("required"); + if (iter != feature.MemberEnd() && iter->value.IsBool()) + required = iter->value.GetBool(); } // The properties are required @@ -297,22 +332,25 @@ Content::addExtensions(Package& package) { } auto eit = std::find_if(mExtensionRequests.begin(), mExtensionRequests.end(), - [&name](const std::pair& element) {return element.first == name;}); + [&name](const ExtensionRequest& request) {return request.name == name;}); if (eit != mExtensionRequests.end()) { - if (eit->second == uri) // The same NAME->URI mapping is ignored + if (eit->uri == uri) { // The same NAME->URI mapping is ignored unless required + eit->required |= required; continue; + } CONSOLE(mSession).log("The extension name='%s' is referencing different URI values", name.c_str()); mState = ERROR; return; } else { - mExtensionRequests.emplace_back(std::pair(name, uri)); + mExtensionRequests.emplace_back(ExtensionRequest{name, uri, required}); } } } void -Content::updateStatus() { +Content::updateStatus() +{ if (mState == LOADING && mPendingParameters.empty() && mRequested.empty() && mPending.empty()) { // Content is ready if the dependency list is successfully ordered, otherwise there is an error. if (orderDependencyList()) { @@ -329,8 +367,8 @@ Content::updateStatus() { * the same Extension by multiple names, existing settings are overwritten and new settings augmented. */ void -Content::loadExtensionSettings() { - +Content::loadExtensionSettings() +{ // Settings reader per package auto sMap = std::map(); // uri -> user assigned settings @@ -338,8 +376,8 @@ Content::loadExtensionSettings() { // find settings for all requested extensions in packages for (auto& extensionRequest : mExtensionRequests) { - auto name = extensionRequest.first; - auto uri = extensionRequest.second; + auto name = extensionRequest.name; + auto uri = extensionRequest.uri; // create a map to hold the settings for the extension auto it = tmpMap.find(uri); @@ -394,7 +432,8 @@ Content::loadExtensionSettings() { Object -Content::getBackground(const Metrics& metrics, const RootConfig& config) const { +Content::getBackground(const Metrics& metrics, const RootConfig& config) const +{ const auto& json = mMainPackage->json(); auto backgroundIter = json.FindMember("background"); if (backgroundIter == json.MemberEnd()) @@ -408,7 +447,12 @@ Content::getBackground(const Metrics& metrics, const RootConfig& config) const { // Create a working context and evaluate any data-binding expression // This is a restricted context because we don't load any resources or styles - auto context = Context::createBackgroundEvaluationContext(metrics, config, theme); + auto context = Context::createBackgroundEvaluationContext( + metrics, + config, + mMainPackage->version(), + theme, + getSession()); auto object = evaluate(*context, backgroundIter->value); return asFill(*context, object); } @@ -455,13 +499,13 @@ Content::getEnvironment(const RootConfig& config) const // If the document has defined an "environment" section, we parse that auto envIter = json.FindMember(DOCUMENT_ENVIRONMENT); if (envIter != json.MemberEnd() && envIter->value.IsObject()) { - auto context = Context::createTypeEvaluationContext(config); + auto context = Context::createTypeEvaluationContext(config, mMainPackage->version(), getSession()); for (const auto& m : mEnvironmentParameters) { if (!isValidIdentifier(m)) CONSOLE(context) << "Unable to add environment parameter '" << m << "' to context. Invalid identifier."; else - context->putUserWriteable(m, mParameterValues.at(m).get()); + context->putUserWriteable(m, mParameterValues.at(m)); } // Update the language, if it is defined @@ -480,22 +524,31 @@ Content::getEnvironment(const RootConfig& config) const } const SettingsPtr -Content::getDocumentSettings() const { +Content::getDocumentSettings() const +{ const rapidjson::Value& settingsValue = Settings::findSettings(*mMainPackage); auto settings = std::make_shared(Settings(settingsValue)); return settings; } std::set -Content::getExtensionRequests() const { +Content::getExtensionRequests() const +{ std::set result; for (const auto& m : mExtensionRequests) - result.emplace(m.second); + result.emplace(m.uri); return result; } +const std::vector& +Content::getExtensionRequestsV2() const +{ + return mExtensionRequests; +} + Object -Content::getExtensionSettings(const std::string& uri) { +Content::getExtensionSettings(const std::string& uri) +{ if (!isReady()) { CONSOLE(mSession).log("Settings for extension name='%s' cannot be returned. The document is not Ready.", uri.c_str()); @@ -520,7 +573,8 @@ Content::getExtensionSettings(const std::string& uri) { * Create a deterministic order for all packages. */ bool -Content::orderDependencyList() { +Content::orderDependencyList() +{ std::set inProgress; bool isOrdered = addToDependencyList(mOrderedDependencies, inProgress, mMainPackage); if (!isOrdered) @@ -535,7 +589,8 @@ Content::orderDependencyList() { bool Content::addToDependencyList(std::vector& ordered, std::set& inProgress, - const PackagePtr& package) { + const PackagePtr& package) +{ LOG_IF(DEBUG_CONTENT).session(mSession) << "addToDependencyList " << package << " dependency count=" << package->getDependencies().size(); diff --git a/aplcore/src/content/jsondata.cpp b/aplcore/src/content/jsondata.cpp index cdb562d..1145631 100644 --- a/aplcore/src/content/jsondata.cpp +++ b/aplcore/src/content/jsondata.cpp @@ -16,6 +16,8 @@ #include "rapidjson/writer.h" #include "rapidjson/stringbuffer.h" +#include "apl/primitives/object.h" + namespace apl { std::string @@ -37,4 +39,19 @@ JsonData::toString() const return buffer.GetString(); } +Object +JsonData::moveToObject() +{ + if (mType == kValue) { + mType = kNullPtr; + Object obj(*mValuePtr); + mValuePtr = nullptr; + return obj; + } + else { + mType = kNullPtr; + return {std::move(mDocument) }; + } +} + } // namespace apl \ No newline at end of file diff --git a/aplcore/src/content/metrics.cpp b/aplcore/src/content/metrics.cpp index bee188b..cce3064 100644 --- a/aplcore/src/content/metrics.cpp +++ b/aplcore/src/content/metrics.cpp @@ -31,13 +31,15 @@ Bimap sViewportModeBimap = { }; std::string Metrics::toDebugString() const { - return "Metrics<" - "theme=" + mTheme + ", " - "size=" + std::to_string(static_cast(mPixelWidth)) + "x" - + std::to_string(static_cast(mPixelHeight)) + ", " - "dpi=" + std::to_string(static_cast(mDpi)) + ", " - "shape=" + (mShape == ROUND ? "round" : "rectangle") + ", " - "mode=" + sViewportModeBimap.at(mMode) + ">"; + return "Metrics<" + "theme=" + mTheme + ", " + "size=" + std::to_string(static_cast(mPixelWidth)) + "x" + + std::to_string(static_cast(mPixelHeight)) + ", " + "autoSizeWidth=" + (mAutoSizeWidth ? "true ": "false ") + + "autoSizeHeight=" + (mAutoSizeHeight ? "true ": "false ") + + "dpi=" + std::to_string(static_cast(mDpi)) + ", " + "shape=" + sScreenShapeBimap.at(mShape) + ", " + "mode=" + sViewportModeBimap.at(mMode) + ">"; } } \ No newline at end of file diff --git a/aplcore/src/content/package.cpp b/aplcore/src/content/package.cpp index dc569e4..5136d66 100644 --- a/aplcore/src/content/package.cpp +++ b/aplcore/src/content/package.cpp @@ -51,18 +51,18 @@ Package::create(const SessionPtr& session, const std::string& name, JsonData&& j return std::make_shared(name, std::move(json)); } -const std::string +std::string Package::version() { auto it_version = mJson.get().FindMember(DOCUMENT_VERSION); - return std::string(it_version->value.GetString()); + return { it_version->value.GetString() }; } -const std::string +std::string Package::type() { auto it_type = mJson.get().FindMember(DOCUMENT_TYPE); - return std::string(it_type->value.GetString()); + return { it_type->value.GetString() }; } } // namespace apl \ No newline at end of file diff --git a/aplcore/src/content/rootconfig.cpp b/aplcore/src/content/rootconfig.cpp index ab7d240..3f6b601 100644 --- a/aplcore/src/content/rootconfig.cpp +++ b/aplcore/src/content/rootconfig.cpp @@ -17,6 +17,7 @@ #include #include +#include #include "apl/animation/coreeasing.h" #include "apl/component/textmeasurement.h" @@ -28,6 +29,33 @@ #include "apl/media/mediaplayerfactory.h" #include "apl/time/coretimemanager.h" #include "apl/utils/corelocalemethods.h" +#include "apl/utils/session.h" + +namespace { + +using namespace apl; + +const std::vector sCopyableConfigProperties = { + RootProperty::kAgentName, + RootProperty::kAgentVersion, + RootProperty::kAllowOpenUrl, + RootProperty::kFontScale, + RootProperty::kScreenMode, + RootProperty::kScreenReader, + RootProperty::kUTCTime, + RootProperty::kInitialDisplayState, + RootProperty::kLocalTimeAdjustment, + RootProperty::kAnimationQuality, + RootProperty::kReportedVersion, + RootProperty::kDoublePressTimeout, + RootProperty::kLongPressTimeout, + RootProperty::kMinimumFlingVelocity, + RootProperty::kPressedDuration, + RootProperty::kTapOrScrollTimeout, + RootProperty::kMaximumTapVelocity +}; + +} // unnamed namespace namespace apl { @@ -40,41 +68,6 @@ inline Object asSlope(const Context& context, const Object& object) { return angleToSlope(object.getDouble()); } -static bool isAllowedEnvironmentName(const std::string &name) { - static std::set sReserved; - if (sReserved.empty()) { - // Don't allow custom env properties to shadow synthesized configuration change event props - for (const auto &synthesizedName : ConfigurationChange::getSynthesizedPropertyNames()) { - sReserved.emplace(synthesizedName); - } - - // Check the name against a clean envaluation context - auto context = Context::createTypeEvaluationContext(RootConfig()); - assert(context); - - // Don't allow custom env properties to shadow top-level names (e.g. "environment") - for (const auto &entry : *context) { - sReserved.emplace(entry.first); - } - - // Don't allow custom env properties to shadow built-in environment properties - auto env = context->opt("environment"); - assert(env.isMap()); - for (const auto &entry : env.getMap()) { - sReserved.emplace(entry.first); - } - - // Don't allow custom env properties to shadow built-in viewport properties - auto viewport = context->opt("viewport"); - assert(viewport.isMap()); - for (const auto &entry : viewport.getMap()) { - sReserved.emplace(entry.first); - } - } - - return sReserved.find(name) == sReserved.end(); -} - Bimap sScreenModeBimap = { { RootConfig::kScreenModeNormal, "normal" }, { RootConfig::kScreenModeHighContrast, "high-contrast" } @@ -105,6 +98,7 @@ RootConfig::RootConfig() mLocaleMethods(std::static_pointer_cast(std::make_shared())), mDefaultComponentSize({ // Set default sizes for components that aren't "auto" width and "auto" height. + {{kComponentTypeHost, true}, {Dimension(100), Dimension(100)}}, {{kComponentTypeImage, true}, {Dimension(100), Dimension(100)}}, {{kComponentTypePager, true}, {Dimension(100), Dimension(100)}}, {{kComponentTypeScrollView, true}, {Dimension(), Dimension(100)}}, @@ -120,7 +114,12 @@ RootConfig::RootConfig() const auto& pd = cpd.second; mProperties.set(pd.key, pd.defvalue); } - mContext = Context::createTypeEvaluationContext(*this); + // Separate session as RootConfig owned by viewhost and errors in it should not be exposed to + // the skill. + mConfigSession = makeDefaultSession(); + mContext = Context::createTypeEvaluationContext(*this, + APLVersion::getDefaultReportedVersionString(), + mConfigSession); } const Context& @@ -194,11 +193,47 @@ RootConfig::propDefSet() const return sRootProperties; } +bool +RootConfig::isAllowedEnvironmentName(const std::string &name) const +{ + static std::set sReserved; + if (sReserved.empty()) { + // Don't allow custom env properties to shadow synthesized configuration change event props + for (const auto &synthesizedName : ConfigurationChange::getSynthesizedPropertyNames()) { + sReserved.emplace(synthesizedName); + } + + // Check the name against a clean evaluation context + auto context = Context::createTypeEvaluationContext(RootConfig(), + APLVersion::getDefaultReportedVersionString(), + mConfigSession); + assert(context); + + // Don't allow custom env properties to shadow top-level names (e.g. "environment") + for (const auto &entry : *context) { + sReserved.emplace(entry.first); + } + + // Don't allow custom env properties to shadow built-in environment properties + auto env = context->opt("environment"); + assert(env.isMap()); + for (const auto &entry : env.getMap()) { + sReserved.emplace(entry.first); + } + + // Don't allow custom env properties to shadow built-in viewport properties + auto viewport = context->opt("viewport"); + assert(viewport.isMap()); + for (const auto &entry : viewport.getMap()) { + sReserved.emplace(entry.first); + } + } + + return sReserved.find(name) == sReserved.end(); +} + RootConfig& RootConfig::session(const SessionPtr& session) { - mSession = session; - // Recreate dummy context to use new session. - mContext = Context::createTypeEvaluationContext(*this); return *this; } @@ -210,7 +245,7 @@ RootConfig::set(const std::string& name, const Object& object) auto propertyKey = static_cast(it->second); return set(propertyKey, object); } else { - LOG(LogLevel::kInfo).session(mSession) << "Unable to find property " << name; + LOG(LogLevel::kInfo).session(mConfigSession) << "Unable to find property " << name; } return *this; @@ -269,7 +304,7 @@ RootConfig::setEnvironmentValue(const std::string& name, const Object& value) { if (isAllowedEnvironmentName(name)) { mEnvironmentValues[name] = value; } else { - LOG(LogLevel::kWarn).session(mSession) << "Ignoring attempt to set environment value: " << name; + LOG(LogLevel::kWarn).session(mConfigSession) << "Ignoring attempt to set environment value: " << name; } return *this; } @@ -282,4 +317,21 @@ RootConfig::getSwipeAwayAnimationEasing() const { return getProperty(RootProperty::kSwipeAwayAnimationEasing).get(); } +RootConfigPtr +RootConfig::copy() const +{ + auto copy = std::make_shared(); + copy->timeManager(getTimeManager()); + copy->audioPlayerFactory(getAudioPlayerFactory()); + copy->documentManager(getDocumentManager()); + copy->mediaPlayerFactory(getMediaPlayerFactory()); + copy->measure(getMeasure()); + + for (auto key : sCopyableConfigProperties) { + copy->set(key, getProperty(key)); + } + + return copy; +} + } // namespace apl diff --git a/aplcore/src/content/viewport.cpp b/aplcore/src/content/viewport.cpp index c203f25..fb958ce 100644 --- a/aplcore/src/content/viewport.cpp +++ b/aplcore/src/content/viewport.cpp @@ -24,6 +24,8 @@ Object makeViewport( const Metrics& metrics, const std::string& theme ) { auto viewport = std::make_shared(); viewport->emplace("width", metrics.getWidth()); viewport->emplace("height", metrics.getHeight()); + viewport->emplace("autoWidth", metrics.getAutoWidth()); + viewport->emplace("autoHeight", metrics.getAutoHeight()); viewport->emplace("dpi", metrics.getDpi()); viewport->emplace("shape", metrics.getShape()); viewport->emplace("pixelWidth", metrics.getPixelWidth()); diff --git a/aplcore/src/datagrammar/CMakeLists.txt b/aplcore/src/datagrammar/CMakeLists.txt index 980eb8c..3de1017 100644 --- a/aplcore/src/datagrammar/CMakeLists.txt +++ b/aplcore/src/datagrammar/CMakeLists.txt @@ -13,7 +13,6 @@ target_sources_local(apl PRIVATE - boundsymbol.cpp bytecode.cpp bytecodeassembler.cpp bytecodeevaluator.cpp diff --git a/aplcore/src/datagrammar/bytecode.cpp b/aplcore/src/datagrammar/bytecode.cpp index d339010..f7ae5a9 100644 --- a/aplcore/src/datagrammar/bytecode.cpp +++ b/aplcore/src/datagrammar/bytecode.cpp @@ -14,10 +14,10 @@ */ #include "apl/datagrammar/bytecode.h" -#include "apl/datagrammar/bytecodeoptimizer.h" #include "apl/datagrammar/bytecodeevaluator.h" -#include "apl/datagrammar/boundsymbol.h" +#include "apl/datagrammar/bytecodeoptimizer.h" #include "apl/engine/context.h" +#include "apl/primitives/boundsymbol.h" #include "apl/utils/session.h" namespace apl { @@ -25,12 +25,20 @@ namespace datagrammar { Object ByteCode::eval() const +{ + return ByteCode::evaluate(nullptr, 0); +} + +Object +ByteCode::evaluate(BoundSymbolSet* symbols, int depth) const { // Check for a trivial instruction if (mInstructions.size() == 1) { const auto& cmd = mInstructions.at(0); switch (cmd.type) { case BC_OPCODE_LOAD_BOUND_SYMBOL: + if (symbols != nullptr) + symbols->emplace(mData.at(cmd.value).get()); return mData.at(cmd.value).eval(); case BC_OPCODE_LOAD_DATA: @@ -48,9 +56,8 @@ ByteCode::eval() const } } - ByteCodeEvaluator evaluator(*this); + ByteCodeEvaluator evaluator(*this, symbols, depth); evaluator.advance(); - if (evaluator.isDone()) return evaluator.getResult(); @@ -58,100 +65,19 @@ ByteCode::eval() const return Object::NULL_OBJECT(); } -Object -ByteCode::simplify() +ContextPtr +ByteCode::getContext() const { - ByteCodeEvaluator evaluator(*this); - evaluator.advance(); - if (evaluator.isDone() && evaluator.isConstant()) - return evaluator.getResult(); - - return shared_from_this(); + return mContext.lock(); } void -ByteCode::symbols(SymbolReferenceMap& symbols) +ByteCode::optimize() { - auto context = mContext.lock(); - if (!context) - return; - if (!mOptimized) { ByteCodeOptimizer::optimize(*this); mOptimized = true; } - - // Find all symbol references by searching the opcodes. - SymbolReference ref; - Object operand; - - for (auto &cmd : mInstructions) { - switch (cmd.type) { - case BC_OPCODE_LOAD_DATA: - operand = mData[cmd.value].asString(); - break; - - case BC_OPCODE_LOAD_IMMEDIATE: - operand = cmd.value; - break; - - case BC_OPCODE_LOAD_BOUND_SYMBOL: - // Store the old symbol - if (!ref.first.empty()) - symbols.emplace(ref); - - ref = mData[cmd.value].get()->getSymbol(); - operand = Object::NULL_OBJECT(); - break; - - case BC_OPCODE_ATTRIBUTE_ACCESS: - if (!ref.first.empty()) - ref.first += mData[cmd.value].asString() + "/"; - operand = Object::NULL_OBJECT(); - break; - - case BC_OPCODE_ARRAY_ACCESS: - if (!ref.first.empty()) { - if (operand.isString() || operand.isNumber()) - ref.first += operand.asString() + "/"; - else { - symbols.emplace(ref); - ref.first.clear(); - } - } - operand = Object::NULL_OBJECT(); - break; - - default: - if (!ref.first.empty()) { - symbols.emplace(ref); - ref.first.clear(); - } - operand = Object::NULL_OBJECT(); - break; - } - } - - if (!ref.first.empty()) - symbols.emplace(ref); -} - -ContextPtr -ByteCode::getContext() const -{ - return mContext.lock(); -} - -void -ByteCode::dump() const -{ - LOG(LogLevel::kDebug).session(getContext()) << "Data"; - for (int i = 0; i < mData.size(); i++) - LOG(LogLevel::kDebug).session(getContext()) << " [" << i << "] " << mData.at(i).toDebugString(); - - LOG(LogLevel::kDebug).session(getContext()) << "Instructions"; - for (int pc = 0; pc < mInstructions.size(); pc++) - LOG(LogLevel::kDebug).session(getContext()) << instructionAsString(pc); } @@ -182,6 +108,7 @@ static const char *BYTE_CODE_COMMAND_STRING[] = { "MERGE_AS_STRING ", "APPEND_ARRAY ", "APPEND_MAP ", + "EVALUATE ", }; // This must match the enumerated ByteCodeComparison values @@ -213,6 +140,24 @@ lineNumber(int i, int num) return std::string(offset, ' ') + result; } +/** + * Convert a generic Object into something that looks good in disassembly + * + * Null -> null + * Boolean -> true / false + * Number -> normal conversion + * String -> 'VALUE' + */ +static std::string +prettyPrint(const Object& object) +{ + if (object.isNull()) return "null"; + if (object.isString()) return "'" + object.asString() + "'"; + if (object.isMap()) return "BuiltInMap<>"; + if (object.isArray()) return "BuiltInArray<>"; + if (object.is()) return object.toDebugString(); + return object.asString(); +} std::string ByteCode::instructionAsString(int pc) const @@ -230,10 +175,13 @@ ByteCode::instructionAsString(int pc) const case BC_OPCODE_LOAD_CONSTANT: result += std::string(" ") + BYTE_CODE_CONSTANT_STRING[cmd.value]; break; + case BC_OPCODE_LOAD_IMMEDIATE: + result += std::string(" ") + std::to_string(cmd.value); + break; case BC_OPCODE_LOAD_DATA: case BC_OPCODE_ATTRIBUTE_ACCESS: case BC_OPCODE_LOAD_BOUND_SYMBOL: - result += " [" + mData.at(cmd.value).toDebugString() + "]"; + result += " [" + prettyPrint(mData.at(cmd.value)) + "]"; break; case BC_OPCODE_COMPARE_OP: result += std::string(" ") + BYTE_CODE_COMPARE_STRING[cmd.value]; @@ -259,6 +207,63 @@ ByteCodeInstruction::toString() const return std::string(BYTE_CODE_COMMAND_STRING[type]) + " (" + std::to_string(value) + ")"; } +/************* Disassembly routines **************/ + +Disassembly::Iterator::Iterator(const ByteCode& byteCode, size_t lineNumber) + : mByteCode(byteCode), mLineNumber(lineNumber) +{} + +Disassembly::Iterator::reference +Disassembly::Iterator::operator*() const +{ + if (mLineNumber == 0) // Line 0 is the "Data" label + return "DATA"; + const auto count = mByteCode.dataCount() + 1; // Line 1-n are the data items + if (mLineNumber < count) { + auto index = mLineNumber - 1; + return lineNumber(index, 6) + " " + prettyPrint(mByteCode.dataAt(index)); + } + if (mLineNumber == count) + return "INSTRUCTIONS"; + auto pc = mLineNumber - count - 1; + if (pc < mByteCode.instructionCount()) + return mByteCode.instructionAsString(pc); + + return ""; // Should never reach this line +} + +Disassembly::Iterator& +Disassembly::Iterator::operator++() +{ + mLineNumber++; + return *this; +} + +bool +operator== (const Disassembly::Iterator& lhs, const Disassembly::Iterator& rhs) +{ + return &lhs.mByteCode == &rhs.mByteCode && lhs.mLineNumber == rhs.mLineNumber; +} + +bool +operator!= (const Disassembly::Iterator& lhs, const Disassembly::Iterator& rhs) +{ + return &lhs.mByteCode != &rhs.mByteCode || lhs.mLineNumber != rhs.mLineNumber; +} + +Disassembly::Iterator +Disassembly::begin() const +{ + return {mByteCode, 0}; +} + +Disassembly::Iterator +Disassembly::end() const +{ + return {mByteCode, 2 + mByteCode.instructionCount() + mByteCode.dataCount()}; +} + + } // namespace datagrammar } // namespace apl \ No newline at end of file diff --git a/aplcore/src/datagrammar/bytecodeassembler.cpp b/aplcore/src/datagrammar/bytecodeassembler.cpp index 55c3967..c9ad798 100644 --- a/aplcore/src/datagrammar/bytecodeassembler.cpp +++ b/aplcore/src/datagrammar/bytecodeassembler.cpp @@ -15,12 +15,13 @@ #include -#include "apl/datagrammar/boundsymbol.h" -#include "apl/datagrammar/bytecodeassembler.h" #include "apl/datagrammar/bytecode.h" +#include "apl/datagrammar/bytecodeassembler.h" #include "apl/datagrammar/bytecodeevaluator.h" -#include "apl/datagrammar/databindingrules.h" #include "apl/datagrammar/databindingerrors.h" +#include "apl/datagrammar/databindingrules.h" + +#include "apl/primitives/boundsymbol.h" #include "apl/engine/context.h" #include "apl/utils/log.h" @@ -67,13 +68,17 @@ Object ByteCodeAssembler::parse(const Context& context, const std::string& value) { // Short-circuit the parser if there are no embedded expressions - if (value.find("${") == std::string::npos) + if (value.find("${") == std::string::npos && value.find("#{") == std::string::npos) return value; pegtl::string_input<> in(value, ""); datagrammar::ByteCodeAssembler assembler(context); fail_state failState; + // Only support #{...} evaluation in version 2023.2 and higher. + if (context.getRequestedAPLVersion().compare("2023.2") >= 0) + assembler.mCanDeferAndEval = true; + if (!pegtl::parse(in, failState, assembler) || failState.failed) { if (failState.failed) { const auto p = failState.positions().front(); @@ -147,7 +152,7 @@ ByteCodeAssembler::loadGlobal(const std::string& name) } // Mutable globals have a bound symbol - mDataRef->emplace_back(Object(std::make_shared(cr.context(), name))); + mDataRef->emplace_back(BoundSymbol(cr.context(), name)); mInstructionRef->emplace_back(ByteCodeInstruction{BC_OPCODE_LOAD_BOUND_SYMBOL, len}); } @@ -438,18 +443,33 @@ ByteCodeAssembler::popGroup() } void -ByteCodeAssembler::pushDBGroup() +ByteCodeAssembler::evaluate() { - mOperatorsRef->emplace_back(Operator{BC_ORDER_DB, BC_OPCODE_NOP, 0}); + mInstructionRef->emplace_back(ByteCodeInstruction{BC_OPCODE_EVALUATE, 0}); } void +ByteCodeAssembler::pushDBGroup(bool defer) +{ + if (defer || mDeferredDepth > 0) + mDeferredDepth++; + else + mOperatorsRef->emplace_back(Operator{BC_ORDER_DB, BC_OPCODE_NOP, 0}); +} + +bool ByteCodeAssembler::popDBGroup() { + if (mDeferredDepth > 0) { + mDeferredDepth--; + return mDeferredDepth == 0; // True if we've popped the last defer + } + assert(!mOperatorsRef->empty()); assert(mOperatorsRef->back().order == BC_ORDER_DB); mOperatorsRef->pop_back(); mOperatorsRef->push_back(Operator{BC_ORDER_STRING_ELEMENT, BC_OPCODE_NOP, 0}); + return false; } void diff --git a/aplcore/src/datagrammar/bytecodeevaluator.cpp b/aplcore/src/datagrammar/bytecodeevaluator.cpp index c363dfe..6693f27 100644 --- a/aplcore/src/datagrammar/bytecodeevaluator.cpp +++ b/aplcore/src/datagrammar/bytecodeevaluator.cpp @@ -15,9 +15,10 @@ #include "apl/datagrammar/bytecodeevaluator.h" -#include "apl/datagrammar/boundsymbol.h" +#include "apl/buildTimeConstants.h" + #include "apl/engine/context.h" -#include "apl/datagrammar/functions.h" +#include "apl/engine/evaluate.h" #include "apl/utils/session.h" namespace apl { @@ -25,8 +26,10 @@ namespace datagrammar { static const bool DEBUG_BYTE_CODE = false; -ByteCodeEvaluator::ByteCodeEvaluator(const ByteCode& byteCode) - : mByteCode(byteCode) +ByteCodeEvaluator::ByteCodeEvaluator(const ByteCode& byteCode, BoundSymbolSet *symbols, int depth) + : mByteCode(byteCode), + mSymbols(symbols), + mEvaluationDepth(depth) { } @@ -71,8 +74,6 @@ ByteCodeEvaluator::advance() args[--argCount] = pop(); auto f = pop(); if (f.isCallable()) { // Normal function or Easing function - if (!f.isPure()) - mIsConstant = false; mStack.emplace_back(f.call(args)); } else { CONSOLE(mByteCode.getContext()) << "Invalid function pc=" << mProgramCounter; @@ -94,8 +95,10 @@ ByteCodeEvaluator::advance() break; case BC_OPCODE_LOAD_BOUND_SYMBOL: + assert(data.at(cmd.value).is()); mStack.emplace_back(data.at(cmd.value).eval()); - mIsConstant = false; + if (mSymbols != nullptr) + mSymbols->emplace(data.at(cmd.value).get()); break; case BC_OPCODE_ATTRIBUTE_ACCESS: @@ -225,6 +228,20 @@ ByteCodeEvaluator::advance() mStack.emplace_back(std::move(a)); } break; + + case BC_OPCODE_EVALUATE: { + auto result = pop(); + auto context = mByteCode.getContext(); + if (context) { + if (mEvaluationDepth >= kEvaluationDepthLimit) + CONSOLE(context) + << "Evaluation depth limit (" << kEvaluationDepthLimit << ") exceeded"; + else + result = evaluateInternal(*context, result, mSymbols, mEvaluationDepth + 1); + } + mStack.emplace_back(result); + } + break; } } diff --git a/aplcore/src/datagrammar/bytecodeoptimizer.cpp b/aplcore/src/datagrammar/bytecodeoptimizer.cpp index 02cc397..b000da6 100644 --- a/aplcore/src/datagrammar/bytecodeoptimizer.cpp +++ b/aplcore/src/datagrammar/bytecodeoptimizer.cpp @@ -16,14 +16,16 @@ #include "apl/datagrammar/bytecodeoptimizer.h" #include "apl/datagrammar/bytecode.h" #include "apl/datagrammar/functions.h" -#include "apl/datagrammar/boundsymbol.h" #include "apl/engine/context.h" +#include "apl/primitives/boundsymbol.h" #include "apl/primitives/functions.h" #include "apl/utils/log.h" namespace apl { namespace datagrammar { +static const bool DEBUG_OPTIMIZER = false; + struct BasicBlock { int entry; // The starting point of the basic block after code reduction int count; // The number of commands in the basic block after code reduction @@ -84,9 +86,9 @@ ByteCodeOptimizer::simplifyOperands() // If the value is already in an operand, use that one auto it = std::find(operands.begin(), operands.end(), object); if (it != operands.end()) - cmd.value = std::distance(operands.begin(), it); + cmd.value = static_cast(std::distance(operands.begin(), it)); else { - cmd.value = operands.size(); + cmd.value = static_cast(operands.size()); operands.emplace_back(std::move(object)); } } @@ -102,8 +104,6 @@ ByteCodeOptimizer::simplifyOperands() using UnaryFunction = Object (*)(const Object&); using BinaryFunction = Object (*)(const Object&, const Object&); -static const bool DEBUG_OPTIMIZER = false; - /** * Peephole optimization * @@ -396,6 +396,10 @@ ByteCodeOptimizer::simplifyOperations() output.emplace_back(cmd); out_constants = 0; break; + case BC_OPCODE_EVALUATE: + output.emplace_back(cmd); + out_constants = 0; + break; } } diff --git a/aplcore/src/datagrammar/functions.cpp b/aplcore/src/datagrammar/functions.cpp index 541c204..8af7db3 100644 --- a/aplcore/src/datagrammar/functions.cpp +++ b/aplcore/src/datagrammar/functions.cpp @@ -17,8 +17,8 @@ #include -#include "apl/datagrammar/boundsymbol.h" #include "apl/engine/context.h" +#include "apl/primitives/boundsymbol.h" #include "apl/primitives/color.h" #include "apl/primitives/dimension.h" #include "apl/utils/log.h" diff --git a/aplcore/src/datasource/datasource.cpp b/aplcore/src/datasource/datasource.cpp index 12c83e4..6b06548 100644 --- a/aplcore/src/datasource/datasource.cpp +++ b/aplcore/src/datasource/datasource.cpp @@ -38,41 +38,24 @@ DataSource::toDebugString() const } Object -DataSource::create(const ContextPtr& context, const Object& object, const std::string& name) +DataSource::create(const ContextPtr& context, const DataSourceProviderPtr& provider, + const Object& object, const std::string& name) { - if (object.getLiveDataObject()) - return object; - - if (!object.isMap()) - return Object::NULL_OBJECT(); - - std::string type = propertyAsString(*context, object, "type"); - if (type.empty()) { - CONSOLE(context) << "Unrecognized type field in DataSource"; - return Object::NULL_OBJECT(); - } - - auto dataSourceProvider = context->getRootConfig().getDataSourceProvider(type); - if(!dataSourceProvider) { - CONSOLE(context) << "Unrecognized DataSource type"; - return Object::NULL_OBJECT(); - } - // If no items specified (even empty ones) - propertyAsRecursive + arrayify() will give us [NULL] which is an item. // Check for this condition and go with empty LiveArray instead to avoid creating empty one. auto items = propertyAsRecursive(*context, object, "items"); auto liveDataSourceArray = items.isNull() ? LiveArray::create() : LiveArray::create(arrayify(*context, items)); - auto dataSourceConnection = dataSourceProvider->create(object, context, liveDataSourceArray); + auto dataSourceConnection = provider->create(object, context, liveDataSourceArray); if (!dataSourceConnection) { CONSOLE(context) << "DataSourceConnection failed to initialize."; return Object::NULL_OBJECT(); } liveDataSourceArray = dataSourceConnection->getLiveArray(); auto dataSource = std::make_shared( - liveDataSourceArray, - context, - dataSourceConnection, - name); + liveDataSourceArray, + context, + dataSourceConnection, + name); context->dataManager().add(dataSource); // If provided with empty initial array - ask for items straight away. @@ -80,7 +63,7 @@ DataSource::create(const ContextPtr& context, const Object& object, const std::s dataSource->ensure(0); } - return Object(dataSource); + return {dataSource}; } DataSource::DataSource( diff --git a/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp b/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp index 64664d4..3935c49 100644 --- a/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp @@ -69,7 +69,7 @@ DynamicIndexListDataSourceConnection::processLazyLoad(int index, const Object& d if (!context) return false; - auto items = evaluateRecursive(*context, data); + auto items = evaluateNested(*context, data); bool result = false; bool outOfRange = false; @@ -94,12 +94,12 @@ DynamicIndexListDataSourceConnection::processLazyLoad(int index, const Object& d size_t idx = index - mMinimumInclusiveIndex; if (overlaps(idx, dataArray.size())) { - constructAndReportError(context->session(), ERROR_REASON_OCCUPIED_LIST_INDEX, index, + constructAndReportError(context, ERROR_REASON_OCCUPIED_LIST_INDEX, index, "Load range overlaps existing items. New items for existing range discarded."); } result = update(idx, dataArray, false); } else { - constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_ITEMS, index, + constructAndReportError(context, ERROR_REASON_MISSING_LIST_ITEMS, index, "No items provided to load."); retryFetchRequest(correlationToken.asString()); return result; @@ -109,7 +109,7 @@ DynamicIndexListDataSourceConnection::processLazyLoad(int index, const Object& d clearTimeouts(context, correlationToken.asString()); if (!result || outOfRange) { - constructAndReportError(context->session(), ERROR_REASON_LOAD_INDEX_OUT_OF_RANGE, index, + constructAndReportError(context, ERROR_REASON_LOAD_INDEX_OUT_OF_RANGE, index, "Requested index out of bounds."); } return result; @@ -124,13 +124,13 @@ DynamicIndexListDataSourceConnection::processUpdate(DynamicIndexListUpdateType t } if (index < mMinimumInclusiveIndex) { - constructAndReportError(context->session(), ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, + constructAndReportError(context, ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, "Requested index out of bounds."); return false; } size_t idx = index - mMinimumInclusiveIndex; - auto items = evaluateRecursive(*context, data); + auto items = evaluateNested(*context, data); bool result = false; @@ -160,7 +160,7 @@ DynamicIndexListDataSourceConnection::processUpdate(DynamicIndexListUpdateType t } if (!items.isArray()) { - constructAndReportError(context->session(), ERROR_REASON_INTERNAL_ERROR, index, + constructAndReportError(context, ERROR_REASON_INTERNAL_ERROR, index, "No array provided for range insert."); return false; } @@ -182,7 +182,7 @@ DynamicIndexListDataSourceConnection::processUpdate(DynamicIndexListUpdateType t break; } if (!result) { - constructAndReportError(context->session(), ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, + constructAndReportError(context, ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, "Requested index out of bounds."); } return result; @@ -258,7 +258,7 @@ DynamicIndexListDataSourceProvider::createConnection( auto ctx = context.lock(); if (!ctx) return nullptr; if (!sourceDefinition.has(START_INDEX) || !sourceDefinition.get(START_INDEX).isNumber()) { - constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); + constructAndReportError(ctx, ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); return nullptr; } @@ -273,7 +273,7 @@ DynamicIndexListDataSourceProvider::createConnection( // * As an exception we allow for all of properties to be equal for proactive loading case. if (!(minimumInclusiveIndex == startIndex && maximumExclusiveIndex == startIndex) && (minimumInclusiveIndex > startIndex || maximumExclusiveIndex < startIndex)) { - constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, listId, "DataSource bounds configuration is wrong."); + constructAndReportError(ctx, ERROR_REASON_INTERNAL_ERROR, listId, "DataSource bounds configuration is wrong."); return nullptr; } @@ -311,18 +311,18 @@ DynamicIndexListDataSourceProvider::processLazyLoadInternal( if (!ctx) return false; if(connection->updateBounds(minimumInclusiveIndex, maximumExclusiveIndex)) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_INCONSISTENT_RANGE, connection, startIndex, + constructAndReportError(connection->getContext(), ERROR_REASON_INCONSISTENT_RANGE, connection, startIndex, "Bounds were changed in runtime."); } if (!responseMap.has(ITEMS)) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_MISSING_LIST_ITEMS, connection, + constructAndReportError(connection->getContext(), ERROR_REASON_MISSING_LIST_ITEMS, connection, Object::NULL_OBJECT(), "No items defined."); return true; } if (!connection->changesAllowed()) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, connection, + constructAndReportError(connection->getContext(), ERROR_REASON_INTERNAL_ERROR, connection, Object::NULL_OBJECT(), "Payload has unexpected fields."); return true; } @@ -335,7 +335,7 @@ bool DynamicIndexListDataSourceProvider::processUpdateInternal( const DILConnectionPtr& connection, const Object& responseMap) { if (connection->isLazyLoadingOnly()) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, + constructAndReportError(connection->getContext(), ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, connection, Object::NULL_OBJECT(), "List supports only lazy loading."); connection->setFailed(); return false; @@ -347,7 +347,7 @@ DynamicIndexListDataSourceProvider::processUpdateInternal( for (const auto& operation : operations) { if (!operation.has(UPDATE_TYPE) || !operation.get(UPDATE_TYPE).isString() || !operation.has(UPDATE_INDEX) || !operation.get(UPDATE_INDEX).isNumber()) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_INVALID_OPERATION, connection, + constructAndReportError(connection->getContext(), ERROR_REASON_INVALID_OPERATION, connection, Object::NULL_OBJECT(), "Operation malformed."); result = false; break; @@ -355,7 +355,7 @@ DynamicIndexListDataSourceProvider::processUpdateInternal( auto typeName = operation.get(UPDATE_TYPE).asString(); if (!sDatasourceUpdateType.count(typeName)) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_INVALID_OPERATION, connection, + constructAndReportError(connection->getContext(), ERROR_REASON_INVALID_OPERATION, connection, Object::NULL_OBJECT(), "Wrong update type."); result = false; break; @@ -401,7 +401,7 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { return false; if(connection->inFailState()) { - constructAndReportError(context->session(), ERROR_REASON_INTERNAL_ERROR, listId, "List in fail state."); + constructAndReportError(context, ERROR_REASON_INTERNAL_ERROR, listId, "List in fail state."); return false; } @@ -415,7 +415,7 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { } else if (responseMap.has(OPERATIONS) && responseMap.get(OPERATIONS).isArray()) { isLazyLoading = false; } else { - constructAndReportError(context->session(), ERROR_REASON_INTERNAL_ERROR, connection, + constructAndReportError(context, ERROR_REASON_INTERNAL_ERROR, connection, Object::NULL_OBJECT(), "Payload missing required fields."); return false; } @@ -427,11 +427,11 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { if (listVersion > currentListVersion + 1) { connection->putCacheUpdate(listVersion - 1, responseMap); } else if (listVersion < 0) { - constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, + constructAndReportError(context, ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, connection, Object::NULL_OBJECT(), "Missing list version."); connection->setFailed(); } else { - constructAndReportError(context->session(), ERROR_REASON_DUPLICATE_LIST_VERSION, connection, + constructAndReportError(context, ERROR_REASON_DUPLICATE_LIST_VERSION, connection, Object::NULL_OBJECT(), "Duplicate list version."); } diff --git a/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp b/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp index 85c8a01..d26c7eb 100644 --- a/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp @@ -106,7 +106,7 @@ DynamicListDataSourceConnection::scheduleTimeout(const std::string& correlationT return; if (self->retryFetchRequest(correlationToken)) { - self->constructAndReportError(ctx->session(), ERROR_REASON_LOAD_TIMEOUT, Object::NULL_OBJECT(), + self->constructAndReportError(ctx, ERROR_REASON_LOAD_TIMEOUT, Object::NULL_OBJECT(), "Retrying timed out request: " + correlationToken); } }, mConfiguration.fetchTimeout); @@ -198,7 +198,7 @@ DynamicListDataSourceConnection::reportUpdateExpired(int version) { auto it = mUpdatesCache.find(version); if (it != mUpdatesCache.end()) { context->getRootConfig().getTimeManager()->clearTimeout(it->second.expiryTimeout); - constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), + constructAndReportError(context, ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), "Update to version " + std::to_string(version + 1) + " buffered longer than expected."); } } @@ -216,7 +216,7 @@ DynamicListDataSourceConnection::putCacheUpdate(int version, const Object& paylo if (mUpdatesCache.size() >= mConfiguration.listUpdateBufferSize) { // Remove highest or discard current one if it's one. - constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), + constructAndReportError(context, ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), "Too many updates buffered. Discarding highest version."); auto it = mUpdatesCache.rbegin(); if (it->first > version) { @@ -231,7 +231,7 @@ DynamicListDataSourceConnection::putCacheUpdate(int version, const Object& paylo Update update = {payload, timeoutId}; mUpdatesCache.emplace(version, update); } else { - constructAndReportError(context->session(), ERROR_REASON_DUPLICATE_LIST_VERSION, Object::NULL_OBJECT(), + constructAndReportError(context, ERROR_REASON_DUPLICATE_LIST_VERSION, Object::NULL_OBJECT(), "Trying to cache existing list version."); } } @@ -255,7 +255,7 @@ DynamicListDataSourceConnection::retrieveCachedUpdate(int version) { void DynamicListDataSourceConnection::constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const Object& operationIndex, const std::string& message) { @@ -267,7 +267,7 @@ DynamicListDataSourceConnection::constructAndReportError( return; } - provider->constructAndReportError(session, reason, shared_from_this(), operationIndex, message); + provider->constructAndReportError(context, reason, shared_from_this(), operationIndex, message); } DynamicListDataSourceProvider::DynamicListDataSourceProvider(const DynamicListConfiguration& config) @@ -282,7 +282,7 @@ DynamicListDataSourceProvider::create( auto ctx = context.lock(); if (!ctx) return nullptr; if (!sourceDefinition.has(LIST_ID) || !sourceDefinition.get(LIST_ID).isString()) { - constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); + constructAndReportError(ctx, ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); return nullptr; } @@ -297,8 +297,8 @@ DynamicListDataSourceProvider::create( return existingConnection; } // Trying to reuse existing listId/DataSource. Should not happen. - constructAndReportError(existingConnection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, listId, - "Trying to reuse existing listId."); + constructAndReportError(existingConnection->getContext(), ERROR_REASON_INTERNAL_ERROR, listId, + "Not allowed to reuse existing listId."); return nullptr; } @@ -408,7 +408,7 @@ DynamicListDataSourceProvider::getPendingErrors() { void DynamicListDataSourceProvider::constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const std::string& listId, const Object& listVersion, @@ -429,26 +429,26 @@ DynamicListDataSourceProvider::constructAndReportError( mPendingErrors.emplace_back(std::move(error)); // Throw errors into log to help debugging on device - LOG(LogLevel::kWarn).session(session) << "Datasource " << listId << "; Error: " << message; + LOG(LogLevel::kWarn).session(context) << "Datasource " << listId << "; Error: " << message; } void DynamicListDataSourceProvider::constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const std::string& listId, const std::string& message) { - constructAndReportError(session, reason, listId, Object::NULL_OBJECT(), Object::NULL_OBJECT(), message); + constructAndReportError(context, reason, listId, Object::NULL_OBJECT(), Object::NULL_OBJECT(), message); } void DynamicListDataSourceProvider::constructAndReportError( - const SessionPtr& session, + const ContextPtr& context, const std::string& reason, const DLConnectionPtr& connection, const Object& operationIndex, const std::string& message) { - constructAndReportError(session, reason, connection->getListId(), connection->getListVersion(), operationIndex, message); + constructAndReportError(context, reason, connection->getListId(), connection->getListVersion(), operationIndex, message); } bool @@ -462,7 +462,7 @@ DynamicListDataSourceProvider::canFetch(const Object& correlationToken, const DL return false; } - constructAndReportError(connection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, connection, + constructAndReportError(connection->getContext(), ERROR_REASON_INTERNAL_ERROR, connection, Object::NULL_OBJECT(), "Wrong correlation token."); return false; } \ No newline at end of file diff --git a/aplcore/src/datasource/dynamictokenlistdatasourceprovider.cpp b/aplcore/src/datasource/dynamictokenlistdatasourceprovider.cpp index 123a205..4ef0565 100644 --- a/aplcore/src/datasource/dynamictokenlistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamictokenlistdatasourceprovider.cpp @@ -53,7 +53,7 @@ DynamicTokenListDataSourceConnection::processLazyLoad( if (!context) { return false; } - auto items = evaluateRecursive(*context, data); + auto items = evaluateNested(*context, data); bool result = false; if (items.isArray() && !items.empty()) { @@ -62,7 +62,7 @@ DynamicTokenListDataSourceConnection::processLazyLoad( result = updateLiveArray(dataArray, pageToken, nextPageToken); } else { - constructAndReportError(getContext()->session(), ERROR_REASON_MISSING_LIST_ITEMS, Object::NULL_OBJECT(), + constructAndReportError(getContext(), ERROR_REASON_MISSING_LIST_ITEMS, Object::NULL_OBJECT(), "No items provided to load."); retryFetchRequest(correlationToken.asString()); return result; @@ -136,7 +136,7 @@ DynamicTokenListDataSourceProvider::createConnection( if (!sourceDefinition.has(LIST_ID) || !sourceDefinition.get(LIST_ID).isString()|| !sourceDefinition.has(PAGE_TOKEN) || !sourceDefinition.get(PAGE_TOKEN).isString()) { - constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); + constructAndReportError(ctx, ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); return nullptr; } @@ -158,7 +158,7 @@ DynamicTokenListDataSourceProvider::processLazyLoadInternal( return false; if (!responseMap.has(ITEMS)) { - constructAndReportError(connection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, connection->getListId(), + constructAndReportError(connection->getContext(), ERROR_REASON_INTERNAL_ERROR, connection->getListId(), "Missing required fields."); return true; } diff --git a/aplcore/src/document/CMakeLists.txt b/aplcore/src/document/CMakeLists.txt index 63bc9cc..8e5cc92 100644 --- a/aplcore/src/document/CMakeLists.txt +++ b/aplcore/src/document/CMakeLists.txt @@ -13,6 +13,8 @@ target_sources_local(apl PRIVATE + coredocumentcontext.cpp + documentcontextdata.cpp displaystate.cpp documentproperties.cpp ) diff --git a/aplcore/src/document/coredocumentcontext.cpp b/aplcore/src/document/coredocumentcontext.cpp new file mode 100644 index 0000000..e8400ee --- /dev/null +++ b/aplcore/src/document/coredocumentcontext.cpp @@ -0,0 +1,806 @@ +/** + * Copyright 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 "apl/document/coredocumentcontext.h" + +#include "rapidjson/stringbuffer.h" + +#include "apl/command/arraycommand.h" +#include "apl/command/configchangecommand.h" +#include "apl/command/displaystatechangecommand.h" +#include "apl/command/documentcommand.h" +#include "apl/datasource/datasource.h" +#include "apl/datasource/datasourceprovider.h" +#include "apl/engine/builder.h" +#include "apl/engine/corerootcontext.h" +#include "apl/engine/keyboardmanager.h" +#include "apl/engine/layoutmanager.h" +#include "apl/engine/resources.h" +#include "apl/engine/sharedcontextdata.h" +#include "apl/engine/styles.h" +#include "apl/engine/uidmanager.h" +#include "apl/extension/extensionmanager.h" +#include "apl/focus/focusmanager.h" +#include "apl/graphic/graphic.h" +#include "apl/livedata/livedatamanager.h" +#include "apl/livedata/livedataobjectwatcher.h" +#include "apl/media/mediamanager.h" +#include "apl/time/sequencer.h" +#include "apl/time/timemanager.h" +#include "apl/touch/pointermanager.h" +#include "apl/utils/tracing.h" + +#ifdef SCENEGRAPH +#include "apl/scenegraph/builder.h" +#include "apl/scenegraph/scenegraph.h" +#endif // SCENEGRAPH + +namespace apl { + +static const char *DISPLAY_STATE = "displayState"; +static const char *ELAPSED_TIME = "elapsedTime"; +static const char *LOCAL_TIME = "localTime"; +static const char *UTC_TIME = "utcTime"; +static const char *ON_MOUNT_HANDLER_NAME = "Mount"; + +static const std::string MOUNT_SEQUENCER = "__MOUNT_SEQUENCER"; + +CoreDocumentContextPtr +CoreDocumentContext::create(const ContextPtr& context, + const ContentPtr& content, + const Object& env, + const Size& size, + const DocumentConfigPtr& documentConfig) +{ + const RootConfig& rootConfig = context->getRootConfig(); + const RootConfigPtr embeddedRootConfig = rootConfig.copy(); + embeddedRootConfig->set(RootProperty::kLang, env.opt("lang", context->getLang())); + embeddedRootConfig->set(RootProperty::kLayoutDirection, + env.opt("layoutDirection", sLayoutDirectionMap.get(context->getLayoutDirection(), ""))); + + embeddedRootConfig->set(RootProperty::kAllowOpenUrl, + rootConfig.getProperty(RootProperty::kAllowOpenUrl).getBoolean() + && env.opt("allowOpenURL", true).asBoolean()); + + auto copyDisallowProp = [&]( RootProperty propName, const std::string& envName ) { + embeddedRootConfig->set(propName, rootConfig.getProperty(propName).getBoolean() || env.opt(envName, false).asBoolean()); + }; + + copyDisallowProp(RootProperty::kDisallowDialog, "disallowDialog"); + copyDisallowProp(RootProperty::kDisallowEditText, "disallowEditText"); + copyDisallowProp(RootProperty::kDisallowVideo, "disallowVideo"); + + if (documentConfig != nullptr) { +#ifdef ALEXAEXTENSIONS + embeddedRootConfig->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider); + embeddedRootConfig->extensionMediator(documentConfig->getExtensionMediator()); +#endif + + for (const auto& provider : documentConfig->getDataSourceProviders()) { + embeddedRootConfig->dataSourceProvider(provider->getType(), provider); + } + } + + // std::lround use copied from Dimension::AbsoluteDimensionObjectType::asInt + const int width = std::lround(context->dpToPx(size.getWidth())); + const int height = std::lround(context->dpToPx(size.getHeight())); + + auto metrics = Metrics() + .size(width, height) + .shape(context->getScreenShape()) + .theme(context->getTheme().c_str()) + .dpi(context->getDpi()) + .mode(context->getViewportMode()); + + return CoreDocumentContext::create(context->getShared(), metrics, content, *embeddedRootConfig); +} + +CoreDocumentContextPtr +CoreDocumentContext::create( + const SharedContextDataPtr& root, + const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config) +{ + if (!content->isReady()) { + LOG(LogLevel::kError).session(content->getSession()) << "Attempting to create root context with illegal content"; + return nullptr; + } + + auto document = std::make_shared(content, config); + document->init(metrics, config, root, false); + + return document; +} + +CoreDocumentContext::CoreDocumentContext(const ContentPtr& content, const RootConfig& config) + : mContent(content), + mDisplayState(static_cast(config.getProperty(RootProperty::kInitialDisplayState).getInteger())) +{} + +CoreDocumentContext::~CoreDocumentContext() { + assert(mCore); + mCore->terminate(); + mCore->getDirtyDatasourceContext().clear(); + mCore->getDirtyVisualContext().clear(); +} + +void +CoreDocumentContext::configurationChange(const ConfigurationChange& change) +{ + // If we're in the middle of a configuration change, drop it + mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); + + mActiveConfigurationChanges.mergeConfigurationChange(change); + if (mActiveConfigurationChanges.empty()) + return; + + auto cmd = ConfigChangeCommand::create(shared_from_this(), + mActiveConfigurationChanges.asEventProperties(mCore->rootConfig(), + mCore->metrics())); + mContext->sequencer().executeOnSequencer(cmd, ConfigChangeCommand::SEQUENCER); +} + +void +CoreDocumentContext::updateDisplayState(DisplayState displayState) +{ + if (!sDisplayStateMap.has(displayState)) { + LOG(LogLevel::kWarn).session(getSession()) << "View specified an invalid display state, ignoring it"; + return; + } + + if (displayState == mDisplayState) return; + + // If we're in the middle of a display state change, drop it + mCore->sequencer().terminateSequencer(DisplayStateChangeCommand::SEQUENCER); + + mDisplayState = displayState; + + const auto& displayStateString = sDisplayStateMap.at(displayState); + mContext->systemUpdateAndRecalculate(DISPLAY_STATE, displayStateString, true); + + ObjectMap properties; + properties.emplace("displayState", displayStateString); + + auto cmd = DisplayStateChangeCommand::create(shared_from_this(), std::move(properties)); + mContext->sequencer().executeOnSequencer(cmd, DisplayStateChangeCommand::SEQUENCER); + +#ifdef ALEXAEXTENSIONS + auto mediator = getRootConfig().getExtensionMediator(); + if (mediator) { + mediator->onDisplayStateChanged(mDisplayState); + } +#endif +} + +bool +CoreDocumentContext::reinflate(const LayoutCallbackFunc& layoutCallback) +{ + // The basic algorithm is to simply re-build CoreDocumentContexData and re-inflate the component hierarchy. + // TODO: Re-use parts of the hierarchy and to maintain state during reinflation. + mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); + + auto preservedSequencers = std::map(); + for (auto& stp : mCore->sequencer().getSequencersToPreserve()) { + preservedSequencers.emplace(stp, mCore->sequencer().detachSequencer(stp)); + } + + auto shared = mCore->mSharedData; + if (!shared) return false; + + auto oldTop = mCore->halt(); + if (oldTop) { + shared->layoutManager().removeAsTopNode(oldTop); + } + + auto metrics = mActiveConfigurationChanges.mergeMetrics(mCore->mMetrics); + auto config = mActiveConfigurationChanges.mergeRootConfig(mCore->mConfig); + + // Update the configuration with the current UTC time and time adjustment + config.set(RootProperty::kUTCTime, mUTCTime); + config.set(RootProperty::kLocalTimeAdjustment, mLocalTimeAdjustment); + + // The initialization routine replaces mCore with a new core + init(metrics, config, shared, true); + setup(oldTop); + + // If we are reinflating root document we need to re-setup. + if (topComponent() && layoutCallback) { + layoutCallback(); + } + + // If there was a previous top component, release it and its children to free memory + if (oldTop) + oldTop->release(); + + // Clear the old active configuration; it is reset on a reinflation + mActiveConfigurationChanges.clear(); + + for (auto& ps : preservedSequencers) { + if(!mCore->sequencer().reattachSequencer(ps.first, ps.second, *this)) { + CONSOLE(getSession()) << "Can't preserve sequencer: " << ps.first; + } + } + + return topComponent() != nullptr; +} + +void +CoreDocumentContext::resize() +{ + // Release any "onConfigChange" action + mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); + mCore->layoutManager().configChange(mActiveConfigurationChanges, shared_from_this()); + // Note: we do not clear the configuration changes - there may be a reinflate() coming in the future. +} + + +void +CoreDocumentContext::init(const Metrics& metrics, + const RootConfig& config, + const SharedContextDataPtr& sharedData, + bool reinflation) +{ + APL_TRACE_BLOCK("DocumentContext:init"); + std::string theme = metrics.getTheme(); + const auto& json = mContent->getDocument()->json(); + auto themeIter = json.FindMember("theme"); + if (themeIter != json.MemberEnd() && themeIter->value.IsString()) + theme = themeIter->value.GetString(); + + mCore = std::make_shared(shared_from_this(), + metrics, + config, + RuntimeState(theme, + mContent->getDocument()->version(), + reinflation), + mContent->getDocumentSettings(), + mContent->getSession(), + mContent->mExtensionRequests, + sharedData); + + auto env = mContent->getEnvironment(config); + mCore->lang(env.language).layoutDirection(env.layoutDirection); + + mContext = Context::createRootEvaluationContext(metrics, mCore); + mContext->putSystemWriteable(ELAPSED_TIME, config.getTimeManager()->currentTime()); + mContext->putSystemWriteable(DISPLAY_STATE, sDisplayStateMap.at(mDisplayState)); + + mUTCTime = config.getUTCTime(); + mLocalTimeAdjustment = config.getLocalTimeAdjustment(); + mContext->putSystemWriteable(UTC_TIME, mUTCTime); + mContext->putSystemWriteable(LOCAL_TIME, mUTCTime + mLocalTimeAdjustment); + + // Insert one LiveArrayObject or LiveMapObject into the top-level context for each defined LiveObject + for (const auto& m : config.getLiveObjectMap()) { + auto ldo = LiveDataObject::create(m.second, mContext, m.first); + auto watchers = config.getLiveDataWatchers(m.first); + for (auto& watcher : watchers) { + if (watcher) + watcher->registerObjectWatcher(ldo); + } + } + +#ifdef ALEXAEXTENSIONS + // Get all known extension clients, via legacy pathway and mediator + auto clients = config.getLegacyExtensionClients(); + auto extensionMediator = config.getExtensionMediator(); + if (extensionMediator) { + const auto& mediatorClients = extensionMediator->getClients(); + clients.insert(mediatorClients.begin(), mediatorClients.end()); + } + // Insert extension-defined live data + for (auto& client : clients) { + const auto& schema = client.second->extensionSchema(); + for (const auto& m : schema.liveData) { + auto ldo = LiveDataObject::create(m.second, mContext, m.first); + client.second->registerObjectWatcher(ldo); + } + } +#endif +} + +void +CoreDocumentContext::clearPending() const +{ + assert(mCore); + + // Run any onMount handlers for something that may have been attached at runtime + // We execute those on the sequencer to avoid messing stuff up. Will work much more similarly to previous behavior, + // but will not interrupt something that may have been scheduled just before. + auto& onMounts = mCore->pendingOnMounts(); + if (!onMounts.empty()) { + const auto& tm = getRootConfig().getTimeManager(); + std::vector parallelCommands; + for (auto& pendingOnMount : onMounts) { + if (auto comp = pendingOnMount.lock()) { + auto commands = comp->getCalculated(kPropertyOnMount); + auto ctx = comp->createDefaultEventContext(ON_MOUNT_HANDLER_NAME); + parallelCommands.emplace_back( + ArrayCommand::create( + ctx, + commands, + comp, + Properties(), + "")->execute(tm, false)); + } + } + onMounts.clear(); + + auto mountAction = Action::makeAll(tm, parallelCommands); + mCore->sequencer().attachToSequencer(mountAction, MOUNT_SEQUENCER); + } + +#ifdef ALEXAEXTENSIONS + // Process any extension events. There are no need to expose those externally. + auto extensionMediator = mCore->rootConfig().getExtensionMediator(); + if (extensionMediator) { + while (!mCore->getExtensionEvents().empty()) { + Event event = mCore->getExtensionEvents().front(); + mCore->getExtensionEvents().pop(); + extensionMediator->invokeCommand(event); + } + } +#endif +} + +bool +CoreDocumentContext::isVisualContextDirty() const +{ + assert(mCore); + return !mCore->getDirtyVisualContext().empty(); +} + +void +CoreDocumentContext::clearVisualContextDirty() +{ + assert(mCore); + mCore->getDirtyVisualContext().clear(); +} + +rapidjson::Value +CoreDocumentContext::serializeVisualContext(rapidjson::Document::AllocatorType& allocator) +{ + clearVisualContextDirty(); + return mCore->mTop->serializeVisualContext(allocator); +} + +bool +CoreDocumentContext::isDataSourceContextDirty() const +{ + assert(mCore); + return !mCore->getDirtyDatasourceContext().empty(); +} + +void +CoreDocumentContext::clearDataSourceContextDirty() +{ + assert(mCore); + mCore->getDirtyDatasourceContext().clear(); +} + +rapidjson::Value +CoreDocumentContext::serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) +{ + clearDataSourceContextDirty(); + + rapidjson::Value outArray(rapidjson::kArrayType); + + for (const auto& tracker : mCore->dataManager().trackers()) { + if (auto sourceConnection = tracker->getDataSourceConnection()) { + rapidjson::Value datasource(rapidjson::kObjectType); + sourceConnection->serialize(datasource, allocator); + + outArray.PushBack(datasource.Move(), allocator); + } + } + return outArray; +} + +rapidjson::Value +CoreDocumentContext::serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) +{ + if (extended) + return mCore->mTop->serializeAll(allocator); + return mCore->mTop->serialize(allocator); +} + +rapidjson::Value +CoreDocumentContext::serializeContext(rapidjson::Document::AllocatorType& allocator) +{ + return mContext->serialize(allocator); +} + + +std::shared_ptr +CoreDocumentContext::createDocumentEventProperties(const std::string& handler) const +{ + auto source = std::make_shared(); + source->emplace("source", "Document"); + source->emplace("type", "Document"); + source->emplace("handler", handler); + source->emplace("id", Object::NULL_OBJECT()); + source->emplace("uid", Object::NULL_OBJECT()); + source->emplace("value", Object::NULL_OBJECT()); + auto event = std::make_shared(); + event->emplace("source", source); + return event; +} + +ContextPtr +CoreDocumentContext::createDocumentContext(const std::string& handler, const ObjectMap& optional) +{ + ContextPtr ctx = Context::createFromParent(payloadContext()); + auto event = createDocumentEventProperties(handler); + for (const auto& m : optional) + event->emplace(m.first, m.second); + ctx->putConstant("event", event); + return ctx; +} + + +ActionPtr +CoreDocumentContext::executeCommands(const apl::Object& commands, bool fastMode) +{ + ContextPtr ctx = createDocumentContext("External"); + return mContext->sequencer().executeCommands(commands, ctx, nullptr, fastMode); +} + +ActionPtr +CoreDocumentContext::invokeExtensionEventHandler(const std::string& uri, const std::string& name, + const ObjectMap& data, bool fastMode, + std::string resourceId) +{ + auto handlerDefinition = ExtensionEventHandler{uri, name}; + auto handler = Object::NULL_OBJECT(); + ContextPtr ctx = nullptr; + auto comp = mCore->extensionManager().findExtensionComponent(resourceId); + if (comp) { + handler = comp->findHandler(handlerDefinition); + if (handler.isNull()) { + CONSOLE(getSession()) << "Extension Component " << comp->name() + << " can't execute event handler " << handlerDefinition.getName(); + return nullptr; + } + + // Create component context. Data is attached on event level. + auto dataPtr = std::make_shared(data); + ctx = comp->createEventContext(name, dataPtr); + } else { + handler = mCore->extensionManager().findHandler(handlerDefinition); + if (handler.isNull()) { + CONSOLE(getSession()) << "Extension Handler " << handlerDefinition.getName() << " don't exist."; + return nullptr; + } + + // Create a document-level context. Data is attached on event level. + ctx = createDocumentContext(name, data); + } + + for (const auto& m : data) + ctx->putConstant(m.first, m.second); + + return mContext->sequencer().executeCommands(handler, ctx, comp, fastMode); +} + +ComponentPtr +CoreDocumentContext::topComponent() +{ + return mCore->mTop; +} + +ContextPtr +CoreDocumentContext::payloadContext() const +{ + // We could cache the payload context, but it is infrequently used. Instead we search upwards from the + // top components context until we find the context right before the top-level context. + if (!mCore || !mCore->mTop) + return mContext; + + auto context = mCore->mTop->getContext(); + if (context == nullptr || context == mContext) + return mContext; + + while (context->parent() != mContext) + context = context->parent(); + + return context; +} + +apl_time_t +CoreDocumentContext::currentTime() +{ + return mCore->mConfig.getTimeManager()->currentTime(); +} + +void +CoreDocumentContext::updateTime(apl_time_t utcTime, apl_duration_t localTimeAdjustment) +{ + mUTCTime = utcTime; + mLocalTimeAdjustment = localTimeAdjustment; + + mContext->systemUpdateAndRecalculate(ELAPSED_TIME, currentTime(), true); + mContext->systemUpdateAndRecalculate(UTC_TIME, mUTCTime, true); + mContext->systemUpdateAndRecalculate(LOCAL_TIME, mUTCTime + mLocalTimeAdjustment, true); +} + +const RootConfig& +CoreDocumentContext::rootConfig() +{ + return mCore->rootConfig(); +} + +bool +CoreDocumentContext::setup(const CoreComponentPtr& top) +{ + APL_TRACE_BLOCK("DocumentContext:setup"); + std::vector ordered = mContent->ordered(); + + // check type field of each package + auto enforceTypeField = mCore->rootConfig().getEnforceTypeField(); + if(!verifyTypeField(ordered, enforceTypeField)) { + return false; + } + + auto supportedVersions = mCore->rootConfig().getEnforcedAPLVersion(); + if(!verifyAPLVersionCompatibility(ordered, supportedVersions)) { + return false; + } + + bool trackProvenance = mCore->rootConfig().getTrackProvenance(); + + // Read settings + // Deprecated, get settings from Content->getDocumentSettings() + { + APL_TRACE_BEGIN("DocumentContext:readSettings"); + mCore->mSettings->read(mCore->rootConfig()); + APL_TRACE_END("DocumentContext:readSettings"); + } + + // Resource processing: + APL_TRACE_BEGIN("DocumentContext:processResources"); + for (const auto& child : ordered) { + const auto& json = child->json(); + const auto path = Path(trackProvenance ? child->name() : std::string()); + addNamedResourcesBlock(*mContext, json, path, "resources"); + } + APL_TRACE_END("DocumentContext:processResources"); + + // Style processing + APL_TRACE_BEGIN("DocumentContext:processStyles"); + for (const auto& child : ordered) { + const auto& json = child->json(); + const auto path = Path(trackProvenance ? child->name() : std::string()); + + auto styleIter = json.FindMember("styles"); + if (styleIter != json.MemberEnd() && styleIter->value.IsObject()) + mCore->styles()->addStyleDefinitions(mCore->session(), &styleIter->value, path.addObject("styles")); + } + APL_TRACE_END("DocumentContext:processStyles"); + + // Layout processing + APL_TRACE_BEGIN("DocumentContext:processLayouts"); + for (const auto& child : ordered) { + const auto& json = child->json(); + const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("layouts"); + + auto layoutIter = json.FindMember("layouts"); + if (layoutIter != json.MemberEnd() && layoutIter->value.IsObject()) { + for (const auto& kv : layoutIter->value.GetObject()) { + const auto& name = kv.name.GetString(); + mCore->mLayouts[name] = { &kv.value, path.addObject(name) }; + } + } + } + APL_TRACE_END("DocumentContext:processLayouts"); + + // Command processing + APL_TRACE_BEGIN("DocumentContext:processCommands"); + for (const auto& child : ordered) { + const auto& json = child->json(); + const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("commands"); + + auto commandIter = json.FindMember("commands"); + if (commandIter != json.MemberEnd() && commandIter->value.IsObject()) { + for (const auto& kv : commandIter->value.GetObject()) { + const auto& name = kv.name.GetString(); + mCore->mCommands[name] = { &kv.value, path.addObject(name) }; + } + } + } + APL_TRACE_END("DocumentContext:processCommands"); + + // Graphics processing + APL_TRACE_BEGIN("DocumentContext:processGraphics"); + for (const auto& child : ordered) { + const auto& json = child->json(); + const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("graphics"); + + auto graphicsIter = json.FindMember("graphics"); + if (graphicsIter != json.MemberEnd() && graphicsIter->value.IsObject()) { + for (const auto& kv : graphicsIter->value.GetObject()) { + const auto& name = kv.name.GetString(); + mCore->mGraphics[name] = { &kv.value, path.addObject(name)}; + } + } + } + APL_TRACE_END("DocumentContext:processGraphics"); + + // Identify all registered event handlers in all ordered documents + APL_TRACE_BEGIN("DocumentContext:processExtensionHandlers"); + auto& em = mCore->extensionManager(); + for (const auto& handler : em.getEventHandlerDefinitions()) { + for (const auto& child : ordered) { + const auto& json = child->json(); + auto h = json.FindMember(handler.first.c_str()); + if (h != json.MemberEnd()) { + auto oldHandler = em.findHandler(handler.second); + if (!oldHandler.isNull()) + CONSOLE(mContext) << "Overwriting existing command handler " << handler.first; + em.addEventHandler(handler.second, asCommand(*mContext, evaluate(*mContext, h->value))); + } + } + } + APL_TRACE_END("DocumentContext:processExtensionHandlers"); + + // Inflate the top component + Properties properties; + + APL_TRACE_BEGIN("DocumentContext:retrieveProperties"); + mContent->getMainProperties(properties); + APL_TRACE_END("DocumentContext:retrieveProperties"); + + mCore->mTop = Builder(top).inflate(mContext, properties, mContent->getMainTemplate()); + + if (!mCore->mTop) + return false; + + mCore->mTop->markGlobalToLocalTransformStale(); + +#ifdef ALEXAEXTENSIONS + // Bind to the extension mediator + // TODO ExtensionMediator is an experimental class facilitating message passing to and from extensions. + // TODO The mediator class should be replaced by direct messaging between extensions and ExtensionManager + auto extensionMediator = mCore->rootConfig().getExtensionMediator(); + if (extensionMediator) { + extensionMediator->bindContext(shared_from_this()); + } +#endif + + return true; +} + +bool +CoreDocumentContext::verifyAPLVersionCompatibility(const std::vector& ordered, + const APLVersion& compatibilityVersion) +{ + for(const auto& child : ordered) { + if(!compatibilityVersion.isValid(child->version())) { + CONSOLE(mContext) << child->name() << " has invalid version: " << child->version(); + return false; + } + } + return true; +} + +bool +CoreDocumentContext::verifyTypeField(const std::vector>& ordered, bool enforce) +{ + for(auto& child : ordered) { + auto type = child->type(); + if (type.compare("APML") == 0) CONSOLE(mContext) + << child->name() << ": Stop using the APML document format!"; + else if (type.compare("APL") != 0) { + CONSOLE(mContext) << child->name() << ": Document type field should be \"APL\"!"; + if(enforce) { + return false; + } + } + } + return true; +} + + +streamer& +operator<<(streamer& os, const CoreDocumentContext& root) +{ + os << "DocumentContext: " << root.context(); + return os; +} + + +const SessionPtr& +CoreDocumentContext::getSession() const +{ + return mCore->session(); +} + +const RootConfig& +CoreDocumentContext::getRootConfig() const +{ + assert(mCore); + return mCore->rootConfig(); +} + +std::string +CoreDocumentContext::getTheme() const +{ + assert(mCore); + return mCore->getTheme(); +} + +const TextMeasurementPtr& +CoreDocumentContext::measure() const +{ + return mCore->measure(); +} + +ComponentPtr +CoreDocumentContext::findComponentById(const std::string& id) const +{ + assert(mCore); + + // Fast path search for uid value + auto *ptr = findByUniqueId(id); + if (ptr && ptr->objectType() == UIDObject::UIDObjectType::COMPONENT) + return static_cast(ptr)->shared_from_this(); + + // Depth-first search + auto top = mCore->top(); + return top ? top->findComponentById(id, false) : nullptr; +} + +UIDObject * +CoreDocumentContext::findByUniqueId(const std::string& uid) const +{ + assert(mCore); + return mCore->uniqueIdManager().find(uid); +} + +void +CoreDocumentContext::processOnMounts() +{ + // Execute the "onMount" document command + APL_TRACE_BEGIN("DocumentContext:executeOnMount"); + auto cmd = DocumentCommand::create(kPropertyOnMount, ON_MOUNT_HANDLER_NAME, shared_from_this()); + mCore->sequencer().execute(cmd, false); + // Clear any pending mounts as we just executed those + mCore->pendingOnMounts().clear(); + APL_TRACE_END("DocumentContext:executeOnMount"); +} + +void +CoreDocumentContext::flushDataUpdates() +{ + mCore->dataManager().flushDirty(); +} + +CoreDocumentContextPtr +CoreDocumentContext::cast(const DocumentContextPtr& documentContext) +{ + return std::static_pointer_cast(documentContext); +} + +ContextPtr +CoreDocumentContext::createKeyEventContext(const std::string& handler, const ObjectMapPtr& keyboard) +{ + ContextPtr ctx = Context::createFromParent(payloadContext()); + auto event = createDocumentEventProperties(handler); + event->emplace("keyboard", keyboard); + ctx->putConstant("event", event); + return ctx; +} + +} // namespace apl diff --git a/aplcore/src/document/documentcontextdata.cpp b/aplcore/src/document/documentcontextdata.cpp new file mode 100644 index 0000000..4fbfab2 --- /dev/null +++ b/aplcore/src/document/documentcontextdata.cpp @@ -0,0 +1,135 @@ +/** + * Copyright 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 "apl/document/documentcontextdata.h" + +#include "apl/engine/keyboardmanager.h" +#include "apl/engine/layoutmanager.h" +#include "apl/engine/sharedcontextdata.h" +#include "apl/engine/uidmanager.h" +#include "apl/extension/extensionmanager.h" +#include "apl/livedata/livedatamanager.h" +#include "apl/time/sequencer.h" +#include "apl/time/timemanager.h" +#include "apl/touch/pointermanager.h" +#include "apl/utils/make_unique.h" + +#ifdef SCENEGRAPH +#include "apl/scenegraph/textpropertiescache.h" +#endif // SCENEGRAPH + +namespace apl { + +/** + * Construct the root context from metrics. + * + * Internally we create a sequencer, a Yoga/Flexbox configuration, + * and a copy of the currently installed TextMeasurement utility. + * + * @param metrics The display metrics. + */ +DocumentContextData::DocumentContextData( + const DocumentContextPtr& document, + const Metrics& metrics, + const RootConfig& config, + RuntimeState runtimeState, + const SettingsPtr& settings, + const SessionPtr& session, + const std::vector& extensions, + const SharedContextDataPtr& sharedContext) + : ContextData(config, std::move(runtimeState), settings, "", kLayoutDirectionInherit), + mSharedData(sharedContext), + mDocument(document), + mMetrics(metrics), + mStyles(new Styles()), + mSequencer(new Sequencer(config.getTimeManager(), mRuntimeState.getRequestedAPLVersion())), + mDataManager(new LiveDataManager()), + mExtensionManager(new ExtensionManager(extensions, config, session)), + mUniqueIdManager(new UIDManager(sharedContext->uidGenerator(), session)), + mSession(session) +{} + +DocumentContextData::~DocumentContextData() { + mSharedData.reset(); + mUniqueIdManager->terminate(); +} + +void +DocumentContextData::terminate() +{ + auto top = halt(); + if (top) + top->release(); +} + +CoreComponentPtr +DocumentContextData::halt() +{ + if (mSequencer) { + mSequencer->terminate(); + } + + // Clear any pending events and dirty components + mSharedData->dirtyComponents().eraseScope(shared_from_this()); + mDirtyVisualContext.clear(); + + auto result = mTop; + mTop = nullptr; + if (result) result->clearActiveState(); + +#ifdef ALEXAEXTENSIONS + // Clear related extension events + mExtensionEvents = std::queue(); +#endif // ALEXAEXTENSIONS + + // Clear out events related to this context + mSharedData->eventManager().eraseScope(shared_from_this()); + + return result; +} + +void +DocumentContextData::pushEvent(Event&& event) +{ + event.setDocument(mDocument); + mSharedData->eventManager().emplace(shared_from_this(), std::move(event)); +} + +bool +DocumentContextData::embedded() const +{ + return top() && top()->getParent() != nullptr; +} + +FocusManager& DocumentContextData::focusManager() const { return mSharedData->focusManager(); } +HoverManager& DocumentContextData::hoverManager() const { return mSharedData->hoverManager(); } +LayoutManager& DocumentContextData::layoutManager() const { return mSharedData->layoutManager(); } +MediaManager& DocumentContextData::mediaManager() const { return mSharedData->mediaManager(); } +MediaPlayerFactory& DocumentContextData::mediaPlayerFactory() const { return mSharedData->mediaPlayerFactory(); } +DependantManager& DocumentContextData::dependantManager() const { return mSharedData->dependantManager(); } +const YGConfigRef& DocumentContextData::ygconfig() const { return mSharedData->ygconfig(); } +const TextMeasurementPtr& DocumentContextData::measure() const { return mSharedData->measure(); } +void DocumentContextData::takeScreenLock() { mSharedData->takeScreenLock(); } +void DocumentContextData::releaseScreenLock() { mSharedData->releaseScreenLock(); } +LruCache& DocumentContextData::cachedMeasures() { return mSharedData->cachedMeasures(); } +LruCache& DocumentContextData::cachedBaselines() { return mSharedData->cachedBaselines(); } +void DocumentContextData::setDirty(const ComponentPtr& component) { mSharedData->dirtyComponents().emplace(shared_from_this(), component); } +void DocumentContextData::clearDirty(const ComponentPtr& component) { mSharedData->dirtyComponents().eraseValue(component); } + +#ifdef SCENEGRAPH +sg::TextPropertiesCache& DocumentContextData::textPropertiesCache() { return mSharedData->textPropertiesCache(); } +#endif // SCENEGRAPH + +} // namespace apl diff --git a/aplcore/src/embed/CMakeLists.txt b/aplcore/src/embed/CMakeLists.txt new file mode 100644 index 0000000..ead0f5f --- /dev/null +++ b/aplcore/src/embed/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright 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. + +target_sources_local(apl + PRIVATE + documentregistrar.cpp + embedrequest.cpp +) diff --git a/aplcore/src/embed/documentregistrar.cpp b/aplcore/src/embed/documentregistrar.cpp new file mode 100644 index 0000000..c38dd8b --- /dev/null +++ b/aplcore/src/embed/documentregistrar.cpp @@ -0,0 +1,55 @@ +/** +* Copyright 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 "apl/embed/documentregistrar.h" + +#include + +#include "apl/utils/log.h" + +namespace apl { + +const std::map& +DocumentRegistrar::list() const +{ + return mDocumentMap; +} + +CoreDocumentContextPtr +DocumentRegistrar::get(int id) const +{ + auto it = mDocumentMap.find(id); + return (it != mDocumentMap.end()) + ? it->second + : nullptr; +} + +int +DocumentRegistrar::registerDocument(const CoreDocumentContextPtr& document) +{ + auto id = mIdGenerator++; + // We never expect it to overflow + assert(id > 0); + mDocumentMap.emplace(id, document); + return id; +} + +void +DocumentRegistrar::deregisterDocument(int id) +{ + if (!mDocumentMap.erase(id)) LOG(LogLevel::kWarn) << "Can't de-register document " << id; +} + +} // namespace apl diff --git a/aplcore/src/embed/embedrequest.cpp b/aplcore/src/embed/embedrequest.cpp new file mode 100644 index 0000000..3253e4c --- /dev/null +++ b/aplcore/src/embed/embedrequest.cpp @@ -0,0 +1,45 @@ +/** +* Copyright 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 "apl/embed/embedrequest.h" + +#include "apl/document/documentcontext.h" + +namespace apl { + +EmbedRequestPtr +EmbedRequest::create(URLRequest url, const DocumentContextPtr& origin) { + return std::make_shared(std::move(url), origin); +} + +EmbedRequest::EmbedRequest(URLRequest url, const DocumentContextPtr& origin) + : mUrl(std::move(url)), mOrigin(origin) +{} + +const URLRequest& +EmbedRequest::getUrlRequest() const +{ + return mUrl; +} + +DocumentContextPtr +EmbedRequest::getOrigin() const +{ + return mOrigin.lock(); +} + +} // namespace apl diff --git a/aplcore/src/engine/CMakeLists.txt b/aplcore/src/engine/CMakeLists.txt index a5759e3..c8d2aad 100644 --- a/aplcore/src/engine/CMakeLists.txt +++ b/aplcore/src/engine/CMakeLists.txt @@ -16,12 +16,12 @@ target_sources_local(apl arrayify.cpp binding.cpp builder.cpp - componentdependant.cpp context.cpp - contextdependant.cpp contextobject.cpp contextwrapper.cpp + corerootcontext.cpp dependant.cpp + dependantmanager.cpp evaluate.cpp event.cpp hovermanager.cpp @@ -33,11 +33,13 @@ target_sources_local(apl properties.cpp resources.cpp rootcontext.cpp - rootcontextdata.cpp + sharedcontextdata.cpp state.cpp styledefinition.cpp styleinstance.cpp styles.cpp + tickscheduler.cpp + uidgenerator.cpp uidmanager.cpp uidobject.cpp ) diff --git a/aplcore/src/engine/builder.cpp b/aplcore/src/engine/builder.cpp index 58cc2c1..97616dd 100644 --- a/aplcore/src/engine/builder.cpp +++ b/aplcore/src/engine/builder.cpp @@ -21,6 +21,7 @@ #include "apl/component/edittextcomponent.h" #include "apl/component/framecomponent.h" #include "apl/component/gridsequencecomponent.h" +#include "apl/component/hostcomponent.h" #include "apl/component/imagecomponent.h" #include "apl/component/pagercomponent.h" #include "apl/component/scrollviewcomponent.h" @@ -33,10 +34,10 @@ #include "apl/engine/arrayify.h" #include "apl/engine/binding.h" #include "apl/engine/context.h" -#include "apl/engine/contextdependant.h" #include "apl/engine/evaluate.h" #include "apl/engine/parameterarray.h" #include "apl/engine/properties.h" +#include "apl/engine/typeddependant.h" #include "apl/extension/extensioncomponent.h" #include "apl/extension/extensionmanager.h" #include "apl/livedata/layoutrebuilder.h" @@ -65,7 +66,8 @@ static const std::map sComponentMap = { {"TouchWrapper", TouchWrapperComponent::create}, {"Pager", PagerComponent::create}, {"VectorGraphic", VectorGraphicComponent::create}, - {"Video", VideoComponent::create} + {"Video", VideoComponent::create}, + {"Host", HostComponent::create} }; void @@ -129,7 +131,7 @@ Builder::populateLayoutComponent(const ContextPtr& context, layoutBuilder->build(useDirtyFlag); } else { - auto dataItems = evaluateRecursive(*context, data); + auto dataItems = evaluateNested(*context, data); if (!dataItems.empty()) { LOG_IF(DEBUG_BUILDER).session(context) << "data size=" << dataItems.size(); @@ -266,7 +268,7 @@ Builder::expandSingleComponent(const ContextPtr& context, CoreComponentPtr oldComponent; if(mOld) { oldComponent = CoreComponent::cast( - mOld->findComponentById(component->getId())); + mOld->findComponentById(component->getId(), false)); copyPreservedBindings(component, oldComponent); } @@ -306,7 +308,7 @@ Builder::expandSingleComponent(const ContextPtr& context, * @param item The item that contains a "bind" property. */ void -Builder::attachBindings(const apl::ContextPtr& context, const apl::Object& item) +Builder::attachBindings(const ContextPtr& context, const Object& item) { APL_TRACE_BLOCK("Builder:attachBindings"); auto bindings = arrayifyProperty(*context, item, "bind"); @@ -328,17 +330,16 @@ Builder::attachBindings(const apl::ContextPtr& context, const apl::Object& item) } // Extract the binding as an optional node tree. - auto tmp = propertyAsNode(*context, binding, "value"); - auto value = evaluateRecursive(*context, tmp); - auto bindingType = propertyAsMapped(*context, binding, "type", kBindingTypeAny, sBindingMap); + auto result = parseAndEvaluate(*context, binding.get("value")); + auto bindingType = + propertyAsMapped(*context, binding, "type", kBindingTypeAny, sBindingMap); auto bindingFunc = sBindingFunctions.at(bindingType); // Store the value in the new context. Binding values are mutable; they can be changed later. - context->putUserWriteable(name, bindingFunc(*context, value)); - - // If it is a node, we connect up the symbols that it is dependant upon - if (tmp.isEvaluable()) - ContextDependant::create(context, name, tmp, context, bindingFunc); + context->putUserWriteable(name, bindingFunc(*context, result.value)); + if (!result.symbols.empty()) + ContextDependant::create(context, name, std::move(result.expression), context, + std::move(bindingFunc), std::move(result.symbols)); } } diff --git a/aplcore/src/engine/componentdependant.cpp b/aplcore/src/engine/componentdependant.cpp deleted file mode 100644 index a31c28a..0000000 --- a/aplcore/src/engine/componentdependant.cpp +++ /dev/null @@ -1,55 +0,0 @@ -/** - * Copyright 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 "apl/engine/componentdependant.h" -#include "apl/engine/context.h" -#include "apl/engine/evaluate.h" -#include "apl/component/corecomponent.h" -#include "apl/primitives/symbolreferencemap.h" - -namespace apl { - -void ComponentDependant::create(const CoreComponentPtr& downstreamComponent, - PropertyKey downstreamKey, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction) -{ - SymbolReferenceMap symbols; - equation.symbols(symbols); - if (symbols.empty()) - return; - - auto dependant = std::make_shared(downstreamComponent, downstreamKey, equation, - bindingContext, bindingFunction); - - for (const auto& symbol : symbols.get()) - symbol.second->addDownstream(symbol.first, dependant); - - downstreamComponent->addUpstream(downstreamKey, dependant); -} - -void -ComponentDependant::recalculate(bool useDirtyFlag) const -{ - auto downstream = mDownstreamComponent.lock(); - auto bindingContext = mBindingContext.lock(); - if (downstream && bindingContext) { - auto value = mBindingFunction(*bindingContext, reevaluate(*bindingContext, mEquation)); - downstream->updateProperty(mDownstreamKey, value); - } -} - -} // namespace apl diff --git a/aplcore/src/engine/context.cpp b/aplcore/src/engine/context.cpp index d5d4495..480da1c 100644 --- a/aplcore/src/engine/context.cpp +++ b/aplcore/src/engine/context.cpp @@ -13,34 +13,95 @@ * permissions and limitations under the License. */ -#include - #include "apl/engine/context.h" +#include + #include "apl/buildTimeConstants.h" #include "apl/component/textmeasurement.h" #include "apl/content/metrics.h" #include "apl/content/rootconfig.h" +#include "apl/content/settings.h" #include "apl/content/viewport.h" +#include "apl/document/documentcontextdata.h" #include "apl/engine/builder.h" +#include "apl/engine/dependantmanager.h" #include "apl/engine/event.h" #include "apl/engine/resources.h" -#include "apl/engine/rootcontextdata.h" +#include "apl/engine/sharedcontextdata.h" #include "apl/engine/styles.h" +#include "apl/extension/extensionmanager.h" #include "apl/primitives/functions.h" -#include "apl/primitives/object.h" -#include "apl/utils/log.h" -#include "apl/utils/lrucache.h" #include "apl/utils/session.h" namespace apl { +class EvaluationContextData : public ContextData { +public: + EvaluationContextData( + const RootConfig& config, + RuntimeState runtimeState, + const SettingsPtr& settings, + const SessionPtr& session) + : ContextData(config, std::move(runtimeState), settings, "", kLayoutDirectionInherit), + mSession(session) + {} + + const SessionPtr& session() const override { return mSession; } + bool embedded() const override { return false; } + +private: + SessionPtr mSession; +}; + +inline ContextDataPtr createEvaluationContextData( + const RootConfig& config, + const std::string& aplVersion, + const std::string& theme, + const SessionPtr& session) +{ + auto contextData = std::make_shared( + config, + RuntimeState(theme, aplVersion, false), + std::make_shared(), + session); + + auto layoutDirection = static_cast(config.getProperty(RootProperty::kLayoutDirection).asInt()); + contextData->lang(config.getProperty(RootProperty::kLang).asString()) + .layoutDirection(layoutDirection); + + return contextData; +} + +inline ContextDataPtr createTestContextData( + const Metrics& metrics, + const RootConfig& config, + const std::string& theme, + const SessionPtr& session) +{ + auto sharedContext = std::make_shared(config); + auto contextData = std::make_shared(nullptr, + metrics, + config, + RuntimeState(theme, config.getReportedAPLVersion(), false), + std::make_shared(), + session, + std::vector(), + sharedContext); + + auto layoutDirection = static_cast(config.getProperty(RootProperty::kLayoutDirection).asInt()); + contextData->lang(config.getProperty(RootProperty::kLang).asString()) + .layoutDirection(layoutDirection); + + return contextData; +} + // Use this to create a free-standing context. Used for testing ContextPtr -Context::createTestContext(const Metrics& metrics, const SessionPtr& session) +Context::createTestContext(const Metrics& metrics, const RootConfig& config, const SessionPtr& session) { - auto config = RootConfig().session(session); - auto contextPtr = std::make_shared(metrics, config, metrics.getTheme()); + auto contextData = createTestContextData(metrics, config, metrics.getTheme(), session); + auto contextPtr = std::make_shared(metrics, contextData); createStandardFunctions(*contextPtr); return contextPtr; } @@ -49,32 +110,45 @@ Context::createTestContext(const Metrics& metrics, const SessionPtr& session) ContextPtr Context::createTestContext(const Metrics& metrics, const RootConfig& config) { - auto contextPtr = std::make_shared(metrics, config, metrics.getTheme()); - createStandardFunctions(*contextPtr); - return contextPtr; + return createTestContext(metrics, config, makeDefaultSession()); +} + +// Use this to create a free-standing context. Used for testing +ContextPtr +Context::createTestContext(const Metrics& metrics, const SessionPtr& session) +{ + auto config = RootConfig(); + return createTestContext(metrics, config, session); } // Use this to create a free-standing context. Used for type conversion and basic environment access. // This method should never add custom enviroment properties to the newly created context as it is // also used to detect collisions with the built-in variables. ContextPtr -Context::createTypeEvaluationContext(const RootConfig& config) +Context::createTypeEvaluationContext(const RootConfig& config, const std::string& aplVersion, const SessionPtr& session) { auto metrics = Metrics(); - auto contextPtr = std::make_shared(metrics, config, metrics.getTheme()); + auto contextData = createEvaluationContextData(config, aplVersion, metrics.getTheme(), session); + auto contextPtr = std::make_shared(metrics, contextData); createStandardFunctions(*contextPtr); return contextPtr; } // Use this to create a free-standing context. Only used for background extraction ContextPtr -Context::createBackgroundEvaluationContext(const Metrics& metrics, const RootConfig& config, const std::string& theme) +Context::createBackgroundEvaluationContext( + const Metrics& metrics, + const RootConfig& config, + const std::string& aplVersion, + const std::string& theme, + const SessionPtr& session) { - return std::make_shared(metrics, config, theme); + auto contextData = createEvaluationContextData(config, aplVersion, theme, session); + return std::make_shared(metrics, contextData); } ContextPtr -Context::createRootEvaluationContext(const Metrics& metrics, const std::shared_ptr& core) +Context::createRootEvaluationContext(const Metrics& metrics, const ContextDataPtr& core) { auto contextPtr = std::make_shared(metrics, core); createStandardFunctions(*contextPtr); @@ -88,8 +162,13 @@ Context::createClean(const ContextPtr& other) return std::make_shared(context); } +DocumentContextDataPtr documentContextData(const ContextDataPtr& data) { + assert(data && data->fullContext()); + return std::static_pointer_cast(data); +} + void -Context::init(const Metrics& metrics, const std::shared_ptr& core) +Context::init(const Metrics& metrics, const ContextDataPtr& core) { auto env = std::make_shared(); auto& config = core->rootConfig(); @@ -101,13 +180,16 @@ Context::init(const Metrics& metrics, const std::shared_ptr& co env->emplace("disallowDialog", config.getProperty(RootProperty::kDisallowDialog).getBoolean()); env->emplace("disallowEditText", config.getProperty(RootProperty::kDisallowEditText).getBoolean()); env->emplace("disallowVideo", config.getDisallowVideo()); - env->emplace("extension", core->extensionManager().getEnvironment()); + if (core->fullContext()) { + env->emplace("extension", documentContextData(core)->extensionManager().getEnvironment()); + } env->emplace("fontScale", config.getFontScale()); env->emplace("lang", core->getLang()); env->emplace("layoutDirection", sLayoutDirectionMap.get(core->getLayoutDirection(), "")); env->emplace("screenMode", config.getScreenMode()); env->emplace("screenReader", config.getScreenReaderEnabled()); env->emplace("reason", core->getReinflationFlag() ? "reinflation" : "initial"); + env->emplace("documentAPLVersion", core->getRequestedAPLVersion()); auto timing = std::make_shared(); timing->emplace("doublePressTimeout", config.getDoublePressTimeout()); @@ -125,69 +207,47 @@ Context::init(const Metrics& metrics, const std::shared_ptr& co putConstant("viewport", makeViewport(metrics, core->getTheme())); } -Context::Context( const Metrics& metrics, const std::shared_ptr& core ) +Context::Context( const Metrics& metrics, const ContextDataPtr& core ) : mCore(core) { + assert(mCore); init(metrics, core); } -Context::Context(const Metrics& metrics, const RootConfig& config, const std::string& theme) -{ - auto session = config.getSession() ? config.getSession() : makeDefaultSession(); - mCore = std::make_shared(metrics, - config, - RuntimeState(theme, config.getReportedAPLVersion(), - false), - std::make_shared(), session, - std::vector>()); - - auto layoutDirection = static_cast(config.getProperty(RootProperty::kLayoutDirection).asInt()); - mCore->lang(config.getProperty(RootProperty::kLang).asString()) - .layoutDirection(layoutDirection); - - init(metrics, mCore); -} - double Context::vwToDp(double vw) const { - assert(mCore); - return mCore->getWidth() * vw / 100; + return documentContextData(mCore)->getWidth() * vw / 100; } double Context::vhToDp(double vh) const { - assert(mCore); - return mCore->getHeight() * vh / 100; + return documentContextData(mCore)->getHeight() * vh / 100; } double Context::pxToDp(double px) const { - assert(mCore); - return mCore->getPxToDp() * px; + return documentContextData(mCore)->getPxToDp() * px; } double Context::dpToPx(double dp) const { - assert(mCore); - return dp / mCore->getPxToDp(); + return dp / documentContextData(mCore)->getPxToDp(); } double Context::width() const { - assert(mCore); - return mCore->getWidth(); + return documentContextData(mCore)->getWidth(); } double Context::height() const { - assert(mCore); - return mCore->getHeight(); + return documentContextData(mCore)->getHeight(); } const RootConfig& @@ -200,15 +260,13 @@ Context::getRootConfig() const StyleInstancePtr Context::getStyle(const std::string& name, const State& state) { - assert(mCore); - return mCore->styles()->get(shared_from_this(), name, state); + return documentContextData(mCore)->styles()->get(shared_from_this(), name, state); } const JsonResource Context::getLayout(const std::string& name) const { - assert(mCore); - const auto& layouts = mCore->layouts(); + const auto& layouts = documentContextData(mCore)->layouts(); auto it = layouts.find(name); if (it != layouts.end()) return it->second; @@ -218,8 +276,7 @@ Context::getLayout(const std::string& name) const const JsonResource Context::getCommand(const std::string& name) const { - assert(mCore); - const auto& commands = mCore->commands(); + const auto& commands = documentContextData(mCore)->commands(); auto it = commands.find(name); if (it != commands.end()) return it->second; @@ -229,9 +286,7 @@ Context::getCommand(const std::string& name) const const JsonResource Context::getGraphic(const std::string& name) const { - assert(mCore); - - const auto& graphics = mCore->graphics(); + const auto& graphics = documentContextData(mCore)->graphics(); auto it = graphics.find(name); if (it != graphics.end()) return it->second; @@ -276,141 +331,155 @@ Context::getReinflationFlag() const std::string Context::getRequestedAPLVersion() const { - assert(mCore); return mCore->getRequestedAPLVersion(); } +int +Context::getDpi() const +{ + return documentContextData(mCore)->getDpi(); +} + +ScreenShape +Context::getScreenShape() const +{ + return documentContextData(mCore)->getScreenShape(); +} + +SharedContextDataPtr +Context::getShared() const +{ + return documentContextData(mCore)->getShared(); +} + +ViewportMode +Context::getViewportMode() const +{ + return documentContextData(mCore)->getViewportMode(); +} + std::shared_ptr Context::styles() const { - assert(mCore); - return mCore->styles(); + return documentContextData(mCore)->styles(); } ComponentPtr -Context::findComponentById(const std::string& id) const +Context::findComponentById(const std::string& id, bool traverseHost) const { - assert(mCore); - - auto top = mCore->top(); - return top ? top->findComponentById(id) : nullptr; + auto top = documentContextData(mCore)->top(); + return top ? top->findComponentById(id, traverseHost) : nullptr; } ComponentPtr Context::topComponent() const { - assert(mCore); - return mCore->top(); + return documentContextData(mCore)->top(); } void Context::pushEvent(Event&& event) { - assert(mCore); - mCore->events->push(event); + documentContextData(mCore)->pushEvent(std::move(event)); } #ifdef ALEXAEXTENSIONS void Context::pushExtensionEvent(Event&& event) { - assert(mCore); - mCore->extesnionEvents.push(event); + documentContextData(mCore)->getExtensionEvents().push(event); } #endif void Context::setDirty(const ComponentPtr& ptr) { - assert(mCore); - mCore->dirty.emplace(ptr); + documentContextData(mCore)->setDirty(ptr); } void Context::clearDirty(const ComponentPtr& ptr) { - assert(mCore); - mCore->dirty.erase(ptr); + documentContextData(mCore)->clearDirty(ptr); } void Context::setDirtyVisualContext(const ComponentPtr& ptr) { - assert(mCore); - mCore->dirtyVisualContext.emplace(ptr); + documentContextData(mCore)->getDirtyVisualContext().emplace(ptr); } bool Context::isVisualContextDirty(const ComponentPtr& ptr) { - auto found = mCore->dirtyVisualContext.find(ptr); - return found != mCore->dirtyVisualContext.end(); + auto found = documentContextData(mCore)->getDirtyVisualContext().find(ptr); + return found != documentContextData(mCore)->getDirtyVisualContext().end(); } void Context::setDirtyDataSourceContext(const DataSourceConnectionPtr& ptr) { - assert(mCore); - mCore->dirtyDatasourceContext.emplace(ptr); + documentContextData(mCore)->getDirtyDatasourceContext().emplace(ptr); } Sequencer& Context::sequencer() const { - return mCore->sequencer(); + return documentContextData(mCore)->sequencer(); } FocusManager& Context::focusManager() const { - return mCore->focusManager(); + return documentContextData(mCore)->focusManager(); } HoverManager& Context::hoverManager() const { - return mCore->hoverManager(); + return documentContextData(mCore)->hoverManager(); } -KeyboardManager & -Context::keyboardManager() const -{ - return mCore->keyboardManager(); -} LiveDataManager& Context::dataManager() const { - return mCore->dataManager(); + return documentContextData(mCore)->dataManager(); } ExtensionManager& Context::extensionManager() const { - return mCore->extensionManager(); + return documentContextData(mCore)->extensionManager(); } LayoutManager& Context::layoutManager() const { - return mCore->layoutManager(); + return documentContextData(mCore)->layoutManager(); } MediaManager& Context::mediaManager() const { - return mCore->mediaManager(); + return documentContextData(mCore)->mediaManager(); } MediaPlayerFactory& Context::mediaPlayerFactory() const { - return mCore->mediaPlayerFactory(); + return documentContextData(mCore)->mediaPlayerFactory(); } UIDManager& Context::uniqueIdManager() const { - return mCore->uniqueIdManager(); + return documentContextData(mCore)->uniqueIdManager(); +} + +DependantManager& +Context::dependantManager() const +{ + return documentContextData(mCore)->dependantManager(); } const SessionPtr& @@ -422,48 +491,60 @@ Context::session() const YGConfigRef Context::ygconfig() const { - return mCore->ygconfig(); + return documentContextData(mCore)->ygconfig(); } const TextMeasurementPtr& Context::measure() const { - return mCore->measure(); + return documentContextData(mCore)->measure(); } void Context::takeScreenLock() const { - mCore->takeScreenLock(); + documentContextData(mCore)->takeScreenLock(); } void Context::releaseScreenLock() const { - mCore->releaseScreenLock(); + documentContextData(mCore)->releaseScreenLock(); } LruCache& Context::cachedMeasures() { - return mCore->cachedMeasures(); + return documentContextData(mCore)->cachedMeasures(); } LruCache& Context::cachedBaselines() { - return mCore->cachedBaselines(); + return documentContextData(mCore)->cachedBaselines(); } WeakPtrSet& Context::pendingOnMounts() { - return mCore->pendingOnMounts(); + return documentContextData(mCore)->pendingOnMounts(); +} + +bool +Context::embedded() const +{ + return documentContextData(mCore)->embedded(); +} + +DocumentContextPtr +Context::documentContext() const +{ + return documentContextData(mCore)->documentContext(); } #ifdef SCENEGRAPH sg::TextPropertiesCache& Context::textPropertiesCache() const { - return mCore->textPropertiesCache(); + return documentContextData(mCore)->textPropertiesCache(); } #endif // SCENEGRAPH @@ -499,12 +580,11 @@ Context::serialize(rapidjson::Document::AllocatorType& allocator) return out; } - streamer& operator<<(streamer& os, const Context& context) { - for (auto it = context.mMap.begin() ; it != context.mMap.end() ; it++) { - os << it->first << ": " << it->second << "\n"; + for (const auto & it : context.mMap) { + os << it.first << ": " << it.second << "\n"; } if (context.mParent) @@ -514,14 +594,17 @@ operator<<(streamer& os, const Context& context) } -bool Context::userUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) +bool +Context::userUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) { auto it = mMap.find(key); if (it != mMap.end()) { if (it->second.isUserWriteable()) { removeUpstream(key); // Break any dependency chain - if (it->second.set(value)) // If the value changes, recalculate downstream values - recalculateDownstream(key, useDirtyFlag); + if (it->second.set(value)) { // If the value changes, recalculate downstream values + enqueueDownstream(key); + dependantManager().processDependencies(useDirtyFlag); + } } else { CONSOLE(mCore->session()) << "Data-binding field '" << key << "' is read-only"; } @@ -535,7 +618,8 @@ bool Context::userUpdateAndRecalculate(const std::string& key, const Object& val return false; } -bool Context::systemUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) +bool +Context::systemUpdateAndRecalculate(const std::string& key, const Object& value, bool useDirtyFlag) { auto it = mMap.find(key); if (it == mMap.end()) @@ -543,8 +627,10 @@ bool Context::systemUpdateAndRecalculate(const std::string& key, const Object& v if (it->second.isMutable()) { removeUpstream(key); // Break any dependency chain - if (it->second.set(value)) // If the value changes, recalculate downstream values - recalculateDownstream(key, useDirtyFlag); + if (it->second.set(value)) { // If the value changes, recalculate downstream values + enqueueDownstream(key); + dependantManager().processDependencies(useDirtyFlag); + } } return true; diff --git a/aplcore/src/engine/contextdependant.cpp b/aplcore/src/engine/contextdependant.cpp deleted file mode 100644 index cf9d4ae..0000000 --- a/aplcore/src/engine/contextdependant.cpp +++ /dev/null @@ -1,62 +0,0 @@ -/** - * Copyright 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 "apl/engine/contextdependant.h" -#include "apl/engine/evaluate.h" -#include "apl/primitives/symbolreferencemap.h" -#include "apl/utils/session.h" - -namespace apl { - -const static bool DEBUG_CONTEXT_DEP = false; - -void -ContextDependant::create(const ContextPtr& downstreamContext, - const std::string& downstreamName, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction) -{ - LOG_IF(DEBUG_CONTEXT_DEP).session(bindingContext) << "to '" << downstreamName << "' (" << downstreamContext.get() << ")"; - - SymbolReferenceMap symbols; - equation.symbols(symbols); - if (symbols.empty()) - return; - - auto dependant = std::make_shared(downstreamContext, downstreamName, equation, - bindingContext, bindingFunction); - - for (const auto& symbol : symbols.get()) - symbol.second->addDownstream(symbol.first, dependant); - - downstreamContext->addUpstream(downstreamName, dependant); -} - -/** - * One context is dependant upon another. - */ -void -ContextDependant::recalculate(bool useDirtyFlag) const -{ - auto downstream = mDownstreamContext.lock(); - auto bindingContext = mBindingContext.lock(); - if (downstream && bindingContext) { - auto value = mBindingFunction(*bindingContext, reevaluate(*bindingContext, mEquation)); - downstream->propagate(mDownstreamName, value, useDirtyFlag); - } -} - -} // namespace apl diff --git a/aplcore/src/engine/corerootcontext.cpp b/aplcore/src/engine/corerootcontext.cpp new file mode 100644 index 0000000..c887960 --- /dev/null +++ b/aplcore/src/engine/corerootcontext.cpp @@ -0,0 +1,716 @@ +/** + * Copyright 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 "apl/engine/corerootcontext.h" + +#include "rapidjson/stringbuffer.h" + +#include "apl/action/scrolltoaction.h" +#include "apl/command/arraycommand.h" +#include "apl/datasource/datasource.h" +#include "apl/datasource/datasourceprovider.h" +#include "apl/embed/documentregistrar.h" +#include "apl/engine/builder.h" +#include "apl/engine/keyboardmanager.h" +#include "apl/engine/layoutmanager.h" +#include "apl/engine/resources.h" +#include "apl/engine/sharedcontextdata.h" +#include "apl/engine/tickscheduler.h" +#include "apl/engine/uidmanager.h" +#include "apl/extension/extensionmanager.h" +#include "apl/focus/focusmanager.h" +#include "apl/graphic/graphic.h" +#include "apl/livedata/livedatamanager.h" +#include "apl/media/mediamanager.h" +#include "apl/time/sequencer.h" +#include "apl/time/timemanager.h" +#include "apl/touch/pointermanager.h" +#include "apl/utils/tracing.h" +#ifdef SCENEGRAPH +#include "apl/scenegraph/builder.h" +#include "apl/scenegraph/scenegraph.h" +#endif // SCENEGRAPH + + +namespace apl { + +static const std::string SCROLL_TO_RECT_SEQUENCER = "__SCROLL_TO_RECT_SEQUENCE"; + +RootContextPtr +CoreRootContext::create(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config, + std::function callback) +{ + if (!content->isReady()) { + LOG(LogLevel::kError).session(content) << "Attempting to create root context with illegal content"; + return nullptr; + } + + auto root = std::make_shared( + metrics, + content, + config); + + root->init(metrics, config, content); + + if (callback) + callback(root); + if (!root->setup(false)) + return nullptr; + + return root; +} + +CoreRootContext::CoreRootContext(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config) + : mTimeManager(config.getTimeManager()), + mDisplayState(static_cast(config.getProperty(RootProperty::kInitialDisplayState).getInteger())) +{ +} + +CoreRootContext::~CoreRootContext() { + assert(mShared); + mTimeManager->terminate(); + mTopDocument = nullptr; + mShared->halt(); +} + +void +CoreRootContext::configurationChange(const ConfigurationChange& change) +{ + assert(mTopDocument); + mTopDocument->configurationChange(change); + + mShared->documentRegistrar().forEach([change](const std::shared_ptr& document) { + // Pass change through as is, document will figure it out itself + return document->configurationChange(change); + }); +} + +void +CoreRootContext::updateDisplayState(DisplayState displayState) +{ + assert(mTopDocument); + mTopDocument->updateDisplayState(displayState); +} + +void +CoreRootContext::reinflate() +{ +#ifdef SCENEGRAPH + // Release the existing scene graph + mSceneGraph = nullptr; +#endif // SCENEGRAPH + + mTopDocument->updateTime(mUTCTime, mLocalTimeAdjustment); + if (!mTopDocument->reinflate([&] () { setup(true); } )) { + LOG(LogLevel::kError) << "Can't reinflate top document, ignoring reinflate command."; + return; + } +} + +void +CoreRootContext::init(const Metrics& metrics, + const RootConfig& config, + const ContentPtr& content) +{ + APL_TRACE_BLOCK("RootContext:init"); + mShared = std::make_shared(shared_from_this(), metrics, config); + + mTopDocument = CoreDocumentContext::create(mShared, metrics, content, config); + + // Hm. Time is interesting. Because it's actually initialized in the context. + mUTCTime = config.getUTCTime(); + mLocalTimeAdjustment = config.getLocalTimeAdjustment(); +} + +void +CoreRootContext::clearPending() const +{ + clearPendingInternal(false); +} + +void +CoreRootContext::clearPendingInternal(bool first) const +{ + assert(mTopDocument && mShared); + + APL_TRACE_BLOCK("RootContext:clearPending"); + // Flush any dynamic data changes, for all documents + mTopDocument->mCore->dataManager().flushDirty(); + mShared->documentRegistrar().forEach([](const std::shared_ptr& document) { + return document->mCore->dataManager().flushDirty(); + }); + + // Make sure any pending events have executed + mTimeManager->runPending(); + + // If we need a layout pass, do it now - it will update the dirty events + if (mShared->layoutManager().needsLayout()) + mShared->layoutManager().layout(true, first); + + mShared->mediaManager().processMediaRequests(std::static_pointer_cast(mTopDocument)->mContext); + + // Clear pending on all docs + mTopDocument->clearPending(); + mShared->documentRegistrar().forEach([](const std::shared_ptr& document) { + return document->clearPending(); + }); +} + +bool +CoreRootContext::hasEvent() const +{ + assert(mShared); + clearPending(); + + return !mShared->eventManager().empty(); +} + +Event +CoreRootContext::popEvent() +{ + assert(mShared); + clearPending(); + + if (!mShared->eventManager().empty()) { + return mShared->eventManager().pop(); + } + + // This should never be reached. + LOG(LogLevel::kError) << "No events available"; + std::exit(EXIT_FAILURE); +} + +bool +CoreRootContext::isDirty() const +{ + assert(mTopDocument); + clearPending(); + return !mShared->dirtyComponents().empty(); +} + +const std::set& +CoreRootContext::getDirty() +{ + assert(mTopDocument); + clearPending(); + return mShared->dirtyComponents().getAll(); +} + +void +CoreRootContext::clearDirty() +{ + assert(mTopDocument); + APL_TRACE_BLOCK("RootContext:clearDirty"); + mShared->dirtyComponents().clear(); +} + +bool +CoreRootContext::isVisualContextDirty() const +{ + assert(mTopDocument); + // TODO: Visual context supposed to be central, but deduped on cloud side. So won't be per doc? + // Or do we stitch it together? + return mTopDocument->isVisualContextDirty(); +} + +void +CoreRootContext::clearVisualContextDirty() +{ + assert(mTopDocument); + mTopDocument->clearVisualContextDirty(); +} + +rapidjson::Value +CoreRootContext::serializeVisualContext(rapidjson::Document::AllocatorType& allocator) +{ + clearVisualContextDirty(); + return mTopDocument->serializeVisualContext(allocator); +} + +bool +CoreRootContext::isDataSourceContextDirty() const +{ + assert(mTopDocument); + return mTopDocument->isDataSourceContextDirty(); +} + +void +CoreRootContext::clearDataSourceContextDirty() +{ + assert(mTopDocument); + mTopDocument->clearDataSourceContextDirty(); +} + +rapidjson::Value +CoreRootContext::serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) +{ + assert(mTopDocument); + return mTopDocument->serializeDataSourceContext(allocator); +} + +rapidjson::Value +CoreRootContext::serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) +{ + assert(mTopDocument); + return mTopDocument->serializeDOM(extended, allocator); +} + +rapidjson::Value +CoreRootContext::serializeContext(rapidjson::Document::AllocatorType& allocator) +{ + assert(mTopDocument); + return mTopDocument->serializeContext(allocator); +} + + +std::shared_ptr +CoreRootContext::createDocumentEventProperties(const std::string& handler) const +{ + auto source = std::make_shared(); + source->emplace("source", "Document"); + source->emplace("type", "Document"); + source->emplace("handler", handler); + source->emplace("id", Object::NULL_OBJECT()); + source->emplace("uid", Object::NULL_OBJECT()); + source->emplace("value", Object::NULL_OBJECT()); + auto event = std::make_shared(); + event->emplace("source", source); + return event; +} + +ContextPtr +CoreRootContext::createDocumentContext(const std::string& handler, const ObjectMap& optional) +{ + ContextPtr ctx = Context::createFromParent(payloadContext()); + auto event = createDocumentEventProperties(handler); + for (const auto& m : optional) + event->emplace(m.first, m.second); + ctx->putConstant("event", event); + return ctx; +} + +ActionPtr +CoreRootContext::executeCommands(const apl::Object& commands, bool fastMode) +{ + assert(mTopDocument); + return mTopDocument->executeCommands(commands, fastMode); +} + +ActionPtr +CoreRootContext::invokeExtensionEventHandler(const std::string& uri, const std::string& name, + const ObjectMap& data, bool fastMode, + std::string resourceId) +{ + assert(mTopDocument); + return mTopDocument->invokeExtensionEventHandler(uri, name, data, fastMode, resourceId); +} + +void +CoreRootContext::cancelExecution() +{ + assert(mTopDocument); + mTopDocument->mCore->sequencer().reset(); +} + +ComponentPtr +CoreRootContext::topComponent() const +{ + assert(mTopDocument); + return mTopDocument->topComponent(); +} + +DocumentContextPtr +CoreRootContext::topDocument() const +{ + assert(mTopDocument); + return mTopDocument; +} + +ContextPtr +CoreRootContext::payloadContext() const +{ + assert(mTopDocument); + return mTopDocument->payloadContext(); +} + +Sequencer& +CoreRootContext::sequencer() const +{ + return mTopDocument->mCore->sequencer(); +} + +double +CoreRootContext::getPxToDp() const +{ + return mTopDocument->mCore->getPxToDp(); +} + +void +CoreRootContext::updateTimeInternal(apl_time_t elapsedTime, apl_time_t utcTime) +{ + APL_TRACE_BLOCK("RootContext:updateTime"); + auto lastTime = mTimeManager->currentTime(); + + APL_TRACE_BEGIN("RootContext:flushDirtyData"); + // Flush any dynamic data changes + mTopDocument->flushDataUpdates(); + APL_TRACE_END("RootContext:flushDirtyData"); + + APL_TRACE_BEGIN("RootContext:timeManagerUpdateTime"); + mTimeManager->updateTime(elapsedTime); + APL_TRACE_END("RootContext:timeManagerUpdateTime"); + + if (utcTime > 0) { + mUTCTime = utcTime; + } else { + // Update the local time by how much time passed on the "elapsed" timer + mUTCTime += mTimeManager->currentTime() - lastTime; + } + + APL_TRACE_BEGIN("RootContext:systemUpdateAndRecalculateTime"); + mTopDocument->updateTime(mUTCTime, mLocalTimeAdjustment); + mShared->documentRegistrar().forEach([&](const std::shared_ptr& document) { + document->updateTime(mUTCTime, mLocalTimeAdjustment); + }); + APL_TRACE_END("RootContext:systemUpdateAndRecalculateTime"); + + APL_TRACE_BEGIN("RootContext:pointerHandleTimeUpdate"); + mShared->pointerManager().handleTimeUpdate(elapsedTime); + APL_TRACE_END("RootContext:pointerHandleTimeUpdate"); +} + +void +CoreRootContext::updateTime(apl_time_t elapsedTime) +{ + updateTimeInternal(elapsedTime, 0); +} + +void +CoreRootContext::updateTime(apl_time_t elapsedTime, apl_time_t utcTime) +{ + updateTimeInternal(elapsedTime, utcTime); +} + +void +CoreRootContext::scrollToRectInComponent(const ComponentPtr& component, const Rect &bounds, CommandScrollAlign align) +{ + auto scrollToAction = ScrollToAction::make( + mTimeManager, align, bounds, std::static_pointer_cast(mTopDocument)->mContext, CoreComponent::cast(component)); + if (scrollToAction && scrollToAction->isPending()) { + mTopDocument->mCore->sequencer().attachToSequencer(scrollToAction, SCROLL_TO_RECT_SEQUENCER); + } +} + + +apl_time_t +CoreRootContext::nextTime() +{ + return mTimeManager->nextTimeout(); +} + +apl_time_t +CoreRootContext::currentTime() const +{ + return mTimeManager->currentTime(); +} + +bool +CoreRootContext::screenLock() const +{ + assert(mShared); + clearPending(); + return mShared->screenLock(); +} + +/** + * @deprecated Use Content->getDocumentSettings() + */ +const Settings& +CoreRootContext::settings() const +{ + return *mTopDocument->content()->getDocumentSettings(); +} + +const RootConfig& +CoreRootContext::rootConfig() const +{ + return mTopDocument->rootConfig(); +} + +bool +CoreRootContext::setup(bool reinflate) +{ + APL_TRACE_BLOCK("RootContext:setup"); + if (!reinflate) { + if (!mTopDocument->setup(nullptr)) + return false; + } else { + // Update LayoutManager with the new overall size + const auto& metrics = mTopDocument->mCore->metrics(); + mShared->layoutManager().setSize( + Size( + static_cast(metrics.getWidth()), + static_cast(metrics.getHeight()) + ) + ); + } + + mShared->layoutManager().firstLayout(); + + //TODO: Verify onMount and Tick handlers when reinflate supported for EmbeddedDocs. + mTopDocument->processOnMounts(); + + // A bunch of commands may be queued up at the start time. Clear those out. + clearPendingInternal(true); + + // Those commands may have set the dirty flags. Clear them. + clearDirty(); + + // Commands or layout may have marked visual context dirty. Clear visual context. + mTopDocument->clearVisualContextDirty(); + + // Process and schedule tick handlers. + mShared->tickScheduler().processTickHandlers(mTopDocument); + + return true; +} + +/* Remove when migrated to handlePointerEvent */ +void +CoreRootContext::updateCursorPosition(Point cursorPosition) +{ + handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, cursorPosition)); +} + +bool +CoreRootContext::handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) +{ + assert(mShared); + auto &km = mShared->keyboardManager(); + auto &fm = mShared->focusManager(); + return km.handleKeyboard(type, fm.getFocus(), keyboard, shared_from_this()); +} + +bool +CoreRootContext::handlePointerEvent(const PointerEvent& pointerEvent) +{ + assert(mShared); + return mShared->pointerManager().handlePointerEvent(pointerEvent, mTimeManager->currentTime()); +} + +const RootConfig& +CoreRootContext::getRootConfig() const +{ + assert(mTopDocument); + return mTopDocument->rootConfig(); +} + +std::string +CoreRootContext::getTheme() const +{ + assert(mTopDocument); + return mTopDocument->getTheme(); +} + +const TextMeasurementPtr& +CoreRootContext::measure() const +{ + return mShared->measure(); +} + +ComponentPtr +CoreRootContext::findComponentById(const std::string& id) const +{ + assert(mTopDocument); + + // Fast path search for uid value + auto *ptr = findByUniqueId(id); + if (ptr && ptr->objectType() == UIDObject::UIDObjectType::COMPONENT) + return static_cast(ptr)->shared_from_this(); + + // Depth-first search + auto top = CoreComponent::cast(mTopDocument->topComponent()); + return top ? top->findComponentById(id, true) : nullptr; +} + +UIDObject* +CoreRootContext::findByUniqueId(const std::string& uid) const +{ + assert(mTopDocument); + // Traverse all documents + auto result = mTopDocument->findByUniqueId(uid); + if (result) return result; + + for (auto& document : mShared->documentRegistrar().list()) { + result = document.second->findByUniqueId(uid); + if (result) return result; + } + + return nullptr; +} + +std::map +CoreRootContext::getFocusableAreas() +{ + assert(mShared); + return mShared->focusManager().getFocusableAreas(); +} + +bool +CoreRootContext::setFocus(FocusDirection direction, const Rect& origin, const std::string& targetId) +{ + assert(mShared); + assert(mTopDocument); + auto top = mTopDocument->topComponent(); + auto target = CoreComponent::cast(findComponentById(targetId)); + + if (!target) { + LOG(LogLevel::kWarn) << "Don't have component: " << targetId; + return false; + } + + Rect targetRect; + target->getBoundsInParent(top, targetRect); + + // Shift origin into target's coordinate space. + auto offsetFocusRect = origin; + offsetFocusRect.offset(-targetRect.getTopLeft()); + + return mShared->focusManager().focus(direction, offsetFocusRect, target); +} + +bool +CoreRootContext::nextFocus(FocusDirection direction, const Rect& origin) +{ + assert(mShared); + return mShared->focusManager().focus(direction, origin); +} + +bool +CoreRootContext::nextFocus(FocusDirection direction) +{ + assert(mShared); + return mShared->focusManager().focus(direction); +} + +void +CoreRootContext::clearFocus() +{ + assert(mShared); + mShared->focusManager().clearFocus(false); +} + +std::string +CoreRootContext::getFocused() +{ + assert(mShared); + auto focused = mShared->focusManager().getFocus(); + return focused ? focused->getUniqueId() : ""; +} + +void +CoreRootContext::mediaLoaded(const std::string& source) +{ + assert(mShared); + mShared->mediaManager().mediaLoadComplete(source, true, -1, std::string()); +} + +void +CoreRootContext::mediaLoadFailed(const std::string& source, int errorCode, const std::string& error) +{ + assert(mShared); + mShared->mediaManager().mediaLoadComplete(source, false, errorCode, error); +} + +Info +CoreRootContext::info() const +{ + return Info(mTopDocument->contextPtr(), mTopDocument->mCore); +} + +/** + * @return The top-level context. + */ +Context& +CoreRootContext::context() const +{ + return mTopDocument->context(); +} + +/** + * @return The top-level context as a shared pointer + */ +ContextPtr +CoreRootContext::contextPtr() const +{ + return mTopDocument->contextPtr(); +} + +const ContentPtr& +CoreRootContext::content() const +{ + return mTopDocument->content(); +} + +bool +CoreRootContext::getAutoWidth() const +{ + return mTopDocument->mCore->metrics().getAutoWidth(); } + +bool +CoreRootContext::getAutoHeight() const +{ + return mTopDocument->mCore->metrics().getAutoHeight(); +} + +#ifdef SCENEGRAPH +/* + * If it does exist, we clean out any dirty markings for the scene graph, then walk + * the list of dirty components and update the scene graph. If the scene graph does not exist, + * we create a new one. + */ +sg::SceneGraphPtr +CoreRootContext::getSceneGraph() +{ + assert(mTopDocument); + + // If we need a layout pass, do it now - this avoids screen flicker when a Text component + // with "auto" width has a new, longer content but a full layout has not yet executed. + if (mShared->layoutManager().needsLayout()) + mShared->layoutManager().layout(true, false); + + if (!mSceneGraph) + mSceneGraph = sg::SceneGraph::create(); + + if (mSceneGraph->getLayer()) { + mSceneGraph->updates().clear(); + for (auto& component : getDirty()) + CoreComponent::cast(component)->updateSceneGraph(mSceneGraph->updates()); + } else { + auto top = CoreComponent::cast(mTopDocument->topComponent()); + if (top) + mSceneGraph->setLayer(top->getSceneGraph(mSceneGraph->updates())); + } + + mSceneGraph->updates().fixCreatedFlags(); + clearDirty(); + return mSceneGraph; +} +#endif // SCENEGRAPH +} // namespace apl diff --git a/aplcore/src/engine/dependant.cpp b/aplcore/src/engine/dependant.cpp index 3d7fab7..cc21589 100644 --- a/aplcore/src/engine/dependant.cpp +++ b/aplcore/src/engine/dependant.cpp @@ -14,20 +14,74 @@ */ #include "apl/engine/dependant.h" +#include "apl/engine/dependantmanager.h" #include "apl/engine/context.h" -#include "apl/primitives/symbolreferencemap.h" +#include "apl/engine/evaluate.h" +#include "apl/primitives/boundsymbolset.h" namespace apl { +Dependant::Dependant(Object expression, + const ContextPtr& bindingContext, + BindingFunction bindingFunction, + BoundSymbolSet symbols) + : mExpression(std::move(expression)), + mBindingContext(bindingContext), + mBindingFunction(std::move(bindingFunction)), + mSymbols(std::move(symbols)), + mOrder(bindingContext->dependantManager().getNextSortOrder()) +{ + assert(bindingContext); +} + +bool +Dependant::enqueue() +{ + auto downstream = mBindingContext.lock(); + if (!downstream) + return false; + + downstream->dependantManager().enqueueDependency(shared_from_this()); + return true; +} + void Dependant::removeFromSource() { - SymbolReferenceMap symbols; - mEquation.symbols(symbols); - for (const auto& symbol : symbols.get()) - symbol.second->removeDownstream(shared_from_this()); - - mEquation = Object::NULL_OBJECT(); + detach(); }; +void +Dependant::attach() +{ + for (const auto& m : mSymbols) { + auto context = m.getContext(); + if (context) + context->addDownstream(m.getName(), shared_from_this()); + } +} + +void +Dependant::detach() +{ + for (const auto& m : mSymbols) { + auto context = m.getContext(); + if (context) + context->removeDownstream(shared_from_this()); + } + + mSymbols.clear(); +} + +void +Dependant::reattach(const BoundSymbolSet& symbols) +{ + if (mSymbols != symbols) { + detach(); + mSymbols = symbols; + attach(); + } +} + + } // namespace apl \ No newline at end of file diff --git a/aplcore/src/engine/dependantmanager.cpp b/aplcore/src/engine/dependantmanager.cpp new file mode 100644 index 0000000..03a922e --- /dev/null +++ b/aplcore/src/engine/dependantmanager.cpp @@ -0,0 +1,68 @@ +/* + * Copyright 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 "apl/engine/dependant.h" +#include "apl/engine/dependantmanager.h" +#include "apl/utils/log.h" + +namespace apl { + +const bool DEBUG_DEPENDANT_MANAGER = false; + +/** + * We enqueue dependencies into the list sorted in topological order. + * For now we store them in normal order in a vector - which means we are generally + * adding to the _back_ of the vector, but popping elements from the _front. Arguably + * we could store them in reverse order (since popping from the back is fast), but that + * would mean most add operations are in the front. Or we could try a dequeue or linked-list + * approach. We'll keep it simple until there is time to do performance measurements. + */ +void +DependantManager::enqueueDependency(const DependantPtr& dependant) +{ + assert(dependant); + LOG_IF(DEBUG_DEPENDANT_MANAGER) << "Enqueue dependant: " << dependant->toDebugString(); + + // Find the first element in the queue that is greater-than or equal-to dependant + auto it = std::lower_bound(mProcessList.begin(), + mProcessList.end(), + dependant, + [](const DependantPtr& lhs, + const DependantPtr& rhs){ + return *lhs < *rhs; + }); + + // Check if it is already in the queue + if (it != mProcessList.end() && *it == dependant) + return; + + mProcessList.insert(it, dependant); +} + +void +DependantManager::processDependencies(bool useDirtyFlag) +{ + while (!mProcessList.empty()) { + // Pop the dependency off the front + auto dependant = mProcessList.front(); + LOG_IF(DEBUG_DEPENDANT_MANAGER) << "Processing dependant: " << dependant->toDebugString(); + mProcessList.erase(mProcessList.begin()); + + dependant->recalculate(useDirtyFlag); + } +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/engine/evaluate.cpp b/aplcore/src/engine/evaluate.cpp index 9af7824..2a72a4e 100644 --- a/aplcore/src/engine/evaluate.cpp +++ b/aplcore/src/engine/evaluate.cpp @@ -17,77 +17,117 @@ #include "apl/engine/evaluate.h" #include "apl/datagrammar/bytecodeassembler.h" +#include "apl/datagrammar/bytecodeoptimizer.h" #include "apl/engine/context.h" #include "apl/utils/log.h" #include "apl/utils/session.h" namespace apl { +/** + * Resource lookup on object if it is of string type. Otherwise original object will be returned. + */ Object -getDataBinding(const Context& context, const std::string& value) +resourceLookup(const Context& context, const Object& result) { - return datagrammar::ByteCodeAssembler::parse(context, value); + if (result.isString()) { + std::string s = result.getString(); + if (!s.empty() && s[0] == '@' && context.has(s)) + return context.opt(s); // This isn't efficient because we do a has() and a get(). + } + + return result; } Object -parseDataBinding(const Context& context, const std::string& value) +parseDataBinding(const Context& context, const std::string& value, bool optimize) { auto result = datagrammar::ByteCodeAssembler::parse(context, value); - if (result.isEvaluable()) - return result.get()->simplify(); + if (optimize && result.is()) + result.get()->optimize(); return result; } Object -parseDataBindingRecursive(const Context& context, const Object& object) +parseDataBindingNested(const Context& context, const Object& object, bool optimize) // NOLINT(misc-no-recursion) { if (object.isString()) { - return parseDataBinding(context, object.getString()); + return parseDataBinding(context, object.getString(), optimize); } else if (object.isTrueMap()) { auto result = std::make_shared>(); for (const auto &m : object.getMap()) - result->emplace(m.first, parseDataBindingRecursive(context, m.second)); - return Object(result); + result->emplace(m.first, parseDataBindingNested(context, m.second, optimize)); + return { result }; } else if (object.isArray()) { auto v = std::make_shared>(); for (auto index = 0; index < object.size(); index++) - v->push_back(parseDataBindingRecursive(context, object.at(index))); - return Object(v); + v->push_back(parseDataBindingNested(context, object.at(index), optimize)); + return { v }; } return object; } Object -applyDataBinding(const Context& context, const std::string& value) +applyDataBindingNested(const Context& context, // NOLINT(misc-no-recursion) + const Object& object, + BoundSymbolSet *symbols, + int depth) { - Object parsed = getDataBinding(context, value); - if (parsed.isEvaluable()) - return parsed.eval(); + if (object.is()) + return resourceLookup(context, object.get()->evaluate(symbols, depth)); + + if (object.isTrueMap()) { + auto result = std::make_shared>(); + for (const auto& m : object.getMap()) + result->emplace(m.first, applyDataBindingNested(context, m.second, symbols, depth)); + return { result }; + } + + if (object.isArray()) { // Embedded data-bound strings are inserted in-line: E.g., [ 1, "${b}" ] + std::vector v; + for (auto index = 0 ; index < object.size() ; index++) { + auto item = object.at(index); + auto itemEvaluated = applyDataBindingNested(context, item, symbols, depth); + if (item.is() && itemEvaluated.isArray()) { // Insert the results into the array + for (const auto& n : itemEvaluated.getArray()) + v.push_back(n); + } else { + v.push_back(itemEvaluated); + } + } + return { std::move(v) }; + } - return parsed; + return resourceLookup(context, object); +} + +ApplyResult +applyDataBinding(const Context& context, + const Object& object, + const BindingFunction& bindingFunction) +{ + BoundSymbolSet symbols; + auto result = applyDataBindingNested(context, object, &symbols, 0); + if (bindingFunction) + result = bindingFunction(context, result); + return { result, std::move(symbols) }; } Object evaluate(const Context& context, const Object& object) { // If it is a string, we check for data-binding - auto result = object.isString() ? getDataBinding(context, object.getString()) : object; + auto result = object.isString() ? parseDataBinding(context, object.getString(), false) : object; - // Nodes get evaluated + // Expressions and bound symbols are evaluated if (result.isEvaluable()) result = result.eval(); // Strings get a resource check - if (result.isString()) { - std::string s = result.getString(); - if (!s.empty() && s[0] == '@' && context.has(s)) - return context.opt(s); // This isn't efficient because we do a has() and a get(). - } - - return result; + return resourceLookup(context, result); } Object @@ -97,69 +137,28 @@ evaluate(const Context& context, const char *expression) } Object -reevaluate(const Context& context, const Object& equation) +evaluateNested(const Context& context, const Object& object, BoundSymbolSet *symbolSet) { - // An evaluable equation is re-calculated. Otherwise, we try a recursive evaluation. - auto result = equation.isEvaluable() ? equation.eval() : evaluateRecursive(context, equation); - - // Strings get a resource check - if (result.isString()) { - std::string s = result.getString(); - if (!s.empty() && s[0] == '@' && context.has(s)) - return context.opt(s); // This isn't efficient because we do a has() and a get(). - } - - return result; + auto result = parseDataBindingNested(context, object, false); + return applyDataBindingNested(context, result, symbolSet, 0); } -/** - * Resource lookup on object if it is of string type. Otherwise original object will be returned. - */ Object -resourceLookup(const Context& context, const Object& result) +evaluateInternal(const Context& context, const Object& object, BoundSymbolSet *symbolSet, int depth) { - if (result.isString()) { - std::string s = result.getString(); - if (!s.empty() && s[0] == '@' && context.has(s)) - return context.opt(s); // This isn't efficient because we do a has() and a get(). - } - - return result; + auto result = parseDataBindingNested(context, object, false); + return applyDataBindingNested(context, result, symbolSet, depth); } -Object -evaluateRecursive(const Context& context, const Object& object) +ParseResult +parseAndEvaluate(const Context& context, + const Object& object, + bool optimize) { - if (object.isString()) { - auto result = applyDataBinding(context, object.getString()); - return resourceLookup(context, result); - } - else if (object.isTrueMap()) { - auto result = std::make_shared>(); - for (const auto& m : object.getMap()) - result->emplace(m.first, evaluateRecursive(context, m.second)); - return Object(result); - } - else if (object.isArray()) { // Embedded data-bound strings are inserted in-line: E.g., [ 1, "${b}" ] - std::vector v; - for (auto index = 0 ; index < object.size() ; index++) { - auto item = object.at(index); - auto itemEvaluated = evaluateRecursive(context, item); - if (item.isString() && itemEvaluated.isArray()) { // Insert the results into the array - for (const auto& n : itemEvaluated.getArray()) - v.push_back(n); - } else { - v.push_back(itemEvaluated); - } - } - return Object(std::move(v)); - } - else if (object.isEvaluable()) { - auto result = object.eval(); - return resourceLookup(context, result); - } - - return object; + BoundSymbolSet symbols; + auto parsed = parseDataBindingNested(context, object, optimize); + auto evaluated = applyDataBindingNested(context, parsed, &symbols, 0); + return { std::move(evaluated), std::move(parsed), std::move(symbols) }; } bool @@ -240,19 +239,7 @@ propertyAsRecursive(const Context& context, if (!item.isMap() || !item.has(name)) return Object::NULL_OBJECT(); - return evaluateRecursive(context, item.get(name)); -} - -Object -propertyAsNode(const Context& context, - const Object& item, - const char *name) -{ - if (!item.isMap() || !item.has(name)) - return Object::NULL_OBJECT(); - - auto object = item.get(name); - return object.isString() ? parseDataBinding(context, object.getString()) : object; + return evaluateNested(context, item.get(name)); } } // namespace apl diff --git a/aplcore/src/engine/event.cpp b/aplcore/src/engine/event.cpp index 5a0f412..6fef128 100644 --- a/aplcore/src/engine/event.cpp +++ b/aplcore/src/engine/event.cpp @@ -154,12 +154,12 @@ Event::serialize(rapidjson::Document::AllocatorType& allocator) const } bool -Event::matches(const Event& rhs) const +Event::operator==(const Event& other) const { - return mData->eventType == rhs.mData->eventType && - mData->component == rhs.mData->component && - mData->bag == rhs.mData->bag; + return mData->eventType == other.mData->eventType && + mData->component == other.mData->component && + !mDocument.owner_before(other.mDocument) && + mData->bag == other.mData->bag; } - } // namespace apl diff --git a/aplcore/src/engine/hovermanager.cpp b/aplcore/src/engine/hovermanager.cpp index 1f51467..54bd948 100644 --- a/aplcore/src/engine/hovermanager.cpp +++ b/aplcore/src/engine/hovermanager.cpp @@ -13,12 +13,11 @@ * permissions and limitations under the License. */ -#include - -#include "apl/engine/event.h" #include "apl/engine/hovermanager.h" -#include "apl/engine/rootcontextdata.h" + #include "apl/component/corecomponent.h" +#include "apl/engine/corerootcontext.h" +#include "apl/engine/event.h" namespace apl { @@ -61,16 +60,16 @@ HoverManager::setCursorPosition(const Point& cursorPosition) { if (previous && !previous->getState().get(kStateDisabled)) { previous->executeOnCursorExit(); - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorExit: " << previous->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER) << "Execute OnCursorExit: " << previous->toDebugSimpleString(); } if (target && !target->getState().get(kStateDisabled)) { target->executeOnCursorEnter(); - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); } // store the new hover component, if any - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "hover change -\n\tfrom: " << previous << "\n\t to:" << target; + LOG_IF(DEBUG_HOVER) << "hover change -\n\tfrom: " << previous << "\n\t to:" << target; mHover = target; } @@ -83,7 +82,7 @@ HoverManager::setCursorPosition(const Point& cursorPosition) { */ CoreComponentPtr HoverManager::findHoverByPosition(const Point& position) const { - auto top = mCore.top(); + auto top = mCore.topComponent(); if (!top) return nullptr; @@ -112,10 +111,10 @@ HoverManager::componentToggledDisabled(const CoreComponentPtr& component) { // execute the OnCursor commands if (target->getState().get(kStateDisabled)) { target->executeOnCursorExit(); - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorExit: " << target->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER) << "Execute OnCursorExit: " << target->toDebugSimpleString(); } else { target->executeOnCursorEnter(); - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); } } @@ -142,14 +141,14 @@ HoverManager::update(const CoreComponentPtr& previous, const CoreComponentPtr& t // UnSet the previous Component's hover state, and the ancestors it inherits state from, if any. if (previousStateOwner) { previousStateOwner->setState(kStateHover, false); - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Hover Previous: " << previous->toDebugSimpleString() << " state: " << previous->getState(); + LOG_IF(DEBUG_HOVER) << "Hover Previous: " << previous->toDebugSimpleString() << " state: " << previous->getState(); } // Set the target Components's hover state, and the ancestors it inherits state from, if any. if (targetStateOwner) { bool isHover = !targetStateOwner->getState().get(kStateDisabled); targetStateOwner->setState(kStateHover, isHover); - LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Hover Target: " << target->toDebugSimpleString() << " state: " << target->getState(); + LOG_IF(DEBUG_HOVER) << "Hover Target: " << target->toDebugSimpleString() << " state: " << target->getState(); } } diff --git a/aplcore/src/engine/info.cpp b/aplcore/src/engine/info.cpp index 731bdf5..c660296 100644 --- a/aplcore/src/engine/info.cpp +++ b/aplcore/src/engine/info.cpp @@ -15,8 +15,8 @@ #include "apl/engine/info.h" +#include "apl/document/documentcontextdata.h" #include "apl/engine/context.h" -#include "apl/engine/rootcontextdata.h" #include "apl/engine/styles.h" namespace apl { diff --git a/aplcore/src/engine/keyboardmanager.cpp b/aplcore/src/engine/keyboardmanager.cpp index f866bf7..0b01f38 100644 --- a/aplcore/src/engine/keyboardmanager.cpp +++ b/aplcore/src/engine/keyboardmanager.cpp @@ -13,15 +13,17 @@ * permissions and limitations under the License. */ +#include "apl/engine/keyboardmanager.h" + #include "apl/common.h" #include "apl/command/documentcommand.h" #include "apl/component/corecomponent.h" #include "apl/content/content.h" -#include "apl/engine/evaluate.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/arrayify.h" +#include "apl/engine/corerootcontext.h" +#include "apl/engine/evaluate.h" #include "apl/engine/event.h" -#include "apl/engine/rootcontext.h" -#include "apl/engine/keyboardmanager.h" #include "apl/focus/focusmanager.h" #include "apl/primitives/keyboard.h" #include "apl/time/sequencer.h" @@ -52,9 +54,11 @@ KeyboardManager::getHandlerPropertyKey(KeyHandlerType type) { } bool -KeyboardManager::handleKeyboard(KeyHandlerType type, const CoreComponentPtr& component, - const Keyboard& keyboard, const RootContextPtr& rootContext) { - LOG_IF(DEBUG_KEYBOARD_MANAGER).session(rootContext) << "type:" << type << ", keyboard:" << keyboard.toDebugString(); +KeyboardManager::handleKeyboard(KeyHandlerType type, + const CoreComponentPtr& component, + const Keyboard& keyboard, + const CoreRootContextPtr& rootContext) { + LOG_IF(DEBUG_KEYBOARD_MANAGER) << "type:" << type << ", keyboard:" << keyboard.toDebugString(); if (keyboard.isReservedKey()) { // do not process handlers when is key reserved for future use by APL @@ -68,7 +72,7 @@ KeyboardManager::handleKeyboard(KeyHandlerType type, const CoreComponentPtr& com while (!consumed && target) { consumed = target->processKeyPress(type, keyboard); if (consumed) { - LOG_IF(DEBUG_KEYBOARD_MANAGER).session(rootContext) << target->getUniqueId() << " " << type << " consumed."; + LOG_IF(DEBUG_KEYBOARD_MANAGER) << target->getUniqueId() << " " << type << " consumed."; } else { // propagate target = CoreComponent::cast(target->getParent()); @@ -77,26 +81,27 @@ KeyboardManager::handleKeyboard(KeyHandlerType type, const CoreComponentPtr& com // TODO:Having intrinsic handler does not really mean blocking from "document handling". Should split those. if (!consumed && !isIntrinsic) { - consumed = executeDocumentKeyHandlers(rootContext, type, keyboard); + consumed = executeDocumentKeyHandlers(CoreDocumentContext::cast(rootContext->topDocument()), + type, keyboard); } return consumed; } bool -KeyboardManager::executeDocumentKeyHandlers(const RootContextPtr& rootContext, +KeyboardManager::executeDocumentKeyHandlers(const CoreDocumentContextPtr& documentContext, KeyHandlerType type, const Keyboard& keyboard) { const auto& property = sComponentPropertyBimap.at(getHandlerPropertyKey(type)); auto handlerId = getHandlerId(type); - const auto& json = rootContext->content()->getDocument()->json(); + const auto& json = documentContext->content()->getDocument()->json(); auto handlers = arrayifyProperty(json, property.c_str()); if (handlers.empty()) return false; - ContextPtr eventContext = rootContext->createKeyboardDocumentContext(handlerId, keyboard.serialize()); + ContextPtr eventContext = documentContext->createKeyEventContext(handlerId, keyboard.serialize()); for (const auto& handler : handlers) { if (propertyAsBoolean(*eventContext, handler, "when", true)) { @@ -104,7 +109,6 @@ KeyboardManager::executeDocumentKeyHandlers(const RootContextPtr& rootContext, if (!commands.empty()) eventContext->sequencer().executeCommands(commands, eventContext, nullptr, false); - // NOTE: Checking for propagation at the document level is useless, except for debugging return !propertyAsBoolean(*eventContext, handler, "propagate", false); } } diff --git a/aplcore/src/engine/layoutmanager.cpp b/aplcore/src/engine/layoutmanager.cpp index a97ee98..6fada42 100644 --- a/aplcore/src/engine/layoutmanager.cpp +++ b/aplcore/src/engine/layoutmanager.cpp @@ -17,7 +17,8 @@ #include "apl/component/corecomponent.h" #include "apl/content/configurationchange.h" -#include "apl/engine/rootcontextdata.h" +#include "apl/document/coredocumentcontext.h" +#include "apl/engine/corerootcontext.h" #include "apl/livedata/layoutrebuilder.h" #include "apl/primitives/size.h" #include "apl/utils/tracing.h" @@ -35,9 +36,9 @@ yogaNodeDirtiedCallback(YGNodeRef node) component->getContext()->layoutManager().requestLayout(component->shared_from_corecomponent(), false); } -LayoutManager::LayoutManager(const apl::RootContextData& core) - : mCore(core), - mConfiguredSize(mCore.getSize()) +LayoutManager::LayoutManager(const CoreRootContext& coreRootContext, const Size& size) + : mRoot(coreRootContext), + mConfiguredSize(size) { } @@ -48,6 +49,12 @@ LayoutManager::terminate() mPendingLayout.clear(); } +void +LayoutManager::setSize(const Size& size) +{ + mConfiguredSize = size; +} + bool LayoutManager::needsLayout() const { @@ -60,32 +67,40 @@ LayoutManager::needsLayout() const void LayoutManager::firstLayout() { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << mTerminated; + LOG_IF(DEBUG_LAYOUT_MANAGER) << mTerminated; if (mTerminated) return; APL_TRACE_BLOCK("LayoutManager:firstLayout"); - assert(mCore.top()); - YGNodeSetDirtiedFunc(mCore.top()->getNode(), yogaNodeDirtiedCallback); - mPendingLayout.emplace(mCore.top()); + auto top = CoreComponent::cast(mRoot.topComponent()); + setAsTopNode(top); + mPendingLayout.emplace(top); layout(false, true); } void -LayoutManager::configChange(const ConfigurationChange& change) +LayoutManager::configChange(const ConfigurationChange& change, + const CoreDocumentContextPtr& document) { if (mTerminated) return; + auto top = CoreComponent::cast(document->topComponent()); + Size size = top->getLayoutSize(); + // Update the global size to match the configuration change - mConfiguredSize = change.mergeSize(mConfiguredSize) * mCore.getPxToDp(); + if (change.hasSizeChange()) { + size = change.getSize() * mRoot.getPxToDp(); + if (!document->isEmbedded()) { + mConfiguredSize = size; + } + } // If there is a size mismatch, schedule a layout - auto top = mCore.top(); - if (top && top->getLayoutSize() != mConfiguredSize) - mPendingLayout.emplace(top); + if (top && top->getLayoutSize() != size) + requestLayout(top, false); } static bool @@ -108,7 +123,7 @@ compareComponents(const CoreComponentPtr& a, const CoreComponentPtr& b) void LayoutManager::layout(bool useDirtyFlag, bool first) { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << mTerminated << " dirty_flag=" << useDirtyFlag; + LOG_IF(DEBUG_LAYOUT_MANAGER) << mTerminated << " dirty_flag=" << useDirtyFlag; if (mTerminated || mInLayout) return; @@ -119,7 +134,7 @@ LayoutManager::layout(bool useDirtyFlag, bool first) mInLayout = true; while (needsLayout()) { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << "Laying out " << mPendingLayout.size() << " component(s)"; + LOG_IF(DEBUG_LAYOUT_MANAGER) << "Laying out " << mPendingLayout.size() << " component(s)"; // Copy the pending components into a vector and sort them from top to bottom std::vector dirty(mPendingLayout.begin(), mPendingLayout.end()); @@ -138,8 +153,11 @@ LayoutManager::layout(bool useDirtyFlag, bool first) auto postProcess = mPostProcess; mPostProcess.clear(); - for (const auto& m : postProcess) - m.first.first->setProperty(m.first.second, m.second); + for (const auto& m : postProcess) { + auto component = m.first.first.lock(); + if (component) + component->setProperty(m.first.second, m.second); + } // After layout has completed we mark individual components as allowing event handlers for (const auto& m : laidOut) @@ -149,7 +167,7 @@ LayoutManager::layout(bool useDirtyFlag, bool first) void LayoutManager::flushLazyInflation() { - flushLazyInflationInternal(mCore.top()); + flushLazyInflationInternal(CoreComponent::cast(mRoot.topComponent())); } void @@ -166,17 +184,28 @@ LayoutManager::flushLazyInflationInternal(const CoreComponentPtr& comp) void LayoutManager::setAsTopNode(const CoreComponentPtr& component) { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString(); assert(component); + LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString(); YGNodeSetDirtiedFunc(component->getNode(), yogaNodeDirtiedCallback); } void LayoutManager::removeAsTopNode(const CoreComponentPtr& component) { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString(); assert(component); + LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString(); YGNodeSetDirtiedFunc(component->getNode(), nullptr); + + // Also remove from pending list + remove(component); +} + +bool +LayoutManager::isTopNode(const std::shared_ptr& component) const +{ + assert(component); + + return YGNodeGetDirtiedFunc(component->getNode()); } void @@ -185,26 +214,38 @@ LayoutManager::layoutComponent(const CoreComponentPtr& component, bool useDirtyF APL_TRACE_BLOCK("LayoutManager:layoutComponent"); auto parent = component->getParent(); - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << "component=" << component->toDebugSimpleString() + LOG_IF(DEBUG_LAYOUT_MANAGER) << "component=" << component->toDebugSimpleString() << " dirty_flag=" << useDirtyFlag << " parent=" << (parent ? parent->toDebugSimpleString() : "none"); - Size size = parent ? parent->getCalculated(kPropertyInnerBounds).get().getSize() : mConfiguredSize; - if (size.empty()) - return; + Size size; + float width, height; + + if (!parent) { // Top component + size = mConfiguredSize; + width = mRoot.getAutoWidth() ? YGUndefined : size.getWidth(); + height = mRoot.getAutoHeight() ? YGUndefined : size.getHeight(); + } else { + size = parent->getCalculated(kPropertyInnerBounds).get().getSize(); + if (size == Size()) + return; + width = size.getWidth(); + height = size.getHeight(); + } auto node = component->getNode(); // Layout the component if it has a dirty Yoga node OR if the cached size doesn't match the target size + // Note that the top-level component may get laid out multiple times if it auto sizes. if (YGNodeIsDirty(node) || size != component->getLayoutSize()) { component->preLayoutProcessing(useDirtyFlag); APL_TRACE_BEGIN("LayoutManager:YGNodeCalculateLayout"); - YGNodeCalculateLayout(node, size.getWidth(), size.getHeight(), component->getLayoutDirection()); + YGNodeCalculateLayout(node, width, height, component->getLayoutDirection()); APL_TRACE_END("LayoutManager:YGNodeCalculateLayout"); component->processLayoutChanges(useDirtyFlag, first); if (mNeedToReProcessLayoutChanges) { - // Previous call may have changed sizes for auto-sized components if any lazyness involved. Apply this changes. + // Previous call may have changed sizes for auto-sized components if any laziness involved. Apply this changes. component->processLayoutChanges(useDirtyFlag, first); mNeedToReProcessLayoutChanges = false; } @@ -218,13 +259,13 @@ LayoutManager::layoutComponent(const CoreComponentPtr& component, bool useDirtyF void LayoutManager::requestLayout(const CoreComponentPtr& component, bool force) { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString() << " force=" << force; + LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString() << " force=" << force; assert(component); if (mTerminated) return; - assert(YGNodeGetDirtiedFunc(component->getNode())); + assert(isTopNode(component)); mPendingLayout.emplace(component); if (force) component->setLayoutSize({}); @@ -258,7 +299,7 @@ LayoutManager::remove(const CoreComponentPtr& component) bool LayoutManager::ensure(const CoreComponentPtr& component) { - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString(); + LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString(); // Walk up the component hierarchy and ensure that Yoga nodes are correctly attached bool result = false; @@ -277,7 +318,7 @@ LayoutManager::ensure(const CoreComponentPtr& component) attachedYogaNodeNeedsLayout = false; } } else { // This child needs to be attached to its parent - LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << "Attaching yoga node from: " << child->toDebugSimpleString(); + LOG_IF(DEBUG_LAYOUT_MANAGER) << "Attaching yoga node from: " << child->toDebugSimpleString(); parent->attachYogaNode(child); attachedYogaNodeNeedsLayout = true; } @@ -301,4 +342,4 @@ LayoutManager::addPostProcess(const CoreComponentPtr& component, PropertyKey key mPostProcess[{component, key}] = value; } -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/engine/propdef.cpp b/aplcore/src/engine/propdef.cpp index c2c69fd..62801a8 100644 --- a/aplcore/src/engine/propdef.cpp +++ b/aplcore/src/engine/propdef.cpp @@ -34,7 +34,7 @@ namespace apl { Object asOldArray(const Context &context, const Object &object) { - auto result = evaluateRecursive(context, arrayify(context, object)); + auto result = evaluateNested(context, arrayify(context, object)); if (context.getRequestedAPLVersion() != "1.0") return result; @@ -156,7 +156,7 @@ Object asTransformOrArray(const Context& context, const Object& object) if (object.is()) return object; - return evaluateRecursive(context, arrayify(context, object)); + return evaluateNested(context, arrayify(context, object)); } Object asEasing(const Context& context, const Object& object) diff --git a/aplcore/src/engine/properties.cpp b/aplcore/src/engine/properties.cpp index 929796a..313d73b 100644 --- a/aplcore/src/engine/properties.cpp +++ b/aplcore/src/engine/properties.cpp @@ -20,8 +20,8 @@ #include "apl/content/rootconfig.h" #include "apl/datasource/datasource.h" #include "apl/engine/context.h" -#include "apl/engine/contextdependant.h" #include "apl/engine/evaluate.h" +#include "apl/engine/typeddependant.h" #include "apl/primitives/dimension.h" #include "apl/utils/identifier.h" #include "apl/utils/session.h" @@ -77,16 +77,6 @@ Properties::asNumber(const Context& context, const char *name, double defvalue) return evaluate(context, s->second).asNumber(); } -Dimension -Properties::asAbsoluteDimension(const Context& context, const char *name, double defvalue) -{ - auto s = mProperties.find(name); - if (s == mProperties.end()) - return Dimension(DimensionType::Absolute, defvalue); - - return evaluate(context, s->second).asAbsoluteDimension(context); -} - void Properties::emplace(const Object& item) { @@ -103,51 +93,54 @@ Properties::emplace(const Object& item) } void -Properties::addToContext(const ContextPtr &context, const Parameter ¶meter, bool userWriteable) -{ - Object tmp; - Object result; - auto bindingFunc = sBindingFunctions.at(parameter.type); - +Properties::addToContext(const ContextPtr &context, const Parameter ¶meter, bool userWriteable) { auto it = mProperties.find(parameter.name); - if (it != mProperties.end()) { - tmp = it->second; - mProperties.erase(it); // Remove the property from the list - } // Parameter names are only added to the data-binding context if they are valid identifiers. if (!isValidIdentifier(parameter.name)) { CONSOLE(context) << "Unable to add parameter '" << parameter.name << "' to context. Invalid identifier."; + if (it != mProperties.end()) mProperties.erase(it); // Remove it just in case return; } - // Extract as an optional node tree for dependant - tmp = tmp.isString() ? parseDataBinding(*context, tmp.getString()) : tmp; - auto value = evaluate(*context, tmp); - if (!value.isNull()) { - result = bindingFunc(*context, value); - - // If type explicitly specified we may want to "enrich" the object. - if (value.isMap() && value.has("type")) { - std::string type = value.get("type").getString(); - if (sBindingMap.has(type)) { - bindingFunc = sBindingFunctions.at(sBindingMap.at(type)); - result = bindingFunc(*context, value); - } else if (context->getRootConfig().isDataSource(type)) { - result = DataSource::create(context, value, parameter.name); + auto bindingFunc = sBindingFunctions.at(parameter.type); + Object result = bindingFunc(*context, evaluate(*context, parameter.defvalue)); + if (it != mProperties.end()) { + Object value = it->second; + mProperties.erase(it); // Remove the property from the list + + if (value.isString()) { + auto v = parseAndEvaluate(*context, value); + if (!v.symbols.empty()) { + ContextDependant::create(context, parameter.name, std::move(v.expression), context, + bindingFunc, std::move(v.symbols)); + } + + // We allow any (not only direct) binding to be "enriched". + value = v.value; + } + + if (!value.isNull()) { + result = bindingFunc(*context, value); + // If type explicitly specified we may want to "enrich" the object. + if (value.isMap() && value.has("type")) { + std::string type = value.get("type").getString(); + if (sBindingMap.has(type)) { + bindingFunc = sBindingFunctions.at(sBindingMap.at(type)); + result = bindingFunc(*context, value); + } + else if (auto provider = context->getRootConfig().getDataSourceProvider(type)) { + result = DataSource::create(context, provider, value, parameter.name); + } } } - } else { - result = bindingFunc(*context, evaluate(*context, parameter.defvalue)); } if (userWriteable) { context->putUserWriteable(parameter.name, result); - // Even if not valid at this point it could become one - if (tmp.isEvaluable()) - ContextDependant::create(context, parameter.name, tmp, context, bindingFunc); - } else { + } + else { context->putConstant(parameter.name, result); } } diff --git a/aplcore/src/engine/rootcontext.cpp b/aplcore/src/engine/rootcontext.cpp index 6a327ea..398ee61 100644 --- a/aplcore/src/engine/rootcontext.cpp +++ b/aplcore/src/engine/rootcontext.cpp @@ -13,54 +13,10 @@ * permissions and limitations under the License. */ -#include "apl/engine/rootcontext.h" - -#include - -#include "rapidjson/stringbuffer.h" - -#include "apl/action/scrolltoaction.h" -#include "apl/command/arraycommand.h" -#include "apl/command/configchangecommand.h" -#include "apl/command/displaystatechangecommand.h" -#include "apl/command/documentcommand.h" -#include "apl/component/yogaproperties.h" -#include "apl/content/configurationchange.h" -#include "apl/content/content.h" -#include "apl/content/metrics.h" -#include "apl/content/rootconfig.h" -#include "apl/datasource/datasource.h" -#include "apl/datasource/datasourceprovider.h" -#include "apl/engine/builder.h" -#include "apl/engine/queueeventmanager.h" -#include "apl/engine/resources.h" -#include "apl/engine/rootcontextdata.h" -#include "apl/engine/styles.h" -#include "apl/engine/uidmanager.h" -#include "apl/extension/extensionmanager.h" -#include "apl/graphic/graphic.h" -#include "apl/livedata/livedataobject.h" -#include "apl/livedata/livedataobjectwatcher.h" -#include "apl/time/timemanager.h" -#include "apl/utils/log.h" -#include "apl/utils/tracing.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/builder.h" -#include "apl/scenegraph/scenegraph.h" -#endif // SCENEGRAPH - +#include "apl/engine/corerootcontext.h" namespace apl { -static const char *DISPLAY_STATE = "displayState"; -static const char *ELAPSED_TIME = "elapsedTime"; -static const char *LOCAL_TIME = "localTime"; -static const char *UTC_TIME = "utcTime"; -static const char *ON_MOUNT_HANDLER_NAME = "Mount"; - -static const std::string MOUNT_SEQUENCER = "__MOUNT_SEQUENCER"; -static const std::string SCROLL_TO_RECT_SEQUENCER = "__SCROLL_TO_RECT_SEQUENCE"; - RootContextPtr RootContext::create(const Metrics& metrics, const ContentPtr& content) { @@ -74,843 +30,14 @@ RootContext::create(const Metrics& metrics, const ContentPtr& content, const Roo } RootContextPtr -RootContext::create(const Metrics& metrics, const ContentPtr& content, - const RootConfig& config, std::function callback) -{ - return create(metrics, content, config, callback, std::make_shared()); -} - -RootContextPtr -RootContext::create(const Metrics& metrics, const ContentPtr& content, - const RootConfig& config, std::function callback, - const EventManagerPtr& eventManager) -{ - if (!content->isReady()) { - LOG(LogLevel::kError).session(content->getSession()) << "Attempting to create root context with illegal content"; - return nullptr; - } - - auto root = std::make_shared(metrics, content, config, eventManager); - if (callback) - callback(root); - if (!root->setup(nullptr)) - return nullptr; - - return root; -} - -RootContext::RootContext(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config) - : RootContext(metrics, content, config, std::make_shared()) -{} - -RootContext::RootContext(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config, - const EventManagerPtr& eventManager) - : mContent(content), - mTimeManager(config.getTimeManager()), - mDisplayState(static_cast(config.getProperty(RootProperty::kInitialDisplayState).getInteger())) -{ - init(metrics, config, false, eventManager); -} - -RootContext::~RootContext() { - assert(mCore); - mCore->terminate(); - mCore->dirtyVisualContext.clear(); - mTimeManager->terminate(); - clearDirty(); -} - -void -RootContext::configurationChange(const ConfigurationChange& change) -{ - // If we're in the middle of a configuration change, drop it - mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); - - mActiveConfigurationChanges.mergeConfigurationChange(change); - if (mActiveConfigurationChanges.empty()) - return; - - auto cmd = ConfigChangeCommand::create(shared_from_this(), - mActiveConfigurationChanges.asEventProperties(mCore->mConfig, - mCore->mMetrics)); - mContext->sequencer().executeOnSequencer(cmd, ConfigChangeCommand::SEQUENCER); -} - -void -RootContext::updateDisplayState(DisplayState displayState) -{ - if (!sDisplayStateMap.has(displayState)) { - LOG(LogLevel::kWarn).session(getSession()) << "View specified an invalid display state, ignoring it"; - return; - } - - if (displayState == mDisplayState) { - return; - } - - // If we're in the middle of a display state change, drop it - mCore->sequencer().terminateSequencer(DisplayStateChangeCommand::SEQUENCER); - - mDisplayState = displayState; - - const std::string displayStateString = sDisplayStateMap.at(displayState); - mContext->systemUpdateAndRecalculate(DISPLAY_STATE, displayStateString, true); - - ObjectMap properties; - properties.emplace("displayState", displayStateString); - - auto cmd = DisplayStateChangeCommand::create(shared_from_this(), std::move(properties)); - mContext->sequencer().executeOnSequencer(cmd, DisplayStateChangeCommand::SEQUENCER); - -#ifdef ALEXAEXTENSIONS - auto mediator = getRootConfig().getExtensionMediator(); - if (mediator) { - mediator->onDisplayStateChanged(mDisplayState); - } -#endif -} - -void -RootContext::reinflate() -{ - // The basic algorithm is to simply re-build core and re-inflate the component hierarchy. - // TODO: Re-use parts of the hierarchy and to maintain state during reinflation. - - // Release any "onConfigChange" action - mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); -#ifdef SCENEGRAPH - // Release the existing scene graph - mSceneGraph = nullptr; -#endif // SCENEGRAPH - - auto metrics = mActiveConfigurationChanges.mergeMetrics(mCore->mMetrics); - auto config = mActiveConfigurationChanges.mergeRootConfig(mCore->mConfig); - - // Update the configuration with the current UTC time and time adjustment - config.set(RootProperty::kUTCTime, mUTCTime); - config.set(RootProperty::kLocalTimeAdjustment, mLocalTimeAdjustment); - - auto preservedSequencers = std::map(); - for (auto& stp : mCore->sequencer().getSequencersToPreserve()) { - preservedSequencers.emplace(stp, mCore->sequencer().detachSequencer(stp)); - } - - // Stop any execution on the old core - auto oldTop = mCore->halt(); - // Ensure that nothing is pending. - assert(mTimeManager->size() == 0); - - // The initialization routine replaces mCore with a new core - init(metrics, config, true, std::make_shared()); - setup(oldTop); // Pass the old top component - - // If there was a previous top component, release it and its children to free memory - if (oldTop) - oldTop->release(); - - // Clear the old active configuration; it is reset on a reinflation - mActiveConfigurationChanges.clear(); - - for (auto& ps : preservedSequencers) { - if(!mCore->sequencer().reattachSequencer(ps.first, ps.second, *this)) { - CONSOLE(getSession()) << "Can't preserve sequencer: " << ps.first; - } - } -} - -void -RootContext::resize() -{ - // Release any "onConfigChange" action - mCore->sequencer().terminateSequencer(ConfigChangeCommand::SEQUENCER); - mCore->layoutManager().configChange(mActiveConfigurationChanges); - // Note: we do not clear the configuration changes - there may be a reinflate() coming in the future. -} - - -void -RootContext::init(const Metrics& metrics, - const RootConfig& config, - bool reinflation, - const EventManagerPtr& eventManager) -{ - APL_TRACE_BLOCK("RootContext:init"); - std::string theme = metrics.getTheme(); - const auto& json = mContent->getDocument()->json(); - auto themeIter = json.FindMember("theme"); - if (themeIter != json.MemberEnd() && themeIter->value.IsString()) - theme = themeIter->value.GetString(); - - auto session = config.getSession(); - if (!session) - session = mContent->getSession(); - - mCore = std::make_shared(metrics, - config, - RuntimeState(theme, - mContent->getDocument()->version(), - reinflation), - mContent->getDocumentSettings(), - session, - mContent->mExtensionRequests, - eventManager); - - auto env = mContent->getEnvironment(config); - mCore->lang(env.language) - .layoutDirection(env.layoutDirection); - - mContext = Context::createRootEvaluationContext(metrics, mCore); - - mContext->putSystemWriteable(ELAPSED_TIME, mTimeManager->currentTime()); - - mContext->putSystemWriteable(DISPLAY_STATE, sDisplayStateMap.at(mDisplayState)); - - mUTCTime = config.getUTCTime(); - mLocalTimeAdjustment = config.getLocalTimeAdjustment(); - mContext->putSystemWriteable(UTC_TIME, mUTCTime); - mContext->putSystemWriteable(LOCAL_TIME, mUTCTime + mLocalTimeAdjustment); - - // Insert one LiveArrayObject or LiveMapObject into the top-level context for each defined LiveObject - for (const auto& m : config.getLiveObjectMap()) { - auto ldo = LiveDataObject::create(m.second, mContext, m.first); - auto watchers = config.getLiveDataWatchers(m.first); - for (auto& watcher : watchers) { - if (watcher) - watcher->registerObjectWatcher(ldo); - } - } -} - -void -RootContext::clearPending() const -{ - clearPendingInternal(false); -} - -void -RootContext::clearPendingInternal(bool first) const -{ - assert(mCore); - - APL_TRACE_BLOCK("RootContext:clearPending"); - // Flush any dynamic data changes - mCore->dataManager().flushDirty(); - - // Make sure any pending events have executed - mTimeManager->runPending(); - - // If we need a layout pass, do it now - it will update the dirty events - if (mCore->layoutManager().needsLayout()) - mCore->layoutManager().layout(true, first); - - mCore->mediaManager().processMediaRequests(mContext); - - // Run any onMount handlers for something that may have been attached at runtime - // We execute those on the sequencer to avoid messing stuff up. Will work much more similarly to previous behavior, - // but will not interrupt something that may have been scheduled just before. - auto& onMounts = mCore->pendingOnMounts(); - if (!onMounts.empty()) { - const auto& tm = getRootConfig().getTimeManager(); - std::vector parallelCommands; - for (auto& pendingOnMount : onMounts) { - if (auto comp = pendingOnMount.lock()) { - auto commands = comp->getCalculated(kPropertyOnMount); - auto ctx = comp->createDefaultEventContext(ON_MOUNT_HANDLER_NAME); - parallelCommands.emplace_back( - ArrayCommand::create( - ctx, - commands, - comp, - Properties(), - "")->execute(tm, false)); - } - } - onMounts.clear(); - - auto mountAction = Action::makeAll(tm, parallelCommands); - mCore->sequencer().attachToSequencer(mountAction, MOUNT_SEQUENCER); - } - -#ifdef ALEXAEXTENSIONS - // Process any extension events. There are no need to expose those externally. - auto extensionMediator = mCore->rootConfig().getExtensionMediator(); - if (extensionMediator) { - while (!mCore->extesnionEvents.empty()) { - Event event = mCore->extesnionEvents.front(); - mCore->extesnionEvents.pop(); - extensionMediator->invokeCommand(event); - } - } -#endif -} - -bool -RootContext::hasEvent() const -{ - assert(mCore); - clearPending(); - return !mCore->events->empty(); -} - -Event -RootContext::popEvent() -{ - assert(mCore); - clearPending(); - - if (!mCore->events->empty()) { - Event event = mCore->events->front(); - mCore->events->pop(); - return event; - } - - // This should never be reached. - LOG(LogLevel::kError).session(getSession()) << "No events available"; - std::exit(EXIT_FAILURE); -} - -bool -RootContext::isDirty() const -{ - assert(mCore); - clearPending(); - return !mCore->dirty.empty(); -} - - -const std::set& -RootContext::getDirty() -{ - assert(mCore); - clearPending(); - return mCore->dirty; -} - -void -RootContext::clearDirty() -{ - assert(mCore); - APL_TRACE_BLOCK("RootContext:clearDirty"); - for (auto& component : mCore->dirty) - component->clearDirty(); - - mCore->dirty.clear(); -} - - -bool -RootContext::isVisualContextDirty() const -{ - assert(mCore); - return !mCore->dirtyVisualContext.empty(); -} - -void -RootContext::clearVisualContextDirty() -{ - assert(mCore); - mCore->dirtyVisualContext.clear(); -} - -rapidjson::Value -RootContext::serializeVisualContext(rapidjson::Document::AllocatorType& allocator) -{ - clearVisualContextDirty(); - return mCore->mTop->serializeVisualContext(allocator); -} - -bool -RootContext::isDataSourceContextDirty() const -{ - assert(mCore); - return !mCore->dirtyDatasourceContext.empty(); -} - -void -RootContext::clearDataSourceContextDirty() -{ - assert(mCore); - mCore->dirtyDatasourceContext.clear(); -} - -rapidjson::Value -RootContext::serializeDataSourceContext(rapidjson::Document::AllocatorType& allocator) -{ - clearDataSourceContextDirty(); - - rapidjson::Value outArray(rapidjson::kArrayType); - - for (const auto& tracker : mCore->dataManager().trackers()) { - if (auto sourceConnection = tracker->getDataSourceConnection()) { - rapidjson::Value datasource(rapidjson::kObjectType); - sourceConnection->serialize(datasource, allocator); - - outArray.PushBack(datasource.Move(), allocator); - } - } - return outArray; -} - -rapidjson::Value -RootContext::serializeDOM(bool extended, rapidjson::Document::AllocatorType& allocator) -{ - if (extended) - return mCore->mTop->serializeAll(allocator); - return mCore->mTop->serialize(allocator); -} - -rapidjson::Value -RootContext::serializeContext(rapidjson::Document::AllocatorType& allocator) -{ - return mContext->serialize(allocator); -} - - -std::shared_ptr -RootContext::createDocumentEventProperties(const std::string& handler) const -{ - auto source = std::make_shared(); - source->emplace("source", "Document"); - source->emplace("type", "Document"); - source->emplace("handler", handler); - source->emplace("id", Object::NULL_OBJECT()); - source->emplace("uid", Object::NULL_OBJECT()); - source->emplace("value", Object::NULL_OBJECT()); - auto event = std::make_shared(); - event->emplace("source", source); - return event; -} - -ContextPtr -RootContext::createDocumentContext(const std::string& handler, const ObjectMap& optional) -{ - ContextPtr ctx = Context::createFromParent(payloadContext()); - auto event = createDocumentEventProperties(handler); - for (const auto& m : optional) - event->emplace(m.first, m.second); - ctx->putConstant("event", event); - return ctx; -} - - -ContextPtr -RootContext::createKeyboardDocumentContext(const std::string& handler, const ObjectMapPtr& keyboard) -{ - ContextPtr ctx = Context::createFromParent(payloadContext()); - auto event = createDocumentEventProperties(handler); - event->emplace("keyboard", keyboard); - ctx->putConstant("event", event); - return ctx; -} - -ActionPtr -RootContext::executeCommands(const apl::Object& commands, bool fastMode) -{ - ContextPtr ctx = createDocumentContext("External"); - return mContext->sequencer().executeCommands(commands, ctx, nullptr, fastMode); -} - -ActionPtr -RootContext::invokeExtensionEventHandler(const std::string& uri, const std::string& name, - const ObjectMap& data, bool fastMode, - std::string resourceId) -{ - auto handlerDefinition = ExtensionEventHandler{uri, name}; - auto handler = Object::NULL_OBJECT(); - ContextPtr ctx = nullptr; - auto comp = mCore->extensionManager().findExtensionComponent(resourceId); - if (comp) { - handler = comp->findHandler(handlerDefinition); - if (handler.isNull()) { - CONSOLE(getSession()) << "Extension Component " << comp->name() - << " can't execute event handler " << handlerDefinition.getName(); - return nullptr; - } - - // Create component context. Data is attached on event level. - auto dataPtr = std::make_shared(data); - ctx = comp->createEventContext(name, dataPtr); - } else { - handler = mCore->extensionManager().findHandler(handlerDefinition); - if (handler.isNull()) { - CONSOLE(getSession()) << "Extension Handler " << handlerDefinition.getName() << " don't exist."; - return nullptr; - } - - // Create a document-level context. Data is attached on event level. - ctx = createDocumentContext(name, data); - } - - for (const auto& m : data) - ctx->putConstant(m.first, m.second); - - return mContext->sequencer().executeCommands(handler, ctx, comp, fastMode); -} - -void -RootContext::cancelExecution() -{ - assert(mCore); - mCore->sequencer().reset(); -} - -ComponentPtr -RootContext::topComponent() -{ - return mCore->mTop; -} - -ContextPtr -RootContext::payloadContext() const -{ - // We could cache the payload context, but it is infrequently used. Instead we search upwards from the - // top components context until we find the context right before the top-level context. - if (!mCore || !mCore->mTop) - return mContext; - - auto context = mCore->mTop->getContext(); - if (context == nullptr || context == mContext) - return mContext; - - while (context->parent() != mContext) - context = context->parent(); - - return context; -} - -void -RootContext::updateTimeInternal(apl_time_t elapsedTime, apl_time_t utcTime) -{ - APL_TRACE_BLOCK("RootContext:updateTime"); - auto lastTime = mTimeManager->currentTime(); - - APL_TRACE_BEGIN("RootContext:flushDirtyData"); - // Flush any dynamic data changes - mCore->dataManager().flushDirty(); - APL_TRACE_END("RootContext:flushDirtyData"); - - APL_TRACE_BEGIN("RootContext:timeManagerUpdateTime"); - mTimeManager->updateTime(elapsedTime); - APL_TRACE_END("RootContext:timeManagerUpdateTime"); - - APL_TRACE_BEGIN("RootContext:systemUpdateAndRecalculateElapsed"); - mContext->systemUpdateAndRecalculate(ELAPSED_TIME, mTimeManager->currentTime(), true); // Read back in case it gets changed - APL_TRACE_END("RootContext:systemUpdateAndRecalculateElapsed"); - - if (utcTime > 0) { - mUTCTime = utcTime; - } else { - // Update the local time by how much time passed on the "elapsed" timer - mUTCTime += mTimeManager->currentTime() - lastTime; - } - - APL_TRACE_BEGIN("RootContext:systemUpdateAndRecalculateTime"); - mContext->systemUpdateAndRecalculate(UTC_TIME, mUTCTime, true); - mContext->systemUpdateAndRecalculate(LOCAL_TIME, mUTCTime + mLocalTimeAdjustment, true); - APL_TRACE_END("RootContext:systemUpdateAndRecalculateTime"); - - APL_TRACE_BEGIN("RootContext:pointerHandleTimeUpdate"); - mCore->pointerManager().handleTimeUpdate(elapsedTime); - APL_TRACE_END("RootContext:pointerHandleTimeUpdate"); -} - -void -RootContext::updateTime(apl_time_t elapsedTime) -{ - updateTimeInternal(elapsedTime, 0); -} - -void -RootContext::updateTime(apl_time_t elapsedTime, apl_time_t utcTime) -{ - updateTimeInternal(elapsedTime, utcTime); -} - -void -RootContext::scrollToRectInComponent(const ComponentPtr& component, const Rect &bounds, CommandScrollAlign align) -{ - auto scrollToAction = ScrollToAction::make( - mTimeManager, align, bounds, mContext, CoreComponent::cast(component)); - if (scrollToAction && scrollToAction->isPending()) { - mCore->sequencer().attachToSequencer(scrollToAction, SCROLL_TO_RECT_SEQUENCER); - } -} - - -apl_time_t -RootContext::nextTime() -{ - return mTimeManager->nextTimeout(); -} - -apl_time_t -RootContext::currentTime() -{ - return mTimeManager->currentTime(); -} - -bool -RootContext::screenLock() -{ - assert(mCore); - clearPending(); - return mCore->screenLock(); -} - -/** - * @deprecated Use Content->getDocumentSettings() - */ -const Settings& -RootContext::settings() -{ - return *(mCore->mSettings); -} - -const RootConfig& -RootContext::rootConfig() -{ - return mCore->rootConfig(); -} - -bool -RootContext::setup(const CoreComponentPtr& top) -{ - APL_TRACE_BLOCK("RootContext:setup"); - std::vector ordered = mContent->ordered(); - - // check type field of each package - auto enforceTypeField = mCore->rootConfig().getEnforceTypeField(); - if(!verifyTypeField(ordered, enforceTypeField)) { - return false; - } - - auto supportedVersions = mCore->rootConfig().getEnforcedAPLVersion(); - if(!verifyAPLVersionCompatibility(ordered, supportedVersions)) { - return false; - } - - bool trackProvenance = mCore->rootConfig().getTrackProvenance(); - - // Read settings - // Deprecated, get settings from Content->getDocumentSettings() - { - APL_TRACE_BEGIN("RootContext:readSettings"); - mCore->mSettings->read(mCore->rootConfig()); - APL_TRACE_END("RootContext:readSettings"); - } - - // Resource processing: - APL_TRACE_BEGIN("RootContext:processResources"); - for (const auto& child : ordered) { - const auto& json = child->json(); - const auto path = Path(trackProvenance ? child->name() : std::string()); - addNamedResourcesBlock(*mContext, json, path, "resources"); - } - APL_TRACE_END("RootContext:processResources"); - - // Style processing - APL_TRACE_BEGIN("RootContext:processStyles"); - for (const auto& child : ordered) { - const auto& json = child->json(); - const auto path = Path(trackProvenance ? child->name() : std::string()); - - auto styleIter = json.FindMember("styles"); - if (styleIter != json.MemberEnd() && styleIter->value.IsObject()) - mCore->styles()->addStyleDefinitions(mCore->session(), &styleIter->value, path.addObject("styles")); - } - APL_TRACE_END("RootContext:processStyles"); - - // Layout processing - APL_TRACE_BEGIN("RootContext:processLayouts"); - for (const auto& child : ordered) { - const auto& json = child->json(); - const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("layouts"); - - auto layoutIter = json.FindMember("layouts"); - if (layoutIter != json.MemberEnd() && layoutIter->value.IsObject()) { - for (const auto& kv : layoutIter->value.GetObject()) { - const auto& name = kv.name.GetString(); - mCore->mLayouts[name] = { &kv.value, path.addObject(name) }; - } - } - } - APL_TRACE_END("RootContext:processLayouts"); - - // Command processing - APL_TRACE_BEGIN("RootContext:processCommands"); - for (const auto& child : ordered) { - const auto& json = child->json(); - const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("commands"); - - auto commandIter = json.FindMember("commands"); - if (commandIter != json.MemberEnd() && commandIter->value.IsObject()) { - for (const auto& kv : commandIter->value.GetObject()) { - const auto& name = kv.name.GetString(); - mCore->mCommands[name] = { &kv.value, path.addObject(name) }; - } - } - } - APL_TRACE_END("RootContext:processCommands"); - - // Graphics processing - APL_TRACE_BEGIN("RootContext:processGraphics"); - for (const auto& child : ordered) { - const auto& json = child->json(); - const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("graphics"); - - auto graphicsIter = json.FindMember("graphics"); - if (graphicsIter != json.MemberEnd() && graphicsIter->value.IsObject()) { - for (const auto& kv : graphicsIter->value.GetObject()) { - const auto& name = kv.name.GetString(); - mCore->mGraphics[name] = { &kv.value, path.addObject(name)}; - } - } - } - APL_TRACE_END("RootContext:processGraphics"); - - // Identify all registered event handlers in all ordered documents - APL_TRACE_BEGIN("RootContext:processExtensionHandlers"); - auto& em = mCore->extensionManager(); - for (const auto& handler : em.qualifiedHandlerMap()) { - for (const auto& child : ordered) { - const auto& json = child->json(); - auto h = json.FindMember(handler.first.c_str()); - if (h != json.MemberEnd()) { - auto oldHandler = em.findHandler(handler.second); - if (!oldHandler.isNull()) - CONSOLE(mContext) << "Overwriting existing command handler " << handler.first; - em.addEventHandler(handler.second, asCommand(*mContext, evaluate(*mContext, h->value))); - } - } - } - APL_TRACE_END("RootContext:processExtensionHandlers"); - - // Inflate the top component - Properties properties; - - APL_TRACE_BEGIN("RootContext:retrieveProperties"); - mContent->getMainProperties(properties); - APL_TRACE_END("RootContext:retrieveProperties"); - - mCore->mTop = Builder(top).inflate(mContext, properties, mContent->getMainTemplate()); - - if (!mCore->mTop) - return false; - - mCore->mTop->markGlobalToLocalTransformStale(); - mCore->layoutManager().firstLayout(); - -#ifdef ALEXAEXTENSIONS - // Bind to the extension mediator - // TODO ExtensionMediator is an experimental class facilitating message passing to and from extensions. - // TODO The mediator class will be replaced by direct messaging between extensions and ExtensionManager - auto extensionMediator = mCore->rootConfig().getExtensionMediator(); - if (extensionMediator) { - extensionMediator->bindContext(shared_from_this()); - } -#endif - - // Execute the "onMount" document command - APL_TRACE_BEGIN("RootContext:executeOnMount"); - auto cmd = DocumentCommand::create(kPropertyOnMount, ON_MOUNT_HANDLER_NAME, shared_from_this()); - mContext->sequencer().execute(cmd, false); - // Clear any pending mounts as we just executed those - mCore->pendingOnMounts().clear(); - APL_TRACE_END("RootContext:executeOnMount"); - - // A bunch of commands may be queued up at the start time. Clear those out. - clearPendingInternal(true); - - // Those commands may have set the dirty flags. Clear them. - clearDirty(); - - // Commands or layout may have marked visual context dirty. Clear visual context. - mCore->dirtyVisualContext.clear(); - - // Process and schedule tick handlers. - processTickHandlers(); - - return true; -} - -void -RootContext::scheduleTickHandler(const Object& handler, double delay) -{ - auto weak_ptr = std::weak_ptr(shared_from_this()); - - // Lambda capture takes care of handler here as it's a copy. - mTimeManager->setTimeout([weak_ptr, handler, delay]() { - auto self = weak_ptr.lock(); - if (!self) - return; - - auto ctx = self->createDocumentContext("Tick"); - if (propertyAsBoolean(*ctx, handler, "when", true)) { - auto commands = Object(arrayifyProperty(*ctx, handler, "commands")); - if (!commands.empty()) - self->context().sequencer().executeCommands(commands, ctx, nullptr, true); - } - - self->scheduleTickHandler(handler, delay); - - }, delay); -} - -void -RootContext::processTickHandlers() +RootContext::create(const Metrics& metrics, + const ContentPtr& content, + const RootConfig& config, + std::function callback) { - auto& json = content()->getDocument()->json(); - auto it = json.FindMember("handleTick"); - if (it == json.MemberEnd()) - return; - - auto tickHandlers = asArray(*mContext, evaluate(*mContext, it->value)); - - if (tickHandlers.empty() || !tickHandlers.isArray()) - return; - - for (const auto& handler : tickHandlers.getArray()) { - auto delay = std::max(propertyAsDouble(*mContext, handler, "minimumDelay", 1000), - mCore->rootConfig().getTickHandlerUpdateLimit()); - scheduleTickHandler(handler, delay); - } -} - -bool -RootContext::verifyAPLVersionCompatibility(const std::vector& ordered, - const APLVersion& compatibilityVersion) -{ - for(const auto& child : ordered) { - if(!compatibilityVersion.isValid(child->version())) { - CONSOLE(mContext) << child->name() << " has invalid version: " << child->version(); - return false; - } - } - return true; + return CoreRootContext::create(metrics, content, config, callback); } -bool -RootContext::verifyTypeField(const std::vector>& ordered, bool enforce) -{ - for(auto& child : ordered) { - auto type = child->type(); - if (type.compare("APML") == 0) CONSOLE(mContext) - << child->name() << ": Stop using the APML document format!"; - else if (type.compare("APL") != 0) { - CONSOLE(mContext) << child->name() << ": Document type field should be \"APL\"!"; - if(enforce) { - return false; - } - } - } - return true; -} - - streamer& operator<<(streamer& os, const RootContext& root) { @@ -918,183 +45,4 @@ operator<<(streamer& os, const RootContext& root) return os; } - -/* Remove when migrated to handlePointerEvent */ -void -RootContext::updateCursorPosition(Point cursorPosition) -{ - handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, cursorPosition)); -} - -bool -RootContext::handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) -{ - - assert(mCore); - auto &km = mCore->keyboardManager(); - auto &fm = mCore->focusManager(); - return km.handleKeyboard(type, fm.getFocus(), keyboard, shared_from_this()); -} - -const SessionPtr& -RootContext::getSession() const -{ - return mCore->session(); -} - -bool -RootContext::handlePointerEvent(const PointerEvent& pointerEvent) -{ - assert(mCore); - return mCore->pointerManager().handlePointerEvent(pointerEvent, mTimeManager->currentTime()); -} - -const RootConfig& -RootContext::getRootConfig() const -{ - assert(mCore); - return mCore->rootConfig(); -} - -std::string -RootContext::getTheme() const -{ - assert(mCore); - return mCore->getTheme(); -} - -const TextMeasurementPtr& -RootContext::measure() const -{ - return mCore->measure(); -} - -ComponentPtr -RootContext::findComponentById(const std::string& id) const -{ - assert(mCore); - - // Fast path search for uid value - auto *ptr = findByUniqueId(id); - if (ptr && ptr->objectType() == UIDObject::UIDObjectType::COMPONENT) - return static_cast(ptr)->shared_from_this(); - - // Depth-first search - auto top = mCore->top(); - return top ? top->findComponentById(id) : nullptr; -} - -UIDObject * -RootContext::findByUniqueId(const std::string& uid) const -{ - assert(mCore); - return mCore->uniqueIdManager().find(uid); -} - -std::map -RootContext::getFocusableAreas() -{ - assert(mCore); - return mCore->focusManager().getFocusableAreas(); -} - -bool -RootContext::setFocus(FocusDirection direction, const Rect& origin, const std::string& targetId) -{ - assert(mCore); - auto top = mCore->top(); - auto target = CoreComponent::cast(findComponentById(targetId)); - - if (!target) { - LOG(LogLevel::kWarn).session(getSession()) << "Don't have component: " << targetId; - return false; - } - - Rect targetRect; - target->getBoundsInParent(top, targetRect); - - // Shift origin into target's coordinate space. - auto offsetFocusRect = origin; - offsetFocusRect.offset(-targetRect.getTopLeft()); - - return mCore->focusManager().focus(direction, offsetFocusRect, target); -} - -bool -RootContext::nextFocus(FocusDirection direction, const Rect& origin) -{ - assert(mCore); - return mCore->focusManager().focus(direction, origin); -} - -bool -RootContext::nextFocus(FocusDirection direction) -{ - assert(mCore); - return mCore->focusManager().focus(direction); -} - -void -RootContext::clearFocus() -{ - assert(mCore); - mCore->focusManager().clearFocus(false); -} - -std::string -RootContext::getFocused() -{ - assert(mCore); - auto focused = mCore->focusManager().getFocus(); - return focused ? focused->getUniqueId() : ""; -} - -void -RootContext::mediaLoaded(const std::string& source) -{ - assert(mCore); - mCore->mediaManager().mediaLoadComplete(source, true, -1, std::string()); -} - -void -RootContext::mediaLoadFailed(const std::string& source, int errorCode, const std::string& error) -{ - assert(mCore); - mCore->mediaManager().mediaLoadComplete(source, false, errorCode, error); -} - -#ifdef SCENEGRAPH -/* - * If it does exist, we clean out any dirty markings for the scene graph, then walk - * the list of dirty components and update the scene graph. If the scene graph does not exist, - * we create a new one. - */ -sg::SceneGraphPtr -RootContext::getSceneGraph() -{ - assert(mCore); - - // If we need a layout pass, do it now - this avoids screen flicker when a Text component - // with "auto" width has a new, longer content but a full layout has not yet executed. - if (mCore->layoutManager().needsLayout()) - mCore->layoutManager().layout(true, false); - - if (!mSceneGraph) - mSceneGraph = sg::SceneGraph::create(); - - if (mSceneGraph->getLayer()) { - mSceneGraph->updates().clear(); - for (auto& component : mCore->dirty) - CoreComponent::cast(component)->updateSceneGraph(mSceneGraph->updates()); - } else { - auto top = mCore->top(); - if (top) - mSceneGraph->setLayer(top->getSceneGraph(mSceneGraph->updates())); - } - - mSceneGraph->updates().fixCreatedFlags(); - clearDirty(); - return mSceneGraph; -} -#endif // SCENEGRAPH } // namespace apl diff --git a/aplcore/src/engine/rootcontextdata.cpp b/aplcore/src/engine/rootcontextdata.cpp deleted file mode 100644 index 5f71eb8..0000000 --- a/aplcore/src/engine/rootcontextdata.cpp +++ /dev/null @@ -1,151 +0,0 @@ -/** - * Copyright 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 "apl/engine/rootcontextdata.h" - -#include "apl/component/corecomponent.h" -#include "apl/component/textmeasurement.h" -#include "apl/content/metrics.h" -#include "apl/engine/queueeventmanager.h" -#include "apl/content/rootconfig.h" -#include "apl/engine/keyboardmanager.h" -#include "apl/engine/styles.h" -#include "apl/engine/uidmanager.h" -#include "apl/focus/focusmanager.h" -#include "apl/livedata/livedatamanager.h" -#include "apl/time/timemanager.h" - -#ifdef SCENEGRAPH -#include "apl/scenegraph/textpropertiescache.h" -#endif // SCENEGRAPH - -namespace apl { - -static const bool DEBUG_YG_PRINT_TREE = false; - -static LogLevel -ygLevelToDebugLevel(YGLogLevel level) -{ - switch (level) { - case YGLogLevelError: return LogLevel::kError; - case YGLogLevelWarn: return LogLevel::kWarn; - case YGLogLevelInfo: return LogLevel::kInfo; - case YGLogLevelDebug: return LogLevel::kDebug; - case YGLogLevelVerbose: return LogLevel::kTrace; - case YGLogLevelFatal: return LogLevel::kCritical; - } - return LogLevel::kDebug; -} - -static int -ygLogger(const YGConfigRef config, - const YGNodeRef node, - YGLogLevel level, - const char *format, - va_list args) -{ - va_list args_copy; - va_copy(args_copy, args); - std::vector buf(1 + std::vsnprintf(nullptr, 0, format, args_copy)); - va_end(args_copy); - - std::vsnprintf(buf.data(), buf.size(), format, args); - va_end(args); - - LOG(ygLevelToDebugLevel(level)) << buf.data(); - return 1; // Does this matter? -} - -/** - * Construct the root context from metrics. - * - * Internally we create a sequencer, a Yoga/Flexbox configuration, - * and a copy of the currently installed TextMeasurement utility. - * - * @param metrics The display metrics. - */ -RootContextData::RootContextData(const Metrics& metrics, - const RootConfig& config, - RuntimeState runtimeState, - const SettingsPtr& settings, - const SessionPtr& session, - const std::vector>& extensions, - const EventManagerPtr& eventManager) - : events(eventManager), - mRuntimeState(std::move(runtimeState)), - mMetrics(metrics), - mStyles(new Styles()), - mSequencer(new Sequencer(config.getTimeManager(), mRuntimeState.getRequestedAPLVersion())), - mFocusManager(new FocusManager(*this)), - mHoverManager(new HoverManager(*this)), - mPointerManager(new PointerManager(*this)), - mKeyboardManager(new KeyboardManager()), - mDataManager(new LiveDataManager()), - mExtensionManager(new ExtensionManager(extensions, config)), - mLayoutManager(new LayoutManager(*this)), - mUniqueIdManager(new UIDManager()), - mYGConfigRef(YGConfigNew()), - mTextMeasurement(config.getMeasure()), - mConfig(config), - mScreenLockCount(0), - mSettings(settings), - mSession(session), - mLayoutDirection(kLayoutDirectionInherit), - mCachedMeasures(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()), - mCachedBaselines(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()) -#ifdef SCENEGRAPH - , - mTextPropertiesCache(new sg::TextPropertiesCache()) -#endif // SCENEGRAPH -{ - YGConfigSetPrintTreeFlag(mYGConfigRef, DEBUG_YG_PRINT_TREE); - YGConfigSetLogger(mYGConfigRef, ygLogger); - YGConfigSetPointScaleFactor(mYGConfigRef, metrics.getDpi() / Metrics::CORE_DPI); -} - -RootContextData::~RootContextData() { - YGConfigFree(mYGConfigRef); -} - -void -RootContextData::terminate() -{ - auto top = halt(); - if (top) - top->release(); -} - -CoreComponentPtr -RootContextData::halt() -{ - mLayoutManager->terminate(); - mConfig.getTimeManager()->clear(); - - if (mSequencer) { - mSequencer->terminate(); - mSequencer = nullptr; - } - - // Clear any pending events and dirty components - events->clear(); - dirty.clear(); - dirtyVisualContext.clear(); - - auto result = mTop; - mTop = nullptr; - return result; -} - -} // namespace apl diff --git a/aplcore/src/engine/sharedcontextdata.cpp b/aplcore/src/engine/sharedcontextdata.cpp new file mode 100644 index 0000000..2f9d42c --- /dev/null +++ b/aplcore/src/engine/sharedcontextdata.cpp @@ -0,0 +1,129 @@ +/** + * Copyright 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 "apl/engine/sharedcontextdata.h" + +#include "apl/embed/documentregistrar.h" +#include "apl/engine/dependantmanager.h" +#include "apl/engine/eventmanager.h" +#include "apl/engine/hovermanager.h" +#include "apl/engine/keyboardmanager.h" +#include "apl/engine/layoutmanager.h" +#include "apl/engine/tickscheduler.h" +#include "apl/engine/uidgenerator.h" +#include "apl/focus/focusmanager.h" +#include "apl/media/mediamanager.h" +#include "apl/touch/pointermanager.h" +#include "apl/utils/make_unique.h" + +#ifdef SCENEGRAPH +#include "apl/scenegraph/textpropertiescache.h" +#endif // SCENEGRAPH + +namespace apl { + +static const bool DEBUG_YG_PRINT_TREE = false; + +static LogLevel +ygLevelToDebugLevel(YGLogLevel level) { + switch (level) { + case YGLogLevelError: return LogLevel::kError; + case YGLogLevelWarn: return LogLevel::kWarn; + case YGLogLevelInfo: return LogLevel::kInfo; + case YGLogLevelDebug: return LogLevel::kDebug; + case YGLogLevelVerbose: return LogLevel::kTrace; + case YGLogLevelFatal: return LogLevel::kCritical; + } + return LogLevel::kDebug; +} + +static int +ygLogger(const YGConfigRef config, + const YGNodeRef node, + YGLogLevel level, + const char* format, + va_list args) { + va_list args_copy; + va_copy(args_copy, args); + std::vector buf(1 + std::vsnprintf(nullptr, 0, format, args_copy)); + va_end(args_copy); + + std::vsnprintf(buf.data(), buf.size(), format, args); + va_end(args); + + LOG(ygLevelToDebugLevel(level)) << buf.data(); + return 1; // Does this matter? +} + +SharedContextData::SharedContextData(const CoreRootContextPtr& root, const Metrics& metrics, + const RootConfig& config) + : mRequestedVersion(config.getReportedAPLVersion()), + mDocumentRegistrar(std::make_unique()), + mFocusManager(std::make_unique(*root)), + mHoverManager(std::make_unique(*root)), + mPointerManager(std::make_unique(*root, *mHoverManager)), + mKeyboardManager(std::make_unique()), + mLayoutManager(std::make_unique( + *root, + Size(static_cast(metrics.getWidth()), static_cast(metrics.getHeight())))), + mTickScheduler(std::make_unique(config.getTimeManager())), + mDirtyComponents(std::make_unique()), + mUniqueIdGenerator(std::make_unique()), + mEventManager(std::make_unique()), + mDependantManager(std::make_unique()), + mDocumentManager(config.getDocumentManager()), + mTimeManager(config.getTimeManager()), + mMediaManager(config.getMediaManager()), + mMediaPlayerFactory(config.getMediaPlayerFactory()), + mYGConfigRef(YGConfigNew()), + mTextMeasurement(config.getMeasure()), + mCachedMeasures(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()), + mCachedBaselines(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()) +#ifdef SCENEGRAPH + , + mTextPropertiesCache(new sg::TextPropertiesCache()) +#endif // SCENEGRAPH +{ + YGConfigSetPrintTreeFlag(mYGConfigRef, DEBUG_YG_PRINT_TREE); + YGConfigSetLogger(mYGConfigRef, ygLogger); + YGConfigSetPointScaleFactor(mYGConfigRef, metrics.getDpi() / Metrics::CORE_DPI); +} + +SharedContextData::SharedContextData(const RootConfig& config) + : mRequestedVersion(config.getReportedAPLVersion()), + mUniqueIdGenerator(std::make_unique()), + mDependantManager(std::make_unique()), + mYGConfigRef(YGConfigNew()), + mTextMeasurement(config.getMeasure()), + mCachedMeasures(0), + mCachedBaselines(0) +#ifdef SCENEGRAPH + , + mTextPropertiesCache(new sg::TextPropertiesCache()) +#endif // SCENEGRAPH +{} + +SharedContextData::~SharedContextData() { + YGConfigFree(mYGConfigRef); +} + +void +SharedContextData::halt() +{ + mLayoutManager->terminate(); + mTimeManager->clear(); + mEventManager->clear(); +} +} // namespace apl diff --git a/aplcore/src/engine/tickscheduler.cpp b/aplcore/src/engine/tickscheduler.cpp new file mode 100644 index 0000000..aa15347 --- /dev/null +++ b/aplcore/src/engine/tickscheduler.cpp @@ -0,0 +1,75 @@ +/** +* Copyright 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 "apl/engine/tickscheduler.h" + +#include "apl/content/content.h" +#include "apl/document/coredocumentcontext.h" +#include "apl/engine/arrayify.h" +#include "apl/engine/evaluate.h" +#include "apl/engine/propdef.h" +#include "apl/time/sequencer.h" + +namespace apl +{ + +TickScheduler::TickScheduler(const std::shared_ptr& timeManager) + : mTimeManager(timeManager) +{} + +void +TickScheduler::processTickHandlers(const CoreDocumentContextPtr& documentContext) const +{ + auto& json = documentContext->content()->getDocument()->json(); + auto it = json.FindMember("handleTick"); + if (it == json.MemberEnd()) + return; + + auto tickHandlers = asArray(documentContext->context(), evaluate(documentContext->context(), it->value)); + + if (tickHandlers.empty() || !tickHandlers.isArray()) + return; + + for (const auto& handler : tickHandlers.getArray()) { + auto delay = std::max(propertyAsDouble(documentContext->context(), handler, "minimumDelay", 1000), + documentContext->rootConfig().getTickHandlerUpdateLimit()); + scheduleTickHandler(std::weak_ptr(documentContext), handler, delay); + } +} + +void +TickScheduler::scheduleTickHandler(const std::weak_ptr& documentContext, + const Object& handler, + double delay) const +{ + // Lambda capture takes care of handler here as it's a copy. + mTimeManager->setTimeout([this, documentContext, handler, delay]() { + auto dc = documentContext.lock(); + if (!dc) + return; + + auto ctx = dc->createDocumentContext("Tick"); + if (propertyAsBoolean(*ctx, handler, "when", true)) { + auto commands = Object(arrayifyProperty(*ctx, handler, "commands")); + if (!commands.empty()) + dc->context().sequencer().executeCommands(commands, ctx, nullptr, true); + } + + scheduleTickHandler(documentContext, handler, delay); + + }, delay); +} + +} // namespace apl diff --git a/aplcore/src/engine/uidgenerator.cpp b/aplcore/src/engine/uidgenerator.cpp new file mode 100644 index 0000000..4c1b0e7 --- /dev/null +++ b/aplcore/src/engine/uidgenerator.cpp @@ -0,0 +1,26 @@ +/** + * Copyright 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 "apl/engine/uidgenerator.h" + +namespace apl { + +std::string +UIDGenerator::get() +{ + return ':' + std::to_string(mCurrentId++); +} + +} \ No newline at end of file diff --git a/aplcore/src/engine/uidmanager.cpp b/aplcore/src/engine/uidmanager.cpp index bf16fb1..1fda42e 100644 --- a/aplcore/src/engine/uidmanager.cpp +++ b/aplcore/src/engine/uidmanager.cpp @@ -20,11 +20,15 @@ namespace apl { std::string -UIDManager::create(UIDObject*element) +UIDManager::create(UIDObject* element) { + if (mTerminated) { + LOG(LogLevel::kError).session(mSession) << "Trying to create new object on terminated state."; + return ""; + } + assert(element != nullptr); - static int sIdGenerator = 1000; - auto id = ':'+std::to_string(sIdGenerator++); + auto id = mGenerator.get(); mMap.emplace(id, element); return id; } @@ -32,23 +36,46 @@ UIDManager::create(UIDObject*element) void UIDManager::remove(const std::string& id, UIDObject* element) { + if (mTerminated) { + LOG(LogLevel::kError).session(mSession) << "Trying to remove an object on terminated state."; + // Handled by map clear, nothing to do. + return; + } + assert(!id.empty()); assert(element != nullptr); auto it = mMap.find(id); - assert(it != mMap.end()); - assert(it->second == element); + if (it == mMap.end()) { + LOG(LogLevel::kError).session(mSession) + << "Should not happen. Check for double destruction of UIDObject based class. ID: " << id; + + assert(false); + return; + } + + if(it->second != element) { + LOG(LogLevel::kError).session(mSession) + << "Should not happen. UIDObject ID should not be reused. ID: " << id; + + assert(false); + return; + } mMap.erase(it); } UIDObject* UIDManager::find(const std::string& id) { + if (mTerminated) { + LOG(LogLevel::kError).session(mSession) << "Trying to find an object on terminated state."; + return nullptr; + } + auto it = mMap.find(id); if (it != mMap.end()) return it->second; return nullptr; } - } \ No newline at end of file diff --git a/aplcore/src/extension/extensionclient.cpp b/aplcore/src/extension/extensionclient.cpp index 7ab4d61..bab47fb 100644 --- a/aplcore/src/extension/extensionclient.cpp +++ b/aplcore/src/extension/extensionclient.cpp @@ -20,11 +20,11 @@ #include "apl/component/componentpropdef.h" #include "apl/component/componentproperties.h" #include "apl/content/content.h" -#include "apl/content/rootconfig.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/arrayify.h" #include "apl/engine/binding.h" +#include "apl/engine/corerootcontext.h" #include "apl/engine/evaluate.h" -#include "apl/engine/rootcontext.h" #include "apl/extension/extensionmanager.h" #include "apl/livedata/livearray.h" #include "apl/livedata/livearrayobject.h" @@ -43,8 +43,6 @@ id_type ExtensionClient::sCommandIdGenerator = 1000; static const std::string IMPLEMENTED_INTERFACE_VERSION = "1.0"; static const std::string MAX_SUPPORTED_SCHEMA_VERSION = "1.1"; -static const std::string ROOT_CONFIG_MISSING = "RootConfig unavailable. Should not happen."; - static const Bimap sExtensionLiveDataUpdateTypeBimap = { {kExtensionLiveDataUpdateTypeInsert, "Insert"}, {kExtensionLiveDataUpdateTypeUpdate, "Update"}, @@ -83,24 +81,34 @@ static const Bimap sExtensionCompo ExtensionClientPtr ExtensionClient::create(const RootConfigPtr& rootConfig, const std::string& uri) { - return std::make_shared(rootConfig, uri); + return create(rootConfig, uri, makeDefaultSession()); +} + +ExtensionClientPtr +ExtensionClient::create(const RootConfigPtr& rootConfig, const std::string& uri, const SessionPtr& session) +{ + if (rootConfig == nullptr) { + LOG(LogLevel::kError) << "Can't create client with a null RootConfig."; + return nullptr; + } + + auto client = std::make_shared(uri, session, rootConfig->getExtensionFlags(uri)); + + // Auto-register client to support legacy non-mediator pathway + rootConfig->registerLegacyExtensionClient(uri, client); + + return client; } -ExtensionClient::ExtensionClient(const RootConfigPtr& rootConfig, const std::string& uri) - : mRegistrationProcessed(false), mRegistered(false), mUri(uri), mRootConfig(rootConfig) +ExtensionClient::ExtensionClient(const std::string& uri, const SessionPtr& session, const Object& flags) + : mRegistrationProcessed(false), mRegistered(false), mUri(uri), mSession(session), mFlags(flags), + mInternalRootConfig(RootConfig::create()) {} rapidjson::Value ExtensionClient::createRegistrationRequest(rapidjson::Document::AllocatorType& allocator, Content& content) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError).session(content.getSession()) << ROOT_CONFIG_MISSING; - return rapidjson::Value(rapidjson::kNullType); - } - const auto& settings = content.getExtensionSettings(mUri); - const auto& flags = rootConfig->getExtensionFlags(mUri); - return createRegistrationRequest(allocator, mUri, settings, flags); + return createRegistrationRequest(allocator, mUri, settings, mFlags); } rapidjson::Value @@ -116,24 +124,35 @@ ExtensionClient::createRegistrationRequest(rapidjson::Document::AllocatorType& a return Object(request).serialize(allocator); } +std::string +ExtensionClient::getUri() const +{ + return mUri; +} + bool -ExtensionClient::registrationMessageProcessed() +ExtensionClient::registrationMessageProcessed() const { return mRegistrationProcessed; } bool -ExtensionClient::registered() +ExtensionClient::registered() const { return mRegistered; } bool -ExtensionClient::registrationFailed() +ExtensionClient::registrationFailed() const { return mRegistrationProcessed && !mRegistered; } +const ParsedExtensionSchema& +ExtensionClient::extensionSchema() const { + return mSchema; +} + std::string ExtensionClient::getConnectionToken() const { @@ -141,55 +160,63 @@ ExtensionClient::getConnectionToken() const } void -ExtensionClient::bindContext(const RootContextPtr& rootContext) { - if (rootContext) { - mCachedContext = rootContext; - flushPendingEvents(rootContext); - } else { +ExtensionClient::bindContext(const CoreRootContextPtr& rootContext) +{ + if (!rootContext) { LOG(LogLevel::kError) << "Can't bind Client to non-existent RootContext."; return; } - LOG_IF(DEBUG_EXTENSION_CLIENT).session(rootContext) << "connection: " << mConnectionToken; + bindContextInternal(std::static_pointer_cast(rootContext)->mTopDocument); +} + +void +ExtensionClient::bindContextInternal(const CoreDocumentContextPtr& documentContext) +{ + mCachedContext = documentContext; + flushPendingEvents(documentContext); + LOG_IF(DEBUG_EXTENSION_CLIENT).session(documentContext) << "connection: " << mConnectionToken; } bool ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& message) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } + auto doc = rootContext ? std::static_pointer_cast(rootContext)->mTopDocument : nullptr; + return processMessageInternal(doc, std::move(message)); +} - LOG_IF(DEBUG_EXTENSION_CLIENT).session(rootContext) << "Connection: " << mConnectionToken << " message: " << message.toString(); +bool +ExtensionClient::processMessageInternal(const CoreDocumentContextPtr& documentContext, JsonData&& message) +{ + LOG_IF(DEBUG_EXTENSION_CLIENT).session(documentContext) + << "Connection: " << mConnectionToken << " message: " << message.toString(); if (!message) { - CONSOLE(rootConfig).log("Malformed offset=%u: %s.", message.offset(), message.error()); + CONSOLE(mSession).log("Malformed offset=%u: %s.", message.offset(), message.error()); return false; } - const auto& context = rootContext ? rootContext->context() : rootConfig->evaluationContext(); + const auto& context = documentContext ? documentContext->context() : mInternalRootConfig->evaluationContext(); auto evaluated = Object(std::move(message).get()); auto method = propertyAsMapped(context, evaluated, "method", static_cast(-1), sExtensionMethodBimap); if (!mRegistered) { if (mRegistrationProcessed) { - CONSOLE(rootConfig).log("Can't process message after failed registration."); + CONSOLE(mSession).log("Can't process message after failed registration."); return false; } else if (method != kExtensionMethodRegisterSuccess && method != kExtensionMethodRegisterFailure) { - CONSOLE(rootConfig).log("Can't process message before registration."); + CONSOLE(mSession).log("Can't process message before registration."); return false; } } - if (rootContext) { - mCachedContext = rootContext; + if (documentContext) { + mCachedContext = documentContext; } auto version = propertyAsObject(context, evaluated, "version"); if (version.isNull() || version.getString() != IMPLEMENTED_INTERFACE_VERSION) { - CONSOLE(rootConfig) << "Interface version is wrong. Expected=" << IMPLEMENTED_INTERFACE_VERSION - << "; Actual=" << version.toDebugString(); + CONSOLE(mSession) << "Interface version is wrong. Expected=" << IMPLEMENTED_INTERFACE_VERSION + << "; Actual=" << version.toDebugString(); return false; } @@ -219,7 +246,7 @@ ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& me result = processComponentResponse(context, evaluated); break; default: - CONSOLE(rootConfig).log("Unknown method"); + CONSOLE(mSession).log("Unknown method"); result = false; break; } @@ -230,26 +257,20 @@ ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& me bool ExtensionClient::processRegistrationResponse(const Context& context, const Object& connectionResponse) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (mRegistered) { - CONSOLE(rootConfig).log("Can't register extension twice."); + CONSOLE(mSession).log("Can't register extension twice."); return false; } auto connectionToken = propertyAsObject(context, connectionResponse, "token"); auto schema = propertyAsObject(context, connectionResponse, "schema"); if (connectionToken.isNull() || connectionToken.empty() || schema.isNull()) { - CONSOLE(rootConfig).log("Malformed connection response message."); + CONSOLE(mSession).log("Malformed connection response message."); return false; } if (!readExtension(context, schema)) { - CONSOLE(rootConfig).log("Malformed schema."); + CONSOLE(mSession).log("Malformed schema."); return false; } @@ -262,11 +283,18 @@ ExtensionClient::processRegistrationResponse(const Context& context, const Objec mConnectionToken = assignedToken; } + // The extension's environment ends up being available to the document author via + // ${environment.extension.Fish} (where Fish is the name assigned to the extension). + // + // The value of Fish is what we're setting below. We want Fish to be truthy (either as a map of + // values or "true") because that's how the author knows whether the extension is available. auto environment = propertyAsRecursive(context, connectionResponse, "environment"); - if(environment.isMap()) { - // Override environment to one that is provided in response as we set it to nothing when initially register - // extension. - rootConfig->registerExtensionEnvironment(mUri, environment); + if (environment.isMap()) { + // Use environment provided by extension, if it provided one + mSchema.environment = std::move(environment); + } else { + // Otherwise mark the environment as "true" + mSchema.environment = Object::TRUE_OBJECT(); } mRegistered = true; @@ -276,28 +304,22 @@ ExtensionClient::processRegistrationResponse(const Context& context, const Objec bool ExtensionClient::processEvent(const Context& context, const Object& event) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - auto name = propertyAsObject(context, event, "name"); - if (!name.isString() || name.empty() || (mEventModes.find(name.getString()) == mEventModes.end())) { - CONSOLE(rootConfig) << "Invalid extension event name for extension=" << mUri + if (!name.isString() || name.empty() || (mSchema.eventModes.find(name.getString()) == mSchema.eventModes.end())) { + CONSOLE(mSession) << "Invalid extension event name for extension=" << mUri << " name:" << name.toDebugString(); return false; } auto target = propertyAsObject(context, event, "target"); if (!target.isString() || target.empty() || target.getString() != mUri) { - CONSOLE(rootConfig) << "Invalid extension event target for extension=" << mUri; + CONSOLE(mSession) << "Invalid extension event target for extension=" << mUri; return false; } auto payload = propertyAsRecursive(context, event, "payload"); if (!payload.isNull() && !payload.isMap()) { - CONSOLE(rootConfig) << "Invalid extension event data for extension=" << mUri; + CONSOLE(mSession) << "Invalid extension event data for extension=" << mUri; return false; } @@ -305,7 +327,7 @@ ExtensionClient::processEvent(const Context& context, const Object& event) invokeExtensionHandler(mUri, name.getString(), payload.isNull() ? Object::EMPTY_MAP().getMap() : payload.getMap(), - mEventModes.at(name.getString()) == kExtensionEventExecutionModeFast, + mSchema.eventModes.at(name.getString()) == kExtensionEventExecutionModeFast, resourceId); return true; } @@ -313,33 +335,27 @@ ExtensionClient::processEvent(const Context& context, const Object& event) rapidjson::Value ExtensionClient::processCommand(rapidjson::Document::AllocatorType& allocator, const Event& event) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return rapidjson::Value(rapidjson::kNullType); - } - if (kEventTypeExtension != event.getType()) { - CONSOLE(rootConfig) << "Invalid extension command type for extension=" << mUri; + CONSOLE(mSession) << "Invalid extension command type for extension=" << mUri; return rapidjson::Value(rapidjson::kNullType); } auto extensionURI = event.getValue(kEventPropertyExtensionURI); if (!extensionURI.isString() || extensionURI.getString() != mUri) { - CONSOLE(rootConfig) << "Invalid extension command target for extension=" << mUri; + CONSOLE(mSession) << "Invalid extension command target for extension=" << mUri; return rapidjson::Value(rapidjson::kNullType); } auto commandName = event.getValue(kEventPropertyName); if (!commandName.isString() || commandName.empty()) { - CONSOLE(rootConfig) << "Invalid extension command name for extension=" << mUri + CONSOLE(mSession) << "Invalid extension command name for extension=" << mUri << " command:" << commandName; return rapidjson::Value(rapidjson::kNullType); } auto resourceId = event.getValue(kEventPropertyExtensionResourceId); if (!resourceId.empty() && !resourceId.isString()) { - CONSOLE(rootConfig) << "Invalid extension component handle for extension=" << mUri; + CONSOLE(mSession) << "Invalid extension component handle for extension=" << mUri; return rapidjson::Value (rapidjson::kNullType); } @@ -377,15 +393,9 @@ ExtensionClient::processCommandResponse(const Context& context, const Object& re { // Resolve any Action associated with a command response. The action is resolved independent // of a Success or Failure in command execution - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - auto id = propertyAsObject(context, response, "id"); if (!id.isNumber() || id.getInteger() > sCommandIdGenerator) { - CONSOLE(rootConfig) << "Invalid extension command response for extension=" << mUri << " id=" + CONSOLE(mSession) << "Invalid extension command response for extension=" << mUri << " id=" << id.toDebugString() << " total pending=" << mActionRefs.size(); return false; } @@ -411,12 +421,12 @@ void ExtensionClient::liveDataObjectFlushed(const std::string& key, LiveDataObject& liveDataObject) { if (!mLiveData.count(key)) { - LOG(LogLevel::kWarn).session(mRootConfig.lock()) << "Received update for unhandled LiveData " << key; + LOG(LogLevel::kWarn).session(mSession) << "Received update for unhandled LiveData " << key; return; } auto ref = mLiveData.at(key); - LOG_IF(DEBUG_EXTENSION_CLIENT).session(mRootConfig.lock()) << " connection: " << mConnectionToken + LOG_IF(DEBUG_EXTENSION_CLIENT).session(mSession) << " connection: " << mConnectionToken << ", key: " << key << ", ref.name: " << ref.name << ", type: " << ref.type; @@ -541,27 +551,21 @@ ExtensionClient::reportLiveArrayChanges(const LiveDataRef& ref, const std::vecto bool ExtensionClient::processLiveDataUpdate(const Context& context, const Object& update) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - auto name = propertyAsObject(context, update, "name"); if (!name.isString() || name.empty() || (mLiveData.find(name.getString()) == mLiveData.end())) { - CONSOLE(rootConfig) << "Invalid LiveData name for extension=" << mUri; + CONSOLE(mSession) << "Invalid LiveData name for extension=" << mUri; return false; } auto target = propertyAsObject(context, update, "target"); if (!target.isString() || target.empty() || target.getString() != mUri) { - CONSOLE(rootConfig) << "Invalid LiveData target for extension=" << mUri; + CONSOLE(mSession) << "Invalid LiveData target for extension=" << mUri; return false; } auto operations = propertyAsRecursive(context, update, "operations"); if (!operations.isArray()) { - CONSOLE(rootConfig) << "Invalid LiveData operations for extension=" << mUri; + CONSOLE(mSession) << "Invalid LiveData operations for extension=" << mUri; return false; } @@ -570,7 +574,7 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd auto type = propertyAsMapped(context, operation, "type", static_cast(-1), sExtensionLiveDataUpdateTypeBimap); if (type == static_cast(-1)) { - CONSOLE(rootConfig) << "Wrong operation type for=" << name; + CONSOLE(mSession) << "Wrong operation type for=" << name; return false; } @@ -584,12 +588,12 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd break; default: result = false; - CONSOLE(rootConfig) << "Unknown LiveObject type=" << dataRef.objectType << " for " << dataRef.name; + CONSOLE(mSession) << "Unknown LiveObject type=" << dataRef.objectType << " for " << dataRef.name; break; } if (!result) { - CONSOLE(rootConfig) << "LiveMap operation failed=" << dataRef.name << " operation=" + CONSOLE(mSession) << "LiveMap operation failed=" << dataRef.name << " operation=" << sExtensionLiveDataUpdateTypeBimap.at(type); } else { dataRef.hasPendingUpdate = true; @@ -601,20 +605,16 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd bool ExtensionClient::updateLiveMap(ExtensionLiveDataUpdateType type, const LiveDataRef& dataRef, const Object& operation) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - std::string triggerEvent; auto keyObj = operation.opt("key", ""); if (keyObj.empty()) { - CONSOLE(rootConfig) << "Invalid LiveData key for=" << dataRef.name; + CONSOLE(mSession) << "Invalid LiveData key for=" << dataRef.name; return false; } const auto& key = keyObj.getString(); - auto typeDef = mTypes.at(dataRef.type); + + // TODO: Why don't we verify the type? Should we? +// auto typeDef = mSchema.types.at(dataRef.type); auto item = operation.get("item"); auto liveMap = std::static_pointer_cast(dataRef.objectPtr); @@ -628,7 +628,7 @@ ExtensionClient::updateLiveMap(ExtensionLiveDataUpdateType type, const LiveDataR result = liveMap->remove(key); break; default: - CONSOLE(rootConfig) << "Unknown operation for=" << dataRef.name; + CONSOLE(mSession) << "Unknown operation for=" << dataRef.name; return false; } @@ -638,23 +638,17 @@ ExtensionClient::updateLiveMap(ExtensionLiveDataUpdateType type, const LiveDataR bool ExtensionClient::updateLiveArray(ExtensionLiveDataUpdateType type, const LiveDataRef& dataRef, const Object& operation) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - std::string triggerEvent; auto item = operation.get("item"); if (item.isNull() && (type != kExtensionLiveDataUpdateTypeRemove && type != kExtensionLiveDataUpdateTypeClear)) { - CONSOLE(rootConfig) << "Malformed items on LiveData update for=" << dataRef.name; + CONSOLE(mSession) << "Malformed items on LiveData update for=" << dataRef.name; return false; } auto indexObj = operation.opt("index", -1); if (!indexObj.isNumber() && type != kExtensionLiveDataUpdateTypeClear) { - CONSOLE(rootConfig) << "Invalid LiveData index for=" << dataRef.name; + CONSOLE(mSession) << "Invalid LiveData index for=" << dataRef.name; return false; } auto index = indexObj.getInteger(); @@ -684,7 +678,7 @@ ExtensionClient::updateLiveArray(ExtensionLiveDataUpdateType type, const LiveDat liveArray->clear(); break; default: - CONSOLE(rootConfig) << "Unknown operation for=" << dataRef.name; + CONSOLE(mSession) << "Unknown operation for=" << dataRef.name; return false; } @@ -694,28 +688,21 @@ ExtensionClient::updateLiveArray(ExtensionLiveDataUpdateType type, const LiveDat bool ExtensionClient::readExtension(const Context& context, const Object& extension) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - // verify extension schema auto schema = propertyAsString(context, extension, "type"); auto version = propertyAsString(context, extension, "version"); if (schema != "Schema" || version.compare(MAX_SUPPORTED_SCHEMA_VERSION) > 0) { - CONSOLE(rootConfig) << "Unsupported extension schema version:" << version; + CONSOLE(mSession) << "Unsupported extension schema version:" << version; return false; } // register extension based on URI auto uriObj = propertyAsObject(context, extension, "uri"); if (!uriObj.isString() || uriObj.empty()) { - CONSOLE(rootConfig).log("Missing or invalid extension URI."); + CONSOLE(mSession).log("Missing or invalid extension URI."); return false; } const auto& uri = uriObj.getString(); - rootConfig->registerExtension(uri); mUri = uri; auto types = arrayifyPropertyAsObject(context, extension, "types"); @@ -752,14 +739,8 @@ ExtensionClient::readExtension(const Context& context, const Object& extension) bool ExtensionClient::readExtensionTypes(const Context& context, const Object& types) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (!types.isArray()) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); + CONSOLE(mSession).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); return false; } @@ -767,7 +748,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) auto name = propertyAsObject(context, t, "name"); auto props = propertyAsObject(context, t, "properties"); if (!name.isString() || !props.isMap()) { - CONSOLE(rootConfig).log("Invalid extension type for extension=%s", + CONSOLE(mSession).log("Invalid extension type for extension=%s", mUri.c_str()); continue; } @@ -776,11 +757,11 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) auto extends = propertyAsObject(context, t, "extends"); if (extends.isString()) { auto& extended = extends.getString(); - auto extendedType = mTypes.find(extended); - if (extendedType != mTypes.end()) { + auto extendedType = mSchema.types.find(extended); + if (extendedType != mSchema.types.end()) { properties->insert(extendedType->second->begin(), extendedType->second->end()); } else { - CONSOLE(rootConfig) << "Unknown type to extend=" << extended + CONSOLE(mSession) << "Unknown type to extend=" << extended << " for type=" << name.getString() << " for extension=" << mUri.c_str(); } @@ -797,7 +778,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) if (ps.isString()) { ptype = sBindingMap.get(ps.getString(), kBindingTypeAny); } else if (!ps.has("type")) { - CONSOLE(rootConfig).log("Invalid extension property for type=%s extension=%s", + CONSOLE(mSession).log("Invalid extension property for type=%s extension=%s", name.getString().c_str(), mUri.c_str()); continue; } else { @@ -814,7 +795,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) properties->emplace(pname, ExtensionProperty{ptype, pfunc(context, defValue), preq}); } - mTypes.emplace(name.getString(), properties); + mSchema.types.emplace(name.getString(), properties); } return true; } @@ -823,17 +804,12 @@ std::vector ExtensionClient::readCommandDefinitionsInternal(const Context& context,const ObjectArray& commands) { std::vector commandDefs; - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return commandDefs; - } for (const auto& command : commands) { // create a command auto name = propertyAsObject(context, command, "name"); if (!name.isString() || name.empty()) { - CONSOLE(rootConfig).log("Invalid extension command for extension=%s", mUri.c_str()); + CONSOLE(mSession).log("Invalid extension command for extension=%s", mUri.c_str()); continue; } auto commandName = name.asString(); @@ -856,21 +832,20 @@ ExtensionClient::readCommandDefinitionsInternal(const Context& context,const Obj type = propertyAsString(context, payload, "type"); } - if (!mTypes.count(type)) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed `payload` block for command=%s", + if (!mSchema.types.count(type)) { + CONSOLE(mSession).log("The extension name=%s has a malformed `payload` block for command=%s", mUri.c_str(), commandName.c_str()); continue; } - auto props = mTypes.at(type); + auto props = mSchema.types.at(type); for (const auto& p : *props) { // add the property commandDef.property(p.first, p.second.btype, p.second.defvalue, p.second.required); } } // properties - // register the command - rootConfig->registerExtensionCommand(commandDef); + mSchema.commandDefinitions.emplace_back(std::move(commandDef)); } return commandDefs; } @@ -878,14 +853,8 @@ ExtensionClient::readCommandDefinitionsInternal(const Context& context,const Obj bool ExtensionClient::readExtensionCommandDefinitions(const Context& context, const Object& commands) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (!commands.isArray()) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); + CONSOLE(mSession).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); return false; } readCommandDefinitionsInternal(context, commands.getArray()); @@ -896,14 +865,8 @@ void ExtensionClient::readExtensionComponentCommandDefinitions(const Context& context, const Object& commands, ExtensionComponentDefinition& def) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return; - } - if (!commands.isArray()) { - CONSOLE(rootConfig).log("The extension component name=%s has a malformed 'commands' block", mUri.c_str()); + CONSOLE(mSession).log("The extension component name=%s has a malformed 'commands' block", mUri.c_str()); return; } // TODO: Remove when customers stopped using it. @@ -913,27 +876,21 @@ ExtensionClient::readExtensionComponentCommandDefinitions(const Context& context bool ExtensionClient::readExtensionEventHandlers(const Context& context, const Object& handlers) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (!handlers.isArray()) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed 'events' block", mUri.c_str()); + CONSOLE(mSession).log("The extension name=%s has a malformed 'events' block", mUri.c_str()); return false; } for (const auto& handler : handlers.getArray()) { auto name = propertyAsObject(context, handler, "name"); if (!name.isString() || name.empty()) { - CONSOLE(rootConfig).log("Invalid extension event handler for extension=%s", mUri.c_str()); + CONSOLE(mSession).log("Invalid extension event handler for extension=%s", mUri.c_str()); return false; } else { auto mode = propertyAsMapped(context, handler, "mode", kExtensionEventExecutionModeFast, sExtensionEventExecutionModeBimap); - mEventModes.emplace(name.asString(), mode); - rootConfig->registerExtensionEventHandler(ExtensionEventHandler(mUri, name.asString())); + mSchema.eventModes.emplace(name.asString(), mode); + mSchema.eventHandlers.emplace_back(ExtensionEventHandler(mUri, name.asString())); } } @@ -943,27 +900,21 @@ ExtensionClient::readExtensionEventHandlers(const Context& context, const Object bool ExtensionClient::readExtensionLiveData(const Context& context, const Object& liveData) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (!liveData.isArray()) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed 'dataBindings' block", mUri.c_str()); + CONSOLE(mSession).log("The extension name=%s has a malformed 'dataBindings' block", mUri.c_str()); return false; } for (const auto& binding : liveData.getArray()) { auto name = propertyAsObject(context, binding, "name"); if (!name.isString() || name.empty()) { - CONSOLE(rootConfig).log("Invalid extension data binding for extension=%s", mUri.c_str()); + CONSOLE(mSession).log("Invalid extension data binding for extension=%s", mUri.c_str()); return false; } auto typeDef = propertyAsObject(context, binding, "type"); if (!typeDef.isString()) { - CONSOLE(rootConfig).log("Invalid extension data binding type for extension=%s", mUri.c_str()); + CONSOLE(mSession).log("Invalid extension data binding type for extension=%s", mUri.c_str()); return false; } @@ -975,9 +926,9 @@ ExtensionClient::readExtensionLiveData(const Context& context, const Object& liv type = type.substr(0, arrayDefinition); } - if (!(mTypes.count(type) // Any LiveData may use defined complex types + if (!(mSchema.types.count(type) // Any LiveData may use defined complex types || (isArray && sBindingMap.has(type)))) { // Arrays also may use primitive types - CONSOLE(rootConfig).log("Data type=%s, for LiveData=%s is invalid", type.c_str(), name.getString().c_str()); + CONSOLE(mSession).log("Data type=%s, for LiveData=%s is invalid", type.c_str(), name.getString().c_str()); continue; } @@ -987,17 +938,35 @@ ExtensionClient::readExtensionLiveData(const Context& context, const Object& liv PropertyTriggerEvent removeEvent; LiveObjectPtr live; + auto initialData = propertyAsRecursive(context, binding, "data"); + if (isArray) { ltype = kExtensionLiveDataTypeArray; - live = LiveArray::create(); + + if (initialData.isArray()) { + live = LiveArray::create(ObjectArray(initialData.getArray())); + } else { + live = LiveArray::create(); + if (!initialData.isNull()) { + CONSOLE(mSession).log("Initial data for LiveData=%s is of invalid type. Should be an Array.", name.getString().c_str()); + } + } } else { ltype = kExtensionLiveDataTypeObject; - live = LiveMap::create(); + + if (initialData.isMap()) { + live = LiveMap::create(ObjectMap(initialData.getMap())); + } else { + live = LiveMap::create(); + if (!initialData.isNull()) { + CONSOLE(mSession).log("Initial data for LiveData=%s is of invalid type. Should be a Map.", name.getString().c_str()); + } + } } auto events = propertyAsObject(context, binding, "events"); if (events.isMap()) { - auto typeProps = mTypes.at(type); + auto typeProps = mSchema.types.at(type); auto event = propertyAsObject(context, events, "add"); if (event.isMap()) { auto propTriggers = propertyAsObject(context, event, "properties"); @@ -1027,9 +996,7 @@ ExtensionClient::readExtensionLiveData(const Context& context, const Object& liv LiveDataRef ldf = {name.getString(), ltype, type, live, false, addEvent, updateEvent, removeEvent}; mLiveData.emplace(name.getString(), ldf); - rootConfig->liveData(name.getString(), live); - auto liveObjectWatcher = std::shared_ptr(shared_from_this()); - rootConfig->liveDataWatcher(name.getString(), liveObjectWatcher); + mSchema.liveData.emplace(name.getString(), live); } return true; @@ -1081,15 +1048,9 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, const Object& handlers, ExtensionComponentDefinition& def) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (!handlers.isNull()) { if (!handlers.isArray()) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed 'events' block", + CONSOLE(mSession).log("The extension name=%s has a malformed 'events' block", mUri.c_str()); return false; } @@ -1097,7 +1058,7 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, for (const auto& handler : handlers.getArray()) { auto name = propertyAsObject(context, handler, "name"); if (!name.isString() || name.empty()) { - CONSOLE(rootConfig).log("Invalid extension event handler for extension=%s", + CONSOLE(mSession).log("Invalid extension event handler for extension=%s", mUri.c_str()); return false; } @@ -1105,7 +1066,7 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, auto mode = propertyAsMapped( context, handler, "mode", kExtensionEventExecutionModeFast, sExtensionEventExecutionModeBimap); - mEventModes.emplace(name.asString(), mode); + mSchema.eventModes.emplace(name.asString(), mode); auto eventKey = sComponentPropertyBimap.append(name.asString()); def.addEventHandler(eventKey, ExtensionEventHandler(mUri, name.asString())); } @@ -1117,21 +1078,15 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, bool ExtensionClient::readExtensionComponentDefinitions(const Context& context, const Object& components) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - if (!components.isArray()) { - CONSOLE(rootConfig).log("The extension name=%s has a malformed 'components' block", mUri.c_str()); + CONSOLE(mSession).log("The extension name=%s has a malformed 'components' block", mUri.c_str()); return false; } for (const auto& component : components.getArray()) { auto name = propertyAsObject(context, component, "name"); if (!name.isString() || name.empty()) { - CONSOLE(rootConfig).log("Invalid extension component name for extension=%s", mUri.c_str()); + CONSOLE(mSession).log("Invalid extension component name for extension=%s", mUri.c_str()); continue; } auto componentName = name.asString(); @@ -1168,7 +1123,7 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const if (ps.isString()) { ptype = sBindingMap.get(ps.getString(), kBindingTypeAny); } else if (!ps.has("type")) { - CONSOLE(rootConfig).log("Invalid extension property extension=%s", mUri.c_str()); + CONSOLE(mSession).log("Invalid extension property extension=%s", mUri.c_str()); continue; } else { defValue = propertyAsObject(context, ps, "default"); @@ -1189,7 +1144,7 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const componentDef.properties(properties); } - rootConfig->registerExtensionComponent(componentDef); + mSchema.componentDefinitions.emplace_back(std::move(componentDef)); } return true; @@ -1198,15 +1153,9 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const rapidjson::Value ExtensionClient::createComponentChange(rapidjson::MemoryPoolAllocator<>& allocator, ExtensionComponent& component) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return rapidjson::Value(rapidjson::kNullType); - } - auto extensionURI = component.getUri(); if (extensionURI != mUri) { - CONSOLE(rootConfig) << "Invalid extension command target for extension=" << mUri; + CONSOLE(mSession) << "Invalid extension command target for extension=" << mUri; return rapidjson::Value(rapidjson::kNullType); } @@ -1241,7 +1190,7 @@ ExtensionClient::createComponentChange(rapidjson::MemoryPoolAllocator<>& allocat result->emplace("payload", payload); } - LOG_IF(DEBUG_EXTENSION_CLIENT).session(component.getContext()) << "Component: " << Object(result); + LOG_IF(DEBUG_EXTENSION_CLIENT).session(mSession) << "Component: " << Object(result); return Object(result).serialize(allocator); } @@ -1263,12 +1212,7 @@ ExtensionClient::processComponentResponse(const Context& context, const Object& // TODO apply component properties } } else { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } - CONSOLE(rootConfig) << "Unable to find component associated with :" << componentId; + CONSOLE(mSession) << "Unable to find component associated with :" << componentId; } return true; } @@ -1278,13 +1222,16 @@ ExtensionClient::handleDisconnection(const RootContextPtr& rootContext, int errorCode, const std::string& message) { - auto rootConfig = mRootConfig.lock(); - if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; - return false; - } + auto doc = rootContext ? std::static_pointer_cast(rootContext)->mTopDocument : nullptr; + return handleDisconnectionInternal(doc, errorCode, message); +} - const auto& context = rootContext ? rootContext->context() : rootConfig->evaluationContext(); +bool +ExtensionClient::handleDisconnectionInternal(const CoreDocumentContextPtr& documentContext, + int errorCode, + const std::string& message) +{ + const auto& context = documentContext ? documentContext->context() : mInternalRootConfig->evaluationContext(); if (errorCode != 0) { for (const auto& componentEntry : context.extensionManager().getExtensionComponents()) { @@ -1324,21 +1271,23 @@ ExtensionClient::invokeExtensionHandler(const std::string& uri, const std::strin const ObjectMap& data, bool fastMode, std::string resourceId) { - auto rootContext = mCachedContext.lock(); - if (rootContext) { - rootContext->invokeExtensionEventHandler(uri, name, data, fastMode, resourceId); + auto documentContext = mCachedContext.lock(); + if (documentContext) { + documentContext->invokeExtensionEventHandler(uri, name, data, fastMode, resourceId); } else { - LOG(LogLevel::kWarn).session(mRootConfig.lock()) << "RootContext not available"; + LOG(LogLevel::kWarn).session(mSession) << "RootContext not available"; ExtensionEvent event = {uri, name, data, fastMode, resourceId}; mPendingEvents.emplace_back(std::move(event)); } } void -ExtensionClient::flushPendingEvents(const RootContextPtr& rootContext) +ExtensionClient::flushPendingEvents(const CoreDocumentContextPtr& documentContext) { - LOG_IF(DEBUG_EXTENSION_CLIENT && (mPendingEvents.size() > 0)).session(rootContext) << "Flushing " << mPendingEvents.size() - << " pending events for " << mConnectionToken; + LOG_IF(DEBUG_EXTENSION_CLIENT && (mPendingEvents.size() > 0)) + .session(documentContext) << "Flushing " + << mPendingEvents.size() + << " pending events for " << mConnectionToken; for (auto& event : mPendingEvents) { invokeExtensionHandler(event.uri, event.name, event.data, event.fastMode, event.resourceId); @@ -1350,7 +1299,7 @@ ExtensionClient::flushPendingEvents(const RootContextPtr& rootContext) auto& ref = kv.second; if (!ref.hasPendingUpdate) continue; - LOG_IF(DEBUG_EXTENSION_CLIENT).session(rootContext) << "Simulate changes for " << ref.name << " in: " << mConnectionToken; + LOG_IF(DEBUG_EXTENSION_CLIENT).session(mSession) << "Simulate changes for " << ref.name << " in: " << mConnectionToken; // Generate changelist based on notion of nothing been there initially if (ref.objectType == kExtensionLiveDataTypeArray) { diff --git a/aplcore/src/extension/extensionmanager.cpp b/aplcore/src/extension/extensionmanager.cpp index ac943e8..8e6d369 100644 --- a/aplcore/src/extension/extensionmanager.cpp +++ b/aplcore/src/extension/extensionmanager.cpp @@ -17,22 +17,78 @@ #include "apl/content/rootconfig.h" #include "apl/extension/extensionmediator.h" +#include "apl/extension/extensionclient.h" namespace apl { static const bool DEBUG_EXTENSION_MANAGER = false; -ExtensionManager::ExtensionManager(const std::vector>& requests, - const RootConfig& rootConfig) { +ExtensionManager::ExtensionManager(const std::vector& requests, + const RootConfig& rootConfig, + const SessionPtr& session) { + auto uriToNamespace = std::multimap(); + for (const auto& m : requests) { + uriToNamespace.emplace(m.uri, m.name); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "URI to Namespace: " << m.uri << "->" << m.name; + } + + // The following map contains keys representing the extension URIs and + // values representing the environment that those extensions registered + ObjectMap supported = rootConfig.getSupportedExtensions(); + + auto clients = rootConfig.getLegacyExtensionClients(); #ifdef ALEXAEXTENSIONS - mMediator = rootConfig.getExtensionMediator(); + auto mediator = rootConfig.getExtensionMediator(); + if (mediator) { + const auto& mediatorClients = mediator->getClients(); + clients.insert(mediatorClients.begin(), mediatorClients.end()); + } + + // Save weak mediator for later + mMediator = mediator; #endif - - auto uriToNamespace = std::multimap(); - for (const auto& m : requests) { - uriToNamespace.emplace(m.second, m.first); - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "URI to Namespace: " << m.second << "->" << m.first; + + for (auto& client : clients) { + if (!client.second->registered()) continue; + + const auto& schema = client.second->extensionSchema(); + + // Mark extension as supported and save environment + supported[client.first] = schema.environment; + + // There may be multiple namespaces for the same extension, so register each of them. + auto range = uriToNamespace.equal_range(client.first); + for (auto it = range.first; it != range.second; ++it) { + for (const auto& m : schema.commandDefinitions) { + auto qualifiedName = it->second + ":" + m.getName(); + mCommandDefinitions.emplace(qualifiedName, m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) + << "extension commands: " << qualifiedName << "->" + m.toDebugString(); + } + + for (const auto& m : schema.filterDefinitions) { + auto qualifiedName = it->second + ":" + m.getName(); + mFilterDefinitions.emplace(qualifiedName, m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) + << "extension filters: " << qualifiedName << "->" + m.toDebugString(); + } + + for (const auto& m : schema.eventHandlers) { + auto qualifiedName = it->second + ":" + m.getName(); + mEventHandlers.emplace(it->second + ":" + m.getName(), m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) + << "qualified handlers: " << it->second + ":" + m.getName() << "->" + << m.toDebugString(); + } + + for (const auto& m : schema.componentDefinitions) { + auto qualifiedName = it->second + ":" + m.getName(); + mComponentDefinitions.emplace(qualifiedName, m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) + << "extension component: " << qualifiedName << "->" + m.toDebugString(); + } + } } // Extensions that define custom commands @@ -41,8 +97,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(); - mExtensionCommands.emplace(qualifiedName, m); - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "extension commands: " << qualifiedName + mCommandDefinitions.emplace(qualifiedName, m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "extension commands: " << qualifiedName << "->" + m.toDebugString(); } } @@ -53,8 +109,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(); - mExtensionFilters.emplace(qualifiedName, m); - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "extension filters: " << qualifiedName + mFilterDefinitions.emplace(qualifiedName, m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "extension filters: " << qualifiedName << "->" + m.toDebugString(); } } @@ -63,26 +119,23 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(), m); - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "qualified handlers: " + mEventHandlers.emplace(it->second + ":" + m.getName(), m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "qualified handlers: " << it->second + ":" + m.getName() << "->" << m.toDebugString(); } } - - // Construct the data-binding environmental information for indicating which extensions are installed - const auto& supported = rootConfig.getSupportedExtensions(); + mEnvironment = std::make_shared(); for (const auto& m : requests) { - auto it = supported.find(m.second); + auto it = supported.find(m.uri); if (it != supported.end()) { - auto cfg = Object(rootConfig.getExtensionEnvironment(m.second)); - mEnvironment->emplace(m.first, cfg);// Add the NAME. The URI should already be there. - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "requestedEnvironment: " << m.first - << "->" << cfg.toDebugString(); + mEnvironment->emplace(m.name, it->second);// Add the NAME. The URI should already be there. + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "requestedEnvironment: " << m.name + << "->" << it->second.toDebugString(); } else { - mEnvironment->emplace(m.first, Object::FALSE_OBJECT()); - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "requestedEnvironment: " << m.first + mEnvironment->emplace(m.name, Object::FALSE_OBJECT()); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "requestedEnvironment: " << m.name << "->" << false; } } @@ -93,8 +146,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(); - mExtensionComponentDefs.emplace(qualifiedName, m); - LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "extension component: " << qualifiedName + mComponentDefinitions.emplace(qualifiedName, m); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(session) << "extension component: " << qualifiedName << "->" + m.toDebugString(); } } @@ -102,7 +155,7 @@ ExtensionManager::ExtensionManager(const std::vectorsecond; return nullptr; @@ -127,8 +180,8 @@ ExtensionManager::findCommandDefinition(const std::string& qualifiedName) { ExtensionComponentDefinition* ExtensionManager::findComponentDefinition(const std::string& qualifiedName) { // If the custom handler doesn't exist - auto it = mExtensionComponentDefs.find(qualifiedName); - if (it != mExtensionComponentDefs.end()) + auto it = mComponentDefinitions.find(qualifiedName); + if (it != mComponentDefinitions.end()) return &it->second; return nullptr; @@ -136,8 +189,8 @@ ExtensionManager::findComponentDefinition(const std::string& qualifiedName) { ExtensionFilterDefinition* ExtensionManager::findFilterDefinition(const std::string& qualifiedName) { - auto it = mExtensionFilters.find(qualifiedName); - if (it != mExtensionFilters.end()) + auto it = mFilterDefinitions.find(qualifiedName); + if (it != mFilterDefinitions.end()) return &it->second; return nullptr; @@ -146,8 +199,8 @@ ExtensionManager::findFilterDefinition(const std::string& qualifiedName) { Object ExtensionManager::findHandler(const ExtensionEventHandler& handler) { - auto it = mExtensionEventHandlers.find(handler); - if (it != mExtensionEventHandlers.end()) + auto it = mEventHandlerCommandMap.find(handler); + if (it != mEventHandlerCommandMap.end()) return it->second; return Object::NULL_OBJECT(); diff --git a/aplcore/src/extension/extensionmediator.cpp b/aplcore/src/extension/extensionmediator.cpp index b5ff4e9..372be00 100644 --- a/aplcore/src/extension/extensionmediator.cpp +++ b/aplcore/src/extension/extensionmediator.cpp @@ -25,9 +25,10 @@ #include #include +#include "apl/content/extensionrequest.h" +#include "apl/document/coredocumentcontext.h" #include "apl/extension/extensioncomponent.h" #include "apl/extension/extensionmediator.h" -#include "apl/primitives/objectdata.h" using namespace alexaext; @@ -316,80 +317,100 @@ ExtensionMediator::ExtensionMediator(const ExtensionProviderPtr& provider, // TODO void -ExtensionMediator::bindContext(const RootContextPtr& context) +ExtensionMediator::bindContext(const CoreDocumentContextPtr& context) { // Called by RootContext on create to create an association for event and data updates. // This goes away when ExtensionManager registers callbacks directly. - mRootContext = context; + mDocumentContext = context; for (auto& client : mClients) { - client.second->bindContext(context); + client.second->bindContextInternal(context); } - onDisplayStateChanged(context->getDisplayState()); + onDisplayStateChanged(context->mDisplayState); } void ExtensionMediator::initializeExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, const ExtensionGrantRequestCallback& grantHandler) { + if (rootConfig == nullptr) { + return; + } + initializeExtensions(rootConfig->getExtensionFlags(), content, grantHandler); +} - auto uris = content->getExtensionRequests(); +void +ExtensionMediator::initializeExtensions(const ObjectMap& flagMap, const ContentPtr& content, + const ExtensionGrantRequestCallback& grantHandler) { + const auto& extensionRequests = content->getExtensionRequestsV2(); auto extensionProvider = mProvider.lock(); - if (rootConfig == nullptr || uris.empty() || extensionProvider == nullptr) { + if (extensionRequests.empty() || extensionProvider == nullptr) { return; } + mSession = content->getSession(); + std::weak_ptr weak_this = shared_from_this(); - std::weak_ptr weak_config = rootConfig; - for (const auto& uri : uris) { - if (mPendingRegistrations.count(uri)) continue; + for (const auto& request : extensionRequests) { + if (request.required) mRequired.emplace(request.uri); + if (mPendingRegistrations.count(request.uri)) continue; - LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "initialize extension: " << uri - << " has extension: " << extensionProvider->hasExtension(uri); + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mSession) << "initialize extension: " << request.uri + << " has extension: " << extensionProvider->hasExtension(request.uri); + + mPendingGrants.insert(request.uri); + + // Check if we've been given flags for this specific extension + Object flags; + auto it = flagMap.find(request.uri); + if (it != flagMap.end()) + flags = it->second; - mPendingGrants.insert(uri); if (grantHandler) { // callback to grant/deny access to extension grantHandler( - uri, - [weak_this, weak_config](const std::string& grantedUri) { + request.uri, + [weak_this, flags](const std::string& grantedUri) { if (auto mediator = weak_this.lock()) - mediator->grantExtension(weak_config.lock(), grantedUri); + mediator->grantExtension(flags, grantedUri); }, - [weak_this, weak_config](const std::string& deniedUri) { + [weak_this](const std::string& deniedUri) { if (auto mediator = weak_this.lock()) - mediator->denyExtension(weak_config.lock(), deniedUri); + mediator->denyExtension(deniedUri); }); } else { // auto-grant when no grant handler - grantExtension(rootConfig, uri); + grantExtension(flags, request.uri); } } } void -ExtensionMediator::grantExtension(const RootConfigPtr& rootConfig, const std::string& uri) { - +ExtensionMediator::grantExtension(const Object& flags, const std::string& uri) +{ if (!mPendingGrants.count(uri)) return; - LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "Extension granted: " << uri; + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mSession) << "Extension granted: " << uri; mPendingGrants.erase(uri); auto extensionProvider = mProvider.lock(); - if (rootConfig == nullptr || extensionProvider == nullptr) { + if (extensionProvider == nullptr) { return; } if (extensionProvider->hasExtension(uri)) { + auto required = mRequired.count(uri); // First get will call initialize. auto proxy = extensionProvider->getExtension(uri); if (!proxy) { - CONSOLE(rootConfig) << "Failed to retrieve proxy for extension: " << uri; + if (required) mFailState = true; + CONSOLE(mSession) << "Failed to retrieve proxy for extension: " << uri; return; } // create a client for message processing - auto client = ExtensionClient::create(rootConfig, uri); + auto client = std::make_shared(uri, mSession, flags); if (!client) { - CONSOLE(rootConfig) << "Failed to create client for extension: " << uri; + if (required) mFailState = true; + CONSOLE(mSession) << "Failed to create client for extension: " << uri; return; } auto activity = getActivity(uri); @@ -403,35 +424,46 @@ ExtensionMediator::grantExtension(const RootConfigPtr& rootConfig, const std::st } void -ExtensionMediator::denyExtension(const RootConfigPtr& rootConfig, const std::string& uri) { +ExtensionMediator::denyExtension(const std::string& uri) +{ mPendingGrants.erase(uri); - LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "Extension denied: " << uri; + if (mRequired.count(uri)) { + mFailState = true; + LOG(LogLevel::kError) << "Required extension " << uri << " denied."; + } + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mSession) << "Extension denied: " << uri; } void -ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const ContentPtr& content) +ExtensionMediator::loadExtensionsInternal(const ObjectMap& flagMap, const ContentPtr& content) { + if (mFailState) { + if (mLoadedCallback) { + mLoadedCallback(false); + } + return; + } if (!mPendingGrants.empty()) { - LOG(LogLevel::kWarn).session(content->getSession()) << "Loading extensions with pending grant requests. " + LOG(LogLevel::kWarn).session(mSession) << "Loading extensions with pending grant requests. " << "Failure to grant extension use makes the extension unavailable for the session."; mPendingGrants.clear(); } // No extensions to load auto extensionProvider = mProvider.lock(); - if (rootConfig == nullptr || mPendingRegistrations.empty() || extensionProvider == nullptr) { + if (mPendingRegistrations.empty() || extensionProvider == nullptr) { if (mLoadedCallback) { - mLoadedCallback(); + mLoadedCallback(true); } return; } - auto session = rootConfig->getSession(); auto pendingRegistrations = mPendingRegistrations; for (const auto& uri : pendingRegistrations) { - LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "load extension: " << uri + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mSession) << "load extension: " << uri << " has extension: " << extensionProvider->hasExtension(uri); + bool required = mRequired.count(uri); auto activity = getActivity(uri); auto sessionState = getExtensionSessionState(); @@ -439,19 +471,32 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const if (sessionState && sessionState->getState(activity) != ExtensionLifecycleStage::kExtensionInitialized) { LOG(LogLevel::kError) << "Ignoring registration for uninitialized extension: " << uri; mPendingRegistrations.erase(uri); + + if (required) { + mFailState = true; + CONSOLE(mSession) << "Required extension " << uri << " not initialized."; + break; + } + continue; } // Get the extension from the registration auto proxy = extensionProvider->getExtension(uri); if (!proxy) { - CONSOLE(session) << "Failed to retrieve proxy for extension: " << uri; + CONSOLE(mSession) << "Failed to retrieve proxy for extension: " << uri; mPendingRegistrations.erase(uri); if (sessionState) { // The update shouldn't fail because we know that the activity is known and // the transition should be allowed. sessionState->updateState(activity, ExtensionLifecycleStage::kExtensionFinalized); } + + if (required) { + mFailState = true; + break; + } + continue; } @@ -459,10 +504,16 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const rapidjson::Document settingsDoc; auto settings = content->getExtensionSettings(uri).serialize(settingsDoc.GetAllocator()); rapidjson::Document flagsDoc; - auto flags = rootConfig->getExtensionFlags(uri).serialize(flagsDoc.GetAllocator()); + + Object flags; + auto it = flagMap.find(uri); + if (it != flagMap.end()) + flags = it->second; + auto serializedFlags = flags.serialize(flagsDoc.GetAllocator()); + rapidjson::Document regReq; // TODO: It was uri instead of the version here. Funny. We need to figure if it's schema or interface here. - regReq.Swap(RegistrationRequest("1.0").uri(uri).settings(settings).flags(flags).getDocument()); + regReq.Swap(RegistrationRequest("1.0").uri(uri).settings(settings).flags(serializedFlags).getDocument()); std::weak_ptr weak_this(shared_from_this()); auto success = proxy->getRegistration(*activity, regReq, @@ -478,7 +529,7 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const if (!success) { // call to extension failed without failure callback - CONSOLE(session) << "Extension registration failure - code: " << kErrorInvalidMessage + CONSOLE(mSession) << "Extension registration failure - code: " << kErrorInvalidMessage << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; mPendingRegistrations.erase(uri); if (sessionState) { @@ -486,11 +537,16 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const // the transition should be allowed. sessionState->updateState(activity, ExtensionLifecycleStage::kExtensionFinalized); } + + if (required) { + mFailState = true; + break; + } } } if (mPendingRegistrations.empty() && mLoadedCallback) { - mLoadedCallback(); + mLoadedCallback(!mFailState); mLoadedCallback = nullptr; } } @@ -499,13 +555,24 @@ void ExtensionMediator::loadExtensions(const RootConfigPtr& rootConfig, const ContentPtr& content, const std::set* grantedExtensions) { - mRootConfig = rootConfig; + if (rootConfig == nullptr) { + return; + } + + loadExtensions(rootConfig->getExtensionFlags(), content, grantedExtensions); +} + +void +ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, + const std::set* grantedExtensions) { if (!content->isReady()) { - CONSOLE(rootConfig) << "Cannot load extensions when Content is not ready"; + CONSOLE(content) << "Cannot load extensions when Content is not ready"; return; } - initializeExtensions(rootConfig, content, + mSession = content->getSession(); + + initializeExtensions(flagMap, content, [&grantedExtensions](const std::string& uri, ExtensionMediator::ExtensionGrantResult grant, ExtensionMediator::ExtensionGrantResult deny) { if (grantedExtensions == nullptr) { @@ -520,26 +587,59 @@ ExtensionMediator::loadExtensions(const RootConfigPtr& rootConfig, const Content deny(uri); } }); - loadExtensionsInternal(rootConfig, content); + + loadExtensionsInternal(flagMap, content); } + void ExtensionMediator::loadExtensions( const RootConfigPtr& rootConfig, const ContentPtr& content, ExtensionsLoadedCallback loaded) { - mRootConfig = rootConfig; + auto callbackV2 = [loaded](bool) { loaded(); }; + loadExtensions(rootConfig, content, std::move(callbackV2)); +} + +void +ExtensionMediator::loadExtensions( + const RootConfigPtr& rootConfig, + const ContentPtr& content, + ExtensionsLoadedCallbackV2 loaded) +{ mLoadedCallback = std::move(loaded); - loadExtensionsInternal(rootConfig, content); + + // Immediately mark extensions as loaded without a root config + if (rootConfig == nullptr) { + if (mLoadedCallback) { + mLoadedCallback(true); + } + } + + loadExtensionsInternal(rootConfig->getExtensionFlags(), content); +} + +void +ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, + ExtensionsLoadedCallback loaded) { + auto callbackV2 = [loaded](bool) { loaded(); }; + loadExtensions(flagMap, content, std::move(callbackV2)); +} + +void +ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, + ExtensionsLoadedCallbackV2 loaded) { + mLoadedCallback = std::move(loaded); + loadExtensionsInternal(flagMap, content); } bool ExtensionMediator::invokeCommand(const apl::Event& event) { - auto root = mRootContext.lock(); + auto documentContext = mDocumentContext.lock(); - if (event.getType() != kEventTypeExtension || !root) + if (event.getType() != kEventTypeExtension || !documentContext) return false; auto uri = event.getValue(EventProperty::kEventPropertyExtensionURI).asString(); @@ -548,7 +648,7 @@ ExtensionMediator::invokeCommand(const apl::Event& event) auto itr = mClients.find(uri); auto extPro = mProvider.lock(); if (itr == mClients.end() || extPro == nullptr || !extPro->hasExtension(uri)) { - CONSOLE(root->getSession()) << "Attempt to execute command on unavailable extension - uri: " << uri; + CONSOLE(documentContext) << "Attempt to execute command on unavailable extension - uri: " << uri; return false; } auto client = itr->second; @@ -556,7 +656,7 @@ ExtensionMediator::invokeCommand(const apl::Event& event) // Get the Extension auto proxy = extPro->getExtension(uri); if (!proxy) { - CONSOLE(root->getSession()) << "Attempt to execute command on unavailable extension - uri: " << uri; + CONSOLE(documentContext) << "Attempt to execute command on unavailable extension - uri: " << uri; return false; } @@ -578,8 +678,9 @@ ExtensionMediator::invokeCommand(const apl::Event& event) }); if (!invoke) { - CONSOLE(root->getSession()) << "Extension command failure - code: " << kErrorInvalidMessage - << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; + CONSOLE(documentContext) + << "Extension command failure - code: " << kErrorInvalidMessage + << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; } return invoke; @@ -590,8 +691,7 @@ ExtensionMediator::getProxy(const std::string &uri) { auto extPro = mProvider.lock(); if (extPro == nullptr || !extPro->hasExtension(uri)) { - auto config = mRootConfig.lock(); - CONSOLE(config) << "Proxy does not exist for uri: " << uri; + CONSOLE(mSession) << "Proxy does not exist for uri: " << uri; return nullptr; } return extPro->getExtension(uri); @@ -602,11 +702,16 @@ ExtensionMediator::getClient(const std::string &uri) { auto itr = mClients.find(uri); if (itr == mClients.end()) { - auto config = mRootConfig.lock(); - CONSOLE(config) << "Attempt to use an unavailable extension - uri: " << uri; + CONSOLE(mSession) << "Attempt to use an unavailable extension - uri: " << uri; return nullptr; } - return itr->second; + return itr->second; +} + +const std::map& +ExtensionMediator::getClients() +{ + return mClients; } void @@ -628,9 +733,8 @@ ExtensionMediator::notifyComponentUpdate(const ExtensionComponentPtr& component, // Notify the extension of the component change auto sent = proxy->sendComponentMessage(*activity, message); if (!sent) { - auto config = mRootConfig.lock(); - CONSOLE(config) << "Extension message failure - code: " << kErrorInvalidMessage - << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; + CONSOLE(mSession) << "Extension message failure - code: " << kErrorInvalidMessage + << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; return; } @@ -672,11 +776,11 @@ ExtensionMediator::sendResourceReady(const std::string& uri, const alexaext::Res void ExtensionMediator::resourceFail(const ExtensionComponentPtr& component, int errorCode, const std::string& error) { - auto root = mRootContext.lock(); + auto document = mDocumentContext.lock(); if (!component) return; - CONSOLE(root->getSession()) << "Extension resource failure - uri:" - << component->getUri() << " resourceId:" << component->getResourceID(); + CONSOLE(document) << "Extension resource failure - uri:" << component->getUri() + << " resourceId:" << component->getResourceID(); component->updateResourceState(kResourceError, errorCode, error); } @@ -749,7 +853,8 @@ ExtensionMediator::registerExtension(const std::string& uri, const ExtensionProx extension->onRegistered(*activity); - LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mRootContext.lock()) << "registered: " << uri << " clients: " << mClients.size(); + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mDocumentContext.lock()) + << "registered: " << uri << " clients: " << mClients.size(); } void @@ -768,7 +873,8 @@ ExtensionMediator::enqueueResponse(const alexaext::ActivityDescriptorPtr& activi } }); if (!enqueued) - LOG(LogLevel::kWarn).session(mRootContext.lock()) << "failed to process message for extension, uri:" << uri; + LOG(LogLevel::kWarn).session(mDocumentContext.lock()) + << "failed to process message for extension, uri:" << uri; } void @@ -785,8 +891,8 @@ ExtensionMediator::processMessage(const alexaext::ActivityDescriptorPtr& activit bool needsRegistration = !client->second->registered() && !client->second->registrationMessageProcessed(); // client handles null root - auto root = mRootContext.lock(); - client->second->processMessage(root, std::move(processMessage)); + auto root = mDocumentContext.lock(); + client->second->processMessageInternal(root, std::move(processMessage)); // register handlers if registered for first time if (needsRegistration) { @@ -796,7 +902,11 @@ ExtensionMediator::processMessage(const alexaext::ActivityDescriptorPtr& activit registerExtension(uri, proxy, client->second); } } else if (client->second->registrationFailed()) { - // Registration failed, since registration was processed but th + if (mRequired.count(uri)) { + mFailState = true; + } + + // Registration failed, since registration was processed but extension returned fail if (auto state = getExtensionSessionState()) { // The update shouldn't fail because we know that the activity is known and // the transition should be allowed. @@ -807,7 +917,7 @@ ExtensionMediator::processMessage(const alexaext::ActivityDescriptorPtr& activit if (mPendingRegistrations.count(uri)) { mPendingRegistrations.erase(uri); if (mPendingRegistrations.empty() && mLoadedCallback) { - mLoadedCallback(); + mLoadedCallback(!mFailState); mLoadedCallback = nullptr; } } @@ -930,6 +1040,7 @@ ExtensionMediator::unregister(const alexaext::ActivityDescriptorPtr& activity) { } } + } // namespace apl #endif diff --git a/aplcore/src/focus/focusfinder.cpp b/aplcore/src/focus/focusfinder.cpp index e36d1a1..f99c1e6 100644 --- a/aplcore/src/focus/focusfinder.cpp +++ b/aplcore/src/focus/focusfinder.cpp @@ -15,10 +15,10 @@ #include "apl/focus/focusfinder.h" +#include "apl/component/corecomponent.h" +#include "apl/document/documentcontextdata.h" #include "apl/focus/beamintersect.h" #include "apl/focus/focusvisitor.h" -#include "apl/engine/rootcontextdata.h" -#include "apl/component/corecomponent.h" namespace apl { diff --git a/aplcore/src/focus/focusmanager.cpp b/aplcore/src/focus/focusmanager.cpp index fe7cc01..08cfbf1 100644 --- a/aplcore/src/focus/focusmanager.cpp +++ b/aplcore/src/focus/focusmanager.cpp @@ -14,11 +14,12 @@ */ #include "apl/focus/focusmanager.h" + #include "apl/action/scrolltoaction.h" #include "apl/component/corecomponent.h" +#include "apl/engine/corerootcontext.h" #include "apl/engine/event.h" -#include "apl/engine/rootcontextdata.h" -#include "apl/primitives/rect.h" +#include "apl/time/sequencer.h" #include "apl/time/timemanager.h" #include "apl/utils/make_unique.h" @@ -27,7 +28,7 @@ namespace apl { static const bool DEBUG_FOCUS = false; static const std::string FOCUS_RELEASE_SEQUENCER = "__FOCUS_RELEASE_SEQUENCER"; -FocusManager::FocusManager(const RootContextData& core) : +FocusManager::FocusManager(const CoreRootContext& core) : mCore(core), mFinder(std::make_unique()) {} @@ -38,7 +39,8 @@ FocusManager::reportFocusedComponent() if (focused) { EventBag bag; Rect bounds; - focused->getBoundsInParent(mCore.top(), bounds); + // TODO: Should cross-cut through documents. + focused->getBoundsInParent(mCore.topComponent(), bounds); bag.emplace(kEventPropertyValue, Object(std::move(bounds))); focused->getContext()->pushEvent(Event(kEventTypeFocus, std::move(bag), focused)); } @@ -145,7 +147,7 @@ FocusManager::clearFocus(bool notifyViewhost, FocusDirection direction, bool for } else { auto timers = std::static_pointer_cast(mCore.rootConfig().getTimeManager()); Rect bounds; - focused->getBoundsInParent(mCore.top(), bounds); + focused->getBoundsInParent(mCore.topComponent(), bounds); auto boundsObject = Object(std::move(bounds)); auto action = Action::make(timers, [focused, boundsObject, direction](ActionRef ref) { EventBag bag; @@ -214,7 +216,7 @@ FocusManager::focus(FocusDirection direction) return true; } else { - auto origin = generateOrigin(direction, mCore.top()->getCalculated(kPropertyBounds).get()); + auto origin = generateOrigin(direction, mCore.topComponent()->getCalculated(kPropertyBounds).get()); return focus(direction, origin); } } @@ -268,7 +270,7 @@ FocusManager::find(FocusDirection direction) CoreComponentPtr FocusManager::find(FocusDirection direction, const Rect& origin) { - return mFinder->findNext(mFocused.lock(), origin, direction, CoreComponent::cast(mCore.top())); + return mFinder->findNext(mFocused.lock(), origin, direction, CoreComponent::cast(mCore.topComponent())); } CoreComponentPtr @@ -281,7 +283,7 @@ std::map FocusManager::getFocusableAreas() { std::map result; - auto root = CoreComponent::cast(mCore.top()); + auto root = CoreComponent::cast(mCore.topComponent()); auto focusables = mFinder->getFocusables(root, false); if(root->isFocusable()) { focusables.push_back(root); diff --git a/aplcore/src/graphic/CMakeLists.txt b/aplcore/src/graphic/CMakeLists.txt index fd25f3c..05818bd 100644 --- a/aplcore/src/graphic/CMakeLists.txt +++ b/aplcore/src/graphic/CMakeLists.txt @@ -16,7 +16,6 @@ target_sources_local(apl graphic.cpp graphicbuilder.cpp graphiccontent.cpp - graphicdependant.cpp graphicelement.cpp graphicelementcontainer.cpp graphicelementgroup.cpp diff --git a/aplcore/src/graphic/graphic.cpp b/aplcore/src/graphic/graphic.cpp index cd0c89c..340163c 100644 --- a/aplcore/src/graphic/graphic.cpp +++ b/aplcore/src/graphic/graphic.cpp @@ -17,7 +17,7 @@ #include "apl/component/vectorgraphiccomponent.h" #include "apl/engine/arrayify.h" -#include "apl/engine/contextdependant.h" +#include "apl/engine/typeddependant.h" #include "apl/engine/propdef.h" #include "apl/engine/resources.h" #include "apl/graphic/graphicbuilder.h" @@ -194,16 +194,14 @@ Graphic::initialize(const ContextPtr& sourceContext, // Check if there is an assigned property auto it = properties.find(param.name); if (it != properties.end()) { - mAssigned.emplace(param.name); // Mark this as an assigned property - - // If the assigned property is a string, check for data-binding - if (it->second.isString()) { - parsed = parseDataBinding(*sourceContext, it->second.getString()); - value = conversionFunc(*sourceContext, evaluate(*sourceContext, parsed)); - } - else { - value = conversionFunc(*sourceContext, evaluate(*sourceContext, it->second)); - } + mAssigned.emplace(param.name); // Mark this as an assigned property + auto result = parseAndEvaluate(*sourceContext, it->second); + value = conversionFunc(*sourceContext, result.value); + + if (!result.symbols.empty()) + ContextDependant::create(mContext, param.name, std::move(result.expression), + sourceContext, conversionFunc, + std::move(result.symbols)); } else if (styledPtr) { // Look for a styled value auto itStyle = styledPtr->find(param.name); @@ -214,10 +212,6 @@ Graphic::initialize(const ContextPtr& sourceContext, // Store the calculated value in the data-binding context LOG_IF(DEBUG_GRAPHIC).session(sourceContext) << "Storing parameter '" << param.name << "' = " << value; mContext->putUserWriteable(param.name, value); - - // After storing the parameter we can wire up any necessary data dependant - if (parsed.isEvaluable()) - ContextDependant::create(mContext, param.name, parsed, sourceContext, conversionFunc); } auto self = std::static_pointer_cast(shared_from_this()); diff --git a/aplcore/src/graphic/graphicbuilder.cpp b/aplcore/src/graphic/graphicbuilder.cpp index 55e0e45..2870fab 100644 --- a/aplcore/src/graphic/graphicbuilder.cpp +++ b/aplcore/src/graphic/graphicbuilder.cpp @@ -16,7 +16,7 @@ #include "apl/graphic/graphicbuilder.h" #include "apl/engine/arrayify.h" -#include "apl/engine/contextdependant.h" +#include "apl/engine/typeddependant.h" #include "apl/graphic/graphic.h" #include "apl/graphic/graphicelementcontainer.h" @@ -93,7 +93,7 @@ GraphicBuilder::addChildren(GraphicElement& element, const Object& json) // TODO: Add live data object later (maybe). Right now LiveData isn't quite exposed to AVG. if (mMultichildSupport) { const auto data = arrayifyPropertyAsObject(context, json, "data"); - const auto dataItems = evaluateRecursive(context, data); + const auto dataItems = evaluateNested(context, data); if (!dataItems.empty()) { LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << "Data child inflation: " << dataItems; const auto length = dataItems.size(); @@ -162,17 +162,14 @@ GraphicBuilder::createChild(const ContextPtr& context, const Object& json) } // Extract the binding as an optional node tree. - auto tmp = propertyAsNode(*expanded, binding, "value"); - auto value = evaluateRecursive(*expanded, tmp); - auto bindingType = propertyAsMapped(*expanded, binding, "type", kBindingTypeAny, sBindingMap); + auto result = parseAndEvaluate(*context, binding.get("value")); + auto bindingType = + propertyAsMapped(*expanded, binding, "type", kBindingTypeAny, sBindingMap); auto bindingFunc = sBindingFunctions.at(bindingType); - - // Store the value in the new context. Binding values are mutable; they can be changed later. - expanded->putUserWriteable(name, bindingFunc(*expanded, value)); - - // If it is a node, we connect up the symbols that it is dependant upon - if (tmp.isEvaluable()) - ContextDependant::create(expanded, name, tmp, expanded, bindingFunc); + context->putUserWriteable(name, bindingFunc(*context, result.value)); + if (!result.symbols.empty()) + ContextDependant::create(context, name, std::move(result.expression), context, + std::move(bindingFunc), std::move(result.symbols)); } // Inflate the child diff --git a/aplcore/src/graphic/graphicdependant.cpp b/aplcore/src/graphic/graphicdependant.cpp deleted file mode 100644 index bccc4af..0000000 --- a/aplcore/src/graphic/graphicdependant.cpp +++ /dev/null @@ -1,63 +0,0 @@ -/** - * Copyright 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 "apl/graphic/graphicdependant.h" -#include "apl/graphic/graphicelement.h" -#include "apl/graphic/graphic.h" -#include "apl/engine/context.h" -#include "apl/engine/evaluate.h" -#include "apl/primitives/symbolreferencemap.h" - -namespace apl { - -const static bool DEBUG_GRAPHIC_DEP = false; - -void -GraphicDependant::create(const GraphicElementPtr& downstreamGraphicElement, - GraphicPropertyKey downstreamKey, - const Object& equation, - const ContextPtr& bindingContext, - BindingFunction bindingFunction) -{ - LOG_IF(DEBUG_GRAPHIC_DEP).session(bindingContext) << " to " << sGraphicPropertyBimap.at(downstreamKey) - << "(" << downstreamGraphicElement.get() << ")"; - - SymbolReferenceMap symbols; - equation.symbols(symbols); - if (symbols.empty()) - return; - - auto dependant = std::make_shared(downstreamGraphicElement, downstreamKey, equation, - bindingContext, bindingFunction); - - for (const auto& symbol : symbols.get()) - symbol.second->addDownstream(symbol.first, dependant); - - downstreamGraphicElement->addUpstream(downstreamKey, dependant); -} - -void -GraphicDependant::recalculate(bool useDirtyFlag) const -{ - auto downstream = mDownstreamGraphicElement.lock(); - auto bindingContext = mBindingContext.lock(); - if (downstream && bindingContext) { - auto value = mBindingFunction(*bindingContext, reevaluate(*bindingContext, mEquation)); - LOG_IF(DEBUG_GRAPHIC_DEP).session(bindingContext) << " new value " << value.toDebugString(); - downstream->setValue(mDownstreamKey, value, useDirtyFlag); - } -} - -} // namespace apl diff --git a/aplcore/src/graphic/graphicelement.cpp b/aplcore/src/graphic/graphicelement.cpp index df9fc83..ccf2759 100644 --- a/aplcore/src/graphic/graphicelement.cpp +++ b/aplcore/src/graphic/graphicelement.cpp @@ -17,8 +17,8 @@ #include "apl/component/corecomponent.h" #include "apl/engine/propdef.h" +#include "apl/engine/typeddependant.h" #include "apl/graphic/graphic.h" -#include "apl/graphic/graphicdependant.h" #include "apl/graphic/graphicpattern.h" #include "apl/graphic/graphicpropdef.h" #include "apl/primitives/color.h" @@ -29,12 +29,12 @@ #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #include "apl/scenegraph/graphicfragment.h" -#include "apl/scenegraph/node.h" #include "apl/scenegraph/scenegraphupdates.h" #endif // SCENEGRAPH namespace apl { +using GraphicDependant = TypedDependant; Object GraphicElement::asAvgFill(const Context& context, const Object& object) { @@ -84,23 +84,16 @@ GraphicElement::initialize(const GraphicPtr& graphic, const Object& json) if ((pd.flags & kPropIn) != 0) { auto p = mProperties.find(pd.names); if (p != mProperties.end()) { - // If the user assigned a string, we need to check for data-binding - if (p->second.isString()) { - auto tmp = parseDataBinding(*mContext, p->second.getString()); - // Can be standalone without parent -> no dependency in such case. - if (mGraphic.lock() && tmp.isEvaluable()) { - auto self = std::static_pointer_cast(shared_from_this()); - GraphicDependant::create(self, pd.key, tmp, mContext, pd.getBindingFunction()); - } - value = pd.calculate(*mContext, evaluate(*mContext, tmp)); - } else if (mGraphic.lock() && (pd.flags & kPropEvaluated) != 0) { - // Explicitly marked for evaluation, so do it. - // Will not attach dependant if no valid symbols. - auto tmp = parseDataBindingRecursive(*mContext, p->second); - auto self = std::static_pointer_cast(shared_from_this()); - GraphicDependant::create(self, pd.key, tmp, mContext, pd.getBindingFunction()); - value = pd.calculate(*mContext, p->second); - } else { // Not a string - just calculate it directly + if (p->second.isString() || (pd.flags & kPropEvaluated) != 0) { + auto result = parseAndEvaluate(*mContext, p->second); + if (!result.symbols.empty() && mGraphic.lock()) + GraphicDependant::create(shared_from_this(), pd.key, + std::move(result.expression), + mContext, pd.getBindingFunction(), + std::move(result.symbols)); + value = pd.calculate(*mContext, result.value); + } + else { value = pd.calculate(*mContext, p->second); } mAssigned.emplace(pd.key); @@ -118,8 +111,6 @@ GraphicElement::initialize(const GraphicPtr& graphic, const Object& json) CONSOLE(mContext) << "Missing required graphic property: " << pd.names; return false; } - - } } @@ -146,12 +137,12 @@ GraphicElement::getLayoutDirection() const { graphic->getRoot()->getValue(kGraphicPropertyLayoutDirection).asInt()); } -bool +void GraphicElement::setValue(GraphicPropertyKey key, const Object& value, bool useDirtyFlag) { // Assume that the property already exists if (mValues.get(key) == value) - return false; + return; // See if we have it at all const auto& pds = propDefSet(); @@ -168,8 +159,6 @@ GraphicElement::setValue(GraphicPropertyKey key, const Object& value, bool useDi if (useDirtyFlag && (it->second.flags & kPropOut) && mDirtyProperties.emplace(key).second) { markAsDirty(); } - - return true; } rapidjson::Value diff --git a/aplcore/src/graphic/graphicelementtext.cpp b/aplcore/src/graphic/graphicelementtext.cpp index 1e6b787..dbf69f8 100644 --- a/aplcore/src/graphic/graphicelementtext.cpp +++ b/aplcore/src/graphic/graphicelementtext.cpp @@ -352,9 +352,11 @@ GraphicElementText::ensureTextProperties() mTextProperties = sg::TextProperties::create( mContext->textPropertiesCache(), sg::splitFontString(mContext->getRootConfig(), + mContext->session(), getValue(kGraphicPropertyFontFamily).getString()), getValue(kGraphicPropertyFontSize).asFloat(), static_cast(getValue(kGraphicPropertyFontStyle).getInteger()), + getLang(), getValue(kGraphicPropertyFontWeight).getInteger(), getValue(kGraphicPropertyLetterSpacing).asFloat()); } diff --git a/aplcore/src/livedata/livedataobject.cpp b/aplcore/src/livedata/livedataobject.cpp index e40e2c4..4ce8f2c 100644 --- a/aplcore/src/livedata/livedataobject.cpp +++ b/aplcore/src/livedata/livedataobject.cpp @@ -15,6 +15,7 @@ #include "apl/livedata/livedataobject.h" #include "apl/engine/context.h" +#include "apl/engine/dependantmanager.h" #include "apl/livedata/livedatamanager.h" #include "apl/livedata/livearrayobject.h" #include "apl/livedata/livemapobject.h" @@ -58,8 +59,10 @@ LiveDataObject::flush() { auto context = mContext.lock(); mIsFlushing = true; - if (context) - context->recalculateDownstream(mKey, true); + if (context) { + context->enqueueDownstream(mKey); + context->dependantManager().processDependencies(true); + } // Make a copy to ensure sane iteration because it's possible that calling a callback will add more callbacks std::map flushCallbacksCopy{mFlushCallbacks}; diff --git a/aplcore/src/primitives/CMakeLists.txt b/aplcore/src/primitives/CMakeLists.txt index 1cb3692..bc44044 100644 --- a/aplcore/src/primitives/CMakeLists.txt +++ b/aplcore/src/primitives/CMakeLists.txt @@ -14,6 +14,8 @@ target_sources_local(apl PRIVATE accessibilityaction.cpp + boundsymbol.cpp + boundsymbolset.cpp color.cpp dimension.cpp filter.cpp @@ -29,7 +31,6 @@ target_sources_local(apl roundedrect.cpp styledtext.cpp styledtextstate.cpp - symbolreferencemap.cpp timefunctions.cpp timegrammar.cpp transform.cpp diff --git a/aplcore/src/primitives/accessibilityaction.cpp b/aplcore/src/primitives/accessibilityaction.cpp index b8d5835..43ec5b5 100644 --- a/aplcore/src/primitives/accessibilityaction.cpp +++ b/aplcore/src/primitives/accessibilityaction.cpp @@ -13,12 +13,13 @@ * permissions and limitations under the License. */ +#include "apl/primitives/accessibilityaction.h" #include "apl/component/corecomponent.h" -#include "apl/engine/componentdependant.h" +#include "apl/engine/typeddependant.h" +#include "apl/engine/dependantmanager.h" #include "apl/engine/evaluate.h" #include "apl/engine/propdef.h" -#include "apl/primitives/accessibilityaction.h" -#include "apl/primitives/symbolreferencemap.h" +#include "apl/primitives/boundsymbolset.h" #include "apl/time/sequencer.h" #include "apl/utils/session.h" @@ -30,45 +31,8 @@ namespace apl { * action properties dynamic in the future (such as "label"), this class will need to be modified * so that we can differentiate which property is being driven. */ -class AccessibilityActionDependant : public Dependant { -public: - static void create(const std::shared_ptr& downstreamAccessibilityAction, - const Object& equation, - const ContextPtr& bindingContext) - { - SymbolReferenceMap symbols; - equation.symbols(symbols); - if (symbols.empty()) - return; - - auto dependant = std::make_shared(downstreamAccessibilityAction, - equation, - bindingContext); - - for (const auto& symbol : symbols.get()) - symbol.second->addDownstream(symbol.first, dependant); - - downstreamAccessibilityAction->addUpstream(kAccessibilityActionEnabled, dependant); - } - - AccessibilityActionDependant(const std::shared_ptr& downstreamAccessibilityAction, - const Object& equation, - const ContextPtr& bindingContext) - : Dependant(equation, bindingContext, asBoolean), - mDownstreamAccessibilityAction(downstreamAccessibilityAction) {} - - void recalculate(bool useDirtyFlag) const override { - auto downstream = mDownstreamAccessibilityAction.lock(); - auto bindingContext = mBindingContext.lock(); - if (downstream && bindingContext) { - auto value = reevaluate(*bindingContext, mEquation).asBoolean(); - downstream->setEnabled(value, useDirtyFlag); - } - } -private: - std::weak_ptr mDownstreamAccessibilityAction; -}; +using AccessibilityActionDependent = TypedDependant; std::shared_ptr AccessibilityAction::create(const CoreComponentPtr& component, const Object& object) @@ -109,14 +73,15 @@ AccessibilityAction::initialize(const ContextPtr& context, const Object& object) if (object.has("enabled")) { auto assigned = object.get("enabled"); if (assigned.isString()) { // It may be a data-binding - auto tmp = parseDataBinding(*context, assigned.getString()); - if (tmp.isEvaluable()) { - AccessibilityActionDependant::create(shared_from_this(), tmp, context); - mEnabled = evaluate(*context, tmp).asBoolean(); - } - else { - mEnabled = tmp.asBoolean(); - } + auto result = parseAndEvaluate(*context, assigned); + if (!result.symbols.empty()) + AccessibilityActionDependent::create(shared_from_this(), + kAccessibilityActionEnabled, + std::move(result.expression), + context, + sBindingFunctions.at(kBindingTypeBoolean), + std::move(result.symbols)); + mEnabled = result.value.asBoolean(); } else { mEnabled = assigned.asBoolean(); @@ -144,8 +109,10 @@ AccessibilityAction::serialize(rapidjson::Document::AllocatorType& allocator) co } void -AccessibilityAction::setEnabled(bool enabled, bool useDirtyFlag) +AccessibilityAction::setValue(AccessibilityActionKey key, const Object& value, bool useDirtyFlag) { + assert(key == kAccessibilityActionEnabled); + auto enabled = value.getBoolean(); if (enabled != mEnabled) { mEnabled = enabled; if (useDirtyFlag) { diff --git a/aplcore/src/datagrammar/boundsymbol.cpp b/aplcore/src/primitives/boundsymbol.cpp similarity index 64% rename from aplcore/src/datagrammar/boundsymbol.cpp rename to aplcore/src/primitives/boundsymbol.cpp index 06d5889..9215b4d 100644 --- a/aplcore/src/datagrammar/boundsymbol.cpp +++ b/aplcore/src/primitives/boundsymbol.cpp @@ -13,11 +13,31 @@ * permissions and limitations under the License. */ -#include "apl/datagrammar/boundsymbol.h" +#include "apl/primitives/boundsymbol.h" #include "apl/engine/context.h" namespace apl { -namespace datagrammar { + +bool +BoundSymbol::empty() const +{ + auto context = mContext.lock(); + return context ? context->opt(mName).empty() : true; +} + +bool +BoundSymbol::truthy() const +{ + auto context = mContext.lock(); + return context ? context->opt(mName).truthy() : false; +} + +rapidjson::Value +BoundSymbol::serialize(rapidjson::Document::AllocatorType& allocator) const +{ + return {"BOUND SYMBOL", allocator}; +} + Object BoundSymbol::eval() const @@ -39,10 +59,18 @@ BoundSymbol::operator==(const BoundSymbol& rhs) const mName == rhs.mName; } +bool +BoundSymbol::operator<(const BoundSymbol& rhs) const +{ + auto result = mName.compare(rhs.mName); + if (result < 0) return true; + if (result > 1) return false; + return mContext.owner_before(rhs.mContext); +} + streamer& operator<<(streamer& os, const BoundSymbol& boundSymbol) { os << boundSymbol.toDebugString(); return os; } -} // namespace datagrammar } // namespace apl diff --git a/aplcore/src/primitives/boundsymbolset.cpp b/aplcore/src/primitives/boundsymbolset.cpp new file mode 100644 index 0000000..72b952f --- /dev/null +++ b/aplcore/src/primitives/boundsymbolset.cpp @@ -0,0 +1,37 @@ +/** + * Copyright 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 "apl/primitives/boundsymbolset.h" + +namespace apl { + +void +BoundSymbolSet::emplace(const BoundSymbol& boundSymbol) +{ + // Use an insertion sort + // Find the first element greater than or equal to this symbol + auto it = std::lower_bound(mSymbols.begin(), + mSymbols.end(), + boundSymbol); + + // If we find a match, don't add anything + if (it != mSymbols.end() && *it == boundSymbol) + return; + + mSymbols.insert(it, boundSymbol); +} + + +} // namespace apl diff --git a/aplcore/src/primitives/gradient.cpp b/aplcore/src/primitives/gradient.cpp index c407af6..9dde72b 100644 --- a/aplcore/src/primitives/gradient.cpp +++ b/aplcore/src/primitives/gradient.cpp @@ -126,13 +126,13 @@ Gradient::create(const Context& context, const Object& object, bool avg) std::map properties; - colorRange = evaluateRecursive(context, colorRange); + colorRange = evaluateNested(context, colorRange); std::vector colors; for (const auto& m : colorRange.getArray()) colors.emplace_back(m.asColor(context)); - inputRange = evaluateRecursive(context, inputRange); + inputRange = evaluateNested(context, inputRange); std::vector inputs; if (!inputRange.empty()) { diff --git a/aplcore/src/primitives/mediasource.cpp b/aplcore/src/primitives/mediasource.cpp index 734f980..c2edd3b 100644 --- a/aplcore/src/primitives/mediasource.cpp +++ b/aplcore/src/primitives/mediasource.cpp @@ -76,7 +76,7 @@ MediaSource::create(const Context& context, const Object& object) auto entities = Object(arrayifyProperty(context, object, "entities", "entity")); TextTrackArray tracks; - for (auto& m : arrayifyProperty(context, object, "textTrack")) { + for (auto& m : arrayifyProperty(context, object, "textTracks", "textTrack")) { if (!m.isMap()) { CONSOLE(context) << "Text Track is not an object."; continue; @@ -142,7 +142,7 @@ MediaSource::serialize(rapidjson::Document::AllocatorType& allocator) const { t.AddMember("type", Value(sTextTrackTypeMap.at(track.type).c_str(), allocator), allocator); vTextTracks.PushBack(t, allocator); } - v.AddMember("textTracks", vTextTracks, allocator); + v.AddMember("textTrack", vTextTracks, allocator); return v; } diff --git a/aplcore/src/primitives/object.cpp b/aplcore/src/primitives/object.cpp index 6e4d530..228eea3 100644 --- a/aplcore/src/primitives/object.cpp +++ b/aplcore/src/primitives/object.cpp @@ -17,9 +17,8 @@ #include -#include "apl/datagrammar/boundsymbol.h" -#include "apl/datagrammar/bytecode.h" #include "apl/engine/context.h" +#include "apl/primitives/boundsymbol.h" #include "apl/primitives/color.h" #include "apl/primitives/dimension.h" #include "apl/primitives/functions.h" @@ -358,7 +357,7 @@ Object::Object(rapidjson::Document&& value) : mType(Null::ObjectType::instance() break; case rapidjson::kStringType: mType = String::ObjectType::instance(); - mU.string = value.GetString(); // TODO: Should we keep the string in place? + new(&mU.string) std::string(value.GetString()); break; case rapidjson::kObjectType: mType = Map::ObjectType::instance(); @@ -520,79 +519,6 @@ Object::isPure() const return visitor.isPure(); } -const bool DEBUG_SYMBOL_VISITOR = false; - -/** - * Internal visitor class used to extract all symbols and symbol paths from within - * an equation. - */ -class SymbolVisitor : public Visitor { -public: - SymbolVisitor(SymbolReferenceMap& map) : mMap(map) {} - - /** - * Visit an individual object. At the end of this visit, the mCurrentSuffix - * should be set to a valid suffix (either a continuation of the parent or empty). - * @param object The object to visit - */ - void visit(const Object& object) override { - LOG_IF(DEBUG_SYMBOL_VISITOR) << object.toDebugString() - << " mParentSuffix=" << mParentSuffix - << " mIndex=" << mIndex; - - mCurrentSuffix.clear(); // In the majority of cases there will be no suffix - - if (object.is()) { // A bound symbol should be added to the map with existing suffixes - auto symbol = object.get()->getSymbol(); - if (symbol.second) // An invalid bound symbol will not have a context - mMap.emplace(symbol.first + (mIndex == 0 ? mParentSuffix : ""), symbol.second); - } - else if (object.is()) { - object.symbols(mMap); - } - - mIndex++; - } - - /** - * Move down to the child nodes below the current node. Stash information on the - * stack so we can recover state - */ - void push() override { - mStack.push({mIndex, mParentSuffix}); - mParentSuffix = mCurrentSuffix; - mIndex = 0; - } - - /** - * Pop up one level, restoring the state - */ - void pop() override { - const auto& ref = mStack.top(); - mIndex = ref.first; - mParentSuffix = ref.second; - mStack.pop(); - } - -private: - SymbolReferenceMap& mMap; - int mIndex = 0; // The index of the child being visited. - std::string mParentSuffix; // The suffix created by the parent of this object - std::string mCurrentSuffix; // The suffix calculated visiting the current object - std::stack> mStack; // Old indexes and parent suffixes -}; - -void -Object::symbols(SymbolReferenceMap& symbols) const -{ - if (mType == datagrammar::ByteCode::ObjectType::instance()) - get()->symbols(symbols); - else { - SymbolVisitor visitor(symbols); - accept(visitor); - } -} - Object Object::call(const ObjectArray& args) const { return mType->call(mU, args); } size_t Object::hash() const { return mType->hash(mU); } diff --git a/aplcore/src/primitives/symbolreferencemap.cpp b/aplcore/src/primitives/symbolreferencemap.cpp deleted file mode 100644 index 3abc229..0000000 --- a/aplcore/src/primitives/symbolreferencemap.cpp +++ /dev/null @@ -1,85 +0,0 @@ -/** - * Copyright 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 "apl/primitives/symbolreferencemap.h" - -namespace apl { - -/** - * Check the existing map. There are two considerations we need to check. First, - * there may be keys in the map that are supersets of this key. For example, if - * the key is "alpha/0/" and the map contains "alpha/0/first", then the existing - * key in the map should be removed. Second, if there is a key in the map that - * is a subset of this key, then the key should not be added. For example, if - * the key is "alpha/0/" and the map contains "alpha/", then we should not add - * the new key. - * @param key The symbol to look up in the map. - * @param map The map of symbols to contexts - * @return True if the symbol exists - */ -static bool -checkExisting(const std::string& key, std::map& map) -{ - auto it = map.lower_bound(key); - - // First, check for a subset of this key. It should be directly before the lower bound - if (it != map.begin()) { - auto beforeKey = std::prev(it, 1)->first; - if (key.compare(0, beforeKey.size(), beforeKey) == 0) - return false; - } - - // Now look for keys that are a superset of this key - while (it != map.end() && it->first.compare(0, key.size(), key) == 0) - it = map.erase(it); - - return true; -} - -void -SymbolReferenceMap::emplace(const std::string& key, const ContextPtr& value) { - if (checkExisting(key, mMap)) - mMap.emplace(key, value); -} - -void -SymbolReferenceMap::emplace(SymbolReference& ref) { - if (checkExisting(ref.first, mMap)) - mMap.emplace(ref); -} - -void -SymbolReferenceMap::emplace(SymbolReference&& ref) { - if (checkExisting(ref.first, mMap)) - mMap.emplace(std::move(ref)); -} - - -std::string -SymbolReferenceMap::toDebugString() const -{ - std::string result; - for (const auto& m : mMap) { - if (!result.empty()) - result += ", "; - result += m.first; - } - return result; -} - - - - -} // namespace apl diff --git a/aplcore/src/primitives/unicode.cpp b/aplcore/src/primitives/unicode.cpp index 0b2ea8b..370682f 100644 --- a/aplcore/src/primitives/unicode.cpp +++ b/aplcore/src/primitives/unicode.cpp @@ -95,20 +95,25 @@ compareUTF8(const uint8_t *lhs, const uint8_t *rhs, const int trailing) return 0; } - int utf8StringLength(const std::string& utf8String) { - uint8_t *ptr = (uint8_t *) utf8String.c_str(); + return utf8StringLength((uint8_t *) utf8String.data(), utf8String.length()); +} + +int +utf8StringLength(const uint8_t* utf8StringPtr, int count) { + const uint8_t *endPtr = utf8StringPtr + count; int length = 0; - while (*ptr) { - auto byte = *ptr++; + // While it's *NOT* the null terminated and currently before the end pointer. + while (*utf8StringPtr && utf8StringPtr < endPtr) { + auto byte = *utf8StringPtr++; if (!isValidUTF8StartingByte(byte)) return -1; length += 1; for (auto trailing = countUTF8TrailingBytes(byte) ; trailing > 0 ; trailing--) { - if (!isValidUTF8TrailingByte(*ptr++)) + if (utf8StringPtr >= endPtr || !isValidUTF8TrailingByte(*utf8StringPtr++)) return -1; } } diff --git a/aplcore/src/scenegraph/edittextconfig.cpp b/aplcore/src/scenegraph/edittextconfig.cpp index 776767b..e0c9013 100644 --- a/aplcore/src/scenegraph/edittextconfig.cpp +++ b/aplcore/src/scenegraph/edittextconfig.cpp @@ -24,7 +24,6 @@ EditTextConfigPtr EditTextConfig::create(Color textColor, Color highlightColor, KeyboardType keyboardType, - const std::string& language, unsigned int maxLength, bool secureInput, SubmitKeyType submitKeyType, @@ -39,7 +38,6 @@ EditTextConfig::create(Color textColor, ptr->mTextColor = textColor; ptr->mHighlightColor = highlightColor; ptr->mKeyboardType = keyboardType; - ptr->mLanguage = language; ptr->mMaxLength = maxLength; ptr->mSecureInput = secureInput; ptr->mSubmitKeyType = submitKeyType; @@ -82,7 +80,6 @@ EditTextConfig::serialize(rapidjson::Document::AllocatorType& allocator) const rapidjson::Value(sKeyboardBehaviorOnFocusMap.at(mKeyboardBehaviorOnFocus).c_str(), allocator), allocator); - result.AddMember("language", rapidjson::Value(mLanguage.c_str(), allocator), allocator); result.AddMember("maxLength", mMaxLength, allocator); result.AddMember("secureInput", static_cast(mSecureInput), allocator); result.AddMember("selectOnFocus", static_cast(mSelectOnFocus), allocator); diff --git a/aplcore/src/scenegraph/textproperties.cpp b/aplcore/src/scenegraph/textproperties.cpp index ec908b5..e79a65c 100644 --- a/aplcore/src/scenegraph/textproperties.cpp +++ b/aplcore/src/scenegraph/textproperties.cpp @@ -25,6 +25,7 @@ TextProperties::create(TextPropertiesCache& cache, std::vector&& fontFamily, float fontSize, FontStyle fontStyle, + const std::string& language, int fontWeight, float letterSpacing, float lineHeight, @@ -37,6 +38,7 @@ TextProperties::create(TextPropertiesCache& cache, hashCombine(hash, m); hashCombine(hash, fontSize); hashCombine(hash, static_cast(fontStyle)); + hashCombine(hash, language); hashCombine(hash, fontWeight); hashCombine(hash, letterSpacing); hashCombine(hash, lineHeight); @@ -53,6 +55,7 @@ TextProperties::create(TextPropertiesCache& cache, if (p->fontFamily() != fontFamily || p->fontSize() != fontSize || p->fontStyle() != fontStyle || + p->language() != language || p->fontWeight() != fontWeight || p->letterSpacing() != letterSpacing || p->lineHeight() != lineHeight || @@ -70,6 +73,7 @@ TextProperties::create(TextPropertiesCache& cache, p->mFontFamily = std::move(fontFamily); p->mFontSize = fontSize; p->mFontStyle = fontStyle; + p->mLanguage = language; p->mFontWeight = fontWeight; p->mLetterSpacing = letterSpacing; p->mLineHeight = lineHeight; @@ -89,6 +93,7 @@ operator==(const TextProperties& lhs, const TextProperties& rhs) return lhs.mFontFamily == rhs.mFontFamily && lhs.mFontSize == rhs.mFontSize && lhs.mFontStyle == rhs.mFontStyle && + lhs.mLanguage == rhs.mLanguage && lhs.mFontWeight == rhs.mFontWeight && lhs.mLetterSpacing == rhs.mLetterSpacing && lhs.mLineHeight == rhs.mLineHeight && @@ -113,6 +118,7 @@ TextProperties::serialize(rapidjson::Document::AllocatorType& allocator) const result.AddMember("fontFamily", fonts, allocator); result.AddMember("fontSize", mFontSize, allocator); result.AddMember("fontStyle", rapidjson::Value(sFontStyleMap.at(mFontStyle).c_str(), allocator), allocator); + result.AddMember("lang", rapidjson::Value(mLanguage.c_str(), allocator), allocator); result.AddMember("fontWeight", mFontWeight, allocator); result.AddMember("letterSpacing", mLetterSpacing, allocator); result.AddMember("lineHeight", mLineHeight, allocator); diff --git a/aplcore/src/scenegraph/utilities.cpp b/aplcore/src/scenegraph/utilities.cpp index 0adcc21..0154bfd 100644 --- a/aplcore/src/scenegraph/utilities.cpp +++ b/aplcore/src/scenegraph/utilities.cpp @@ -99,12 +99,12 @@ template<> struct action< quoted > std::vector -splitFontString(const RootConfig& rootConfig, const std::string& text) +splitFontString(const RootConfig& rootConfig, const SessionPtr& session, const std::string& text) { grammar::split_state state; pegtl::string_input<> in(text, ""); if (!pegtl::parse(in, state) || state.failed) { - CONSOLE(rootConfig.getSession()) << "Parse error in '" << text << "' - " << state.what(); + CONSOLE(session) << "Parse error in '" << text << "' - " << state.what(); state.strings.clear(); // Throw away any partial data that was parsed } diff --git a/aplcore/src/time/sequencer.cpp b/aplcore/src/time/sequencer.cpp index b09074c..d8140e0 100644 --- a/aplcore/src/time/sequencer.cpp +++ b/aplcore/src/time/sequencer.cpp @@ -14,10 +14,12 @@ */ #include "apl/time/sequencer.h" -#include "apl/utils/log.h" -#include "apl/command/arraycommand.h" + #include "apl/action/delayaction.h" +#include "apl/command/arraycommand.h" +#include "apl/document/documentcontext.h" #include "apl/time/timemanager.h" +#include "apl/utils/log.h" namespace apl { @@ -145,7 +147,7 @@ Sequencer::execute(const CommandPtr& commandPtr, bool fastMode) } ActionPtr -Sequencer::executeCommands(const Object& commands, +Sequencer::executeCommands(CommandData&& commandData, const ContextPtr& context, const CoreComponentPtr& baseComponent, bool fastMode) @@ -153,25 +155,24 @@ Sequencer::executeCommands(const Object& commands, if (mTerminated) return nullptr; - if (!commands.isArray()) { + if (!commandData.get().isArray()) { LOG(LogLevel::kError).session(context) << "executeCommands: invalid command list"; return nullptr; } - if (commands.empty()) + if (commandData.get().empty()) return nullptr; if (!context->has("event") && !fastMode) LOG(LogLevel::kWarn).session(context) << "missing event in context"; - Properties props; - auto commandPtr = ArrayCommand::create(context, commands, baseComponent, props, ""); + auto commandPtr = ArrayCommand::create(context, std::move(commandData), baseComponent, Properties(), ""); return execute(commandPtr, fastMode); } ActionPtr -Sequencer::executeCommandsOnSequencer(const Object& commands, +Sequencer::executeCommandsOnSequencer(CommandData&& commandData, const ContextPtr& context, const CoreComponentPtr& baseComponent, const std::string& sequencer) @@ -179,19 +180,18 @@ Sequencer::executeCommandsOnSequencer(const Object& commands, if (mTerminated) return nullptr; - if (!commands.isArray()) { + if (!commandData.get().isArray()) { LOG(LogLevel::kError).session(context) << "executeCommands: invalid command list"; return nullptr; } - if (commands.empty()) + if (commandData.get().empty()) return nullptr; if (!context->has("event")) LOG(LogLevel::kWarn).session(context) << "missing event in context"; - Properties props; - auto commandPtr = ArrayCommand::create(context, commands, baseComponent, props, ""); + auto commandPtr = ArrayCommand::create(context, std::move(commandData), baseComponent, Properties(), ""); return executeOnSequencer(commandPtr, sequencer); } @@ -361,10 +361,10 @@ Sequencer::detachSequencer(const std::string& sequencerName) } bool -Sequencer::reattachSequencer(const std::string& sequencerName, const ActionPtr& action, const RootContext& root) +Sequencer::reattachSequencer(const std::string& sequencerName, const ActionPtr& action, const CoreDocumentContext& context) { terminateSequencer(sequencerName); - if (!action->rehydrate(root)) return false; + if (!action->rehydrate(context)) return false; attachToSequencer(action, sequencerName); return true; } diff --git a/aplcore/src/touch/pointermanager.cpp b/aplcore/src/touch/pointermanager.cpp index a4a3ed9..b08d16c 100644 --- a/aplcore/src/touch/pointermanager.cpp +++ b/aplcore/src/touch/pointermanager.cpp @@ -17,9 +17,9 @@ #include "apl/component/corecomponent.h" #include "apl/component/touchablecomponent.h" -#include "apl/engine/rootcontextdata.h" -#include "apl/touch/pointer.h" -#include "apl/utils/log.h" +#include "apl/engine/corerootcontext.h" +#include "apl/engine/hovermanager.h" +#include "apl/time/sequencer.h" #include "apl/utils/searchvisitor.h" namespace apl { @@ -36,7 +36,7 @@ sendEventToComponent(PointerEventType type, PointerEvent event(type, pointer->getPosition(), pointer->getId(), pointer->getPointerType()); - component->processPointerEvent(event, timestamp); + component->processPointerEvent(event, timestamp, false); } static inline void @@ -64,7 +64,8 @@ Bimap sEventHandlers = {{kPointerCancel, kPropert {kPointerMove, kPropertyOnMove}, {kPointerUp, kPropertyOnUp}}; -PointerManager::PointerManager(const RootContextData& core) : mCore(core) +PointerManager::PointerManager(const CoreRootContext& core, HoverManager& hover) : + mCore(core), mHoverManager(hover) {} /** @@ -79,13 +80,18 @@ class HitListIterator { reset(); } + struct HitTarget { + CoreComponentPtr ptr; + bool onlyProcessGestures; + }; + void reset() { mCurrent = mTarget; - mTouchableFound = mCurrent->isTouchable(); + mFirstTouchable = (mCurrent && mCurrent->isTouchable()) ? mCurrent : nullptr; } - CoreComponentPtr next() { - auto result = mCurrent; + HitTarget next() { + HitTarget result = {mCurrent, mCurrent && mCurrent->isTouchable() && mCurrent != mFirstTouchable}; advance(); return result; } @@ -95,11 +101,8 @@ class HitListIterator { while (mCurrent) { mCurrent = CoreComponent::cast(mCurrent->getParent()); if (mCurrent && mCurrent->isActionable()) { - if (mCurrent->isTouchable()) { - // Skip this component because we already found a touchable component - if (mTouchableFound) - continue; - mTouchableFound = true; + if (mFirstTouchable == nullptr && mCurrent->isTouchable()) { + mFirstTouchable = mCurrent; } // mCurrent is pointing to an actionable component return; @@ -109,7 +112,7 @@ class HitListIterator { CoreComponentPtr mTarget; CoreComponentPtr mCurrent; - bool mTouchableFound; + CoreComponentPtr mFirstTouchable; }; bool @@ -134,8 +137,8 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t break; case kPointerTargetChanged: default: - LOG(LogLevel::kWarn).session(mCore.session()) << "Unknown pointer event type ignored" - << pointerEvent.pointerEventType; + LOG(LogLevel::kWarn) << "Unknown pointer event type ignored" + << pointerEvent.pointerEventType; return false; } @@ -148,7 +151,7 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t } if (pointer->isCaptured()) { - target->processPointerEvent(pointerEvent, timestamp); + target->processPointerEvent(pointerEvent, timestamp, false); } else { auto hitListIt = HitListIterator(target); // Each component in the list in turn receives the event. @@ -159,12 +162,15 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t // and subsequent hit targets will not be given a chance to process the event. In the // case of kPointerStatusPendingCapture, the event will be allowed to keep bubbling up // through the component hierarchy. - while (auto hitTarget = hitListIt.next()) { - PointerCaptureStatus pointerStatus = hitTarget->processPointerEvent(pointerEvent, timestamp); + while (true) { + auto hitTarget = hitListIt.next(); + if (!hitTarget.ptr) break; + PointerCaptureStatus pointerStatus = hitTarget.ptr->processPointerEvent( + pointerEvent, timestamp, hitTarget.onlyProcessGestures); // If component claims the event - pointer should be captured by it. if (pointerStatus != kPointerStatusNotCaptured) { if (!pointer->isCaptured()) - pointer->setCapture(ActionableComponent::cast(hitTarget)); + pointer->setCapture(ActionableComponent::cast(hitTarget.ptr)); if (pointerStatus == kPointerStatusCaptured) break; } @@ -173,9 +179,11 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t // If pointer was captured - cancel events to all the other components on the list. if (pointer->isCaptured()) { hitListIt.reset(); - while (auto hitTarget = hitListIt.next()) { - if (hitTarget != pointer->getTarget()) { - sendEventToComponent(kPointerCancel, mActivePointer, hitTarget, timestamp); + while (true) { + auto hitTarget = hitListIt.next(); + if (!hitTarget.ptr) break; + if (hitTarget.ptr != pointer->getTarget()) { + sendEventToComponent(kPointerCancel, mActivePointer, hitTarget.ptr, timestamp); } } } @@ -217,7 +225,7 @@ PointerManager::handlePointerStart(const std::shared_ptr& pointer, if (mActivePointer != nullptr) return nullptr; - auto top = CoreComponent::cast(mCore.top()); + auto top = CoreComponent::cast(mCore.topComponent()); if (!top) return nullptr; @@ -238,7 +246,7 @@ PointerManager::handlePointerUpdate(const std::shared_ptr& pointer, { auto target = pointer->getTarget(); if (pointer->getPointerType() == kMousePointer && !target) { - mCore.hoverManager().setCursorPosition(pointerEvent.pointerEventPosition); + mHoverManager.setCursorPosition(pointerEvent.pointerEventPosition); } pointer->setPosition(pointerEvent.pointerEventPosition); return target; @@ -253,7 +261,7 @@ PointerManager::handlePointerEnd(const std::shared_ptr& pointer, case kTouchPointer: break; case kMousePointer: - mCore.hoverManager().setCursorPosition(pointerEvent.pointerEventPosition); + mHoverManager.setCursorPosition(pointerEvent.pointerEventPosition); break; } diff --git a/aplcore/src/touch/utils/pagemovehandler.cpp b/aplcore/src/touch/utils/pagemovehandler.cpp index b00be51..7acc3b3 100644 --- a/aplcore/src/touch/utils/pagemovehandler.cpp +++ b/aplcore/src/touch/utils/pagemovehandler.cpp @@ -161,6 +161,26 @@ PageMoveHandler::getTargetPageIndex(const CoreComponentPtr& component) const { return -1; } +CoreComponentPtr +PageMoveHandler::getCheckedCurrentPage(const CoreComponentPtr& component) const { + if (auto currentPage = mCurrentPage.lock()) { + if (component->getChildIndex(currentPage) >= 0) { + return currentPage; + } + } + return nullptr; +} + +CoreComponentPtr +PageMoveHandler::getCheckedTargetPage(const CoreComponentPtr& component) const { + if (auto targetPage = mTargetPage.lock()) { + if (component->getChildIndex(targetPage) >= 0) { + return targetPage; + } + } + return nullptr; +} + ContextPtr PageMoveHandler::createPageMoveContext(float amount, SwipeDirection direction, PageDirection pageDirection, const CoreComponentPtr& self, const CoreComponentPtr& currentChild, diff --git a/aplcore/src/utils/log.cpp b/aplcore/src/utils/log.cpp index cf3e348..e160492 100644 --- a/aplcore/src/utils/log.cpp +++ b/aplcore/src/utils/log.cpp @@ -14,12 +14,13 @@ */ #include "apl/utils/log.h" -#include "apl/utils/session.h" -#include "apl/component/corecomponent.h" + #include "apl/command/corecommand.h" +#include "apl/component/corecomponent.h" +#include "apl/content/content.h" #include "apl/engine/context.h" #include "apl/engine/rootcontext.h" -#include "apl/content/rootconfig.h" +#include "apl/utils/session.h" namespace apl { @@ -77,39 +78,33 @@ Logger::session(const ContextPtr& context) } Logger& -Logger::session(const RootConfigPtr& config) -{ - return config ? session(config->getSession()) : *this; -} - -Logger& -Logger::session(const RootConfig& config) +Logger::session(const Component& component) { - return session(config.getSession()); + return session(component.getContext()->session()); } Logger& -Logger::session(const RootContextPtr& root) +Logger::session(const ComponentPtr& component) { - return root ? session(root->getSession()) : *this; + return component ? session(component->getContext()->session()) : *this; } Logger& -Logger::session(const Component& component) +Logger::session(const ConstCommandPtr& command) { - return session(component.getContext()->session()); + return command ? session(std::static_pointer_cast(command)->context()->session()) : *this; } Logger& -Logger::session(const ComponentPtr& component) +Logger::session(const ContentPtr& content) { - return component ? session(component->getContext()->session()) : *this; + return content ? session(content->getSession()) : *this; } Logger& -Logger::session(const ConstCommandPtr& command) +Logger::session(const CoreDocumentContextPtr& document) { - return command ? session(std::static_pointer_cast(command)->context()->session()) : *this; + return document ? session(document->getSession()) : *this; } void diff --git a/aplcore/src/utils/session.cpp b/aplcore/src/utils/session.cpp index baa0b3d..fac408a 100644 --- a/aplcore/src/utils/session.cpp +++ b/aplcore/src/utils/session.cpp @@ -13,10 +13,13 @@ * permissions and limitations under the License. */ +#include "apl/utils/session.h" + +#include "apl/content/content.h" #include "apl/content/rootconfig.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/context.h" #include "apl/utils/random.h" -#include "apl/utils/session.h" namespace apl { @@ -69,8 +72,14 @@ SessionMessage::SessionMessage(const std::weak_ptr& contextPtr, const c mSession = context->session(); } -SessionMessage::SessionMessage(const RootConfigPtr& config, const char *filename, const char *function) - : mSession(config->getSession()), +SessionMessage::SessionMessage(const ContentPtr& content, const char *filename, const char *function) + : mSession(content->getSession()), + mFilename(filename), + mFunction(function), + mUncaught(std::uncaught_exception()) {} + +SessionMessage::SessionMessage(const CoreDocumentContextPtr& document, const char *filename, const char *function) + : mSession(document->getSession()), mFilename(filename), mFunction(function), mUncaught(std::uncaught_exception()) {} diff --git a/unit/CMakeLists.txt b/aplcore/unit/CMakeLists.txt similarity index 72% rename from unit/CMakeLists.txt rename to aplcore/unit/CMakeLists.txt index b4f619d..44c33de 100644 --- a/unit/CMakeLists.txt +++ b/aplcore/unit/CMakeLists.txt @@ -12,11 +12,8 @@ # permissions and limitations under the License. # Unit tests -include_directories(../aplcore/include) -include_directories(../tools/src) -include_directories(${RAPIDJSON_INCLUDE}) -include_directories(${PEGTL_INCLUDE}) -include_directories(${YOGA_INCLUDE}) + +message("Adding APL Core unit test target") # Google testing add_executable( unittest @@ -25,12 +22,12 @@ add_executable( unittest unittest_simpletextmeasurement.cpp unittest_testeventloop.cpp) -if (BUILD_ENUMGEN) - target_sources(unittest PRIVATE - ../tools/src/enumparser.cpp - unittest_enumgen.cpp - ) -endif() +# Unit tests are not restricted to public headers, make sure they can find private headers as well +target_include_directories(unittest + PRIVATE + ../../aplcore/include + ${PEGTL_INCLUDE} + ${YOGA_INCLUDE}) add_subdirectory(action) add_subdirectory(animation) @@ -40,6 +37,7 @@ add_subdirectory(component) add_subdirectory(content) add_subdirectory(datagrammar) add_subdirectory(datasource) +add_subdirectory(embed) add_subdirectory(engine) add_subdirectory(extension) add_subdirectory(focus) @@ -55,24 +53,11 @@ if(ENABLE_SCENEGRAPH) add_subdirectory(scenegraph) endif(ENABLE_SCENEGRAPH) -# Add googletest directly to our build. This defines -# the gtest and gtest_main targets. -add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src - ${CMAKE_BINARY_DIR}/googletest-build - EXCLUDE_FROM_ALL) if(COVERAGE) target_add_code_coverage(unittest apl) endif() target_link_libraries(unittest apl gtest gtest_main) -if (ENABLE_ALEXAEXTENSIONS) - if (NOT BUILD_ALEXAEXTENSIONS) - # Check to see if it's available from the system. - find_package(alexaext REQUIRED) - endif (NOT BUILD_ALEXAEXTENSIONS) - target_link_libraries(unittest alexa::extensions) -endif(ENABLE_ALEXAEXTENSIONS) - if (ANDROID) target_link_libraries(unittest log) endif(ANDROID) @@ -88,4 +73,3 @@ else() # Adds the entire unittest executable as a single ctest. Great for speed. add_test(all-tests unittest) endif() - diff --git a/unit/action/CMakeLists.txt b/aplcore/unit/action/CMakeLists.txt similarity index 100% rename from unit/action/CMakeLists.txt rename to aplcore/unit/action/CMakeLists.txt diff --git a/unit/action/unittest_action.cpp b/aplcore/unit/action/unittest_action.cpp similarity index 100% rename from unit/action/unittest_action.cpp rename to aplcore/unit/action/unittest_action.cpp diff --git a/unit/animation/CMakeLists.txt b/aplcore/unit/animation/CMakeLists.txt similarity index 100% rename from unit/animation/CMakeLists.txt rename to aplcore/unit/animation/CMakeLists.txt diff --git a/unit/animation/testeasingcurve.h b/aplcore/unit/animation/testeasingcurve.h similarity index 100% rename from unit/animation/testeasingcurve.h rename to aplcore/unit/animation/testeasingcurve.h diff --git a/unit/animation/unittest_easing.cpp b/aplcore/unit/animation/unittest_easing.cpp similarity index 100% rename from unit/animation/unittest_easing.cpp rename to aplcore/unit/animation/unittest_easing.cpp diff --git a/unit/animation/unittest_easing_approximation.cpp b/aplcore/unit/animation/unittest_easing_approximation.cpp similarity index 100% rename from unit/animation/unittest_easing_approximation.cpp rename to aplcore/unit/animation/unittest_easing_approximation.cpp diff --git a/unit/audio/CMakeLists.txt b/aplcore/unit/audio/CMakeLists.txt similarity index 100% rename from unit/audio/CMakeLists.txt rename to aplcore/unit/audio/CMakeLists.txt diff --git a/unit/audio/audiotest.h b/aplcore/unit/audio/audiotest.h similarity index 100% rename from unit/audio/audiotest.h rename to aplcore/unit/audio/audiotest.h diff --git a/unit/audio/testaudioplayer.cpp b/aplcore/unit/audio/testaudioplayer.cpp similarity index 100% rename from unit/audio/testaudioplayer.cpp rename to aplcore/unit/audio/testaudioplayer.cpp diff --git a/unit/audio/testaudioplayer.h b/aplcore/unit/audio/testaudioplayer.h similarity index 99% rename from unit/audio/testaudioplayer.h rename to aplcore/unit/audio/testaudioplayer.h index 5ec2990..2b0e0dd 100644 --- a/unit/audio/testaudioplayer.h +++ b/aplcore/unit/audio/testaudioplayer.h @@ -72,7 +72,7 @@ class TestAudioPlayer : public AudioPlayer, static std::string toString(EventType eventType); timeout_id getTimeoutId() const { return mTimeoutId; } - + private: enum State { INIT, diff --git a/unit/audio/testaudioplayerfactory.h b/aplcore/unit/audio/testaudioplayerfactory.h similarity index 100% rename from unit/audio/testaudioplayerfactory.h rename to aplcore/unit/audio/testaudioplayerfactory.h diff --git a/unit/audio/unittest_audio_player.cpp b/aplcore/unit/audio/unittest_audio_player.cpp similarity index 100% rename from unit/audio/unittest_audio_player.cpp rename to aplcore/unit/audio/unittest_audio_player.cpp diff --git a/unit/audio/unittest_command_page_audio.cpp b/aplcore/unit/audio/unittest_command_page_audio.cpp similarity index 99% rename from unit/audio/unittest_command_page_audio.cpp rename to aplcore/unit/audio/unittest_command_page_audio.cpp index b4be86c..8e3e0d7 100644 --- a/unit/audio/unittest_command_page_audio.cpp +++ b/aplcore/unit/audio/unittest_command_page_audio.cpp @@ -149,7 +149,7 @@ TEST_F(CommandPageAudioTest, SpeakItemCombination) ASSERT_EQ(0, component->pagePosition()); rapidjson::Document doc; doc.Parse(COMBINATION_COMMANDS); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); // Should have preroll for first speech ASSERT_TRUE(CheckPlayer("https://iamspeech.com/2.mp3", TestAudioPlayer::kPreroll)); diff --git a/unit/audio/unittest_dynamictokenlist_audio.cpp b/aplcore/unit/audio/unittest_dynamictokenlist_audio.cpp similarity index 100% rename from unit/audio/unittest_dynamictokenlist_audio.cpp rename to aplcore/unit/audio/unittest_dynamictokenlist_audio.cpp diff --git a/unit/audio/unittest_sequencer_audio.cpp b/aplcore/unit/audio/unittest_sequencer_audio.cpp similarity index 99% rename from unit/audio/unittest_sequencer_audio.cpp rename to aplcore/unit/audio/unittest_sequencer_audio.cpp index 4e609ef..388ea93 100644 --- a/unit/audio/unittest_sequencer_audio.cpp +++ b/aplcore/unit/audio/unittest_sequencer_audio.cpp @@ -24,7 +24,7 @@ class SequencerAudioTest : public AudioTest { public: ActionPtr execute(const std::string& cmds, bool fastMode) { command.Parse(cmds.c_str()); - return root->executeCommands(command, fastMode); + return executeCommands(command, fastMode); } }; diff --git a/unit/audio/unittest_speak_item_audio.cpp b/aplcore/unit/audio/unittest_speak_item_audio.cpp similarity index 91% rename from unit/audio/unittest_speak_item_audio.cpp rename to aplcore/unit/audio/unittest_speak_item_audio.cpp index fb4baaa..485e3b4 100644 --- a/unit/audio/unittest_speak_item_audio.cpp +++ b/aplcore/unit/audio/unittest_speak_item_audio.cpp @@ -15,6 +15,8 @@ #include "audiotest.h" +#include "../embed/testdocumentmanager.h" + using namespace apl; class SpeakItemAudioTest : public AudioTest {}; @@ -181,7 +183,7 @@ TEST_F(SpeakItemAudioTest, SpeakItemThenSend) static const char *TEST_STAGES = R"apl( { "type": "APL", - "version": "1.1", + "version": "2023.2", "styles": { "base": { "values": [ @@ -1065,6 +1067,142 @@ TEST_F(SpeakItemAudioTest, TransitionalRequests) ASSERT_EQ(-1, event.getValue(apl::kEventPropertyRangeEnd).getInteger()); } +static const char* HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ], + "onFail": [ + { + "type": "InsertItem", + "sequencer": "SEND_EVENTER", + "arguments": ["FAILED"] + } + ] + } + } + } +})"; + +TEST_F(SpeakItemAudioTest, EmbeddedTestStages) +{ + factory->addFakeContent({ + {"URL1", 3000, 100, -1, + { + {SpeechMarkType::kSpeechMarkWord, 0, 5, 0, "Since"}, + {SpeechMarkType::kSpeechMarkWord, 42, 46, 1300, "year"}, + {SpeechMarkType::kSpeechMarkWord, 64, 70, 1900, "should"}, + {SpeechMarkType::kSpeechMarkWord, 90, 97, 2600, "holiday"}, + {SpeechMarkType::kSpeechMarkWord, 98, 102, 2800, "look"} + } + } + }); + + config->measure(std::make_shared()); + + std::shared_ptr documentManager = std::make_shared(); + + config->documentManager(std::static_pointer_cast(documentManager)); + + ////////////////////////////////////////////////////////////// + loadDocument(HOST_DOC); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto content = Content::create(BOSS_KARAOKE, session); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + root->clearPending(); + root->clearDirty(); + + ////////////////////////////////////////////////////////////// + + rapidjson::Document commandDocument; + commandDocument.Parse(R"([{ + "type": "SpeakItem", + "componentId": "text1", + "align": "first", + "highlightMode": "line", + "minimumDwellTime": 1000 + }])"); + + embeddedDocumentContext->executeCommands(std::move(commandDocument), false); + + ASSERT_TRUE(CheckPlayer("URL1", TestAudioPlayer::kPreroll)); + ASSERT_FALSE(factory->hasEvent()); + + // Preroll scroll should be here. With rect request + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeRequestLineBounds, event.getType()); + ASSERT_EQ(embeddedDocumentContext, event.getDocument()); + auto textFieldBoundary = root->findComponentById("text1")->getCalculated(apl::kPropertyBounds).get(); + event.getActionRef().resolve({0, 0, textFieldBoundary.getWidth(), 10}); + + advanceTime(100); + ASSERT_TRUE(CheckPlayer("URL1", TestAudioPlayer::kReady)); + + // Move forward a bit in time to finished scrolling. + advanceTime(900); + // Should start playing "Since you are not going" + ASSERT_TRUE(CheckPlayer("URL1", TestAudioPlayer::kPlay)); + ASSERT_EQ(root->findComponentById("scroll")->scrollPosition().getY(), textFieldBoundary.getY()); + + auto text = root->findComponentById("text1"); + + // Will also ask to scroll to the first line for play + ASSERT_TRUE(verifyLineUpdate(root, text, 0, 0, 4)); + + // "on a holiday this year" + advanceTime(1300); + ASSERT_TRUE(verifyLineUpdate(root, text, 70, 42, 45)); + + // "Boss I thought I should" + advanceTime(600); + ASSERT_TRUE(verifyLineUpdate(root, text, 140, 64, 69)); + + // "give your office a holiday" + advanceTime(700); + ASSERT_TRUE(verifyLineUpdate(root, text, 210, 90, 96)); + + // "look" + advanceTime(200); + ASSERT_TRUE(verifyLineUpdate(root, text, 280, 98, 101)); + + advanceTime(500); + + ASSERT_TRUE(CheckPlayer("URL1", TestAudioPlayer::kDone)); + ASSERT_TRUE(CheckPlayer("URL1", TestAudioPlayer::kRelease)); + ASSERT_FALSE(factory->hasEvent()); + + // Highlight clean + event = root->popEvent(); + ASSERT_EQ(kEventTypeLineHighlight, event.getType()); + ASSERT_EQ(embeddedDocumentContext, event.getDocument()); + ASSERT_EQ(-1, event.getValue(apl::kEventPropertyRangeStart).getInteger()); + ASSERT_EQ(-1, event.getValue(apl::kEventPropertyRangeEnd).getInteger()); +} + TEST_F(SpeakItemAudioTest, LineRequestTerminated) { // Limited subset of marks to avoid too much verification diff --git a/unit/audio/unittest_speak_list_audio.cpp b/aplcore/unit/audio/unittest_speak_list_audio.cpp similarity index 100% rename from unit/audio/unittest_speak_list_audio.cpp rename to aplcore/unit/audio/unittest_speak_list_audio.cpp diff --git a/unit/audio/unittest_speech_marks.cpp b/aplcore/unit/audio/unittest_speech_marks.cpp similarity index 100% rename from unit/audio/unittest_speech_marks.cpp rename to aplcore/unit/audio/unittest_speech_marks.cpp diff --git a/unit/command/CMakeLists.txt b/aplcore/unit/command/CMakeLists.txt similarity index 93% rename from unit/command/CMakeLists.txt rename to aplcore/unit/command/CMakeLists.txt index 1bc5422..6d8960c 100644 --- a/unit/command/CMakeLists.txt +++ b/aplcore/unit/command/CMakeLists.txt @@ -18,10 +18,12 @@ target_sources_local(unittest unittest_command_array.cpp unittest_command_document.cpp unittest_command_event_binding.cpp + unittest_command_insertitem.cpp unittest_command_macros.cpp unittest_command_media.cpp unittest_command_openurl.cpp unittest_command_page.cpp + unittest_command_removeitem.cpp unittest_command_select.cpp unittest_command_sendevent.cpp unittest_command_setvalue.cpp diff --git a/unit/command/unittest_command_animateitem.cpp b/aplcore/unit/command/unittest_command_animateitem.cpp similarity index 100% rename from unit/command/unittest_command_animateitem.cpp rename to aplcore/unit/command/unittest_command_animateitem.cpp diff --git a/unit/command/unittest_command_animateitem_values.cpp b/aplcore/unit/command/unittest_command_animateitem_values.cpp similarity index 100% rename from unit/command/unittest_command_animateitem_values.cpp rename to aplcore/unit/command/unittest_command_animateitem_values.cpp diff --git a/unit/command/unittest_command_array.cpp b/aplcore/unit/command/unittest_command_array.cpp similarity index 85% rename from unit/command/unittest_command_array.cpp rename to aplcore/unit/command/unittest_command_array.cpp index 1540add..42aefaf 100644 --- a/unit/command/unittest_command_array.cpp +++ b/aplcore/unit/command/unittest_command_array.cpp @@ -33,6 +33,15 @@ class TestCommand : public Command { return std::make_shared(value); } + static CommandPtr createFull(const ContextPtr& context, + CommandData&& commandData, + Properties&& props, + const CoreComponentPtr& base, + const std::string& parentSequencer = "") { + auto value = props.asNumber(*context, "argument", -1); + return std::make_shared(value); + } + TestCommand(int value) : mValue(value) {} unsigned long delay() const override { return 1000; } @@ -54,7 +63,7 @@ class ArrayCommandTest : public ActionWrapper { ArrayCommandTest() : ActionWrapper() { - CommandFactory::instance().set("Test", TestCommand::create); + CommandFactory::instance().set("Test", TestCommand::createFull); TestCommand::sSum = 0; context = Context::createTestContext(Metrics(), RootConfig().timeManager(loop)); @@ -116,7 +125,7 @@ static const char *COMMAND_LIST = TEST_F(ArrayCommandTest, MultipleCommand) { auto json = JsonData(COMMAND_LIST); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties()); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties()); auto action = context->sequencer().execute(command, false); ASSERT_TRUE(action); @@ -131,7 +140,7 @@ TEST_F(ArrayCommandTest, MultipleCommand) TEST_F(ArrayCommandTest, MultipleCommandCleanUp) { auto json = JsonData(COMMAND_LIST); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties()); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties()); auto action = context->sequencer().execute(command, true); ASSERT_FALSE(action); @@ -162,7 +171,7 @@ static const char *BASIC = TEST_F(ArrayCommandTest, Basic) { auto json = JsonData(BASIC); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties()); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties()); auto action = command->execute(loop, false); ASSERT_EQ(1, loop->size()); @@ -178,7 +187,7 @@ TEST_F(ArrayCommandTest, Basic) TEST_F(ArrayCommandTest, AbortEarly) { auto json = JsonData(BASIC); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties()); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties()); auto action = command->execute(loop, false); ASSERT_EQ(1, loop->size()); @@ -198,7 +207,7 @@ TEST_F(ArrayCommandTest, AbortEarly) TEST_F(ArrayCommandTest, AbortEarlyWithTerminateFinish) { auto json = JsonData(BASIC); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties(), "", true); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties(), "", true); auto action = command->execute(loop, false); ASSERT_EQ(1, loop->size()); @@ -221,7 +230,7 @@ TEST_F(ArrayCommandTest, AbortEarlyWithTerminateFinish) TEST_F(ArrayCommandTest, SequencerNormalMode) { auto json = JsonData(BASIC); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties()); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties()); auto action = context->sequencer().execute(command, false); ASSERT_TRUE(action); @@ -239,7 +248,7 @@ TEST_F(ArrayCommandTest, SequencerNormalMode) TEST_F(ArrayCommandTest, SequencerFastMode) { auto json = JsonData(BASIC); - auto command = ArrayCommand::create(context, json.get(), nullptr, Properties()); + auto command = ArrayCommand::create(context, Object(json.get()), nullptr, Properties()); auto action = context->sequencer().execute(command, true); ASSERT_FALSE(action); diff --git a/unit/command/unittest_command_document.cpp b/aplcore/unit/command/unittest_command_document.cpp similarity index 94% rename from unit/command/unittest_command_document.cpp rename to aplcore/unit/command/unittest_command_document.cpp index 168c150..f238ffe 100644 --- a/unit/command/unittest_command_document.cpp +++ b/aplcore/unit/command/unittest_command_document.cpp @@ -613,24 +613,24 @@ TEST_F(MountTest, PagerChildOnMount) { loadDocument(PAGER_CHILD_ONMOUNT); - auto affectedText = component->findComponentById("affectedText0"); + auto affectedText = root->findComponentById("affectedText0"); ASSERT_EQ("triggered", affectedText->getCalculated(kPropertyText).asString()); - affectedText = component->findComponentById("affectedText1"); + affectedText = root->findComponentById("affectedText1"); ASSERT_FALSE(affectedText); advanceTime(10); - affectedText = component->findComponentById("affectedText1"); + affectedText = root->findComponentById("affectedText1"); ASSERT_EQ("triggered", affectedText->getCalculated(kPropertyText).asString()); - affectedText = component->findComponentById("affectedText2"); + affectedText = root->findComponentById("affectedText2"); ASSERT_FALSE(affectedText); component->update(kUpdatePagerPosition, 1); root->clearPending(); - affectedText = component->findComponentById("affectedText2"); + affectedText = root->findComponentById("affectedText2"); ASSERT_TRUE(affectedText); ASSERT_EQ("triggered", affectedText->getCalculated(kPropertyText).asString()); } @@ -674,31 +674,31 @@ TEST_F(MountTest, SequenceChildOnMount) { loadDocument(SEQUENCE_CHILD_ONMOUNT); - auto affectedText = component->findComponentById("text0"); + auto affectedText = root->findComponentById("text0"); ASSERT_TRUE(affectedText->getCalculated(kPropertyLaidOut).getBoolean()); ASSERT_EQ("hit", affectedText->getCalculated(kPropertyText).asString()); - affectedText = component->findComponentById("text2"); + affectedText = root->findComponentById("text2"); ASSERT_EQ("hit", affectedText->getCalculated(kPropertyText).asString()); - affectedText = component->findComponentById("text3"); + affectedText = root->findComponentById("text3"); ASSERT_EQ("hit", affectedText->getCalculated(kPropertyText).asString()); - affectedText = component->findComponentById("text6"); + affectedText = root->findComponentById("text6"); ASSERT_FALSE(affectedText); advanceTime(10); - affectedText = component->findComponentById("text6"); + affectedText = root->findComponentById("text6"); ASSERT_TRUE(affectedText); ASSERT_EQ("hit", affectedText->getCalculated(kPropertyText).asString()); - affectedText = component->findComponentById("text9"); + affectedText = root->findComponentById("text9"); ASSERT_FALSE(affectedText); component->update(kUpdateScrollPosition, 300); advanceTime(10); - affectedText = component->findComponentById("text9"); + affectedText = root->findComponentById("text9"); ASSERT_TRUE(affectedText); ASSERT_EQ("hit", affectedText->getCalculated(kPropertyText).asString()); } @@ -753,26 +753,26 @@ TEST_F(MountTest, PagerDelayedOnmount) advanceTime(10); - auto affectedText = component->findComponentById("page1"); + auto affectedText = root->findComponentById("page1"); ASSERT_TRUE(affectedText->getCalculated(kPropertyLaidOut).getBoolean()); ASSERT_EQ(0.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page2"); + affectedText = root->findComponentById("page2"); ASSERT_EQ(0.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page3"); + affectedText = root->findComponentById("page3"); ASSERT_EQ(0.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); advanceTime(1000); - affectedText = component->findComponentById("page1"); + affectedText = root->findComponentById("page1"); ASSERT_TRUE(affectedText->getCalculated(kPropertyLaidOut).getBoolean()); ASSERT_EQ(1.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page2"); + affectedText = root->findComponentById("page2"); ASSERT_EQ(1.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page3"); + affectedText = root->findComponentById("page3"); ASSERT_EQ(1.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); } @@ -831,25 +831,25 @@ TEST_F(MountTest, PagerFinalOnmount) advanceTime(10); - auto affectedText = component->findComponentById("page1"); + auto affectedText = root->findComponentById("page1"); ASSERT_TRUE(affectedText->getCalculated(kPropertyLaidOut).getBoolean()); ASSERT_EQ(0.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page2"); + affectedText = root->findComponentById("page2"); ASSERT_EQ(0.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page3"); + affectedText = root->findComponentById("page3"); ASSERT_EQ(0.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); advanceTime(1000); - affectedText = component->findComponentById("page1"); + affectedText = root->findComponentById("page1"); ASSERT_TRUE(affectedText->getCalculated(kPropertyLaidOut).getBoolean()); ASSERT_EQ(1.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page2"); + affectedText = root->findComponentById("page2"); ASSERT_EQ(1.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); - affectedText = component->findComponentById("page3"); + affectedText = root->findComponentById("page3"); ASSERT_EQ(1.0, affectedText->getCalculated(kPropertyOpacity).asNumber()); } \ No newline at end of file diff --git a/unit/command/unittest_command_event_binding.cpp b/aplcore/unit/command/unittest_command_event_binding.cpp similarity index 100% rename from unit/command/unittest_command_event_binding.cpp rename to aplcore/unit/command/unittest_command_event_binding.cpp diff --git a/aplcore/unit/command/unittest_command_insertitem.cpp b/aplcore/unit/command/unittest_command_insertitem.cpp new file mode 100644 index 0000000..d2a1c5a --- /dev/null +++ b/aplcore/unit/command/unittest_command_insertitem.cpp @@ -0,0 +1,453 @@ +/** +* Copyright 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 "apl/component/textcomponent.h" + +#include "../testeventloop.h" + +using namespace apl; + +static const char *DEFAULT_INSERT = R"( + { + "type": "Text", + "id": "newArrival", + "text": "I have arrived!" + })"; + +class CommandInsertItemTest : public CommandTest { +public: + ActionPtr + executeInsertItem(const std::string& componentId, const int index = 0, const std::string& item = DEFAULT_INSERT) { + std::map properties = {}; + + rapidjson::Document itemDoc; + itemDoc.Parse(item.c_str()); + + properties.emplace("componentId", componentId); + properties.emplace("item", itemDoc); + + if (index != INT_MAX) + properties.emplace("at", index); + + return executeCommand("InsertItem", properties, false); + } + + void + validateInsert( + const CoreComponentPtr& target, + const CoreComponentPtr& child, + const int initialChildCount, + const int expectedIndex) { + + ASSERT_FALSE(session->checkAndClear()); + ASSERT_TRUE(root->isDirty()); + ASSERT_EQ(target->getChildCount(), initialChildCount + 1); + + ASSERT_TRUE(child); + ASSERT_EQ(target->getChildIndex(child), expectedIndex); + ASSERT_TRUE(CheckDirtyAtLeast(root, target, child)); + ASSERT_EQ(child->getParent()->getId(), target->getId()); + ASSERT_EQ(child->getPathObject().toString(), "_virtual"); + ASSERT_EQ(child->getContext()->parent(), target->getContext()); + } + + void + validateNonInsert( + const std::string& expectedSessionMessage, + const CoreComponentPtr& target = nullptr, + const int expectedChildCount = -1, // expected to pass non-negative value "if target" + const std::string& missingChild = "newArrival") { + + if (target) { + ASSERT_EQ(target->getChildCount(), expectedChildCount); + } + + ASSERT_TRUE(session->checkAndClear(expectedSessionMessage)); + ASSERT_FALSE(root->isDirty()); + ASSERT_FALSE(root->findComponentById(missingChild)); + } +}; + +static const char *INSERT_ITEM = R"( + { + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "parameters": [], + "item": { + "id": "main", + "type": "Container", + "items": [ + { + "type": "Text", + "id": "cannotHaveChildren", + "text": "Hello, World!" + }, + { + "type": "Frame", + "id": "hasNoChildren", + "bind": [ + { "name": "Color", "value": "blue" } + ] + }, + { + "type": "Frame", + "id": "alreadyHasAChild", + "item": { + "type": "Text", + "id": "onlyChild", + "text": "There can only be one!" + } + }, + { + "type": "Container", + "id": "multiChild", + "firstItem": { + "type": "Text", + "id": "firstChild", + "text": "The Original" + }, + "items":[{ + "type": "Text", + "id": "middleChild", + "text": "Definitive Edition" + }], + "lastItem": { + "type": "Text", + "id": "lastChild", + "text": "The Remix" + } + } + ] + } + } + })"; + +TEST_F(CommandInsertItemTest, InsertItemWhenComponentIdMissing) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + executeCommand( + "InsertItem", + { + { "at", 0 }, + // missing componentId property + { "item", INSERT_ITEM }}, + false); + + validateNonInsert("Missing required property 'componentId' for InsertItem"); +} + +TEST_F(CommandInsertItemTest, InsertItemWhenTargetDoesNotExist) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = root->findComponentById("missingTargetComponent"); + ASSERT_FALSE(target); + + executeInsertItem("missingTargetComponent"); + + validateNonInsert( "Illegal command InsertItem - need to specify a target componentId"); +} + +TEST_F(CommandInsertItemTest, InsertInvalidItem) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("hasNoChildren")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 0); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem( + "hasNoChildren", + 0, + // The json below cannot be inflated as a Component because it is missing the "type" property + R"({"id":"newArrival","text":"I have arrived!"})"); + + validateNonInsert("Could not inflate item to be inserted", target, initialChildCount); +} + +TEST_F(CommandInsertItemTest, InsertItemWithFalseWhenClause) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("hasNoChildren")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 0); + ASSERT_TRUE(target->canInsertChild()); + + // when evaluates to false + executeInsertItem( + "hasNoChildren", + 0, + R"({ + "when": "${viewport.shape == 'round'}", + "type": "Text", + "id": "newArrival", + "text": "I have arrived!" + })"); + + validateNonInsert("Could not inflate item to be inserted", target, initialChildCount); +} + +TEST_F(CommandInsertItemTest, InsertItemSkippingFalseWhenClause) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("hasNoChildren")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 0); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem( + "hasNoChildren", + 0, + R"([{ + "when": "${viewport.shape == 'round'}", + "type": "Text", + "id": "whenIsFalse", + "text": "I won't inflate" + }, + { + "type": "Text", + "id": "newArrival", + "text": "I have arrived!" + }, + { + "type": "Text", + "id": "unreachable", + "text": "I never had a chance!" + }])"); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, 0); + ASSERT_FALSE(root->findComponentById("whenIsFalse")); + ASSERT_FALSE(root->findComponentById("unreachable")); +} + +TEST_F(CommandInsertItemTest, InsertItemWhenTargetCannotHaveChildren) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("cannotHaveChildren")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 0); + ASSERT_FALSE(target->canInsertChild()); + + executeInsertItem("cannotHaveChildren"); + + validateNonInsert("Could not insert child into 'cannotHaveChildren'", target, initialChildCount); +} + +TEST_F(CommandInsertItemTest, InsertItemWhenTargetAlreadyHasOnlyChild) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("alreadyHasAChild")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 1); + ASSERT_FALSE(target->canInsertChild()); + + executeInsertItem("alreadyHasAChild"); + + validateNonInsert("Could not insert child into 'alreadyHasAChild'", target, initialChildCount); +} + +TEST_F(CommandInsertItemTest, InsertItem) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("hasNoChildren")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 0); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem( + "hasNoChildren", + 0, + R"({ + "type": "Text", + "id": "newArrival", + "text": "${Color}" + })"); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, 0); + ASSERT_EQ(TextComponent::cast(child)->getValue().asString(), "blue"); +} + +TEST_F(CommandInsertItemTest, InsertItems) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("hasNoChildren")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_EQ(initialChildCount, 0); + ASSERT_TRUE(target->canInsertChild()); + + rapidjson::Document itemDoc; + itemDoc.Parse(DEFAULT_INSERT); + + executeCommand( + "InsertItem", + { + {"at", 0}, + {"componentId", "hasNoChildren"}, + {"items", itemDoc}}, // "items" instead of "item" + false); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, 0); +} + +TEST_F(CommandInsertItemTest, InsertItemDefaultAtAppends) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("main")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_TRUE(initialChildCount > 0); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem("main",INT_MAX); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, initialChildCount); +} + +TEST_F(CommandInsertItemTest, InsertItemNegativeInsertsFromEnd) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("multiChild")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_TRUE(initialChildCount > 1); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem("multiChild",-1); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, initialChildCount - 1); +} + +TEST_F(CommandInsertItemTest, InsertItemNegativeWalksOffLeftEnd) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("multiChild")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_TRUE(initialChildCount > 1); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem("multiChild", -1 * (initialChildCount + 1)); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, 0); +} + +TEST_F(CommandInsertItemTest, InsertItemMultiChildZeroIsBeforeFirstItem) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("multiChild")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_TRUE(initialChildCount > 1); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem("multiChild"); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, 0); + ASSERT_EQ(target->getChildAt(1)->getId(), "firstChild"); +} + +TEST_F(CommandInsertItemTest, InsertItemMultiChildAppendIsAfterLastItem) +{ + loadDocument(INSERT_ITEM); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("multiChild")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_TRUE(initialChildCount > 1); + ASSERT_TRUE(target->canInsertChild()); + + executeInsertItem("multiChild", INT_MAX); + + auto child = CoreComponent::cast(root->findComponentById("newArrival")); + validateInsert(target, child, initialChildCount, initialChildCount); + ASSERT_EQ(target->getChildAt(initialChildCount - 1)->getId(), "lastChild"); +} + +TEST_F(CommandInsertItemTest, InsertItemWhenTargetUsesArrayDataInflation) +{ + loadDocument( + R"( + { + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "parameters": [], + "item": { + "id": "main", + "type": "Container", + "item": { + "type": "Text", + "text": "${index+1}. ${data}" + }, + "data": [ + "Some data", + "Some other data" + ] + } + } + })"); + root->clearPending(); + + auto target = CoreComponent::cast(root->findComponentById("main")); + ASSERT_TRUE(target); + const int initialChildCount = (int) target->getChildCount(); + ASSERT_TRUE(initialChildCount > 0); + ASSERT_FALSE(target->canInsertChild()); + + executeInsertItem("main"); + + validateNonInsert("Could not insert child into 'main'", target, initialChildCount); +} \ No newline at end of file diff --git a/unit/command/unittest_command_macros.cpp b/aplcore/unit/command/unittest_command_macros.cpp similarity index 100% rename from unit/command/unittest_command_macros.cpp rename to aplcore/unit/command/unittest_command_macros.cpp diff --git a/unit/command/unittest_command_media.cpp b/aplcore/unit/command/unittest_command_media.cpp similarity index 98% rename from unit/command/unittest_command_media.cpp rename to aplcore/unit/command/unittest_command_media.cpp index 2c81309..8e81edd 100644 --- a/unit/command/unittest_command_media.cpp +++ b/aplcore/unit/command/unittest_command_media.cpp @@ -30,7 +30,7 @@ class CommandMediaTest : public CommandTest { cmd.AddMember("command", rapidjson::Value(command.c_str(), alloc).Move(), alloc); cmd.AddMember("value", value, alloc); doc.SetArray().PushBack(cmd, alloc); - return root->executeCommands(doc, fastMode); + return executeCommands(doc, fastMode); } ActionPtr executePlayMedia(const std::string& component, const std::string& audioTrack, const Object& source, bool fastMode) { @@ -41,7 +41,7 @@ class CommandMediaTest : public CommandTest { cmd.AddMember("audioTrack", rapidjson::Value(audioTrack.c_str(), alloc).Move(), alloc); cmd.AddMember("source", source.serialize(alloc).Move(), alloc); doc.SetArray().PushBack(cmd, alloc); - return root->executeCommands(doc, fastMode); + return executeCommands(doc, fastMode); } rapidjson::Document doc; @@ -301,7 +301,7 @@ TEST_F(CommandMediaTest, ControlSeries) ASSERT_TRUE(video); auto json = JsonData(COMMAND_SERIES); - auto action = root->executeCommands(json.get(), false); + auto action = executeCommands(json.get(), false); ASSERT_TRUE(action); ASSERT_TRUE(action->isPending()); diff --git a/unit/command/unittest_command_openurl.cpp b/aplcore/unit/command/unittest_command_openurl.cpp similarity index 99% rename from unit/command/unittest_command_openurl.cpp rename to aplcore/unit/command/unittest_command_openurl.cpp index 393ba96..d94286d 100644 --- a/unit/command/unittest_command_openurl.cpp +++ b/aplcore/unit/command/unittest_command_openurl.cpp @@ -313,7 +313,7 @@ TEST_F(CommandOpenURLTest, OpenURLArrayFail) rapidjson::Document doc; doc.Parse(OPEN_URL_WITH_ARRAY_FAIL); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); bool actionResolved = false; action->then([&](const ActionPtr& ptr) { actionResolved = true; diff --git a/unit/command/unittest_command_page.cpp b/aplcore/unit/command/unittest_command_page.cpp similarity index 99% rename from unit/command/unittest_command_page.cpp rename to aplcore/unit/command/unittest_command_page.cpp index 0305a06..24646b2 100644 --- a/unit/command/unittest_command_page.cpp +++ b/aplcore/unit/command/unittest_command_page.cpp @@ -29,7 +29,7 @@ class CommandPageTest : public CommandTest { cmd.AddMember("position", rapidjson::Value(position.c_str(), alloc).Move(), alloc); cmd.AddMember("value", value, alloc); doc.SetArray().PushBack(cmd, alloc); - return root->executeCommands(doc, false); + return executeCommands(doc, false); } ActionPtr executeAutoPage(const std::string& component, int count, int duration) { @@ -40,7 +40,7 @@ class CommandPageTest : public CommandTest { cmd.AddMember("count", count, alloc); cmd.AddMember("duration", duration, alloc); doc.SetArray().PushBack(cmd, alloc); - return root->executeCommands(doc, false); + return executeCommands(doc, false); } ::testing::AssertionResult @@ -612,7 +612,7 @@ TEST_F(CommandPageTest, SpeakItemCombination) ASSERT_EQ(0, component->pagePosition()); doc.Parse(COMBINATION_COMMANDS); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); // Should have preroll for first speech ASSERT_TRUE(root->hasEvent()); diff --git a/aplcore/unit/command/unittest_command_removeitem.cpp b/aplcore/unit/command/unittest_command_removeitem.cpp new file mode 100644 index 0000000..c0c5851 --- /dev/null +++ b/aplcore/unit/command/unittest_command_removeitem.cpp @@ -0,0 +1,711 @@ +/** +* Copyright 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 "../testeventloop.h" +#include "../audio/audiotest.h" + +#include "apl/component/textcomponent.h" +#include "apl/engine/event.h" +#include "apl/focus/focusmanager.h" + + +using namespace apl; + +class CommandRemoveItemTest : public AudioTest { +public: + ActionPtr executeRemoveItem(const std::string& component) { + std::map properties = {}; + + if (!component.empty()) { + properties.emplace("componentId", component); + } + + return executeCommand("RemoveItem", properties, false); + } + + ActionPtr executeSetPage(const std::string& component, int value) { + std::map properties = {}; + properties.emplace("componentId", component); + properties.emplace("position", "relative"); + properties.emplace("value", value); + + return executeCommand("SetPage", properties, false); + } +}; + +static const char *REMOVE_ITEM = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "id": "main", + "type": "Container", + "items": [ + { + "type": "Text", + "id": "unique", + "text": "Goodbye, World!" + }, + { + "type": "Text", + "id": "nonUnique", + "text": "first nonUnique" + }, + { + "type": "Text", + "id": "nonUnique", + "text": "second nonUnique" + } + ] + } + } +})"; + +TEST_F(CommandRemoveItemTest, RemoveItemMissingComponentId) +{ + loadDocument(REMOVE_ITEM); + root->clearPending(); + + executeRemoveItem(""); + + ASSERT_TRUE(session->checkAndClear("Missing required property 'componentId' for RemoveItem")); + ASSERT_FALSE(root->isDirty()); +} + +TEST_F(CommandRemoveItemTest, RemoveItemWithNonExistentComponentId) +{ + loadDocument(REMOVE_ITEM); + root->clearPending(); + auto id = "missing"; + auto toRemove = root->findComponentById(id); + ASSERT_EQ(toRemove, nullptr); + + executeRemoveItem(id); + + ASSERT_TRUE(session->checkAndClear("Illegal command RemoveItem - need to specify a target componentId")); + ASSERT_FALSE(root->isDirty()); +} + +TEST_F(CommandRemoveItemTest, RemoveItemWithNoParent) +{ + loadDocument(REMOVE_ITEM); + root->clearPending(); + auto id = "main"; + auto toRemove = root->findComponentById(id); + ASSERT_NE(toRemove, nullptr); + + executeRemoveItem(id); + + ASSERT_TRUE(session->checkAndClear("Component 'main' cannot be removed")); + ASSERT_FALSE(root->isDirty()); +} + +TEST_F(CommandRemoveItemTest, RemoveOnlyComponentWithGivenComponentId) +{ + loadDocument(REMOVE_ITEM); + root->clearPending(); + auto id = "unique"; + auto toRemove = root->findComponentById(id); + auto parent = toRemove->getParent(); + + executeRemoveItem(id); + + ASSERT_EQ(toRemove->getParent(), nullptr); + ASSERT_EQ(root->findComponentById(id), nullptr); + ASSERT_TRUE(CheckDirtyDoNotClear(parent, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(root->isDirty()); +} + +TEST_F(CommandRemoveItemTest, RemoveFirstComponentWithGivenComponentId) +{ + loadDocument(REMOVE_ITEM); + root->clearPending(); + auto id = "nonUnique"; + auto toRemove = root->findComponentById(id); + auto parent = toRemove->getParent(); + + executeRemoveItem(id); + + ASSERT_EQ(toRemove->getParent(), nullptr); + ASSERT_TRUE(CheckDirtyDoNotClear(parent, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(root->isDirty()); + + auto componentWithSameId = root->findComponentById(id); + ASSERT_NE(componentWithSameId, nullptr); + ASSERT_EQ(TextComponent::cast(componentWithSameId)->getValue(), "second nonUnique"); +} + +static const char *REMOVE_LIVEDATA = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "id": "main", + "type": "Container", + "data": "${UnRemovableArray}", + "items": [ + { + "type": "Text", + "id": "text${data}", + "text": "${data}" + } + ] + } + } +})"; + +TEST_F(CommandRemoveItemTest, RemoveItemWithLiveData) +{ + auto myArray = LiveArray::create(ObjectArray{1, 2}); + config->liveData("UnRemovableArray", myArray); + + loadDocument(REMOVE_LIVEDATA); + + executeRemoveItem("text1"); + root->clearPending(); + + ASSERT_TRUE(session->checkAndClear("Component 'text1' cannot be removed")); + ASSERT_FALSE(root->isDirty()); +} + +static const char *REMOVE_SHRINK = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "direction": "row", + "items": [ + { + "type": "Container", + "id": "parent", + "width": "auto", + "height": "auto", + "direction": "row", + "shrink": 1, + "items": [ + { + "type": "Frame", + "id": "frame1", + "width": 100, + "height": 100 + }, + { + "type": "Frame", + "id": "frame2", + "width": 100, + "height": 100 + } + ] + } + ] + } + } +})"; + +TEST_F(CommandRemoveItemTest, RemoveChildCausesLayout) +{ + loadDocument(REMOVE_SHRINK); + auto parent = root->findComponentById("parent"); + + ASSERT_TRUE(parent); + ASSERT_EQ(Rect(0,0,200,800), parent->getCalculated(apl::kPropertyBounds).get()); + + executeRemoveItem("frame1"); + root->clearPending(); + + ASSERT_EQ(Rect(0,0,100,800), parent->getCalculated(apl::kPropertyBounds).get()); +} + +static const char *REMOVE_MEDIA = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Video", + "id": "MyVideo", + "autoplay": true, + "source": "track1" + } + ] + } + } +})apl"; + +class SinglePlayerMediaFactory : public MediaPlayerFactory { +public: + class SingleMediaPlayer : public MediaPlayer { + public: + SingleMediaPlayer(MediaPlayerCallback callback) : MediaPlayer(std::move(callback)) {} + + void release() override { mReleased = true; } + void halt() override { mHalted = true; } + void setTrackList(std::vector tracks) override { mTracks = tracks; } + void play(ActionRef actionRef) override { mPlaying = true; } + void pause() override { mPlaying = false; } + void next() override {} + void previous() override {} + void rewind() override {} + void seek( int offset ) override {} + void seekTo( int offset ) override {} + void setTrackIndex( int trackIndex ) override {} + void setAudioTrack( AudioTrack audioTrack ) override {} + void setMute( bool mute ) override {} + + bool released() const { return mReleased; } + bool halted() const { return mHalted ; } + bool playing() const { return mPlaying; } + + private: + bool mReleased = false; + bool mHalted = false; + bool mPlaying = false; + std::vector mTracks; + }; + + MediaPlayerPtr createPlayer(MediaPlayerCallback callback) override { + if (!mPlayer) mPlayer = std::make_shared(std::move(callback)); + return mPlayer; + } + + MediaPlayerPtr player() const { return mPlayer; } + + +private: + MediaPlayerPtr mPlayer; +}; + +TEST_F(CommandRemoveItemTest, RemoveStopsMediaPlayback) +{ + std::shared_ptr mediaPlayerFactory = std::make_shared(); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureManageMediaRequests); + config->mediaPlayerFactory(mediaPlayerFactory); + + loadDocument(REMOVE_MEDIA); + + ASSERT_TRUE(mediaPlayerFactory->player()); + auto player = std::static_pointer_cast(mediaPlayerFactory->player()); + + ASSERT_TRUE(player->playing()); + ASSERT_FALSE(player->halted()); + + executeRemoveItem("MyVideo"); + root->clearPending(); + + ASSERT_TRUE(player->halted()); +} + +static const char *REMOVE_MEDIA_CHILD = R"apl({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Container", + "id": "MyVideoContainer", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Video", + "autoplay": true, + "source": "track1" + } + ] + } + ] + } + } +})apl"; + +TEST_F(CommandRemoveItemTest, RemoveStopsChildMediaPlayback) +{ + std::shared_ptr mediaPlayerFactory = std::make_shared(); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureManageMediaRequests); + config->mediaPlayerFactory(mediaPlayerFactory); + + loadDocument(REMOVE_MEDIA_CHILD); + + ASSERT_TRUE(mediaPlayerFactory->player()); + auto player = std::static_pointer_cast(mediaPlayerFactory->player()); + + ASSERT_TRUE(player->playing()); + ASSERT_FALSE(player->halted()); + + executeRemoveItem("MyVideoContainer"); + root->clearPending(); + + ASSERT_TRUE(player->halted()); +} + +static const char *REMOVE_SPEAK_ITEM = R"apl( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "MyText", + "speech": "URL" + } + ] + } + } +})apl"; + +TEST_F(CommandRemoveItemTest, RemoveStopsKaraokePlayback) +{ + factory->addFakeContent({ + {"URL", 100, 100, -1, {}} // 100 ms duration, 100 ms initial delay + }); + + loadDocument(REMOVE_SPEAK_ITEM); + + executeSpeakItem("MyText", apl::kCommandScrollAlignCenter, apl::kCommandHighlightModeLine, 230); + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kPreroll)); + ASSERT_FALSE(factory->hasEvent()); + + // Advance until the preroll has finished + advanceTime(100); // This should take us to the start of speech + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kReady)); + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kPlay)); + ASSERT_FALSE(factory->hasEvent()); + + executeRemoveItem("MyText"); + root->clearPending(); + + // The audio gets stopped and released. + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kPause)); + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kRelease)); + ASSERT_FALSE(factory->hasEvent()); +} + +static const char *REMOVE_SPEAK_ITEM_CHILD = R"apl( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Container", + "id": "MyTextContainer", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "MyText", + "speech": "URL" + } + ] + } + ] + } + } +})apl"; + +TEST_F(CommandRemoveItemTest, RemoveStopsKaraokeChildPlayback) +{ + factory->addFakeContent({ + {"URL", 100, 100, -1, {}} // 100 ms duration, 100 ms initial delay + }); + + loadDocument(REMOVE_SPEAK_ITEM_CHILD); + + executeSpeakItem("MyText", apl::kCommandScrollAlignCenter, apl::kCommandHighlightModeLine, 230); + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kPreroll)); + ASSERT_FALSE(factory->hasEvent()); + + // Advance until the preroll has finished + advanceTime(100); // This should take us to the start of speech + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kReady)); + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kPlay)); + ASSERT_FALSE(factory->hasEvent()); + + executeRemoveItem("MyTextContainer"); + root->clearPending(); + + // The audio gets stopped and released. + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kPause)); + ASSERT_TRUE(CheckPlayer("URL", TestAudioPlayer::kRelease)); + ASSERT_FALSE(factory->hasEvent()); +} + +static const char *FOCUS_TEST = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Sequence", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "TouchWrapper", + "id": "thing1", + "width": 100, + "height": 100 + }, + { + "type": "TouchWrapper", + "id": "thing2", + "width": 100, + "height": 100 + } + ] + } + } +})"; + +TEST_F(CommandRemoveItemTest, RemoveFocusedDoesStuff) +{ + loadDocument(FOCUS_TEST); + auto thing1 = CoreComponent::cast(root->context().findComponentById("thing1")); + auto thing2 = CoreComponent::cast(root->context().findComponentById("thing2")); + ASSERT_TRUE(thing1); + ASSERT_TRUE(thing2); + + ASSERT_FALSE(thing1->getState().get(kStateFocused)); + ASSERT_FALSE(thing2->getState().get(kStateFocused)); + + auto& fm = root->context().focusManager(); + ASSERT_FALSE(fm.getFocus()); + + fm.setFocus(thing2, true); + ASSERT_FALSE(thing1->getState().get(kStateFocused)); + ASSERT_TRUE(thing2->getState().get(kStateFocused)); + ASSERT_EQ(thing2, fm.getFocus()); + + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(thing2, event.getComponent()); + + executeRemoveItem("thing2"); + root->clearPending(); + + event = root->popEvent(); + ASSERT_EQ(kEventTypeFocus, event.getType()); + ASSERT_EQ(nullptr, event.getComponent()); + + ASSERT_FALSE(fm.getFocus()); +} + +/** + * Test using a custom MediaManager. This one assumes that all media objects are immediately + * available. + */ +class StaticMediaObject : public MediaObject { +public: + StaticMediaObject(std::string url, EventMediaType type, const HeaderArray& headers) + : mURL(std::move(url)), mType(type), mHeaders(headers) {} + + std::string url() const override { return mURL; } + State state() const override { return kReady; } + const HeaderArray& headers() const override { return mHeaders; } + EventMediaType type() const override { return mType; } + Size size() const override { return apl::Size(20,20); } + CallbackID addCallback(MediaObjectCallback callback) override { return 0; } + void removeCallback(CallbackID callbackToken) override {} + int errorCode() const override { return 0; } + std::string errorDescription() const override { return std::string(); } + + std::string mURL; + EventMediaType mType; + HeaderArray mHeaders; +}; + +class WeakHoldingMediaManager : public MediaManager { +public: + MediaObjectPtr request( + const std::string& url, + EventMediaType type, + const HeaderArray& headers) override { + auto result = std::make_shared(url, type, headers); + weakReferences.emplace(url, result); + return result; + } + + std::map> weakReferences; +}; + +static const char* REMOVABLE_MEDIA_ELEMENTS = R"({ + "type": "APL", + "version": "1.5", + "mainTemplate": { + "item": { + "type": "Container", + "items": [ + { + "type": "VectorGraphic", + "source": "http://myAVG", + "width": 100, + "height": 200, + "scale": "fill", + "id": "myAVG" + }, + { + "type": "Image", + "source": "http://myImage", + "id": "myImage" + } + ] + } + } +} +)"; + +TEST_F(CommandRemoveItemTest, RemoveClearsMediaResource) +{ + auto testManager = std::make_shared(); + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureManageMediaRequests); + config->mediaManager(testManager); + + loadDocument(REMOVABLE_MEDIA_ELEMENTS); + + ASSERT_EQ(2, testManager->weakReferences.size()); + + ASSERT_EQ(kMediaStateReady, root->findComponentById("myImage")->getCalculated(kPropertyMediaState).getInteger()); + ASSERT_EQ(kMediaStateReady, root->findComponentById("myAVG")->getCalculated(kPropertyMediaState).getInteger()); + + ASSERT_TRUE(CheckDirty(root)); + + executeRemoveItem("myImage"); + root->clearPending(); + + // Check that image got freed + ASSERT_FALSE(testManager->weakReferences.at("http://myImage").lock()); + + executeRemoveItem("myAVG"); + root->clearPending(); + + // Check that image got freed + ASSERT_FALSE(testManager->weakReferences.at("http://myAVG").lock()); +} + +static const char *PAGER_TEST = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Container", + "width": 500, + "height": 500, + "items": [ + { + "type": "Pager", + "id": "PapaPager", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Frame", + "id": "frame1", + "width": "100%", + "height": "100%", + "backgroundColor": "red" + }, + { + "type": "Frame", + "id": "frame2", + "width": "100%", + "height": "100%", + "backgroundColor": "green" + }, + { + "type": "Frame", + "id": "frame3", + "width": "100%", + "height": "100%", + "backgroundColor": "yellow" + } + ] + } + ] + } + } +})"; + +TEST_F(CommandRemoveItemTest, RemovePagerClearsPageAnimation) +{ + loadDocument(PAGER_TEST); + + auto actionRef = executeSetPage("PapaPager", 1); + + advanceTime(100); + + ASSERT_TRUE(actionRef->isPending()); + + executeRemoveItem("PapaPager"); + root->clearPending(); + + advanceTime(500); + + ASSERT_TRUE(actionRef->isTerminated()); +} + +TEST_F(CommandRemoveItemTest, RemovePagerChildMovedPage) +{ + loadDocument(PAGER_TEST); + auto pager = component->getChildAt(0); + + ASSERT_EQ(0, pager->pagePosition()); + ASSERT_EQ("frame1", pager->getChildAt(0)->getId()); + + executeRemoveItem("frame1"); + root->clearPending(); + + ASSERT_EQ(0, pager->pagePosition()); + ASSERT_EQ("frame2", pager->getChildAt(0)->getId()); +} + +TEST_F(CommandRemoveItemTest, RemovePagerLastChildMovedPage) +{ + loadDocument(PAGER_TEST); + auto actionRef = executeSetPage("PapaPager", 2); + advanceTime(600); + + auto pager = component->getChildAt(0); + + ASSERT_EQ(2, pager->pagePosition()); + ASSERT_EQ("frame3", pager->getChildAt(2)->getId()); + + executeRemoveItem("frame3"); + root->clearPending(); + + ASSERT_EQ(1, pager->pagePosition()); + ASSERT_EQ("frame2", pager->getChildAt(1)->getId()); +} \ No newline at end of file diff --git a/unit/command/unittest_command_select.cpp b/aplcore/unit/command/unittest_command_select.cpp similarity index 100% rename from unit/command/unittest_command_select.cpp rename to aplcore/unit/command/unittest_command_select.cpp diff --git a/unit/command/unittest_command_sendevent.cpp b/aplcore/unit/command/unittest_command_sendevent.cpp similarity index 98% rename from unit/command/unittest_command_sendevent.cpp rename to aplcore/unit/command/unittest_command_sendevent.cpp index 13409aa..d73b4f7 100644 --- a/unit/command/unittest_command_sendevent.cpp +++ b/aplcore/unit/command/unittest_command_sendevent.cpp @@ -78,12 +78,12 @@ const static std::vector EXPECTED = { "string", "10", "2.5", - "#00caffff", // Alpha will be apppended + "#00caffff", // Alpha will be appended "150dp", "50%", "auto", - "[1.0,2.0,3.0]", // Array - note that we use the rapidjson serialization of a number - "{\"a\":1.0,\"b\":2.0}" // Object + "[1,2,3]", // Array - note that we use the rapidjson serialization of a number + "{\"a\":1,\"b\":2}" // Object }; TEST_F(CommandSendEventTest, WithOldArguments) diff --git a/unit/command/unittest_command_setvalue.cpp b/aplcore/unit/command/unittest_command_setvalue.cpp similarity index 97% rename from unit/command/unittest_command_setvalue.cpp rename to aplcore/unit/command/unittest_command_setvalue.cpp index 94bb961..841f740 100644 --- a/unit/command/unittest_command_setvalue.cpp +++ b/aplcore/unit/command/unittest_command_setvalue.cpp @@ -30,7 +30,7 @@ class CommandSetValueTest : public CommandTest { cmd.AddMember("property", rapidjson::Value(property.c_str(), alloc).Move(), alloc); cmd.AddMember("value", value.serialize(alloc), alloc); doc.SetArray().PushBack(cmd, alloc); - return root->executeCommands(doc, false); + return executeCommands(doc, false); } ActionPtr executeSetValue(const std::string& component, const std::string& property, rapidjson::Value& value) { @@ -41,7 +41,7 @@ class CommandSetValueTest : public CommandTest { cmd.AddMember("property", rapidjson::Value(property.c_str(), alloc).Move(), alloc); cmd.AddMember("value", value.Move(), alloc); doc.SetArray().PushBack(cmd, alloc); - return root->executeCommands(doc, false); + return executeCommands(doc, false); } rapidjson::Document doc; @@ -343,17 +343,17 @@ TEST_F(CommandSetValueTest, Bind) ASSERT_TRUE(component); ASSERT_EQ(kComponentTypeContainer, component->getType()); - auto text1 = component->findComponentById("text1"); + auto text1 = root->findComponentById("text1"); ASSERT_TRUE(text1); auto t1 = text1->getCalculated(kPropertyText).asString(); ASSERT_EQ("Price1 $3.50", t1); - auto text2 = component->findComponentById("text2"); + auto text2 = root->findComponentById("text2"); ASSERT_TRUE(text2); auto t2 = text2->getCalculated(kPropertyText).asString(); ASSERT_EQ("Price2 $3.50", t2); - auto text3 = component->findComponentById("text3"); + auto text3 = root->findComponentById("text3"); ASSERT_TRUE(text3); auto t3 = text3->getCalculated(kPropertyText).asString(); ASSERT_EQ("Price3 $3.50", t3); @@ -425,21 +425,21 @@ TEST_F(CommandSetValueTest, BindObject) ASSERT_TRUE(component); ASSERT_EQ(kComponentTypeContainer, component->getType()); - auto text1 = component->findComponentById("text1"); + auto text1 = root->findComponentById("text1"); ASSERT_TRUE(text1); auto t1 = text1->getCalculated(kPropertyText).asString(); auto c1 = text1->getCalculated(kPropertyColor).asString(); ASSERT_EQ("Price1 $3.50", t1); ASSERT_EQ("#000000ff", c1); - auto text2 = component->findComponentById("text2"); + auto text2 = root->findComponentById("text2"); ASSERT_TRUE(text2); auto t2 = text2->getCalculated(kPropertyText).asString(); auto c2 = text1->getCalculated(kPropertyColor).asString(); ASSERT_EQ("Price2 $3.50", t2); ASSERT_EQ("#000000ff", c2); - auto text3 = component->findComponentById("text3"); + auto text3 = root->findComponentById("text3"); ASSERT_TRUE(text3); auto t3 = text3->getCalculated(kPropertyText).asString(); auto c3 = text1->getCalculated(kPropertyColor).asString(); diff --git a/unit/command/unittest_commands.cpp b/aplcore/unit/command/unittest_commands.cpp similarity index 100% rename from unit/command/unittest_commands.cpp rename to aplcore/unit/command/unittest_commands.cpp diff --git a/unit/command/unittest_screenlock.cpp b/aplcore/unit/command/unittest_screenlock.cpp similarity index 100% rename from unit/command/unittest_screenlock.cpp rename to aplcore/unit/command/unittest_screenlock.cpp diff --git a/unit/command/unittest_sequencer_preservation.cpp b/aplcore/unit/command/unittest_sequencer_preservation.cpp similarity index 99% rename from unit/command/unittest_sequencer_preservation.cpp rename to aplcore/unit/command/unittest_sequencer_preservation.cpp index 537d34b..aff7364 100644 --- a/unit/command/unittest_sequencer_preservation.cpp +++ b/aplcore/unit/command/unittest_sequencer_preservation.cpp @@ -260,7 +260,7 @@ TEST_F(SequencerPreservationTest, Parallel) rapidjson::Document doc; doc.Parse(COMMAND_PARALLEL_EVENT); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); advanceTime(250); @@ -313,7 +313,7 @@ TEST_F(SequencerPreservationTest, Sequential) rapidjson::Document doc; doc.Parse(COMMAND_SEQUENTIAL_EVENT); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); advanceTime(250); @@ -367,7 +367,7 @@ TEST_F(SequencerPreservationTest, AnimateNoTargetSequential) rapidjson::Document doc; doc.Parse(COMMAND_SEQUENTIAL_ANIMATE); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); auto framy = component->getCoreChildAt(0); @@ -409,7 +409,7 @@ TEST_F(SequencerPreservationTest, AnimateNoTargetParallel) rapidjson::Document doc; doc.Parse(COMMAND_PARALLEL_ANIMATE); - auto action = root->executeCommands(apl::Object(doc), false); + auto action = executeCommands(apl::Object(doc), false); auto framy = component->getCoreChildAt(0); diff --git a/unit/command/unittest_serialize_event.cpp b/aplcore/unit/command/unittest_serialize_event.cpp similarity index 97% rename from unit/command/unittest_serialize_event.cpp rename to aplcore/unit/command/unittest_serialize_event.cpp index da130d0..b859e06 100644 --- a/unit/command/unittest_serialize_event.cpp +++ b/aplcore/unit/command/unittest_serialize_event.cpp @@ -136,23 +136,9 @@ CompareValue(const Object& actual, const char * expected) return CompareValue(actualJSON, doc); } -class PokeCommand : public CoreCommand { +class PokeCommand : public TemplatedCommand { public: - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) - { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - PokeCommand(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} + COMMAND_CONSTRUCTOR(PokeCommand); const CommandPropDefSet& propDefSet() const override { @@ -198,10 +184,12 @@ class SerializeEventTest : public DocumentWrapper { // Add a new internal command. This won't do anything, but will stash what was set in the event CommandFactory::instance().set("Poke", [&](const ContextPtr& context, + CommandData&& commandData, Properties&& properties, const CoreComponentPtr& base, const std::string& parentSequencer) { auto cmd = std::static_pointer_cast(PokeCommand::create(context, + std::move(commandData), std::move(properties), base, parentSequencer)); @@ -1749,7 +1737,7 @@ TEST_F(SerializeEventTest, TargetContainer) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -1829,7 +1817,7 @@ TEST_F(SerializeEventTest, TargetFrame) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -1911,7 +1899,7 @@ TEST_F(SerializeEventTest, TargetImage) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -1999,7 +1987,7 @@ TEST_F(SerializeEventTest, TargetPager) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -2086,10 +2074,10 @@ TEST_F(SerializeEventTest, TargetScrollView) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); - auto scrollView = component->findComponentById("MyTarget"); + auto scrollView = root->findComponentById("MyTarget"); ASSERT_TRUE(scrollView); scrollView->update(kUpdateScrollPosition, 100); // Should be position 1.0 (height = 100, scrolled by 100) @@ -2178,10 +2166,10 @@ TEST_F(SerializeEventTest, TargetSequence) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); - auto sequence = component->findComponentById("MyTarget"); + auto sequence = root->findComponentById("MyTarget"); ASSERT_TRUE(sequence); // Update the scroll position. Because we limit scrolling to the "laid-out" range, we have to call @@ -2275,7 +2263,7 @@ TEST_F(SerializeEventTest, TargetText) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -2360,10 +2348,10 @@ TEST_F(SerializeEventTest, TargetTouchWrapper) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto target = component->findComponentById("MyTarget"); + auto target = root->findComponentById("MyTarget"); ASSERT_TRUE(target); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -2476,10 +2464,10 @@ TEST_F(SerializeEventTest, TouchVectorGraphic) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto target = component->findComponentById("MyTarget"); + auto target = root->findComponentById("MyTarget"); ASSERT_TRUE(target); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); // We click in global coordinates, which should be at (1,1) in the VectorGraphic component. @@ -2787,10 +2775,10 @@ TEST_F(SerializeEventTest, TargetVectorGraphic) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto target = component->findComponentById("MyTarget"); + auto target = root->findComponentById("MyTarget"); ASSERT_TRUE(target); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); performTap(0,100); @@ -2840,10 +2828,10 @@ TEST_F(SerializeEventTest, TargetVectorGraphicDirectPress) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto target = component->findComponentById("MyTarget"); + auto target = root->findComponentById("MyTarget"); ASSERT_TRUE(target); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 0); @@ -2930,10 +2918,10 @@ TEST_F(SerializeEventTest, TargetVideo) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto target = component->findComponentById("MyTarget"); + auto target = root->findComponentById("MyTarget"); ASSERT_TRUE(target); - auto touchWrapper = component->findComponentById("MyButton"); + auto touchWrapper = root->findComponentById("MyButton"); ASSERT_TRUE(touchWrapper); touchWrapper->update(kUpdatePressed, 1); @@ -3220,9 +3208,9 @@ TEST_F(SerializeEventTest, BindReferences) { ASSERT_TRUE(component); ASSERT_FALSE(root->hasEvent()); - auto button = component->findComponentById("MyButton"); + auto button = root->findComponentById("MyButton"); ASSERT_TRUE(button); - auto text = component->findComponentById("MyText"); + auto text = root->findComponentById("MyText"); ASSERT_TRUE(text); performTap(0, 10); @@ -3255,8 +3243,8 @@ TEST_F(SerializeEventTest, WeakReferences) { loadDocument(COMPARE_WEAK_REFERENCES); ASSERT_TRUE(component); - auto a = component->findComponentById("A"); - auto b = component->findComponentById("B"); + auto a = root->findComponentById("A"); + auto b = root->findComponentById("B"); ASSERT_TRUE(a); ASSERT_TRUE(b); diff --git a/unit/command/unittest_setstate.cpp b/aplcore/unit/command/unittest_setstate.cpp similarity index 100% rename from unit/command/unittest_setstate.cpp rename to aplcore/unit/command/unittest_setstate.cpp diff --git a/unit/command/unittest_setvalue.cpp b/aplcore/unit/command/unittest_setvalue.cpp similarity index 100% rename from unit/command/unittest_setvalue.cpp rename to aplcore/unit/command/unittest_setvalue.cpp diff --git a/unit/command/unittest_speak_item.cpp b/aplcore/unit/command/unittest_speak_item.cpp similarity index 99% rename from unit/command/unittest_speak_item.cpp rename to aplcore/unit/command/unittest_speak_item.cpp index 90a2dda..acf3adb 100644 --- a/unit/command/unittest_speak_item.cpp +++ b/aplcore/unit/command/unittest_speak_item.cpp @@ -28,7 +28,7 @@ class SpeakItemTest : public CommandTest { cmd.AddMember("highlightMode", rapidjson::StringRef(sHighlightModeMap.at(highlightMode).c_str()), alloc); cmd.AddMember("minimumDwellTime", minimumDwell, alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void executeSpeakItem(const ComponentPtr& component, CommandScrollAlign align, CommandHighlightMode highlightMode, int minimumDwell) { diff --git a/unit/command/unittest_speak_list.cpp b/aplcore/unit/command/unittest_speak_list.cpp similarity index 99% rename from unit/command/unittest_speak_list.cpp rename to aplcore/unit/command/unittest_speak_list.cpp index 4337346..ff36f22 100644 --- a/unit/command/unittest_speak_list.cpp +++ b/aplcore/unit/command/unittest_speak_list.cpp @@ -39,7 +39,7 @@ class SpeakListTest : public CommandTest { cmd.AddMember("minimumDwellTime", minimumDwell, alloc); cmd.AddMember("delay", delay, alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void executeSpeakList(const ComponentPtr& component, diff --git a/unit/component/CMakeLists.txt b/aplcore/unit/component/CMakeLists.txt similarity index 94% rename from unit/component/CMakeLists.txt rename to aplcore/unit/component/CMakeLists.txt index 56d4ab0..c152087 100644 --- a/unit/component/CMakeLists.txt +++ b/aplcore/unit/component/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources_local(unittest unittest_bounds.cpp unittest_component_events.cpp unittest_default_component_size.cpp + unittest_deferred_evaluation.cpp unittest_draw.cpp unittest_dynamic_component.cpp unittest_dynamic_container_properties.cpp @@ -27,6 +28,7 @@ target_sources_local(unittest unittest_flexbox.cpp unittest_frame_component.cpp unittest_grid_sequence_component.cpp + unittest_host_component.cpp unittest_layout_direction.cpp unittest_pager.cpp unittest_properties.cpp diff --git a/unit/component/unittest_accessibility_actions.cpp b/aplcore/unit/component/unittest_accessibility_actions.cpp similarity index 100% rename from unit/component/unittest_accessibility_actions.cpp rename to aplcore/unit/component/unittest_accessibility_actions.cpp diff --git a/unit/component/unittest_accessibility_api.cpp b/aplcore/unit/component/unittest_accessibility_api.cpp similarity index 87% rename from unit/component/unittest_accessibility_api.cpp rename to aplcore/unit/component/unittest_accessibility_api.cpp index 959edbf..2274d9b 100644 --- a/unit/component/unittest_accessibility_api.cpp +++ b/aplcore/unit/component/unittest_accessibility_api.cpp @@ -107,16 +107,16 @@ TEST_F(AccessibilityApiTest, Basic) { loadDocument(BASIC_TEST); - auto notAccessibleFrame = component->findComponentById("notAccessibleFrame"); + auto notAccessibleFrame = root->findComponentById("notAccessibleFrame"); ASSERT_FALSE(notAccessibleFrame->isFocusable()); ASSERT_FALSE(notAccessibleFrame->isAccessible()); - auto transparentParent = component->findComponentById("transparentParent"); + auto transparentParent = root->findComponentById("transparentParent"); ASSERT_FALSE(transparentParent->isFocusable()); ASSERT_FALSE(transparentParent->isAccessible()); ASSERT_EQ(Object(0.5), transparentParent->getCalculated(kPropertyOpacity)); - auto slightlyTransparent = component->findComponentById("slightlyTransparent"); + auto slightlyTransparent = root->findComponentById("slightlyTransparent"); ASSERT_TRUE(slightlyTransparent->isFocusable()); ASSERT_TRUE(slightlyTransparent->isAccessible()); @@ -126,27 +126,27 @@ TEST_F(AccessibilityApiTest, Basic) slightlyTransparent->getBoundsInParent(component, rect); ASSERT_EQ(Rect(0, 100, 100, 100), rect); - auto accessibleFrame = component->findComponentById("accessibleFrame"); + auto accessibleFrame = root->findComponentById("accessibleFrame"); ASSERT_FALSE(accessibleFrame->isFocusable()); ASSERT_TRUE(accessibleFrame->isAccessible()); - auto sequence = component->findComponentById("sequence"); + auto sequence = root->findComponentById("sequence"); ASSERT_TRUE(sequence->isFocusable()); ASSERT_TRUE(sequence->isAccessible()); ASSERT_TRUE(sequence->allowForward()); ASSERT_FALSE(sequence->allowBackwards()); - auto pager = component->findComponentById("pager"); + auto pager = root->findComponentById("pager"); ASSERT_TRUE(pager->isFocusable()); ASSERT_TRUE(pager->isAccessible()); ASSERT_TRUE(pager->allowForward()); ASSERT_TRUE(pager->allowBackwards()); - auto nonAccessibleVG = component->findComponentById("nonAccessibleVG"); + auto nonAccessibleVG = root->findComponentById("nonAccessibleVG"); ASSERT_FALSE(nonAccessibleVG->isFocusable()); ASSERT_FALSE(nonAccessibleVG->isAccessible()); - auto accessibleVG = component->findComponentById("accessibleVG"); + auto accessibleVG = root->findComponentById("accessibleVG"); ASSERT_TRUE(accessibleVG->isFocusable()); ASSERT_TRUE(accessibleVG->isAccessible()); } diff --git a/unit/component/unittest_bounds.cpp b/aplcore/unit/component/unittest_bounds.cpp similarity index 100% rename from unit/component/unittest_bounds.cpp rename to aplcore/unit/component/unittest_bounds.cpp diff --git a/unit/component/unittest_component_events.cpp b/aplcore/unit/component/unittest_component_events.cpp similarity index 99% rename from unit/component/unittest_component_events.cpp rename to aplcore/unit/component/unittest_component_events.cpp index 6751943..590c6ea 100644 --- a/unit/component/unittest_component_events.cpp +++ b/aplcore/unit/component/unittest_component_events.cpp @@ -828,7 +828,7 @@ static const char *MEDIA_SEND_EVENT = R"( )"; void -validateMediaEvent(RootContextPtr root, +validateMediaEvent(const CoreRootContextPtr& root, const std::string& handler, const std::string& trackIndex, const std::string& paused, diff --git a/unit/component/unittest_default_component_size.cpp b/aplcore/unit/component/unittest_default_component_size.cpp similarity index 100% rename from unit/component/unittest_default_component_size.cpp rename to aplcore/unit/component/unittest_default_component_size.cpp diff --git a/aplcore/unit/component/unittest_deferred_evaluation.cpp b/aplcore/unit/component/unittest_deferred_evaluation.cpp new file mode 100644 index 0000000..0438df2 --- /dev/null +++ b/aplcore/unit/component/unittest_deferred_evaluation.cpp @@ -0,0 +1,583 @@ +/* + * Copyright 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 "../testeventloop.h" +#include "../debugtools.h" + +using namespace apl; + +class DeferredEvaluationTest : public DocumentWrapper {}; + +// The document must be APL version 2023.2 or higher to use deferred evaluation. +static const char *VERSION_TOO_OLD_RESOURCE = R"( +{ + "type": "APL", + "version": "1.9", + "resources": [ + { + "strings": { + "A": "#{2+3}", + "B": "${eval(2+3)}" + } + } + ], + "mainTemplate": { + "items": { + "type": "Text", + "text": "A=${@A} A2=${eval(@A)} B=${@B}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, VersionTooOldResource) +{ + loadDocument(VERSION_TOO_OLD_RESOURCE); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=#{2+3} A2= B="); + ASSERT_TRUE(ConsoleMessage()); // An "Invalid function" message will be logged the first time +} + +static const char *VERSION_TOO_OLD_BINDING = R"( +{ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": { + "type": "Text", + "bind": [ + { + "name": "A", + "value": "#{2+3}" + }, + { + "name": "B", + "value": "${eval(2+3)}" + } + ], + "text": "A=${A} A2=${eval(A)} B=${B}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, VersionTooOldBinding) +{ + loadDocument(VERSION_TOO_OLD_BINDING); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=#{2+3} A2= B="); + ASSERT_TRUE(ConsoleMessage()); // An "Invalid function" message will be logged the first time +} + + +static const char *VERSION_NOT_TOO_OLD_RESOURCE = R"( +{ + "type": "APL", + "version": "2023.2", + "resources": [ + { + "strings": { + "A": "#{2+3}", + "B": "${eval(2+3)}" + } + } + ], + "mainTemplate": { + "items": { + "type": "Text", + "text": "A=${@A} A2=${eval(@A)} B=${@B}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, VersionNotTooOldResource) +{ + loadDocument(VERSION_NOT_TOO_OLD_RESOURCE); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=${2+3} A2=5 B=5"); +} + +static const char *VERSION_NOT_TOO_OLD_BINDING = R"( +{ + "type": "APL", + "version": "2023.2", + "resources": [ + { + "strings": { + "A": "#{2+3}", + "B": "${eval(2+3)}" + } + } + ], + "mainTemplate": { + "items": { + "type": "Text", + "bind": [ + { + "name": "A", + "value": "#{2+3}" + }, + { + "name": "B", + "value": "${eval(2+3)}" + } + ], + "text": "A=${@A} A2=${eval(@A)} B=${@B}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, VersionNotTooOldBinding) +{ + loadDocument(VERSION_NOT_TOO_OLD_BINDING); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=${2+3} A2=5 B=5"); +} + + +static const char *PASSING_LOCAL_ARGUMENT = R"( +{ + "type": "APL", + "version": "2023.2", + "resources": [ + { + "strings": { + "A": "The temperature is #{TEMP}" + } + } + ], + "mainTemplate": { + "items": { + "type": "Text", + "bind": { + "name": "TEMP", + "value": 23 + }, + "text": "${eval(@A)}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, PassingLocalArgument) +{ + loadDocument(PASSING_LOCAL_ARGUMENT); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "The temperature is 23"); +} + + +static const char *SHOWING_LOCALIZATION = R"( +{ + "type": "APL", + "version": "2023.2", + "resources": [ + { + "strings": { + "CELSIUS": "#{TEMP} °C", + "FAREN": "#{TEMP * 9 / 5 + 32} °F" + } + }, + { + "strings": { + "TEMPERATURE_FORMAT": "The temperature is ${@CELSIUS}" + } + }, + { + "when": "${environment.lang == 'en_US'}", + "strings": { + "TEMPERATURE_FORMAT": "The temperature is ${@FAREN}" + } + }, + { + "when": "${environment.lang == 'fr_CA'}", + "strings": { + "TEMPERATURE_FORMAT": "La température est ${@CELSIUS}" + } + } + ], + "mainTemplate": { + "items": { + "type": "Text", + "bind": { + "name": "TEMP", + "value": 25.0 + }, + "text": "${eval(@TEMPERATURE_FORMAT)}" + } + } +})"; + +static const std::vector> EXPECTED = { + { "en_US", u8"The temperature is 77 °F" }, + { "en_GB", u8"The temperature is 25 °C" }, + { "fr_CA", u8"La température est 25 °C" }, +}; + +TEST_F(DeferredEvaluationTest, ShowingLocalization) +{ + for (const auto& m : EXPECTED) { + config->set(kLang, m.first); + loadDocument(SHOWING_LOCALIZATION); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), m.second) << m.first; + } +} + + +static const char * DEFERRED_BINDINGS = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "bind": [ + { + "name": "A", + "value": "Duck" + }, + { + "name": "B", + "value": "Test value #{A}" + } + ], + "item": { + "type": "Text", + "text": "${eval(B)}" + } + } +} +)"; + +TEST_F(DeferredEvaluationTest, Bindings) +{ + loadDocument(DEFERRED_BINDINGS); + ASSERT_TRUE(component); + auto B = component->getContext()->opt("B"); + ASSERT_TRUE(IsEqual(B, "Test value ${A}")); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Test value Duck"); +} + +static const char * DEFERRED_BINDINGS_LOCAL_VALUE = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "bind": [ + { + "name": "B", + "value": "Test value #{A}" + } + ], + "item": { + "type": "Text", + "bind": { + "name": "A", + "value": "Duck" + }, + "text": "${eval(B)}" + } + } +} +)"; + +TEST_F(DeferredEvaluationTest, BindingsLocalValue) +{ + loadDocument(DEFERRED_BINDINGS_LOCAL_VALUE); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Test value Duck"); +} + +static const char * DEFERRED_BINDINGS_TWISTED = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "bind": [ + { + "name": "B", + "value": "Test value #{eval(C)}" + } + ], + "item": { + "type": "Text", + "bind": [ + { + "name": "A", + "value": "Duck" + }, + { + "name": "C", + "value": "#{'This is a ${A}'}" + } + ], + "text": "${eval(B)}" + } + } +} +)"; + +TEST_F(DeferredEvaluationTest, Twisted) +{ + loadDocument(DEFERRED_BINDINGS_TWISTED); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Test value This is a Duck"); +} + + +static const char *SYMBOL_RESOLUTION = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Text", + "id": "TEST", + "bind": [ + { + "name": "A", + "value": "#{B?C:D}" + }, + { + "name": "B", + "value": true + }, + { + "name": "C", + "value": "Foo" + }, + { + "name": "D", + "value": "Bar" + } + ], + "text": "${eval(A)}" + } + } +} +)"; + +TEST_F(DeferredEvaluationTest, SymbolResolution) { + loadDocument(SYMBOL_RESOLUTION); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Foo"); + + // Changing the value of C should result in the re-evaluation of the text + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "C"}, {"value", "Baz"}}, + true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Baz"); + + // Changing the value of D should not change things + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "D"}, {"value", "Turtle"}}, + true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Baz"); + + // Changing B will change the output text + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "B"}, {"value", false}}, + true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "Turtle"); +} + +// Test nested evaluations +static const char *NESTED_EVALUATION = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Text", + "id": "TEST", + "bind": [ + { + "name": "A", + "value": "#{B}" + }, + { + "name": "B", + "value": "#{C}" + }, + { + "name": "C", + "value": "FOO" + }, + { + "name": "D", + "value": "TURTLE" + } + ], + "text": "A=${A} eval(A)=${eval(A)} eval(eval(A))=${eval(eval(A))}" + } + } +} +)"; + +TEST_F(DeferredEvaluationTest, NestedEvaluation) +{ + loadDocument(NESTED_EVALUATION); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=${B} eval(A)=${C} eval(eval(A))=FOO"); + + // Change the value of "C" + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "C"}, {"value", "BAR"}}, true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=${B} eval(A)=${C} eval(eval(A))=BAR"); + + // Change the value of "B" + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "B"}, {"value", "#{D}"}}, true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=${B} eval(A)=${D} eval(eval(A))=TURTLE"); + + // Change the value of "D" + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "D"}, {"value", "WOMBAT"}}, true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=${B} eval(A)=${D} eval(eval(A))=WOMBAT"); + + // Change the value of "A". Note that evaluating a non-data-bound string just gives you the string + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "A"}, {"value", "THUD"}}, true); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "A=THUD eval(A)=THUD eval(eval(A))=THUD"); +} + + +// Test for infinite evaluation loops +static const char *INFINITE_LOOP_SINGLE = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Text", + "id": "TEST", + "bind": { + "name": "A", + "value": "-#{eval(A)}-" + }, + "text": "A=${A} eval(A)=${eval(A)}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, InfiniteLoopSingle) +{ + loadDocument(INFINITE_LOOP_SINGLE); + ASSERT_TRUE(component); + std::string s(kEvaluationDepthLimit, '-'); // The evaluation limit is compile-time defined + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), + "A=-${eval(A)}- eval(A)="+s+"-${eval(A)}-"+s); + ASSERT_TRUE(ConsoleMessage()); // Expect a warning message +} + +static const char *INFINITE_LOOP_PAIR = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Text", + "id": "TEST", + "bind": [ + { + "name": "A", + "value": "X#{eval(B)}" + }, + { + "name": "B", + "value": "Y#{eval(A)}" + } + ], + "text": "A=${A} B=${B} eval(A)=${eval(A)}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, InfiniteLoopPair) +{ + loadDocument(INFINITE_LOOP_PAIR); + ASSERT_TRUE(component); + // Generate the string based on the evaluation depth limit + std::string s; + for (int i = 0 ; i <= kEvaluationDepthLimit ; i++) + s += (i % 2) ? 'Y' : 'X'; + std::string s2(kEvaluationDepthLimit % 2 ? "A" : "B"); + + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), + "A=X${eval(B)} B=Y${eval(A)} eval(A)="+s+"${eval("+s2+")}"); + ASSERT_TRUE(ConsoleMessage()); // Expect a warning message +} + + +static const char *ARRAY_EVALUATION = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "bind": [ + { + "name": "A", + "value": [ + 1, + "${2}", + "#{B}" + ] + }, + { + "name": "B", + "value": 3 + }, + { + "name": "C", + "value": "${eval(A)}" + } + ], + "text": "C0=${C[0]} C1=${C[1]} C2=${C[2]}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, ArrayEvaluation) +{ + loadDocument(ARRAY_EVALUATION); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "C0=1 C1=2 C2=3"); +} + + +static const char *OBJECT_EVALUATION = R"( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "bind": [ + { + "name": "A", + "value": { + "X": 1, + "Y": "${2}", + "Z": "#{B}" + } + }, + { + "name": "B", + "value": 3 + }, + { + "name": "C", + "value": "${eval(A)}" + } + ], + "text": "CX=${C.X} CY=${C.Y} CZ=${C.Z}" + } + } +})"; + +TEST_F(DeferredEvaluationTest, ObjectEvaluation) +{ + loadDocument(OBJECT_EVALUATION); + ASSERT_TRUE(component); + ASSERT_EQ(component->getCalculated(kPropertyText).asString(), "CX=1 CY=2 CZ=3"); +} \ No newline at end of file diff --git a/unit/component/unittest_draw.cpp b/aplcore/unit/component/unittest_draw.cpp similarity index 97% rename from unit/component/unittest_draw.cpp rename to aplcore/unit/component/unittest_draw.cpp index 8dd2045..5c3d737 100644 --- a/unit/component/unittest_draw.cpp +++ b/aplcore/unit/component/unittest_draw.cpp @@ -116,8 +116,8 @@ TEST_F(ComponentDrawTest, ChildInParent) { TEST_F(ComponentDrawTest, ChildDisplay) { loadDocument(CHILD_IN_PARENT); - auto touchWrapper = as(component->findComponentById("TouchWrapper")); - auto frame = as(component->findComponentById("Frame")); + auto touchWrapper = as(root->findComponentById("TouchWrapper")); + auto frame = as(root->findComponentById("Frame")); ASSERT_TRUE(touchWrapper); ASSERT_TRUE(frame); @@ -175,8 +175,8 @@ TEST_F(ComponentDrawTest, ChildDisplay) { TEST_F(ComponentDrawTest, Opacity) { loadDocument(CHILD_IN_PARENT); - auto touchWrapper = as(component->findComponentById("TouchWrapper")); - auto frame = as(component->findComponentById("Frame")); + auto touchWrapper = as(root->findComponentById("TouchWrapper")); + auto frame = as(root->findComponentById("Frame")); ASSERT_TRUE(touchWrapper); ASSERT_TRUE(frame); @@ -807,7 +807,7 @@ TEST_F(ComponentDrawTest, Transforms) { // translated child "1" is just off bottom edge of parent // skew-ing it should bring the corner back into display - child = as(component->findComponentById("1")); + child = as(root->findComponentById("1")); ASSERT_TRUE(CheckAABB(Rect(0, 100, 100, 100), child)); TransformComponent(root, "1", "translateY", 100, "skewY", 45); ASSERT_TRUE(CheckDirty(child, kPropertyTransform)); @@ -816,7 +816,7 @@ TEST_F(ComponentDrawTest, Transforms) { // translated child "2" is just off right edge of parent // rotating it should bring the corner back into display - child = as(component->findComponentById("2")); + child = as(root->findComponentById("2")); ASSERT_TRUE(CheckAABB(Rect(100, 0, 100, 100), child)); TransformComponent(root, "2", "translateY", 100, "rotate", 45); ASSERT_TRUE(CheckDirty(child, kPropertyTransform)); @@ -824,7 +824,7 @@ TEST_F(ComponentDrawTest, Transforms) { ASSERT_TRUE(CheckAABB(Rect(-20.7, 79.3, 141.4, 141.4), child)); // child "3" is scaled to 0 size, reset transform to bring it back into display - child = as(component->findComponentById("3")); + child = as(root->findComponentById("3")); ASSERT_TRUE(CheckAABB(Rect(50, 50, 0, 0), child)); child->setProperty(kPropertyTransformAssigned, Object::EMPTY_ARRAY()); ASSERT_TRUE(CheckDirty(child, kPropertyTransform)); @@ -832,7 +832,7 @@ TEST_F(ComponentDrawTest, Transforms) { ASSERT_TRUE(CheckAABB(Rect(0, 0, 100, 100), child)); // child "4" is rotated and visible, rotate more - child = as(component->findComponentById("3")); + child = as(root->findComponentById("3")); ASSERT_TRUE(CheckAABB(Rect(0, 0, 100, 100), child)); TransformComponent(root, "3", "rotate", 90); ASSERT_TRUE(CheckDirty(child, kPropertyTransform)); @@ -843,7 +843,7 @@ TEST_F(ComponentDrawTest, Transforms) { // on the right edge of the parent // rotating it by 225 (effectively 45 degrees) should bring one of its corners // into view of the parent container - child = as(component->findComponentById("5")); + child = as(root->findComponentById("5")); ASSERT_TRUE(CheckAABB(Rect(100, 0, 100, 100), child)); TransformComponent(root, "5", "rotate", 225); ASSERT_TRUE(CheckDirty(child, kPropertyTransform)); diff --git a/unit/component/unittest_dynamic_component.cpp b/aplcore/unit/component/unittest_dynamic_component.cpp similarity index 99% rename from unit/component/unittest_dynamic_component.cpp rename to aplcore/unit/component/unittest_dynamic_component.cpp index 2e41553..a1e2de5 100644 --- a/unit/component/unittest_dynamic_component.cpp +++ b/aplcore/unit/component/unittest_dynamic_component.cpp @@ -808,8 +808,8 @@ TEST_F(DynamicComponentTest, MoveBetween) ASSERT_TRUE(component); auto height = metrics.getHeight(); - auto container = component->findComponentById("myContainer"); - auto sequence = component->findComponentById("mySequence"); + auto container = root->findComponentById("myContainer"); + auto sequence = root->findComponentById("mySequence"); ASSERT_TRUE(container); ASSERT_TRUE(sequence); diff --git a/unit/component/unittest_dynamic_container_properties.cpp b/aplcore/unit/component/unittest_dynamic_container_properties.cpp similarity index 100% rename from unit/component/unittest_dynamic_container_properties.cpp rename to aplcore/unit/component/unittest_dynamic_container_properties.cpp diff --git a/unit/component/unittest_dynamic_properties.cpp b/aplcore/unit/component/unittest_dynamic_properties.cpp similarity index 100% rename from unit/component/unittest_dynamic_properties.cpp rename to aplcore/unit/component/unittest_dynamic_properties.cpp diff --git a/unit/component/unittest_edit_text_component.cpp b/aplcore/unit/component/unittest_edit_text_component.cpp similarity index 99% rename from unit/component/unittest_edit_text_component.cpp rename to aplcore/unit/component/unittest_edit_text_component.cpp index 0e0324c..b4b872b 100644 --- a/unit/component/unittest_edit_text_component.cpp +++ b/aplcore/unit/component/unittest_edit_text_component.cpp @@ -613,10 +613,10 @@ TEST_F(EditTextComponentTest, Handlers) { auto top = root->topComponent(); ASSERT_TRUE(top); - auto et = top->findComponentById("myEditText"); + auto et = root->findComponentById("myEditText"); ASSERT_TRUE(et); ASSERT_EQ(kComponentTypeEditText, et->getType()); - auto result = top->findComponentById("myResult"); + auto result = root->findComponentById("myResult"); ASSERT_TRUE(result); ASSERT_EQ((kComponentTypeText), result->getType()); @@ -847,7 +847,7 @@ TEST_F(EditTextComponentTest, OpenKeyboardEventOnFocus) { performClick(0, 0); loop->advanceToEnd(); - auto edittext = component->findComponentById("stickyNote"); + auto edittext = root->findComponentById("stickyNote"); ASSERT_TRUE(edittext); ASSERT_EQ(kComponentTypeEditText, edittext->getType()); ASSERT_TRUE(IsEqual(kBehaviorOnFocusOpenKeyboard, edittext->getCalculated(kPropertyKeyboardBehaviorOnFocus))); diff --git a/unit/component/unittest_find_component_at_position.cpp b/aplcore/unit/component/unittest_find_component_at_position.cpp similarity index 100% rename from unit/component/unittest_find_component_at_position.cpp rename to aplcore/unit/component/unittest_find_component_at_position.cpp diff --git a/unit/component/unittest_flexbox.cpp b/aplcore/unit/component/unittest_flexbox.cpp similarity index 100% rename from unit/component/unittest_flexbox.cpp rename to aplcore/unit/component/unittest_flexbox.cpp diff --git a/unit/component/unittest_frame_component.cpp b/aplcore/unit/component/unittest_frame_component.cpp similarity index 100% rename from unit/component/unittest_frame_component.cpp rename to aplcore/unit/component/unittest_frame_component.cpp diff --git a/unit/component/unittest_grid_sequence_component.cpp b/aplcore/unit/component/unittest_grid_sequence_component.cpp similarity index 99% rename from unit/component/unittest_grid_sequence_component.cpp rename to aplcore/unit/component/unittest_grid_sequence_component.cpp index 4872246..7a5a77c 100644 --- a/unit/component/unittest_grid_sequence_component.cpp +++ b/aplcore/unit/component/unittest_grid_sequence_component.cpp @@ -29,7 +29,7 @@ class GridSequenceComponentTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component->getId().c_str(), alloc).Move(), alloc); cmd.AddMember("distance", distance, alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void completeScroll(const ComponentPtr& component, float distance) { @@ -648,7 +648,7 @@ TEST_F(GridSequenceComponentTest, ScrollEvent) { loop->runPending(); // our onScroll puts the itemsPerCourse in the Text property of the textId component - ASSERT_EQ("2", component->findComponentById("textId")->getCalculated(kPropertyText).asString()); + ASSERT_EQ("2", root->findComponentById("textId")->getCalculated(kPropertyText).asString()); } static const char* AUTO_SIZE_ALL_DOC = R"({ diff --git a/aplcore/unit/component/unittest_host_component.cpp b/aplcore/unit/component/unittest_host_component.cpp new file mode 100644 index 0000000..d581213 --- /dev/null +++ b/aplcore/unit/component/unittest_host_component.cpp @@ -0,0 +1,584 @@ +/** +* Copyright 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 "../embed/testdocumentmanager.h" +#include "../testeventloop.h" +#include "apl/common.h" +#include "apl/component/hostcomponent.h" +#include "apl/component/textcomponent.h" +#include "apl/embed/documentmanager.h" +#include "apl/engine/event.h" +#include "apl/primitives/rect.h" + +using namespace apl; + +static const char* DEFAULT_DOC = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "EmbeddedParameter": "Hello, World!", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "onLoadArtifact", + "value": "hostComponent::onLoad triggered" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "onFailArtifact", + "value": "hostComponent::onFail triggered" + } + } + ] + } + } + } +})"; + +static const char* EMBEDDED_DEFAULT = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "embeddedTop", + "item": { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + } + } + }, + "onConfigChange": [ + { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": ["EMBEDDED_DEFAULT::onConfigChange triggered"] + } + ] +})"; + +class HostComponentTest : public DocumentWrapper { +public: + HostComponentTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + + void TearDown() override + { + host = nullptr; + } + +protected: + + /** + * Load a valid APL document containing a single Host-type component with id "hostComponent." + */ + void loadDocument(const char* doc = DEFAULT_DOC) + { + DocumentWrapper::loadDocument(doc); + host = std::static_pointer_cast(root->findComponentById("hostComponent")); + ASSERT_TRUE(host); + } + + /** + * All requirements of loadDocument, in addition to the following: + * 1. The APL document must not contain components having ids: "onLoadArtifact," or "onFailArtifact." + * 2. The APL document must define the Host component to have onLoad and onFail handlers. + * 3. The Host component onLoad handler must insert a component having id "onLoadArtifact" + * 4. The Host component onFail handler must insert a component having id "onFailArtifact" + * 5. The Host component source property must be "embeddedDocumentUrl" + * Additionally, the embedded APL document must satisfy the following: + * 1. Valid APL (expected to inflate) + * 2. Does not declare any mainTemplate parameters + */ + void + nominalLoadHostAndEmbedded(const char* hostDoc = DEFAULT_DOC, const char* embedded = EMBEDDED_DEFAULT) + { + loadDocument(hostDoc); + // TODO: remove onLoad/onFail requirements, leaving them only in those tests targeting onLoad/onFail + ASSERT_FALSE(root->findComponentById("onLoadArtifact")); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + + auto content = Content::create(embedded, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + embeddedDoc = CoreDocumentContext::cast(documentManager->succeed("embeddedDocumentUrl", content, true)); + + // Displayed children ensured on the next frame + advanceTime(10); + + ASSERT_TRUE(root->findComponentById("onLoadArtifact")); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + ASSERT_TRUE(embeddedDoc.lock()); + } + + std::shared_ptr documentManager; + + std::shared_ptr host; + std::weak_ptr embeddedDoc; +}; + +TEST_F(HostComponentTest, ComponentDefaults) +{ + loadDocument(); + ASSERT_EQ(kComponentTypeHost, host->getType()); + ASSERT_TRUE(IsEqual(Rect(0,0,100,100), host->getCalculated(kPropertyBounds))); + ASSERT_TRUE(documentManager->get("embeddedDocumentUrl").lock()); +} + +TEST_F(HostComponentTest, AuthorSuppliedDimensions) +{ + loadDocument(R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "id": "hostComponent", + "type": "Host", + "height": "600", + "width": "800", + "source": "embeddedDocumentUrl" + } + } + } + })"); + + ASSERT_TRUE(IsEqual(Rect(0,0,800,600), host->getCalculated(kPropertyBounds))); + ASSERT_TRUE(documentManager->get("embeddedDocumentUrl").lock()); +} + +TEST_F(HostComponentTest, MissingSourceProperty) +{ + DocumentWrapper::loadDocument(R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "id": "hostComponent", + "type": "Host" + } + } + } + })"); + + ASSERT_EQ(session->getCount(), 2); + ASSERT_TRUE(session->check("Missing required property: source/sources")); + ASSERT_TRUE(session->check("Unable to inflate component")); + session->clear(); + + auto request = documentManager->get("embeddedDocumentUrl"); + ASSERT_FALSE(request.lock()); + ASSERT_EQ(documentManager->getResolvedRequestCount(), 0); +} + +TEST_F(HostComponentTest, TestSuccessAndFailDoNothingAfterRelease) +{ + loadDocument(); + + host->release(); + + documentManager->succeed("embeddedDocumentUrl", nullptr); + + ASSERT_FALSE(root->findComponentById("onLoadArtifact")); + + documentManager->fail("embeddedDocumentUrl", "Something went wrong"); + + ASSERT_FALSE(root->findComponentById("onFailArtifact")); +} + +TEST_F(HostComponentTest, TestSuccessAndFailDoNothingAfterDelete) +{ + loadDocument(); + + std::weak_ptr weak; + { + host->remove(); + weak = host; + host = nullptr; + } + // nobody has a reference to "host" anymore + ASSERT_TRUE(weak.lock() == nullptr); + + documentManager->succeed("embeddedDocumentUrl", nullptr); + + ASSERT_FALSE(root->findComponentById("onLoadArtifact")); + + documentManager->fail("embeddedDocumentUrl", "Something went wrong"); + + ASSERT_FALSE(root->findComponentById("onFailArtifact")); +} + +TEST_F(HostComponentTest, TestSuccessTriggersOnLoadOnce) +{ + loadDocument(); + + const std::string onLoadArtifactId = "onLoadArtifact"; + const std::string hostSourceValue = "embeddedDocumentUrl"; + + ASSERT_FALSE(root->findComponentById(onLoadArtifactId)); + + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed(hostSourceValue, content); + + auto onLoadArtifact = root->findComponentById(onLoadArtifactId); + ASSERT_TRUE(onLoadArtifact); + + onLoadArtifact->remove(); + ASSERT_FALSE(root->findComponentById(onLoadArtifactId)); + + documentManager->succeed(hostSourceValue, content); + ASSERT_FALSE(root->findComponentById(onLoadArtifactId)); + + documentManager->fail(hostSourceValue, "Something went wrong"); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); +} + +TEST_F(HostComponentTest, TestFailTriggersOnFailOnce) +{ + loadDocument(); + + const std::string onFailArtifactId = "onFailArtifact"; + const std::string hostSourceValue = "embeddedDocumentUrl"; + + ASSERT_FALSE(root->findComponentById(onFailArtifactId)); + + documentManager->fail(hostSourceValue, "Failed to resolve Content"); + + auto onFailArtifact = root->findComponentById(onFailArtifactId); + ASSERT_TRUE(onFailArtifact); + + onFailArtifact->remove(); + ASSERT_FALSE(root->findComponentById(onFailArtifactId)); + + documentManager->fail(hostSourceValue, "Failed to resolve Content"); + ASSERT_FALSE(root->findComponentById(onFailArtifactId)); + + documentManager->succeed(hostSourceValue, nullptr); + ASSERT_FALSE(root->findComponentById("onLoadArtifact")); +} + +TEST_F(HostComponentTest, TestSetSourcePropertyCancelsRequestAndNewRequestSucceeds) +{ + loadDocument(); + + const std::string originalSource = "embeddedDocumentUrl"; + const std::string newSource = "newEmbeddedDocumentUrl"; + const std::string onLoadArtifactId = "onLoadArtifact"; + + ASSERT_TRUE(documentManager->get(originalSource).lock()); + + std::static_pointer_cast(host)->setProperty(kPropertySource, newSource); + + ASSERT_FALSE(documentManager->get(originalSource).lock()); + ASSERT_TRUE(documentManager->get(newSource).lock()); + + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + documentManager->succeed(originalSource, content, false, nullptr, true); + ASSERT_FALSE(root->findComponentById(onLoadArtifactId)); + + // ...now the old request is gone + ASSERT_FALSE(documentManager->get(originalSource).lock()); + ASSERT_EQ(documentManager->getResolvedRequestCount(), 1); + ASSERT_TRUE(documentManager->get(newSource).lock()); + + documentManager->succeed(newSource, content, false, nullptr, true); + ASSERT_EQ(documentManager->getResolvedRequestCount(), 2); + ASSERT_TRUE(root->findComponentById(onLoadArtifactId)); +} + +TEST_F(HostComponentTest, TestSetSourcePropertyCancelsRequestAndNewRequestFails) +{ + loadDocument(); + + const std::string originalSource = "embeddedDocumentUrl"; + const std::string newSource = "newEmbeddedDocumentUrl"; + const std::string onFailArtifactId = "onFailArtifact"; + + ASSERT_TRUE(documentManager->get(originalSource).lock()); + + std::static_pointer_cast(host)->setProperty(kPropertySource, newSource); + + ASSERT_FALSE(documentManager->get(originalSource).lock()); + ASSERT_TRUE(documentManager->get(newSource).lock()); + + documentManager->fail(originalSource, "Something went wrong", true); + ASSERT_FALSE(root->findComponentById(onFailArtifactId)); + + // ...now the old request is gone + ASSERT_FALSE(documentManager->get(originalSource).lock()); + ASSERT_EQ(documentManager->getResolvedRequestCount(), 1); + ASSERT_TRUE(documentManager->get(newSource).lock()); + + documentManager->fail(newSource, "Something went wrong", true); + ASSERT_EQ(documentManager->getResolvedRequestCount(), 2); + ASSERT_TRUE(root->findComponentById(onFailArtifactId)); +} + +TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterSuccess) +{ + auto content = Content::create( + R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "parameters": "EmbeddedParameter", + "item": { + "type": "Container", + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${EmbeddedParameter}" + } + } + } + })", + makeDefaultSession() + ); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 1); + ASSERT_NE(pendingParameters.find("EmbeddedParameter"), pendingParameters.end()); + + loadDocument(DEFAULT_DOC); + ASSERT_TRUE(host); + ASSERT_FALSE(root->findComponentById("onLoadArtifact")); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + // onLoadHandle runs before the pending parameters are resolved, so its artifact is not sufficient + ASSERT_TRUE(root->findComponentById("onLoadArtifact")); + // If the pending parameter is not resolved, onFail would be invoked; verify onFail did not run + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + ASSERT_EQ( + std::static_pointer_cast( + CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedText"))->getValue(), + "Hello, World!"); +} + +TEST_F(HostComponentTest, TestResolvedContentWithPendingParamterFailure) +{ + auto content = Content::create( + R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "parameters": [ + "EmbeddedParameter", + "MissingParameter" + ], + "item": { + "type": "Container", + "item": { + "type": "Text", + "value": "${EmbeddedParameter}" + } + } + } + })", + makeDefaultSession() + ); + + std::set pendingParameters = content->getPendingParameters(); + ASSERT_EQ(pendingParameters.size(), 2); + ASSERT_NE(pendingParameters.find("EmbeddedParameter"), pendingParameters.end()); + ASSERT_NE(pendingParameters.find("MissingParameter"), pendingParameters.end()); + + loadDocument(); + ASSERT_FALSE(root->findComponentById("onLoadArtifact")); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + documentManager->succeed("embeddedDocumentUrl", content, true); + + ASSERT_TRUE(session->checkAndClear("Missing value for parameter MissingParameter")); + + // onLoadHandle runs before the pending parameters are resolved, so its artifact is not sufficient + ASSERT_TRUE(root->findComponentById("onLoadArtifact")); + // onFailHandler should run because the "Missing" parameter will not be resolved + ASSERT_TRUE(root->findComponentById("onFailArtifact")); +} + +TEST_F(HostComponentTest, TestFindComponentByIdTraversingHostForHostById) +{ + nominalLoadHostAndEmbedded(); + ASSERT_EQ(host->findComponentById(host->getId(), true), host); +} + +TEST_F(HostComponentTest, TestFindComponentByIdTraversingHostForHostByUId) +{ + nominalLoadHostAndEmbedded(); + ASSERT_EQ(host->findComponentById(host->getUniqueId(), true), host); +} + +TEST_F(HostComponentTest, TestFindComponentByIdTraversingHostForEmpty) +{ + nominalLoadHostAndEmbedded(); + ASSERT_EQ(host->findComponentById("", true), nullptr); +} + +TEST_F(HostComponentTest, TestFindComponentByIdTraversingHostForHostChild) +{ + nominalLoadHostAndEmbedded(); + + auto child = host->getChildAt(0); + ASSERT_TRUE(child); + auto targetId = child->getId(); + ASSERT_EQ(targetId, "embeddedTop"); + ASSERT_EQ(host->findComponentById(targetId, true), child); +} + +TEST_F(HostComponentTest, TestFindComponentByIdNotTraversingHostForHostById) +{ + nominalLoadHostAndEmbedded(); + + auto targetId = host->getId(); + ASSERT_EQ(host->findComponentById(targetId, false), host); +} + +TEST_F(HostComponentTest, TestFindComponentByIdNotTraversingHostForHostByUId) +{ + nominalLoadHostAndEmbedded(); + + auto targetId = host->getUniqueId(); + ASSERT_EQ(host->findComponentById(targetId, false), host); +} + +TEST_F(HostComponentTest, TestFindComponentByIdNotTraversingHostForEmpty) +{ + nominalLoadHostAndEmbedded(); + + ASSERT_EQ(host->findComponentById("", false), nullptr); +} + +TEST_F(HostComponentTest, TestFindComponentByIdNotTraversingHostForHostChild) +{ + nominalLoadHostAndEmbedded(); + + auto child = host->getChildAt(0); + ASSERT_TRUE(child); + auto targetId = child->getId(); + ASSERT_EQ(targetId, "embeddedTop"); + ASSERT_EQ(host->findComponentById(targetId, false), nullptr); +} + +TEST_F(HostComponentTest, TestEmbedRequestSuccessWithInflationFailure) +{ + // The following APL will fail to inflate because the "mainTemplate" layout + // is not a JSON Object, but is rather just a JSON string. + auto content = Content::create( + R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": "notAnObject" + })", + makeDefaultSession() + ); + + loadDocument(); + ASSERT_TRUE(content->isReady()); + ASSERT_FALSE(root->findComponentById("onFailArtifact")); + ASSERT_FALSE(documentManager->succeed("embeddedDocumentUrl", content)); + ASSERT_TRUE(root->findComponentById("onFailArtifact")); +} + +TEST_F(HostComponentTest, TestGetChildCountWithEmbedded) +{ + nominalLoadHostAndEmbedded(); + ASSERT_EQ(host->getChildCount(), 1); +} + +TEST_F(HostComponentTest, TestGetChildCountWithoutEmbedded) +{ + loadDocument(); + ASSERT_EQ(host->getChildCount(), 0); +} + +TEST_F(HostComponentTest, TestValidGetChildAtWithEmbedded) +{ + nominalLoadHostAndEmbedded(); + auto child = host->getChildAt(0); + ASSERT_EQ(child->getId(), "embeddedTop"); +} + +TEST_F(HostComponentTest, TestGetDisplayedChildCountWithEmbedded) +{ + nominalLoadHostAndEmbedded(); + ASSERT_EQ(host->getDisplayedChildCount(), 1); +} + +TEST_F(HostComponentTest, TestGetDisplayedChildCountWithoutEmbedded) +{ + loadDocument(); + ASSERT_EQ(host->getDisplayedChildCount(), 0); +} + +TEST_F(HostComponentTest, TestGetDisplayedChildAtWithEmbedded) +{ + nominalLoadHostAndEmbedded(); + auto child = host->getDisplayedChildAt(0); + ASSERT_EQ(child->getId(), "embeddedTop"); +} + +TEST_F(HostComponentTest, TestHostSizeChangeSendsConfigurationChangeToEmbedded) +{ + nominalLoadHostAndEmbedded(); + ASSERT_TRUE(root->isDirty()); + + auto hostInitialBounds = host->getProperty(kPropertyInnerBounds).get(); + auto embeddedTop = CoreComponent::cast(embeddedDoc.lock()->findComponentById("embeddedTop")); + auto embeddedTopInitialBounds = embeddedTop->getProperty(kPropertyBounds).get(); + + rapidjson::Document doc; + doc.Parse(R"([{ + "type": "SetValue", + "componentId": "hostComponent", + "property": "width", + "value": 50 + }])"); + root->topDocument()->executeCommands(doc, false); + + ASSERT_TRUE(CheckSendEvent(root, "EMBEDDED_DEFAULT::onConfigChange triggered")); + + ASSERT_TRUE(root->isDirty()); + auto hostNewBounds = host->getProperty(kPropertyInnerBounds).get(); + ASSERT_NE(hostInitialBounds, hostNewBounds); + auto embeddedNewBounds = embeddedTop->getProperty(kPropertyBounds).get(); + ASSERT_NE(embeddedTopInitialBounds, embeddedNewBounds); + ASSERT_EQ(hostNewBounds, embeddedNewBounds); +} diff --git a/unit/component/unittest_layout_direction.cpp b/aplcore/unit/component/unittest_layout_direction.cpp similarity index 100% rename from unit/component/unittest_layout_direction.cpp rename to aplcore/unit/component/unittest_layout_direction.cpp diff --git a/unit/component/unittest_pager.cpp b/aplcore/unit/component/unittest_pager.cpp similarity index 100% rename from unit/component/unittest_pager.cpp rename to aplcore/unit/component/unittest_pager.cpp diff --git a/unit/component/unittest_properties.cpp b/aplcore/unit/component/unittest_properties.cpp similarity index 100% rename from unit/component/unittest_properties.cpp rename to aplcore/unit/component/unittest_properties.cpp diff --git a/unit/component/unittest_scroll.cpp b/aplcore/unit/component/unittest_scroll.cpp similarity index 99% rename from unit/component/unittest_scroll.cpp rename to aplcore/unit/component/unittest_scroll.cpp index 494351a..1b11c60 100644 --- a/unit/component/unittest_scroll.cpp +++ b/aplcore/unit/component/unittest_scroll.cpp @@ -30,7 +30,7 @@ class ScrollTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); cmd.AddMember("distance", distance, alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void executeScroll(const std::string& component, const std::string& distance) { @@ -40,7 +40,7 @@ class ScrollTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); cmd.AddMember("distance", rapidjson::Value(distance.c_str(), alloc).Move(), alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void completeScroll(const ComponentPtr& component, float distance) { @@ -63,7 +63,7 @@ class ScrollTest : public DocumentWrapper { cmd.AddMember("index", index, alloc); cmd.AddMember("align", rapidjson::StringRef(sCommandAlignMap.at(align).c_str()), alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void scrollToIndex(const ComponentPtr& component, int index, CommandScrollAlign align) { @@ -79,7 +79,7 @@ class ScrollTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); cmd.AddMember("align", rapidjson::StringRef(sCommandAlignMap.at(align).c_str()), alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void scrollToComponent(const ComponentPtr& component, CommandScrollAlign align) { diff --git a/unit/component/unittest_selector.cpp b/aplcore/unit/component/unittest_selector.cpp similarity index 100% rename from unit/component/unittest_selector.cpp rename to aplcore/unit/component/unittest_selector.cpp diff --git a/unit/component/unittest_serialize.cpp b/aplcore/unit/component/unittest_serialize.cpp similarity index 100% rename from unit/component/unittest_serialize.cpp rename to aplcore/unit/component/unittest_serialize.cpp diff --git a/unit/component/unittest_signature.cpp b/aplcore/unit/component/unittest_signature.cpp similarity index 98% rename from unit/component/unittest_signature.cpp rename to aplcore/unit/component/unittest_signature.cpp index a436958..71a9ad1 100644 --- a/unit/component/unittest_signature.cpp +++ b/aplcore/unit/component/unittest_signature.cpp @@ -164,12 +164,13 @@ TEST_F(SignatureTest, EditText) { TEST_F(SignatureTest, TypesAndString) { - unsigned long len = std::string("CEXFMIPSQTWGV").length(); + unsigned long len = std::string("CEXFMHIPSQTWGV").length(); for (ComponentType type : {kComponentTypeContainer, kComponentTypeEditText, kComponentTypeExtension, kComponentTypeFrame, kComponentTypeGridSequence, + kComponentTypeHost, kComponentTypeImage, kComponentTypePager, kComponentTypeScrollView, diff --git a/unit/component/unittest_state.cpp b/aplcore/unit/component/unittest_state.cpp similarity index 100% rename from unit/component/unittest_state.cpp rename to aplcore/unit/component/unittest_state.cpp diff --git a/unit/component/unittest_text_component.cpp b/aplcore/unit/component/unittest_text_component.cpp similarity index 100% rename from unit/component/unittest_text_component.cpp rename to aplcore/unit/component/unittest_text_component.cpp diff --git a/unit/component/unittest_tick.cpp b/aplcore/unit/component/unittest_tick.cpp similarity index 100% rename from unit/component/unittest_tick.cpp rename to aplcore/unit/component/unittest_tick.cpp diff --git a/unit/component/unittest_transform.cpp b/aplcore/unit/component/unittest_transform.cpp similarity index 97% rename from unit/component/unittest_transform.cpp rename to aplcore/unit/component/unittest_transform.cpp index 8b9b300..c7bb687 100644 --- a/unit/component/unittest_transform.cpp +++ b/aplcore/unit/component/unittest_transform.cpp @@ -66,8 +66,8 @@ static const char *CHILD_IN_PARENT = TEST_F(ComponentTransformTest, ChildInParent) { loadDocument(CHILD_IN_PARENT); - auto touchWrapper = as(component->findComponentById("TouchWrapper")); - auto frame = as(component->findComponentById("Frame")); + auto touchWrapper = as(root->findComponentById("TouchWrapper")); + auto frame = as(root->findComponentById("Frame")); ASSERT_TRUE(touchWrapper); ASSERT_TRUE(frame); @@ -117,8 +117,8 @@ static const char *TRANSFORMATIONS = TEST_F(ComponentTransformTest, Transformations) { loadDocument(TRANSFORMATIONS); - auto touchWrapper = as(component->findComponentById("TouchWrapper")); - auto frame = as(component->findComponentById("Frame")); + auto touchWrapper = as(root->findComponentById("TouchWrapper")); + auto frame = as(root->findComponentById("Frame")); ASSERT_TRUE(touchWrapper); ASSERT_TRUE(frame); @@ -130,8 +130,8 @@ TEST_F(ComponentTransformTest, Transformations) { TEST_F(ComponentTransformTest, ToLocalPoint) { loadDocument(TRANSFORMATIONS); - auto touchWrapper = as(component->findComponentById("TouchWrapper")); - auto frame = as(component->findComponentById("Frame")); + auto touchWrapper = as(root->findComponentById("TouchWrapper")); + auto frame = as(root->findComponentById("Frame")); ASSERT_TRUE(touchWrapper); ASSERT_TRUE(frame); diff --git a/unit/component/unittest_visual_context.cpp b/aplcore/unit/component/unittest_visual_context.cpp similarity index 50% rename from unit/component/unittest_visual_context.cpp rename to aplcore/unit/component/unittest_visual_context.cpp index 1545d14..78b7355 100644 --- a/unit/component/unittest_visual_context.cpp +++ b/aplcore/unit/component/unittest_visual_context.cpp @@ -45,24 +45,24 @@ class VisualContextTest : public DocumentWrapper { static const char* DATA = "{}"; -static const char* BASIC = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"item\":" - " {" - " \"type\": \"Text\"," - " \"id\": \"text\"," - " \"text\": \"Text.\"," - " \"entities\": [\"entity\"]" - " }" - " }" - " }" - "}"; +static const char* BASIC = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "item": + { + "type": "Text", + "id": "text", + "text": "Text.", + "entities": ["entity"] + } + } + } +})apl"; TEST_F(VisualContextTest, Basic) { loadDocument(BASIC); @@ -127,19 +127,19 @@ TEST_F(VisualContextTest, BasicAVG) { ASSERT_FALSE(visualContext.HasMember("visibility")); } -static const char* TRANSFORM = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"," - " \"id\": \"text\"," - " \"text\": \"Text.\"," - " \"entities\": [\"entity\"]," - " \"transform\": [{ \"rotate\": 45}]" - " }" - " }" - "}"; +static const char* TRANSFORM = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Text", + "id": "text", + "text": "Text.", + "entities": ["entity"], + "transform": [{ "rotate": 45}] + } + } +})apl"; TEST_F(VisualContextTest, Transform) { loadDocument(TRANSFORM); @@ -166,15 +166,15 @@ TEST_F(VisualContextTest, Transform) { ASSERT_NEAR(-244.8, transform[5].GetFloat(), 0.1); } -static const char* EMPTY_SEQUENCE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Sequence\"" - " }" - " }" - "}"; +static const char* EMPTY_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Sequence" + } + } +})apl"; TEST_F(VisualContextTest, EmptySequence) { loadDocument(EMPTY_SEQUENCE); @@ -193,66 +193,66 @@ TEST_F(VisualContextTest, EmptySequence) { ASSERT_FALSE(tags.HasMember("list")); } -static const char* SEQUENCE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"scrollDirection\": \"vertical\"," - " \"numbered\": true," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"" - " }" - " ]" - " }" - " }" - "}"; +static const char* SEQUENCE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Sequence", + "id": "seq", + "scrollDirection": "vertical", + "numbered": true, + "items": [ + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "C ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "C ${index}-${ordinal}-${length}" + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Sequence) { loadDocument(SEQUENCE, DATA); @@ -384,66 +384,66 @@ TEST_F(VisualContextTest, Sequence) { ASSERT_EQ(4, c3t["listItem"]["index"]); } -static const char* HORIZONTAL_SEQUENCE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"scrollDirection\": \"horizontal\"," - " \"numbered\": true," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"width\": \"40dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"width\": \"40dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"width\": \"40dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"width\": \"40dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"width\": \"40dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"width\": \"40dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"" - " }" - " ]" - " }" - " }" - "}"; +static const char* HORIZONTAL_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Sequence", + "id": "seq", + "scrollDirection": "horizontal", + "numbered": true, + "items": [ + { + "type": "Text", + "id": "item_${index}", + "width": "40dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "width": "40dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "width": "40dp", + "text": "C ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "width": "40dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "width": "40dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "width": "40dp", + "text": "C ${index}-${ordinal}-${length}" + } + ] + } + } +})apl"; TEST_F(VisualContextTest, HorizontalSequence) { loadDocument(HORIZONTAL_SEQUENCE, DATA); @@ -708,72 +708,72 @@ TEST_F(VisualContextTest, RevertedSequence) { ASSERT_EQ(2, c3t["listItem"]["index"]); } -static const char* SHIFTED_SEQUENCE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"items\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"scrollDirection\": \"vertical\"," - " \"numbered\": true," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"40dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"" - " }" - " ]" - " }" - " }" - " }" - "}"; +static const char* SHIFTED_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "items": { + "type": "Sequence", + "id": "seq", + "scrollDirection": "vertical", + "numbered": true, + "position": "absolute", + "left": "100dp", + "top": "100dp", + "items": [ + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "C ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "40dp", + "text": "C ${index}-${ordinal}-${length}" + } + ] + } + } + } +})apl"; TEST_F(VisualContextTest, ShiftedSequence) { loadDocument(SHIFTED_SEQUENCE, DATA); @@ -900,69 +900,69 @@ TEST_F(VisualContextTest, ShiftedSequence) { ASSERT_EQ(4, c3t["listItem"]["index"]); } -static const char* ORDINAL_SEQUENCE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"scrollDirection\": \"vertical\"," - " \"numbered\": true," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"," - " \"numbering\": \"reset\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"numbering\": \"skip\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"" - " }" - " ]" - " }" - " }" - "}"; +static const char* ORDINAL_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Sequence", + "id": "seq", + "scrollDirection": "vertical", + "numbered": true, + "position": "absolute", + "left": "100dp", + "top": "100dp", + "items": [ + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "B ${index}-${ordinal}-${length}", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "C ${index}-${ordinal}-${length}", + "numbering": "reset", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "B ${index}-${ordinal}-${length}", + "numbering": "skip", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "C ${index}-${ordinal}-${length}" + } + ] + } + } +})apl"; TEST_F(VisualContextTest, MissingOrdinalSequence) { loadDocument(ORDINAL_SEQUENCE, DATA); @@ -987,66 +987,66 @@ TEST_F(VisualContextTest, MissingOrdinalSequence) { ASSERT_EQ(3, list["highestOrdinalSeen"].GetInt()); } -static const char* NO_ORDINAL_SEQUENCE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Sequence\"," - " \"id\": \"seq\"," - " \"scrollDirection\": \"vertical\"," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"A ${index}-${ordinal}-${length}\"," - " \"entities\": [\"${index}\", \"${ordinal}\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"B ${index}-${ordinal}-${length}\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"height\": \"10dp\"," - " \"text\": \"C ${index}-${ordinal}-${length}\"" - " }" - " ]" - " }" - " }" - "}"; +static const char* NO_ORDINAL_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Sequence", + "id": "seq", + "scrollDirection": "vertical", + "position": "absolute", + "left": "100dp", + "top": "100dp", + "items": [ + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "B ${index}-${ordinal}-${length}", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "C ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "A ${index}-${ordinal}-${length}", + "entities": ["${index}", "${ordinal}"] + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "B ${index}-${ordinal}-${length}", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_${index}", + "height": "10dp", + "text": "C ${index}-${ordinal}-${length}" + } + ] + } + } +})apl"; TEST_F(VisualContextTest, NoOrdinalSequence) { loadDocument(NO_ORDINAL_SEQUENCE, DATA); @@ -1071,59 +1071,57 @@ TEST_F(VisualContextTest, NoOrdinalSequence) { ASSERT_FALSE(list.HasMember("highestOrdinalSeen")); } -static const char* PADDED_SEQUENCE = - "{\n" - " \"type\": \"APL\",\n" - " \"version\": \"1.0\",\n" - " \"mainTemplate\": {\n" - " \"item\": {\n" - " \"type\": \"Sequence\",\n" - " \"id\": \"seq\"," - " \"scrollDirection\": \"%s\"," - " \"data\": [\"red\", \"blue\", \"green\", \"yellow\", \"purple\", \"red\", \"blue\", \"green\", \"yellow\", \"purple\", \"red\", \"blue\", \"green\", \"yellow\", \"purple\"],\n" - " \"width\": 200,\n" - " \"height\": 200,\n" - " \"left\": 0,\n" - " \"right\": 0,\n" - " \"paddingTop\": 50,\n" - " \"paddingBottom\": 25,\n" - " \"item\": {\n" - " \"type\": \"Frame\",\n" - " \"width\": 100,\n" - " \"height\": 100,\n" - " \"backgroundColor\": \"${data}\"\n" - " }\n" - " }\n" - " }\n" - "}"; - -static const char* PADDED_SCROLLVIEW = - "{\n" - " \"type\": \"APL\",\n" - " \"version\": \"1.1\",\n" - " \"mainTemplate\": {\n" - " \"item\": {\n" - " \"type\": \"ScrollView\",\n" - " \"id\": \"seq\"," - " \"width\": \"100%\",\n" - " \"height\": \"100%\",\n" - " \"paddingTop\": 25,\n" - " \"paddingLeft\": 25,\n" - " \"paddingBottom\": 50, \n" - " \"paddingRight\": 50,\n" - " \"item\": {\n" - " \"type\": \"Container\",\n" - " \"item\": {\n" - " \"type\": \"Frame\",\n" - " \"width\": 100,\n" - " \"height\": 100,\n" - " \"backgroundColor\": \"${data}\"\n" - " },\n" - " \"data\": [\"red\", \"blue\", \"green\", \"yellow\", \"purple\", \"red\", \"blue\", \"green\", \"yellow\", \"purple\", \"red\", \"blue\", \"green\", \"yellow\", \"purple\"]\n" - " }\n" - " }\n" - " }\n" - "}"; +static const char* PADDED_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "item": { + "type": "Sequence", + "id": "seq", + "scrollDirection": "%s", + "data": ["red", "blue", "green", "yellow", "purple", "red", "blue", "green", "yellow", "purple", "red", "blue", "green", "yellow", "purple"], + "width": 200, + "height": 200, + "left": 0, + "right": 0, + "paddingTop": 50, + "paddingBottom": 25, + "item": { + "type": "Frame", + "width": 100, + "height": 100, + "backgroundColor": "${data}" + } + } + } +})apl"; + +static const char* PADDED_SCROLLVIEW = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "ScrollView", + "id": "seq", + "width": "100%", + "height": "100%", + "paddingTop": 25, + "paddingLeft": 25, + "paddingBottom": 50, + "paddingRight": 50, + "item": { + "type": "Container", + "item": { + "type": "Frame", + "width": 100, + "height": 100, + "backgroundColor": "${data}" + }, + "data": ["red", "blue", "green", "yellow", "purple", "red", "blue", "green", "yellow", "purple", "red", "blue", "green", "yellow", "purple"] + } + } + } +})apl"; struct PaddedScrollableTest { PaddedScrollableTest(ComponentType type, const char* doc, std::string direction, @@ -1193,40 +1191,40 @@ TEST_F(VisualContextTest, PaddedScrollableTests) { } } -static const char* PAGER = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Pager\"," - " \"id\": \"page\"," - " \"navigation\": \"forward-only\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_0\"," - " \"text\": \"A\"," - " \"speech\": \"ssml\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_1\"," - " \"text\": \"B\"," - " \"entities\": [\"entity\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"item_2\"," - " \"text\": \"C\"," - " \"speech\": \"ssml\"" - " }" - " ]" - " }" - " }" - "}"; +static const char* PAGER = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Pager", + "id": "page", + "navigation": "forward-only", + "items": [ + { + "type": "Text", + "id": "item_0", + "text": "A", + "speech": "ssml" + }, + { + "type": "Text", + "id": "item_1", + "text": "B", + "entities": ["entity"] + }, + { + "type": "Text", + "id": "item_2", + "text": "C", + "speech": "ssml" + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Pager) { loadDocument(PAGER, DATA); @@ -1277,41 +1275,40 @@ TEST_F(VisualContextTest, Pager) { ASSERT_FALSE(reportedChild2.HasMember("tags")); } -static const char* MEDIA = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"theme\": \"auto\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Pager\"," - " \"id\": \"page\"," - " \"height\": \"100%\"," - " \"width\": \"100%\"," - " \"items\": [" - " {\n" - " \"type\": \"Video\"," - " \"id\": \"video\"," - " \"height\": \"100%\"," - " \"width\": \"100%\"," - " \"autoplay\": true," - " \"audioTrack\": \"background\"," - " \"source\": [" - " \"SOURCE0\"," - " {" - " \"url\": \"https://s3.amazonaws.com/elon-video-urls/minion1.mp4\"," - " \"entities\": [\"source\"]" - " }" - " ]," - " \"entities\": [\"video\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* MEDIA = R"apl({ + "type": "APL", + "version": "1.1", + "theme": "auto", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Pager", + "id": "page", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Video", + "id": "video", + "height": "100%", + "width": "100%", + "autoplay": true, + "audioTrack": "background", + "source": [ + "SOURCE0", + { + "url": "https://testsource.com/video-urls/video.mp4", + "entities": ["source"] + } + ], + "entities": ["video"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Media) { loadDocument(MEDIA, DATA); @@ -1355,22 +1352,22 @@ TEST_F(VisualContextTest, Media) { ASSERT_STREQ("source", entity[0].GetString()); ASSERT_EQ(1000, media["positionInMilliseconds"].GetInt()); ASSERT_STREQ("paused", media["state"].GetString()); - ASSERT_STREQ("https://s3.amazonaws.com/elon-video-urls/minion1.mp4", media["url"].GetString()); + ASSERT_STREQ("https://testsource.com/video-urls/video.mp4", media["url"].GetString()); } -static const char* EMPTY_MEDIA = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"theme\": \"auto\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Video\"" - " }" - " }" - "}"; +static const char* EMPTY_MEDIA = R"apl({ + "type": "APL", + "version": "1.1", + "theme": "auto", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Video" + } + } +})apl"; TEST_F(VisualContextTest, EmptyMedia) { loadDocument(EMPTY_MEDIA, DATA); @@ -1383,36 +1380,36 @@ TEST_F(VisualContextTest, EmptyMedia) { ASSERT_FALSE(tags.HasMember("media")); } -static const char* DEEP = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"157dp\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"50%\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"id\": \"text\"," - " \"text\": \"Short text.\"," - " \"inheritParentState\": true," - " \"entities\": [\"deep text\"]" - " }" - " }" - " ]" - " }" - " }" - "}"; +static const char* DEEP = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "157dp", + "items": [ + { + "type": "TouchWrapper", + "id": "touchWrapper", + "width": "100%", + "height": "50%", + "item": { + "type": "Text", + "id": "text", + "text": "Short text.", + "inheritParentState": true, + "entities": ["deep text"] + } + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Deep) { loadDocument(DEEP, DATA); @@ -1448,28 +1445,28 @@ TEST_F(VisualContextTest, Deep) { ASSERT_STREQ("1024x10+0+0:0", text["position"].GetString()); } -static const char* EMPTY = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"157dp\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_${index}\"," - " \"text\": \"Text without entity or spokeability.\"" - " }" - " ]" - " }" - " }" - "}"; +static const char* EMPTY = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "157dp", + "items": [ + { + "type": "Text", + "id": "item_${index}", + "text": "Text without entity or spokeability." + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Empty) { loadDocument(EMPTY, DATA); @@ -1486,28 +1483,28 @@ TEST_F(VisualContextTest, Empty) { ASSERT_FALSE(visualContext.HasMember("children")); } -static const char* INHERIT_STATE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\":" - " {" - " \"type\": \"Text\"," - " \"id\": \"item-0\"," - " \"text\": \"Inherit.\"," - " \"entities\": [\"entity\"]," - " \"inheritParentState\": true" - " }" - " }" - " }" - "}"; +static const char* INHERIT_STATE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "items": + { + "type": "Text", + "id": "item-0", + "text": "Inherit.", + "entities": ["entity"], + "inheritParentState": true + } + } + } +})apl"; TEST_F(VisualContextTest, InheritState) { loadDocument(INHERIT_STATE, DATA); @@ -1540,40 +1537,40 @@ TEST_F(VisualContextTest, InheritState) { ASSERT_FALSE(textContext["tags"].HasMember("checked")); } -static const char* STATES = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"157dp\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"item_0\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"Disabled clickable.\"" - " }" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"item_1\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"Disabled but with entity.\"" - " }," - " \"entities\": [\"entity\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* STATES = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "157dp", + "items": [ + { + "type": "TouchWrapper", + "id": "item_0", + "item": { + "type": "Text", + "text": "Disabled clickable." + } + }, + { + "type": "TouchWrapper", + "id": "item_1", + "item": { + "type": "Text", + "text": "Disabled but with entity." + }, + "entities": ["entity"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, States) { loadDocument(STATES, DATA); @@ -1643,71 +1640,70 @@ TEST_F(VisualContextTest, States) { ASSERT_FALSE(childContext["tags"].HasMember("disabled")); } -static const char* TYPE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\":" - " {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text\"," - " \"text\": \"Text.\"," - " \"entities\": [\"entity\"]" - " }," - " {" - " \"type\": \"Video\"," - " \"id\": \"video\"," - " \"height\": 300," - " \"width\": 716.8," - " \"top\": 10," - " \"left\": 100," - " \"autoplay\": true," - " \"audioTrack\": \"background\"," - " \"source\": [" - " {" - " \"url\": \"https://s3.amazonaws.com/elon-video-urls/minion1.mp4\"" - " }" - " ]," - " \"entities\": [\"video\"]" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"tw\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"id\": \"item_20\"," - " \"text\": \"Clickable.\"" - " }" - " }," - " {" - " \"type\": \"Image\"," - " \"id\": \"image\"," - " \"source\": \"http://images.amazon.com/image/foo.png\"," - " \"scale\": \"fill\"," - " \"width\": 300," - " \"height\": 300," - " \"entities\": [\"entity\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"empty\"," - " \"text\": \"\"," - " \"entities\": [\"entity\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* TYPE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": + { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "text", + "text": "Text.", + "entities": ["entity"] + }, + { + "type": "Video", + "id": "video", + "height": 300, + "width": 716.8, + "top": 10, + "left": 100, + "autoplay": true, + "audioTrack": "background", + "source": [ + { + "url": "https://testsource.com/video-urls/video.mp4" + } + ], + "entities": ["video"] + }, + { + "type": "TouchWrapper", + "id": "tw", + "item": { + "type": "Text", + "id": "item_20", + "text": "Clickable." + } + }, + { + "type": "Image", + "id": "image", + "source": "http://images.amazon.com/image/foo.png", + "scale": "fill", + "width": 300, + "height": 300, + "entities": ["entity"] + }, + { + "type": "Text", + "id": "empty", + "text": "", + "entities": ["entity"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Type) { loadDocument(TYPE, DATA); @@ -1739,30 +1735,30 @@ TEST_F(VisualContextTest, Type) { ASSERT_STREQ("graphic", c4["type"].GetString()); } -static const char* TYPE_PROPAGATE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\":" - " {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"empty\"," - " \"text\": \"text\"," - " \"entities\": [\"entity\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* TYPE_PROPAGATE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": + { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "empty", + "text": "text", + "entities": ["entity"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, TypePropagate) { loadDocument(TYPE_PROPAGATE, DATA); @@ -1783,40 +1779,40 @@ TEST_F(VisualContextTest, TypePropagate) { ASSERT_STREQ("text", c1["type"].GetString()); } -static const char* OPACITY = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"opacity\": 0.5," - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"opacity\": 0.5," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text\"," - " \"text\": \"Magic visible text.\"," - " \"entities\": [\"blah\"]," - " \"opacity\": 1.0" - " }" - " ]" - " }" - " ]" - " }" - " }" - "}"; +static const char* OPACITY = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "opacity": 0.5, + "items": [ + { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "opacity": 0.5, + "items": [ + { + "type": "Text", + "id": "text", + "text": "Magic visible text.", + "entities": ["blah"], + "opacity": 1.0 + } + ] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, Opacity) { loadDocument(OPACITY, DATA); @@ -1837,56 +1833,56 @@ TEST_F(VisualContextTest, Opacity) { ASSERT_EQ(0.25, opaqueChild["visibility"]); } -static const char* LAYERING_DEEP = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"10dp\"," - " \"top\": \"10dp\"," - " \"text\": \"Background.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"20dp\"," - " \"top\": \"20dp\"," - " \"text\": \"Middle.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text3\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"30dp\"," - " \"top\": \"30dp\"," - " \"text\": \"Forward.\"," - " \"entities\": [\"blah\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* LAYERING_DEEP = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "text1", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "10dp", + "top": "10dp", + "text": "Background.", + "entities": ["blah"] + }, + { + "type": "Text", + "id": "text2", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "20dp", + "top": "20dp", + "text": "Middle.", + "entities": ["blah"] + }, + { + "type": "Text", + "id": "text3", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "30dp", + "top": "30dp", + "text": "Forward.", + "entities": ["blah"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, LayeringDeep) { loadDocument(LAYERING_DEEP, DATA); @@ -1909,56 +1905,56 @@ TEST_F(VisualContextTest, LayeringDeep) { ASSERT_STREQ("100x100+30+30:2", child3["position"].GetString()); } -static const char* LAYERING_ONE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"text\": \"Background.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"50dp\"," - " \"top\": \"50dp\"," - " \"text\": \"Middle.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text3\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"200dp\"," - " \"top\": \"200dp\"," - " \"text\": \"Forward.\"," - " \"entities\": [\"blah\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* LAYERING_ONE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "text1", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "100dp", + "top": "100dp", + "text": "Background.", + "entities": ["blah"] + }, + { + "type": "Text", + "id": "text2", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "50dp", + "top": "50dp", + "text": "Middle.", + "entities": ["blah"] + }, + { + "type": "Text", + "id": "text3", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "200dp", + "top": "200dp", + "text": "Forward.", + "entities": ["blah"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, LayeringOne) { loadDocument(LAYERING_ONE, DATA); @@ -1981,34 +1977,34 @@ TEST_F(VisualContextTest, LayeringOne) { ASSERT_STREQ("100x100+200+200:0", child3["position"].GetString()); } -static const char* LAYERING_SINGLE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"text\": \"Background.\"," - " \"entities\": [\"blah\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* LAYERING_SINGLE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "text1", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "100dp", + "top": "100dp", + "text": "Background.", + "entities": ["blah"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, LayeringSingle) { loadDocument(LAYERING_SINGLE, DATA); @@ -2027,56 +2023,56 @@ TEST_F(VisualContextTest, LayeringSingle) { ASSERT_STREQ("100x100+100+100:0", child["position"].GetString()); } -static const char* LAYERING_TWO = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"text\": \"Background.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"50dp\"," - " \"top\": \"50dp\"," - " \"text\": \"Middle.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text3\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"150dp\"," - " \"top\": \"150dp\"," - " \"text\": \"Forward.\"," - " \"entities\": [\"blah\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* LAYERING_TWO = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "text1", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "100dp", + "top": "100dp", + "text": "Background.", + "entities": ["blah"] + }, + { + "type": "Text", + "id": "text2", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "50dp", + "top": "50dp", + "text": "Middle.", + "entities": ["blah"] + }, + { + "type": "Text", + "id": "text3", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "150dp", + "top": "150dp", + "text": "Forward.", + "entities": ["blah"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, LayeringTwo) { loadDocument(LAYERING_TWO, DATA); @@ -2099,54 +2095,54 @@ TEST_F(VisualContextTest, LayeringTwo) { ASSERT_STREQ("100x100+150+150:1", child3["position"].GetString()); } -static const char* LAYERING_INC = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"100dp\"," - " \"top\": \"100dp\"," - " \"text\": \"Background.\"," - " \"entities\": [\"blah\"]" - " }," - " {" - " \"type\": \"Container\"," - " \"id\": \"ctr2\"," - " \"height\": \"100dp\"," - " \"width\": \"100dp\"," - " \"position\": \"absolute\"," - " \"left\": \"50dp\"," - " \"top\": \"50dp\"," - " \"items\":" - " [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text3\"," - " \"height\": \"100%\"," - " \"width\": \"100%\"," - " \"text\": \"Forward.\"," - " \"entities\": [\"blah\"]" - " }" - " ]" - " }" - " ]" - " }" - " }" - "}"; +static const char* LAYERING_INC = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Text", + "id": "text1", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "100dp", + "top": "100dp", + "text": "Background.", + "entities": ["blah"] + }, + { + "type": "Container", + "id": "ctr2", + "height": "100dp", + "width": "100dp", + "position": "absolute", + "left": "50dp", + "top": "50dp", + "items": + [ + { + "type": "Text", + "id": "text3", + "height": "100%", + "width": "100%", + "text": "Forward.", + "entities": ["blah"] + } + ] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, LayeringIncapsulated) { loadDocument(LAYERING_INC, DATA); @@ -2167,30 +2163,30 @@ TEST_F(VisualContextTest, LayeringIncapsulated) { ASSERT_STREQ("100x100+50+50:1", child2["position"].GetString()); } -static const char* OPACITY_CHANGE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"157dp\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_0\"," - " \"text\": \"Text.\"," - " \"entities\": [\"entity\"]," - " \"opacity\": 0.0" - " }" - " ]" - " }" - " }" - "}"; +static const char* OPACITY_CHANGE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "157dp", + "items": [ + { + "type": "Text", + "id": "item_0", + "text": "Text.", + "entities": ["entity"], + "opacity": 0.0 + } + ] + } + } +})apl"; TEST_F(VisualContextTest, OpacityChange) { loadDocument(OPACITY_CHANGE, DATA); @@ -2230,29 +2226,29 @@ TEST_F(VisualContextTest, OpacityChange) { ASSERT_FALSE(visualContext.HasMember("children")); } -static const char* DISPLAY_CHANGE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"100%\"," - " \"height\": \"157dp\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_0\"," - " \"text\": \"Text.\"," - " \"entities\": [\"entity\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* DISPLAY_CHANGE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "100%", + "height": "157dp", + "items": [ + { + "type": "Text", + "id": "item_0", + "text": "Text.", + "entities": ["entity"] + } + ] + } + } +})apl"; TEST_F(VisualContextTest, DisplayChange) { loadDocument(DISPLAY_CHANGE, DATA); @@ -2290,31 +2286,31 @@ TEST_F(VisualContextTest, DisplayChange) { root->clearPending(); } -static const char* LAYOUT_CHANGE = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"id\": \"ctr\"," - " \"width\": \"50dp\"," - " \"height\": \"50dp\"," - " \"direction\": \"column\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"item_0\"," - " \"text\": \"Text.\"," - " \"shrink\": 1," - " \"entities\": [\"entity\"]" - " }" - " ]" - " }" - " }" - "}"; +static const char* LAYOUT_CHANGE = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "id": "ctr", + "width": "50dp", + "height": "50dp", + "direction": "column", + "items": [ + { + "type": "Text", + "id": "item_0", + "text": "Text.", + "shrink": 1, + "entities": ["entity"] + } + ] + } + } +})apl"; class VCTextMeasure : public TextMeasurement { public: @@ -2370,8 +2366,7 @@ TEST_F(VisualContextTest, LayoutChange) { ASSERT_EQ("50x30+0+0:0", child["position"]); } -static const char* EDIT_TEXT_LAYOUT_CHANGE = R"( -{ +static const char* EDIT_TEXT_LAYOUT_CHANGE = R"({ "type":"APL", "version":"1.4", "mainTemplate":{ @@ -2397,8 +2392,7 @@ static const char* EDIT_TEXT_LAYOUT_CHANGE = R"( ] } } -} -)"; +})"; TEST_F(VisualContextTest, EditTextLayoutChange) { config->measure(std::make_shared()); @@ -2499,14 +2493,7 @@ static const char* SEQUENCE_WITH_HOLE = R"apl( "height": 100, "opacity": "${index == 3 ? 0 : 1}" }, - "data": [ - 0, - 1, - 2, - 3, - 4, - 5 - ] + "data": [0, 1, 2, 3, 4, 5] } } } @@ -2624,5 +2611,178 @@ TEST_F(VisualContextTest, OddDPI) { ASSERT_EQ(0.5, child["visibility"].GetDouble()); } +static const char* DYNAMIC_ENTITIES = R"apl({ +"type": "APL", +"version": "1.1", +"mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "bind": { + "name": "COUNT", + "value": 0 + }, + "onPress": { + "type": "SetValue", + "property": "COUNT", + "value": "${COUNT + 1}" + }, + "item": { + "type": "Text", + "id": "text", + "text": "Text.", + "entities": [ + { + "id": "xyzzy", + "value": "${COUNT}" + } + ] + } + } +} +})apl"; + +TEST_F(VisualContextTest, DynamicEntities) +{ + loadDocument(DYNAMIC_ENTITIES); + ASSERT_EQ(kComponentTypeTouchWrapper, component->getType()); + // Check parent + ASSERT_TRUE(visualContext.HasMember("tags")); + ASSERT_FALSE(visualContext.HasMember("transform")); + ASSERT_FALSE(visualContext.HasMember("id")); + ASSERT_TRUE(visualContext.HasMember("uid")); + ASSERT_TRUE(visualContext["tags"].HasMember("viewport")); + ASSERT_TRUE(visualContext["tags"].HasMember("clickable")); + ASSERT_FALSE(visualContext.HasMember("visibility")); + ASSERT_STREQ("text", visualContext["type"].GetString()); + + // Check children + ASSERT_EQ(1, visualContext["children"].GetArray().Size()); + auto& child = visualContext["children"][0]; + ASSERT_STREQ("text", child["id"].GetString()); + ASSERT_STREQ("text", child["type"].GetString()); + ASSERT_FALSE(child.HasMember("tags")); + ASSERT_TRUE(child.HasMember("entities")); + ASSERT_STREQ("xyzzy", child["entities"][0]["id"].GetString()); + ASSERT_EQ(0, child["entities"][0]["value"].GetInt()); + + // Touch (verify that touching marks the visual context as dirty) + ASSERT_FALSE(root->isVisualContextDirty()); + performClick(0, 0); + ASSERT_TRUE(root->isVisualContextDirty()); + serializeVisualContext(); + ASSERT_FALSE(root->isVisualContextDirty()); + child = visualContext["children"][0]; + ASSERT_STREQ("xyzzy", child["entities"][0]["id"].GetString()); + ASSERT_EQ(1, child["entities"][0]["value"].GetInt()); + + // Touch again (just to be sure) + performClick(0, 0); + serializeVisualContext(); + child = visualContext["children"][0]; + ASSERT_STREQ("xyzzy", child["entities"][0]["id"].GetString()); + ASSERT_EQ(2, child["entities"][0]["value"].GetInt()); +} + +static const char* DYNAMIC_ENTITIES_DIRECT = R"apl({ +"type": "APL", +"version": "1.1", +"mainTemplate": { + "item": { + "type": "Text", + "id": "MAIN", + "text": "X is ${X}", + "bind": [ + { + "name": "X", + "value": 13 + }, + { + "name": "ENTITIES", + "value": { + "name": "Original", + "value": "${X}" + } + } + ], + "entity": "${ENTITIES}" + } +} +})apl"; + +TEST_F(VisualContextTest, DynamicEntitiesDirect) +{ + loadDocument(DYNAMIC_ENTITIES_DIRECT); + ASSERT_TRUE(component); + + // The initial visual context + ASSERT_TRUE(visualContext.HasMember("tags")); + ASSERT_FALSE(visualContext.HasMember("transform")); + ASSERT_TRUE(visualContext.HasMember("id")); + ASSERT_TRUE(visualContext.HasMember("uid")); + ASSERT_TRUE(visualContext["tags"].HasMember("viewport")); + ASSERT_FALSE(visualContext.HasMember("visibility")); + ASSERT_STREQ("text", visualContext["type"].GetString()); + ASSERT_TRUE(visualContext.HasMember("entities")); + ASSERT_TRUE(visualContext["entities"].IsArray()); + ASSERT_EQ(1, visualContext["entities"].GetArray().Size()); + ASSERT_STREQ("Original", visualContext["entities"][0]["name"].GetString()); + ASSERT_EQ(13, visualContext["entities"][0]["value"].GetInt()); + + ASSERT_FALSE(root->isVisualContextDirty()); + // Now change X + executeCommand("SetValue", {{"componentId", "MAIN"}, {"property", "X"}, {"value", false}}, true); + ASSERT_TRUE(root->isVisualContextDirty()); + serializeVisualContext(); + ASSERT_FALSE(root->isVisualContextDirty()); + ASSERT_TRUE(visualContext.HasMember("entities")); + ASSERT_TRUE(visualContext["entities"].IsArray()); + ASSERT_EQ(1, visualContext["entities"].GetArray().Size()); + ASSERT_STREQ("Original", visualContext["entities"][0]["name"].GetString()); + ASSERT_EQ(false, visualContext["entities"][0]["value"].GetBool()); + + // Change ENTITIES + auto value = std::make_shared(ObjectMap{{"name", "New"}, {"value", "duck"}}); + executeCommand("SetValue", {{"componentId", "MAIN"}, {"property", "ENTITIES"}, {"value", value}}, true); + ASSERT_TRUE(root->isVisualContextDirty()); + serializeVisualContext(); + ASSERT_FALSE(root->isVisualContextDirty()); + ASSERT_TRUE(visualContext.HasMember("entities")); + ASSERT_TRUE(visualContext["entities"].IsArray()); + ASSERT_EQ(1, visualContext["entities"].GetArray().Size()); + ASSERT_STREQ("New", visualContext["entities"][0]["name"].GetString()); + ASSERT_STREQ("duck", visualContext["entities"][0]["value"].GetString()); + + // Change the size of ENTITIES + auto array = std::make_shared(); + array->push_back(std::make_shared(ObjectMap{{"name", "A"}, {"value", "aardwolf"}})); + array->push_back(std::make_shared(ObjectMap{{"name", "B"}, {"value", "budgie"}})); + executeCommand("SetValue", {{"componentId", "MAIN"}, {"property", "ENTITIES"}, {"value", array}}, true); + serializeVisualContext(); + ASSERT_TRUE(visualContext.HasMember("entities")); + ASSERT_TRUE(visualContext["entities"].IsArray()); + ASSERT_EQ(2, visualContext["entities"].GetArray().Size()); + ASSERT_STREQ("A", visualContext["entities"][0]["name"].GetString()); + ASSERT_STREQ("aardwolf", visualContext["entities"][0]["value"].GetString()); + ASSERT_STREQ("B", visualContext["entities"][1]["name"].GetString()); + ASSERT_STREQ("budgie", visualContext["entities"][1]["value"].GetString()); + + // Change to string ENTITIES + executeCommand("SetValue", {{"componentId", "MAIN"}, {"property", "ENTITIES"}, {"value", "toad"}}, true); + serializeVisualContext(); + ASSERT_TRUE(visualContext.HasMember("entities")); + ASSERT_TRUE(visualContext["entities"].IsArray()); + ASSERT_EQ(1, visualContext["entities"].GetArray().Size()); + ASSERT_STREQ("toad", visualContext["entities"][0].GetString()); + + // Empty string for ENTITIES + executeCommand("SetValue", {{"componentId", "MAIN"}, {"property", "ENTITIES"}, {"value", ""}}, true); + serializeVisualContext(); + ASSERT_TRUE(visualContext.HasMember("entities")); + ASSERT_TRUE(visualContext["entities"].IsArray()); + ASSERT_EQ(1, visualContext["entities"].GetArray().Size()); + ASSERT_STREQ("", visualContext["entities"][0].GetString()); +} \ No newline at end of file diff --git a/unit/component/unittest_visual_hash.cpp b/aplcore/unit/component/unittest_visual_hash.cpp similarity index 100% rename from unit/component/unittest_visual_hash.cpp rename to aplcore/unit/component/unittest_visual_hash.cpp diff --git a/unit/content/CMakeLists.txt b/aplcore/unit/content/CMakeLists.txt similarity index 95% rename from unit/content/CMakeLists.txt rename to aplcore/unit/content/CMakeLists.txt index 51ac63b..8592cd5 100644 --- a/unit/content/CMakeLists.txt +++ b/aplcore/unit/content/CMakeLists.txt @@ -18,5 +18,6 @@ target_sources_local(unittest unittest_document.cpp unittest_document_background.cpp unittest_jsondata.cpp + unittest_metrics.cpp unittest_rootconfig.cpp ) \ No newline at end of file diff --git a/unit/content/unittest_apl.cpp b/aplcore/unit/content/unittest_apl.cpp similarity index 65% rename from unit/content/unittest_apl.cpp rename to aplcore/unit/content/unittest_apl.cpp index 2bbe02f..3a0a15e 100644 --- a/unit/content/unittest_apl.cpp +++ b/aplcore/unit/content/unittest_apl.cpp @@ -27,82 +27,80 @@ using namespace apl; -static const char *MAIN = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"import\": [" - " {" - " \"name\": \"basic\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"SendEvent\"," - " \"arguments\": \"test\"" - " }" - " ]," - " \"item\": {" - " \"type\": \"Frame\"," - " \"inheritParentState\": true," - " \"style\": \"frameStyle\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"inheritParentState\": true," - " \"text\": \"${payload}\"," - " \"style\": \"textStyle\"" - " }" - " }" - " }" - " }" - "}"; +static const char *MAIN = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "basic", + "version": "1.2" + } + ], + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "SendEvent", + "arguments": "test" + } + ], + "item": { + "type": "Frame", + "inheritParentState": true, + "style": "frameStyle", + "item": { + "type": "Text", + "inheritParentState": true, + "text": "${payload}", + "style": "textStyle" + } + } + } + } +})apl"; -static const char *BASIC = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"resources\": [" - " {" - " \"colors\": {" - " \"myRed\": \"rgb(255, 16, 32)\"" - " }" - " }" - " ]," - " \"styles\": {" - " \"frameStyle\": {" - " \"values\": [" - " {" - " \"borderWidth\": 2," - " \"borderColor\": \"transparent\"" - " }," - " {" - " \"when\": \"${state.pressed}\"," - " \"borderColor\": \"green\"" - " }" - " ]" - " }," - " \"textStyle\": {" - " \"values\": [" - " {" - " \"color\": \"@myRed\"" - " }," - " {" - " \"when\": \"${state.pressed}\"," - " \"color\": \"blue\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *BASIC = R"apl({ + "type": "APL", + "version": "1.0", + "resources": [ + { + "colors": { + "myRed": "rgb(255, 16, 32)" + } + } + ], + "styles": { + "frameStyle": { + "values": [ + { + "borderWidth": 2, + "borderColor": "transparent" + }, + { + "when": "${state.pressed}", + "borderColor": "green" + } + ] + }, + "textStyle": { + "values": [ + { + "color": "@myRed" + }, + { + "when": "${state.pressed}", + "color": "blue" + } + ] + } + } +})apl"; class MyTextMeasure : public TextMeasurement { public: @@ -120,7 +118,8 @@ class MyTextMeasure : public TextMeasurement { TEST(APLTest, Basic) { // Load the main document - auto content = Content::create(MAIN, makeDefaultSession()); + auto session = makeDefaultSession(); + auto content = Content::create(MAIN, session); ASSERT_TRUE(content); // The document has one import it is waiting for @@ -155,7 +154,7 @@ TEST(APLTest, Basic) ASSERT_EQ(Rect(2, 2, 120, 60), text->getCalculated(kPropertyBounds).get()); // Frame has a 2 dp border ASSERT_EQ(StyledText::create(root->context(), "Your text inserted here"), text->getCalculated(kPropertyText).get()); - ASSERT_EQ(Object(Color(root->getSession(), "#ff1020")), text->getCalculated(kPropertyColor)); + ASSERT_EQ(Object(Color(session, "#ff1020")), text->getCalculated(kPropertyColor)); // Simulate a user touching on the screen root->handlePointerEvent(PointerEvent(kPointerDown, Point(1,1))); @@ -167,7 +166,7 @@ TEST(APLTest, Basic) ASSERT_EQ(Object(Color(Color::GREEN)), frame->getCalculated(kPropertyBorderColor)); ASSERT_EQ(1, dirty.count(text)); ASSERT_EQ(1, text->getDirty().count(kPropertyColor)); - ASSERT_EQ(Object(Color(root->getSession(), "blue")), text->getCalculated(kPropertyColor)); + ASSERT_EQ(Object(Color(session, "blue")), text->getCalculated(kPropertyColor)); root->clearDirty(); // Simulate releasing in the touchwrapper @@ -180,4 +179,9 @@ TEST(APLTest, Basic) ASSERT_EQ(1, args.size()); ASSERT_EQ(Object("test"), args.at(0)); ASSERT_TRUE(event.getActionRef().empty()); + + // Check findByURI API + ASSERT_EQ( + static_cast(root->findByUniqueId(root->topComponent()->getUniqueId())), + root->topComponent().get()); } diff --git a/unit/content/unittest_directive.cpp b/aplcore/unit/content/unittest_directive.cpp similarity index 100% rename from unit/content/unittest_directive.cpp rename to aplcore/unit/content/unittest_directive.cpp diff --git a/unit/content/unittest_document.cpp b/aplcore/unit/content/unittest_document.cpp similarity index 85% rename from unit/content/unittest_document.cpp rename to aplcore/unit/content/unittest_document.cpp index e7a9559..3b5dfad 100644 --- a/unit/content/unittest_document.cpp +++ b/aplcore/unit/content/unittest_document.cpp @@ -16,13 +16,14 @@ #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" #include "gtest/gtest.h" + #include "apl/buildTimeConstants.h" #include "apl/content/content.h" +#include "apl/content/importrequest.h" #include "apl/content/metrics.h" -#include "apl/content/rootconfig.h" +#include "apl/document/documentcontext.h" #include "apl/engine/context.h" #include "apl/engine/rootcontext.h" -#include "apl/content/importrequest.h" using namespace apl; @@ -1098,16 +1099,16 @@ TEST(DocumentTest, ExternalCommandTest) content->addData("payload", R"({"start": "Is Not Set", "end": "Is Set"})"); ASSERT_TRUE(content->isReady()); - auto doc = RootContext::create(Metrics(), content, RootConfig()); + auto root = RootContext::create(Metrics(), content, RootConfig()); - ASSERT_TRUE(doc); - ASSERT_STREQ("Is Not Set", doc->topComponent()->getCalculated(kPropertyText).asString().c_str()); + ASSERT_TRUE(root); + ASSERT_STREQ("Is Not Set", root->topComponent()->getCalculated(kPropertyText).asString().c_str()); auto cmd = JsonData(EXTERNAL_COMMAND_TEST_COMMAND); ASSERT_TRUE(cmd); - doc->executeCommands(cmd.get(), false); - ASSERT_STREQ("Is Set", doc->topComponent()->getCalculated(kPropertyText).asString().c_str()); + root->topDocument()->executeCommands(cmd.get(), false); + ASSERT_STREQ("Is Set", root->topComponent()->getCalculated(kPropertyText).asString().c_str()); } @@ -1212,7 +1213,8 @@ TEST(DocumentTest, LogId) auto logBridge = std::make_shared(); LoggerFactory::instance().initialize(logBridge); - auto content = Content::create(NO_DIAGNOSTIC_TAG, makeDefaultSession()); + auto session = makeDefaultSession(); + auto content = Content::create(NO_DIAGNOSTIC_TAG, session); ASSERT_TRUE(content->isReady()); auto m = Metrics().size(1024,800).theme("dark"); @@ -1221,14 +1223,14 @@ TEST(DocumentTest, LogId) ASSERT_TRUE(doc); - ASSERT_EQ(doc->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + + ASSERT_EQ(session->getLogId() + ":content.cpp:Content : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); logBridge->reset(); - ASSERT_EQ(10, doc->getSession()->getLogId().size()); - LOG(LogLevel::kInfo).session(doc->getSession()) << "TEST"; - ASSERT_EQ(doc->getSession()->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); + ASSERT_EQ(10, session->getLogId().size()); + LOG(LogLevel::kInfo).session(session) << "TEST"; + ASSERT_EQ(session->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); LoggerFactory::instance().reset(); } @@ -1251,7 +1253,8 @@ TEST(DocumentTest, ShortLogId) auto logBridge = std::make_shared(); LoggerFactory::instance().initialize(logBridge); - auto content = Content::create(LOG_ID_WITH_PREFIX, makeDefaultSession()); + auto session = makeDefaultSession(); + auto content = Content::create(LOG_ID_WITH_PREFIX, session); ASSERT_TRUE(content->isReady()); auto m = Metrics().size(1024,800).theme("dark"); @@ -1260,13 +1263,13 @@ TEST(DocumentTest, ShortLogId) ASSERT_TRUE(doc); - ASSERT_TRUE(doc->getSession()->getLogId().rfind("FOOBAR-", 0) == 0); - ASSERT_EQ(doc->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); + ASSERT_TRUE(session->getLogId().rfind("FOOBAR-", 0) == 0); + ASSERT_EQ(session->getLogId() + ":content.cpp:Content : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); logBridge->reset(); - LOG(LogLevel::kInfo).session(doc->getSession()) << "TEST"; - ASSERT_EQ(17, doc->getSession()->getLogId().size()); - ASSERT_EQ(doc->getSession()->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); + LOG(LogLevel::kInfo).session(session) << "TEST"; + ASSERT_EQ(17, session->getLogId().size()); + ASSERT_EQ(session->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); LoggerFactory::instance().reset(); } @@ -1276,13 +1279,15 @@ TEST(DocumentTest, TwoDocuments) auto logBridge = std::make_shared(); LoggerFactory::instance().initialize(logBridge); - auto content1 = Content::create(LOG_ID_WITH_PREFIX, makeDefaultSession()); + auto session1 = makeDefaultSession(); + auto content1 = Content::create(LOG_ID_WITH_PREFIX, session1); ASSERT_TRUE(content1->isReady()); ASSERT_TRUE(content1->getSession()->getLogId().rfind("FOOBAR-", 0) == 0); ASSERT_EQ(content1->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + std::string(sCoreRepositoryVersion), logBridge->mLog); - auto content2 = Content::create(LOG_ID_WITH_PREFIX, makeDefaultSession()); + auto session2 = makeDefaultSession(); + auto content2 = Content::create(LOG_ID_WITH_PREFIX, session2); ASSERT_TRUE(content2->isReady()); ASSERT_TRUE(content2->getSession()->getLogId().rfind("FOOBAR-", 0) == 0); ASSERT_EQ(content2->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + @@ -1298,15 +1303,179 @@ TEST(DocumentTest, TwoDocuments) auto doc2 = RootContext::create(m, content2, config2); ASSERT_TRUE(doc2); - LOG(LogLevel::kInfo).session(doc1->getSession()) << "TEST"; - ASSERT_EQ(17, doc1->getSession()->getLogId().size()); - ASSERT_EQ(doc1->getSession()->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); + LOG(LogLevel::kInfo).session(session1) << "TEST"; + ASSERT_EQ(17, session1->getLogId().size()); + ASSERT_EQ(session1->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); - LOG(LogLevel::kInfo).session(doc2->getSession()) << "TEST"; - ASSERT_EQ(17, doc2->getSession()->getLogId().size()); - ASSERT_EQ(doc2->getSession()->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); + LOG(LogLevel::kInfo).session(session2) << "TEST"; + ASSERT_EQ(17, session2->getLogId().size()); + ASSERT_EQ(session2->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); - ASSERT_NE(doc1->getSession()->getLogId(), doc2->getSession()->getLogId()); + ASSERT_NE(session1->getLogId(), session2->getLogId()); LoggerFactory::instance().reset(); } + +const char* NESTED_REPEATED_IMPORT = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "A", + "version": "1.0" + }, + { + "name": "B", + "version": "1.0" + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +const char* A_IMPORTS_B = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "B", + "version": "1.0", + "source": "custom.json" + } + ], + "resources": [ + { + "strings": { + "A": "A" + } + } + ] +})apl"; + +const char* B = R"apl({ + "type": "APL", + "version": "1.0", + "resources": [ + { + "strings": { + "B": "B" + } + } + ] +})apl"; + +TEST(DocumentTest, NestedRepeatedImportPendingDoesNotRequest) +{ + auto doc = Content::create(NESTED_REPEATED_IMPORT, makeDefaultSession()); + ASSERT_TRUE(doc); + ASSERT_TRUE(doc->isWaiting()); + auto requested = doc->getRequestedPackages(); + ASSERT_EQ(2, requested.size()); + + // The requested list is cleared + ASSERT_EQ(0, doc->getRequestedPackages().size()); + + for (auto it = requested.begin() ; it != requested.end() ; it++) { + auto s = it->reference(); + if (s == ImportRef("A", "1.0")) + doc->addPackage(*it, A_IMPORTS_B); + } + + // We don't request "B" again even though "A" imports it with a different version + ASSERT_EQ(0, doc->getRequestedPackages().size()); + + for (auto it = requested.begin() ; it != requested.end() ; it++) { + auto s = it->reference(); + if (s == ImportRef("B", "1.0")) + doc->addPackage(*it, B); + } + + ASSERT_FALSE(doc->isWaiting()); + ASSERT_TRUE(doc->isReady()); + + auto expected = std::vector{ "A:1.0", "B:1.0" }; + ASSERT_EQ(expected, doc->getLoadedPackageNames()); +} + +TEST(DocumentTest, NestedRepeatedImportLoadedDoesNotRequest) +{ + auto doc = Content::create(NESTED_REPEATED_IMPORT, makeDefaultSession()); + ASSERT_TRUE(doc); + ASSERT_TRUE(doc->isWaiting()); + auto requested = doc->getRequestedPackages(); + ASSERT_EQ(2, requested.size()); + + // The requested list is cleared + ASSERT_EQ(0, doc->getRequestedPackages().size()); + + for (auto it = requested.begin() ; it != requested.end() ; it++) { + auto s = it->reference(); + if (s == ImportRef("B", "1.0")) + doc->addPackage(*it, B); + } + + for (auto it = requested.begin() ; it != requested.end() ; it++) { + auto s = it->reference(); + if (s == ImportRef("A", "1.0")) + doc->addPackage(*it, A_IMPORTS_B); + } + + ASSERT_EQ(0, doc->getRequestedPackages().size()); + + ASSERT_FALSE(doc->isWaiting()); + ASSERT_TRUE(doc->isReady()); + + auto expected = std::vector{ "A:1.0", "B:1.0" }; + ASSERT_EQ(expected, doc->getLoadedPackageNames()); +} + +const char* REPEATED_IMPORT_DIFFERENT_SOURCES = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "B", + "version": "1.0", + "source": "custom.json" + }, + { + "name": "B", + "version": "1.0", + "source": "other.json" + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST(DocumentTest, RepeatedImportDifferentSources) +{ + auto doc = Content::create(REPEATED_IMPORT_DIFFERENT_SOURCES, makeDefaultSession()); + ASSERT_TRUE(doc); + ASSERT_TRUE(doc->isWaiting()); + auto requested = doc->getRequestedPackages(); + ASSERT_EQ(1, requested.size()); + + // The requested list is cleared + ASSERT_EQ(0, doc->getRequestedPackages().size()); + + for (auto it = requested.begin() ; it != requested.end() ; it++) { + auto s = it->reference(); + if (s == ImportRef("B", "1.0") && it->source() == "custom.json") + doc->addPackage(*it, B); + } + + ASSERT_EQ(0, doc->getRequestedPackages().size()); + + ASSERT_FALSE(doc->isWaiting()); + ASSERT_TRUE(doc->isReady()); + + auto expected = std::vector{ "B:1.0" }; + ASSERT_EQ(expected, doc->getLoadedPackageNames()); +} \ No newline at end of file diff --git a/unit/content/unittest_document_background.cpp b/aplcore/unit/content/unittest_document_background.cpp similarity index 100% rename from unit/content/unittest_document_background.cpp rename to aplcore/unit/content/unittest_document_background.cpp diff --git a/unit/content/unittest_jsondata.cpp b/aplcore/unit/content/unittest_jsondata.cpp similarity index 100% rename from unit/content/unittest_jsondata.cpp rename to aplcore/unit/content/unittest_jsondata.cpp diff --git a/aplcore/unit/content/unittest_metrics.cpp b/aplcore/unit/content/unittest_metrics.cpp new file mode 100644 index 0000000..1ea1209 --- /dev/null +++ b/aplcore/unit/content/unittest_metrics.cpp @@ -0,0 +1,57 @@ +/* + * Copyright 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 "../testeventloop.h" + +using namespace apl; + +class MetricsTest : public MemoryWrapper {}; + +TEST_F(MetricsTest, Basic) { + auto m = Metrics() + .theme("floppy") + .size(300, 400) + .autoSizeWidth(true) + .autoSizeHeight(false) + .dpi(320) + .shape(ScreenShape::ROUND) + .mode(ViewportMode::kViewportModePC); + + ASSERT_EQ("floppy", m.getTheme()); + ASSERT_EQ(200, m.getHeight()); // Scaling factor of 160/320 + ASSERT_EQ(150, m.getWidth()); // Scaling factor of 160/320 + ASSERT_TRUE(m.getAutoWidth()); + ASSERT_FALSE(m.getAutoHeight()); + ASSERT_EQ(320, m.getDpi()); + ASSERT_EQ(ScreenShape::ROUND, m.getScreenShape()); + ASSERT_EQ(ViewportMode::kViewportModePC, m.getViewportMode()); + + ASSERT_EQ(200, m.dpToPx(100)); + ASSERT_EQ(100, m.pxToDp(200)); + ASSERT_EQ("round", m.getShape()); + ASSERT_EQ("pc", m.getMode()); + + // Check the debug format + auto s = m.toDebugString(); + const std::regex check_regex("Metrics<.*>$"); + std::smatch match; + ASSERT_TRUE(std::regex_match(s, match, check_regex)); + + for (const auto& test : { "theme=floppy", "size=300x400", "autoSizeWidth=true", + "autoSizeHeight=false", "dpi=320", "shape=round", "mode=pc"}) { + ASSERT_TRUE(s.find(test) != std::string::npos) << test; + } +} \ No newline at end of file diff --git a/unit/content/unittest_rootconfig.cpp b/aplcore/unit/content/unittest_rootconfig.cpp similarity index 67% rename from unit/content/unittest_rootconfig.cpp rename to aplcore/unit/content/unittest_rootconfig.cpp index 76d8d43..475e079 100644 --- a/unit/content/unittest_rootconfig.cpp +++ b/aplcore/unit/content/unittest_rootconfig.cpp @@ -30,6 +30,10 @@ TEST(RootConfigTest, CustomEnvironmentProperties) rootConfig.setEnvironmentValue("string", "all your base"); ASSERT_EQ("all your base", rootConfig.getEnvironmentValues().at("string").asString()); + + // Wrong env property. + rootConfig.setEnvironmentValue("environment", "oops"); + ASSERT_TRUE(rootConfig.getEnvironmentValues().find("environment") == rootConfig.getEnvironmentValues().end()); } TEST(RootConfigTest, CannotShadowExistingNames) @@ -37,12 +41,12 @@ TEST(RootConfigTest, CannotShadowExistingNames) RootConfig rootConfig; rootConfig.setEnvironmentValue("rotated", true) // synthesized ConfigurationChange property - .setEnvironmentValue("environment", {}) // top-level name - .setEnvironmentValue("viewport", {}) // top-level name - .setEnvironmentValue("agentName", "tests") // part of default env - .setEnvironmentValue("width", 42) // part of default viewport - .setEnvironmentValue("height", 42) // part of default viewport - .setEnvironmentValue("theme", "night"); // part of default viewport + .setEnvironmentValue("environment", {}) // top-level name + .setEnvironmentValue("viewport", {}) // top-level name + .setEnvironmentValue("agentName", "tests") // part of default env + .setEnvironmentValue("width", 42) // part of default viewport + .setEnvironmentValue("height", 42) // part of default viewport + .setEnvironmentValue("theme", "night"); // part of default viewport // Check that all invalid names have been rejected, so the environment still appears empty ASSERT_TRUE(rootConfig.getEnvironmentValues().empty()); diff --git a/unit/datagrammar/CMakeLists.txt b/aplcore/unit/datagrammar/CMakeLists.txt similarity index 100% rename from unit/datagrammar/CMakeLists.txt rename to aplcore/unit/datagrammar/CMakeLists.txt diff --git a/unit/datagrammar/unittest_arithmetic.cpp b/aplcore/unit/datagrammar/unittest_arithmetic.cpp similarity index 100% rename from unit/datagrammar/unittest_arithmetic.cpp rename to aplcore/unit/datagrammar/unittest_arithmetic.cpp diff --git a/aplcore/unit/datagrammar/unittest_decompile.cpp b/aplcore/unit/datagrammar/unittest_decompile.cpp new file mode 100644 index 0000000..f87b657 --- /dev/null +++ b/aplcore/unit/datagrammar/unittest_decompile.cpp @@ -0,0 +1,145 @@ +/** + * Copyright 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 "../testeventloop.h" +#include "apl/datagrammar/bytecode.h" +#include "apl/datagrammar/bytecodeassembler.h" + +using namespace apl; + +class DecompileTest : public ::testing::Test { +public: + DecompileTest() { + context = Context::createTestContext(Metrics(), makeDefaultSession()); + context->putConstant("FixedArray", ObjectArray{10,20,30}); + context->putUserWriteable("TestArray", testArray); + context->putUserWriteable("TestMap", testMap); + } + + ContextPtr context; + ObjectArrayPtr testArray = std::make_shared(ObjectArray{1,2,3}); + ObjectMapPtr testMap = std::make_shared(ObjectMap{{"a", 1}, {"b", 2}}); +}; + +// Static method for splitting strings on whitespace +static std::vector +splitStringWS(const std::string& text, int maxCount=3) +{ + std::vector lines; + auto it = text.begin(); + while (it != text.end()) { + it = std::find_if_not(it, text.end(), isspace); + if (it != text.end()) { + auto it2 = std::find_if(it, text.end(), isspace); + lines.emplace_back(std::string(it, it2)); + it = it2; + if (lines.size() == maxCount) + return lines; + } + } + + return lines; +} + +::testing::AssertionResult +CheckByteCode(const std::vector& lines, const std::shared_ptr& bc) +{ + auto it = bc->disassemble().begin(); + if (*it != "DATA") + return ::testing::AssertionFailure() << "Missing DATA"; + + ++it; + size_t index = 0; + bool found_instructions = false; + + while (it != bc->disassemble().end()) { + if (*it == "INSTRUCTIONS") { + if (found_instructions) + return ::testing::AssertionFailure() << "Double INSTRUCTIONS!"; + + found_instructions = true; + } + else { + if (index >= lines.size()) + return ::testing::AssertionFailure() << "Out of bounds, index=" << index; + auto expected = splitStringWS(lines.at(index)); + auto actual = splitStringWS(*it); + auto result = IsEqual(expected, actual); + if (!result) + return result << " actual='" << *it << "' index=" << index; + ++index; + } + ++it; + } + + if (!found_instructions) + return ::testing::AssertionFailure() << "Missing INSTRUCTIONS"; + + return ::testing::AssertionSuccess(); +} + +struct DecompileTestCase { + std::string expression; + std::vector instructions; +}; + +static const auto DECOMPILE_TEST_CASES = std::vector{ + {"${}", {"0 LOAD_CONSTANT (3) empty_string"}}, + {"${3}", {"0 LOAD_IMMEDIATE (3) "}}, + {"${'foo'}", {"0 'foo'", "0 LOAD_DATA (0) ['foo'] "}}, + {"${1 < 2}", {"0 LOAD_IMMEDIATE (1)", "1 LOAD_IMMEDIATE (2)", "2 COMPARE_OP (0) <"}}, + {"${true ? 2 : 3}", + {"0 LOAD_CONSTANT (2) true", "1 POP_JUMP_IF_FALSE (2) GOTO 4", "2 LOAD_IMMEDIATE (2)", + "3 JUMP (1) GOTO 5", "4 LOAD_IMMEDIATE (3)"}}, + {"${Math.min(1,2)}", + {"0 BuiltInMap<>", "1 'min'", "0 LOAD_DATA (0) [BuiltInMap<>]", + "1 ATTRIBUTE_ACCESS (1) ['min']", "2 LOAD_IMMEDIATE (1)", "3 LOAD_IMMEDIATE (2)", + "4 CALL_FUNCTION (2) argument_count=2"}}, + {"${FixedArray[2]}", + {"0 BuiltInArray<>", "0 LOAD_DATA (0) [BuildInArray<>]", "1 LOAD_IMMEDIATE (2)", + "2 ARRAY_ACCESS (0)"}}, + {"${TestArray[2]}", + {"0 BoundSymbol", "0 LOAD_BOUND_SYMBOL (0) [BoundSymbol]", + "1 LOAD_IMMEDIATE (2)", "2 ARRAY_ACCESS (0)"}}, + {"${TestMap['a']}", + {"0 BoundSymbol", "1 'a'", "0 LOAD_BOUND_SYMBOL (0) [BoundSymbol]", + "1 LOAD_DATA (1) ['a']", "2 ARRAY_ACCESS (0)"}}, +}; + +TEST_F(DecompileTest, Basic) +{ + for (const auto& m : DECOMPILE_TEST_CASES) { + auto v = datagrammar::ByteCodeAssembler::parse(*context, m.expression); + ASSERT_TRUE(v.isEvaluable()); + auto bc = v.get(); + ASSERT_TRUE(CheckByteCode(m.instructions, bc)) << "Test case '" << m.expression << "'"; + } +} + +/* + * Ensure iterator-related methods work. + */ +TEST_F(DecompileTest, Iterator) +{ + auto result = parseAndEvaluate(*context, "${TestArray[0]}"); + ASSERT_TRUE(IsEqual(result.value, 1)); + ASSERT_EQ(1, result.symbols.size()); + ASSERT_TRUE(result.expression.is()); + auto disassembly = result.expression.get()->disassemble(); + ASSERT_FALSE(disassembly.begin() == disassembly.end()); + ASSERT_TRUE(disassembly.begin() != disassembly.end()); + // See the example from the Basic test for the expected disassembly values + ASSERT_EQ(std::distance(disassembly.begin(), disassembly.end()), 6); +} \ No newline at end of file diff --git a/unit/datagrammar/unittest_grammar.cpp b/aplcore/unit/datagrammar/unittest_grammar.cpp similarity index 96% rename from unit/datagrammar/unittest_grammar.cpp rename to aplcore/unit/datagrammar/unittest_grammar.cpp index d1fb885..3f52f20 100644 --- a/unit/datagrammar/unittest_grammar.cpp +++ b/aplcore/unit/datagrammar/unittest_grammar.cpp @@ -16,20 +16,17 @@ #include "../testeventloop.h" #include -#include -#include #include "gtest/gtest.h" #include "apl/animation/easing.h" #include "apl/content/content.h" #include "apl/content/metrics.h" -#include "apl/datagrammar/bytecode.h" #include "apl/engine/context.h" #include "apl/engine/evaluate.h" #include "apl/engine/rootcontext.h" +#include "apl/primitives/boundsymbolset.h" #include "apl/primitives/functions.h" -#include "apl/primitives/symbolreferencemap.h" using namespace apl; @@ -84,8 +81,8 @@ class GrammarTest : public ::testing::Test { loadDocument(doc, 1024, 800); } - Object - eval(const char *expression, int width, int height, int dpi, bool optimize = false) + static Object + eval(const char *expression, int width=1024, int height=800, int dpi=160) { auto m = Metrics().size(width, height).dpi(dpi); auto ctx = Context::createTestContext(m, RootConfig()); @@ -95,30 +92,9 @@ class GrammarTest : public ::testing::Test { person.AddMember("pet", rapidjson::Value("Cat").Move(), person.GetAllocator()); ctx->putConstant("person", person); - if(optimize) { - auto result = getDataBinding(*ctx, std::string(expression)); - if (result.isEvaluable()) { - SymbolReferenceMap symbols; - result.symbols(symbols); - return result.eval(); - } - } - return evaluate(*ctx, expression); } - Object - eval(const char *expression, int width, int height, bool optimize = false) - { - return eval(expression, width, height, 160, optimize); - } - - Object - eval(const char *expression, bool optimize = false) - { - return eval(expression, 1024, 800, optimize); - } - RootContextPtr root; ContextPtr c; }; @@ -253,8 +229,7 @@ TEST_F(GrammarTest, Comparison) TEST_F(GrammarTest, NaNComparison) { - auto m = Metrics().size(1024,800); - auto context = Context::createTestContext(m, RootConfig()); + auto context = Context::createTestContext(Metrics().size(1024,800), RootConfig()); std::vector> NaNComparisons{ {"${(0/0) < 0}", false}, @@ -1202,7 +1177,7 @@ TEST_F(GrammarTest, LocaleMethodsDefault) { // Inflate the document auto metrics = Metrics().size(800,800).dpi(320); RootConfig rootConfig = RootConfig(); - auto root = RootContext::create( metrics, content, rootConfig ); + auto root = std::static_pointer_cast(RootContext::create( metrics, content, rootConfig )); ASSERT_TRUE(root); // Check toLower integration @@ -1227,7 +1202,7 @@ TEST_F(GrammarTest, LocaleMethodsIntegration) { auto metrics = Metrics().size(800,800).dpi(320); auto dummyMethods = std::make_shared(); RootConfig rootConfig = RootConfig().localeMethods(dummyMethods); - auto root = RootContext::create( metrics, content, rootConfig ); + auto root = std::static_pointer_cast(RootContext::create( metrics, content, rootConfig)); ASSERT_TRUE(root); // Check toLower integration @@ -1262,3 +1237,34 @@ TEST_F(GrammarTest, InlineObject) ASSERT_TRUE(IsEqual(m.second, result)) << m.first << ":" << m.second; } } + +// Each evaluation step should return the next object in the array +static const auto INLINE_DELAY_TESTS = std::vector> { + {"#{}", "${}", ""}, + {"#{'#{}'}", "${'#{}'}", "${}", ""}, + {"A_#{1}", "A_${1}", "A_1"}, + {"B_#{1+2}", "B_${1+2}", "B_3"}, + {"C_#{}", "C_${}", "C_"}, + {"D_${'${1+3}'}", "D_4" }, + {"E_${'#{1+3}'}", "E_${1+3}", "E_4" }, + {"F_#{'#{1+3}'}", "F_${'#{1+3}'}", "F_${1+3}", "F_4" }, + {"G_#{'H_#{1+3}' + 'I_#{2+4}'}", "G_${'H_#{1+3}' + 'I_#{2+4}'}", "G_H_${1+3}I_${2+4}", "G_H_4I_6"}, + {"I_${'${1+3}:#{1+2}'}", "I_4:${1+2}", "I_4:3"}, + {"J_#{2+3} ${eval(2+3)}", "J_${2+3} 5", "J_5 5"}, + {"K_${eval('#{2+3}')}", "K_5"}, + {"L_#{eval('#{eval(2+3)}')}", "L_${eval('#{eval(2+3)}')}", "L_5"}, +}; + +TEST_F(GrammarTest, Delayed) +{ + auto c = Context::createTestContext(Metrics(), RootConfig()); + + for (const auto& m : INLINE_DELAY_TESTS) { + for (size_t i = 0 ; i < m.size() - 1 ; i++) { + auto result = evaluate(*c, m.at(i)); + ASSERT_TRUE(IsEqual(m.at(i + 1), result)) + << " index=" << i << " start=" << m.at(i) << " evaluated=" << result + << " expected=" << m.at(i + 1); + } + } +} \ No newline at end of file diff --git a/unit/datagrammar/unittest_grammar_error.cpp b/aplcore/unit/datagrammar/unittest_grammar_error.cpp similarity index 91% rename from unit/datagrammar/unittest_grammar_error.cpp rename to aplcore/unit/datagrammar/unittest_grammar_error.cpp index 03cac00..9857956 100644 --- a/unit/datagrammar/unittest_grammar_error.cpp +++ b/aplcore/unit/datagrammar/unittest_grammar_error.cpp @@ -30,11 +30,6 @@ class GrammarErrorTest : public ::testing::Test { return session->checkAndClear(msg); } - Object eval(const std::string& expression) { - auto result = parseDataBinding(*context, expression); - return result; - } - ContextPtr context; std::shared_ptr session; }; @@ -72,13 +67,15 @@ auto TEST_CASES = std::vector>{ {"${ {x:2} }", MALFORMED_MAP}, {"${ x[ }", UNEXPECTED_TOKEN}, {"${ x[] }", UNEXPECTED_TOKEN}, - {"${ x. }", UNEXPECTED_TOKEN} + {"${ x. }", UNEXPECTED_TOKEN}, + {"#{ 1 ", UNEXPECTED_TOKEN}, + {"#{ 1 + ", EXPECTED_OPERAND_AFTER_ADDITIVE}, }; TEST_F(GrammarErrorTest, Tests) { for (const auto& m : TEST_CASES) { auto expected = errorToString(m.second); - ASSERT_TRUE(IsEqual(m.first, eval(m.first))); // Failed evaluation returns the original string + ASSERT_TRUE(IsEqual(m.first, evaluate(*context, m.first))); // Failed evaluation returns the original string ASSERT_TRUE(ConsoleMessage(expected)) << m.first << ":" << expected; } } \ No newline at end of file diff --git a/aplcore/unit/datagrammar/unittest_optimize.cpp b/aplcore/unit/datagrammar/unittest_optimize.cpp new file mode 100644 index 0000000..46c2045 --- /dev/null +++ b/aplcore/unit/datagrammar/unittest_optimize.cpp @@ -0,0 +1,191 @@ +/** + * Copyright 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 "../testeventloop.h" +#include "apl/datagrammar/bytecode.h" +#include "apl/primitives/boundsymbolset.h" + +using namespace apl; + +class OptimizeTest : public ::testing::Test { +public: + OptimizeTest() { + Metrics m; + context = Context::createTestContext(m, makeDefaultSession()); + } + + ContextPtr context; +}; + +static std::vector> BASIC = { + {"${1+2+a}", 4}, + {"${a || b}", 1}, + {"${false || a}", 1}, + {"${b || 100 || a}", 100}, + {"${a && b}", 0}, + {"${c[0]}", 1}, + {"${d.y}", "foobar"}, + {"${d.x}", 1}, + {"${c[0] - d.x}", 0}, + {"${c[0] - d.x ? d['y'] : d['z'][0]}", -1}, + {"${Math.min( a, b, c.length, d.x, d.z[0] ) }", -1}, + {"${Math.max( a , b , c.length , d.x , d.z[3-3] ) }", 3}, + {"${+2+a}", 3}, + {"${!(aputUserWriteable("a", 1); + context->putUserWriteable("b", 0); + + auto array = JsonData("[1,2,3]"); + ASSERT_TRUE(array); + context->putUserWriteable("c", array.get()); + + auto map = JsonData(R"({"x": 1, "y": "foobar", "z": [-1, 0, false]})"); + ASSERT_TRUE(map); + context->putUserWriteable("d", map.get()); + + for (const auto& m : BASIC) { + auto result = parseAndEvaluate(*context, m.first, false); + ASSERT_TRUE(IsEqual(m.second, result.value)) << m.first; + ASSERT_TRUE(result.expression.isEvaluable()) << m.first; + ASSERT_FALSE(result.expression.get()->isOptimized()) << m.first; + + result = parseAndEvaluate(*context, m.first, true); + ASSERT_TRUE(IsEqual(m.second, result.value)) << m.first; + ASSERT_TRUE(result.expression.isEvaluable()) << m.first; + ASSERT_TRUE(result.expression.get()->isOptimized()) << m.first; + } +} + +static std::vector> MERGE_STRINGS = { + {"This value is ${23}", "This value is 23"}, + {"${1+1} is the value", "2 is the value"}, + {"Where are ${1+1} tigers?", "Where are 2 tigers?"}, + {"A ${null ?? 'friendly'} tiger is not ${3-1} easy ${4/2} find", "A friendly tiger is not 2 easy 2 find"} +}; + +TEST_F(OptimizeTest, MergeStrings) +{ + context->putUserWriteable("a", 23); + + for (const auto& m : MERGE_STRINGS) { + auto result = parseAndEvaluate(*context, m.first, true); + ASSERT_TRUE(IsEqual(m.second, result.value)) << m.first; + ASSERT_EQ(result.symbols.size(), 0) << m.first; + ASSERT_TRUE(result.expression.isEvaluable()) << m.first; + } +} + + +TEST_F(OptimizeTest, DeadCodeRemoval) +{ + context->putUserWriteable("a", 23); + auto result = parseAndEvaluate(*context, "${a?(1!=2? 10:3):4}"); + ASSERT_TRUE(IsEqual(result.value, 10)); + ASSERT_TRUE(result.expression.is()); + ASSERT_TRUE(IsEqual(10, result.expression.eval())); + + context->userUpdateAndRecalculate("a", 0, false); + ASSERT_TRUE(IsEqual(4, result.expression.eval())); + + context->userUpdateAndRecalculate("a", 23, false); + ASSERT_TRUE(IsEqual(10, result.expression.eval())); +} + +TEST_F(OptimizeTest, RemoveDuplicateOperands) +{ + context->putUserWriteable("a", 10); + + BoundSymbolSet expected; + expected.emplace({context, "a"}); + + // An un-optimized expression has duplicated references + auto result = parseAndEvaluate(*context, "${a+a+a}", false); + ASSERT_TRUE(IsEqual(30, result.value)); // Correct final value + ASSERT_EQ(result.symbols, expected); // Correct set of symbols + ASSERT_TRUE(result.expression.is()); + auto bc = result.expression.get(); + ASSERT_EQ(3, bc->dataCount()); // But three copies of the "a" symbol + for (int i = 0 ; i < 3 ; i++) + ASSERT_TRUE(IsEqual(BoundSymbol{context, "a"}, bc->dataAt(i))) << i; + + // An optimized expression has no duplicated references + result = parseAndEvaluate(*context, "${a+a+a}", true); + ASSERT_TRUE(IsEqual(30, result.value)); // Correct final value + ASSERT_EQ(result.symbols, expected); // Correct set of symbols + ASSERT_TRUE(result.expression.is()); + bc = result.expression.get(); + ASSERT_EQ(1, bc->dataCount()); // Just a single copy + ASSERT_TRUE(IsEqual(BoundSymbol{context, "a"}, bc->dataAt(0))); +} + +TEST_F(OptimizeTest, RemoveDuplicateOperands2) +{ + context->putUserWriteable("a", 10); + context->putUserWriteable("b", 7); + + BoundSymbolSet expected; + expected.emplace({context, "a"}); + expected.emplace({context, "b"}); + + // An un-optimized expression has duplicated references + auto result = parseAndEvaluate(*context, "${b+a+b+a}", false); + ASSERT_TRUE(IsEqual(34, result.value)); // Correct final value + ASSERT_EQ(result.symbols, expected); // Correct set of symbols + ASSERT_TRUE(result.expression.is()); + auto bc = result.expression.get(); + ASSERT_EQ(4, bc->dataCount()); // But four copies of the symbols + for (int i = 0 ; i < 4 ; i++) + ASSERT_TRUE(IsEqual(BoundSymbol{context, i % 2 == 0 ? "b" : "a"}, bc->dataAt(i))) << i; + + // An optimized expression has no duplicated references + result = parseAndEvaluate(*context, "${b+a+b+a}", true); + ASSERT_TRUE(IsEqual(34, result.value)); // Correct final value + ASSERT_EQ(result.symbols, expected); // Correct set of symbols + ASSERT_TRUE(result.expression.is()); + bc = result.expression.get(); + ASSERT_EQ(2, bc->dataCount()); // Just two symbols + ASSERT_TRUE(IsEqual(BoundSymbol{context, "b"}, bc->dataAt(0))); + ASSERT_TRUE(IsEqual(BoundSymbol{context, "a"}, bc->dataAt(1))); +} + +TEST_F(OptimizeTest, ShrinkCode) +{ + context->putUserWriteable("a", 10); + + auto result = parseAndEvaluate(*context, "${false ? a : 10}", false); + ASSERT_TRUE(IsEqual(10, result.value)); + ASSERT_EQ(0, result.symbols.size()); + ASSERT_TRUE(result.expression.is()); + auto bc = result.expression.get(); + ASSERT_EQ(1, bc->dataCount()); // There's a reference to "a" in the byte code + auto unoptimized_length = bc->instructionCount(); + + result = parseAndEvaluate(*context, "${false ? a : 10}", true); + ASSERT_TRUE(IsEqual(10, result.value)); + ASSERT_EQ(0, result.symbols.size()); + ASSERT_TRUE(result.expression.is()); + bc = result.expression.get(); + ASSERT_EQ(0, bc->dataCount()); + auto optimized_length = bc->instructionCount(); + + ASSERT_TRUE( optimized_length < unoptimized_length ); +} \ No newline at end of file diff --git a/unit/datagrammar/unittest_parse.cpp b/aplcore/unit/datagrammar/unittest_parse.cpp similarity index 74% rename from unit/datagrammar/unittest_parse.cpp rename to aplcore/unit/datagrammar/unittest_parse.cpp index 052363a..e2a4586 100644 --- a/unit/datagrammar/unittest_parse.cpp +++ b/aplcore/unit/datagrammar/unittest_parse.cpp @@ -17,8 +17,6 @@ #include -#include "apl/primitives/symbolreferencemap.h" - using namespace apl; class ParseTest : public ::testing::Test { @@ -32,41 +30,61 @@ class ParseTest : public ::testing::Test { std::shared_ptr session; }; - TEST_F(ParseTest, Simple) { - auto foo = parseDataBinding(*context, "${}"); - ASSERT_TRUE(foo.isString()); - - foo = parseDataBinding(*context, " ${}"); - ASSERT_TRUE(foo.isString()); - ASSERT_STREQ(" ", foo.asString().c_str()); - - foo = parseDataBinding(*context, "${1+3}"); - ASSERT_TRUE(foo.isNumber()); - ASSERT_EQ(4, foo.asNumber()); - - foo = parseDataBinding(*context, "${Math.min(23,4)}"); - ASSERT_TRUE(foo.isNumber()); - ASSERT_EQ(4, foo.asNumber()); - - foo = parseDataBinding(*context, "${@red}"); - ASSERT_FALSE(foo.isEvaluable()); - ASSERT_TRUE(foo.isNull()); + auto result = parseAndEvaluate(*context, "${}"); + ASSERT_TRUE(result.value.isString()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_TRUE(IsEqual(result.value, "")); + ASSERT_EQ(0, result.symbols.size()); + + result = parseAndEvaluate(*context, " ${}"); + ASSERT_TRUE(result.value.isString()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_TRUE(IsEqual(result.value, " ")); + ASSERT_EQ(0, result.symbols.size()); + + + result = parseAndEvaluate(*context, "${1+3}"); + ASSERT_TRUE(result.value.isNumber()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_TRUE(IsEqual(result.value, 4)); + ASSERT_EQ(0, result.symbols.size()); + + + result = parseAndEvaluate(*context, "${Math.min(23,4)}"); + ASSERT_TRUE(result.value.isNumber()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_TRUE(IsEqual(result.value, 4)); + ASSERT_EQ(0, result.symbols.size()); + + + result = parseAndEvaluate(*context, "${@red}"); + ASSERT_TRUE(result.value.isNull()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(0, result.symbols.size()); context->putConstant("@red", Color(Color::RED)); - foo = parseDataBinding(*context, "${@red}"); - ASSERT_FALSE(foo.isEvaluable()); - ASSERT_TRUE(foo.is()); - ASSERT_TRUE(IsEqual(Color(Color::RED), foo)); + + result = parseAndEvaluate(*context, "${@red}"); + ASSERT_TRUE(result.value.is()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_TRUE(IsEqual(result.value, Color(Color::RED))); + ASSERT_EQ(0, result.symbols.size()); context->putUserWriteable("b", 82); - foo = parseDataBinding(*context, "${Math.max(23,44,b)}"); - ASSERT_TRUE(foo.isEvaluable()); - - foo = foo.eval(); - ASSERT_TRUE(foo.isNumber()); - ASSERT_EQ(82, foo.asNumber()); + + result = parseAndEvaluate(*context, "${Math.max(23,44,b)}"); + ASSERT_TRUE(result.value.isNumber()); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_TRUE(IsEqual(result.value, 82)); + ASSERT_EQ(1, result.symbols.size()); + + context->userUpdateAndRecalculate("b", 100, false); + auto applied = applyDataBinding(*context, result.expression, [](const Context&, const Object& obj) {return obj;}); + ASSERT_TRUE(applied.value.isNumber()); + ASSERT_TRUE(IsEqual(applied.value, 100)); + ASSERT_EQ(1, applied.symbols.size()); } TEST_F(ParseTest, DataBindingIgnoresCLocale) @@ -74,38 +92,39 @@ TEST_F(ParseTest, DataBindingIgnoresCLocale) std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); - auto binding = parseDataBinding(*context, " ${12.4}"); + auto binding = evaluate(*context, " ${12.4}"); EXPECT_EQ(12.4, binding.asNumber()); - binding = parseDataBinding(*context, " ${12.4 + 5}"); + binding = evaluate(*context, " ${12.4 + 5}"); EXPECT_EQ(17.4, binding.asNumber()); std::setlocale(LC_NUMERIC, previousLocale.c_str()); } static const std::vector>> SYMBOL_TESTS = { - {"${a+Math.min(b+(c-d),c/d)} ${e-f}", {"a/", "b/", "c/", "d/", "e/", "f/"}}, - {"${a[b].c ? (e || f) : 'foo ${g}'}", {"a/", "b/", "e/", "f/", "g/"}}, - {"${viewport.width > 10000 ? a : b.c}", {"b/c/"}} + {"${a+Math.min(b+(c-d),c/d)} ${e-f}", {"a", "b", "c", "d", "e", "f"}}, + {"${a[b].c ? (e || f) : 'foo ${g}'}", {"a", "b", "g"}}, + {"${!a[b].c ? (e || f) : 'foo ${g}'}", {"a", "b", "e"}}, + {"${!a[b].c ? (e && f) : 'foo ${g}'}", {"a", "b", "e", "f"}}, + {"${viewport.width > 10000 ? a : b.c}", {"b"}}, }; - TEST_F(ParseTest, Symbols) { for (auto m : "abcdefg") context->putUserWriteable(std::string(1, m), "test_"+std::string(1,m)); for (auto& m : SYMBOL_TESTS) { - auto result = parseDataBinding(*context, m.first); - ASSERT_TRUE(result.isEvaluable()) << m.first; - - SymbolReferenceMap symbols; - result.symbols(symbols); - std::set syms; - for (auto& p : symbols.get()) - syms.insert(p.first); + auto result = parseAndEvaluate(*context, m.first); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(result.symbols.size(), m.second.size()); - ASSERT_EQ(m.second, syms) << m.first; + for (auto& symbolName : m.second) { + ASSERT_TRUE(std::find(result.symbols.begin(), result.symbols.end(), + BoundSymbol{context, symbolName}) != + result.symbols.end()) + << m.first << " symbol " << symbolName; + } } } @@ -130,22 +149,23 @@ static const std::vector UNARY_PLUS_NAN = { TEST_F(ParseTest, UnaryPlus) { for (auto& m : UNARY_PLUS_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } context->putUserWriteable("a", 99); - auto result = parseDataBinding(*context, "${+a}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${+a}"); + ASSERT_TRUE(IsEqual(result.value, 99)); + ASSERT_TRUE(result.expression.isEvaluable()); for (auto& m : UNARY_PLUS_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.first, false)); - ASSERT_TRUE(IsEqual(m.second, result.eval())) << m.first; + ASSERT_TRUE(IsEqual(m.second, result.expression.eval())) << m.first; } for (auto& m : UNARY_PLUS_NAN) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m, false)); - ASSERT_TRUE(result.eval().isNaN()) << m; + ASSERT_TRUE(result.expression.eval().isNaN()) << m; } } @@ -171,22 +191,23 @@ static const std::vector UNARY_MINUS_NAN = { TEST_F(ParseTest, UnaryMinus) { for (auto& m : UNARY_MINUS_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } context->putUserWriteable("a", 99); - auto result = parseDataBinding(*context, "${-a}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${-a}"); + ASSERT_TRUE(IsEqual(result.value, -99)); + ASSERT_TRUE(result.expression.isEvaluable()); for (auto& m : UNARY_MINUS_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.first, false)); - ASSERT_TRUE(IsEqual(m.second, result.eval())) << m.first; + ASSERT_TRUE(IsEqual(m.second, result.expression.eval())) << m.first; } for (auto& m : UNARY_MINUS_NAN) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m, false)); - ASSERT_TRUE(result.eval().isNaN()) << m; + ASSERT_TRUE(result.expression.eval().isNaN()) << m; } } @@ -218,17 +239,18 @@ static const std::vector> UNARY_NOT_EVAL = { TEST_F(ParseTest, UnaryNot) { for (auto& m : UNARY_NOT_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } context->putUserWriteable("a", 99); - auto result = parseDataBinding(*context, "${!a}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${!a}"); + ASSERT_TRUE(IsEqual(result.value, false)); + ASSERT_TRUE(result.expression.isEvaluable()); for (auto& m : UNARY_NOT_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.first, false)); - ASSERT_TRUE(IsEqual(m.second, result.eval())) << m.first; + ASSERT_TRUE(IsEqual(m.second, result.expression.eval())) << m.first; } } @@ -264,23 +286,25 @@ static const std::vector> MULTIPLY_EVAL = { TEST_F(ParseTest, Multiply) { for (auto& m : MULTIPLY_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } for (auto& m : MULTIPLY_NAN_TESTS) { - ASSERT_TRUE(parseDataBinding(*context, m).isNaN()) << m; + ASSERT_TRUE(evaluate(*context, m).isNaN()) << m; } context->putUserWriteable("a", 99); context->putUserWriteable("b", 99); - auto result = parseDataBinding(*context, "${a*b}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${a*b}"); + ASSERT_TRUE(IsEqual(result.value, 99*99)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : MULTIPLY_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(IsEqual(m.at(2), result.eval())) << m.at(1) << "*" << m.at(2); + ASSERT_TRUE(IsEqual(m.at(2), result.expression.eval())) << m.at(1) << "*" << m.at(2); } } @@ -314,29 +338,31 @@ static const std::vector> DIVIDE_NAN_EVAL = { TEST_F(ParseTest, Divide) { for (auto& m : DIVIDE_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } for (auto& m : DIVIDE_NAN_TESTS) { - ASSERT_TRUE(parseDataBinding(*context, m).isNaN()) << m; + ASSERT_TRUE(evaluate(*context, m).isNaN()) << m; } context->putUserWriteable("a", 99); context->putUserWriteable("b", 99); - auto result = parseDataBinding(*context, "${a/b}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${a/b}"); + ASSERT_TRUE(IsEqual(result.value, 1.0)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : DIVIDE_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(IsEqual(m.at(2), result.eval())) << m.at(1) << "/" << m.at(2); + ASSERT_TRUE(IsEqual(m.at(2), result.expression.eval())) << m.at(1) << "/" << m.at(2); } for (auto& m : DIVIDE_NAN_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(result.eval().isNaN()) << m.at(1) << "/" << m.at(2); + ASSERT_TRUE(result.expression.eval().isNaN()) << m.at(1) << "/" << m.at(2); } } @@ -371,29 +397,31 @@ static const std::vector> REMAINDER_NAN_EVAL = { TEST_F(ParseTest, Remainder) { for (auto& m : REMAINDER_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } for (auto& m : REMAINDER_NAN_TESTS) { - ASSERT_TRUE(parseDataBinding(*context, m).isNaN()) << m; + ASSERT_TRUE(evaluate(*context, m).isNaN()) << m; } context->putUserWriteable("a", 99); context->putUserWriteable("b", 99); - auto result = parseDataBinding(*context, "${a%b}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${a%b}"); + ASSERT_TRUE(IsEqual(result.value, 0)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : REMAINDER_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(IsEqual(m.at(2), result.eval())) << m.at(0) << "%" << m.at(1); + ASSERT_TRUE(IsEqual(m.at(2), result.expression.eval())) << m.at(0) << "%" << m.at(1); } for (auto& m : REMAINDER_NAN_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(result.eval().isNaN()) << m.at(0) << "%" << m.at(1); + ASSERT_TRUE(result.expression.eval().isNaN()) << m.at(0) << "%" << m.at(1); } } @@ -431,23 +459,25 @@ static const std::vector> ADD_EVAL = { TEST_F(ParseTest, Add) { for (auto& m : ADD_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } for (auto& m : ADD_CONCATENTATE) { - ASSERT_TRUE(IsEqual(m.second, parseDataBinding(*context, m.first))) << m.first; + ASSERT_TRUE(IsEqual(m.second, evaluate(*context, m.first))) << m.first; } context->putUserWriteable("a", 99); context->putUserWriteable("b", 99); - auto result = parseDataBinding(*context, "${a+b}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${a+b}"); + ASSERT_TRUE(IsEqual(result.value, 198)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : ADD_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(IsEqual(m.at(2), result.eval())) << m.at(1) << "+" << m.at(2); + ASSERT_TRUE(IsEqual(m.at(2), result.expression.eval())) << m.at(1) << "+" << m.at(2); } } @@ -488,29 +518,31 @@ static const std::vector> SUBTRACT_NAN_EVAL = { TEST_F(ParseTest, Subtract) { for (auto& m : SUBTRACT_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } for (auto& m : SUBTRACT_NAN) { - ASSERT_TRUE(parseDataBinding(*context, m).isNaN()) << m; + ASSERT_TRUE(evaluate(*context, m).isNaN()) << m; } context->putUserWriteable("a", 99); context->putUserWriteable("b", 99); - auto result = parseDataBinding(*context, "${a-b}"); - ASSERT_TRUE(result.isEvaluable()); + auto result = parseAndEvaluate(*context, "${a-b}"); + ASSERT_TRUE(IsEqual(result.value, 0)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : SUBTRACT_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(IsEqual(m.at(2), result.eval())) << m.at(0) << "-" << m.at(1); + ASSERT_TRUE(IsEqual(m.at(2), result.expression.eval())) << m.at(0) << "-" << m.at(1); } for (auto& m : SUBTRACT_NAN_EVAL) { ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); - ASSERT_TRUE(result.eval().isNaN()) << m.at(0) << m.at(1); + ASSERT_TRUE(result.expression.eval().isNaN()) << m.at(0) << m.at(1); } } @@ -583,7 +615,7 @@ static const std::vector> COMPARE_EVAL = { TEST_F(ParseTest, Compare) { for (auto& m : COMPARE_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } @@ -591,75 +623,102 @@ TEST_F(ParseTest, Compare) context->putUserWriteable("b", 99); // Less-than - auto result = parseDataBinding(*context, "${auserUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); bool target = (m.at(2).asInt() == -1); // Must be less-than - ASSERT_TRUE(IsEqual(target, result.eval())) << m.at(0) << "<" << m.at(1); + ASSERT_TRUE(IsEqual(target, result.expression.eval())) << m.at(0) << "<" << m.at(1); } // Greater-than - result = parseDataBinding(*context, "${a>b}"); - ASSERT_TRUE(result.isEvaluable()); + + context->userUpdateAndRecalculate("a", 99, false); + context->userUpdateAndRecalculate("b", 99, false); + result = parseAndEvaluate(*context, "${a>b}"); + ASSERT_TRUE(IsEqual(result.value, false)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : COMPARE_EVAL) { auto c = Context::createFromParent(context); ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); bool target = (m.at(2).asInt() == 1); // Must be less-than - ASSERT_TRUE(IsEqual(target, result.eval())) << m.at(0) << ">" << m.at(1); + ASSERT_TRUE(IsEqual(target, result.expression.eval())) << m.at(0) << ">" << m.at(1); } // Less-equal - result = parseDataBinding(*context, "${a<=b}"); - ASSERT_TRUE(result.isEvaluable()); + + context->userUpdateAndRecalculate("a", 99, false); + context->userUpdateAndRecalculate("b", 99, false); + result = parseAndEvaluate(*context, "${a<=b}"); + ASSERT_TRUE(IsEqual(result.value, true)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : COMPARE_EVAL) { auto c = Context::createFromParent(context); ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); bool target = (m.at(2).asInt() != 1); // Must be equal or less-than - ASSERT_TRUE(IsEqual(target, result.eval())) << m.at(0) << "<=" << m.at(1); + ASSERT_TRUE(IsEqual(target, result.expression.eval())) << m.at(0) << "<=" << m.at(1); } // Greater-equal - result = parseDataBinding(*context, "${a>=b}"); - ASSERT_TRUE(result.isEvaluable()); + + context->userUpdateAndRecalculate("a", 99, false); + context->userUpdateAndRecalculate("b", 99, false); + result = parseAndEvaluate(*context, "${a>=b}"); + ASSERT_TRUE(IsEqual(result.value, true)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : COMPARE_EVAL) { auto c = Context::createFromParent(context); ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); bool target = (m.at(2).asInt() != -1); // Must be equal or greater-than - ASSERT_TRUE(IsEqual(target, result.eval())) << m.at(0) << ">=" << m.at(1); + ASSERT_TRUE(IsEqual(target, result.expression.eval())) << m.at(0) << ">=" << m.at(1); } // Equal - result = parseDataBinding(*context, "${a==b}"); - ASSERT_TRUE(result.isEvaluable()); + + context->userUpdateAndRecalculate("a", 99, false); + context->userUpdateAndRecalculate("b", 99, false); + result = parseAndEvaluate(*context, "${a==b}"); + ASSERT_TRUE(IsEqual(result.value, true)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : COMPARE_EVAL) { auto c = Context::createFromParent(context); ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); bool target = (m.at(2).asInt() == 0); // Must be equal - ASSERT_TRUE(IsEqual(target, result.eval())) << m.at(0) << "==" << m.at(1); + ASSERT_TRUE(IsEqual(target, result.expression.eval())) << m.at(0) << "==" << m.at(1); } // Not-Equal - result = parseDataBinding(*context, "${a!=b}"); - ASSERT_TRUE(result.isEvaluable()); + + context->userUpdateAndRecalculate("a", 99, false); + context->userUpdateAndRecalculate("b", 99, false); + result = parseAndEvaluate(*context, "${a!=b}"); + ASSERT_TRUE(IsEqual(result.value, false)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(2, result.symbols.size()); for (auto& m : COMPARE_EVAL) { auto c = Context::createFromParent(context); ASSERT_TRUE(context->userUpdateAndRecalculate("a", m.at(0), false)); ASSERT_TRUE(context->userUpdateAndRecalculate("b", m.at(1), false)); bool target = (m.at(2).asInt() != 0); // Must be equal - ASSERT_TRUE(IsEqual(target, result.eval())) << m.at(0) << "!=" << m.at(1); + ASSERT_TRUE(IsEqual(target, result.expression.eval())) << m.at(0) << "!=" << m.at(1); } } @@ -707,7 +766,7 @@ static std::vector> AND_OR_NULLC_TESTS = { TEST_F(ParseTest, AndOrNullC) { for (const auto& m : AND_OR_NULLC_TESTS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } } @@ -744,7 +803,7 @@ static std::vector> TERNARY_TEST = { TEST_F(ParseTest, Ternary) { for (const auto& m : TERNARY_TEST) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } } @@ -769,7 +828,7 @@ TEST_F(ParseTest, FieldArrayAccess) context->putConstant("y", map.get()); for (const auto& m : FIELD_ARRAY_ACCESS) { - auto result = parseDataBinding(*context, m.first); + auto result = evaluate(*context, m.first); ASSERT_TRUE(IsEqual(m.second, result)) << m.first; } } diff --git a/unit/datasource/CMakeLists.txt b/aplcore/unit/datasource/CMakeLists.txt similarity index 84% rename from unit/datasource/CMakeLists.txt rename to aplcore/unit/datasource/CMakeLists.txt index e522c77..79fedd0 100644 --- a/unit/datasource/CMakeLists.txt +++ b/aplcore/unit/datasource/CMakeLists.txt @@ -16,6 +16,8 @@ target_sources_local(unittest testdatasourceprovider.cpp unittest_datasource.cpp unittest_datasource_context.cpp - unittest_dynamicindexlist.cpp + unittest_dynamicindexlistembedded.cpp + unittest_dynamicindexlistlazy.cpp + unittest_dynamicindexlistupdate.cpp unittest_dynamictokenlist.cpp ) \ No newline at end of file diff --git a/aplcore/unit/datasource/dynamicindexlisttest.h b/aplcore/unit/datasource/dynamicindexlisttest.h new file mode 100644 index 0000000..9d37f68 --- /dev/null +++ b/aplcore/unit/datasource/dynamicindexlisttest.h @@ -0,0 +1,323 @@ +/** + * Copyright 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 _APL_DYNAMIC_INDEX_LIST_TEST_H +#define _APL_DYNAMIC_INDEX_LIST_TEST_H + +#include "../testeventloop.h" + +#include "apl/apl.h" +#include "apl/dynamicdata.h" + +namespace apl { + +static const char *SOURCE_TYPE = "dynamicIndexList"; +static const char *LIST_ID = "listId"; +static const char *CORRELATION_TOKEN = "correlationToken"; +static const char *START_INDEX = "startIndex"; +static const char *COUNT = "count"; +static const size_t TEST_CHUNK_SIZE = 5; + +class DynamicIndexListTest : public DocumentWrapper { +public: + ::testing::AssertionResult + CheckFetchRequest(const std::string& listId, const std::string& correlationToken, int startIndex, int count) { + bool fetchCalled = root->hasEvent(); + auto event = root->popEvent(); + fetchCalled &= (event.getType() == kEventTypeDataSourceFetchRequest); + + if (!fetchCalled) + return ::testing::AssertionFailure() << "Fetch was not called."; + + auto incomingType = event.getValue(kEventPropertyName).getString(); + if (SOURCE_TYPE != incomingType) + return ::testing::AssertionFailure() + << "DataSource type is wrong. Expected: " << SOURCE_TYPE + << ", actual: " << incomingType; + + auto request = event.getValue(kEventPropertyValue); + + auto incomingListId = request.opt(LIST_ID, ""); + if (incomingListId != listId) + return ::testing::AssertionFailure() + << "listId is wrong. Expected: " << listId + << ", actual: " << incomingListId; + + auto incomingCorrelationToken = request.opt(CORRELATION_TOKEN, ""); + if (incomingCorrelationToken != correlationToken) + return ::testing::AssertionFailure() + << "correlationToken is wrong. Expected: " << correlationToken + << ", actual: " << incomingCorrelationToken; + + auto incomingStartIndex = request.opt(START_INDEX, -1); + if (incomingStartIndex != startIndex) + return ::testing::AssertionFailure() + << "startIndex is wrong. Expected: " << startIndex + << ", actual: " << incomingStartIndex; + + auto incomingCount = request.opt(COUNT, -1); + if (incomingCount != count) + return ::testing::AssertionFailure() + << "count is wrong. Expected: " << count + << ", actual: " << incomingCount; + + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult + CheckChild(size_t idx, int exp) { + auto text = std::to_string(exp); + auto actualText = component->getChildAt(idx)->getCalculated(kPropertyText).asString(); + if (actualText != text) { + return ::testing::AssertionFailure() + << "text " << idx + << " is wrong. Expected: " << text + << ", actual: " << actualText; + } + + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult + CheckChildren(size_t startIdx, std::vector values) { + if (values.size() != component->getChildCount()) { + return ::testing::AssertionFailure() + << "Wrong child number. Expected: " << values.size() + << ", actual: " << component->getChildCount(); + } + int idx = startIdx; + for (int exp : values) { + auto result = CheckChild(idx, exp); + if (!result) { + return result; + } + idx++; + } + + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult + CheckChildren(std::vector values) { + return CheckChildren(0, values); + } + + ::testing::AssertionResult + CheckBounds(int minInclusive, int maxExclusive) { + auto actual = ds->getBounds("vQdpOESlok"); + std::pair expected(minInclusive, maxExclusive); + + if (actual != expected) { + return ::testing::AssertionFailure() + << "bounds is wrong. Expected: (" << expected.first << "," << expected.second + << "), actual: (" << actual.first << "," << actual.second << ")"; + } + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult + CheckErrors(std::vector reasons) { + auto errors = ds->getPendingErrors().getArray(); + + if (errors.size() != reasons.size()) { + return ::testing::AssertionFailure() + << "Number of errors is wrong. Expected: " << reasons.size() + << ", actual: " << errors.size(); + } + + for (int i = 0; i expectedPages) { + if (expectedPages.size() != component->getChildCount()) { + return ::testing::AssertionFailure() << "Expected " << expectedPages.size() << " page(s), " + << "found " << component->getChildCount(); + } + if (expectedCurrentPage != component->getCalculated(kPropertyCurrentPage).asNumber()) { + return ::testing::AssertionFailure() << "Expected the current page to be " + << expectedCurrentPage << " but was " + << component->getCalculated(kPropertyCurrentPage).asNumber(); + } + for (int i = 0; i < expectedPages.size(); i++) { + auto child = component->getChildAt(i); + auto expectedPage = expectedPages[i]; + if (expectedPage.id != child->getId()) { + return ::testing::AssertionFailure() << "Expected page " << i + << " to have an id of " << expectedPage.id + << " but was " << child->getId(); + } + if (expectedPage.isTransforming == CheckTransform(Transform2D(), child)) { + return ::testing::AssertionFailure() << "Expected page " << i << " (id=" + << expectedPage.id << ") to be" + << (expectedPage.isTransforming ? "" : " NOT") + << " transforming, but it was" + << (expectedPage.isTransforming ? " NOT" : ""); + } + } + return ::testing::AssertionSuccess(); + } + + static std::string + createLazyLoad(int listVersion, int correlationToken, int index, const std::string& items ) { + std::string listVersionString = listVersion < 0 ? "" : ("\"listVersion\": " + std::to_string(listVersion) + ","); + std::string ctString = correlationToken < 0 ? "" : ("\"correlationToken\": \"" + std::to_string(correlationToken) + "\","); + return "{" + " \"presentationToken\": \"presentationToken\"," + " \"listId\": \"vQdpOESlok\"," + + listVersionString + ctString + + " \"startIndex\": " + std::to_string(index) + "," + " \"items\": [" + items + "]" + "}"; + } + + static std::string + createInsert(int listVersion, int index, int item ) { + return "{" + " \"presentationToken\": \"presentationToken\"," + " \"listId\": \"vQdpOESlok\"," + " \"listVersion\": " + std::to_string(listVersion) + "," + " \"operations\": [" + " {" + " \"type\": \"InsertItem\"," + " \"index\": " + std::to_string(index) + "," + " \"item\": " + std::to_string(item) + + " }" + " ]" + "}"; + } + + static std::string + createReplace(int listVersion, int index, int item ) { + return "{" + " \"presentationToken\": \"presentationToken\"," + " \"listId\": \"vQdpOESlok\"," + " \"listVersion\": " + std::to_string(listVersion) + "," + " \"operations\": [" + " {" + " \"type\": \"SetItem\"," + " \"index\": " + std::to_string(index) + "," + " \"item\": " + std::to_string(item) + + " }" + " ]" + "}"; + } + + static std::string + createDelete(int listVersion, int index ) { + return "{" + " \"presentationToken\": \"presentationToken\"," + " \"listId\": \"vQdpOESlok\"," + " \"listVersion\": " + std::to_string(listVersion) + "," + " \"operations\": [" + " {" + " \"type\": \"DeleteItem\"," + " \"index\": " + std::to_string(index) + + " }" + " ]" + "}"; + } + + static std::string + createMultiInsert(int listVersion, int index, const std::vector& items ) { + std::string itemsString; + for (size_t i = 0; i(cnf); + config->dataSourceProvider(SOURCE_TYPE, ds); + } + + void TearDown() override { + // Check for unprocessed errors. + auto errors = ds->getPendingErrors(); + for (const auto& error : errors.getArray()) { + LOG(LogLevel::kError) << error; + } + ASSERT_TRUE(errors.empty()); + + // Clean any pending timeouts. Tests will check them explicitly. + if(root) { + loop->advanceToEnd(); + while (root->hasEvent()) { + root->popEvent(); + } + } + DocumentWrapper::TearDown(); + } + + std::shared_ptr ds; +}; + +} // namespace apl + +#endif //_APL_DYNAMIC_INDEX_LIST_TEST_H diff --git a/unit/datasource/testdatasourceprovider.cpp b/aplcore/unit/datasource/testdatasourceprovider.cpp similarity index 100% rename from unit/datasource/testdatasourceprovider.cpp rename to aplcore/unit/datasource/testdatasourceprovider.cpp diff --git a/unit/datasource/testdatasourceprovider.h b/aplcore/unit/datasource/testdatasourceprovider.h similarity index 100% rename from unit/datasource/testdatasourceprovider.h rename to aplcore/unit/datasource/testdatasourceprovider.h diff --git a/unit/datasource/unittest_datasource.cpp b/aplcore/unit/datasource/unittest_datasource.cpp similarity index 60% rename from unit/datasource/unittest_datasource.cpp rename to aplcore/unit/datasource/unittest_datasource.cpp index a426bb3..9ee2f6f 100644 --- a/unit/datasource/unittest_datasource.cpp +++ b/aplcore/unit/datasource/unittest_datasource.cpp @@ -18,6 +18,29 @@ using namespace apl; +static const std::vector ITEMS = { + R"({"color": "#000000", "text": "0"})", + R"({"color": "#010000", "text": "1"})", + R"({"color": "#020000", "text": "2"})", + R"({"color": "#030000", "text": "3"})", + R"({"color": "#040000", "text": "4"})", + R"({"color": "#050000", "text": "5"})", + R"({"color": "#060000", "text": "6"})", + R"({"color": "#070000", "text": "7"})", + R"({"color": "#080000", "text": "8"})", + R"({"color": "#090000", "text": "9"})", + R"({"color": "#0A0000", "text": "10"})", + R"({"color": "#0B0000", "text": "11"})", + R"({"color": "#0C0000", "text": "12"})", + R"({"color": "#0D0000", "text": "13"})", + R"({"color": "#0E0000", "text": "14"})", + R"({"color": "#0F0000", "text": "15"})", + R"({"color": "#100000", "text": "16"})", + R"({"color": "#110000", "text": "17"})", + R"({"color": "#120000", "text": "18"})", + R"({"color": "#130000", "text": "19"})", +}; + class DynamicSourceTest : public DocumentWrapper { protected: ::testing::AssertionResult @@ -37,123 +60,102 @@ class DynamicSourceTest : public DocumentWrapper { CheckChild(size_t idx, const std::string& exp) { return CheckChild(component, idx, exp); } -}; -static const char *DATA = - "{" - " \"dynamicSource\": {" - " \"type\": \"GenericList\"," - " \"listId\": \"vQdpOESlok\"," - " \"offset\": 0," - " \"maxItems\": 20," - " \"items\": [" - " { \"color\": \"#000000\", \"text\": \"0\" }," - " { \"color\": \"#010000\", \"text\": \"1\" }," - " { \"color\": \"#020000\", \"text\": \"2\" }," - " { \"color\": \"#030000\", \"text\": \"3\" }," - " { \"color\": \"#040000\", \"text\": \"4\" }" - " ]" - " }" - "}"; + DynamicSourceTest() { + ds = std::make_shared(ITEMS); + config->dataSourceProvider("GenericList", ds); + } -static const std::vector ITEMS = { - R"({"color": "#000000", "text": "0"})", - R"({"color": "#010000", "text": "1"})", - R"({"color": "#020000", "text": "2"})", - R"({"color": "#030000", "text": "3"})", - R"({"color": "#040000", "text": "4"})", - R"({"color": "#050000", "text": "5"})", - R"({"color": "#060000", "text": "6"})", - R"({"color": "#070000", "text": "7"})", - R"({"color": "#080000", "text": "8"})", - R"({"color": "#090000", "text": "9"})", - R"({"color": "#0A0000", "text": "10"})", - R"({"color": "#0B0000", "text": "11"})", - R"({"color": "#0C0000", "text": "12"})", - R"({"color": "#0D0000", "text": "13"})", - R"({"color": "#0E0000", "text": "14"})", - R"({"color": "#0F0000", "text": "15"})", - R"({"color": "#100000", "text": "16"})", - R"({"color": "#110000", "text": "17"})", - R"({"color": "#120000", "text": "18"})", - R"({"color": "#130000", "text": "19"})", + std::shared_ptr ds; }; -static const char *BASIC = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"theme\": \"dark\"," - " \"layouts\": {" - " \"square\": {" - " \"parameters\": [\"color\", \"text\"]," - " \"item\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100," - " \"id\": \"frame${text}\"," - " \"backgroundColor\": \"${color}\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"${text}\"," - " \"color\": \"black\"," - " \"width\": 100," - " \"height\": 100" - " }" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"dynamicSource\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Sequence\"," - " \"id\": \"sequence\"," - " \"width\": 500," - " \"data\": \"${dynamicSource}\"," - " \"items\": {" - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"${data.text}\"" - " }" - " }," - " {" - " \"type\": \"Pager\"," - " \"id\": \"pager\"," - " \"data\": \"${dynamicSource}\"," - " \"items\": {" - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"${data.text}\"" - " }" - " }," - " {" - " \"type\": \"Container\"," - " \"id\": \"cont\"," - " \"data\": \"${dynamicSource}\"," - " \"items\": {" - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"${data.text}\"" - " }" - " }" - " ]" - " }" - " }" - "}"; +static const char *DATA = R"apl({ + "dynamicSource": { + "type": "GenericList", + "listId": "vQdpOESlok", + "offset": 0, + "maxItems": 20, + "items": [ + { "color": "#000000", "text": "0" }, + { "color": "#010000", "text": "1" }, + { "color": "#020000", "text": "2" }, + { "color": "#030000", "text": "3" }, + { "color": "#040000", "text": "4" } + ] + } +})apl"; + +static const char *BASIC = R"apl({ + "type": "APL", + "version": "1.3", + "theme": "dark", + "layouts": { + "square": { + "parameters": ["color", "text"], + "item": { + "type": "Frame", + "width": 100, + "height": 100, + "id": "frame${text}", + "backgroundColor": "${color}", + "item": { + "type": "Text", + "text": "${text}", + "color": "black", + "width": 100, + "height": 100 + } + } + } + }, + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Container", + "items": [ + { + "type": "Sequence", + "id": "sequence", + "width": 500, + "data": "${dynamicSource}", + "items": { + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + } + }, + { + "type": "Pager", + "id": "pager", + "data": "${dynamicSource}", + "items": { + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + } + }, + { + "type": "Container", + "id": "cont", + "data": "${dynamicSource}", + "items": { + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + } + } + ] + } + } +})apl"; TEST_F(DynamicSourceTest, Basic) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(BASIC, DATA); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -211,22 +213,18 @@ TEST_F(DynamicSourceTest, Basic) ASSERT_EQ("frame9", cont->getChildAt(9)->getId()); } -static const char *DATA_EMPTY = - "{\n" - " \"dynamicSource\": {\n" - " \"type\": \"GenericList\",\n" - " \"listId\": \"vQdpOESlok\",\n" - " \"offset\": 0,\n" - " \"maxItems\": 20,\n" - " \"items\": []\n" - " }\n" - "}"; +static const char *DATA_EMPTY = R"apl({ + "dynamicSource": { + "type": "GenericList", + "listId": "vQdpOESlok", + "offset": 0, + "maxItems": 20, + "items": [] + } +})apl"; TEST_F(DynamicSourceTest, Empty) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(BASIC, DATA_EMPTY); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -291,9 +289,6 @@ TEST_F(DynamicSourceTest, Empty) TEST_F(DynamicSourceTest, EmptyNotAligned) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(BASIC, DATA_EMPTY); ASSERT_FALSE(ds->getConnection()->processResponse(0, 5, 5)); @@ -325,28 +320,24 @@ TEST_F(DynamicSourceTest, EmptyNotAligned) ASSERT_FALSE(ds->getConnection()->processResponse(0, 10, 5)); } -static const char *DATA_BACKWARDS = - "{\n" - " \"dynamicSource\": {\n" - " \"type\": \"GenericList\",\n" - " \"listId\": \"vQdpOESlok\",\n" - " \"offset\": 15,\n" - " \"maxItems\": 20,\n" - " \"items\": [\n" - " { \"color\": \"#0F0000\", \"text\": \"15\" },\n" - " { \"color\": \"#100000\", \"text\": \"16\" },\n" - " { \"color\": \"#110000\", \"text\": \"17\" },\n" - " { \"color\": \"#120000\", \"text\": \"18\" },\n" - " { \"color\": \"#130000\", \"text\": \"19\" }\n" - " ]\n" - " }\n" - "}"; +static const char *DATA_BACKWARDS = R"apl({ + "dynamicSource": { + "type": "GenericList", + "listId": "vQdpOESlok", + "offset": 15, + "maxItems": 20, + "items": [ + { "color": "#0F0000", "text": "15" }, + { "color": "#100000", "text": "16" }, + { "color": "#110000", "text": "17" }, + { "color": "#120000", "text": "18" }, + { "color": "#130000", "text": "19" } + ] + } +})apl"; TEST_F(DynamicSourceTest, Backwards) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(BASIC, DATA_BACKWARDS); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -403,28 +394,25 @@ TEST_F(DynamicSourceTest, Backwards) ASSERT_EQ("frame19", cont->getChildAt(9)->getId()); } -static const char *DATA_OFFSET = - "{" - " \"dynamicSource\": {" - " \"type\": \"GenericList\"," - " \"listId\": \"vQdpOESlok\"," - " \"offset\": 10," - " \"maxItems\": 20," - " \"items\": [" - " { \"color\": \"#0A0000\", \"text\": \"10\" }," - " { \"color\": \"#0B0000\", \"text\": \"11\" }," - " { \"color\": \"#0C0000\", \"text\": \"12\" }," - " { \"color\": \"#0D0000\", \"text\": \"13\" }," - " { \"color\": \"#0E0000\", \"text\": \"14\" }" - " ]" - " }" - "}"; +static const char *DATA_OFFSET = R"apl({ + "dynamicSource": { + "type": "GenericList", + "listId": "vQdpOESlok", + "offset": 10, + "maxItems": 20, + "items": [ + { "color": "#0A0000", "text": "10" }, + { "color": "#0B0000", "text": "11" }, + { "color": "#0C0000", "text": "12" }, + { "color": "#0D0000", "text": "13" }, + { "color": "#0E0000", "text": "14" } + ] + } +})apl"; TEST_F(DynamicSourceTest, Offset) { config->set(RootProperty::kSequenceChildCache, 5); - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); loadDocument(BASIC, DATA_OFFSET); @@ -522,9 +510,6 @@ TEST_F(DynamicSourceTest, Offset) TEST_F(DynamicSourceTest, OffsetSourceInitiated) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(BASIC, DATA_OFFSET); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -590,111 +575,107 @@ TEST_F(DynamicSourceTest, OffsetSourceInitiated) ASSERT_EQ("frame19", page->getChildAt(19)->getId()); } -static const char *CONDITIONAL = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"theme\": \"dark\"," - " \"layouts\": {" - " \"square\": {" - " \"parameters\": [\"color\", \"text\"]," - " \"item\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100," - " \"id\": \"frame${text}\"," - " \"backgroundColor\": \"${color}\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"${text}\"," - " \"id\": \"text${index}\"," - " \"color\": \"black\"," - " \"width\": 100," - " \"height\": 100" - " }" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"dynamicSource\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Sequence\"," - " \"id\": \"sequence\"," - " \"data\": \"${dynamicSource}\"," - " \"items\": [" - " {" - " \"when\": \"${index%3 != 0}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"${data.text}\"" - " }," - " {" - " \"when\": \"${index%3 == 0}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"W ${data.text}\"" - " }" - " ]" - " }," - " {" - " \"type\": \"Pager\"," - " \"id\": \"pager\"," - " \"data\": \"${dynamicSource}\"," - " \"items\": [" - " {" - " \"when\": \"${index%3 != 0}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"${data.text}\"" - " }," - " {" - " \"when\": \"${index%3 == 0}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"W ${data.text}\"" - " }" - " ]" - " }," - " {" - " \"type\": \"Container\"," - " \"id\": \"cont\"," - " \"data\": \"${dynamicSource}\"," - " \"items\": [" - " {" - " \"when\": \"${index%3 != 0}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"${data.text}\"" - " }," - " {" - " \"when\": \"${index%3 == 0}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${data.color}\"," - " \"text\": \"W ${data.text}\"" - " }" - " ]" - " }" - " ]" - " }" - " }" - "}"; +static const char *CONDITIONAL = R"apl({ + "type": "APL", + "version": "1.3", + "theme": "dark", + "layouts": { + "square": { + "parameters": ["color", "text"], + "item": { + "type": "Frame", + "width": 100, + "height": 100, + "id": "frame${text}", + "backgroundColor": "${color}", + "item": { + "type": "Text", + "text": "${text}", + "id": "text${index}", + "color": "black", + "width": 100, + "height": 100 + } + } + } + }, + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Container", + "items": [ + { + "type": "Sequence", + "id": "sequence", + "data": "${dynamicSource}", + "items": [ + { + "when": "${index%3 != 0}", + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + }, + { + "when": "${index%3 == 0}", + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "W ${data.text}" + } + ] + }, + { + "type": "Pager", + "id": "pager", + "data": "${dynamicSource}", + "items": [ + { + "when": "${index%3 != 0}", + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + }, + { + "when": "${index%3 == 0}", + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "W ${data.text}" + } + ] + }, + { + "type": "Container", + "id": "cont", + "data": "${dynamicSource}", + "items": [ + { + "when": "${index%3 != 0}", + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + }, + { + "when": "${index%3 == 0}", + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "W ${data.text}" + } + ] + } + ] + } + } +})apl"; TEST_F(DynamicSourceTest, Conditional) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(CONDITIONAL, DATA); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -760,139 +741,134 @@ TEST_F(DynamicSourceTest, Conditional) ASSERT_EQ("frameW9", cont->getChildAt(9)->getId()); } -static const char *EXPLICIT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"theme\": \"dark\"," - " \"layouts\": {" - " \"square\": {" - " \"parameters\": [\"color\", \"text\"]," - " \"item\": {" - " \"type\": \"Frame\"," - " \"width\": 100," - " \"height\": 100," - " \"id\": \"frame${text}\"," - " \"backgroundColor\": \"${color}\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"${text}\"," - " \"id\": \"text${index}\"," - " \"color\": \"black\"," - " \"width\": 100," - " \"height\": 100" - " }" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"dynamicSource\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Sequence\"," - " \"id\": \"sequence\"," - " \"items\": [" - " {" - " \"when\": \"${dynamicSource[7]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[7].color}\"," - " \"text\": \"1E ${dynamicSource[7].text}\"" - " }," - " {" - " \"when\": \"${dynamicSource[10]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[10].color}\"," - " \"text\": \"2E ${dynamicSource[10].text}\"" - " }" - " ]" - " }," - " {" - " \"type\": \"Pager\"," - " \"id\": \"pager\"," - " \"items\": [" - " {" - " \"when\": \"${dynamicSource[2]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[2].color}\"," - " \"text\": \"1E ${dynamicSource[2].text}\"" - " }," - " {" - " \"when\": \"${dynamicSource[9]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[9].color}\"," - " \"text\": \"2E ${dynamicSource[9].text}\"" - " }" - " ]" - " }," - " {" - " \"type\": \"Container\"," - " \"id\": \"cont\"," - " \"items\": [" - " {" - " \"when\": \"${dynamicSource[2]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[2].color}\"," - " \"text\": \"1E ${dynamicSource[2].text}\"" - " }," - " {" - " \"when\": \"${dynamicSource[4]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[4].color}\"," - " \"text\": \"2E ${dynamicSource[4].text}\"" - " }," - " {" - " \"when\": \"${dynamicSource[9]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[9].color}\"," - " \"text\": \"3E ${dynamicSource[9].text}\"" - " }," - " {" - " \"when\": \"${dynamicSource[10]}\"," - " \"type\": \"square\"," - " \"index\": \"${index}\"," - " \"color\": \"${dynamicSource[10].color}\"," - " \"text\": \"4E ${dynamicSource[10].text}\"" - " }" - " ]" - " }" - " ]" - " }" - " }" - "}"; - -static const char *DATA_EXPLICIT = - "{" - " \"dynamicSource\": {" - " \"type\": \"GenericList\"," - " \"listId\": \"vQdpOESlok\"," - " \"offset\": 5," - " \"maxItems\": 20," - " \"items\": [" - " { \"color\": \"#050000\", \"text\": \"5\" }," - " { \"color\": \"#060000\", \"text\": \"6\" }," - " { \"color\": \"#070000\", \"text\": \"7\" }," - " { \"color\": \"#080000\", \"text\": \"8\" }," - " { \"color\": \"#090000\", \"text\": \"9\" }" - " ]" - " }" - "}"; +static const char *EXPLICIT = R"apl({ + "type": "APL", + "version": "1.3", + "theme": "dark", + "layouts": { + "square": { + "parameters": ["color", "text"], + "item": { + "type": "Frame", + "width": 100, + "height": 100, + "id": "frame${text}", + "backgroundColor": "${color}", + "item": { + "type": "Text", + "text": "${text}", + "id": "text${index}", + "color": "black", + "width": 100, + "height": 100 + } + } + } + }, + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Container", + "items": [ + { + "type": "Sequence", + "id": "sequence", + "items": [ + { + "when": "${dynamicSource[7]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[7].color}", + "text": "1E ${dynamicSource[7].text}" + }, + { + "when": "${dynamicSource[10]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[10].color}", + "text": "2E ${dynamicSource[10].text}" + } + ] + }, + { + "type": "Pager", + "id": "pager", + "items": [ + { + "when": "${dynamicSource[2]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[2].color}", + "text": "1E ${dynamicSource[2].text}" + }, + { + "when": "${dynamicSource[9]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[9].color}", + "text": "2E ${dynamicSource[9].text}" + } + ] + }, + { + "type": "Container", + "id": "cont", + "items": [ + { + "when": "${dynamicSource[2]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[2].color}", + "text": "1E ${dynamicSource[2].text}" + }, + { + "when": "${dynamicSource[4]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[4].color}", + "text": "2E ${dynamicSource[4].text}" + }, + { + "when": "${dynamicSource[9]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[9].color}", + "text": "3E ${dynamicSource[9].text}" + }, + { + "when": "${dynamicSource[10]}", + "type": "square", + "index": "${index}", + "color": "${dynamicSource[10].color}", + "text": "4E ${dynamicSource[10].text}" + } + ] + } + ] + } + } +})apl"; + +static const char *DATA_EXPLICIT = R"apl({ + "dynamicSource": { + "type": "GenericList", + "listId": "vQdpOESlok", + "offset": 5, + "maxItems": 20, + "items": [ + { "color": "#050000", "text": "5" }, + { "color": "#060000", "text": "6" }, + { "color": "#070000", "text": "7" }, + { "color": "#080000", "text": "8" }, + { "color": "#090000", "text": "9" } + ] + } +})apl"; // We assume that explicit references was present in initial array, and referred by EXISTING index, not data source index. TEST_F(DynamicSourceTest, Explicit) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(EXPLICIT, DATA_EXPLICIT); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -917,22 +893,18 @@ TEST_F(DynamicSourceTest, Explicit) ASSERT_FALSE(root->isDirty()); } -static const char *DATA_EMPTY_OFFSET = - "{\n" - " \"dynamicSource\": {\n" - " \"type\": \"GenericList\",\n" - " \"listId\": \"vQdpOESlok\",\n" - " \"offset\": 5,\n" - " \"maxItems\": 20,\n" - " \"items\": []\n" - " }\n" - "}"; +static const char *DATA_EMPTY_OFFSET = R"apl({ + "dynamicSource": { + "type": "GenericList", + "listId": "vQdpOESlok", + "offset": 5, + "maxItems": 20, + "items": [] + } +})apl"; TEST_F(DynamicSourceTest, ExplicitEmpty) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(EXPLICIT, DATA_EMPTY_OFFSET); ASSERT_EQ(kComponentTypeContainer, component->getType()); @@ -954,37 +926,33 @@ TEST_F(DynamicSourceTest, ExplicitEmpty) ASSERT_FALSE(root->isDirty()); } -static const char *SIMPLE_SEQUENCE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"theme\": \"dark\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"dynamicSource\"" - " ]," - " \"item\": {" - " \"type\": \"Sequence\"," - " \"id\": \"sequence\"," - " \"data\": \"${dynamicSource}\"," - " \"height\": 500," - " \"items\": {" - " \"type\": \"Text\"," - " \"id\": \"text${data.text}\"," - " \"text\": \"text${data.text}\"," - " \"color\": \"black\"," - " \"width\": 100," - " \"height\": 100" - " }" - " }" - " }" - "}"; +static const char *SIMPLE_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.3", + "theme": "dark", + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Sequence", + "id": "sequence", + "data": "${dynamicSource}", + "height": 500, + "items": { + "type": "Text", + "id": "text${data.text}", + "text": "text${data.text}", + "color": "black", + "width": 100, + "height": 100 + } + } + } +})apl"; TEST_F(DynamicSourceTest, IncompleteResponse) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1007,9 +975,6 @@ TEST_F(DynamicSourceTest, IncompleteResponse) TEST_F(DynamicSourceTest, BiggerResponse) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1030,9 +995,6 @@ TEST_F(DynamicSourceTest, BiggerResponse) TEST_F(DynamicSourceTest, IntersectResponse) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1055,9 +1017,6 @@ TEST_F(DynamicSourceTest, IntersectResponse) TEST_F(DynamicSourceTest, GapResponse) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1074,9 +1033,6 @@ TEST_F(DynamicSourceTest, GapResponse) TEST_F(DynamicSourceTest, SimpleReplace) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); // We have 5 initial items. @@ -1163,9 +1119,6 @@ TEST_F(DynamicSourceTest, SimpleReplace) TEST_F(DynamicSourceTest, InsertAndReplace) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1235,9 +1188,6 @@ TEST_F(DynamicSourceTest, InsertAndReplace) TEST_F(DynamicSourceTest, SimpleInsertAndRemove) { - auto ds = std::make_shared(ITEMS); - config->dataSourceProvider("GenericList", ds); - loadDocument(SIMPLE_SEQUENCE, DATA_OFFSET); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1281,3 +1231,226 @@ TEST_F(DynamicSourceTest, SimpleInsertAndRemove) ASSERT_TRUE(CheckChild(4, "text13")); ASSERT_TRUE(CheckChild(5, "textI15")); } + +static const char* NESTED_DATASOURCE_DOC = R"({ + "type": "APL", + "version": "2023.2", + "layouts": { + "DynamicContainer": { + "parameters": [ + { + "name": "listItems", + "type": "array" + } + ], + "item": [ + { + "type": "Container", + "data": "${listItems}", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Text", + "text": "${data.text}" + } + ] + } + ] + } + }, + "mainTemplate": { + "parameters": [ "magic" ], + "item": [ + { + "type": "DynamicContainer", + "listItems": "${magic.dynamicList}" + } + ] + } +})"; + +static const char* NESTED_DATASOURCE_DATA = R"({ + "magic": { + "dynamicList": { + "listId": "vQdpOESlok", + "type": "GenericList", + "offset": 0, + "maxItems": 2, + "items": [ + { "text": "Text 1" }, + { "text": "Text 2" } + ] + } + } +})"; + +TEST_F(DynamicSourceTest, DynamicSourceNestedToLayout) { + loadDocument(NESTED_DATASOURCE_DOC, NESTED_DATASOURCE_DATA); + advanceTime(10); + + ASSERT_EQ(component->getChildCount(), 2); + + auto text1 = component->getChildAt(0); + auto text2 = component->getChildAt(1); + + ASSERT_EQ(text1->getCalculated(kPropertyText).asString(), "Text 1"); + ASSERT_EQ(text2->getCalculated(kPropertyText).asString(), "Text 2"); +} + +static const char* INLINE_LAYOUT_DATASOURCE_DOC = R"({ + "type": "APL", + "version": "2023.2", + "layouts": { + "DynamicContainer": { + "parameters": [ + { + "name": "listItems", + "type": "array" + } + ], + "item": [ + { + "type": "Container", + "data": "${listItems}", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Text", + "text": "${data.text}" + } + ] + } + ] + } + }, + "mainTemplate": { + "parameters": [ "magic" ], + "item": [ + { + "type": "DynamicContainer", + "listItems": { + "listId": "vQdpOESlok", + "type": "GenericList", + "offset": 0, + "maxItems": 2, + "items": "${magic.items}" + } + } + ] + } +})"; + +static const char* INLINE_LAYOUT_DATASOURCE_DATA = R"({ + "magic": { + "items": [ + { "text": "Text 1" }, + { "text": "Text 2" } + ] + } +})"; + +TEST_F(DynamicSourceTest, DynamicSourceInlineInLayout) { + loadDocument(INLINE_LAYOUT_DATASOURCE_DOC, INLINE_LAYOUT_DATASOURCE_DATA); + advanceTime(10); + + ASSERT_EQ(component->getChildCount(), 2); + + auto text1 = component->getChildAt(0); + auto text2 = component->getChildAt(1); + + ASSERT_EQ(text1->getCalculated(kPropertyText).asString(), "Text 1"); + ASSERT_EQ(text2->getCalculated(kPropertyText).asString(), "Text 2"); +} + +static const char* PAYLOAD_SELECTION_DATASOURCE_DOC = R"({ + "type": "APL", + "version": "2023.2", + "layouts": { + "DynamicContainer": { + "parameters": [ "dynamicSource", "staticList" ], + "item": [ + { + "type": "Container", + "data": "${!(staticList[0]) ? dynamicSource : staticList}", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Text", + "text": "${data.text}" + } + ] + } + ] + } + }, + "mainTemplate": { + "parameters": [ "payload" ], + "item": [ + { + "type": "DynamicContainer", + "dynamicSource": "${payload['dynamicList']}", + "staticList": "${payload.staticList}" + } + ] + } +})"; + +static const char* PAYLOAD_SELECTION_HAVE_STATIC_DATASOURCE_DATA = R"({ + "dynamicList": { + "listId": "vQdpOESlok", + "type": "GenericList", + "offset": 0, + "maxItems": 2, + "items": [ + { "text": "Text 3" }, + { "text": "Text 4" } + ] + }, + "staticList": [ + { "text": "Text 1" }, + { "text": "Text 2" } + ] +})"; + +TEST_F(DynamicSourceTest, DynamicSourcePayloadSelectionStatic) { + loadDocument(PAYLOAD_SELECTION_DATASOURCE_DOC, PAYLOAD_SELECTION_HAVE_STATIC_DATASOURCE_DATA); + advanceTime(10); + + ASSERT_EQ(component->getChildCount(), 2); + + auto text1 = component->getChildAt(0); + auto text2 = component->getChildAt(1); + + ASSERT_EQ(text1->getCalculated(kPropertyText).asString(), "Text 1"); + ASSERT_EQ(text2->getCalculated(kPropertyText).asString(), "Text 2"); +} + +static const char* PAYLOAD_SELECTION_STATIC_EMPTY_DATASOURCE_DATA = R"({ + "dynamicList": { + "listId": "vQdpOESlok", + "type": "GenericList", + "offset": 0, + "maxItems": 2, + "items": [ + { "text": "Text 3" }, + { "text": "Text 4" } + ] + }, + "staticList": [] +})"; + +TEST_F(DynamicSourceTest, DynamicSourcePayloadSelectionStaticEmpty) { + loadDocument(PAYLOAD_SELECTION_DATASOURCE_DOC, PAYLOAD_SELECTION_STATIC_EMPTY_DATASOURCE_DATA); + advanceTime(10); + + ASSERT_EQ(component->getChildCount(), 2); + + auto text1 = component->getChildAt(0); + auto text2 = component->getChildAt(1); + + ASSERT_EQ(text1->getCalculated(kPropertyText).asString(), "Text 3"); + ASSERT_EQ(text2->getCalculated(kPropertyText).asString(), "Text 4"); +} diff --git a/unit/datasource/unittest_datasource_context.cpp b/aplcore/unit/datasource/unittest_datasource_context.cpp similarity index 100% rename from unit/datasource/unittest_datasource_context.cpp rename to aplcore/unit/datasource/unittest_datasource_context.cpp diff --git a/aplcore/unit/datasource/unittest_dynamicindexlistembedded.cpp b/aplcore/unit/datasource/unittest_dynamicindexlistembedded.cpp new file mode 100644 index 0000000..4e42123 --- /dev/null +++ b/aplcore/unit/datasource/unittest_dynamicindexlistembedded.cpp @@ -0,0 +1,391 @@ +/** + * Copyright 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 "../testeventloop.h" + +#include "./dynamicindexlisttest.h" +#include "../embed/testdocumentmanager.h" + +#include "apl/dynamicdata.h" + +using namespace apl; + +class DynamicIndexListEmbeddedTest : public DynamicIndexListTest { +public: + DynamicIndexListEmbeddedTest() { + documentManager = std::make_shared(); + config->documentManager(std::static_pointer_cast(documentManager)); + + auto cnf = DynamicIndexListConfiguration() + .setCacheChunkSize(5) + .setListUpdateBufferSize(5) + .setFetchRetries(0) + .setFetchTimeout(100) + .setCacheExpiryTimeout(500); + + eds = std::make_shared(cnf); + documentConfig = DocumentConfig::create(); + documentConfig->dataSourceProvider(eds); + } + + std::shared_ptr documentManager; + std::shared_ptr documentConfig; + std::shared_ptr eds; +}; + +static const char *EMBEDDED_DOC = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "parameters": [ "dynamicSource" ], + "item": { + "type": "Container", + "id": "EmbeddedExpandable", + "height": "100%", + "width": "100%", + "data": "${dynamicSource}", + "item": { + "type": "Text", + "width": 100, + "height": 100, + "text": "${data}" + } + } + } +})"; + +static const char* HOST_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "parameters": [ "dynamicSource" ], + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Host", + "width": "50%", + "height": "50%", + "source": "embeddedDocumentUrl" + }, + { + "type": "Container", + "id": "HostExpandable", + "width": "50%", + "height": "50%", + "item": { + "type": "Text", + "width": 100, + "height": 100, + "text": "${data}" + }, + "data": "${dynamicSource}" + } + ] + } + } +})"; + +static const char *STATIC_DATA_1 = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "list1", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 3, + "items": [ 0, 1, 2 ] + } +})"; + +static const char *STATIC_DATA_1_EMBED = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "list1", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 2, + "items": [ 5, 6 ] + } +})"; + +static const char *STATIC_DATA_2 = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "list2", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 3, + "items": [ 5, 6, 7 ] + } +})"; + +TEST_F(DynamicIndexListEmbeddedTest, SimpleStaticCase) { + loadDocument(HOST_DOC, STATIC_DATA_1); + auto content = Content::create(EMBEDDED_DOC, session); + + rawData = std::make_unique(STATIC_DATA_2); + ASSERT_TRUE(rawData->get().IsObject()); + for (auto it = rawData->get().MemberBegin(); it != rawData->get().MemberEnd(); it++) { + content->addData(it->name.GetString(), it->value); + } + + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, documentConfig); + ASSERT_TRUE(embeddedDocumentContext); + + auto hostParent = root->findComponentById("HostExpandable"); + ASSERT_EQ(3, hostParent->getChildCount()); + auto embeddedParent = root->findComponentById("EmbeddedExpandable"); + ASSERT_EQ(3, embeddedParent->getChildCount()); +} + +TEST_F(DynamicIndexListEmbeddedTest, SameListId) { + loadDocument(HOST_DOC, STATIC_DATA_1); + auto content = Content::create(EMBEDDED_DOC, session); + + rawData = std::make_unique(STATIC_DATA_1_EMBED); + ASSERT_TRUE(rawData->get().IsObject()); + for (auto it = rawData->get().MemberBegin(); it != rawData->get().MemberEnd(); it++) { + content->addData(it->name.GetString(), it->value); + } + + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, documentConfig); + ASSERT_TRUE(embeddedDocumentContext); + + auto hostParent = root->findComponentById("HostExpandable"); + ASSERT_EQ(3, hostParent->getChildCount()); + + // Reuse is not allowed due to sandboxing + auto embeddedParent = root->findComponentById("EmbeddedExpandable"); + ASSERT_EQ(2, embeddedParent->getChildCount()); + + + auto cmd = JsonData(R"apl([{"type": "Reinflate"}])apl"); + ASSERT_TRUE(cmd); + + embeddedDocumentContext->executeCommands(cmd.get(), true); + advanceTime(1500); + + // Reinflate should not pick-up parent's data + embeddedParent = root->findComponentById("EmbeddedExpandable"); + ASSERT_EQ(2, embeddedParent->getChildCount()); +} + +static const char* HOST_PASS_PARAMETER_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "parameters": [ "dynamicSource" ], + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Host", + "width": "50%", + "height": "50%", + "source": "embeddedDocumentUrl", + "EmbeddedDynamicSource": "${dynamicSource}" + }, + { + "type": "Container", + "id": "HostExpandable", + "width": "50%", + "height": "50%", + "item": { + "type": "Text", + "width": 100, + "height": 100, + "text": "${data}" + }, + "data": "${dynamicSource}" + } + ] + } + } +})"; + +static const char *EMBEDDED_AS_PARAMETER_DOC = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "parameters": [ "EmbeddedDynamicSource" ], + "item": { + "type": "Container", + "id": "EmbeddedExpandable", + "height": "100%", + "width": "100%", + "data": "${EmbeddedDynamicSource}", + "item": { + "type": "Text", + "width": 100, + "height": 100, + "text": "${data}" + } + } + } +})"; + +TEST_F(DynamicIndexListEmbeddedTest, PassedAsParameter) { + loadDocument(HOST_PASS_PARAMETER_DOC, STATIC_DATA_1); + auto content = Content::create(EMBEDDED_AS_PARAMETER_DOC, session); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + + auto hostParent = root->findComponentById("HostExpandable"); + ASSERT_EQ(3, hostParent->getChildCount()); + auto embeddedParent = root->findComponentById("EmbeddedExpandable"); + ASSERT_EQ(3, embeddedParent->getChildCount()); +} + +static const char *WRONG_MISSING_FIELDS_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "listWrong", + "minimumInclusiveIndex": 15, + "maximumExclusiveIndex": 20, + "items": [ 10, 11, 12, 13, 14 ] + } +})"; + +TEST_F(DynamicIndexListEmbeddedTest, EmbeddedDocErrors) { + loadDocument(HOST_DOC, STATIC_DATA_1); + auto content = Content::create(EMBEDDED_DOC, session); + + rawData = std::make_unique(WRONG_MISSING_FIELDS_DATA); + ASSERT_TRUE(rawData->get().IsObject()); + for (auto it = rawData->get().MemberBegin(); it != rawData->get().MemberEnd(); it++) { + content->addData(it->name.GetString(), it->value); + } + + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, documentConfig); + ASSERT_TRUE(embeddedDocumentContext); + + ASSERT_TRUE(session->checkAndClear()); + + auto errors = eds->getPendingErrors(); + ASSERT_TRUE(errors.size() == 1); +} + +static const char* HOST_ONLY_DOC = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Frame", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": { + "type": "SendEvent", + "sequencer": "SEND_EVENT", + "arguments": ["LOADED"] + }, + "onFail": { + "type": "SendEvent", + "sequencer": "SEND_EVENT", + "arguments": ["FAILED"] + } + } + } + } +})"; + +static const char *DATA = R"({ + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 1, + "items": [] +})"; + +static const char *EMBEDDED_DYNAMIC_LIST = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Sequence", + "id": "sequence", + "height": 300, + "data": "${dynamicSource}", + "items": { + "type": "Text", + "id": "id${data}", + "width": 100, + "height": 100, + "text": "${data}" + } + } + } +})"; + +TEST_F(DynamicIndexListEmbeddedTest, DynamicIndexListRequestsTagged) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_DYNAMIC_LIST, makeDefaultSession()); + content->addData("dynamicSource", DATA); + ASSERT_TRUE(content->isReady()); + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, documentConfig); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + advanceTime(10); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeDataSourceFetchRequest, event.getType()); + ASSERT_EQ(embeddedDocumentContext, event.getDocument()); +} + +TEST_F(DynamicIndexListEmbeddedTest, NotAvailableForEmbedded) { + loadDocument(HOST_DOC, STATIC_DATA_1); + auto content = Content::create(EMBEDDED_DOC, session); + + rawData = std::make_unique(STATIC_DATA_2); + ASSERT_TRUE(rawData->get().IsObject()); + for (auto it = rawData->get().MemberBegin(); it != rawData->get().MemberEnd(); it++) { + content->addData(it->name.GetString(), it->value); + } + + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + + auto hostParent = root->findComponentById("HostExpandable"); + ASSERT_EQ(3, hostParent->getChildCount()); + auto embeddedParent = root->findComponentById("EmbeddedExpandable"); + ASSERT_EQ(1, embeddedParent->getChildCount()); +} \ No newline at end of file diff --git a/unit/datasource/unittest_dynamicindexlist.cpp b/aplcore/unit/datasource/unittest_dynamicindexlistlazy.cpp similarity index 58% rename from unit/datasource/unittest_dynamicindexlist.cpp rename to aplcore/unit/datasource/unittest_dynamicindexlistlazy.cpp index ed48955..2d762a7 100644 --- a/unit/datasource/unittest_dynamicindexlist.cpp +++ b/aplcore/unit/datasource/unittest_dynamicindexlistlazy.cpp @@ -16,308 +16,15 @@ #include "../testeventloop.h" #include "apl/component/pagercomponent.h" +#include "./dynamicindexlisttest.h" -/** - * The purpose of this include statement is to verify that apl/dataprovider.h includes - * required file(s) that a consumer will need in order to use the datasource provider - * functionality of APL. - */ #include "apl/dynamicdata.h" using namespace apl; -static const char *SOURCE_TYPE = "dynamicIndexList"; -static const char *LIST_ID = "listId"; -static const char *CORRELATION_TOKEN = "correlationToken"; -static const char *START_INDEX = "startIndex"; -static const char *COUNT = "count"; -static const size_t TEST_CHUNK_SIZE = 5; - -class DynamicIndexListTest : public DocumentWrapper { -public: - ::testing::AssertionResult - CheckFetchRequest(const std::string& listId, const std::string& correlationToken, int startIndex, int count) { - bool fetchCalled = root->hasEvent(); - auto event = root->popEvent(); - fetchCalled &= (event.getType() == kEventTypeDataSourceFetchRequest); - - if (!fetchCalled) - return ::testing::AssertionFailure() << "Fetch was not called."; - - auto incomingType = event.getValue(kEventPropertyName).getString(); - if (SOURCE_TYPE != incomingType) - return ::testing::AssertionFailure() - << "DataSource type is wrong. Expected: " << SOURCE_TYPE - << ", actual: " << incomingType; - - auto request = event.getValue(kEventPropertyValue); - - auto incomingListId = request.opt(LIST_ID, ""); - if (incomingListId != listId) - return ::testing::AssertionFailure() - << "listId is wrong. Expected: " << listId - << ", actual: " << incomingListId; - - auto incomingCorrelationToken = request.opt(CORRELATION_TOKEN, ""); - if (incomingCorrelationToken != correlationToken) - return ::testing::AssertionFailure() - << "correlationToken is wrong. Expected: " << correlationToken - << ", actual: " << incomingCorrelationToken; - - auto incomingStartIndex = request.opt(START_INDEX, -1); - if (incomingStartIndex != startIndex) - return ::testing::AssertionFailure() - << "startIndex is wrong. Expected: " << startIndex - << ", actual: " << incomingStartIndex; - - auto incomingCount = request.opt(COUNT, -1); - if (incomingCount != count) - return ::testing::AssertionFailure() - << "count is wrong. Expected: " << count - << ", actual: " << incomingCount; - - return ::testing::AssertionSuccess(); - } - - ::testing::AssertionResult - CheckChild(size_t idx, int exp) { - auto text = std::to_string(exp); - auto actualText = component->getChildAt(idx)->getCalculated(kPropertyText).asString(); - if (actualText != text) { - return ::testing::AssertionFailure() - << "text " << idx - << " is wrong. Expected: " << text - << ", actual: " << actualText; - } - - return ::testing::AssertionSuccess(); - } - - ::testing::AssertionResult - CheckChildren(size_t startIdx, std::vector values) { - if (values.size() != component->getChildCount()) { - return ::testing::AssertionFailure() - << "Wrong child number. Expected: " << values.size() - << ", actual: " << component->getChildCount(); - } - int idx = startIdx; - for (int exp : values) { - auto result = CheckChild(idx, exp); - if (!result) { - return result; - } - idx++; - } - - return ::testing::AssertionSuccess(); - } - - ::testing::AssertionResult - CheckChildren(std::vector values) { - return CheckChildren(0, values); - } - - ::testing::AssertionResult - CheckBounds(int minInclusive, int maxExclusive) { - auto actual = ds->getBounds("vQdpOESlok"); - std::pair expected(minInclusive, maxExclusive); - - if (actual != expected) { - return ::testing::AssertionFailure() - << "bounds is wrong. Expected: (" << expected.first << "," << expected.second - << "), actual: (" << actual.first << "," << actual.second << ")"; - } - return ::testing::AssertionSuccess(); - } - - ::testing::AssertionResult - CheckErrors(std::vector reasons) { - auto errors = ds->getPendingErrors().getArray(); - - if (errors.size() != reasons.size()) { - return ::testing::AssertionFailure() - << "Number of errors is wrong. Expected: " << reasons.size() - << ", actual: " << errors.size(); - } - - for (int i = 0; i expectedPages) { - if (expectedPages.size() != component->getChildCount()) { - return ::testing::AssertionFailure() << "Expected " << expectedPages.size() << " page(s), " - << "found " << component->getChildCount(); - } - if (expectedCurrentPage != component->getCalculated(kPropertyCurrentPage).asNumber()) { - return ::testing::AssertionFailure() << "Expected the current page to be " - << expectedCurrentPage << " but was " - << component->getCalculated(kPropertyCurrentPage).asNumber(); - } - for (int i = 0; i < expectedPages.size(); i++) { - auto child = component->getChildAt(i); - auto expectedPage = expectedPages[i]; - if (expectedPage.id != child->getId()) { - return ::testing::AssertionFailure() << "Expected page " << i - << " to have an id of " << expectedPage.id - << " but was " << child->getId(); - } - if (expectedPage.isTransforming == CheckTransform(Transform2D(), child)) { - return ::testing::AssertionFailure() << "Expected page " << i << " (id=" - << expectedPage.id << ") to be" - << (expectedPage.isTransforming ? "" : " NOT") - << " transforming, but it was" - << (expectedPage.isTransforming ? " NOT" : ""); - } - } - return ::testing::AssertionSuccess(); - } - - static std::string - createLazyLoad(int listVersion, int correlationToken, int index, const std::string& items ) { - std::string listVersionString = listVersion < 0 ? "" : ("\"listVersion\": " + std::to_string(listVersion) + ","); - std::string ctString = correlationToken < 0 ? "" : ("\"correlationToken\": \"" + std::to_string(correlationToken) + "\","); - return "{" - " \"presentationToken\": \"presentationToken\"," - " \"listId\": \"vQdpOESlok\"," - + listVersionString + ctString + - " \"startIndex\": " + std::to_string(index) + "," - " \"items\": [" + items + "]" - "}"; - } - - static std::string - createInsert(int listVersion, int index, int item ) { - return "{" - " \"presentationToken\": \"presentationToken\"," - " \"listId\": \"vQdpOESlok\"," - " \"listVersion\": " + std::to_string(listVersion) + "," - " \"operations\": [" - " {" - " \"type\": \"InsertItem\"," - " \"index\": " + std::to_string(index) + "," - " \"item\": " + std::to_string(item) + - " }" - " ]" - "}"; - } - - static std::string - createReplace(int listVersion, int index, int item ) { - return "{" - " \"presentationToken\": \"presentationToken\"," - " \"listId\": \"vQdpOESlok\"," - " \"listVersion\": " + std::to_string(listVersion) + "," - " \"operations\": [" - " {" - " \"type\": \"SetItem\"," - " \"index\": " + std::to_string(index) + "," - " \"item\": " + std::to_string(item) + - " }" - " ]" - "}"; - } - - static std::string - createDelete(int listVersion, int index ) { - return "{" - " \"presentationToken\": \"presentationToken\"," - " \"listId\": \"vQdpOESlok\"," - " \"listVersion\": " + std::to_string(listVersion) + "," - " \"operations\": [" - " {" - " \"type\": \"DeleteItem\"," - " \"index\": " + std::to_string(index) + - " }" - " ]" - "}"; - } - - static std::string - createMultiInsert(int listVersion, int index, const std::vector& items ) { - std::string itemsString; - for (size_t i = 0; i(cnf); - config->dataSourceProvider(SOURCE_TYPE, ds); - } - - void TearDown() override { - // Check for unprocessed errors. - ASSERT_TRUE(ds->getPendingErrors().empty()); +class DynamicIndexListLazyTest : public DynamicIndexListTest {}; - // Clean any pending timeouts. Tests will check them explicitly. - if(root) { - loop->advanceToEnd(); - while (root->hasEvent()) { - root->popEvent(); - } - } - DocumentWrapper::TearDown(); - } - - std::shared_ptr ds; -}; - -TEST_F(DynamicIndexListTest, Configuration) { +TEST_F(DynamicIndexListLazyTest, Configuration) { // Backward compatibility auto source = std::make_shared("magic", 42); auto actualConfiguration = source->getConfiguration(); @@ -378,17 +85,6 @@ static const char *SMALLER_DATA = R"({ } })"; -static const char *RESTRICTED_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 10, - "minimumInclusiveIndex": 10, - "maximumExclusiveIndex": 15, - "items": [ 10, 11, 12, 13, 14 ] - } -})"; - static const char *BASIC = R"({ "type": "APL", "version": "1.3", @@ -413,7 +109,7 @@ static const char *BASIC = R"({ } })"; -TEST_F(DynamicIndexListTest, Basic) +TEST_F(DynamicIndexListLazyTest, Basic) { loadDocument(BASIC, DATA); advanceTime(10); @@ -460,7 +156,7 @@ TEST_F(DynamicIndexListTest, Basic) ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, BasicAsMap) +TEST_F(DynamicIndexListLazyTest, BasicAsMap) { loadDocument(BASIC, DATA); advanceTime(10); @@ -519,7 +215,7 @@ static const char *BASIC_HORIZONTAL_RTL = R"({ } })"; -TEST_F(DynamicIndexListTest, BasicRTL) +TEST_F(DynamicIndexListLazyTest, BasicRTL) { loadDocument(BASIC_HORIZONTAL_RTL, DATA); advanceTime(10); @@ -566,7 +262,7 @@ TEST_F(DynamicIndexListTest, BasicRTL) ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, BasicAsMapRTL) +TEST_F(DynamicIndexListLazyTest, BasicAsMapRTL) { loadDocument(BASIC_HORIZONTAL_RTL, DATA); advanceTime(10); @@ -609,7 +305,7 @@ static const char *EMPTY = R"({ } })"; -TEST_F(DynamicIndexListTest, Empty) +TEST_F(DynamicIndexListLazyTest, Empty) { loadDocument(BASIC, EMPTY); advanceTime(10); @@ -697,7 +393,7 @@ R"({ } })"; -TEST_F(DynamicIndexListTest, WithFirstAndLast) +TEST_F(DynamicIndexListLazyTest, WithFirstAndLast) { loadDocument(FIRST_AND_LAST, FIRST_AND_LAST_DATA); advanceTime(10); @@ -794,7 +490,7 @@ static const char *FIRST_AND_LAST_HORIZONTAL_RTL = R"({ } })"; -TEST_F(DynamicIndexListTest, WithFirstAndLastHorizontalRTL) +TEST_F(DynamicIndexListLazyTest, WithFirstAndLastHorizontalRTL) { loadDocument(FIRST_AND_LAST_HORIZONTAL_RTL, FIRST_AND_LAST_DATA); advanceTime(10); @@ -882,7 +578,7 @@ static const char *FIRST = R"({ } })"; -TEST_F(DynamicIndexListTest, WithFirst) +TEST_F(DynamicIndexListLazyTest, WithFirst) { loadDocument(FIRST, FIRST_AND_LAST_DATA); advanceTime(10); @@ -964,7 +660,7 @@ static const char *LAST = R"({ } })"; -TEST_F(DynamicIndexListTest, WithLast) +TEST_F(DynamicIndexListLazyTest, WithLast) { loadDocument(LAST, FIRST_AND_LAST_DATA); advanceTime(10); @@ -1028,7 +724,7 @@ static const char *LAST_DATA = R"({ } })"; -TEST_F(DynamicIndexListTest, WithLastOneWay) +TEST_F(DynamicIndexListLazyTest, WithLastOneWay) { loadDocument(LAST, LAST_DATA); advanceTime(10); @@ -1102,7 +798,7 @@ static const char *SHRINKABLE_DATA = R"({ } })"; -TEST_F(DynamicIndexListTest, ShrinkData) +TEST_F(DynamicIndexListLazyTest, ShrinkData) { loadDocument(BASIC, SHRINKABLE_DATA); advanceTime(10); @@ -1123,7 +819,7 @@ static const char *EMPTY_DATA = R"({ } })"; -TEST_F(DynamicIndexListTest, EmptySequence) +TEST_F(DynamicIndexListLazyTest, EmptySequence) { loadDocument(BASIC, EMPTY_DATA); advanceTime(10); @@ -1208,7 +904,7 @@ static const char *MULTI_DATA = R"({ } })"; -TEST_F(DynamicIndexListTest, Multi) { +TEST_F(DynamicIndexListLazyTest, Multi) { loadDocument(MULTI, MULTI_DATA); advanceTime(10); @@ -1267,14 +963,14 @@ static const char *MULTI_CLONED_DATA = R"({ } })"; -TEST_F(DynamicIndexListTest, WrongMissingFieldsData) { +TEST_F(DynamicIndexListLazyTest, WrongMissingFieldsData) { loadDocument(BASIC, WRONG_MISSING_FIELDS_DATA); ASSERT_TRUE(session->checkAndClear()); ASSERT_TRUE(CheckErrors({"INTERNAL_ERROR"})); ASSERT_EQ(component->getChildCount(), 1); } -TEST_F(DynamicIndexListTest, WrongNINIndexData) { +TEST_F(DynamicIndexListLazyTest, WrongNINIndexData) { loadDocument(BASIC, WRONG_NIN_INDEX_DATA); ASSERT_TRUE(session->checkAndClear()); @@ -1282,21 +978,21 @@ TEST_F(DynamicIndexListTest, WrongNINIndexData) { ASSERT_EQ(component->getChildCount(), 1); } -TEST_F(DynamicIndexListTest,WrongMaxIndexData) { +TEST_F(DynamicIndexListLazyTest,WrongMaxIndexData) { loadDocument(BASIC, WRONG_MAX_INDEX_DATA); ASSERT_TRUE(session->checkAndClear()); ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); ASSERT_EQ(component->getChildCount(), 1); } -TEST_F(DynamicIndexListTest,MultiCloneData) { +TEST_F(DynamicIndexListLazyTest,MultiCloneData) { loadDocument(MULTI, MULTI_CLONED_DATA); ASSERT_TRUE(session->checkAndClear()); ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); ASSERT_EQ(component->getChildCount(), 2); } -TEST_F(DynamicIndexListTest, DuplicateListVersionErrorForRemovedComponent) +TEST_F(DynamicIndexListLazyTest, DuplicateListVersionErrorForRemovedComponent) { loadDocument(BASIC, DATA); advanceTime(10); @@ -1304,11 +1000,12 @@ TEST_F(DynamicIndexListTest, DuplicateListVersionErrorForRemovedComponent) ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); component = nullptr; + rootDocument = nullptr; root = nullptr; ASSERT_FALSE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); } -TEST_F(DynamicIndexListTest, MissingListVersionErrorForRemovedComponent) +TEST_F(DynamicIndexListLazyTest, MissingListVersionErrorForRemovedComponent) { loadDocument(BASIC, DATA); advanceTime(10); @@ -1316,11 +1013,12 @@ TEST_F(DynamicIndexListTest, MissingListVersionErrorForRemovedComponent) ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); component = nullptr; + rootDocument = nullptr; root = nullptr; ASSERT_FALSE(ds->processUpdate(createLazyLoad(-1, 101, 15, "15, 16, 17, 18, 19"))); } -TEST_F(DynamicIndexListTest, ConnectionInFailedStateForRemovedComponent) +TEST_F(DynamicIndexListLazyTest, ConnectionInFailedStateForRemovedComponent) { loadDocument(BASIC, DATA); advanceTime(10); @@ -1335,7 +1033,7 @@ TEST_F(DynamicIndexListTest, ConnectionInFailedStateForRemovedComponent) ASSERT_FALSE(ds->getPendingErrors().empty()); } -TEST_F(DynamicIndexListTest, InvalidUpdatePayloadForRemovedComponent) +TEST_F(DynamicIndexListLazyTest, InvalidUpdatePayloadForRemovedComponent) { loadDocument(BASIC, DATA); advanceTime(10); @@ -1343,6 +1041,7 @@ TEST_F(DynamicIndexListTest, InvalidUpdatePayloadForRemovedComponent) ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); component = nullptr; + rootDocument = nullptr; root = nullptr; auto invalidPayload = "{\"presentationToken\": \"presentationToken\", \"listId\": \"vQdpOESlok\"}"; ASSERT_FALSE(ds->processUpdate(invalidPayload)); @@ -1371,7 +1070,7 @@ static const char *BASIC_CONTAINER = R"({ } })"; -TEST_F(DynamicIndexListTest, Container) +TEST_F(DynamicIndexListLazyTest, Container) { loadDocument(BASIC_CONTAINER, DATA); @@ -1440,7 +1139,7 @@ static const char *NOT_ARRAY_ITEMS_FOLLOWUP = R"({ "items": { "abr": 1 } })"; -TEST_F(DynamicIndexListTest, WrongUpdates) +TEST_F(DynamicIndexListLazyTest, WrongUpdates) { loadDocument(BASIC, DATA); @@ -1489,7 +1188,7 @@ static const char *DATA_PARTIAL_OOR = R"({ } })"; -TEST_F(DynamicIndexListTest, PartialOutOfRange) +TEST_F(DynamicIndexListLazyTest, PartialOutOfRange) { loadDocument(BASIC, DATA_PARTIAL_OOR); @@ -1529,7 +1228,7 @@ static const char *RESPONSE_AND_BOUND_UNKNOWN_DOWN = R"({ "items": [ -20, -19, -18, -17, -16 ] })"; -TEST_F(DynamicIndexListTest, UnknownBounds) +TEST_F(DynamicIndexListLazyTest, UnknownBounds) { loadDocument(BASIC, UNKNOWN_BOUNDS_DATA); advanceTime(10); @@ -1582,7 +1281,7 @@ static const char *SIMPLE_UPDATE = R"({ "items": [ "-17U", "-16U", "-15U", "-14U", "-13U" ] })"; -TEST_F(DynamicIndexListTest, SimpleUpdate) +TEST_F(DynamicIndexListLazyTest, SimpleUpdate) { loadDocument(BASIC, UNKNOWN_BOUNDS_DATA); @@ -1640,7 +1339,7 @@ static const char *RESPONSE_AND_BOUND_EXTEND = R"({ "items": [ 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, PositiveBounds) +TEST_F(DynamicIndexListLazyTest, PositiveBounds) { loadDocument(BASIC, POSITIVE_BOUNDS_DATA); @@ -1669,979 +1368,204 @@ TEST_F(DynamicIndexListTest, PositiveBounds) ASSERT_EQ("id14", component->getChildAt(7)->getId()); } -static const char *BASIC_CRUD_SERIES = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "listVersion": 1, - "operations": [ - { - "type": "InsertListItem", - "index": 11, - "item": 111 - }, - { - "type": "ReplaceListItem", - "index": 13, - "item": 113 - }, - { - "type": "DeleteListItem", - "index": 12 + +static const char *BASIC_PAGER = R"({ + "type": "APL", + "version": "1.2", + "theme": "light", + "layouts": { + "square": { + "parameters": ["color", "text"], + "item": { + "type": "Frame", + "width": 200, + "height": 200, + "id": "frame-${text}", + "backgroundColor": "${color}", + "item": { + "type": "Text", + "text": "${text}", + "color": "black", + "width": 200, + "height": 200 + } + } } - ] + }, + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Pager", + "id": "pager", + "data": "${dynamicSource}", + "width": "100%", + "height": "100%", + "navigation": "normal", + "items": { + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + } + } + } })"; -TEST_F(DynamicIndexListTest, CrudBasicSeries) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckBounds(10, 15)); - - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - - ASSERT_TRUE(ds->processUpdate(BASIC_CRUD_SERIES)); - root->clearPending(); - - ASSERT_TRUE(CheckChildren({10, 111, 113, 13, 14})); -} +static const char *BASIC_PAGER_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 10, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 20, + "items": [ + { "color": "blue", "text": "10" }, + { "color": "red", "text": "11" }, + { "color": "green", "text": "12" }, + { "color": "yellow", "text": "13" }, + { "color": "white", "text": "14" } + ] + } +})"; -static const char *BROKEN_CRUD_SERIES = R"({ - "presentationToken": "presentationToken", +static const char *FIVE_TO_NINE_FOLLOWUP_PAGER = R"({ + "token": "presentationToken", "listId": "vQdpOESlok", - "listVersion": 1, - "operations": [ - { - "type": "InsertListItem", - "index": 11, - "item": 111 - }, - { - "type": "InsertListItem", - "index": 27, - "item": 27 - }, - { - "type": "ReplaceListItem", - "index": 13, - "item": 113 - }, - { - "type": "DeleteListItem", - "index": 27, - "item": 27 - }, - { - "type": "DeleteListItem", - "index": 12 - } + "startIndex": 5, + "items": [ + { "color": "blue", "text": "5" }, + { "color": "red", "text": "6" }, + { "color": "green", "text": "7" }, + { "color": "yellow", "text": "8" }, + { "color": "white", "text": "9" } ] })"; -TEST_F(DynamicIndexListTest, CrudInvalidInbetweenSeries) +TEST_F(DynamicIndexListLazyTest, BasicPager) { - loadDocument(BASIC, RESTRICTED_DATA); + loadDocument(BASIC_PAGER, BASIC_PAGER_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); + ASSERT_EQ(kComponentTypePager, component->getType()); + advanceTime(10); + root->clearDirty(); ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckBounds(10, 15)); + ASSERT_TRUE(CheckBounds(0, 20)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 4}, false)); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + // Load 5 pages BEFORE the current set of pages + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 5, 5)); + ASSERT_TRUE(ds->processUpdate(FIVE_TO_NINE_FOLLOWUP_PAGER)); + root->clearPending(); + ASSERT_EQ(10, component->getChildCount()); + ASSERT_EQ("frame-5", component->getChildAt(0)->getId()); + ASSERT_EQ("frame-14", component->getChildAt(9)->getId()); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 4)); // Page 4 gets loaded because we're on page 5 + ASSERT_TRUE(CheckChildrenLaidOut(component, {0,3}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {4,6}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {7,9}, false)); + + // Switch to the first page (index=0) + component->update(UpdateType::kUpdatePagerByEvent, 0); + root->clearPending(); + ASSERT_TRUE(CheckChildrenLaidOutDirtyFlagsWithNotify(component, {0, 1})); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 3}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {4, 6}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {7, 9}, false)); + + // Load 5 more pages BEFORE the current set of pages + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", 15, 5)); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 0, 5)); + ASSERT_TRUE(ds->processUpdate(createLazyLoad(0, 102, 15, + R"({ "color": "blue", "text": "15" }, + { "color": "red", "text": "16" }, + { "color": "green", "text": "17" }, + { "color": "yellow", "text": "18" }, + { "color": "white", "text": "19" })" ))); + ASSERT_TRUE(ds->processUpdate(createLazyLoad(0, 103, 0, + R"({ "color": "blue", "text": "0" }, + { "color": "red", "text": "1" }, + { "color": "green", "text": "2" }, + { "color": "yellow", "text": "3" }, + { "color": "white", "text": "4" })" ))); + root->clearPending(); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 3}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {4, 6}, true)); // Page 4 gets loaded because we're on page 5 + ASSERT_TRUE(CheckChildrenLaidOut(component, {7, 8}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {9, 11}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {12, 14}, false)); - ASSERT_FALSE(ds->processUpdate(BROKEN_CRUD_SERIES)); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + // Switch to the last page (index=14) + component->update(UpdateType::kUpdatePagerByEvent, 14); root->clearPending(); + ASSERT_TRUE(CheckChildrenLaidOutDirtyFlagsWithNotify(component, {13, 14})); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 3}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {4, 6}, true)); // Page 4 gets loaded because we're on page 5 + ASSERT_TRUE(CheckChildrenLaidOut(component, {7, 8}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {9, 11}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {12, 12}, false)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {13, 15}, true)); // Page 15 gets loaded because we're on page 14 + ASSERT_TRUE(CheckChildrenLaidOut(component, {16, 19}, false)); + + ASSERT_TRUE(root->isDirty()); + + auto dirty = root->getDirty(); + ASSERT_EQ(1, dirty.count(component)); + ASSERT_EQ(1, component->getDirty().count(kPropertyNotifyChildrenChanged)); - ASSERT_TRUE(CheckChildren({10, 111, 11, 12, 13, 14})); + ASSERT_EQ("frame-0", component->getChildAt(0)->getId()); + ASSERT_EQ("frame-19", component->getChildAt(19)->getId()); } -static const char *STARTING_BOUNDS_DATA = R"({ +static const char *EMPTY_PAGER_DATA = R"({ "dynamicSource": { "type": "dynamicIndexList", "listId": "vQdpOESlok", - "startIndex": -5, - "minimumInclusiveIndex": -5, - "maximumExclusiveIndex": 5, - "items": [ -5, -4, -3, -2, -1, 0, 1, 2, 3, 4 ] + "startIndex": 10, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 20, + "items": [] } })"; -TEST_F(DynamicIndexListTest, CrudBoundsVerification) -{ - loadDocument(BASIC, STARTING_BOUNDS_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); +static const char *TEN_TO_FIFTEEN_RESPONSE_PAGER = R"({ + "token": "presentationToken", + "correlationToken": "101", + "listId": "vQdpOESlok", + "startIndex": 10, + "items": [ + { "color": "blue", "text": "10" }, + { "color": "red", "text": "11" }, + { "color": "green", "text": "12" }, + { "color": "yellow", "text": "13" }, + { "color": "white", "text": "14" } + ] +})"; - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); +TEST_F(DynamicIndexListLazyTest, EmptyPager) +{ + loadDocument(BASIC_PAGER, EMPTY_PAGER_DATA); - ASSERT_TRUE(CheckBounds(-5, 5)); + ASSERT_EQ(kComponentTypePager, component->getType()); - // Negative insert - ASSERT_TRUE(ds->processUpdate(createInsert(1, -3, -103))); - root->clearPending(); - ASSERT_EQ(11, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 6)); - ASSERT_TRUE(CheckChildren({-5, -4, -103, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_EQ(0, component->getChildCount()); - // Positive insert - ASSERT_TRUE(ds->processUpdate(createInsert(2, 3, 103))); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 10, 5)); + ASSERT_TRUE(ds->processUpdate(TEN_TO_FIFTEEN_RESPONSE_PAGER)); root->clearPending(); - ASSERT_EQ(12, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 7)); - ASSERT_TRUE(CheckChildren({-5, -4, -103, -3, -2, -1, 0, 1, 103, 2, 3, 4})); - // Insert on 0 - ASSERT_TRUE(ds->processUpdate(createInsert(3, 0, 100))); - root->clearPending(); - ASSERT_EQ(13, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 8)); - ASSERT_TRUE(CheckChildren({-5, -4, -103, -3, -2, 100, -1, 0, 1, 103, 2, 3, 4})); + ASSERT_EQ(5, component->getChildCount()); - // Negative delete - ASSERT_TRUE(ds->processUpdate(createDelete(4, -5))); - root->clearPending(); - ASSERT_EQ(12, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 7)); - ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, 100, -1, 0, 1, 103, 2, 3, 4})); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); + ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 4}, false)); - // Positive delete - ASSERT_TRUE(ds->processUpdate(createDelete(5, 3))); - root->clearPending(); - ASSERT_EQ(11, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 6)); - ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, 100, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(0, 20)); - // Delete on 0 - ASSERT_TRUE(ds->processUpdate(createDelete(6, 0))); - root->clearPending(); - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 5)); - ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, 100, 0, 1, 2, 3, 4})); -} - -TEST_F(DynamicIndexListTest, CrudPayloadGap) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Insert with gap - ASSERT_FALSE(ds->processUpdate(createInsert(1, 17, 17))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudPayloadInsertOOB) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Insert out of bounds - ASSERT_FALSE(ds->processUpdate(createInsert(1, 21, 21))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudPayloadRemoveOOB) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Remove out of bounds - ASSERT_FALSE(ds->processUpdate(createDelete(1, 21))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudPayloadReplaceOOB) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Replace out of bounds - ASSERT_FALSE(ds->processUpdate(createReplace(1, 21, 1000))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -static const char *WRONG_TYPE_CRUD = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "listVersion": 1, - "operations": [ - { - "type": "7", - "index": 10, - "item": 101 - } - ] -})"; - -TEST_F(DynamicIndexListTest, CrudPayloadInvalidOperation) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Specify wrong operation - ASSERT_FALSE(ds->processUpdate(WRONG_TYPE_CRUD)); - ASSERT_TRUE(CheckErrors({ "INVALID_OPERATION" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -static const char *MALFORMED_OPERATION_CRUD = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "listVersion": 1, - "operations": [ - { - "type": "InsertItem", - "item": 101 - } - ] -})"; - -TEST_F(DynamicIndexListTest, CrudPayloadMalformedOperation) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Specify wrong operation - ASSERT_FALSE(ds->processUpdate(MALFORMED_OPERATION_CRUD)); - ASSERT_TRUE(CheckErrors({ "INVALID_OPERATION" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -static const char *MISSING_OPERATIONS_CRUD = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "listVersion": 1 -})"; - -TEST_F(DynamicIndexListTest, CrudPayloadNoOperation) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - // Don't specify any operations - ASSERT_FALSE(ds->processUpdate(MISSING_OPERATIONS_CRUD)); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -static const char *MISSING_LIST_VERSION_CRUD = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "operations": [ - { - "type": "InsertItem", - "index": 10, - "item": 101 - } - ] -})"; - -TEST_F(DynamicIndexListTest, CrudPayloadNoListVersion) -{ - loadDocument(BASIC, RESTRICTED_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); - ASSERT_TRUE(CheckBounds(10, 15)); - - ASSERT_FALSE(ds->processUpdate(MISSING_LIST_VERSION_CRUD)); - ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION_IN_SEND_DATA" })); -} - -TEST_F(DynamicIndexListTest, CrudMultiInsert) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Negative insert - ASSERT_TRUE(ds->processUpdate(createMultiInsert(1, -3, {-31, -32}))); - root->clearPending(); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -31, -32, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 7)); - - // Positive insert - ASSERT_TRUE(ds->processUpdate(createMultiInsert(2, 3, {31, 32}))); - root->clearPending(); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -31, -32, -2, -1, 0, 31, 32, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 9)); - - // Above loaded adjust insert - ASSERT_TRUE(ds->processUpdate(createMultiInsert(3, 9, {71, 72}))); - root->clearPending(); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -31, -32, -2, -1, 0, 31, 32, 1, 2, 3, 4, 71, 72})); - ASSERT_TRUE(CheckBounds(-5, 11)); -} - -TEST_F(DynamicIndexListTest, CrudMultiInsertAbove) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Attach at the end - ASSERT_FALSE(ds->processUpdate(createMultiInsert(1, 10, {100, 101}))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudMultiInsertBelow) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Below loaded insert - ASSERT_FALSE(ds->processUpdate(createMultiInsert(1, -10, {-100, -101}))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -static const char *NON_ARRAY_MULTI_INSERT = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "listVersion": 1, - "operations": [ - { - "type": "InsertMultipleItems", - "index": 11, - "items": 111 - } - ] -})"; - -TEST_F(DynamicIndexListTest, CrudMultiInsertNonArray) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Below loaded insert - ASSERT_FALSE(ds->processUpdate(NON_ARRAY_MULTI_INSERT)); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudMultiDelete) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Remove across - ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, -1, 3))); - root->clearPending(); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 2)); - - // Delete negative - ASSERT_TRUE(ds->processUpdate(createMultiDelete(2, -5, 2))); - root->clearPending(); - ASSERT_TRUE(CheckChildren({-3, -2, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 0)); - - // Delete at the end - ASSERT_TRUE(ds->processUpdate(createMultiDelete(3, -2, 2))); - root->clearPending(); - ASSERT_TRUE(CheckChildren({-3, -2, 2})); - ASSERT_TRUE(CheckBounds(-5, -2)); -} - -TEST_F(DynamicIndexListTest, CrudMultiDeleteOOB) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Out of range - ASSERT_FALSE(ds->processUpdate(createMultiDelete(1, 7, 2))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudMultiDeletePartialOOB) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Some out of range - ASSERT_FALSE(ds->processUpdate(createMultiDelete(1, 15, 3))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudMultiDeleteAll) { - loadDocument(BASIC, STARTING_BOUNDS_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, -5, 10))); - root->clearPending(); - ASSERT_EQ(0, component->getChildCount()); -} - - -static const char *SINGULAR_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 0, - "minimumInclusiveIndex": -5, - "maximumExclusiveIndex": 5, - "items": [ 0 ] - } -})"; - -TEST_F(DynamicIndexListTest, CrudMultiDeleteMore) { - loadDocument(BASIC, SINGULAR_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(1, component->getChildCount()); - ASSERT_TRUE(CheckChildren({0})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - // Some out of range - ASSERT_FALSE(ds->processUpdate(createMultiDelete(1, 15, 3))); - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); - - ASSERT_EQ(1, component->getChildCount()); -} - -TEST_F(DynamicIndexListTest, CrudMultiDeleteLast) { - loadDocument(BASIC, SINGULAR_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(1, component->getChildCount()); - ASSERT_TRUE(CheckChildren({0})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, 0, 1))); - root->clearPending(); - ASSERT_EQ(0, component->getChildCount()); -} - -TEST_F(DynamicIndexListTest, CrudDeleteLast) { - loadDocument(BASIC, SINGULAR_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(1, component->getChildCount()); - ASSERT_TRUE(CheckChildren({0})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(createDelete(1, 0))); - root->clearPending(); - ASSERT_EQ(0, component->getChildCount()); -} - -TEST_F(DynamicIndexListTest, CrudInsertAdjascent) { - loadDocument(BASIC, SINGULAR_DATA); - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(1, component->getChildCount()); - ASSERT_TRUE(CheckChildren({0})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(createInsert(1, 1, 1))); // This allowed (N+1) - ASSERT_TRUE(ds->processUpdate(createInsert(2, 0, 11))); // This is also allowed (M) - ASSERT_FALSE(ds->processUpdate(createInsert(3, -1, -1))); // This is not (M-1) - ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); - root->clearPending(); - - ASSERT_TRUE(CheckChildren({11, 0, 1})); - ASSERT_TRUE(CheckBounds(-5, 7)); - ASSERT_EQ(3, component->getChildCount()); -} - -static const char *LAZY_CRUD_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": -2, - "minimumInclusiveIndex": -5, - "maximumExclusiveIndex": 5, - "items": [ -2, -1, 0, 1, 2 ] - } -})"; - -TEST_F(DynamicIndexListTest, CrudLazyCombination) -{ - loadDocument(BASIC, LAZY_CRUD_DATA); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 3, "3, 4" ))); - ASSERT_TRUE(ds->processUpdate(createLazyLoad(2, 102, -5, "-5, -4, -3" ))); - root->clearPending(); - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - - ASSERT_TRUE(ds->processUpdate(createInsert(3, -2, -103))); - root->clearPending(); - ASSERT_EQ(11, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 6)); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -103, -2, -1, 0, 1, 2, 3, 4})); - - ASSERT_TRUE(ds->processUpdate(createInsert(4, 4, 103))); - root->clearPending(); - ASSERT_EQ(12, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 7)); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -103, -2, -1, 0, 1, 2, 103, 3, 4})); - - -} - -static const char *LAZY_WITHOUT_VERSION = R"({ - "token": "presentationToken", - "listId": "vQdpOESlok", - "correlationToken": "102", - "startIndex": -5, - "items": [ -5, -4, -3 ] -})"; - -TEST_F(DynamicIndexListTest, CrudAfterNoVersionLazy) -{ - loadDocument(BASIC, LAZY_CRUD_DATA); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(LAZY_WITHOUT_VERSION)); - root->clearPending(); - - ASSERT_EQ(8, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2})); - - ASSERT_FALSE(ds->processUpdate(createInsert(1, 0, 101))); - ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION_IN_SEND_DATA" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudBeforeNoVersionLazy) -{ - loadDocument(BASIC, LAZY_CRUD_DATA); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_TRUE(ds->processUpdate(createInsert(1, 0, 101))); - root->clearPending(); - - ASSERT_EQ(6, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-2, -1, 101, 0, 1, 2})); - - ASSERT_FALSE(ds->processUpdate(LAZY_WITHOUT_VERSION)); - ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION_IN_SEND_DATA" })); - - // In fail state so will not allow other operation - ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); - ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); -} - -TEST_F(DynamicIndexListTest, CrudWrongData) -{ - loadDocument(BASIC, LAZY_CRUD_DATA); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - - ASSERT_TRUE(ds->processUpdate(createInsert(1, -2, -103))); - root->clearPending(); - ASSERT_EQ(6, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 6)); - ASSERT_TRUE(CheckChildren({-103, -2, -1, 0, 1, 2})); - - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 4, 2)); - - // Wrong version crud will not fly - ASSERT_FALSE(ds->processUpdate(createInsert(3, 0, 100))); // This is cached - ASSERT_FALSE(ds->processUpdate(createInsert(1, 0, 100))); // This is not - ASSERT_TRUE(CheckErrors({ "DUPLICATE_LIST_VERSION" })); -} - -TEST_F(DynamicIndexListTest, CrudOutOfOrder) -{ - loadDocument(BASIC, STARTING_BOUNDS_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_FALSE(ds->processUpdate(createInsert(2, 4, 103))); - ASSERT_FALSE(ds->processUpdate(createInsert(3, 2, 100))); - ASSERT_FALSE(ds->processUpdate(createDelete(5, 5))); - - // Duplicate version in cache - ASSERT_FALSE(ds->processUpdate(createDelete(5, 5))); - ASSERT_TRUE(CheckErrors({ "DUPLICATE_LIST_VERSION" })); - - ASSERT_TRUE(ds->processUpdate(createInsert(1, -3, -103))); - ASSERT_TRUE(ds->processUpdate(createDelete(4, -5))); - - ASSERT_TRUE(ds->processUpdate(createDelete(6, 2))); - root->clearPending(); - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckBounds(-5, 5)); - ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, -1, 0, 100, 2, 103, 4})); -} - -TEST_F(DynamicIndexListTest, CrudBadOutOfOrder) -{ - loadDocument(BASIC, STARTING_BOUNDS_DATA); - - ASSERT_EQ(kComponentTypeSequence, component->getType()); - - ASSERT_EQ(10, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); - ASSERT_TRUE(CheckBounds(-5, 5)); - - ASSERT_FALSE(ds->processUpdate(createInsert(6, 0, 7))); - loop->advanceToTime(500); - - // Update 6 will expire - ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION" })); - - ASSERT_FALSE(ds->processUpdate(createInsert(5, 0, 6))); - ASSERT_FALSE(ds->processUpdate(createInsert(4, 0, 5))); - ASSERT_FALSE(ds->processUpdate(createInsert(2, 0, 3))); - ASSERT_FALSE(ds->processUpdate(createInsert(7, 0, 8))); - ASSERT_FALSE(ds->processUpdate(createInsert(3, 0, 4))); - ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION" })); - ASSERT_FALSE(ds->processUpdate(createInsert(8, 0, 9))); - ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION" })); - - ASSERT_TRUE(ds->processUpdate(createInsert(1, 0, 2))); - loop->advanceToEnd(); - ASSERT_TRUE(CheckErrors({})); - - root->clearPending(); - ASSERT_EQ(16, component->getChildCount()); - ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 7, 6, 5, 4, 3, 2, 0, 1, 2, 3, 4})); -} - -static const char *BASIC_PAGER = R"({ - "type": "APL", - "version": "1.2", - "theme": "light", - "layouts": { - "square": { - "parameters": ["color", "text"], - "item": { - "type": "Frame", - "width": 200, - "height": 200, - "id": "frame-${text}", - "backgroundColor": "${color}", - "item": { - "type": "Text", - "text": "${text}", - "color": "black", - "width": 200, - "height": 200 - } - } - } - }, - "mainTemplate": { - "parameters": [ - "dynamicSource" - ], - "item": { - "type": "Pager", - "id": "pager", - "data": "${dynamicSource}", - "width": "100%", - "height": "100%", - "navigation": "normal", - "items": { - "type": "square", - "index": "${index}", - "color": "${data.color}", - "text": "${data.text}" - } - } - } -})"; - -static const char *BASIC_PAGER_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 10, - "minimumInclusiveIndex": 0, - "maximumExclusiveIndex": 20, - "items": [ - { "color": "blue", "text": "10" }, - { "color": "red", "text": "11" }, - { "color": "green", "text": "12" }, - { "color": "yellow", "text": "13" }, - { "color": "white", "text": "14" } - ] - } -})"; - -static const char *FIVE_TO_NINE_FOLLOWUP_PAGER = -R"({ -"token": "presentationToken", -"listId": "vQdpOESlok", -"startIndex": 5, -"items": [ - { "color": "blue", "text": "5" }, - { "color": "red", "text": "6" }, - { "color": "green", "text": "7" }, - { "color": "yellow", "text": "8" }, - { "color": "white", "text": "9" } -] -})"; - -TEST_F(DynamicIndexListTest, BasicPager) -{ - loadDocument(BASIC_PAGER, BASIC_PAGER_DATA); - - ASSERT_EQ(kComponentTypePager, component->getType()); - advanceTime(10); - root->clearDirty(); - - ASSERT_EQ(5, component->getChildCount()); - ASSERT_TRUE(CheckBounds(0, 20)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 4}, false)); - - // Load 5 pages BEFORE the current set of pages - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 5, 5)); - ASSERT_TRUE(ds->processUpdate(FIVE_TO_NINE_FOLLOWUP_PAGER)); - root->clearPending(); - ASSERT_EQ(10, component->getChildCount()); - ASSERT_EQ("frame-5", component->getChildAt(0)->getId()); - ASSERT_EQ("frame-14", component->getChildAt(9)->getId()); - ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 4)); // Page 4 gets loaded because we're on page 5 - ASSERT_TRUE(CheckChildrenLaidOut(component, {0,3}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {4,6}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {7,9}, false)); - - // Switch to the first page (index=0) - component->update(UpdateType::kUpdatePagerByEvent, 0); - root->clearPending(); - ASSERT_TRUE(CheckChildrenLaidOutDirtyFlagsWithNotify(component, {0, 1})); - ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 3}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {4, 6}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {7, 9}, false)); - - // Load 5 more pages BEFORE the current set of pages - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", 15, 5)); - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 0, 5)); - ASSERT_TRUE(ds->processUpdate(createLazyLoad(0, 102, 15, - R"({ "color": "blue", "text": "15" }, - { "color": "red", "text": "16" }, - { "color": "green", "text": "17" }, - { "color": "yellow", "text": "18" }, - { "color": "white", "text": "19" })" ))); - ASSERT_TRUE(ds->processUpdate(createLazyLoad(0, 103, 0, - R"({ "color": "blue", "text": "0" }, - { "color": "red", "text": "1" }, - { "color": "green", "text": "2" }, - { "color": "yellow", "text": "3" }, - { "color": "white", "text": "4" })" ))); - root->clearPending(); - ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 3}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {4, 6}, true)); // Page 4 gets loaded because we're on page 5 - ASSERT_TRUE(CheckChildrenLaidOut(component, {7, 8}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {9, 11}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {12, 14}, false)); - - // Switch to the last page (index=14) - component->update(UpdateType::kUpdatePagerByEvent, 14); - root->clearPending(); - ASSERT_TRUE(CheckChildrenLaidOutDirtyFlagsWithNotify(component, {13, 14})); - ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 3}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {4, 6}, true)); // Page 4 gets loaded because we're on page 5 - ASSERT_TRUE(CheckChildrenLaidOut(component, {7, 8}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {9, 11}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {12, 12}, false)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {13, 15}, true)); // Page 15 gets loaded because we're on page 14 - ASSERT_TRUE(CheckChildrenLaidOut(component, {16, 19}, false)); - - ASSERT_TRUE(root->isDirty()); - - auto dirty = root->getDirty(); - ASSERT_EQ(1, dirty.count(component)); - ASSERT_EQ(1, component->getDirty().count(kPropertyNotifyChildrenChanged)); - - ASSERT_EQ("frame-0", component->getChildAt(0)->getId()); - ASSERT_EQ("frame-19", component->getChildAt(19)->getId()); -} - -static const char *EMPTY_PAGER_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 10, - "minimumInclusiveIndex": 0, - "maximumExclusiveIndex": 20, - "items": [] - } -})"; - -static const char *TEN_TO_FIFTEEN_RESPONSE_PAGER = R"({ - "token": "presentationToken", - "correlationToken": "101", - "listId": "vQdpOESlok", - "startIndex": 10, - "items": [ - { "color": "blue", "text": "10" }, - { "color": "red", "text": "11" }, - { "color": "green", "text": "12" }, - { "color": "yellow", "text": "13" }, - { "color": "white", "text": "14" } - ] -})"; - -TEST_F(DynamicIndexListTest, EmptyPager) -{ - loadDocument(BASIC_PAGER, EMPTY_PAGER_DATA); - - ASSERT_EQ(kComponentTypePager, component->getType()); - - ASSERT_EQ(0, component->getChildCount()); - - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 10, 5)); - ASSERT_TRUE(ds->processUpdate(TEN_TO_FIFTEEN_RESPONSE_PAGER)); - root->clearPending(); - - ASSERT_EQ(5, component->getChildCount()); - - ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); - ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 4}, false)); - - ASSERT_TRUE(CheckBounds(0, 20)); - - ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", 5, 5)); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", 5, 5)); } static const char *WRAPPING_PAGER = R"({ @@ -2703,7 +1627,7 @@ static const char *WRAPPING_PAGER_DATA = R"({ } })"; -TEST_F(DynamicIndexListTest, WrappedPager) +TEST_F(DynamicIndexListLazyTest, WrappedPager) { loadDocument(WRAPPING_PAGER, WRAPPING_PAGER_DATA); @@ -2792,7 +1716,7 @@ static const char *OLD_WRAPPING_PAGER = R"({ } })"; -TEST_F(DynamicIndexListTest, OldWrappedPager) +TEST_F(DynamicIndexListLazyTest, OldWrappedPager) { loadDocument(OLD_WRAPPING_PAGER, WRAPPING_PAGER_DATA); @@ -2835,7 +1759,7 @@ static const char *SMALLER_DATA_BACK = R"({ } })"; -TEST_F(DynamicIndexListTest, GarbageCollection) { +TEST_F(DynamicIndexListLazyTest, GarbageCollection) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); root->clearDirty(); @@ -2854,6 +1778,7 @@ TEST_F(DynamicIndexListTest, GarbageCollection) { // Kill RootContext and re-inflate. component = nullptr; context = nullptr; + rootDocument = nullptr; root = nullptr; loop = std::make_shared(); @@ -2889,7 +1814,7 @@ static const char *FIFTEEN_TO_NINETEEN_WRONG_LIST_RESPONSE = R"({ "items": [ 15, 16, 17, 18, 19 ] })"; -TEST_F(DynamicIndexListTest, CorrelationTokenSubstitute) { +TEST_F(DynamicIndexListLazyTest, CorrelationTokenSubstitute) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); root->clearDirty(); @@ -2918,7 +1843,7 @@ static const char *FIFTEEN_TO_TWENTY_FOUR_RESPONSE = R"({ "items": [ 15, 16, 17, 18, 19, 20, 21, 22, 23, 24 ] })"; -TEST_F(DynamicIndexListTest, BigLazyLoad) { +TEST_F(DynamicIndexListLazyTest, BigLazyLoad) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); root->clearDirty(); @@ -2945,7 +1870,7 @@ static const char *FIFTEEN_TO_NINETEEN_SHRINK_RESPONSE = R"({ "items": [ 15, 16, 17, 18, 19 ] })"; -TEST_F(DynamicIndexListTest, BoundsShrinkBottom) { +TEST_F(DynamicIndexListLazyTest, BoundsShrinkBottom) { loadDocument(BASIC, SMALLER_DATA); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -2971,7 +1896,7 @@ static const char *FIVE_TO_NINE_SHRINK_RESPONSE = R"({ "items": [ 5, 6, 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, BoundsShrinkTop) { +TEST_F(DynamicIndexListLazyTest, BoundsShrinkTop) { loadDocument(BASIC, SMALLER_DATA_BACK); advanceTime(10); @@ -2999,7 +1924,7 @@ static const char *SHRINK_FULL_RESPONSE = R"({ "items": [ 5, 6, 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, BoundsShrinkFull) { +TEST_F(DynamicIndexListLazyTest, BoundsShrinkFull) { loadDocument(BASIC, SMALLER_DATA_BACK); advanceTime(10); @@ -3026,7 +1951,7 @@ static const char *EXPAND_BOTTOM_RESPONSE = R"({ "items": [ 15, 16, 17, 18, 19 ] })"; -TEST_F(DynamicIndexListTest, BoundsExpandBottom) { +TEST_F(DynamicIndexListLazyTest, BoundsExpandBottom) { loadDocument(BASIC, SMALLER_DATA); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -3052,7 +1977,7 @@ static const char *EXPAND_TOP_RESPONSE = R"({ "items": [ 5, 6, 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, BoundsExpandTop) { +TEST_F(DynamicIndexListLazyTest, BoundsExpandTop) { loadDocument(BASIC, SMALLER_DATA_BACK); advanceTime(10); @@ -3080,7 +2005,7 @@ static const char *EXPAND_FULL_RESPONSE = R"({ "items": [ 5, 6, 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, BoundsExpandFull) { +TEST_F(DynamicIndexListLazyTest, BoundsExpandFull) { loadDocument(BASIC, SMALLER_DATA_BACK); advanceTime(10); @@ -3106,7 +2031,7 @@ static const char *FIFTEEN_EMPTY_RESPONSE = R"({ "items": [] })"; -TEST_F(DynamicIndexListTest, EmptyLazyResponseRetryFail) { +TEST_F(DynamicIndexListLazyTest, EmptyLazyResponseRetryFail) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); @@ -3127,7 +2052,7 @@ TEST_F(DynamicIndexListTest, EmptyLazyResponseRetryFail) { ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, EmptyLazyResponseRetryResolved) { +TEST_F(DynamicIndexListLazyTest, EmptyLazyResponseRetryResolved) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); @@ -3160,7 +2085,7 @@ static const char *FIFTEEN_SHRINK_RESPONSE = R"({ "items": [] })"; -TEST_F(DynamicIndexListTest, EmptyLazyResponseRetryBoundsUpdated) { +TEST_F(DynamicIndexListLazyTest, EmptyLazyResponseRetryBoundsUpdated) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); @@ -3179,7 +2104,7 @@ TEST_F(DynamicIndexListTest, EmptyLazyResponseRetryBoundsUpdated) { ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, LazyResponseTimeout) { +TEST_F(DynamicIndexListLazyTest, LazyResponseTimeout) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); @@ -3204,7 +2129,7 @@ TEST_F(DynamicIndexListTest, LazyResponseTimeout) { ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, LazyResponseTimeoutResolvedAfterLost) { +TEST_F(DynamicIndexListLazyTest, LazyResponseTimeoutResolvedAfterLost) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); @@ -3234,7 +2159,7 @@ TEST_F(DynamicIndexListTest, LazyResponseTimeoutResolvedAfterLost) { ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, LazyResponseTimeoutResolvedAfterDelayed) { +TEST_F(DynamicIndexListLazyTest, LazyResponseTimeoutResolvedAfterDelayed) { loadDocument(BASIC, SMALLER_DATA); advanceTime(10); @@ -3268,232 +2193,6 @@ TEST_F(DynamicIndexListTest, LazyResponseTimeoutResolvedAfterDelayed) { ASSERT_FALSE(root->hasEvent()); } -static const char *SWIPE_TO_DELETE = R"({ - "type": "APL", - "version": "1.4", - "theme": "dark", - "layouts": { - "swipeAway" : { - "parameters": ["text1", "text2"], - "item": { - "type": "TouchWrapper", - "width": 200, - "item": { - "type": "Frame", - "backgroundColor": "blue", - "height": 100, - "items": { - "type": "Text", - "text": "${text1}", - "fontSize": 60 - } - }, - "gestures": [ - { - "type": "SwipeAway", - "direction": "left", - "action":"reveal", - "items": { - "type": "Frame", - "backgroundColor": "purple", - "width": "100%", - "items": { - "type": "Text", - "text": "${text2}", - "fontSize": 60, - "color": "white" - } - }, - "onSwipeDone": { - "type": "SendEvent", - "arguments": ["${event.source.uid}", "${index}"] - } - } - ] - } - } - }, - "mainTemplate": { - "parameters": [ - "dynamicSource" - ], - "items": [ - { - "type": "Sequence", - "width": "100%", - "height": 500, - "alignItems": "center", - "justifyContent": "spaceAround", - "data": "${dynamicSource}", - "items": [ - { - "type": "swipeAway", - "text1": "${data}", - "text2": "${data}" - } - ] - } - ] - } -})"; - -static const char *SWIPE_TO_DELETE_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 0, - "minimumInclusiveIndex": 0, - "maximumExclusiveIndex": 5, - "items": [ 1, 2, 3, 4, 5 ] - } -})"; - -TEST_F(DynamicIndexListTest, SwipeToDelete) -{ - config->set({ - {RootProperty::kSwipeAwayAnimationEasing, "linear"}, - {RootProperty::kPointerSlopThreshold, 5}, - {RootProperty::kSwipeVelocityThreshold, 5}, - {RootProperty::kTapOrScrollTimeout, 10}, - {RootProperty::kPointerInactivityTimeout, 1000} - }); - loadDocument(SWIPE_TO_DELETE, SWIPE_TO_DELETE_DATA); - advanceTime(10); - - ASSERT_TRUE(component); - ASSERT_EQ(5, component->getChildCount()); - ASSERT_EQ(5, component->getDisplayedChildCount()); - - auto idToDelete = component->getChildAt(0)->getUniqueId(); - - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); - - advanceTime(800); - auto event = root->popEvent(); - ASSERT_EQ(kEventTypeSendEvent, event.getType()); - auto deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); - int indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); - ASSERT_EQ(idToDelete, deletedId); - ASSERT_EQ(0, indexToDelete); - - ASSERT_TRUE(ds->processUpdate(createDelete(1, indexToDelete))); - advanceTime(100); - ASSERT_EQ(4, component->getChildCount()); - ASSERT_EQ(4, component->getDisplayedChildCount()); - ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBounds, kPropertyNotifyChildrenChanged, kPropertyVisualHash)); - root->clearDirty(); - - - // Repeat for very first - idToDelete = component->getChildAt(0)->getUniqueId(); - - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); - - advanceTime(800); - event = root->popEvent(); - ASSERT_EQ(kEventTypeSendEvent, event.getType()); - deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); - indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); - ASSERT_EQ(idToDelete, deletedId); - ASSERT_EQ(0, indexToDelete); - root->clearDirty(); - - ASSERT_TRUE(ds->processUpdate(createDelete(2, indexToDelete))); - root->clearPending(); - ASSERT_EQ(3, component->getChildCount()); - ASSERT_EQ(3, component->getDisplayedChildCount()); - ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBounds,kPropertyNotifyChildrenChanged, kPropertyVisualHash)); - root->clearDirty(); - - - // Remove one at the end - idToDelete = component->getChildAt(2)->getUniqueId(); - - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,201), false)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,201), true)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,201), true)); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,201), true)); - - advanceTime(800); - event = root->popEvent(); - ASSERT_EQ(kEventTypeSendEvent, event.getType()); - deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); - indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); - ASSERT_EQ(idToDelete, deletedId); - ASSERT_EQ(2, indexToDelete); - root->clearDirty(); - - ASSERT_TRUE(ds->processUpdate(createDelete(3, indexToDelete))); - root->clearPending(); - root->clearDirty(); - - ASSERT_EQ(2, component->getChildCount()); - ASSERT_EQ(2, component->getDisplayedChildCount()); - - // again - idToDelete = component->getChildAt(0)->getUniqueId(); - - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); - - advanceTime(800); - event = root->popEvent(); - ASSERT_EQ(kEventTypeSendEvent, event.getType()); - deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); - indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); - ASSERT_EQ(idToDelete, deletedId); - ASSERT_EQ(0, indexToDelete); - root->clearDirty(); - - ASSERT_TRUE(ds->processUpdate(createDelete(4, indexToDelete))); - root->clearPending(); - ASSERT_EQ(1, component->getChildCount()); - ASSERT_EQ(1, component->getDisplayedChildCount()); - ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBounds,kPropertyNotifyChildrenChanged, kPropertyVisualHash)); - root->clearDirty(); - - // empty the list - idToDelete = component->getChildAt(0)->getUniqueId(); - - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); - advanceTime(100); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); - ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); - - advanceTime(800); - event = root->popEvent(); - ASSERT_EQ(kEventTypeSendEvent, event.getType()); - deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); - indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); - ASSERT_EQ(idToDelete, deletedId); - ASSERT_EQ(0, indexToDelete); - root->clearDirty(); - - ASSERT_TRUE(ds->processUpdate(createDelete(5, indexToDelete))); - root->clearPending(); - ASSERT_EQ(0, component->getChildCount()); - ASSERT_EQ(0, component->getDisplayedChildCount()); - root->clearDirty(); -} - static const char *PROACTIVE_LOAD_ONLY = R"({ "dynamicSource": { "type": "dynamicIndexList", @@ -3513,7 +2212,7 @@ static const char *PROACTIVE_EXPAND_RESPONSE = R"({ "items": [ 5, 6, 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, ProactiveLoadOnly) +TEST_F(DynamicIndexListLazyTest, ProactiveLoadOnly) { loadDocument(BASIC, PROACTIVE_LOAD_ONLY); @@ -3545,7 +2244,7 @@ static const char *PROACTIVE_EXPAND_BAD_RESPONSE = R"({ "items": [ 5, 6, 7, 8, 9 ] })"; -TEST_F(DynamicIndexListTest, ProactiveLoadOnlyBadJson) +TEST_F(DynamicIndexListLazyTest, ProactiveLoadOnlyBadJson) { loadDocument(BASIC, PROACTIVE_LOAD_ONLY); @@ -3588,7 +2287,7 @@ static const char *BASIC_CONFIG_CHANGE = R"({ ] })"; -TEST_F(DynamicIndexListTest, Reinflate) { +TEST_F(DynamicIndexListLazyTest, Reinflate) { config->set(RootProperty::kSequenceChildCache, 0); loadDocument(BASIC_CONFIG_CHANGE, DATA); @@ -3669,7 +2368,7 @@ static const char *MULTITYPE_SEQUENCE = R"({ } })"; -TEST_F(DynamicIndexListTest, ConditionalSequenceChildren) +TEST_F(DynamicIndexListLazyTest, ConditionalSequenceChildren) { loadDocument(MULTITYPE_SEQUENCE, TYPED_DATA); advanceTime(10); @@ -3714,7 +2413,7 @@ static const char *TYPED_DATA_BACK = R"({ } })"; -TEST_F(DynamicIndexListTest, ConditionalSequenceChildrenBackwards) +TEST_F(DynamicIndexListLazyTest, ConditionalSequenceChildrenBackwards) { loadDocument(MULTITYPE_SEQUENCE, TYPED_DATA_BACK); advanceTime(10); @@ -3771,7 +2470,7 @@ static const char *TYPED_DATA_START_EMPTY = R"({ } })"; -TEST_F(DynamicIndexListTest, ConditionalSequenceChildrenStartEmpty) +TEST_F(DynamicIndexListLazyTest, ConditionalSequenceChildrenStartEmpty) { loadDocument(MULTITYPE_SEQUENCE, TYPED_DATA_START_EMPTY); advanceTime(10); @@ -3816,7 +2515,7 @@ static const char *MULTITYPE_PAGER = R"({ } })"; -TEST_F(DynamicIndexListTest, ConditionalPagerChildren) +TEST_F(DynamicIndexListLazyTest, ConditionalPagerChildren) { loadDocument(MULTITYPE_PAGER, TYPED_DATA); advanceTime(10); @@ -3847,7 +2546,7 @@ TEST_F(DynamicIndexListTest, ConditionalPagerChildren) ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, ConditionalPagerChildrenBackwards) +TEST_F(DynamicIndexListLazyTest, ConditionalPagerChildrenBackwards) { loadDocument(MULTITYPE_PAGER, TYPED_DATA_BACK); advanceTime(10); @@ -3890,7 +2589,7 @@ TEST_F(DynamicIndexListTest, ConditionalPagerChildrenBackwards) ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, ConditionalPagerChildrenStartEmpty) +TEST_F(DynamicIndexListLazyTest, ConditionalPagerChildrenStartEmpty) { loadDocument(MULTITYPE_PAGER, TYPED_DATA_START_EMPTY); advanceTime(10); @@ -3910,133 +2609,6 @@ TEST_F(DynamicIndexListTest, ConditionalPagerChildrenStartEmpty) ASSERT_FALSE(root->hasEvent()); } -static const char *SEQUENCE_RECREATE_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 0, - "minimumInclusiveIndex": 0, - "maximumExclusiveIndex": 1, - "items": [ - { "label": "I am a label.", "sequence": ["red", "green", "blue", "yellow", "purple"] } - ] - } -})"; - -static const char *SEQUENCE_RECREATE = R"({ - "type": "APL", - "version": "1.7", - "theme": "dark", - "mainTemplate": { - "parameters": [ - "dynamicSource" - ], - "item": { - "type": "Container", - "height": 300, - "width": 300, - "data": "${dynamicSource}", - "items": { - "type": "Container", - "height": "100%", - "width": "100%", - "items": [ - { - "type": "Sequence", - "height": "50%", - "width": "100%", - "data": "${data.sequence}", - "items": { - "type": "Frame", - "backgroundColor": "${data}", - "height": 10, - "width": "100%" - } - } - ] - } - } - } -})"; - -static const char *REPLACE_SEQUENCE_CRUD = R"({ - "presentationToken": "presentationToken", - "listId": "vQdpOESlok", - "listVersion": 1, - "operations": [ - { - "type": "DeleteListItem", - "index": 0 - }, - { - "type": "InsertListItem", - "index": 0, - "item": { "sequence": ["purple", "yellow", "blue", "green", "red"] } - } - ] -})"; - -TEST_F(DynamicIndexListTest, SequenceRecreate) -{ - loadDocument(SEQUENCE_RECREATE, SEQUENCE_RECREATE_DATA); - advanceTime(10); - - ASSERT_EQ(1, component->getChildCount()); - auto sequence = component->getCoreChildAt(0)->getCoreChildAt(0); - ASSERT_EQ(5, sequence->getChildCount()); - - ASSERT_EQ(Rect(0, 0, 300, 300), component->getCoreChildAt(0)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 0, 300, 150), sequence->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 0, 300, 10), sequence->getCoreChildAt(0)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 10, 300, 10), sequence->getCoreChildAt(1)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 20, 300, 10), sequence->getCoreChildAt(2)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 30, 300, 10), sequence->getCoreChildAt(3)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 40, 300, 10), sequence->getCoreChildAt(4)->getCalculated(kPropertyBounds).get()); - - ASSERT_TRUE(ds->processUpdate(REPLACE_SEQUENCE_CRUD)); - root->clearPending(); - - sequence = component->getCoreChildAt(0)->getCoreChildAt(0); - ASSERT_EQ(5, sequence->getChildCount()); - - ASSERT_EQ(Rect(0, 0, 300, 300).toDebugString(), component->getCoreChildAt(0)->getCalculated(kPropertyBounds).get().toDebugString()); - ASSERT_EQ(Rect(0, 0, 300, 150), sequence->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 0, 300, 10), sequence->getCoreChildAt(0)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 10, 300, 10), sequence->getCoreChildAt(1)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 20, 300, 10), sequence->getCoreChildAt(2)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 30, 300, 10), sequence->getCoreChildAt(3)->getCalculated(kPropertyBounds).get()); - ASSERT_EQ(Rect(0, 40, 300, 10), sequence->getCoreChildAt(4)->getCalculated(kPropertyBounds).get()); -} - -static const char *FILLED_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 0, - "minimumInclusiveIndex": 0, - "maximumExclusiveIndex": 5, - "items": [ 0, 1, 2, 3, 4 ] - } -})"; - -TEST_F(DynamicIndexListTest, DeleteMultipleAll) -{ - loadDocument(BASIC, FILLED_DATA); - advanceTime(10); - - ASSERT_TRUE(CheckBounds(0, 5)); - ASSERT_EQ(5, component->getChildCount()); - - ASSERT_FALSE(root->hasEvent()); - - ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, 0, 100))); - root->clearPending(); - - ASSERT_EQ(0, component->getChildCount()); -} - - - static const char *FORWARD_ONLY_DATA = R"({ "dynamicSource": { "type": "dynamicIndexList", @@ -4056,7 +2628,7 @@ static const char *SHRINK_BOUNDS_WITHOUT_ITEMS = R"({ "maximumExclusiveIndex": 5 })"; -TEST_F(DynamicIndexListTest, ShrinkWithoutItems) +TEST_F(DynamicIndexListLazyTest, ShrinkWithoutItems) { loadDocument(BASIC, FORWARD_ONLY_DATA); advanceTime(10); @@ -4075,7 +2647,7 @@ TEST_F(DynamicIndexListTest, ShrinkWithoutItems) ASSERT_FALSE(root->hasEvent()); } -TEST_F(DynamicIndexListTest, NewDataCanArriveDuringPageTransitions) +TEST_F(DynamicIndexListLazyTest, NewDataCanArriveDuringPageTransitions) { auto swipeToNextPage = [&]() { root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(150, 10))); @@ -4116,175 +2688,3 @@ TEST_F(DynamicIndexListTest, NewDataCanArriveDuringPageTransitions) // Some errors are expected from unfulfilled requests ASSERT_TRUE(ds->getPendingErrors().size() > 0); } - -TEST_F(DynamicIndexListTest, CurrentOrTargetPageCanBeDeleted) -{ - auto swipeToNextPage = [&]() { - root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(150, 10))); - advanceTime(100); - root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(50, 10))); - root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(50, 10))); - root->clearPending(); - }; - - int listVersion = 0; - auto createInsertItem = [&](int index, std::string text = "") { - if (text.empty()) text = std::to_string(index); - return "{" - " \"presentationToken\": \"presentationToken\"," - " \"listId\": \"vQdpOESlok\"," - " \"listVersion\": " + std::to_string(++listVersion) + "," - " \"operations\": [" - " {" - " \"type\": \"InsertItem\"," - " \"index\": " + std::to_string(index) + "," - " \"item\": { \"color\": \"green\", \"text\": \"" + text + "\" }" + - " }" - " ]" - "}"; - }; - - loadDocument(BASIC_PAGER, EMPTY_PAGER_DATA); - - // Insert a few items - for (int i = 10; i <= 15; i++) { - ASSERT_TRUE(ds->processUpdate(createInsertItem(i))); - } - root->clearPending(); - - // We start on the first page - ASSERT_TRUE(CheckPager(0, {{"frame-10"}, {"frame-11"}, {"frame-12"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); - - // Swipe! But before you reach the next page, delete it - swipeToNextPage(); - ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 11))); - advanceTime(1000); - - // We remain on the first page - ASSERT_TRUE(CheckPager(0, {{"frame-10"}, {"frame-12"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); - - // Swipe! Now we reach the next page - swipeToNextPage(); - advanceTime(1000); - ASSERT_TRUE(CheckPager(1, {{"frame-10"}, {"frame-12"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); - - // Swipe! Now delete the source page, but the swipe still succeeds in moving to the next page - swipeToNextPage(); - ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 11))); - advanceTime(1000); - ASSERT_TRUE(CheckPager(1, {{"frame-10"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); - - // Swipe! Again, delete target page, but also try to jump to page 3 - swipeToNextPage(); - advanceTime(10); - // The animation has progressed a bit - ASSERT_TRUE(CheckPager(1, {{"frame-10"}, {"frame-13", true}, {"frame-14", true}, {"frame-15"}})); - // Now delete the target page - ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 12))); - // And also manually try to go to page 3 - PagerComponent::setPageUtil(context, component, 3, kPageDirectionForward, ActionRef(nullptr)); - advanceTime(1000); - // We succeed in reaching what was formally page 3 (now page 2) - ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15"}})); - - // Need to insert a couple of items - ASSERT_TRUE(ds->processUpdate(createInsertItem(13, "88"))); - ASSERT_TRUE(ds->processUpdate(createInsertItem(14, "99"))); - root->clearPending(); - ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15"}, {"frame-88"}, {"frame-99"}})); - - // Swipe! This time, delete the source page and jump to page 4 - swipeToNextPage(); - advanceTime(10); - // The animation has progressed a bit - ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15", true}, {"frame-88", true}, {"frame-99"}})); - // Now delete the source page - ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 12))); - // And also manually try to go to the last page - PagerComponent::setPageUtil(context, component, 4, kPageDirectionForward, ActionRef(nullptr)); - advanceTime(1000); - // We succeed in reaching the last page - ASSERT_TRUE(CheckPager(3, {{"frame-10"}, {"frame-13"}, {"frame-88"}, {"frame-99"}})); - - // Some errors are expected from unfulfilled requests - ASSERT_TRUE(ds->getPendingErrors().size() > 0); -} - -static const char *DYNAMIC_DATA = R"({ - "dynamicSource": { - "type": "dynamicIndexList", - "listId": "vQdpOESlok", - "startIndex": 0, - "minimumInclusiveIndex": 0, - "maximumExclusiveIndex": 2, - "items": [ 11 ] - } -})"; - -static const char *DYNAMIC_DATA_SEQUENCE = R"({ - "type": "APL", - "version": "1.8", - "mainTemplate": { - "parameters": [ - "dynamicSource" - ], - "items": { - "type": "Sequence", - "numbered": true, - "id": "testSequence", - "width": "100%", - "height": "100%", - "data": "${dynamicSource}", - "items": [ - { - "type": "Container", - "items": [ - { - "type": "Text", - "id": "${data}", - "width": "100dp", - "textAlign": "center", - "maxLines": 1, - "text": "${ordinal}", - "position": "absolute" - } - ] - } - ] - } - } -})"; - -TEST_F(DynamicIndexListTest, VisualHashRecalculatedOnDynamicDataUpdate) { - auto spyTextMeasure = std::make_shared(); - config->measure(spyTextMeasure); - - loadDocument(DYNAMIC_DATA_SEQUENCE, DYNAMIC_DATA); - ASSERT_TRUE(component); - ASSERT_EQ(1, component->getChildCount()); - ASSERT_TRUE(CheckChildLaidOut(component, 0, true)); - ASSERT_EQ(1, spyTextMeasure->visualHashes.size()); - - auto previousTopItem = component->findComponentById("11"); - auto previousTopItemOldVisualHash = previousTopItem->getCalculated(kPropertyVisualHash).asString(); - ASSERT_EQ(previousTopItemOldVisualHash, spyTextMeasure->visualHashes.at(0).asString()); - - spyTextMeasure->visualHashes.clear(); - // Insert item on top - ASSERT_TRUE(ds->processUpdate(createMultiInsert(1, 0, {10}))); - root->clearPending(); - - ASSERT_EQ(2, component->getChildCount()); - ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); - ASSERT_EQ(2, spyTextMeasure->visualHashes.size()); - - auto currentTopItem = component->findComponentById("10");; - auto currentTopItemVisualHash = currentTopItem->getCalculated(kPropertyVisualHash).asString(); - ASSERT_EQ(currentTopItemVisualHash, spyTextMeasure->visualHashes.at(0).asString()); - - auto previousTopItemNewVisualHash = previousTopItem->getCalculated(kPropertyVisualHash).asString(); - // Check visual hash changed for prev top item - ASSERT_NE(previousTopItemOldVisualHash, previousTopItemNewVisualHash); - // Check current visual hash for current top and second item are different - ASSERT_NE(currentTopItemVisualHash, previousTopItemNewVisualHash); -} diff --git a/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp b/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp new file mode 100644 index 0000000..29ca7f7 --- /dev/null +++ b/aplcore/unit/datasource/unittest_dynamicindexlistupdate.cpp @@ -0,0 +1,1434 @@ +/** + * Copyright 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 "../testeventloop.h" + +#include "apl/component/pagercomponent.h" +#include "./dynamicindexlisttest.h" + +#include "apl/dynamicdata.h" + +using namespace apl; + +class DynamicIndexListUpdateTest : public DynamicIndexListTest {}; + +static const char *RESTRICTED_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 10, + "minimumInclusiveIndex": 10, + "maximumExclusiveIndex": 15, + "items": [ 10, 11, 12, 13, 14 ] + } +})"; + +static const char *BASIC = R"({ + "type": "APL", + "version": "1.3", + "theme": "dark", + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Sequence", + "id": "sequence", + "height": 300, + "data": "${dynamicSource}", + "items": { + "type": "Text", + "id": "id${data}", + "width": 100, + "height": 100, + "text": "${data}" + } + } + } +})"; + +static const char *SHRINKABLE_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 10, + "minimumInclusiveIndex": 10, + "maximumExclusiveIndex": 15, + "items": [ 10, 11, 12, 13, 14, 15, 16, 17, 18, 19 ] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, ShrinkData) +{ + loadDocument(BASIC, SHRINKABLE_DATA); + advanceTime(10); + ASSERT_TRUE(CheckBounds(10, 15)); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 4}, true)); +} + +static const char *BASIC_CRUD_SERIES = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1, + "operations": [ + { + "type": "InsertListItem", + "index": 11, + "item": 111 + }, + { + "type": "ReplaceListItem", + "index": 13, + "item": 113 + }, + { + "type": "DeleteListItem", + "index": 12 + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudBasicSeries) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckBounds(10, 15)); + + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + + ASSERT_TRUE(ds->processUpdate(BASIC_CRUD_SERIES)); + root->clearPending(); + + ASSERT_TRUE(CheckChildren({10, 111, 113, 13, 14})); +} + +static const char *BROKEN_CRUD_SERIES = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1, + "operations": [ + { + "type": "InsertListItem", + "index": 11, + "item": 111 + }, + { + "type": "InsertListItem", + "index": 27, + "item": 27 + }, + { + "type": "ReplaceListItem", + "index": 13, + "item": 113 + }, + { + "type": "DeleteListItem", + "index": 27, + "item": 27 + }, + { + "type": "DeleteListItem", + "index": 12 + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudInvalidInbetweenSeries) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckBounds(10, 15)); + + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + + ASSERT_FALSE(ds->processUpdate(BROKEN_CRUD_SERIES)); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + root->clearPending(); + + ASSERT_TRUE(CheckChildren({10, 111, 11, 12, 13, 14})); +} + +static const char *STARTING_BOUNDS_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": -5, + "minimumInclusiveIndex": -5, + "maximumExclusiveIndex": 5, + "items": [ -5, -4, -3, -2, -1, 0, 1, 2, 3, 4 ] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudBoundsVerification) +{ + loadDocument(BASIC, STARTING_BOUNDS_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Negative insert + ASSERT_TRUE(ds->processUpdate(createInsert(1, -3, -103))); + root->clearPending(); + ASSERT_EQ(11, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 6)); + ASSERT_TRUE(CheckChildren({-5, -4, -103, -3, -2, -1, 0, 1, 2, 3, 4})); + + // Positive insert + ASSERT_TRUE(ds->processUpdate(createInsert(2, 3, 103))); + root->clearPending(); + ASSERT_EQ(12, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 7)); + ASSERT_TRUE(CheckChildren({-5, -4, -103, -3, -2, -1, 0, 1, 103, 2, 3, 4})); + + // Insert on 0 + ASSERT_TRUE(ds->processUpdate(createInsert(3, 0, 100))); + root->clearPending(); + ASSERT_EQ(13, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 8)); + ASSERT_TRUE(CheckChildren({-5, -4, -103, -3, -2, 100, -1, 0, 1, 103, 2, 3, 4})); + + // Negative delete + ASSERT_TRUE(ds->processUpdate(createDelete(4, -5))); + root->clearPending(); + ASSERT_EQ(12, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 7)); + ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, 100, -1, 0, 1, 103, 2, 3, 4})); + + // Positive delete + ASSERT_TRUE(ds->processUpdate(createDelete(5, 3))); + root->clearPending(); + ASSERT_EQ(11, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 6)); + ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, 100, -1, 0, 1, 2, 3, 4})); + + // Delete on 0 + ASSERT_TRUE(ds->processUpdate(createDelete(6, 0))); + root->clearPending(); + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 5)); + ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, 100, 0, 1, 2, 3, 4})); +} + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadGap) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Insert with gap + ASSERT_FALSE(ds->processUpdate(createInsert(1, 17, 17))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadInsertOOB) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Insert out of bounds + ASSERT_FALSE(ds->processUpdate(createInsert(1, 21, 21))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadRemoveOOB) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Remove out of bounds + ASSERT_FALSE(ds->processUpdate(createDelete(1, 21))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadReplaceOOB) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Replace out of bounds + ASSERT_FALSE(ds->processUpdate(createReplace(1, 21, 1000))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +static const char *WRONG_TYPE_CRUD = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1, + "operations": [ + { + "type": "7", + "index": 10, + "item": 101 + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadInvalidOperation) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Specify wrong operation + ASSERT_FALSE(ds->processUpdate(WRONG_TYPE_CRUD)); + ASSERT_TRUE(CheckErrors({ "INVALID_OPERATION" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +static const char *MALFORMED_OPERATION_CRUD = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1, + "operations": [ + { + "type": "InsertItem", + "item": 101 + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadMalformedOperation) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Specify wrong operation + ASSERT_FALSE(ds->processUpdate(MALFORMED_OPERATION_CRUD)); + ASSERT_TRUE(CheckErrors({ "INVALID_OPERATION" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +static const char *MISSING_OPERATIONS_CRUD = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1 +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadNoOperation) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + // Don't specify any operations + ASSERT_FALSE(ds->processUpdate(MISSING_OPERATIONS_CRUD)); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +static const char *MISSING_LIST_VERSION_CRUD = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "operations": [ + { + "type": "InsertItem", + "index": 10, + "item": 101 + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudPayloadNoListVersion) +{ + loadDocument(BASIC, RESTRICTED_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); + ASSERT_TRUE(CheckBounds(10, 15)); + + ASSERT_FALSE(ds->processUpdate(MISSING_LIST_VERSION_CRUD)); + ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION_IN_SEND_DATA" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiInsert) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Negative insert + ASSERT_TRUE(ds->processUpdate(createMultiInsert(1, -3, {-31, -32}))); + root->clearPending(); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -31, -32, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 7)); + + // Positive insert + ASSERT_TRUE(ds->processUpdate(createMultiInsert(2, 3, {31, 32}))); + root->clearPending(); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -31, -32, -2, -1, 0, 31, 32, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 9)); + + // Above loaded adjust insert + ASSERT_TRUE(ds->processUpdate(createMultiInsert(3, 9, {71, 72}))); + root->clearPending(); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -31, -32, -2, -1, 0, 31, 32, 1, 2, 3, 4, 71, 72})); + ASSERT_TRUE(CheckBounds(-5, 11)); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiInsertAbove) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Attach at the end + ASSERT_FALSE(ds->processUpdate(createMultiInsert(1, 10, {100, 101}))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiInsertBelow) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Below loaded insert + ASSERT_FALSE(ds->processUpdate(createMultiInsert(1, -10, {-100, -101}))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +static const char *NON_ARRAY_MULTI_INSERT = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1, + "operations": [ + { + "type": "InsertMultipleItems", + "index": 11, + "items": 111 + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudMultiInsertNonArray) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Below loaded insert + ASSERT_FALSE(ds->processUpdate(NON_ARRAY_MULTI_INSERT)); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiDelete) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Remove across + ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, -1, 3))); + root->clearPending(); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 2)); + + // Delete negative + ASSERT_TRUE(ds->processUpdate(createMultiDelete(2, -5, 2))); + root->clearPending(); + ASSERT_TRUE(CheckChildren({-3, -2, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 0)); + + // Delete at the end + ASSERT_TRUE(ds->processUpdate(createMultiDelete(3, -2, 2))); + root->clearPending(); + ASSERT_TRUE(CheckChildren({-3, -2, 2})); + ASSERT_TRUE(CheckBounds(-5, -2)); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiDeleteOOB) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Out of range + ASSERT_FALSE(ds->processUpdate(createMultiDelete(1, 7, 2))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiDeletePartialOOB) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Some out of range + ASSERT_FALSE(ds->processUpdate(createMultiDelete(1, 15, 3))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiDeleteAll) { + loadDocument(BASIC, STARTING_BOUNDS_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, -5, 10))); + root->clearPending(); + ASSERT_EQ(0, component->getChildCount()); +} + + +static const char *SINGULAR_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 0, + "minimumInclusiveIndex": -5, + "maximumExclusiveIndex": 5, + "items": [ 0 ] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudMultiDeleteMore) { + loadDocument(BASIC, SINGULAR_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(CheckChildren({0})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + // Some out of range + ASSERT_FALSE(ds->processUpdate(createMultiDelete(1, 15, 3))); + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); + + ASSERT_EQ(1, component->getChildCount()); +} + +TEST_F(DynamicIndexListUpdateTest, CrudMultiDeleteLast) { + loadDocument(BASIC, SINGULAR_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(CheckChildren({0})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, 0, 1))); + root->clearPending(); + ASSERT_EQ(0, component->getChildCount()); +} + +TEST_F(DynamicIndexListUpdateTest, CrudDeleteLast) { + loadDocument(BASIC, SINGULAR_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(CheckChildren({0})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(createDelete(1, 0))); + root->clearPending(); + ASSERT_EQ(0, component->getChildCount()); +} + +TEST_F(DynamicIndexListUpdateTest, CrudInsertAdjascent) { + loadDocument(BASIC, SINGULAR_DATA); + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(CheckChildren({0})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(createInsert(1, 1, 1))); // This allowed (N+1) + ASSERT_TRUE(ds->processUpdate(createInsert(2, 0, 11))); // This is also allowed (M) + ASSERT_FALSE(ds->processUpdate(createInsert(3, -1, -1))); // This is not (M-1) + ASSERT_TRUE(CheckErrors({ "LIST_INDEX_OUT_OF_RANGE" })); + root->clearPending(); + + ASSERT_TRUE(CheckChildren({11, 0, 1})); + ASSERT_TRUE(CheckBounds(-5, 7)); + ASSERT_EQ(3, component->getChildCount()); +} + +static const char *LAZY_CRUD_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": -2, + "minimumInclusiveIndex": -5, + "maximumExclusiveIndex": 5, + "items": [ -2, -1, 0, 1, 2 ] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudLazyCombination) +{ + loadDocument(BASIC, LAZY_CRUD_DATA); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 3, "3, 4" ))); + ASSERT_TRUE(ds->processUpdate(createLazyLoad(2, 102, -5, "-5, -4, -3" ))); + root->clearPending(); + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + + ASSERT_TRUE(ds->processUpdate(createInsert(3, -2, -103))); + root->clearPending(); + ASSERT_EQ(11, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 6)); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -103, -2, -1, 0, 1, 2, 3, 4})); + + ASSERT_TRUE(ds->processUpdate(createInsert(4, 4, 103))); + root->clearPending(); + ASSERT_EQ(12, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 7)); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -103, -2, -1, 0, 1, 2, 103, 3, 4})); + + +} + +static const char *LAZY_WITHOUT_VERSION = R"({ + "token": "presentationToken", + "listId": "vQdpOESlok", + "correlationToken": "102", + "startIndex": -5, + "items": [ -5, -4, -3 ] +})"; + +TEST_F(DynamicIndexListUpdateTest, CrudAfterNoVersionLazy) +{ + loadDocument(BASIC, LAZY_CRUD_DATA); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(LAZY_WITHOUT_VERSION)); + root->clearPending(); + + ASSERT_EQ(8, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2})); + + ASSERT_FALSE(ds->processUpdate(createInsert(1, 0, 101))); + ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION_IN_SEND_DATA" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudBeforeNoVersionLazy) +{ + loadDocument(BASIC, LAZY_CRUD_DATA); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_TRUE(ds->processUpdate(createInsert(1, 0, 101))); + root->clearPending(); + + ASSERT_EQ(6, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-2, -1, 101, 0, 1, 2})); + + ASSERT_FALSE(ds->processUpdate(LAZY_WITHOUT_VERSION)); + ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION_IN_SEND_DATA" })); + + // In fail state so will not allow other operation + ASSERT_FALSE(ds->processUpdate(createInsert(2, 10, 100))); + ASSERT_TRUE(CheckErrors({ "INTERNAL_ERROR" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudWrongData) +{ + loadDocument(BASIC, LAZY_CRUD_DATA); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 3, 2)); + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", -5, 3)); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(5, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-2, -1, 0, 1, 2})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + + ASSERT_TRUE(ds->processUpdate(createInsert(1, -2, -103))); + root->clearPending(); + ASSERT_EQ(6, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 6)); + ASSERT_TRUE(CheckChildren({-103, -2, -1, 0, 1, 2})); + + ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 4, 2)); + + // Wrong version crud will not fly + ASSERT_FALSE(ds->processUpdate(createInsert(3, 0, 100))); // This is cached + ASSERT_FALSE(ds->processUpdate(createInsert(1, 0, 100))); // This is not + ASSERT_TRUE(CheckErrors({ "DUPLICATE_LIST_VERSION" })); +} + +TEST_F(DynamicIndexListUpdateTest, CrudOutOfOrder) +{ + loadDocument(BASIC, STARTING_BOUNDS_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_FALSE(ds->processUpdate(createInsert(2, 4, 103))); + ASSERT_FALSE(ds->processUpdate(createInsert(3, 2, 100))); + ASSERT_FALSE(ds->processUpdate(createDelete(5, 5))); + + // Duplicate version in cache + ASSERT_FALSE(ds->processUpdate(createDelete(5, 5))); + ASSERT_TRUE(CheckErrors({ "DUPLICATE_LIST_VERSION" })); + + ASSERT_TRUE(ds->processUpdate(createInsert(1, -3, -103))); + ASSERT_TRUE(ds->processUpdate(createDelete(4, -5))); + + ASSERT_TRUE(ds->processUpdate(createDelete(6, 2))); + root->clearPending(); + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckBounds(-5, 5)); + ASSERT_TRUE(CheckChildren({-4, -103, -3, -2, -1, 0, 100, 2, 103, 4})); +} + +TEST_F(DynamicIndexListUpdateTest, CrudBadOutOfOrder) +{ + loadDocument(BASIC, STARTING_BOUNDS_DATA); + + ASSERT_EQ(kComponentTypeSequence, component->getType()); + + ASSERT_EQ(10, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 0, 1, 2, 3, 4})); + ASSERT_TRUE(CheckBounds(-5, 5)); + + ASSERT_FALSE(ds->processUpdate(createInsert(6, 0, 7))); + loop->advanceToTime(500); + + // Update 6 will expire + ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION" })); + + ASSERT_FALSE(ds->processUpdate(createInsert(5, 0, 6))); + ASSERT_FALSE(ds->processUpdate(createInsert(4, 0, 5))); + ASSERT_FALSE(ds->processUpdate(createInsert(2, 0, 3))); + ASSERT_FALSE(ds->processUpdate(createInsert(7, 0, 8))); + ASSERT_FALSE(ds->processUpdate(createInsert(3, 0, 4))); + ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION" })); + ASSERT_FALSE(ds->processUpdate(createInsert(8, 0, 9))); + ASSERT_TRUE(CheckErrors({ "MISSING_LIST_VERSION" })); + + ASSERT_TRUE(ds->processUpdate(createInsert(1, 0, 2))); + loop->advanceToEnd(); + ASSERT_TRUE(CheckErrors({})); + + root->clearPending(); + ASSERT_EQ(16, component->getChildCount()); + ASSERT_TRUE(CheckChildren({-5, -4, -3, -2, -1, 7, 6, 5, 4, 3, 2, 0, 1, 2, 3, 4})); +} + +static const char *BASIC_PAGER = R"({ + "type": "APL", + "version": "1.2", + "theme": "light", + "layouts": { + "square": { + "parameters": ["color", "text"], + "item": { + "type": "Frame", + "width": 200, + "height": 200, + "id": "frame-${text}", + "backgroundColor": "${color}", + "item": { + "type": "Text", + "text": "${text}", + "color": "black", + "width": 200, + "height": 200 + } + } + } + }, + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Pager", + "id": "pager", + "data": "${dynamicSource}", + "width": "100%", + "height": "100%", + "navigation": "normal", + "items": { + "type": "square", + "index": "${index}", + "color": "${data.color}", + "text": "${data.text}" + } + } + } +})"; + +static const char *SWIPE_TO_DELETE = R"({ + "type": "APL", + "version": "1.4", + "theme": "dark", + "layouts": { + "swipeAway" : { + "parameters": ["text1", "text2"], + "item": { + "type": "TouchWrapper", + "width": 200, + "item": { + "type": "Frame", + "backgroundColor": "blue", + "height": 100, + "items": { + "type": "Text", + "text": "${text1}", + "fontSize": 60 + } + }, + "gestures": [ + { + "type": "SwipeAway", + "direction": "left", + "action":"reveal", + "items": { + "type": "Frame", + "backgroundColor": "purple", + "width": "100%", + "items": { + "type": "Text", + "text": "${text2}", + "fontSize": 60, + "color": "white" + } + }, + "onSwipeDone": { + "type": "SendEvent", + "arguments": ["${event.source.uid}", "${index}"] + } + } + ] + } + } + }, + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "items": [ + { + "type": "Sequence", + "width": "100%", + "height": 500, + "alignItems": "center", + "justifyContent": "spaceAround", + "data": "${dynamicSource}", + "items": [ + { + "type": "swipeAway", + "text1": "${data}", + "text2": "${data}" + } + ] + } + ] + } +})"; + +static const char *SWIPE_TO_DELETE_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 5, + "items": [ 1, 2, 3, 4, 5 ] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, SwipeToDelete) +{ + config->set({ + {RootProperty::kSwipeAwayAnimationEasing, "linear"}, + {RootProperty::kPointerSlopThreshold, 5}, + {RootProperty::kSwipeVelocityThreshold, 5}, + {RootProperty::kTapOrScrollTimeout, 10}, + {RootProperty::kPointerInactivityTimeout, 1000} + }); + loadDocument(SWIPE_TO_DELETE, SWIPE_TO_DELETE_DATA); + advanceTime(10); + + ASSERT_TRUE(component); + ASSERT_EQ(5, component->getChildCount()); + ASSERT_EQ(5, component->getDisplayedChildCount()); + + auto idToDelete = component->getChildAt(0)->getUniqueId(); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); + + advanceTime(800); + auto event = root->popEvent(); + ASSERT_EQ(kEventTypeSendEvent, event.getType()); + auto deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); + int indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); + ASSERT_EQ(idToDelete, deletedId); + ASSERT_EQ(0, indexToDelete); + + ASSERT_TRUE(ds->processUpdate(createDelete(1, indexToDelete))); + advanceTime(100); + ASSERT_EQ(4, component->getChildCount()); + ASSERT_EQ(4, component->getDisplayedChildCount()); + ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBounds, kPropertyNotifyChildrenChanged, kPropertyVisualHash)); + root->clearDirty(); + + + // Repeat for very first + idToDelete = component->getChildAt(0)->getUniqueId(); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); + + advanceTime(800); + event = root->popEvent(); + ASSERT_EQ(kEventTypeSendEvent, event.getType()); + deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); + indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); + ASSERT_EQ(idToDelete, deletedId); + ASSERT_EQ(0, indexToDelete); + root->clearDirty(); + + ASSERT_TRUE(ds->processUpdate(createDelete(2, indexToDelete))); + root->clearPending(); + ASSERT_EQ(3, component->getChildCount()); + ASSERT_EQ(3, component->getDisplayedChildCount()); + ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBounds,kPropertyNotifyChildrenChanged, kPropertyVisualHash)); + root->clearDirty(); + + + // Remove one at the end + idToDelete = component->getChildAt(2)->getUniqueId(); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,201), false)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,201), true)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,201), true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,201), true)); + + advanceTime(800); + event = root->popEvent(); + ASSERT_EQ(kEventTypeSendEvent, event.getType()); + deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); + indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); + ASSERT_EQ(idToDelete, deletedId); + ASSERT_EQ(2, indexToDelete); + root->clearDirty(); + + ASSERT_TRUE(ds->processUpdate(createDelete(3, indexToDelete))); + root->clearPending(); + root->clearDirty(); + + ASSERT_EQ(2, component->getChildCount()); + ASSERT_EQ(2, component->getDisplayedChildCount()); + + // again + idToDelete = component->getChildAt(0)->getUniqueId(); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); + + advanceTime(800); + event = root->popEvent(); + ASSERT_EQ(kEventTypeSendEvent, event.getType()); + deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); + indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); + ASSERT_EQ(idToDelete, deletedId); + ASSERT_EQ(0, indexToDelete); + root->clearDirty(); + + ASSERT_TRUE(ds->processUpdate(createDelete(4, indexToDelete))); + root->clearPending(); + ASSERT_EQ(1, component->getChildCount()); + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyBounds,kPropertyNotifyChildrenChanged, kPropertyVisualHash)); + root->clearDirty(); + + // empty the list + idToDelete = component->getChildAt(0)->getUniqueId(); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(200,1), false)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(190,1), true)); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(140,1), true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(140,1), true)); + + advanceTime(800); + event = root->popEvent(); + ASSERT_EQ(kEventTypeSendEvent, event.getType()); + deletedId = event.getValue(kEventPropertyArguments).getArray().at(0).asString(); + indexToDelete = event.getValue(kEventPropertyArguments).getArray().at(1).asNumber(); + ASSERT_EQ(idToDelete, deletedId); + ASSERT_EQ(0, indexToDelete); + root->clearDirty(); + + ASSERT_TRUE(ds->processUpdate(createDelete(5, indexToDelete))); + root->clearPending(); + ASSERT_EQ(0, component->getChildCount()); + ASSERT_EQ(0, component->getDisplayedChildCount()); + root->clearDirty(); +} + +static const char *SEQUENCE_RECREATE_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 1, + "items": [ + { "label": "I am a label.", "sequence": ["red", "green", "blue", "yellow", "purple"] } + ] + } +})"; + +static const char *SEQUENCE_RECREATE = R"({ + "type": "APL", + "version": "1.7", + "theme": "dark", + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "item": { + "type": "Container", + "height": 300, + "width": 300, + "data": "${dynamicSource}", + "items": { + "type": "Container", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Sequence", + "height": "50%", + "width": "100%", + "data": "${data.sequence}", + "items": { + "type": "Frame", + "backgroundColor": "${data}", + "height": 10, + "width": "100%" + } + } + ] + } + } + } +})"; + +static const char *REPLACE_SEQUENCE_CRUD = R"({ + "presentationToken": "presentationToken", + "listId": "vQdpOESlok", + "listVersion": 1, + "operations": [ + { + "type": "DeleteListItem", + "index": 0 + }, + { + "type": "InsertListItem", + "index": 0, + "item": { "sequence": ["purple", "yellow", "blue", "green", "red"] } + } + ] +})"; + +TEST_F(DynamicIndexListUpdateTest, SequenceRecreate) +{ + loadDocument(SEQUENCE_RECREATE, SEQUENCE_RECREATE_DATA); + advanceTime(10); + + ASSERT_EQ(1, component->getChildCount()); + auto sequence = component->getCoreChildAt(0)->getCoreChildAt(0); + ASSERT_EQ(5, sequence->getChildCount()); + + ASSERT_EQ(Rect(0, 0, 300, 300), component->getCoreChildAt(0)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 0, 300, 150), sequence->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 0, 300, 10), sequence->getCoreChildAt(0)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 10, 300, 10), sequence->getCoreChildAt(1)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 20, 300, 10), sequence->getCoreChildAt(2)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 30, 300, 10), sequence->getCoreChildAt(3)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 40, 300, 10), sequence->getCoreChildAt(4)->getCalculated(kPropertyBounds).get()); + + ASSERT_TRUE(ds->processUpdate(REPLACE_SEQUENCE_CRUD)); + root->clearPending(); + + sequence = component->getCoreChildAt(0)->getCoreChildAt(0); + ASSERT_EQ(5, sequence->getChildCount()); + + ASSERT_EQ(Rect(0, 0, 300, 300).toDebugString(), component->getCoreChildAt(0)->getCalculated(kPropertyBounds).get().toDebugString()); + ASSERT_EQ(Rect(0, 0, 300, 150), sequence->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 0, 300, 10), sequence->getCoreChildAt(0)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 10, 300, 10), sequence->getCoreChildAt(1)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 20, 300, 10), sequence->getCoreChildAt(2)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 30, 300, 10), sequence->getCoreChildAt(3)->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(Rect(0, 40, 300, 10), sequence->getCoreChildAt(4)->getCalculated(kPropertyBounds).get()); +} + +static const char *FILLED_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 5, + "items": [ 0, 1, 2, 3, 4 ] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, DeleteMultipleAll) +{ + loadDocument(BASIC, FILLED_DATA); + advanceTime(10); + + ASSERT_TRUE(CheckBounds(0, 5)); + ASSERT_EQ(5, component->getChildCount()); + + ASSERT_FALSE(root->hasEvent()); + + ASSERT_TRUE(ds->processUpdate(createMultiDelete(1, 0, 100))); + root->clearPending(); + + ASSERT_EQ(0, component->getChildCount()); +} + +static const char *EMPTY_PAGER_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 10, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 20, + "items": [] + } +})"; + +TEST_F(DynamicIndexListUpdateTest, CurrentOrTargetPageCanBeDeleted) +{ + auto swipeToNextPage = [&]() { + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(150, 10))); + advanceTime(100); + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(50, 10))); + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(50, 10))); + root->clearPending(); + }; + + int listVersion = 0; + auto createInsertItem = [&](int index, std::string text = "") { + if (text.empty()) text = std::to_string(index); + return "{" + " \"presentationToken\": \"presentationToken\"," + " \"listId\": \"vQdpOESlok\"," + " \"listVersion\": " + std::to_string(++listVersion) + "," + " \"operations\": [" + " {" + " \"type\": \"InsertItem\"," + " \"index\": " + std::to_string(index) + "," + " \"item\": { \"color\": \"green\", \"text\": \"" + text + "\" }" + + " }" + " ]" + "}"; + }; + + loadDocument(BASIC_PAGER, EMPTY_PAGER_DATA); + + // Insert a few items + for (int i = 10; i <= 15; i++) { + ASSERT_TRUE(ds->processUpdate(createInsertItem(i))); + } + root->clearPending(); + + // We start on the first page + ASSERT_TRUE(CheckPager(0, {{"frame-10"}, {"frame-11"}, {"frame-12"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); + + // Swipe! But before you reach the next page, delete it + swipeToNextPage(); + ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 11))); + advanceTime(1000); + + // We remain on the first page + ASSERT_TRUE(CheckPager(0, {{"frame-10"}, {"frame-12"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); + + // Swipe! Now we reach the next page + swipeToNextPage(); + advanceTime(1000); + ASSERT_TRUE(CheckPager(1, {{"frame-10"}, {"frame-12"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); + + // Swipe! Now delete the source page, but the swipe still succeeds in moving to the next page + swipeToNextPage(); + ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 11))); + advanceTime(1000); + ASSERT_TRUE(CheckPager(1, {{"frame-10"}, {"frame-13"}, {"frame-14"}, {"frame-15"}})); + + // Swipe! Again, delete target page, but also try to jump to page 3 + swipeToNextPage(); + advanceTime(10); + // The animation has progressed a bit + ASSERT_TRUE(CheckPager(1, {{"frame-10"}, {"frame-13", true}, {"frame-14", true}, {"frame-15"}})); + // Now delete the target page + ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 12))); + // And also manually try to go to page 3 + PagerComponent::setPageUtil(context, component, 3, kPageDirectionForward, ActionRef(nullptr)); + advanceTime(1000); + // We succeed in reaching what was formally page 3 (now page 2) + ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15"}})); + + // Need to insert a couple of items + ASSERT_TRUE(ds->processUpdate(createInsertItem(13, "88"))); + ASSERT_TRUE(ds->processUpdate(createInsertItem(14, "99"))); + root->clearPending(); + ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15"}, {"frame-88"}, {"frame-99"}})); + + // Swipe! This time, delete the source page and jump to page 4 + swipeToNextPage(); + advanceTime(10); + // The animation has progressed a bit + ASSERT_TRUE(CheckPager(2, {{"frame-10"}, {"frame-13"}, {"frame-15", true}, {"frame-88", true}, {"frame-99"}})); + // Now delete the source page + ASSERT_TRUE(ds->processUpdate(createDelete(++listVersion, 12))); + // And also manually try to go to the last page + PagerComponent::setPageUtil(context, component, 4, kPageDirectionForward, ActionRef(nullptr)); + advanceTime(1000); + // We succeed in reaching the last page + ASSERT_TRUE(CheckPager(3, {{"frame-10"}, {"frame-13"}, {"frame-88"}, {"frame-99"}})); + + // Some errors are expected from unfulfilled requests + ASSERT_TRUE(ds->getPendingErrors().size() > 0); +} + +static const char *DYNAMIC_DATA = R"({ + "dynamicSource": { + "type": "dynamicIndexList", + "listId": "vQdpOESlok", + "startIndex": 0, + "minimumInclusiveIndex": 0, + "maximumExclusiveIndex": 2, + "items": [ 11 ] + } +})"; + +static const char *DYNAMIC_DATA_SEQUENCE = R"({ + "type": "APL", + "version": "1.8", + "mainTemplate": { + "parameters": [ + "dynamicSource" + ], + "items": { + "type": "Sequence", + "numbered": true, + "id": "testSequence", + "width": "100%", + "height": "100%", + "data": "${dynamicSource}", + "items": [ + { + "type": "Container", + "items": [ + { + "type": "Text", + "id": "${data}", + "width": "100dp", + "textAlign": "center", + "maxLines": 1, + "text": "${ordinal}", + "position": "absolute" + } + ] + } + ] + } + } +})"; + +TEST_F(DynamicIndexListUpdateTest, VisualHashRecalculatedOnDynamicDataUpdate) { + auto spyTextMeasure = std::make_shared(); + config->measure(spyTextMeasure); + + loadDocument(DYNAMIC_DATA_SEQUENCE, DYNAMIC_DATA); + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(CheckChildLaidOut(component, 0, true)); + ASSERT_EQ(1, spyTextMeasure->visualHashes.size()); + + auto previousTopItem = root->findComponentById("11"); + auto previousTopItemOldVisualHash = previousTopItem->getCalculated(kPropertyVisualHash).asString(); + ASSERT_EQ(previousTopItemOldVisualHash, spyTextMeasure->visualHashes.at(0).asString()); + + // Insert item on top + ASSERT_TRUE(ds->processUpdate(createMultiInsert(1, 0, {10}))); + root->clearPending(); + + ASSERT_EQ(2, component->getChildCount()); + ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 1}, true)); + // Same hash reused, as it should + ASSERT_EQ(3, spyTextMeasure->visualHashes.size()); + + auto currentTopItem = root->findComponentById("10"); + auto currentTopItemVisualHash = currentTopItem->getCalculated(kPropertyVisualHash).asString(); + ASSERT_EQ(currentTopItemVisualHash, spyTextMeasure->visualHashes.at(0).asString()); + + auto previousTopItemNewVisualHash = previousTopItem->getCalculated(kPropertyVisualHash).asString(); + // Check visual hash changed for prev top item + ASSERT_NE(previousTopItemOldVisualHash, previousTopItemNewVisualHash); + // Check current visual hash for current top and second item are different + ASSERT_NE(currentTopItemVisualHash, previousTopItemNewVisualHash); +} diff --git a/unit/datasource/unittest_dynamictokenlist.cpp b/aplcore/unit/datasource/unittest_dynamictokenlist.cpp similarity index 99% rename from unit/datasource/unittest_dynamictokenlist.cpp rename to aplcore/unit/datasource/unittest_dynamictokenlist.cpp index 72a8214..20c7752 100644 --- a/unit/datasource/unittest_dynamictokenlist.cpp +++ b/aplcore/unit/datasource/unittest_dynamictokenlist.cpp @@ -1345,6 +1345,7 @@ TEST_F(DynamicTokenListTest, GarbageCollection) { // Kill RootContext and re-inflate. component = nullptr; context = nullptr; + rootDocument = nullptr; root = nullptr; loop = std::make_shared(); diff --git a/unit/debugtools.cpp b/aplcore/unit/debugtools.cpp similarity index 100% rename from unit/debugtools.cpp rename to aplcore/unit/debugtools.cpp diff --git a/unit/debugtools.h b/aplcore/unit/debugtools.h similarity index 100% rename from unit/debugtools.h rename to aplcore/unit/debugtools.h diff --git a/aplcore/unit/embed/CMakeLists.txt b/aplcore/unit/embed/CMakeLists.txt new file mode 100644 index 0000000..319866f --- /dev/null +++ b/aplcore/unit/embed/CMakeLists.txt @@ -0,0 +1,22 @@ +# Copyright 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. + +target_sources_local(unittest + PRIVATE + testdocumentmanager.cpp + unittest_documentcreate.cpp + unittest_embedded_extensions.cpp + unittest_embedded_lifecycle.cpp + unittest_embedded_reinflate.cpp + unittest_rootcontexttargeting.cpp + ) diff --git a/aplcore/unit/embed/testdocumentmanager.cpp b/aplcore/unit/embed/testdocumentmanager.cpp new file mode 100644 index 0000000..54cf1b9 --- /dev/null +++ b/aplcore/unit/embed/testdocumentmanager.cpp @@ -0,0 +1,107 @@ +/** +* Copyright 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 "testdocumentmanager.h" + +using namespace apl; + +void +TestDocumentManager::request(const std::weak_ptr& request, + EmbedRequestSuccessCallback success, + EmbedRequestFailureCallback error) +{ + requests.emplace_back(TestEmbedRequest{request.lock()->getUrlRequest().getUrl(), request, std::move(success), std::move(error)}); +} + +DocumentContextPtr +TestDocumentManager::succeed(const std::string &url, + const ContentPtr &content, + bool connectedVisualContext, + DocumentConfigPtr documentConfig, + bool cleanup) +{ + DocumentContextPtr documentContext; + + auto it = std::find_if(requests.begin(), requests.end(), [url](const TestEmbedRequest& request){ + return request.url == url; + }); + + + if (it != requests.end()) + { + auto ptr = it->request.lock(); + if (ptr != nullptr) { + documentContext = it->success({ptr, content, connectedVisualContext, documentConfig}); + } + + if (cleanup) { + resolvedRequests.push_back(it->request); + requests.erase(it); + } + } + + return documentContext; +} + +DocumentContextPtr +TestDocumentManager::succeed(const ContentPtr &content) +{ + DocumentContextPtr documentContext; + auto it = requests.begin(); + + if (it != requests.end()) { + documentContext = it->success({it->request.lock(), content, false}); + } + + if (documentContext) { + resolvedRequests.push_back(it->request); + requests.erase(it); + } + + return documentContext; +} + +const std::weak_ptr& +TestDocumentManager::get(const std::string &url) const +{ + static std::weak_ptr sMissingNo = std::weak_ptr(); + + auto it = std::find_if(requests.begin(), requests.end(), [url](const TestEmbedRequest& request){ + return request.url == url; + }); + return it != requests.end() + ? it->request + : sMissingNo; +} + +void +TestDocumentManager::fail(const std::string &url, const std::string &failure, bool cleanup) +{ + auto it = std::find_if(requests.begin(), requests.end(), [url](const TestEmbedRequest& request){ + return request.url == url; + }); + + if (it != requests.end()) + { + auto ptr = it->request.lock(); + if (ptr != nullptr) { + it->error({ptr, failure}); + } + if (cleanup) { + resolvedRequests.push_back(it->request); + requests.erase(it); + } + } +} diff --git a/aplcore/unit/embed/testdocumentmanager.h b/aplcore/unit/embed/testdocumentmanager.h new file mode 100644 index 0000000..6543620 --- /dev/null +++ b/aplcore/unit/embed/testdocumentmanager.h @@ -0,0 +1,93 @@ +/** +* Copyright 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 APL_TEST_DOCUMENT_MANAGER_H +#define APL_TEST_DOCUMENT_MANAGER_H + +#include +#include +#include + +#include "apl/common.h" +#include "apl/embed/documentmanager.h" + +namespace apl +{ + +class TestDocumentManager : public DocumentManager +{ +public: + struct TestEmbedRequest { + std::string url; + std::weak_ptr request; + EmbedRequestSuccessCallback success; + EmbedRequestFailureCallback error; + }; + + void request(const std::weak_ptr& request, + EmbedRequestSuccessCallback success, + EmbedRequestFailureCallback error) override; + + /** + * Invoke success callback with content for the request associated to url. By default, request + * state is preserved for repeated invocations with the same url; request state can be erased by + * setting cleanup to true. + * + * @param url The url identifying the request to succeed + * @param content The resolved content; may not be ready if host is to supply pending parameters + * @param cleanup if true then erase the request and callbacks for url post-processing + * @return DocumentContext + */ + DocumentContextPtr succeed(const std::string &url, + const ContentPtr &content, + bool sameOrigin = false, + DocumentConfigPtr documentConfig = nullptr, + bool cleanup = false); + + /** + * Invoke success callback with content for the request in the FIFO order. + * + * @return DocumentContext + */ + DocumentContextPtr succeed(const ContentPtr &content); + + /** + * Invoke fail callback with failure for the request associated to url. By default, request + * state is preserved for repeated invocations with the same url; request state can be erased by + * setting cleanup to true. + * + * @param url The url identifying the request to succeed + * @param failure describes the failure to resolve content for url + * @param cleanup iff true then erase the request and callbacks for url post-processing + */ + void fail(const std::string &url, const std::string &failure, bool cleanup = false); + + /** + * Get first request with matching url + */ + const std::weak_ptr& get(const std::string &url) const; + + const std::vector& getUnresolvedRequests() const { return requests; } + + int getResolvedRequestCount() const { return resolvedRequests.size(); } + +private: + std::vector requests; + std::vector> resolvedRequests; +}; + +} // namespace apl + +#endif // APL_TEST_DOCUMENT_MANAGER_H diff --git a/aplcore/unit/embed/unittest_documentcreate.cpp b/aplcore/unit/embed/unittest_documentcreate.cpp new file mode 100644 index 0000000..6471396 --- /dev/null +++ b/aplcore/unit/embed/unittest_documentcreate.cpp @@ -0,0 +1,387 @@ +/** +* Copyright 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 "../testeventloop.h" +#include "../audio/testaudioplayerfactory.h" +#include "../embed/testdocumentmanager.h" +#include "../media/testmediaplayerfactory.h" + +using namespace apl; + +class DocumentCreateTest : public DocumentWrapper { +public: + DocumentCreateTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + +protected: + std::shared_ptr documentManager; +}; + +static const char* DEFAULT_DOC = R"({ + "type": "APL", + "version": "2022.3", + "environment": { + "lang": "en-UK", + "layoutDirection": "RTL" + }, + "theme": "light", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "height": 125.0, + "width": 250.0, + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnLoadArtifact" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnFailArtifact" + } + } + ] + } + } + } +})"; + +static const char* EFFECTIVE_OVERRIDES_DOC = R"({ + "type": "APL", + "version": "2022.3", + "environment": { + "lang": "en-UK", + "layoutDirection": "LTR" + }, + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "environment": { + "allowOpenURL": false, + "disallowDialog": true, + "disallowEditText": true, + "disallowVideo": true, + "lang": "en-IN", + "layoutDirection": "RTL" + }, + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnLoadArtifact" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnFailArtifact" + } + } + ] + } + } + } +})"; + +static const char* INEFFECTIVE_OVERRIDES_DOC = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "environment": { + "allowOpenURL": true, + "disallowDialog": false, + "disallowEditText": false, + "disallowVideo": false + }, + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnLoadArtifact" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnFailArtifact" + } + } + ] + } + } + } +})"; + +static const char* EMBEDDED_DEFAULT = R"({ + "type": "APL", + "version": "2022.3", + "mainTemplate": { + "item": { + "type": "Container", + "id": "embeddedTop", + "items": [ + { + "type": "EditText", + "id": "embeddedEditText", + "onSubmit": [ + { + "type": "SendEvent" + } + ] + }, + { + "type": "Host", + "id": "nestedHost", + "source": "nestedEmbeddedUrl", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "embeddedTop", + "item": { + "type": "Text", + "id": "nestedHostOnLoadArtifact", + "value": "hostComponentOnLoad triggered" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "embeddedTop", + "item": { + "type": "Text", + "id": "nestedHostOnFailArtifact", + "value": "hostComponentOnFail triggered" + } + } + ] + } + ] + } + } +})"; + +TEST_F(DocumentCreateTest, TestEnvironmentCreationWithEffectiveOverrides) +{ + config->set(RootProperty::kAllowOpenUrl, true); + config->set(RootProperty::kDisallowDialog, false); + config->set(RootProperty::kDisallowEditText, false); + config->set(RootProperty::kDisallowVideo, false); + + loadDocument(EFFECTIVE_OVERRIDES_DOC); + + // Inflate and verify the embedded document + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + auto embeddedTop = CoreComponent::cast(CoreDocumentContext::cast(embeddedDoc)->topComponent()); + + auto embeddedConfig = embeddedTop->getRootConfig(); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kAllowOpenUrl), false); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowDialog), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowEditText), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowVideo), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kLang), "en-IN"); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kLayoutDirection), LayoutDirection::kLayoutDirectionRTL); +} + +TEST_F(DocumentCreateTest, TestEnvironmentCreationWithIneffectiveOverrides) +{ + config->set(RootProperty::kAllowOpenUrl, false); + config->set(RootProperty::kDisallowDialog, true); + config->set(RootProperty::kDisallowEditText, true); + config->set(RootProperty::kDisallowVideo, true); + + loadDocument(INEFFECTIVE_OVERRIDES_DOC); + + // Inflate and verify the embedded document + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + auto embeddedTop = CoreComponent::cast(CoreDocumentContext::cast(embeddedDoc)->topComponent()); + + auto embeddedConfig = embeddedTop->getRootConfig(); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kAllowOpenUrl), false); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowDialog), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowEditText), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowVideo), true); +} + +TEST_F(DocumentCreateTest, TestRootConfigCreation) +{ + auto dpi = Metrics::CORE_DPI; + auto mode = ViewportMode::kViewportModeHub; + auto shape = ScreenShape::RECTANGLE; + + metrics.dpi(dpi) + .mode(mode) + .shape(shape); + + auto audioPlayerFactory = std::make_shared(config->getTimeManager()); + config->audioPlayerFactory(audioPlayerFactory); + auto mediaPlayerFactory = std::make_shared(); + config->mediaPlayerFactory(mediaPlayerFactory); + config->set(RootProperty::kInitialDisplayState, DisplayState::kDisplayStateBackground); + config->set(RootProperty::kAgentName, "unittest"); + config->set(RootProperty::kAgentVersion, "90210"); + config->set(RootProperty::kAnimationQuality, "slow"); + config->set(RootProperty::kReportedVersion, "2023.1"); + config->set(RootProperty::kFontScale, 1.5); + config->set(RootProperty::kScreenMode, "high-contrast"); + config->set(RootProperty::kScreenReader, true); + config->set(RootProperty::kDoublePressTimeout, 350); + config->set(RootProperty::kLongPressTimeout, 450); + config->set(RootProperty::kMinimumFlingVelocity, 45); + config->set(RootProperty::kPressedDuration, 60); + config->set(RootProperty::kTapOrScrollTimeout, 99); + config->set(RootProperty::kMaximumTapVelocity, 555); + config->set(RootProperty::kAllowOpenUrl, true); + config->set(RootProperty::kDisallowDialog, false); + config->set(RootProperty::kDisallowEditText, false); + config->set(RootProperty::kDisallowVideo, false); + config->set(RootProperty::kUTCTime, 12345678); + config->set(RootProperty::kLocalTimeAdjustment, 4000); + + loadDocument(DEFAULT_DOC); + + // Inflate and verify the embedded document + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + auto embeddedTop = CoreComponent::cast(CoreDocumentContext::cast(embeddedDoc)->topComponent()); + + auto embeddedConfig = embeddedTop->getRootConfig(); + auto embeddedContext = embeddedTop->getContext(); + auto embeddedViewport = embeddedContext->opt("viewport"); + + // Not copied from host document + ASSERT_EQ(embeddedContext->opt("elapsedTime"), 0); + ASSERT_EQ(embeddedContext->opt("environment").get("reason"), "initial"); + + // Copied from host document + ASSERT_EQ(embeddedViewport.get("dpi"), dpi); + ASSERT_EQ(embeddedViewport.get("shape"), "rectangle"); + ASSERT_EQ(embeddedViewport.get("mode"), "hub"); + ASSERT_EQ(embeddedConfig.getDocumentManager(), documentManager); + ASSERT_EQ(embeddedConfig.getAudioPlayerFactory(), audioPlayerFactory); + ASSERT_EQ(embeddedConfig.getMediaPlayerFactory(), mediaPlayerFactory); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kUTCTime), 12345678); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kLocalTimeAdjustment), 4000); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kAgentName), "unittest"); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kAgentVersion), "90210"); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kFontScale), 1.5); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kScreenMode), RootConfig::ScreenMode::kScreenModeHighContrast); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kScreenReader), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kInitialDisplayState), DisplayState::kDisplayStateBackground); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kAnimationQuality), RootConfig::AnimationQuality::kAnimationQualitySlow); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kReportedVersion), "2023.1"); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDoublePressTimeout), 350); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kLongPressTimeout), 450); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kMinimumFlingVelocity), 45); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kPressedDuration), 60); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kTapOrScrollTimeout), 99); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kMaximumTapVelocity), 555); + + // TODO: verify extension ? + + // Can be overridden by the top-level document, so Metrics/RootConfig is not the authority + ASSERT_EQ(embeddedViewport.get("theme"), "light"); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kLang), "en-UK"); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kLayoutDirection), LayoutDirection::kLayoutDirectionRTL); + + // Set by the Host Component + ASSERT_EQ(embeddedViewport.get("height"), 125.0); + ASSERT_EQ(embeddedViewport.get("width"), 250.0); + + // Can be overridden by the Host Component, but aren't in this test + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kAllowOpenUrl), true); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowDialog), false); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowEditText), false); + ASSERT_EQ(embeddedConfig.getProperty(RootProperty::kDisallowVideo), false); +} + +TEST_F(DocumentCreateTest, TestEventManagerPassedThrough) +{ + loadDocument(DEFAULT_DOC); + + // Inflate and verify the embedded document + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + auto editText = std::static_pointer_cast(CoreDocumentContext::cast(embeddedDoc)->findComponentById("embeddedEditText")); + ASSERT_FALSE(editText->getRootConfig().getProperty(RootProperty::kDisallowEditText).asBoolean()); + + // Verifying the Event published via the embedded EditText implicitly verifies the embedded doc + // has the same EventManager as the host doc + ASSERT_FALSE(root->hasEvent()); + editText->update(kUpdateSubmit, 0.0); + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(event.getType(), kEventTypeSendEvent); + ASSERT_EQ(event.getValue(apl::kEventPropertySource).get("id"), editText->getId()); +} diff --git a/aplcore/unit/embed/unittest_embedded_extensions.cpp b/aplcore/unit/embed/unittest_embedded_extensions.cpp new file mode 100644 index 0000000..d95574f --- /dev/null +++ b/aplcore/unit/embed/unittest_embedded_extensions.cpp @@ -0,0 +1,353 @@ +/** +* Copyright 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. + */ + +#ifdef ALEXAEXTENSIONS + +#include + +#include "../testeventloop.h" +#include "../embed/testdocumentmanager.h" + +using namespace apl; +using namespace alexaext; + +class SimpleTestExtension final : public alexaext::ExtensionBase { +public: + explicit SimpleTestExtension(const std::set& uris) : ExtensionBase(uris) {}; + + rapidjson::Document createRegistration(const std::string& uri, const rapidjson::Value& registerRequest) override { + std::string schema = R"({ + "type": "Schema", + "version": "1.0", + "commands": [ + { + "name": "Test" + } + ] + })"; + rapidjson::Document doc; + doc.Parse(schema.c_str()); + doc.AddMember("uri", rapidjson::Value(uri.c_str(), doc.GetAllocator()), doc.GetAllocator()); + return alexaext::RegistrationSuccess("1.0").uri(uri).token("I_AM_A_TOKEN").schema(doc); + } + + bool invokeCommand(const std::string& uri, const rapidjson::Value& command) override { + commandTriggered = true; + return true; + } + + bool commandTriggered = false; +}; + +class EmbeddedExtensionsTest : public DocumentWrapper { +public: + EmbeddedExtensionsTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + + void createProvider() { + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + alexaext::Executor::getSynchronousExecutor()); + } + + void loadExtensions(const char* document) { + createContent(document, nullptr); + + if (!extensionProvider) { + createProvider(); + } + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ensureRequestedExtensions(content->getExtensionRequests()); + + // load them into config via the mediator + mediator->loadExtensions(config, content); + } + + void ensureRequestedExtensions(std::set requestedExtensions) { + // Create a test extension for every request unless it's been requested before + for (auto& req: requestedExtensions) { + if (testExtensions.count(req) > 0) continue; + auto ext = std::make_shared(std::set({req})); + auto proxy = std::make_shared(ext); + extensionProvider->registerExtension(proxy); + // save direct access to extension for test use + testExtensions.emplace(req, ext); + } + } + + void TearDown() override { + extensionProvider = nullptr; + mediator = nullptr; + testExtensions.clear(); + + DocumentWrapper::TearDown(); + } + + alexaext::ExtensionRegistrarPtr extensionProvider; // provider instance for tests + ExtensionMediatorPtr mediator; + std::map> testExtensions; // direct access to extensions for test + +protected: + std::shared_ptr documentManager; +}; + +static const char* HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "extension": [ + { + "uri": "aplext:hello:10", + "name": "Hello" + } + ], + "onMount": { + "type": "Hello:Test" + }, + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ], + "onFail": [ + { + "type": "InsertItem", + "sequencer": "SEND_EVENTER", + "arguments": ["FAILED"] + } + ] + } + } + } +})"; + +static const char* EMBEDDED_DOC_TRIES_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "onMount": { + "type": "Hello:Test" + }, + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + } + } +})"; + +TEST_F(EmbeddedExtensionsTest, NoHostExtensionsAccess) +{ + loadExtensions(HOST_DOC); + + // verify the extension was registered + ASSERT_TRUE(extensionProvider->hasExtension("aplext:hello:10")); + auto ext = extensionProvider->getExtension("aplext:hello:10"); + ASSERT_TRUE(ext); + // direct access to extension for test inspection + auto hello = testExtensions["aplext:hello:10"].lock(); + + // We have all we need. Inflate. + inflate(); + + // Check onMount triggered extension command. + ASSERT_TRUE(hello->commandTriggered); + hello->commandTriggered = false; + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + auto content = Content::create(EMBEDDED_DOC_TRIES_EXTENSION, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Check onMount not triggered extension command. + ASSERT_FALSE(hello->commandTriggered); + // Complained about command not been there + ASSERT_TRUE(session->checkAndClear()); +} + +static const char* EMBEDDED_DOC_REQUESTS_HOST_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "extension": [ + { + "uri": "aplext:hello:10", + "name": "Hello" + } + ], + "onMount": { + "type": "Hello:Test" + }, + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + } + } +})"; + +TEST_F(EmbeddedExtensionsTest, NoHostRequestedExtensionsAccess) +{ + loadExtensions(HOST_DOC); + + // verify the extension was registered + ASSERT_TRUE(extensionProvider->hasExtension("aplext:hello:10")); + auto ext = extensionProvider->getExtension("aplext:hello:10"); + ASSERT_TRUE(ext); + // direct access to extension for test inspection + auto hello = testExtensions["aplext:hello:10"].lock(); + + // We have all we need. Inflate. + inflate(); + + // Check onMount triggered extension command. + ASSERT_TRUE(hello->commandTriggered); + hello->commandTriggered = false; + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + auto content = Content::create(EMBEDDED_DOC_REQUESTS_HOST_EXTENSION, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Check onMount not triggered extension command. + ASSERT_FALSE(hello->commandTriggered); + // Complained about command not been there + ASSERT_TRUE(session->checkAndClear()); + + // Verify no extension handling set up + auto coreDoc = std::static_pointer_cast(embeddedDocumentContext); + ASSERT_TRUE(coreDoc->rootConfig().getSupportedExtensions().empty()); + ASSERT_FALSE(coreDoc->rootConfig().getExtensionMediator()); + ASSERT_FALSE(coreDoc->rootConfig().getExtensionProvider()); +} + +static const char* EMBEDDED_DOC_WITH_ALLOWED_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "extension": [ + { + "uri": "aplext:goodbye:10", + "name": "Bye" + } + ], + "onMount": { + "type": "Bye:Test" + }, + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + } + } +})"; + +TEST_F(EmbeddedExtensionsTest, AccessGrantedToEmbeddedExtension) +{ + loadExtensions(HOST_DOC); + + // The provider knows about the extension that was requested by the host document + ASSERT_TRUE(extensionProvider->hasExtension("aplext:hello:10")); + // but not the one that will be requested by the embedded document + ASSERT_FALSE(extensionProvider->hasExtension("aplext:goodbye:10")); + + // Load the embedded content a little early, so that we know what extensions it wants + auto embeddedContent = Content::create(EMBEDDED_DOC_WITH_ALLOWED_EXTENSION, session); + ASSERT_TRUE(embeddedContent->isReady()); + + ensureRequestedExtensions(embeddedContent->getExtensionRequests()); + + // Now the other extension is available + ASSERT_TRUE(extensionProvider->hasExtension("aplext:goodbye:10")); + + // Direct access to extensions for test inspection + auto hello = testExtensions["aplext:hello:10"].lock(); + auto goodbye = testExtensions["aplext:goodbye:10"].lock(); + + // Inflate the primary document + inflate(); + + // Reset the Hello command triggered flag (which triggered in primary document) + ASSERT_TRUE(hello->commandTriggered); + hello->commandTriggered = false; + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + auto request = requestWeak.lock(); + ASSERT_TRUE(request); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // Prepare a fresh mediator for the embedded document + auto embeddedMediator = ExtensionMediator::create(extensionProvider, + alexaext::Executor::getSynchronousExecutor()); + embeddedMediator->loadExtensions(ObjectMap(), embeddedContent); + + // Prepare document config + auto documentConfig = DocumentConfig::create(); + documentConfig->extensionMediator(embeddedMediator); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", embeddedContent, true, documentConfig); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Check onMount not triggered Hello extension command. + ASSERT_FALSE(hello->commandTriggered); + + // Check onMount triggered Goodbye extension command. + ASSERT_TRUE(goodbye->commandTriggered); + ASSERT_FALSE(session->checkAndClear()); + + // Verify that mediator is set up + auto coreDoc = std::static_pointer_cast(embeddedDocumentContext); + ASSERT_TRUE(coreDoc->rootConfig().getExtensionMediator()); +} + +#endif diff --git a/aplcore/unit/embed/unittest_embedded_lifecycle.cpp b/aplcore/unit/embed/unittest_embedded_lifecycle.cpp new file mode 100644 index 0000000..6205e95 --- /dev/null +++ b/aplcore/unit/embed/unittest_embedded_lifecycle.cpp @@ -0,0 +1,733 @@ +/** +* Copyright 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 "../testeventloop.h" +#include "../embed/testdocumentmanager.h" + +using namespace apl; + +class EmbeddedLifecycleTest : public DocumentWrapper { +public: + EmbeddedLifecycleTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + +protected: + std::shared_ptr documentManager; +}; + +static const char* HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Container", + "entities": "ROOT", + "id": "top", + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ], + "onFail": [ + { + "type": "InsertItem", + "sequencer": "SEND_EVENTER", + "arguments": ["FAILED"] + } + ] + } + } + } +})"; + +static const char* EMBEDDED_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "Hello, World!", + "entities": "EMBEDDED" + } + } +})"; + +static const char* PSEUDO_LOG_COMMAND = R"apl([ + { + "type": "Log" + } +])apl"; + +TEST_F(EmbeddedLifecycleTest, SimpleLoad) +{ + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Document causes update in DOM + auto host = component->getCoreChildAt(0); + ASSERT_EQ(1, host->getChildCount()); + auto update = std::vector{ + ObjectMap{{"index", 0}, {"uid", host->getCoreChildAt(0)->getUniqueId()}, {"action", "insert"}} + }; + ASSERT_TRUE(CheckUpdatedChildrenNotification(root, host, update)); + ASSERT_TRUE(CheckChildLaidOut(host, 0, true)); + + // Required by VH code in order to do dynamic dom changes. + ASSERT_EQ(host, CoreDocumentContext::cast(embeddedDocumentContext)->topComponent()->getParent()); + + // We can send commands to the root doc + auto cmd = JsonData(PSEUDO_LOG_COMMAND); + ASSERT_TRUE(cmd); + + ASSERT_TRUE(session->getCount() == 0); + rootDocument->executeCommands(cmd.get(), false); + ASSERT_TRUE(session->checkAndClear()); + + ASSERT_TRUE(embeddedSession->getCount() == 0); + embeddedDocumentContext->executeCommands(cmd.get(), false); + ASSERT_TRUE(embeddedSession->checkAndClear()); +} + +TEST_F(EmbeddedLifecycleTest, DoubleResolve) +{ + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext1 = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext1); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // And again + ASSERT_FALSE(documentManager->succeed("embeddedDocumentUrl", content, true)); +} + +static const char* EMBEDDED_DEEPER_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Text", + "id": "embeddedText1", + "text": "Hello, World!" + }, + { + "type": "Text", + "height": 200, + "width": 200, + "id": "embeddedText2", + "text": "Hello, World!" + } + ] + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, EmbeddedTextMeasurement) +{ + // Host document inflates + loadDocument(HOST_DOC); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto content = Content::create(EMBEDDED_DEEPER_DOC, session); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Document causes update in DOM + auto host = component->getCoreChildAt(0); + ASSERT_TRUE(host); + + auto text1 = root->findComponentById("embeddedText1"); + ASSERT_TRUE(text1); + ASSERT_EQ(Rect(0,0,1024,10), text1->getCalculated(kPropertyBounds).get()); + + auto text2 = root->findComponentById("embeddedText2"); + ASSERT_TRUE(text2); + ASSERT_EQ(Rect(0,10,200,200), text2->getCalculated(kPropertyBounds).get()); +} + +static const char* EMBEDDED_PAGER_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Pager", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Text", + "id": "embeddedText1", + "text": "Hello, World!" + }, + { + "type": "Text", + "height": 200, + "width": 200, + "id": "embeddedText2", + "text": "Hello, World!" + } + ] + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, EmbeddedTextPager) +{ + // Host document inflates + loadDocument(HOST_DOC); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto content = Content::create(EMBEDDED_PAGER_DOC, session); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Document causes update in DOM + auto host = component->getCoreChildAt(0); + ASSERT_TRUE(host); + + auto text1 = root->findComponentById("embeddedText1"); + ASSERT_TRUE(text1); + ASSERT_EQ(Rect(0,0,1024,800), text1->getCalculated(kPropertyBounds).get()); + + auto text2 = root->findComponentById("embeddedText2"); + ASSERT_TRUE(text2); + ASSERT_EQ(Rect(0,0,1024,800), text2->getCalculated(kPropertyBounds).get()); +} + +static const char* EMBEDDED_SEND_EVENT_MOUNT_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText1", + "text": "Hello, World!", + "onMount": { + "type": "SendEvent", + "delay": 1000, + "sequencer": "COMPONENT_MOUNT", + "arguments": ["EMBEDDED_COMPONENT"] + } + } + }, + "onMount": { + "type": "SendEvent", + "delay": 500, + "sequencer": "DOCUMENT_MOUNT", + "arguments": ["DOCUMENT"] + } +})"; + +TEST_F(EmbeddedLifecycleTest, EmbeddedSendEventTagging) +{ + // Host document inflates + loadDocument(HOST_DOC); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto content = Content::create(EMBEDDED_SEND_EVENT_MOUNT_DOC, session); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + + // Check that first SendEvent is load success and tagged by host doc + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(event.getType(), kEventTypeSendEvent); + ASSERT_EQ(event.getValue(apl::kEventPropertyArguments).at(0).getString(), "LOADED"); + ASSERT_EQ(rootDocument, event.getDocument()); + + advanceTime(500); + + // Embedded doc fires it's onmount + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(event.getType(), kEventTypeSendEvent); + ASSERT_EQ(event.getValue(apl::kEventPropertyArguments).at(0).getString(), "DOCUMENT"); + ASSERT_EQ(embeddedDocumentContext, event.getDocument()); + + advanceTime(1000); + + // Embedded doc component fires it's onmount + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(event.getType(), kEventTypeSendEvent); + ASSERT_EQ(event.getValue(apl::kEventPropertyArguments).at(0).getString(), "EMBEDDED_COMPONENT"); + ASSERT_EQ(embeddedDocumentContext, event.getDocument()); +} + +static const char* EMBEDDED_OPEN_URL_MOUNT_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText1", + "text": "Hello, World!" + } + }, + "onMount": { + "type": "OpenURL", + "delay": 500, + "source": "SOURCE" + } +})"; + +TEST_F(EmbeddedLifecycleTest, EmbeddedOpenUrlTagging) +{ + config->set(RootProperty::kAllowOpenUrl, true); + // Host document inflates + loadDocument(HOST_DOC); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto content = Content::create(EMBEDDED_OPEN_URL_MOUNT_DOC, session); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + + // Check that first SendEvent is load success and tagged by host doc + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + ASSERT_EQ(event.getType(), kEventTypeSendEvent); + ASSERT_EQ(event.getValue(apl::kEventPropertyArguments).at(0).getString(), "LOADED"); + ASSERT_EQ(rootDocument, event.getDocument()); + + advanceTime(500); + + // Embedded doc fires it's onmount + ASSERT_TRUE(root->hasEvent()); + event = root->popEvent(); + ASSERT_EQ(event.getType(), kEventTypeOpenURL); + ASSERT_EQ(event.getValue(apl::kEventPropertySource).getString(), "SOURCE"); + ASSERT_EQ(embeddedDocumentContext, event.getDocument()); +} + +TEST_F(EmbeddedLifecycleTest, Finish) { + loadDocument(HOST_DOC); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + root->clearDirty(); + + // We can send commands to the root doc + auto cmd = JsonData(R"([{"type": "Finish"}])"); + ASSERT_TRUE(cmd); + + // Finish ignored by embedded doc + embeddedDocumentContext->executeCommands(cmd.get(), false); + ASSERT_FALSE(root->hasEvent()); + + // But not by host + rootDocument->executeCommands(cmd.get(), false); + ASSERT_TRUE(root->hasEvent()); + ASSERT_EQ(kEventTypeFinish, root->popEvent().getType()); +} + +const static char *PARENT_VC = R"({ + "children": [ + { + "entities": [ + "HOST" + ], + "tags": { + "focused": false + }, + "id": "hostComponent", + "uid": "HOSTID", + "position": "1024x800+0+0:0", + "type": "empty" + } + ], + "entities": [ + "ROOT" + ], + "tags": { + "viewport": {} + }, + "id": "top", + "uid": "ROOTID", + "position": "1024x800+0+0:0", + "type": "empty" +})"; + +const static char *EMBEDDED_VC = R"({ + "entities": [ + "EMBEDDED" + ], + "tags": { + "viewport": {} + }, + "id": "embeddedText", + "uid": "EMBEDDEDID", + "position": "1024x800+0+0:0", + "type": "text" +})"; + +TEST_F(EmbeddedLifecycleTest, VisualContextDetached) +{ + loadDocument(HOST_DOC); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + // Host and embedded documents have different origin. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, false); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + root->clearDirty(); + + rapidjson::Document document(rapidjson::kObjectType); + auto rootVisualContextDocument = rootDocument->serializeVisualContext(document.GetAllocator()); + + // Load the JSON result above + auto parentVC = std::string(PARENT_VC); + parentVC = std::regex_replace(parentVC, std::regex("ROOTID"), component->getUniqueId()); + parentVC = std::regex_replace(parentVC, std::regex("HOSTID"), component->getChildAt(0)->getUniqueId()); + + rapidjson::Document result; + rapidjson::ParseResult ok = result.Parse(parentVC.c_str()); + ASSERT_TRUE(ok); + + ASSERT_TRUE(rootVisualContextDocument == result); + + auto embeddedContextDocument = embeddedDocumentContext->serializeVisualContext(document.GetAllocator()); + + // Load the JSON result above + auto embeddedVc = std::string(EMBEDDED_VC); + embeddedVc = std::regex_replace(embeddedVc, std::regex("EMBEDDEDID"), component->getChildAt(0)->getChildAt(0)->getUniqueId()); + + ok = result.Parse(embeddedVc.c_str()); + ASSERT_TRUE(ok); + + ASSERT_TRUE(embeddedContextDocument == result); +} + +const static char *FULL_VC = R"({ + "children": + [ + { + "children": [ + { + "entities": [ + "EMBEDDED" + ], + "id": "embeddedText", + "uid": "EMBEDDEDID", + "position": "1024x800+0+0:0", + "type": "text" + } + ], + "entities": [ + "HOST" + ], + "tags": { + "focused": false + }, + "id": "hostComponent", + "uid": "HOSTID", + "position": "1024x800+0+0:0", + "type": "text" + } + ], + "entities": [ + "ROOT" + ], + "tags": { + "viewport": {} + }, + "id": "top", + "uid": "ROOTID", + "position": "1024x800+0+0:0", + "type": "text" +})"; + +TEST_F(EmbeddedLifecycleTest, VisualContextAttached) +{ + loadDocument(HOST_DOC); + + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + // Check that origin is host document + ASSERT_EQ(rootDocument, request->getOrigin()); + + // Host and embedded documents have same origin. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + root->clearDirty(); + + rapidjson::Document document(rapidjson::kObjectType); + auto foolVisualContextDocument = rootDocument->serializeVisualContext(document.GetAllocator()); + + auto fullVc = std::string(FULL_VC); + fullVc = std::regex_replace(fullVc, std::regex("ROOTID"), component->getUniqueId()); + fullVc = std::regex_replace(fullVc, std::regex("HOSTID"), component->getChildAt(0)->getUniqueId()); + fullVc = std::regex_replace(fullVc, std::regex("EMBEDDEDID"), component->getChildAt(0)->getChildAt(0)->getUniqueId()); + + rapidjson::Document result; + rapidjson::ParseResult ok = result.Parse(fullVc.c_str()); + ASSERT_TRUE(ok); + + ASSERT_TRUE(foolVisualContextDocument == result); +} + +static const char* EMBEDDED_DOC_TIMED = R"({ + "type": "APL", + "version": "2023.2", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${utcTime}" + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, TimeUpdatesPropagation) +{ + // Host document inflates + loadDocument(HOST_DOC); + + auto content = Content::create(EMBEDDED_DOC_TIMED, session); + + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto text = root->findComponentById("embeddedText"); + ASSERT_EQ("0", text->getCalculated(apl::kPropertyText).asString()); + + advanceTime(100); + + ASSERT_EQ("100", text->getCalculated(apl::kPropertyText).asString()); +} + +static const char* HOST_DOC_DOUBLE = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Container", + "entities": "ROOT", + "id": "top", + "items": [ + { + "type": "Host", + "width": "50%", + "height": "50%", + "id": "hostComponent1", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED1"] + } + ] + }, + { + "type": "Host", + "width": "50%", + "height": "50%", + "id": "hostComponent2", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED2"] + } + ] + } + ] + } + } +})"; + +// Content should be reusable, even behind the same source. +TEST_F(EmbeddedLifecycleTest, ContentAndSourceReuse) +{ + // Host document inflates + loadDocument(HOST_DOC_DOUBLE); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext1 = documentManager->succeed("embeddedDocumentUrl", content, true, std::make_shared(), true); + ASSERT_TRUE(embeddedDocumentContext1); + ASSERT_TRUE(CheckSendEvent(root, "LOADED1")); + + auto embeddedDocumentContext2 = documentManager->succeed("embeddedDocumentUrl", content, true, std::make_shared(), true); + ASSERT_TRUE(embeddedDocumentContext2); + ASSERT_TRUE(CheckSendEvent(root, "LOADED2")); +} + +static const char* SINGLE_HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, SingleHost) +{ + // Host document inflates + loadDocument(SINGLE_HOST_DOC); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true, std::make_shared(), true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); +} diff --git a/aplcore/unit/embed/unittest_embedded_reinflate.cpp b/aplcore/unit/embed/unittest_embedded_reinflate.cpp new file mode 100644 index 0000000..4b6ab40 --- /dev/null +++ b/aplcore/unit/embed/unittest_embedded_reinflate.cpp @@ -0,0 +1,539 @@ +/** +* Copyright 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 "../testeventloop.h" +#include "../embed/testdocumentmanager.h" + +using namespace apl; + +class EmbeddedReinflateTest : public DocumentWrapper { +public: + EmbeddedReinflateTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + +protected: + std::shared_ptr documentManager; +}; + +static const char* HOST_DOC_CONFIG_CHANGE = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": [ + "${event.height}", "${event.width}", "${event.theme}", "${event.viewportMode}", + "${event.fontScale}", "${event.screenMode}", "${event.screenReader}", + "${event.sizeChanged}", "${event.rotated}" + ] + }, + "mainTemplate": { + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + }, + { + "type": "Text", + "id": "hostText", + "text": "${viewport.theme}", + "entities": "EMBEDDED" + } + ] + } + } +})"; + +static const char* HOST_DOC_REINFLATE_SIMPLE = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "preserve": [ "embeddedDocument" ] + } + } +})"; + +static const char* HOST_DOC_REINFLATE = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "preserve": [ "embeddedDocument" ], + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + }, + { + "type": "Text", + "id": "hostText", + "text": "${viewport.theme}", + "entities": "EMBEDDED" + } + ] + } + } +})"; + +static const char* EMBEDDED_DOC_CONFIG_SIMPLE = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${viewport.theme}", + "entities": "EMBEDDED" + } + } +})"; + +static const char* EMBEDDED_DOC_CONFIG = R"({ + "type": "APL", + "version": "2023.2", + "onConfigChange": { + "type": "SendEvent", + "sequencer": "SEND_EVENTER_EMBEDDED", + "delay": 100, + "arguments": [ + "${event.height}", "${event.width}", "${event.theme}", "${event.viewportMode}", + "${event.fontScale}", "${event.screenMode}", "${event.screenReader}", + "${event.sizeChanged}", "${event.rotated}" + ] + }, + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${viewport.theme}", + "entities": "EMBEDDED" + } + } +})"; + +static const char* EMBEDDED_DOC_REINFLATE = R"({ + "type": "APL", + "version": "2023.2", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${viewport.theme}", + "entities": "EMBEDDED" + } + } +})"; + +static const char* HOST_DOC_REINFLATE_NO_PRESERVE = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ] + }, + { + "type": "Text", + "id": "hostText", + "text": "${viewport.theme}", + "entities": "EMBEDDED" + } + ] + } + } +})"; + +// Simple size config change just triggers resize across the board +TEST_F(EmbeddedReinflateTest, ConfigChangeSize) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_CONFIG_CHANGE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto configChange = ConfigurationChange(500, 500); + root->configurationChange(configChange); + ASSERT_TRUE(CheckSendEvent(root, 500, 500, "dark", "hub", 1, "normal", false, true, false)); + + advanceTime(100); + ASSERT_TRUE(CheckSendEvent(root, 500, 500, "dark", "hub", 1, "normal", false, true, false)); +} + +// Size change without config change causes one only in Embedded document +TEST_F(EmbeddedReinflateTest, DirectChangeSize) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_CONFIG_CHANGE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + executeCommands(JsonData(R"([{ "type": "SetValue", "componentId": "hostComponent", "property": "height", "value": 300 }])").moveToObject(), false); + advanceTime(10); + + ASSERT_FALSE(root->hasEvent()); + + advanceTime(100); + ASSERT_TRUE(CheckSendEvent(root, 300, 400, "dark", "hub", 1, "normal", false, true, false)); +} + +// Relevant config change passed over to the embedded doc +TEST_F(EmbeddedReinflateTest, ConfigChangeTheme) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_CONFIG_CHANGE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + ASSERT_TRUE(CheckSendEvent(root, 100, 100, "light", "hub", 1, "normal", false, false, false)); + + advanceTime(100); + ASSERT_TRUE(CheckSendEvent(root, 100, 100, "light", "hub", 1, "normal", false, false, false)); + + hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); +} + +// Config change may lead to Embedded document reinflate +TEST_F(EmbeddedReinflateTest, ConfigChangeThemeEmbedded) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_CONFIG_CHANGE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_REINFLATE, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + ASSERT_TRUE(CheckSendEvent(root, 100, 100, "light", "hub", 1, "normal", false, false, false)); + + advanceTime(100); + + hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("light", embeddedText->getCalculated(kPropertyText).asString()); +} + + +// Config change may lead to Embedded document reinflate +TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHostNonResolved) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_REINFLATE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + auto hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + processReinflate(); + + advanceTime(100); + + // Embedded doc shouldn't reinflate + hostText = root->findComponentById("hostText"); + ASSERT_EQ("light", hostText->getCalculated(kPropertyText).asString()); +} + +TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHostSimple) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_REINFLATE_SIMPLE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG_SIMPLE, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + processReinflate(); + + advanceTime(100); + + // Embedded doc shouldn't reinflate + embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); +} + +// Embedded preserved +TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHost) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_REINFLATE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + embeddedDocumentContext = nullptr; + hostText = nullptr; + embeddedText = nullptr; + processReinflate(); + + advanceTime(100); + + // Embedded doc shouldn't reinflate + ASSERT_TRUE(CheckSendEvent(root, 100, 100, "light", "hub", 1, "normal", false, false, false)); + hostText = root->findComponentById("hostText"); + ASSERT_EQ("light", hostText->getCalculated(kPropertyText).asString()); + embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); +} + +// Config change may lead to Embedded document reinflate +TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHostAndEmbedded) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_REINFLATE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_REINFLATE, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + processReinflate(); + + advanceTime(100); + + // Embedded doc shouldn't reinflate + hostText = root->findComponentById("hostText"); + ASSERT_EQ("light", hostText->getCalculated(kPropertyText).asString()); + embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("light", embeddedText->getCalculated(kPropertyText).asString()); +} + +// Config change may lead to Embedded document reinflate +TEST_F(EmbeddedReinflateTest, ConfigChangeThemeHostNoPreserve) +{ + metrics.size(400, 400); + + // Host document inflates + session = std::make_shared(); + loadDocument(HOST_DOC_REINFLATE_NO_PRESERVE); + + advanceTime(100); + + auto content = Content::create(EMBEDDED_DOC_CONFIG, session); + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto hostText = root->findComponentById("hostText"); + ASSERT_EQ("dark", hostText->getCalculated(kPropertyText).asString()); + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("dark", embeddedText->getCalculated(kPropertyText).asString()); + + auto configChange = ConfigurationChange().theme("light"); + root->configurationChange(configChange); + + // TODO: Open question. If runtime holds strong reference - handlers may still run. Stop explicitly? + embeddedDocumentContext = nullptr; + processReinflate(); + + advanceTime(100); + + // Embedded doc shouldn't reinflate, but will be effectively recreated + ASSERT_FALSE(root->hasEvent()); + + // Replacement requested. + ASSERT_TRUE(!documentManager->getUnresolvedRequests().empty()); + embeddedDocumentContext = documentManager->succeed(content); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + hostText = root->findComponentById("hostText"); + ASSERT_EQ("light", hostText->getCalculated(kPropertyText).asString()); + embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("light", embeddedText->getCalculated(kPropertyText).asString()); +} \ No newline at end of file diff --git a/aplcore/unit/embed/unittest_rootcontexttargeting.cpp b/aplcore/unit/embed/unittest_rootcontexttargeting.cpp new file mode 100644 index 0000000..da787b9 --- /dev/null +++ b/aplcore/unit/embed/unittest_rootcontexttargeting.cpp @@ -0,0 +1,663 @@ +/** +* Copyright 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 "../testeventloop.h" +#include "../embed/testdocumentmanager.h" + +#include "apl/dynamicdata.h" + +using namespace apl; + +class RootContextTargetingTest : public DocumentWrapper { +public: + RootContextTargetingTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + +protected: + std::shared_ptr documentManager; +}; + +static const char* DEFAULT_DOC = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Container", + "id": "top", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnLoadArtifact", + "value": "hostComponentOnLoad triggered" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "id": "hostOnFailArtifact", + "value": "hostComponentOnFail triggered" + } + } + ] + } + } + } +})"; + +static const char* EMBEDDED_DEFAULT = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "embeddedTop", + "items": [ + { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + }, + { + "type": "Host", + "id": "nestedHost", + "source": "nestedEmbeddedUrl", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "embeddedTop", + "item": { + "type": "Text", + "id": "nestedHostOnLoadArtifact", + "value": "hostComponentOnLoad triggered" + } + } + ], + "onFail": [ + { + "type": "InsertItem", + "componentId": "embeddedTop", + "item": { + "type": "Text", + "id": "nestedHostOnFailArtifact", + "value": "hostComponentOnFail triggered" + } + } + ] + } + ] + } + } +})"; + +//static const char* EMBEDDED_NESTED = R"({ +// "type": "APL", +// "version": "2023.2", +// "mainTemplate": { +// "item": { +// "type": "Container", +// "id": "nestedEmbeddedTop", +// "items": [ +// { +// "type": "Text", +// "id": "nestedEmbeddedText", +// "value": "Hello, World!" +// } +// ] +// } +// } +//})"; + +TEST_F(RootContextTargetingTest, TestFindComponentByIdWithoutDocumentIdForTopLevelComponent) +{ + loadDocument(DEFAULT_DOC); + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + ASSERT_TRUE(root->findComponentById("hostComponent")); +} + +TEST_F(RootContextTargetingTest, TestFindComponentByIdWithoutDocumentIdForEmbeddedComponent) +{ + loadDocument(DEFAULT_DOC); + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + // It's a public API used by VH + ASSERT_TRUE(root->findComponentById("embeddedText")); +} + +TEST_F(RootContextTargetingTest, TestFindComponentByIdWithDocumentIdForUnregisteredDocumentId) +{ + loadDocument(DEFAULT_DOC); + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embed = CoreDocumentContext::cast(documentManager->succeed("embeddedDocumentUrl", content, true)); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + ASSERT_FALSE(embed->findComponentById("hostComponent")); +} + +TEST_F(RootContextTargetingTest, TestFindComponentByIdWithDocumentIdForTopLevelComponent) +{ + loadDocument(DEFAULT_DOC); + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embed = CoreDocumentContext::cast(documentManager->succeed("embeddedDocumentUrl", content, true)); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + ASSERT_FALSE(embed->findComponentById("hostComponent")); +} + +TEST_F(RootContextTargetingTest, TestFindComponentByIdWithDocumentIdForTargetEmbeddedComponent) +{ + loadDocument(DEFAULT_DOC); + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + auto embed = CoreDocumentContext::cast(documentManager->succeed("embeddedDocumentUrl", content, true)); + ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); + ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); + + ASSERT_TRUE(embed->findComponentById("embeddedText")); +} + +//TEST_F(RootContextTargetingTest, TestFindComponentByIdWithDocumentIdForNestedEmbeddedComponent) +//{ +// loadDocument(DEFAULT_DOC); +// auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); +// ASSERT_TRUE(content->isReady()); +// documentManager->succeed("embeddedDocumentUrl", content, true); +// ASSERT_TRUE(root->findComponentById("hostOnLoadArtifact")); +// ASSERT_FALSE(root->findComponentById("hostOnFailArtifact")); +// +// content = Content::create(EMBEDDED_NESTED, makeDefaultSession()); +// ASSERT_TRUE(content->isReady()); +// documentManager->succeed("nestedEmbeddedUrl", content, true); +// ASSERT_TRUE(root->findComponentById("nestedHostOnLoadArtifact", "embeddedDocumentUrl")); +// ASSERT_FALSE(root->findComponentById("nestedHostOnFailArtifact", "embeddedDocumentUrl")); +// +// ASSERT_FALSE(root->findComponentById("embeddedText", "nestedEmbeddedUrl")); +// ASSERT_TRUE(root->findComponentById("nestedEmbeddedText", "nestedEmbeddedUrl")); +//} + +static const char* HOST_ONLY_DOC = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Frame", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": { + "type": "SendEvent", + "sequencer": "SEND_EVENT", + "arguments": ["LOADED"] + }, + "onFail": { + "type": "SendEvent", + "sequencer": "SEND_EVENT", + "arguments": ["FAILED"] + } + } + } + } +})"; + +static const char* EMBEDDED_DYNAMIC_WITH_ON_MOUNT = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "Hello, World!", + "onMount": { + "delay": 1000, + "sequencer": "EMBEDDED_CHANGE", + "type": "SetValue", + "property": "text", + "value": "Potatoes coming!" + } + } + } +})"; + +TEST_F(RootContextTargetingTest, TestDirtyEmbeddedDocumentComponent) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_DYNAMIC_WITH_ON_MOUNT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + auto rootComp = component->findComponentById("hostComponent", true); + ASSERT_TRUE(CheckDirty(rootComp, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(1, rootComp->getDisplayedChildCount()); + + root->clearDirty(); + + auto text = component->findComponentById("embeddedText", true); + ASSERT_EQ("Hello, World!", text->getCalculated(apl::kPropertyText).asString()); + + ASSERT_EQ(1, loop->size()); + + advanceTime(1500); + ASSERT_TRUE(CheckDirty(text, kPropertyText, kPropertyVisualHash)); + + ASSERT_EQ("Potatoes coming!", text->getCalculated(apl::kPropertyText).asString()); + + root->clearDirty(); + + ASSERT_FALSE(root->isDirty()); +} + +TEST_F(RootContextTargetingTest, TestEmbeddedDocumentRemoveCleanup) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_DYNAMIC_WITH_ON_MOUNT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + root->clearDirty(); + + auto text = component->findComponentById("embeddedText", true); + ASSERT_EQ("Hello, World!", text->getCalculated(apl::kPropertyText).asString()); + + advanceTime(1500); + + ASSERT_EQ("Potatoes coming!", text->getCalculated(apl::kPropertyText).asString()); + + auto actionRef = executeCommand("RemoveItem", + {{"componentId", "hostComponent"}}, + false); + + advanceTime(50); + + ASSERT_EQ(0, loop->size()); + + ASSERT_FALSE(actionRef->isPending()); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(CheckDirty(root, component)); +} + +static const char* EMBEDDED_DYNAMIC_WITH_SEND_EVENT = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "Hello, World!", + "onMount": { + "delay": 1000, + "sequencer": "EMBEDDED_SEND", + "type": "SendEvent", + "arguments": ["EMBEDDED"] + } + } + } +})"; + +TEST_F(RootContextTargetingTest, TestEmbeddedDocumentEvent) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_DYNAMIC_WITH_SEND_EVENT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + + advanceTime(1500); + + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + ASSERT_TRUE(CheckSendEvent(root, "EMBEDDED")); + + root->clearDirty(); +} + +TEST_F(RootContextTargetingTest, TestEmbeddedDocumentEventClearOnRemove) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_DYNAMIC_WITH_SEND_EVENT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + + advanceTime(1500); + + ASSERT_TRUE(root->hasEvent()); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + // Remove embedded doc + std::map properties = {}; + properties.emplace("componentId", "hostComponent"); + executeCommand("RemoveItem", properties, false); + + advanceTime(500); + + ASSERT_FALSE(root->hasEvent()); + + root->clearDirty(); +} + +TEST_F(RootContextTargetingTest, VerifyUniqueComponentIds) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + auto rootComp = component->findComponentById("hostComponent", true); + ASSERT_NE(component->getUniqueId(), rootComp->getUniqueId()); + auto text = component->findComponentById("embeddedText", true); + ASSERT_NE(component->getUniqueId(), text->getUniqueId()); + ASSERT_NE(component->getUniqueId(), rootComp->getUniqueId()); + + // Can search for any UID from RootContext API + ASSERT_EQ(component.get(), root->findByUniqueId(component->getUniqueId())); + ASSERT_EQ(text.get(), root->findByUniqueId(text->getUniqueId())); +} + +static const char* EMBEDDED_KEY_HANDLER = R"({ + "type": "APL", + "version": "2023.2", + "handleKeyUp": [ + { + "when": "${event.keyboard.code == 'KeyG'}", + "commands": [ + { + "type": "SendEvent", + "arguments": ["GREEN"] + } + ] + } + ], + "handleKeyDown": [ + { + "when": "${event.keyboard.code == 'KeyB'}", + "commands": [ + { + "type": "SendEvent", + "arguments": ["BLUE"] + } + ] + } + ], + "mainTemplate": { + "items": { + "type": "Frame", + "id": "testFrame", + "backgroundColor": "red" + } + } +})"; + +TEST_F(RootContextTargetingTest, FocusedHostDocumentKeyboard) +{ + loadDocument(HOST_ONLY_DOC); + auto content = Content::create(EMBEDDED_KEY_HANDLER, makeDefaultSession()); + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + advanceTime(10); + + root->setFocus(FocusDirection::kFocusDirectionNone, Rect(0,0,10,10), "hostComponent"); + + root->popEvent().getActionRef().resolve(); + root->clearPending(); + root->clearDirty(); + + // send valid key down + root->handleKeyboard(kKeyDown, Keyboard("KeyB", "b")); + // verify down command was executed + ASSERT_TRUE(CheckSendEvent(root, "BLUE")); + + // send valid key up + root->handleKeyboard(kKeyUp, Keyboard("KeyG", "g")); + // verify up command was executed + ASSERT_TRUE(CheckSendEvent(root, "GREEN")); +} + +static const char* HOST_WITH_KEYBOARD_ONLY_DOC = R"({ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "item": { + "type": "Frame", + "item": { + "type": "Host", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "onLoad": { + "type": "SendEvent", + "sequencer": "SEND_EVENT", + "arguments": ["LOADED"] + }, + "onFail": { + "type": "SendEvent", + "sequencer": "SEND_EVENT", + "arguments": ["FAILED"] + }, + "handleKeyUp": [ + { + "when": "${event.keyboard.code == 'KeyG'}", + "commands": [ + { + "type": "SendEvent", + "arguments": ["GARBAGE"] + } + ] + } + ], + "handleKeyDown": [ + { + "when": "${event.keyboard.code == 'KeyB'}", + "commands": [ + { + "type": "SendEvent", + "arguments": ["BLUEBERRY"] + } + ] + } + ] + } + } + } +})"; + +TEST_F(RootContextTargetingTest, FocusedHostComponentKeyboard) +{ + loadDocument(HOST_WITH_KEYBOARD_ONLY_DOC); + auto content = Content::create(EMBEDDED_DYNAMIC_WITH_SEND_EVENT, makeDefaultSession()); + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + advanceTime(1000); + + ASSERT_TRUE(CheckSendEvent(root, "EMBEDDED")); + + root->setFocus(FocusDirection::kFocusDirectionNone, Rect(0,0,10,10), "hostComponent"); + + root->popEvent().getActionRef().resolve(); + root->clearPending(); + root->clearDirty(); + + // send valid key down + root->handleKeyboard(kKeyDown, Keyboard("KeyB", "b")); + // verify down command was executed + ASSERT_TRUE(CheckSendEvent(root, "BLUEBERRY")); + + // send valid key up + root->handleKeyboard(kKeyUp, Keyboard("KeyG", "g")); + // verify up command was executed + ASSERT_TRUE(CheckSendEvent(root, "GARBAGE")); +} + +static const char* EMBEDDED_KEY_HANDLER_PROPAGATE = R"({ + "type": "APL", + "version": "2023.2", + "handleKeyUp": [ + { + "when": "${event.keyboard.code == 'KeyG'}", + "propagate": true, + "commands": [ + { + "type": "SendEvent", + "arguments": ["GREEN"] + } + ] + } + ], + "handleKeyDown": [ + { + "when": "${event.keyboard.code == 'KeyB'}", + "propagate": true, + "commands": [ + { + "type": "SendEvent", + "arguments": ["BLUE"] + } + ] + } + ], + "mainTemplate": { + "items": { + "type": "Frame", + "id": "testFrame", + "backgroundColor": "red" + } + } +})"; + +TEST_F(RootContextTargetingTest, FocusedHostPropagatedKeyboard) +{ + loadDocument(HOST_WITH_KEYBOARD_ONLY_DOC); + auto content = Content::create(EMBEDDED_KEY_HANDLER_PROPAGATE, makeDefaultSession()); + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + advanceTime(1000); + + root->setFocus(FocusDirection::kFocusDirectionNone, Rect(0,0,10,10), "hostComponent"); + + root->popEvent().getActionRef().resolve(); + root->clearPending(); + root->clearDirty(); + + // send valid key down + root->handleKeyboard(kKeyDown, Keyboard("KeyB", "b")); + // verify down command was executed + ASSERT_TRUE(CheckSendEvent(root, "BLUE")); + ASSERT_TRUE(CheckSendEvent(root, "BLUEBERRY")); + + // send valid key up + root->handleKeyboard(kKeyUp, Keyboard("KeyG", "g")); + // verify up command was executed + ASSERT_TRUE(CheckSendEvent(root, "GREEN")); + ASSERT_TRUE(CheckSendEvent(root, "GARBAGE")); +} + +static const char* EMBEDDED_KEY_HANDLER_PROPAGATE_DEEPER = R"({ + "type": "APL", + "version": "2023.2", + "handleKeyUp": [ + { + "when": "${event.keyboard.code == 'KeyG'}", + "propagate": true, + "commands": [ + { + "type": "SendEvent", + "arguments": ["GREEN"] + } + ] + } + ], + "handleKeyDown": [ + { + "when": "${event.keyboard.code == 'KeyB'}", + "propagate": true, + "commands": [ + { + "type": "SendEvent", + "arguments": ["BLUE"] + } + ] + } + ], + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "id": "INTERNALTW", + "width": "100%", + "height": "100%", + "item": { + "type": "Frame", + "id": "testFrame", + "backgroundColor": "red" + } + } + } +})"; + +TEST_F(RootContextTargetingTest, FocusedHostPropagatedDeeperKeyboard) +{ + loadDocument(HOST_WITH_KEYBOARD_ONLY_DOC); + auto content = Content::create(EMBEDDED_KEY_HANDLER_PROPAGATE_DEEPER, makeDefaultSession()); + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + advanceTime(1000); + + root->setFocus(FocusDirection::kFocusDirectionNone, Rect(0,0,10,10), "INTERNALTW"); + + root->popEvent().getActionRef().resolve(); + root->clearPending(); + root->clearDirty(); + + // send valid key down + root->handleKeyboard(kKeyDown, Keyboard("KeyB", "b")); + // verify down command was executed + ASSERT_TRUE(CheckSendEvent(root, "BLUE")); + ASSERT_TRUE(CheckSendEvent(root, "BLUEBERRY")); + + // send valid key up + root->handleKeyboard(kKeyUp, Keyboard("KeyG", "g")); + // verify up command was executed + ASSERT_TRUE(CheckSendEvent(root, "GREEN")); + ASSERT_TRUE(CheckSendEvent(root, "GARBAGE")); +} diff --git a/unit/engine/CMakeLists.txt b/aplcore/unit/engine/CMakeLists.txt similarity index 92% rename from unit/engine/CMakeLists.txt rename to aplcore/unit/engine/CMakeLists.txt index 4bb51e4..0384801 100644 --- a/unit/engine/CMakeLists.txt +++ b/aplcore/unit/engine/CMakeLists.txt @@ -17,21 +17,23 @@ target_sources_local(unittest unittest_builder.cpp unittest_builder_bind.cpp unittest_builder_config_change.cpp - unittest_builder_pager.cpp unittest_builder_padding.cpp + unittest_builder_pager.cpp unittest_builder_preserve.cpp unittest_builder_preserve_scroll.cpp unittest_builder_sequence.cpp unittest_context.cpp + unittest_context_apl_version.cpp unittest_current_time.cpp unittest_dependant.cpp unittest_display_state.cpp + unittest_event.cpp + unittest_event_manager.cpp unittest_hover.cpp unittest_keyboard_manager.cpp unittest_layouts.cpp unittest_memory.cpp unittest_propdef.cpp - unittest_queue_event_manager.cpp unittest_resources.cpp unittest_styles.cpp unittest_viewhost.cpp diff --git a/unit/engine/unittest_arrayify.cpp b/aplcore/unit/engine/unittest_arrayify.cpp similarity index 100% rename from unit/engine/unittest_arrayify.cpp rename to aplcore/unit/engine/unittest_arrayify.cpp diff --git a/unit/engine/unittest_builder.cpp b/aplcore/unit/engine/unittest_builder.cpp similarity index 98% rename from unit/engine/unittest_builder.cpp rename to aplcore/unit/engine/unittest_builder.cpp index 8b69237..8af1333 100644 --- a/unit/engine/unittest_builder.cpp +++ b/aplcore/unit/engine/unittest_builder.cpp @@ -1751,14 +1751,14 @@ static const char *MEDIA_SOURCE_WITH_TEXTTRACK = R"({ "type": "Video", "source": { "url": "URL1", - "textTrack": [{ "url": "URL1", "type": "caption" }] + "textTracks": [{ "url": "URL2", "type": "caption" }] } }, { "type": "Video", "source": { "url": "URL1", - "textTrack": [{ "url": "URL1", "type": "notcaption" }] + "textTrack": [{ "url": "URL3", "type": "notcaption" }, { "url": "URL4", "type": "caption" }] } } ] @@ -1792,7 +1792,6 @@ TEST_F(BuilderTest, MediaSourceWithTextTrack) ASSERT_EQ("URL1", track.url); ASSERT_EQ(kTextTrackTypeCaption, track.type); - //4 - Testing textTrack with no description. sources = video1->getCalculated(kPropertySource); ASSERT_TRUE(sources.isArray()); ASSERT_EQ(1, sources.size()); @@ -1801,17 +1800,20 @@ TEST_F(BuilderTest, MediaSourceWithTextTrack) ASSERT_EQ(1, tracks.size()); track = tracks[0]; ASSERT_EQ("", track.description); - ASSERT_EQ("URL1", track.url); + ASSERT_EQ("URL2", track.url); ASSERT_EQ(kTextTrackTypeCaption, track.type); - //5 - Testing textTrack with invalid type. ASSERT_TRUE(ConsoleMessage()); sources = video2->getCalculated(kPropertySource); ASSERT_TRUE(sources.isArray()); ASSERT_EQ(1, sources.size()); source = sources.at(0).get(); tracks = source.getTextTracks(); - ASSERT_EQ(0, tracks.size()); + ASSERT_EQ(1, tracks.size()); + track = tracks[0]; + ASSERT_EQ("", track.description); + ASSERT_EQ("URL4", track.url); + ASSERT_EQ(kTextTrackTypeCaption, track.type); } static const char *MEDIA_SOURCE_2 = R"( @@ -2708,7 +2710,7 @@ TEST_F(BuilderTest, ResourceLookupAtBinding) { loadDocument(RESOURCE_LOOKUP_AT_BINDING); ASSERT_TRUE(component); - auto text = component->findComponentById("myText"); + auto text = root->findComponentById("myText"); //Default value of text component ASSERT_TRUE(IsEqual("11", text->getCalculated(kPropertyText).asString())); @@ -2777,10 +2779,10 @@ TEST_F(BuilderTest, BasicStartEndPaddingLTR) { loadDocument(BASIC_START_END_PADDING); - auto frame = component->findComponentById("paddedFrame"); - auto frame2 = component->findComponentById("paddedFrame2"); - auto child = component->findComponentById("paddedFrameChild"); - auto child2 = component->findComponentById("paddedFrameChild2"); + auto frame = root->findComponentById("paddedFrame"); + auto frame2 = root->findComponentById("paddedFrame2"); + auto child = root->findComponentById("paddedFrameChild"); + auto child2 = root->findComponentById("paddedFrameChild2"); ASSERT_EQ(Object::NULL_OBJECT(), frame->getCalculated(kPropertyPaddingBottom)); ASSERT_EQ(Object::NULL_OBJECT(), frame->getCalculated(kPropertyPaddingLeft)); ASSERT_EQ(Object::NULL_OBJECT(), frame->getCalculated(kPropertyPaddingRight)); @@ -2856,10 +2858,10 @@ TEST_F(BuilderTest, ComplexStartEndPaddingLTR) { loadDocument(START_END_PADDING_OVERRIDE); - auto frame = component->findComponentById("paddedFrame"); - auto frame2 = component->findComponentById("paddedFrame2"); - auto child = component->findComponentById("paddedFrameChild"); - auto child2 = component->findComponentById("paddedFrameChild2"); + auto frame = root->findComponentById("paddedFrame"); + auto frame2 = root->findComponentById("paddedFrame2"); + auto child = root->findComponentById("paddedFrameChild"); + auto child2 = root->findComponentById("paddedFrameChild2"); ASSERT_EQ(Object::NULL_OBJECT(), frame->getCalculated(kPropertyPaddingBottom)); ASSERT_EQ(Object::NULL_OBJECT(), frame->getCalculated(kPropertyPaddingLeft)); ASSERT_EQ(10.0, frame->getCalculated(kPropertyPaddingRight).asNumber()); @@ -2931,10 +2933,10 @@ TEST_F(BuilderTest, DynamicStartEndPaddingLTR) { loadDocument(START_END_NO_PADDING_OVERRIDE); - auto frame = CoreComponent::cast(component->findComponentById("paddedFrame")); - auto frame2 = CoreComponent::cast(component->findComponentById("paddedFrame2")); - auto child = CoreComponent::cast(component->findComponentById("paddedFrameChild")); - auto child2 = CoreComponent::cast(component->findComponentById("paddedFrameChild2")); + auto frame = CoreComponent::cast(root->findComponentById("paddedFrame")); + auto frame2 = CoreComponent::cast(root->findComponentById("paddedFrame2")); + auto child = CoreComponent::cast(root->findComponentById("paddedFrameChild")); + auto child2 = CoreComponent::cast(root->findComponentById("paddedFrameChild2")); //Check setting End and the right doesn't apply the right padding { @@ -3051,10 +3053,10 @@ TEST_F(BuilderTest, BasicStartEndPaddingRTL) { loadDocument(START_END_PADDING_OVERRIDE_RTL); - auto frame = component->findComponentById("paddedFrame"); - auto frame2 = component->findComponentById("paddedFrame2"); - auto child = component->findComponentById("paddedFrameChild"); - auto child2 = component->findComponentById("paddedFrameChild2"); + auto frame = root->findComponentById("paddedFrame"); + auto frame2 = root->findComponentById("paddedFrame2"); + auto child = root->findComponentById("paddedFrameChild"); + auto child2 = root->findComponentById("paddedFrameChild2"); EXPECT_TRUE(expectBounds(frame, 0, 380, 200, 500)); EXPECT_TRUE(expectInnerBounds(frame, 0, 20, 200, 110)); @@ -3073,14 +3075,14 @@ TEST_F(BuilderTest, ComplexDynamicStartEndPaddingRTL) { loadDocument(START_END_PADDING_OVERRIDE); - auto cont = CoreComponent::cast(component->findComponentById("cont")); + auto cont = CoreComponent::cast(root->findComponentById("cont")); cont->setProperty(kPropertyLayoutDirectionAssigned, "RTL"); root->clearPending(); - auto frame = component->findComponentById("paddedFrame"); - auto frame2 = component->findComponentById("paddedFrame2"); - auto child = component->findComponentById("paddedFrameChild"); - auto child2 = component->findComponentById("paddedFrameChild2"); + auto frame = root->findComponentById("paddedFrame"); + auto frame2 = root->findComponentById("paddedFrame2"); + auto child = root->findComponentById("paddedFrameChild"); + auto child2 = root->findComponentById("paddedFrameChild2"); EXPECT_TRUE(expectBounds(frame, 0, 380, 200, 500)); EXPECT_TRUE(expectInnerBounds(frame, 0, 20, 200, 110)); @@ -3136,7 +3138,7 @@ TEST_F(BuilderTest, PositionTypeRelativeToAbsolute) { loadDocument(POSITION_TYPE_TEST); - auto cont = CoreComponent::cast(component->findComponentById("frameComp1")); + auto cont = CoreComponent::cast(root->findComponentById("frameComp1")); cont->setProperty(kPropertyRight, 0); EXPECT_TRUE(expectBounds(cont, 0, 0, 100, 100)); @@ -3186,8 +3188,8 @@ TEST_F(BuilderTest, PositionTypeRelativeToAbsoluteStartEndInsets) { loadDocument(POSITION_TYPE_TEST); - auto cont = CoreComponent::cast(component->findComponentById("frameComp1")); - auto containerComp = CoreComponent::cast(component->findComponentById("containerComp")); + auto cont = CoreComponent::cast(root->findComponentById("frameComp1")); + auto containerComp = CoreComponent::cast(root->findComponentById("containerComp")); cont->setProperty(kPropertyStart, 10); cont->setProperty(kPropertyRight, 20); diff --git a/unit/engine/unittest_builder_bind.cpp b/aplcore/unit/engine/unittest_builder_bind.cpp similarity index 100% rename from unit/engine/unittest_builder_bind.cpp rename to aplcore/unit/engine/unittest_builder_bind.cpp diff --git a/unit/engine/unittest_builder_config_change.cpp b/aplcore/unit/engine/unittest_builder_config_change.cpp similarity index 88% rename from unit/engine/unittest_builder_config_change.cpp rename to aplcore/unit/engine/unittest_builder_config_change.cpp index 7d3e010..51b51c3 100644 --- a/unit/engine/unittest_builder_config_change.cpp +++ b/aplcore/unit/engine/unittest_builder_config_change.cpp @@ -87,6 +87,14 @@ TEST_F(BuilderConfigChange, CheckEnvironment) loadDocument(CHECK_ENVIRONMENT); ASSERT_TRUE(component); + // Empty change + configChange(ConfigurationChange()); + ASSERT_FALSE(root->hasEvent()); + + // Just theme, to existing one + configChange(ConfigurationChange().theme("dark")); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 100, "dark", "hub", false, 1.0, "normal", false, false, false)); + // Rotate the screen configChange(ConfigurationChange(200,100)); ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 200, 100, "dark", "hub", false, 1.0, "normal", false, true, true)); @@ -111,6 +119,12 @@ TEST_F(BuilderConfigChange, CheckEnvironment) configChange(ConfigurationChange().disallowVideo(true)); ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "auto", true, 3.0, "high-contrast", true, false, false)); + + configChange(ConfigurationChange().mode("tv")); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "tv", true, 3.0, "high-contrast", true, false, false)); + + configChange(ConfigurationChange().screenMode("normal")); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "tv", true, 3.0, "normal", true, false, false)); } TEST_F(BuilderConfigChange, NoopConfigurationChangeDoesNotCreateEvent) @@ -122,6 +136,26 @@ TEST_F(BuilderConfigChange, NoopConfigurationChangeDoesNotCreateEvent) ASSERT_FALSE(root->hasEvent()); } +TEST_F(BuilderConfigChange, InvalidConfigurationChangeEmitsLog) +{ + loadDocument(CHECK_ENVIRONMENT); + ASSERT_TRUE(component); + + // Clear logs + LogMessage(); + configChange(ConfigurationChange().mode("foo")); + // Assert log is emitted + ASSERT_TRUE(LogMessage()); + ASSERT_FALSE(root->hasEvent()); + + // Clear logs + LogMessage(); + configChange(ConfigurationChange().screenMode("")); + // Assert log is emitted + ASSERT_TRUE(LogMessage()); + ASSERT_FALSE(root->hasEvent()); +} + static const char *CHECK_CUSTOM_ENVIRONMENT = R"apl( { "type": "APL", @@ -232,6 +266,62 @@ TEST_F(BuilderConfigChange, Basic) ASSERT_TRUE(IsEqual("reinflation", evaluate(*context, "${environment.reason}"))); } +static const char *BASIC_REINFLATE_WITH_DROP = R"apl( + { + "type": "APL", + "version": "1.5", + "resources": [ + { + "colors": { + "BKGND": "blue" + } + }, + { + "when": "${viewport.width < viewport.height}", + "colors": { + "BKGND": "red" + } + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "backgroundColor": "@BKGND" + } + }, + "onConfigChange": { "type": "Reinflate" }, + "onMount": { + "type": "SendEvent", + "delay": 200, + "sequencer": "MOUNT_SEQUENCER" + } + } +)apl"; + +/** + * Rebuild the DOM and verify that resources change appropriately with viewport size + pending + * command drops. + */ +TEST_F(BuilderConfigChange, BasicWithDrop) +{ + metrics.size(1000,500); + loadDocument(BASIC_REINFLATE_WITH_DROP); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), component->getCalculated(kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual("initial", evaluate(*context, "${environment.reason}"))); + + advanceTime(100); + + configChangeReinflate(ConfigurationChange(500, 1000)); + + advanceTime(100); + + component = CoreComponent::cast(root->topComponent()); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Color(Color::RED), component->getCalculated(kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual("reinflation", evaluate(*context, "${environment.reason}"))); +} + static const char *ALL_SETTINGS = R"apl( { @@ -457,8 +547,6 @@ TEST_F(BuilderConfigChange, StopEventsPopped) ASSERT_EQ(0, component->getCalculated(kPropertyCurrentPage).getInteger()); // Still on page zero } - - static const char *SINGLE_COMPONENT = R"apl( { "type": "APL", @@ -472,7 +560,6 @@ static const char *SINGLE_COMPONENT = R"apl( } )apl"; - /** * After a configuration change the old components should go away UNLESS someone is holding * onto a reference to them. @@ -650,6 +737,35 @@ TEST_F(BuilderConfigChange, DefaultResizeBehavior) ASSERT_TRUE(CheckDirty(root, component)); } +static const char *SINGLE_RELATIVE_COMPONENT = R"apl( + { + "type": "APL", + "version": "1.5", + "mainTemplate": { + "item": { + "type": "Frame", + "height": "100%", + "width": "100%" + } + } + } +)apl"; + +TEST_F(BuilderConfigChange, OtherDpi) { + metrics.size(400,400).theme("light").dpi(320);; + + loadDocument(SINGLE_RELATIVE_COMPONENT); + ASSERT_TRUE(component); + + ASSERT_EQ(Rect(0,0,200,200), component->getCalculated(apl::kPropertyBounds).get()); + + // Verify that changing theme changes only theme + configChange(ConfigurationChange().theme("dark")); + root->clearPending(); + + ASSERT_EQ(Rect(0,0,200,200), component->getCalculated(apl::kPropertyBounds).get()); +} + static const char *ON_CONFIG_CHANGE_NO_RELAYOUT = R"apl( { @@ -860,7 +976,7 @@ TEST_F(BuilderConfigChange, ReinflateWithHandleTick) ASSERT_TRUE(component); ASSERT_EQ(Rect({0,0,200,200}), component->getCalculated(kPropertyBounds).get()); - auto text = component->findComponentById("textField"); + auto text = root->findComponentById("textField"); ASSERT_TRUE(text); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ(0.5, text->getCalculated(kPropertyOpacity).getDouble()); @@ -872,7 +988,7 @@ TEST_F(BuilderConfigChange, ReinflateWithHandleTick) ASSERT_TRUE(component); ASSERT_EQ(Rect({0,0,300,300}), component->getCalculated(kPropertyBounds).get()); - text = component->findComponentById("textField"); + text = root->findComponentById("textField"); ASSERT_TRUE(text); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ(0.5, text->getCalculated(kPropertyOpacity).getDouble()); diff --git a/unit/engine/unittest_builder_padding.cpp b/aplcore/unit/engine/unittest_builder_padding.cpp similarity index 100% rename from unit/engine/unittest_builder_padding.cpp rename to aplcore/unit/engine/unittest_builder_padding.cpp diff --git a/unit/engine/unittest_builder_pager.cpp b/aplcore/unit/engine/unittest_builder_pager.cpp similarity index 100% rename from unit/engine/unittest_builder_pager.cpp rename to aplcore/unit/engine/unittest_builder_pager.cpp diff --git a/unit/engine/unittest_builder_preserve.cpp b/aplcore/unit/engine/unittest_builder_preserve.cpp similarity index 100% rename from unit/engine/unittest_builder_preserve.cpp rename to aplcore/unit/engine/unittest_builder_preserve.cpp diff --git a/unit/engine/unittest_builder_preserve_scroll.cpp b/aplcore/unit/engine/unittest_builder_preserve_scroll.cpp similarity index 79% rename from unit/engine/unittest_builder_preserve_scroll.cpp rename to aplcore/unit/engine/unittest_builder_preserve_scroll.cpp index ed28147..2ea8ebe 100644 --- a/unit/engine/unittest_builder_preserve_scroll.cpp +++ b/aplcore/unit/engine/unittest_builder_preserve_scroll.cpp @@ -134,6 +134,322 @@ TEST_F(BuilderPreserveScrollTest, ScrollViewPercent) ASSERT_TRUE(IsEqual(Dimension(50), component->getCalculated(kPropertyScrollPosition))); // This is 50% of the NEW height } +static const char *SCROLL_VIEW_INSIDE_PAGER = R"( + { + "type": "APL", + "version": "2022.2", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "parameters": [ "preserve" ], + "items": [ + { + "type": "Pager", + "width": "100vw", + "height": "100vh", + "preserve": ["pageId"], + "id": "pager", + "data": [ + "red", + "blue", + "green" + ], + "item": { + "type": "ScrollView", + "width": "100%", + "height": "100%", + "id": "sv-${data}", + "preserve": [ + "${preserve}" + ], + "item": { + "type": "Frame", + "width": 100, + "height": "500vh", + "borderWidth": 2, + "borderColor": "${data}" + } + } + } + ] + } + } + )"; + +TEST_F(BuilderPreserveScrollTest, PreserveScrollPercentFirstPage) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER, R"({"preserve": "scrollPercent"})"); + + ASSERT_TRUE(component); + auto sv = root->findComponentById("sv-red"); + ASSERT_TRUE(sv); + + ASSERT_TRUE(IsEqual(Dimension(0), sv->getCalculated(kPropertyScrollPosition))); + sv->update(kUpdateScrollPosition, 100); // 50% of viewport height + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); + + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + sv = root->findComponentById("sv-red"); + + ASSERT_TRUE(sv); + ASSERT_TRUE(IsEqual(Dimension(200), sv->getCalculated(kPropertyScrollPosition))); // scroll position is still half of the window +} + +TEST_F(BuilderPreserveScrollTest, PreserveScrollPercentSecondPage) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER, R"({"preserve": "scrollPercent"})"); + ASSERT_TRUE(component); + + // Switch to page 2 + executeCommand("SetPage", {{"componentId", "pager"}, {"value", 1}}, false); + advanceTime(1000); + + auto sv = root->findComponentById("sv-blue"); + ASSERT_TRUE(sv); + + ASSERT_TRUE(IsEqual(Dimension(0), sv->getCalculated(kPropertyScrollPosition))); + sv->update(kUpdateScrollPosition, 100); // 50% of viewport height + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); + + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + sv = root->findComponentById("sv-blue"); + + ASSERT_TRUE(sv); + ASSERT_TRUE(IsEqual(Dimension(200), sv->getCalculated(kPropertyScrollPosition))); // scroll position is still half of the window +} + +TEST_F(BuilderPreserveScrollTest, PreserveScrollPercentHiddenPage) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER, R"({"preserve": "scrollPercent"})"); + ASSERT_TRUE(component); + + + auto red = root->findComponentById("sv-red"); + ASSERT_TRUE(red); + + // Scroll the first page + ASSERT_TRUE(IsEqual(Dimension(0), red->getCalculated(kPropertyScrollPosition))); + red->update(kUpdateScrollPosition, 100); // 50% of viewport height + ASSERT_TRUE(IsEqual(Dimension(100), red->getCalculated(kPropertyScrollPosition))); + + // Switch to page 2 + executeCommand("SetPage", {{"componentId", "pager"}, {"value", 1}}, false); // switch to page 2 + advanceTime(1000); + + // Scroll the second page + auto blue = root->findComponentById("sv-blue"); + ASSERT_TRUE(blue); + + ASSERT_TRUE(IsEqual(Dimension(0), blue->getCalculated(kPropertyScrollPosition))); + blue->update(kUpdateScrollPosition, 200); // 100% of viewport height + ASSERT_TRUE(IsEqual(Dimension(200), blue->getCalculated(kPropertyScrollPosition))); + + // Reinflate + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + // Check the second page + blue = root->findComponentById("sv-blue"); + ASSERT_TRUE(blue); + ASSERT_TRUE(IsEqual(Dimension(400), blue->getCalculated(kPropertyScrollPosition))); // scroll position is still 100% of the window + + // Switch to page 1 + executeCommand("SetPage", {{"componentId", "pager"}, {"value", 0}}, false); + advanceTime(1000); + + // Check the first page + red = root->findComponentById("sv-red"); + ASSERT_TRUE(red); + ASSERT_TRUE(IsEqual(Dimension(200), red->getCalculated(kPropertyScrollPosition))); // scroll position is still 50% of the window +} + +TEST_F(BuilderPreserveScrollTest, PreserveScrollOffsetFirstPage) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER, R"({"preserve": "scrollOffset"})"); + advanceTime(500); + + ASSERT_TRUE(component); + auto sv = root->findComponentById("sv-red"); + ASSERT_TRUE(sv); + + ASSERT_TRUE(IsEqual(Dimension(0), sv->getCalculated(kPropertyScrollPosition))); + sv->update(kUpdateScrollPosition, 100); + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); + advanceTime(1000); + + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + sv = root->findComponentById("sv-red"); + + ASSERT_TRUE(sv); + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); // scroll position is still 100 +} + +TEST_F(BuilderPreserveScrollTest, PreserveScrollOffsetSecondPage) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER, R"({"preserve": "scrollOffset"})"); + ASSERT_TRUE(component); + + // Switch to page 2 + executeCommand("SetPage", {{"componentId", "pager"}, {"value", 1}}, false); + advanceTime(1000); + + auto sv = root->findComponentById("sv-blue"); + ASSERT_TRUE(sv); + + ASSERT_TRUE(IsEqual(Dimension(0), sv->getCalculated(kPropertyScrollPosition))); + sv->update(kUpdateScrollPosition, 100); + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); + + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + sv = root->findComponentById("sv-blue"); + + ASSERT_TRUE(sv); + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); // scroll position is still 100 +} + +TEST_F(BuilderPreserveScrollTest, PreserveScrollOffsetHiddenPage) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER, R"({"preserve": "scrollOffset"})"); + ASSERT_TRUE(component); + + + auto red = root->findComponentById("sv-red"); + ASSERT_TRUE(red); + + // Scroll the first page + ASSERT_TRUE(IsEqual(Dimension(0), red->getCalculated(kPropertyScrollPosition))); + red->update(kUpdateScrollPosition, 100); + ASSERT_TRUE(IsEqual(Dimension(100), red->getCalculated(kPropertyScrollPosition))); + + // Switch to page 2 + executeCommand("SetPage", {{"componentId", "pager"}, {"value", 1}}, false); // switch to page 2 + advanceTime(1000); + + // Scroll the second page + auto blue = root->findComponentById("sv-blue"); + ASSERT_TRUE(blue); + + ASSERT_TRUE(IsEqual(Dimension(0), blue->getCalculated(kPropertyScrollPosition))); + blue->update(kUpdateScrollPosition, 150); + ASSERT_TRUE(IsEqual(Dimension(150), blue->getCalculated(kPropertyScrollPosition))); + + // Reinflate + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + // Check the second page + blue = root->findComponentById("sv-blue"); + ASSERT_TRUE(blue); + ASSERT_TRUE(IsEqual(Dimension(150), blue->getCalculated(kPropertyScrollPosition))); // scroll position is still 150 + + // Switch to page 1 + executeCommand("SetPage", {{"componentId", "pager"}, {"value", 0}}, false); + advanceTime(1000); + + // Check the first page + red = root->findComponentById("sv-red"); + ASSERT_TRUE(red); + ASSERT_TRUE(IsEqual(Dimension(100), red->getCalculated(kPropertyScrollPosition))); // scroll position is still 100 +} + +static const char *SCROLL_VIEW_INSIDE_PAGER_INDIRECT = R"( + { + "type": "APL", + "version": "2022.2", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "parameters": [ "preserve" ], + "items": [ + { + "type": "Pager", + "width": "100vw", + "height": "100vh", + "preserve": ["pageId"], + "id": "pager", + "data": [ + "red", + "blue", + "green" + ], + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "ScrollView", + "width": "100%", + "height": "100%", + "id": "sv-${data}", + "preserve": [ + "${preserve}" + ], + "item": { + "type": "Frame", + "width": 100, + "height": "500vh", + "borderWidth": 2, + "borderColor": "${data}" + } + } + } + } + ] + } + } + )"; + +TEST_F(BuilderPreserveScrollTest, PreserveScrollPercentFirstPageIndirect) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER_INDIRECT, R"({"preserve": "scrollPercent"})"); + + ASSERT_TRUE(component); + auto sv = root->findComponentById("sv-red"); + ASSERT_TRUE(sv); + + ASSERT_TRUE(IsEqual(Dimension(0), sv->getCalculated(kPropertyScrollPosition))); + sv->update(kUpdateScrollPosition, 100); // 50% of viewport height + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); + + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + sv = root->findComponentById("sv-red"); + + ASSERT_TRUE(sv); + ASSERT_TRUE(IsEqual(Dimension(200), sv->getCalculated(kPropertyScrollPosition))); // scroll position is still half of the window +} + +TEST_F(BuilderPreserveScrollTest, PreserveScrollOffsetFirstPageIndirect) +{ + metrics.size(400, 200); // landscape + loadDocument(SCROLL_VIEW_INSIDE_PAGER_INDIRECT, R"({"preserve": "scrollOffset"})"); + + ASSERT_TRUE(component); + auto sv = root->findComponentById("sv-red"); + ASSERT_TRUE(sv); + + ASSERT_TRUE(IsEqual(Dimension(0), sv->getCalculated(kPropertyScrollPosition))); + sv->update(kUpdateScrollPosition, 100); + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); + + configChangeReinflate(ConfigurationChange(200, 400)); // portrait + + sv = root->findComponentById("sv-red"); + + ASSERT_TRUE(sv); + ASSERT_TRUE(IsEqual(Dimension(100), sv->getCalculated(kPropertyScrollPosition))); // scroll position is still 100 +} static const char *SCROLL_VIEW_CANCEL_SCROLL_COMMAND = R"apl( { @@ -314,7 +630,6 @@ TEST_F(BuilderPreserveScrollTest, ScrollViewCancelNativeScrolling) ASSERT_TRUE(IsEqual(Dimension(20), component->getCalculated(kPropertyScrollPosition))); } - const static char *SEQUENCE_PRESERVE_FIRST_INDEX = R"apl( { "type": "APL", diff --git a/unit/engine/unittest_builder_sequence.cpp b/aplcore/unit/engine/unittest_builder_sequence.cpp similarity index 100% rename from unit/engine/unittest_builder_sequence.cpp rename to aplcore/unit/engine/unittest_builder_sequence.cpp diff --git a/unit/engine/unittest_context.cpp b/aplcore/unit/engine/unittest_context.cpp similarity index 68% rename from unit/engine/unittest_context.cpp rename to aplcore/unit/engine/unittest_context.cpp index 5e12298..abe9a80 100644 --- a/unit/engine/unittest_context.cpp +++ b/aplcore/unit/engine/unittest_context.cpp @@ -21,16 +21,18 @@ using namespace apl; class ContextTest : public MemoryWrapper { protected: - void SetUp() override - { - auto m = Metrics().size(2048,2048) - .dpi(320) - .theme("green") - .shape(apl::ROUND) - .mode(apl::kViewportModeTV); + void SetUp() override { + auto m = Metrics() + .size(2048, 2048) + .dpi(320) + .theme("green") + .shape(apl::ROUND) + .autoSizeWidth(true) + .autoSizeHeight(true) + .mode(apl::kViewportModeTV); auto r = RootConfig().set(RootProperty::kAgentName, "UnitTests"); r.setEnvironmentValue("testEnvironment", "23.2"); - c = Context::createTestContext(m,r); + c = Context::createTestContext(m, r); } void TearDown() override @@ -49,7 +51,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("1.0", env.get("agentVersion").asString()); EXPECT_EQ("normal", env.get("animation").asString()); EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); - EXPECT_EQ("2023.1", env.get("aplVersion").asString()); + EXPECT_EQ("2023.2", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); @@ -59,6 +61,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("", env.get("lang").asString()); EXPECT_EQ("LTR", env.get("layoutDirection").asString()); EXPECT_EQ(false, env.get("screenReader").asBoolean()); + EXPECT_EQ("2023.2", env.get("documentAPLVersion").asString()); auto timing = env.get("timing"); EXPECT_EQ(500, timing.get("doublePressTimeout").asNumber()); @@ -77,6 +80,10 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("round", viewport.get("shape").asString()); EXPECT_EQ("green", viewport.get("theme").asString()); EXPECT_EQ(Object("tv"), viewport.get("mode")); + EXPECT_EQ(true, viewport.get("autoWidth").asBoolean()); + EXPECT_EQ(true, viewport.get("autoHeight").asBoolean()); + + EXPECT_TRUE(env.has("extension")); EXPECT_TRUE(c->opt("Math").get("asin").is()); @@ -91,6 +98,89 @@ TEST_F(ContextTest, Basic) ASSERT_NE("unknown", buildVersion.c_str()); } +TEST_F(ContextTest, Evaluation) +{ + auto r = RootConfig().set(RootProperty::kAgentName, "UnitTests"); + r.setEnvironmentValue("testEnvironment", "23.2"); + c = Context::createTypeEvaluationContext(r, APLVersion::getDefaultReportedVersionString(), session); + + auto env = c->opt("environment"); + EXPECT_EQ("UnitTests", env.get("agentName").asString()); + EXPECT_EQ("1.0", env.get("agentVersion").asString()); + EXPECT_EQ("normal", env.get("animation").asString()); + EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); + EXPECT_EQ("2023.2", env.get("aplVersion").asString()); + EXPECT_FALSE(env.get("disallowDialog").asBoolean()); + EXPECT_FALSE(env.get("disallowEditText").asBoolean()); + EXPECT_FALSE(env.get("disallowVideo").asBoolean()); + EXPECT_EQ("23.2", env.get("testEnvironment").asString()); + EXPECT_EQ(1.0, env.get("fontScale").asNumber()); + EXPECT_EQ("normal", env.get("screenMode").asString()); + EXPECT_EQ("", env.get("lang").asString()); + EXPECT_EQ("LTR", env.get("layoutDirection").asString()); + EXPECT_EQ(false, env.get("screenReader").asBoolean()); + + auto timing = env.get("timing"); + EXPECT_EQ(500, timing.get("doublePressTimeout").asNumber()); + EXPECT_EQ(1000, timing.get("longPressTimeout").asNumber()); + EXPECT_EQ(50, timing.get("minimumFlingVelocity").asNumber()); + EXPECT_EQ(64, timing.get("pressedDuration").asNumber()); + EXPECT_EQ(100, timing.get("tapOrScrollTimeout").asNumber()); + EXPECT_EQ(50, timing.get("maximumTapVelocity").asNumber()); + + auto viewport = c->opt("viewport"); + EXPECT_EQ(1024, viewport.get("pixelWidth").asNumber()); + EXPECT_EQ(1024, viewport.get("width").asNumber()); + EXPECT_EQ(800, viewport.get("pixelHeight").asNumber()); + EXPECT_EQ(800, viewport.get("height").asNumber()); + EXPECT_EQ(160, viewport.get("dpi").asNumber()); + EXPECT_EQ("rectangle", viewport.get("shape").asString()); + EXPECT_EQ("dark", viewport.get("theme").asString()); + EXPECT_EQ(Object("hub"), viewport.get("mode")); + + EXPECT_FALSE(env.has("extension")); + + EXPECT_TRUE(c->opt("Math").get("asin").is()); + + // Dry-run can't really play with EXPECT_DEATH :( +// EXPECT_DEATH(c->vwToDp(1), ""); +// EXPECT_DEATH(c->vhToDp(1), ""); +// EXPECT_DEATH(c->pxToDp(1), ""); +// EXPECT_DEATH(c->dpToPx(1), ""); +// EXPECT_DEATH(c->width(), ""); +// EXPECT_DEATH(c->height(), ""); +// EXPECT_DEATH(c->getStyle("name", State()), ""); +// EXPECT_DEATH(c->getLayout("name"), ""); +// EXPECT_DEATH(c->getCommand("name"), ""); +// EXPECT_DEATH(c->getGraphic("name"), ""); +// EXPECT_DEATH(c->getRequestedAPLVersion(), ""); +// EXPECT_DEATH(c->styles(), ""); +// EXPECT_DEATH(c->findComponentById("id"), ""); +// EXPECT_DEATH(c->topComponent(), ""); +// EXPECT_DEATH(c->sequencer(), ""); +// EXPECT_DEATH(c->focusManager(), ""); +// EXPECT_DEATH(c->hoverManager(), ""); +// EXPECT_DEATH(c->dataManager(), ""); +// EXPECT_DEATH(c->extensionManager(), ""); +// EXPECT_DEATH(c->layoutManager(), ""); +// EXPECT_DEATH(c->mediaManager(), ""); +// EXPECT_DEATH(c->mediaPlayerFactory(), ""); +// EXPECT_DEATH(c->uniqueIdManager(), ""); +// EXPECT_DEATH(c->ygconfig(), ""); +// EXPECT_DEATH(c->measure(), ""); +// EXPECT_DEATH(c->takeScreenLock(), ""); +// EXPECT_DEATH(c->releaseScreenLock(), ""); +// EXPECT_DEATH(c->cachedMeasures(), ""); +// EXPECT_DEATH(c->cachedBaselines(), ""); +// EXPECT_DEATH(c->pendingOnMounts(), ""); + + EXPECT_EQ(APLVersion(APLVersion::kAPLVersionIgnore), c->getRootConfig().getEnforcedAPLVersion()); + + auto buildVersion = env.get("_coreRepositoryVersion").asString(); + ASSERT_FALSE(buildVersion.empty()); + ASSERT_NE("unknown", buildVersion.c_str()); +} + TEST_F(ContextTest, AlternativeConfig) { auto root = RootConfig() @@ -168,6 +258,25 @@ TEST_F(ContextTest, Shape) }) { c = Context::createTestContext(Metrics().shape(m.first), session); ASSERT_EQ(Object(m.second), c->opt("viewport").get("shape")) << m.second; + // Use string setter + c = Context::createTestContext(Metrics().shape(m.second), session); + ASSERT_EQ(Object(m.second), c->opt("viewport").get("shape")) << m.second; + ASSERT_FALSE(LogMessage()); + } +} + +TEST_F(ContextTest, UnknownShapeString) +{ + for (auto m : std::vector{ + "foo", + "unknown", + "12 34", + "" + }) { + c = Context::createTestContext(Metrics().shape(m), session); + ASSERT_EQ(Object("rectangle"), c->opt("viewport").get("shape")) << m; + // Complain that shape wasn't set properly + ASSERT_TRUE(LogMessage()); } } @@ -182,9 +291,44 @@ TEST_F(ContextTest, Mode) }) { c = Context::createTestContext(Metrics().mode(m.first), session); ASSERT_EQ(Object(m.second), c->opt("viewport").get("mode")) << m.second; + // Use string setter + c = Context::createTestContext(Metrics().mode(m.second), session); + ASSERT_EQ(Object(m.second), c->opt("viewport").get("mode")) << m.second; + ASSERT_FALSE(LogMessage()); } } +TEST_F(ContextTest, UnknownModeString) +{ + for (auto m : std::vector{ + "foo", + "unknown", + "12 34", + "" + }) { + c = Context::createTestContext(Metrics().mode(m), session); + ASSERT_EQ(Object("hub"), c->opt("viewport").get("mode")) << m; + // Complain that mode wasn't set properly + ASSERT_TRUE(LogMessage()); + } +} + +TEST_F(ContextTest, AutoSize) { + auto localTest = [&](bool width, bool height) -> ::testing::AssertionResult { + auto c = Context::createTestContext(Metrics().autoSizeWidth(width).autoSizeHeight(height), session); + if (width != c->opt("viewport").get("autoWidth").asBoolean()) + return ::testing::AssertionFailure() << "Incorrect width"; + if (height != c->opt("viewport").get("autoHeight").asBoolean()) + return ::testing::AssertionFailure() << "Incorrect height"; + return ::testing::AssertionSuccess(); + }; + + ASSERT_TRUE(localTest(false, false)); + ASSERT_TRUE(localTest(true, false)); + ASSERT_TRUE(localTest(false, true)); + ASSERT_TRUE(localTest(true, true)); +} + static const char * TIME_DOC = "{" " \"type\": \"APL\"," @@ -294,14 +438,16 @@ TEST_F(ContextTest, LangAndLayoutDirectionCheck) * Verify standard functions are included for type-evaluation contexts, but not for * the background evaluation context. */ -TEST_F(ContextTest, NoStandardFunction) -{ +TEST_F(ContextTest, NoStandardFunction) { auto rootConfig = RootConfig(); auto metrics = Metrics(); auto session = makeDefaultSession(); - auto ctx1 = Context::createTypeEvaluationContext(rootConfig); - auto ctx2 = Context::createBackgroundEvaluationContext(metrics, rootConfig, metrics.getTheme()); + auto ctx1 = Context::createTypeEvaluationContext( + rootConfig, APLVersion::getDefaultReportedVersionString(), session); + auto ctx2 = Context::createBackgroundEvaluationContext( + metrics, rootConfig, APLVersion::getDefaultReportedVersionString(), metrics.getTheme(), + session); ASSERT_FALSE(ctx1->opt("Array").empty()); ASSERT_FALSE(ctx1->opt("Math").empty()); @@ -318,7 +464,7 @@ TEST_F(ContextTest, TrivialMethodChecks) { auto rootConfig = RootConfig().set(RootProperty::kLang, "de-DE"); auto content = Content::create(BASIC_ENV_DOC); - auto root = RootContext::create(Metrics().theme("dark"), content, rootConfig); + auto root = std::static_pointer_cast(RootContext::create(Metrics().theme("dark"), content, rootConfig)); ASSERT_EQ(std::string("de-DE"), root->getRootConfig().getProperty(RootProperty::kLang).asString()); ASSERT_EQ(std::string("dark"), root->getTheme()); @@ -443,8 +589,7 @@ static const char *INVALID_ENVIRONMENT_PARAMETER = R"apl( TEST_F(ContextTest, InvalidEnvironmentParameter) { auto rootConfig = RootConfig(); - rootConfig.session(session); - auto content = Content::create(INVALID_ENVIRONMENT_PARAMETER); + auto content = Content::create(INVALID_ENVIRONMENT_PARAMETER, session); content->addData("0_payload", R"({"lang": "en-ES", "layoutDirection": "RTL"})" ); auto root = RootContext::create(Metrics(), content, rootConfig); ASSERT_TRUE(root); diff --git a/aplcore/unit/engine/unittest_context_apl_version.cpp b/aplcore/unit/engine/unittest_context_apl_version.cpp new file mode 100644 index 0000000..7fa1b82 --- /dev/null +++ b/aplcore/unit/engine/unittest_context_apl_version.cpp @@ -0,0 +1,64 @@ +/* + * Copyright 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 "../testeventloop.h" + +using namespace apl; + +/** + * These tests are to verify that the data-binding context is created with the APL version specified + * by the APL document. A number of features are gated upon the version of APL requested by the + * document. The environment variables of the context report APL version in two places: + * + * environment.aplVersion + * + * This is the reported APL version. By default it is set to the current (most recent) version + * in core. It can be overridden by calling: + * + * rootConfig.set(RootProperty::kReportedVersion, STRING); + * + * environment.documentAPLVersion + * + * This is the version of APL specified by the APL document. This applies to data-binding + * contexts that have been created when the document is known. Some contexts are only used + * for simple evaluations; in those cases they default to the current Core APL version. + */ +class ContextAPLVersionTest : public DocumentWrapper {}; + +static const char *BASIC = R"( +{ + "type": "APL", + "version": "1.9", + "background": "${environment.documentAPLVersion == '1.9' ? 'red' : 'blue' }", + "mainTemplate": { + "item": { + "type": "Text", + "text": "" + } + } +})"; + +TEST_F(ContextAPLVersionTest, Basic) +{ + loadDocument(BASIC); + auto context = component->getContext(); + ASSERT_EQ("1.9", context->getRequestedAPLVersion()); + ASSERT_TRUE(IsEqual("2023.2", evaluate(*context, "${environment.aplVersion}"))); + ASSERT_TRUE(IsEqual("1.9", evaluate(*context, "${environment.documentAPLVersion}"))); + + // The document background is evaluated is a special data-binding context + ASSERT_TRUE(IsEqual(content->getBackground(Metrics(), RootConfig()), Color(Color::RED))); +} \ No newline at end of file diff --git a/unit/engine/unittest_current_time.cpp b/aplcore/unit/engine/unittest_current_time.cpp similarity index 100% rename from unit/engine/unittest_current_time.cpp rename to aplcore/unit/engine/unittest_current_time.cpp diff --git a/unit/engine/unittest_dependant.cpp b/aplcore/unit/engine/unittest_dependant.cpp similarity index 94% rename from unit/engine/unittest_dependant.cpp rename to aplcore/unit/engine/unittest_dependant.cpp index 69324a4..d10f2a3 100644 --- a/unit/engine/unittest_dependant.cpp +++ b/aplcore/unit/engine/unittest_dependant.cpp @@ -16,7 +16,8 @@ #include "../testeventloop.h" #include "apl/component/touchwrappercomponent.h" -#include "apl/engine/contextdependant.h" +#include "apl/engine/typeddependant.h" +#include "apl/datagrammar/bytecode.h" using namespace apl; @@ -256,10 +257,13 @@ TEST_F(DependantTest, FreeContext) ASSERT_EQ(10, second->opt("target").asNumber()); // Manually construct a dependency between source and target - auto node = parseDataBinding(*first, "${source * 2}"); - ASSERT_TRUE(node.isEvaluable()); + auto result = parseAndEvaluate(*first, "${source * 2}"); + ASSERT_TRUE(IsEqual(result.value, 46)); + ASSERT_TRUE(result.expression.isEvaluable()); + ASSERT_EQ(1, result.symbols.size()); auto bf = sBindingFunctions.at(BindingType::kBindingTypeNumber); - ContextDependant::create(second, "target", node, second, bf); + ContextDependant::create(second, "target", std::move(result.expression), first, bf, + std::move(result.symbols)); // Test that changing the source now changes the target ASSERT_TRUE(first->userUpdateAndRecalculate("source", 10, false)); @@ -371,6 +375,74 @@ TEST_F(DependantTest, BreakChain) ASSERT_EQ(1, component->countUpstream(kPropertyText)); } +static const char *REATTACH = R"( +{ + "type": "APL", + "version": "2023.1", + "mainTemplate": { + "items": { + "type": "Text", + "bind": [ + { + "name": "Rodent", + "value": true + }, + { + "name": "HasTail", + "value": false + }, + { + "name": "Tailful", + "value": "Rat" + }, + { + "name": "Tailless", + "value": "Hamster" + }, + { + "name": "WagsTail", + "value": false + }, + { + "name": "Waggly", + "value": "Dog" + }, + { + "name": "NotWaggly", + "value": "Cat" + } + ], + "text": "${Rodent ? (HasTail ? Tailful : Tailless) : (WagsTail ? Waggly : NotWaggly)}" + } + } +})"; + +TEST_F(DependantTest, Reattach) +{ + loadDocument(REATTACH); + ASSERT_TRUE(component); + auto c = component->getContext(); + + ASSERT_TRUE(IsEqual("Hamster", component->getCalculated(apl::kPropertyText).asString())); + ASSERT_EQ(1, c->countDownstream("Rodent")); + ASSERT_EQ(1, c->countDownstream("HasTail")); + ASSERT_EQ(0, c->countDownstream("Tailful")); + ASSERT_EQ(1, c->countDownstream("Tailless")); + ASSERT_EQ(0, c->countDownstream("WagsTail")); + ASSERT_EQ(0, c->countDownstream("Waggly")); + ASSERT_EQ(0, c->countDownstream("NotWaggly")); + + ASSERT_TRUE(c->userUpdateAndRecalculate("Rodent", false, false)); + ASSERT_EQ(1, c->countDownstream("Rodent")); + ASSERT_EQ(0, c->countDownstream("HasTail")); + ASSERT_EQ(0, c->countDownstream("Tailful")); + ASSERT_EQ(0, c->countDownstream("Tailless")); + ASSERT_EQ(1, c->countDownstream("WagsTail")); + ASSERT_EQ(0, c->countDownstream("Waggly")); + ASSERT_EQ(1, c->countDownstream("NotWaggly")); +} + + static const char *STATIC_PROPERTY = R"({ "type": "APL", "version": "1.1", @@ -543,10 +615,10 @@ TEST_F(DependantTest, Nested) loadDocument(NESTED); ASSERT_TRUE(component); - auto wrapper = TouchWrapperComponent::cast(component->findComponentById("TouchId")); + auto wrapper = TouchWrapperComponent::cast(root->findComponentById("TouchId")); ASSERT_TRUE(wrapper); - auto text = component->findComponentById("TextId"); + auto text = root->findComponentById("TextId"); ASSERT_TRUE(text); // First, we change the parameter passed to the TestLayout to verify that the name changes correctly @@ -669,7 +741,7 @@ TEST_F(DependantTest, LayoutMissingProperty) ASSERT_TRUE(IsEqual("Count: ", text->getCalculated(kPropertyText).asString())); - // Property should still be live and writtable. + // Property should still be live and writable. text->setProperty("cnt", 1); ASSERT_TRUE(IsEqual("Count: 1", text->getCalculated(kPropertyText).asString())); ASSERT_TRUE(CheckDirty(text, kPropertyText, kPropertyVisualHash)); @@ -1070,8 +1142,8 @@ TEST_F(DependantTest, LayoutLiveArray) loadDocument(LAYOUT_LIVE_ARRAY); ASSERT_TRUE(component); - auto calculatedThings = component->findComponentById("calculatedThings"); - auto calculatedStuff = component->findComponentById("calculatedStuff"); + auto calculatedThings = root->findComponentById("calculatedThings"); + auto calculatedStuff = root->findComponentById("calculatedStuff"); ASSERT_EQ("0", calculatedThings->getCalculated(kPropertyText).asString()); diff --git a/unit/engine/unittest_display_state.cpp b/aplcore/unit/engine/unittest_display_state.cpp similarity index 96% rename from unit/engine/unittest_display_state.cpp rename to aplcore/unit/engine/unittest_display_state.cpp index 75b7e8a..ebf73bc 100644 --- a/unit/engine/unittest_display_state.cpp +++ b/aplcore/unit/engine/unittest_display_state.cpp @@ -135,12 +135,6 @@ TEST_F(DisplayStateTest, DisplayStateChangesWithoutHandlerWork) ASSERT_TRUE(IsEqual("background", evaluate(*context, "${displayState}"))); } -TEST_F(DisplayStateTest, DisplayStateChangeCommandHasExpectedName) -{ - auto command = DisplayStateChangeCommand::create(root, ObjectMap()); - ASSERT_TRUE(IsEqual("DisplayStateChangeCommand", command->name())); -} - static const char *DISPLAY_STATE_DELAY = R"apl( { "type": "APL", diff --git a/aplcore/unit/engine/unittest_event.cpp b/aplcore/unit/engine/unittest_event.cpp new file mode 100644 index 0000000..4f71579 --- /dev/null +++ b/aplcore/unit/engine/unittest_event.cpp @@ -0,0 +1,63 @@ +/** + * Copyright 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 "gtest/gtest.h" + +#include "../faketextcomponent.h" + +#include "apl/content/metrics.h" +#include "apl/engine/context.h" +#include "apl/engine/event.h" +#include "apl/utils/session.h" + +using namespace apl; + +class EventTest : public ::testing::Test {}; + +TEST_F(EventTest, Equality) +{ + auto context = Context::createTestContext(Metrics(), makeDefaultSession()); + + auto component1 = std::make_shared(context, "fake1", "fake1"); + EventBag bag1; + bag1.emplace(kEventPropertyName, "arbitraryName"); + const Event event(kEventTypeSendEvent, std::move(bag1), component1); + + EventBag bag2; + bag2.emplace(kEventPropertyName, "arbitraryName"); + const Event sameEvent(kEventTypeSendEvent, std::move(bag2), component1); + + ASSERT_EQ(event, sameEvent); + + EventBag bag3; + bag3.emplace(kEventPropertyName, "arbitraryName"); + const Event differentTypeEvent(kEventTypeOpenURL, std::move(bag3), component1); + + ASSERT_FALSE(event == differentTypeEvent); + + auto component2 = std::make_shared(context, "fake2", "fake2"); + EventBag bag4; + bag4.emplace(kEventPropertyName, "arbitraryName"); + const Event differentComponentEvent(kEventTypeSendEvent, std::move(bag4), component2); + + ASSERT_FALSE(event == differentComponentEvent); + + EventBag bag5; + bag5.emplace(kEventPropertyName, "arbitraryName"); + bag5.emplace(kEventPropertyExtensionURI, "no"); + const Event differentBagEvent(kEventTypeSendEvent, std::move(bag5), component2); + + ASSERT_FALSE(event == differentBagEvent); +} diff --git a/unit/engine/unittest_queue_event_manager.cpp b/aplcore/unit/engine/unittest_event_manager.cpp similarity index 68% rename from unit/engine/unittest_queue_event_manager.cpp rename to aplcore/unit/engine/unittest_event_manager.cpp index f02a5df..8733814 100644 --- a/unit/engine/unittest_queue_event_manager.cpp +++ b/aplcore/unit/engine/unittest_event_manager.cpp @@ -1,88 +1,88 @@ -/** -* Copyright 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 "gtest/gtest.h" - -#include "apl/engine/queueeventmanager.h" - -using namespace apl; - -class QueueEventManagerTest : public ::testing::Test -{ -protected: - QueueEventManager eventManager; -}; - -TEST_F(QueueEventManagerTest, TestPushFrontPopEmpty) -{ - ASSERT_TRUE(eventManager.empty()); - EventBag bag; - bag.emplace(kEventPropertyName, "arbitraryName"); - Event event(kEventTypeSendEvent, std::move(bag)); - eventManager.push(event); - ASSERT_FALSE(eventManager.empty()); - ASSERT_TRUE(event.matches(eventManager.front())); - ASSERT_FALSE(eventManager.empty()); - eventManager.pop(); - ASSERT_TRUE(eventManager.empty()); -} - -TEST_F(QueueEventManagerTest, TestPushFrontPopEmptyConst) -{ - ASSERT_TRUE(eventManager.empty()); - EventBag bag; - bag.emplace(kEventPropertyName, "arbitraryName"); - const Event event(kEventTypeSendEvent, std::move(bag)); - eventManager.push(event); - ASSERT_FALSE(eventManager.empty()); - ASSERT_TRUE(event.matches(eventManager.front())); - ASSERT_FALSE(eventManager.empty()); - eventManager.pop(); - ASSERT_TRUE(eventManager.empty()); -} - -TEST_F(QueueEventManagerTest, TestPushClearEmpty) -{ - ASSERT_TRUE(eventManager.empty()); - EventBag bag; - bag.emplace(kEventPropertyName, "arbitraryName"); - const Event event(kEventTypeSendEvent, std::move(bag)); - eventManager.push(event); - eventManager.push(event); - ASSERT_FALSE(eventManager.empty()); - eventManager.clear(); - ASSERT_TRUE(eventManager.empty()); -} - -TEST_F(QueueEventManagerTest, TestFIFO) -{ - ASSERT_TRUE(eventManager.empty()); - EventBag firstBag; - firstBag.emplace(kEventPropertyName, "arbitraryName"); - const Event first(kEventTypeSendEvent, std::move(firstBag)); - EventBag secondBag; - firstBag.emplace(kEventPropertyName, "differentArbitraryName"); - const Event second(kEventTypeSendEvent, std::move(secondBag)); - eventManager.push(first); - eventManager.push(second); - - ASSERT_TRUE(first.matches(eventManager.front())); - eventManager.pop(); - ASSERT_TRUE(second.matches(eventManager.front())); - eventManager.pop(); - ASSERT_TRUE(eventManager.empty()); +/** +* Copyright 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 "gtest/gtest.h" + +#include "apl/engine/eventmanager.h" + +using namespace apl; + +class EventManagerTest : public ::testing::Test +{ +protected: + EventManager eventManager; +}; + +TEST_F(EventManagerTest, TestPushFrontPopEmpty) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + Event event(kEventTypeSendEvent, std::move(bag)); + eventManager.emplace(nullptr, event); + ASSERT_FALSE(eventManager.empty()); + ASSERT_EQ(event, eventManager.front()); + ASSERT_FALSE(eventManager.empty()); + eventManager.pop(); + ASSERT_TRUE(eventManager.empty()); +} + +TEST_F(EventManagerTest, TestPushFrontPopEmptyConst) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + const Event event(kEventTypeSendEvent, std::move(bag)); + eventManager.emplace(nullptr, event); + ASSERT_FALSE(eventManager.empty()); + ASSERT_EQ(event, eventManager.front()); + ASSERT_FALSE(eventManager.empty()); + eventManager.pop(); + ASSERT_TRUE(eventManager.empty()); +} + +TEST_F(EventManagerTest, TestPushClearEmpty) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + const Event event(kEventTypeSendEvent, std::move(bag)); + eventManager.emplace(nullptr, event); + eventManager.emplace(nullptr, event); + ASSERT_FALSE(eventManager.empty()); + eventManager.clear(); + ASSERT_TRUE(eventManager.empty()); +} + +TEST_F(EventManagerTest, TestFIFO) +{ + ASSERT_TRUE(eventManager.empty()); + EventBag firstBag; + firstBag.emplace(kEventPropertyName, "arbitraryName"); + const Event first(kEventTypeSendEvent, std::move(firstBag)); + EventBag secondBag; + secondBag.emplace(kEventPropertyName, "differentArbitraryName"); + const Event second(kEventTypeSendEvent, std::move(secondBag)); + eventManager.emplace(nullptr, first); + eventManager.emplace(nullptr, second); + + ASSERT_EQ(first, eventManager.front()); + eventManager.pop(); + ASSERT_EQ(second, eventManager.front()); + eventManager.pop(); + ASSERT_TRUE(eventManager.empty()); } \ No newline at end of file diff --git a/unit/engine/unittest_hover.cpp b/aplcore/unit/engine/unittest_hover.cpp similarity index 99% rename from unit/engine/unittest_hover.cpp rename to aplcore/unit/engine/unittest_hover.cpp index 77788cb..4d395bb 100644 --- a/unit/engine/unittest_hover.cpp +++ b/aplcore/unit/engine/unittest_hover.cpp @@ -164,7 +164,7 @@ class HoverTest : public DocumentWrapper { void init(const char* frameProperties, const char* textProperties) { static char json[4096]; - sprintf(json, DOCUMENT.c_str(), frameProperties, textProperties); + snprintf(json, sizeof(json), DOCUMENT.c_str(), frameProperties, textProperties); init(json); LOG_IF(DEBUG_HOVER_TEST) << json; } @@ -217,7 +217,7 @@ class HoverTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); cmd.AddMember("distance", distance, alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void completeScroll(const ComponentPtr& component, float distance) { @@ -233,7 +233,7 @@ class HoverTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); cmd.AddMember("align", rapidjson::StringRef(sCommandAlignMap.at(align).c_str()), alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } void printBounds(const std::string& name, const ComponentPtr& component) { diff --git a/unit/engine/unittest_keyboard_manager.cpp b/aplcore/unit/engine/unittest_keyboard_manager.cpp similarity index 99% rename from unit/engine/unittest_keyboard_manager.cpp rename to aplcore/unit/engine/unittest_keyboard_manager.cpp index 436f3e1..1d4c851 100644 --- a/unit/engine/unittest_keyboard_manager.cpp +++ b/aplcore/unit/engine/unittest_keyboard_manager.cpp @@ -1069,7 +1069,7 @@ TEST_F(KeyboardManagerTest, ArrowKeysForAvg) loadDocument(ARROW_KEYS_CONTROLLING_AVG); ASSERT_TRUE(component); - auto vg = CoreComponent::cast(component->findComponentById("vg")); + auto vg = CoreComponent::cast(root->findComponentById("vg")); ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); auto event = root->popEvent(); diff --git a/unit/engine/unittest_layouts.cpp b/aplcore/unit/engine/unittest_layouts.cpp similarity index 72% rename from unit/engine/unittest_layouts.cpp rename to aplcore/unit/engine/unittest_layouts.cpp index c751d6c..8740eb4 100644 --- a/unit/engine/unittest_layouts.cpp +++ b/aplcore/unit/engine/unittest_layouts.cpp @@ -580,3 +580,201 @@ TEST_F(LayoutTest, MapParameter) component->getCalculated(apl::kPropertyText).asString())); ASSERT_TRUE(ConsoleMessage()); } + +static const char *LAYOUT_UNRESOLVED_PARAMETERS = R"apl({ + "type": "APL", + "version": "2023.2", + "theme": "light", + "layouts": { + "TextLayout": { + "parameters": [ + { + "name": "color", + "default": "black" + }, + { + "name": "text", + "default": "${7+4}" + } + ], + "item": { + "type": "Text", + "id": "t1", + "width": "20vw", + "height": "10vh", + "color": "${color}", + "text": "${text}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "TextLayout", + "color": "${data.color}", + "text": "${data.text}" + } + ] + } +})apl"; + +TEST_F(LayoutTest, UnresolvedParameters) +{ + loadDocument(LAYOUT_UNRESOLVED_PARAMETERS); + ASSERT_TRUE(component); + + // We expect it to be resolved to default specified in parameter, and this parameter may be binding + ASSERT_EQ("11", component->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ(Color(Color::BLACK), component->getCalculated(apl::kPropertyColor).asColor(session)); +} + +static const char *LAYOUT_MISSING_PARAMETERS = R"apl({ + "type": "APL", + "version": "2023.2", + "theme": "light", + "layouts": { + "TextLayout": { + "parameters": [ + { + "name": "color", + "default": "black" + }, + { + "name": "text", + "default": "${7+4}" + } + ], + "item": { + "type": "Text", + "id": "t1", + "width": "20vw", + "height": "10vh", + "color": "${color}", + "text": "${text}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "TextLayout" + } + ] + } +})apl"; + +TEST_F(LayoutTest, MissingParameters) +{ + loadDocument(LAYOUT_MISSING_PARAMETERS); + ASSERT_TRUE(component); + + // We expect it to be resolved to default specified in parameter, and this parameter may be binding + ASSERT_EQ("11", component->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ(Color(Color::BLACK), component->getCalculated(apl::kPropertyColor).asColor(session)); +} + +static const char *LAYOUT_UNRESOLVED_AND_MISSING_PARAMETERS_AS_REFS = R"apl({ + "type": "APL", + "version": "2023.2", + "theme": "light", + "resources": [ + { + "strings": { + "longText": "BANANAS" + } + } + ], + "layouts": { + "TextLayout": { + "parameters": [ + { + "name": "color", + "default": "black" + }, + { + "name": "text", + "default": "${7+4}" + }, + { + "name": "fontFamily", + "default": "amazon-ember" + }, + { + "name":"fontSize", + "default":"25dp" + } + ], + "item": { + "type": "Text", + "id": "t1", + "width": "20vw", + "height": "10vh", + "color": "${color}", + "fontFamily":"${fontFamily}", + "fontSize":"${fontSize}", + "text": "${text}" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "Container", + "items": [ + { + "type": "TextLayout", + "color": "${data.color}", + "text": "${data.text}", + "fontFamily": "${data.fontFamily}", + "fontSize": "${data.fontSize}" + } + ], + "data": [ { "text": "@longText" } ] + } + ] + } +})apl"; + +TEST_F(LayoutTest, UnresolvedAndMissingParametersAsRefs) +{ + loadDocument(LAYOUT_UNRESOLVED_AND_MISSING_PARAMETERS_AS_REFS); + ASSERT_TRUE(component); + + auto text = root->findComponentById("t1"); + + // We expect it to be resolved to default specified in parameter, and this parameter may be binding + ASSERT_EQ("BANANAS", text->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ(Color(Color::BLACK), text->getCalculated(apl::kPropertyColor).asColor(session)); + ASSERT_EQ("amazon-ember", text->getCalculated(apl::kPropertyFontFamily).asString()); + ASSERT_EQ("25dp", text->getCalculated(apl::kPropertyFontSize).asString()); +} + +static const char* BAD_PARAMETER_NAME = R"apl({ + "type": "APL", + "version": "2023.1", + "theme": "dark", + "layouts": { + "Foo": { + "parameters": [ + "invalid}" + ], + "item": { + "type": "Container" + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "Foo" + } + ] + } +})apl"; + +TEST_F(LayoutTest, BAD_PARAMETER_NAME) +{ + loadDocument(BAD_PARAMETER_NAME); + ASSERT_TRUE(component); + ASSERT_TRUE(ConsoleMessage()); +} \ No newline at end of file diff --git a/unit/engine/unittest_memory.cpp b/aplcore/unit/engine/unittest_memory.cpp similarity index 100% rename from unit/engine/unittest_memory.cpp rename to aplcore/unit/engine/unittest_memory.cpp diff --git a/unit/engine/unittest_propdef.cpp b/aplcore/unit/engine/unittest_propdef.cpp similarity index 100% rename from unit/engine/unittest_propdef.cpp rename to aplcore/unit/engine/unittest_propdef.cpp diff --git a/unit/engine/unittest_resources.cpp b/aplcore/unit/engine/unittest_resources.cpp similarity index 100% rename from unit/engine/unittest_resources.cpp rename to aplcore/unit/engine/unittest_resources.cpp diff --git a/unit/engine/unittest_styles.cpp b/aplcore/unit/engine/unittest_styles.cpp similarity index 100% rename from unit/engine/unittest_styles.cpp rename to aplcore/unit/engine/unittest_styles.cpp diff --git a/unit/engine/unittest_viewhost.cpp b/aplcore/unit/engine/unittest_viewhost.cpp similarity index 100% rename from unit/engine/unittest_viewhost.cpp rename to aplcore/unit/engine/unittest_viewhost.cpp diff --git a/unit/extension/CMakeLists.txt b/aplcore/unit/extension/CMakeLists.txt similarity index 100% rename from unit/extension/CMakeLists.txt rename to aplcore/unit/extension/CMakeLists.txt diff --git a/unit/extension/unittest_extension_client.cpp b/aplcore/unit/extension/unittest_extension_client.cpp similarity index 87% rename from unit/extension/unittest_extension_client.cpp rename to aplcore/unit/extension/unittest_extension_client.cpp index ae0230b..b5f4180 100644 --- a/unit/extension/unittest_extension_client.cpp +++ b/aplcore/unit/extension/unittest_extension_client.cpp @@ -13,6 +13,8 @@ * permissions and limitations under the License. */ +#ifdef ALEXAEXTENSIONS + #include "../testeventloop.h" #include "apl/extension/extensioncomponent.h" @@ -29,18 +31,17 @@ class ExtensionClientTest : public DocumentWrapper { void createConfig(JsonData&& document) { configPtr = std::make_shared(); - configPtr->set(RootProperty::kAgentName, "Unit tests").timeManager(loop).session(session); + configPtr->set(RootProperty::kAgentName, "Unit tests").timeManager(loop); content = Content::create(std::move(document), session); ASSERT_TRUE(content->isReady()); } std::shared_ptr createClient(const std::string& extension) { - return ExtensionClient::create(configPtr, extension); + return ExtensionClient::create(configPtr, extension, session); } - void initializeContext() { - root = RootContext::create(metrics, content, *configPtr, createCallback); + root = std::static_pointer_cast(RootContext::create(metrics, content, *configPtr, createCallback)); if (root) { context = root->contextPtr(); ASSERT_TRUE(context); @@ -52,12 +53,22 @@ class ExtensionClientTest : public DocumentWrapper { std::shared_ptr client; rapidjson::Document doc; + void SetUp() override + { + logBridge = std::make_shared(); + LoggerFactory::instance().initialize(logBridge); + DocumentWrapper::SetUp(); + } + void TearDown() override { client = nullptr; configPtr = nullptr; + LoggerFactory::instance().reset(); DocumentWrapper::TearDown(); } + + std::shared_ptr logBridge; }; static const char* EXT_DOC = R"({ @@ -228,8 +239,7 @@ static const char* WRONG_MESSAGE = R"({ TEST_F(ExtensionClientTest, ExtensionParseRequiredMalFormed) { const std::string HEADER = REGISTER_HEADER; auto configPtr = std::make_shared(); - configPtr->session(session); - auto client = ExtensionClient::create(configPtr, "aplext:hello:10"); + auto client = ExtensionClient::create(configPtr, "aplext:hello:10", session); ASSERT_FALSE(client->processMessage(nullptr, R"()")); ASSERT_TRUE(ConsoleMessage()); @@ -255,6 +265,21 @@ TEST_F(ExtensionClientTest, ExtensionParseRequiredMalFormed) { ASSERT_TRUE(ConsoleMessage()); } +TEST_F(ExtensionClientTest, MissingRootConfig) { + createConfig(EXT_DOC); + logBridge->clear(); + auto client = ExtensionClient::create(nullptr, "aplext:hello:10", session); + ASSERT_FALSE(client); + ASSERT_EQ(logBridge->getCount(), 1); +} + +TEST_F(ExtensionClientTest, BadBind) { + createConfigAndClient(EXT_DOC); + logBridge->clear(); + client->bindContext(nullptr); + ASSERT_EQ(logBridge->getCount(), 1); +} + static const char* EXTENSION_SIMPLE = R"({ "method": "RegisterSuccess", "version": "1.0", @@ -269,9 +294,36 @@ TEST_F(ExtensionClientTest, ExtensionParseRequired) { ASSERT_FALSE(ConsoleMessage()); ASSERT_TRUE(client->registrationMessageProcessed()); ASSERT_EQ("TOKEN-12", client->getConnectionToken()); + ASSERT_EQ("aplext:hello:10", client->getUri()); + + // Extension client does not directly modify RootConfig auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - ASSERT_NE(ext.end(), ext.find("aplext:hello:10")); + ASSERT_EQ(0, ext.size()); +} + +static const char* BAD_METHOD = R"({ + "method": "Oops", + "version": "1.0", + "token": "TOKEN-12", + "extension": "aplext:hello:10" +})"; + +TEST_F(ExtensionClientTest, ExtensionBadMethod) { + createConfigAndClient(EXT_DOC); + ASSERT_FALSE(client->processMessage(nullptr, BAD_METHOD)); + ASSERT_TRUE(ConsoleMessage()); +} + +static const char* BAD_REGISTRATION_MESSAGE = R"({ + "method": "RegisterSuccess", + "version": "1.0", + "extension": "aplext:hello:10" +})"; + +TEST_F(ExtensionClientTest, ExtensionBadRegistrationResponse) { + createConfigAndClient(EXT_DOC); + ASSERT_FALSE(client->processMessage(nullptr, BAD_REGISTRATION_MESSAGE)); + ASSERT_TRUE(ConsoleMessage()); } static const char* EXTENSION_DEFERRED = R"({ @@ -289,9 +341,6 @@ TEST_F(ExtensionClientTest, ExtensionParseAutoToken){ ASSERT_TRUE(client->registrationMessageProcessed()); const auto &token = client->getConnectionToken(); ASSERT_EQ(0, token.rfind("aplext:hello:10", 0)); - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - ASSERT_NE(ext.end(), ext.find("aplext:hello:10")); } @@ -536,7 +585,7 @@ TEST_F(ExtensionClientTest, ExtensionParseCommands) { ASSERT_TRUE(client->processMessage(nullptr, doc)); ASSERT_FALSE(ConsoleMessage()); - auto commands = configPtr->getExtensionCommands(); + const auto &commands = client->extensionSchema().commandDefinitions; ASSERT_EQ(4, commands.size()); ASSERT_EQ("aplext:hello:10", commands[0].getURI()); @@ -594,7 +643,7 @@ TEST_F(ExtensionClientTest, ExtensionParseCommandsInvalidType) { ASSERT_TRUE(client->processMessage(nullptr, doc)); ASSERT_FALSE(ConsoleMessage()); - auto commands = configPtr->getExtensionCommands(); + const auto &commands = client->extensionSchema().commandDefinitions; ASSERT_EQ(4, commands.size()); auto invalidTypeCommand = commands[2]; auto fooValue = invalidTypeCommand.getPropertyMap().find("foo")->second; @@ -627,6 +676,7 @@ TEST_F(ExtensionClientTest, ExtensionParseEventHandlersMalformed) { ASSERT_FALSE(client->processMessage(nullptr, doc)); ASSERT_TRUE(ConsoleMessage()); + ASSERT_FALSE(client->registered()); createConfigAndClient(EXT_DOC); std::string doc2 = "{"; @@ -637,6 +687,7 @@ TEST_F(ExtensionClientTest, ExtensionParseEventHandlersMalformed) { ASSERT_TRUE(client->processMessage(nullptr, doc2)); ASSERT_FALSE(ConsoleMessage()); + ASSERT_TRUE(client->registered()); createConfigAndClient(EXT_DOC); std::string doc3 = "{"; @@ -647,6 +698,7 @@ TEST_F(ExtensionClientTest, ExtensionParseEventHandlersMalformed) { ASSERT_FALSE(client->processMessage(nullptr, doc3)); ASSERT_TRUE(ConsoleMessage()); + ASSERT_FALSE(client->registered()); createConfigAndClient(EXT_DOC); std::string doc4 = "{"; @@ -657,6 +709,7 @@ TEST_F(ExtensionClientTest, ExtensionParseEventHandlersMalformed) { ASSERT_FALSE(client->processMessage(nullptr, doc4)); ASSERT_TRUE(ConsoleMessage()); + ASSERT_FALSE(client->registered()); } @@ -669,15 +722,13 @@ TEST_F(ExtensionClientTest, ExtensionParseEventHandlers) { doc += EXTENSION_EVENTS; doc += "}}"; + ASSERT_FALSE(client->registered()); ASSERT_TRUE(client->processMessage(nullptr, doc)); - ASSERT_FALSE(ConsoleMessage()); + ASSERT_TRUE(client->registered()); - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); + ASSERT_FALSE(ConsoleMessage()); - auto handlers = configPtr->getExtensionEventHandlers(); + const auto &handlers = client->extensionSchema().eventHandlers; ASSERT_EQ(3, handlers.size()); ASSERT_EQ("aplext:hello:10", handlers[0].getURI()); ASSERT_EQ("onEntityAdded", handlers[0].getName()); @@ -703,15 +754,12 @@ TEST_F(ExtensionClientTest, ExtensionParseEventDataBindings) { doc += EXTENSION_DATA_BINDINGS; doc += "}}"; + ASSERT_FALSE(client->registered()); ASSERT_TRUE(client->processMessage(nullptr, doc)); ASSERT_FALSE(ConsoleMessage()); + ASSERT_TRUE(client->registered()); - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); - - auto liveDataMap = configPtr->getLiveObjectMap(); + const auto &liveDataMap = client->extensionSchema().liveData; ASSERT_EQ(2, liveDataMap.size()); auto arr = liveDataMap.at("entityList"); auto map = liveDataMap.at("deviceState"); @@ -729,27 +777,24 @@ TEST_F(ExtensionClientTest, ExtensionParseComponent) { doc += EXTENSION_COMPONENTS; doc += "}}"; + ASSERT_FALSE(client->registered()); ASSERT_TRUE(client->processMessage(nullptr, doc)); ASSERT_FALSE(ConsoleMessage()); + ASSERT_TRUE(client->registered()); - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); - - auto components = configPtr->getExtensionComponentDefinitions(); + const auto &components = client->extensionSchema().componentDefinitions; ASSERT_EQ(1, components.size()); auto def = components.at(0); ASSERT_EQ("aplext:hello:10", def.getURI()); ASSERT_EQ("MyComponent", def.getName()); ASSERT_EQ("Surface", def.getResourceType()); - auto commands = configPtr->getExtensionCommands(); + const auto &commands = client->extensionSchema().commandDefinitions; ASSERT_EQ(1, commands.size()); auto command = commands.at(0); ASSERT_STREQ(command.getName().c_str(), "componentCommand"); - auto handlers = configPtr->getExtensionComponentDefinitions().at(0).getEventHandlers(); + const auto &handlers = client->extensionSchema().componentDefinitions.at(0).getEventHandlers(); ASSERT_EQ(2, handlers.size()); } @@ -1225,7 +1270,7 @@ TEST_F(ExtensionClientTest, ExtensionLifecycle) { ASSERT_TRUE(evaluate(*context, "${environment.extension.Hello}").isMap()); ASSERT_TRUE(IsEqual("additional", evaluate(*context, "${environment.extension.Hello.something}"))); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); // Tap happened! @@ -1274,13 +1319,15 @@ TEST_F(ExtensionClientTest, CommandResolve) { // We have all we need. Inflate. initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); // Tap happened! performTap(1,1); ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); + ASSERT_EQ(kEventTypeExtension, event.getType()); + // Runtime needs to redirect this events to the server. auto processedCommand = client->processCommand(doc.GetAllocator(), event); ASSERT_STREQ("Command", processedCommand["method"].GetString()); @@ -1306,13 +1353,15 @@ TEST_F(ExtensionClientTest, CommandResolveWrong) { // We have all we need. Inflate. initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); // Tap happened! performTap(1,1); ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); + ASSERT_EQ(kEventTypeExtension, event.getType()); + // Runtime needs to redirect this events to the server. auto processedCommand = client->processCommand(doc.GetAllocator(), event); ASSERT_STREQ("Command", processedCommand["method"].GetString()); @@ -1338,13 +1387,15 @@ TEST_F(ExtensionClientTest, CommandInterruptedResolve) { // We have all we need. Inflate. initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); // Tap happened! performTap(1,1); ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); + ASSERT_EQ(kEventTypeExtension, event.getType()); + // Runtime needs to redirect this events to the server. auto processedCommand = client->processCommand(doc.GetAllocator(), event); ASSERT_STREQ("Command", processedCommand["method"].GetString()); @@ -1406,16 +1457,11 @@ TEST_F(ExtensionClientTest, Registered) { ASSERT_TRUE(client->registrationMessageProcessed()); ASSERT_TRUE(client->registered()); - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); - - auto env = configPtr->getExtensionEnvironment("aplext:hello:10"); + const auto &env = client->extensionSchema().environment; ASSERT_TRUE(env.has("something")); ASSERT_EQ("additional", env.get("something").asString()); - auto commands = configPtr->getExtensionCommands(); + const auto &commands = client->extensionSchema().commandDefinitions; ASSERT_EQ(1, commands.size()); auto freeze = commands.at(0); ASSERT_EQ("freeze", freeze.getName()); @@ -1427,12 +1473,12 @@ TEST_F(ExtensionClientTest, Registered) { ASSERT_TRUE(freezeParams.count("baz")); ASSERT_TRUE(freezeParams.count("entity")); - auto events = configPtr->getExtensionEventHandlers(); + const auto &events = client->extensionSchema().eventHandlers; ASSERT_EQ(6, events.size()); auto event = events.at(0); ASSERT_EQ("onEntityAdded", event.getName()); - auto liveData = configPtr->getLiveObjectMap(); + const auto &liveData = client->extensionSchema().liveData; ASSERT_EQ(2, liveData.size()); ASSERT_TRUE(liveData.count("entityList")); ASSERT_TRUE(liveData.count("deviceState")); @@ -1495,6 +1541,18 @@ TEST_F(ExtensionClientTest, OrderOfOperation) { ASSERT_FALSE(ConsoleMessage()); } +TEST_F(ExtensionClientTest, SimpleEvent) { + createConfigAndClient(EXT_DOC); + + ASSERT_TRUE(client->processMessage(nullptr, EXT_REGISTER_SUCCESS)); + ASSERT_FALSE(ConsoleMessage()); + + initializeContext(); + + // Event comes up from service to be intercepted and directed to client by runtime + ASSERT_TRUE(client->processMessage(root, EXT_EVENT)); +} + static const char* LIVE_DATA_INIT = R"({ "type": "APL", "version": "1.4", @@ -1550,7 +1608,7 @@ TEST_F(ExtensionClientTest, LiveDataInitialize) { initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ("true:7.9", text->getCalculated(kPropertyText).asString()); @@ -1564,7 +1622,7 @@ TEST_F(ExtensionClientTest, LiveDataUpdates) { initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_TRUE(client->processMessage(root, ENTITY_LIST_INSERT)); @@ -1651,16 +1709,11 @@ TEST_F(ExtensionClientTest, ManyClients) { client1->processMessage(nullptr, EXT_REGISTER_SUCCESS); ASSERT_FALSE(ConsoleMessage()); + ASSERT_TRUE(client1->registered()); client2->processMessage(nullptr, EXT_REGISTER_GREETINGS); ASSERT_FALSE(ConsoleMessage()); - - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(2, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); - ex = ext.find("aplext:greetings:10"); - ASSERT_NE(ext.end(), ex); + ASSERT_TRUE(client2->registered()); } TEST_F(ExtensionClientTest, Command) { @@ -1675,6 +1728,8 @@ TEST_F(ExtensionClientTest, Command) { performTap(1,1); ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); + ASSERT_EQ(kEventTypeExtension, event.getType()); + // Runtime needs to redirect this events to the server. auto processedCommand = client->processCommand(doc.GetAllocator(), event); ASSERT_TRUE(std::string("1.0").compare(processedCommand["version"].GetString()) <= 0); @@ -1700,6 +1755,59 @@ TEST_F(ExtensionClientTest, Command) { ASSERT_EQ(kEventTypeSendEvent, event.getType()); } +TEST_F(ExtensionClientTest, CommandBadEventType) { + createConfig(EXT_DOC); + client = ExtensionClient::create(config, "aplext:hello:10", session); + + initializeContext(); + + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + Event event(apl::kEventTypeSendEvent, std::move(bag)); + ASSERT_TRUE(client->processCommand(doc.GetAllocator(), event).GetType() == rapidjson::kNullType); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(ExtensionClientTest, CommandBadExtension) { + createConfig(EXT_DOC); + client = ExtensionClient::create(config, "aplext:hello:10", session); + + initializeContext(); + + EventBag bag; + Event event(apl::kEventTypeExtension, std::move(bag)); + ASSERT_TRUE(client->processCommand(doc.GetAllocator(), event).GetType() == rapidjson::kNullType); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(ExtensionClientTest, CommandBadCommandName) { + createConfig(EXT_DOC); + client = ExtensionClient::create(config, "aplext:hello:10", session); + + initializeContext(); + + EventBag bag; + bag.emplace(kEventPropertyExtensionURI, "aplext:hello:10"); + Event event(apl::kEventTypeExtension, std::move(bag)); + ASSERT_TRUE(client->processCommand(doc.GetAllocator(), event).GetType() == rapidjson::kNullType); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(ExtensionClientTest, CommandBadResourceId) { + createConfig(EXT_DOC); + client = ExtensionClient::create(config, "aplext:hello:10", session); + + initializeContext(); + + EventBag bag; + bag.emplace(kEventPropertyName, "arbitraryName"); + bag.emplace(kEventPropertyExtensionURI, "aplext:hello:10"); + bag.emplace(kEventPropertyExtensionResourceId, 7); + Event event(apl::kEventTypeExtension, std::move(bag)); + ASSERT_TRUE(client->processCommand(doc.GetAllocator(), event).GetType() == rapidjson::kNullType); + ASSERT_TRUE(ConsoleMessage()); +} + TEST_F(ExtensionClientTest, CommandMissingRequired) { createConfigAndClient(EXT_DOC); @@ -1726,6 +1834,8 @@ TEST_F(ExtensionClientTest, CommandMissingNonRequired) { performTap(1,201); ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); + ASSERT_EQ(kEventTypeExtension, event.getType()); + // Runtime needs to redirect this events to the server. auto processedCommand = client->processCommand(doc.GetAllocator(), event); ASSERT_TRUE(std::string("1.0").compare(processedCommand["version"].GetString()) <= 0); @@ -1762,6 +1872,8 @@ TEST_F(ExtensionClientTest, CommandFail) { performTap(1,1); ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); + ASSERT_EQ(kEventTypeExtension, event.getType()); + // Runtime needs to redirect this events to the server. auto processedCommand = client->processCommand(doc.GetAllocator(), event); @@ -1785,7 +1897,7 @@ TEST_F(ExtensionClientTest, Event) { initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_TRUE(client->processMessage(root, EXT_EVENT)); @@ -1807,7 +1919,7 @@ TEST_F(ExtensionClientTest, EventEmpty) { initializeContext(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_TRUE(client->processMessage(root, EXT_EVENT_NO_PAYLOAD)); @@ -2106,7 +2218,7 @@ TEST_F(ExtensionClientTest, InitialArrayEvent) { client->bindContext(root); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); root->clearPending(); @@ -2114,6 +2226,244 @@ TEST_F(ExtensionClientTest, InitialArrayEvent) { ASSERT_EQ("onEntityAdded:3", text->getCalculated(kPropertyText).asString()); } +static const char* EXT_INIT_DATA_DOC = R"({ + "type": "APL", + "version": "2023.2", + "extension": { + "uri": "aplext:hello:10", + "name": "Hello" + }, + "mainTemplate": { + "item": { + "type": "Text", + "width": 500, + "height": 100, + "text": "${EntityMap.label ? EntityMap.label : 'nope'}" + } + } +})"; + +static const char* EXT_REGISTER_WITHOUT_DATA_SUCCESS = R"({ + "method": "RegisterSuccess", + "version": "1.0", + "token": "TOKEN", + "schema": { + "type": "Schema", + "version": "1.0", + "uri": "aplext:hello:10", + "types": [ + { + "name": "Entity", + "properties": { + "label": "string" + } + } + ], + "liveData": [ + { + "name": "EntityMap", + "type": "Entity" + } + ] + } +})"; + +static const char* INSERT_INIT_DATA = R"({ + "version": "1.0", + "method": "LiveDataUpdate", + "name": "EntityMap", + "target": "aplext:hello:10", + "operations": [ + { + "type": "Set", + "key": "label", + "item": "Hello there" + } + ] +})"; + +TEST_F(ExtensionClientTest, InitialDataFollowup) { + createConfigAndClient(EXT_INIT_DATA_DOC); + + ASSERT_TRUE(client->processMessage(nullptr, EXT_REGISTER_WITHOUT_DATA_SUCCESS)); + ASSERT_FALSE(ConsoleMessage()); + + initializeContext(); + ASSERT_TRUE(client->processMessage(nullptr, INSERT_INIT_DATA)); + + client->bindContext(root); + + ASSERT_EQ("nope", component->getCalculated(kPropertyText).asString()); +} + +static const char* EXT_REGISTER_WITH_DATA_SUCCESS = R"({ + "method": "RegisterSuccess", + "version": "1.0", + "token": "TOKEN", + "schema": { + "type": "Schema", + "version": "1.0", + "uri": "aplext:hello:10", + "types": [ + { + "name": "Entity", + "properties": { + "label": "string" + } + } + ], + "liveData": [ + { + "name": "EntityMap", + "type": "Entity", + "data": { + "label": "Hello there" + } + } + ] + } +})"; + +TEST_F(ExtensionClientTest, InitialDataInRegistration) { + createConfigAndClient(EXT_INIT_DATA_DOC); + + ASSERT_TRUE(client->processMessage(nullptr, EXT_REGISTER_WITH_DATA_SUCCESS)); + ASSERT_FALSE(ConsoleMessage()); + + initializeContext(); + + client->bindContext(root); + + ASSERT_EQ("Hello there", component->getCalculated(kPropertyText).asString()); +} + +static const char* EXT_INIT_ARRAY_DOC = R"({ + "type": "APL", + "version": "2023.2", + "extension": { + "uri": "aplext:hello:10", + "name": "Hello" + }, + "mainTemplate": { + "item": { + "type": "Container", + "width": 500, + "height": 500, + "data": "${EntityArray}", + "items": [ + { + "type": "Text", + "width": 500, + "height": 100, + "text": "${data.label}" + } + ] + } + } +})"; + +static const char* EXT_REGISTER_WITH_ARRAY_SUCCESS = R"({ + "method": "RegisterSuccess", + "version": "1.0", + "token": "TOKEN", + "schema": { + "type": "Schema", + "version": "1.0", + "uri": "aplext:hello:10", + "types": [ + { + "name": "Entity", + "properties": { + "label": "string" + } + } + ], + "liveData": [ + { + "name": "EntityArray", + "type": "Entity[]", + "data": [ + { + "label": "Hello there" + }, + { + "label": "Bye now" + } + ] + } + ] + } +})"; + +TEST_F(ExtensionClientTest, InitialArrayInRegistration) { + createConfigAndClient(EXT_INIT_ARRAY_DOC); + + ASSERT_TRUE(client->processMessage(nullptr, EXT_REGISTER_WITH_ARRAY_SUCCESS)); + ASSERT_FALSE(ConsoleMessage()); + + initializeContext(); + + client->bindContext(root); + + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_EQ("Hello there", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Bye now", component->getChildAt(1)->getCalculated(kPropertyText).asString()); +} + +static const char* EXT_REGISTER_SUCCESS_WITH_BAD_DATA = R"({ + "method": "RegisterSuccess", + "version": "1.0", + "token": "TOKEN", + "schema": { + "type": "Schema", + "version": "1.0", + "uri": "aplext:hello:10", + "types": [ + { + "name": "Entity", + "properties": { + "label": "string" + } + } + ], + "liveData": [ + { + "name": "EntityArray", + "type": "Entity[]", + "data": { + "label": "Hello there" + } + }, + { + "name": "EntityMap", + "type": "Entity", + "data": [ + { + "label": "Hello there" + }, + { + "label": "Bye now" + } + ] + } + ] + } +})"; + +TEST_F(ExtensionClientTest, InitialBadStartingData) { + createConfigAndClient(EXT_INIT_ARRAY_DOC); + + ASSERT_TRUE(client->processMessage(nullptr, EXT_REGISTER_SUCCESS_WITH_BAD_DATA)); + + initializeContext(); + + client->bindContext(root); + + ASSERT_TRUE(session->checkAndClear("Initial data for LiveData=EntityArray is of invalid type. Should be an Array.", + "Initial data for LiveData=EntityMap is of invalid type. Should be a Map.")); +} + static const char* EXT_REGISTER_SUCCESS_EXTENDED_TYPE = R"({ "method": "RegisterSuccess", "version": "1.0", @@ -2165,7 +2515,7 @@ TEST_F(ExtensionClientTest, ExtendedTypes) { ASSERT_TRUE(client->registrationMessageProcessed()); ASSERT_TRUE(client->registered()); - auto commands = configPtr->getExtensionCommands(); + const auto &commands = client->extensionSchema().commandDefinitions; ASSERT_EQ(2, commands.size()); auto ping = commands.at(0); ASSERT_EQ("Ping", ping.getName()); @@ -2234,7 +2584,7 @@ TEST_F(ExtensionClientTest, BadlyExtendedTypes) { ASSERT_TRUE(client->registrationMessageProcessed()); ASSERT_TRUE(client->registered()); - auto commands = configPtr->getExtensionCommands(); + const auto &commands = client->extensionSchema().commandDefinitions; ASSERT_EQ(2, commands.size()); auto ping = commands.at(0); ASSERT_EQ("Ping", ping.getName()); @@ -2293,7 +2643,7 @@ static const char* WEATHER_MAP_SET_PROP = R"( ] })"; -TEST_F(ExtensionClientTest, TypeWithoutPropertis) { +TEST_F(ExtensionClientTest, TypeWithoutProperties) { createConfigAndClient(EXT_DOC); std::string doc = "{"; @@ -2303,15 +2653,10 @@ TEST_F(ExtensionClientTest, TypeWithoutPropertis) { ASSERT_TRUE(client->processMessage(nullptr, doc)); ASSERT_FALSE(ConsoleMessage()); - - // Verify the extension is registered - auto ext = configPtr->getSupportedExtensions(); - ASSERT_EQ(1, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); + ASSERT_TRUE(client->registered()); // Verify the live map is configured, without properties - auto liveDataMap = configPtr->getLiveObjectMap(); + const auto &liveDataMap = client->extensionSchema().liveData; ASSERT_EQ(1, liveDataMap.size()); auto& map = liveDataMap.at("MyWeather"); ASSERT_EQ(LiveObject::ObjectType::kMapType, map->getType()); @@ -2707,7 +3052,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentCommandAndEvent) { initializeContext(); ASSERT_EQ(component->getType(), kComponentTypeContainer); - auto touchwrapper = component->findComponentById("AlexaButton"); + auto touchwrapper = root->findComponentById("AlexaButton"); ASSERT_EQ(touchwrapper->getType(), kComponentTypeTouchWrapper); // Perform a touch to trigger an extension Component command. @@ -2716,7 +3061,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentCommandAndEvent) { auto event = root->popEvent(); ASSERT_EQ(event.getType(), kEventTypeExtension); - auto extensionComponent = component->findComponentById("DrawArea"); + auto extensionComponent = root->findComponentById("DrawArea"); ASSERT_EQ(extensionComponent->getType(), kComponentTypeExtension); auto extnComp = ExtensionComponent::cast(extensionComponent); @@ -2745,7 +3090,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentProperty) { initializeContext(); - auto extensionComponent = component->findComponentById("DrawArea"); + auto extensionComponent = root->findComponentById("DrawArea"); ASSERT_EQ(extensionComponent->getType(), kComponentTypeExtension); auto extnComp = ExtensionComponent::cast(extensionComponent); ASSERT_NE(extnComp, nullptr); @@ -2801,8 +3146,8 @@ TEST_F(ExtensionClientTest, ExtensionComponentKPropOutProperty) { initializeContext(); - auto alexaButton = component->findComponentById("AlexaButton"); - auto extensionComponent = CoreComponent::cast(component->findComponentById("DrawArea")); + auto alexaButton = root->findComponentById("AlexaButton"); + auto extensionComponent = CoreComponent::cast(root->findComponentById("DrawArea")); ASSERT_EQ(extensionComponent->getType(), kComponentTypeExtension); extensionComponent->setProperty(kPropertyDisplay, "none"); @@ -2842,7 +3187,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentInvalidProperty) { initializeContext(); - auto extensionComponent = component->findComponentById("DrawArea"); + auto extensionComponent = root->findComponentById("DrawArea"); ASSERT_EQ(extensionComponent->getType(), kComponentTypeExtension); // Perform a touch to trigger a change in extension property @@ -2879,7 +3224,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentInvalidComponentInvoke) { initializeContext(); - auto extensionComponent = component->findComponentById("DrawArea"); + auto extensionComponent = root->findComponentById("DrawArea"); ASSERT_EQ(extensionComponent->getType(), kComponentTypeExtension); auto extnComp = ExtensionComponent::cast(extensionComponent); @@ -2900,7 +3245,7 @@ TEST_F(ExtensionClientTest, ExtensionClientDisconnection) { initializeContext(); - auto extensionComponent = component->findComponentById("DrawArea"); + auto extensionComponent = root->findComponentById("DrawArea"); ASSERT_EQ(extensionComponent->getType(), kComponentTypeExtension); auto extnComp = ExtensionComponent::cast(extensionComponent); @@ -2909,7 +3254,7 @@ TEST_F(ExtensionClientTest, ExtensionClientDisconnection) { ASSERT_TRUE(client->handleDisconnection(root, 500, "Service not available")); ASSERT_EQ(extnComp->getCalculated(kPropertyResourceState).asInt(), kResourceError); - auto alexaButton = component->findComponentById("AlexaButton"); + auto alexaButton = root->findComponentById("AlexaButton"); ASSERT_EQ(alexaButton->getType(), kComponentTypeTouchWrapper); // Verifies that onFatalError was called. ASSERT_EQ(alexaButton->getCalculated(kPropertyShadowColor).getColor(), Color::ColorConstants::BLACK); @@ -2976,7 +3321,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentCommandInvalidComponentId) { initializeContext(); ASSERT_EQ(component->getType(), kComponentTypeContainer); - auto touchwrapper = component->findComponentById("AlexaButton"); + auto touchwrapper = root->findComponentById("AlexaButton"); ASSERT_EQ(touchwrapper->getType(), kComponentTypeTouchWrapper); // Perform a touch to trigger an extension Component command. @@ -3083,7 +3428,7 @@ TEST_F(ExtensionClientTest, PrimitiveLiveArray) { // We have all we need. Inflate. initializeContext(); - auto text = component->findComponentById("root"); + auto text = root->findComponentById("root"); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ("0", text->getCalculated(kPropertyText).asString()); @@ -3116,7 +3461,9 @@ TEST_F(ExtensionClientTest, WrongLiveArray) { // We have all we need. Inflate. initializeContext(); - auto text = component->findComponentById("root"); + auto text = root->findComponentById("root"); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ("", text->getCalculated(kPropertyText).asString()); -} \ No newline at end of file +} + +#endif diff --git a/unit/extension/unittest_extension_command.cpp b/aplcore/unit/extension/unittest_extension_command.cpp similarity index 98% rename from unit/extension/unittest_extension_command.cpp rename to aplcore/unit/extension/unittest_extension_command.cpp index 59d664f..7dc1bf3 100644 --- a/unit/extension/unittest_extension_command.cpp +++ b/aplcore/unit/extension/unittest_extension_command.cpp @@ -100,7 +100,7 @@ TEST_F(ExtensionCommandTest, BasicMissingCommand) loadDocument(BASIC); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::WHITE), frame->getCalculated(kPropertyBackgroundColor))); @@ -124,7 +124,7 @@ TEST_F(ExtensionCommandTest, BasicCommand) loadDocument(BASIC); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::WHITE), frame->getCalculated(kPropertyBackgroundColor))); @@ -165,7 +165,7 @@ TEST_F(ExtensionCommandTest, BasicCommandWithActionRef) loadDocument(BASIC); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::WHITE), frame->getCalculated(kPropertyBackgroundColor))); @@ -397,7 +397,7 @@ TEST_F(ExtensionCommandTest, FastModeNotAllowed) loadDocument(SCROLL_VIEW); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::GREEN), frame->getCalculated(kPropertyBackgroundColor))); @@ -427,7 +427,7 @@ TEST_F(ExtensionCommandTest, FastModeAllowed) loadDocument(SCROLL_VIEW); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::GREEN), frame->getCalculated(kPropertyBackgroundColor))); @@ -504,7 +504,7 @@ TEST_F(ExtensionCommandTest, MissingRequiredProperty) loadDocument(SCROLL_VIEW_BAD_COMMAND); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::GREEN), frame->getCalculated(kPropertyBackgroundColor))); @@ -533,7 +533,7 @@ TEST_F(ExtensionCommandTest, OptionalProperties) loadDocument(SCROLL_VIEW_BAD_COMMAND); ASSERT_TRUE(component); - auto frame = component->findComponentById("MyFrame"); + auto frame = root->findComponentById("MyFrame"); ASSERT_TRUE(frame); ASSERT_TRUE(IsEqual(Color(Color::GREEN), frame->getCalculated(kPropertyBackgroundColor))); diff --git a/unit/extension/unittest_extension_component.cpp b/aplcore/unit/extension/unittest_extension_component.cpp similarity index 100% rename from unit/extension/unittest_extension_component.cpp rename to aplcore/unit/extension/unittest_extension_component.cpp diff --git a/unit/extension/unittest_extension_handler.cpp b/aplcore/unit/extension/unittest_extension_handler.cpp similarity index 100% rename from unit/extension/unittest_extension_handler.cpp rename to aplcore/unit/extension/unittest_extension_handler.cpp diff --git a/unit/extension/unittest_extension_mediator.cpp b/aplcore/unit/extension/unittest_extension_mediator.cpp similarity index 83% rename from unit/extension/unittest_extension_mediator.cpp rename to aplcore/unit/extension/unittest_extension_mediator.cpp index 29c5d18..6226344 100644 --- a/unit/extension/unittest_extension_mediator.cpp +++ b/aplcore/unit/extension/unittest_extension_mediator.cpp @@ -17,7 +17,9 @@ #include "../testeventloop.h" #include "apl/extension/extensioncomponent.h" - +#include "apl/extension/extensionmanager.h" +#include "apl/livedata/livedatamanager.h" +#include "apl/livedata/livedataobject.h" #include #include #include @@ -612,18 +614,21 @@ class ExtensionMediatorTest : public DocumentWrapper { .extensionProvider(extensionProvider) .extensionMediator(mediator); - auto requests = content->getExtensionRequests(); - // create a test extension for every request - for (auto& req: requests) { - auto ext = std::make_shared( - std::set({req})); + ensureRequestedExtensions(content->getExtensionRequests()); + + // load them into config via the mediator + mediator->loadExtensions(config, content); + } + + void ensureRequestedExtensions(std::set requestedExtensions) { + // Create a test extension for every request unless it's been requested before + for (auto& req: requestedExtensions) { + auto ext = std::make_shared(std::set({req})); auto proxy = std::make_shared(ext); extensionProvider->registerExtension(proxy); // save direct access to extension for test use testExtensions.emplace(req, ext); } - // load them into config via the mediator - mediator->loadExtensions(config, content); } void TearDown() override { @@ -822,33 +827,28 @@ TEST_F(ExtensionMediatorTest, ExperimentalFeature) { } /** - * Test that the mediator loads available extensions into the RootConfig. + * Test that the mediator loads registration into the extension clients */ TEST_F(ExtensionMediatorTest, RegistrationConfig) { - loadExtensions(EXT_DOC); - // 2 extensions with the same schema are registered - auto uris = config->getSupportedExtensions(); - ASSERT_EQ(2, uris.size()); - ASSERT_EQ(1, uris.count("aplext:hello:10")); - ASSERT_EQ(1, uris.count("aplext:goodbye:10")); - - auto commands = config->getExtensionCommands(); - ASSERT_EQ(4, commands.size()); + // We do not rely on direct registration on the config + ASSERT_EQ(0, config->getSupportedExtensions().size()); - auto events = config->getExtensionEventHandlers(); - ASSERT_EQ(6, events.size()); + inflate(); + auto env = context->extensionManager().getEnvironment(); - auto liveDataMap = config->getLiveObjectMap(); - ASSERT_EQ(2, liveDataMap.size()); + ASSERT_EQ(2, env->size()); + ASSERT_EQ(1, env->count("Hello")); + ASSERT_EQ(1, env->count("Bye")); + ASSERT_TRUE(env->at("Hello").truthy()); + ASSERT_TRUE(env->at("Bye").truthy()); } /** * Test that runtime flags are passed to the extension. */ TEST_F(ExtensionMediatorTest, RegistrationFlags) { - config->registerExtensionFlags("aplext:hello:10", "--hello"); loadExtensions(EXT_DOC); @@ -859,6 +859,40 @@ TEST_F(ExtensionMediatorTest, RegistrationFlags) { ASSERT_EQ("--hello", hello->mType); } +TEST_F(ExtensionMediatorTest, LoadExtensionsWithFlagsParameter) { + createContent(EXT_DOC, nullptr); + createProvider(); + ensureRequestedExtensions(content->getExtensionRequests()); + + mediator->loadExtensions(ObjectMap{{"aplext:hello:10", "--hello"}}, content); + + // direct access to extension for test inspection + auto hello = testExtensions["aplext:hello:10"].lock(); + ASSERT_TRUE(hello); + + ASSERT_EQ("--hello", hello->mType); +} + +TEST_F(ExtensionMediatorTest, LoadExtensionsWithFlagsParameterAndCallback) { + createContent(EXT_DOC, nullptr); + createProvider(); + ensureRequestedExtensions(content->getExtensionRequests()); + + ObjectMap flagMap{{"aplext:goodbye:10", "BYE"}}; + mediator->initializeExtensions(flagMap, content); + auto loaded = std::make_shared(false); + mediator->loadExtensions(flagMap, content, [loaded](){ + *loaded = true; + }); + + ASSERT_TRUE(*loaded); + + // direct access to extension for test inspection + auto extension = testExtensions["aplext:goodbye:10"].lock(); + ASSERT_TRUE(extension); + ASSERT_EQ("BYE", extension->mType); +} + /** * Test that the document settings are passed to the extension. */ @@ -878,30 +912,40 @@ TEST_F(ExtensionMediatorTest, ParseSettings) { ASSERT_EQ("MAGIC", hello->mAuthorizationCode); } - - TEST_F(ExtensionMediatorTest, ExtensionParseCommands) { - loadExtensions(EXT_DOC); - auto commands = config->getExtensionCommands(); - ASSERT_EQ(4, commands.size()); - - ASSERT_EQ("aplext:hello:10", commands[0].getURI()); - ASSERT_EQ("follow", commands[0].getName()); - ASSERT_FALSE(commands[0].getRequireResolution()); - ASSERT_TRUE(commands[0].getPropertyMap().empty()); + // We do not rely on RootConfig to store extension commands + ASSERT_EQ(0, config->getExtensionCommands().size()); - ASSERT_EQ("aplext:hello:10", commands[1].getURI()); - ASSERT_EQ("lead", commands[1].getName()); - ASSERT_TRUE(commands[1].getRequireResolution()); - ASSERT_TRUE(commands[1].getPropertyMap().empty()); + inflate(); + auto commands = context->extensionManager().getCommandDefinitions(); - ASSERT_EQ("aplext:hello:10", commands[2].getURI()); - ASSERT_EQ("freeze", commands[2].getName()); - ASSERT_FALSE(commands[3].getRequireResolution()); + ASSERT_EQ(4, commands.size()); - auto props = commands[2].getPropertyMap(); + auto it = commands.find("Hello:follow"); + ASSERT_NE(commands.end(), it); + auto command = it->second; + ASSERT_EQ("aplext:hello:10", command.getURI()); + ASSERT_EQ("follow", command.getName()); + ASSERT_FALSE(command.getRequireResolution()); + ASSERT_TRUE(command.getPropertyMap().empty()); + + it = commands.find("Hello:lead"); + ASSERT_NE(commands.end(), it); + command = it->second; + ASSERT_EQ("aplext:hello:10", command.getURI()); + ASSERT_EQ("lead", command.getName()); + ASSERT_TRUE(command.getRequireResolution()); + ASSERT_TRUE(command.getPropertyMap().empty()); + + it = commands.find("Hello:freeze"); + ASSERT_NE(commands.end(), it); + command = it->second; + ASSERT_EQ("aplext:hello:10", command.getURI()); + ASSERT_EQ("freeze", command.getName()); + ASSERT_FALSE(command.getRequireResolution()); + auto props = command.getPropertyMap(); ASSERT_EQ(4, props.size()); ASSERT_TRUE(IsEqual(true, props.at("foo").required)); ASSERT_TRUE(IsEqual(64, props.at("foo").defvalue)); @@ -910,11 +954,14 @@ TEST_F(ExtensionMediatorTest, ExtensionParseCommands) { ASSERT_TRUE(IsEqual(true, props.at("baz").required)); ASSERT_TRUE(IsEqual(true, props.at("baz").defvalue)); - ASSERT_EQ("aplext:hello:10", commands[3].getURI()); - ASSERT_EQ("clipEntity", commands[3].getName()); - ASSERT_FALSE(commands[3].getRequireResolution()); + it = commands.find("Hello:clipEntity"); + ASSERT_NE(commands.end(), it); + command = it->second; + ASSERT_EQ("aplext:hello:10", command.getURI()); + ASSERT_EQ("clipEntity", command.getName()); + ASSERT_FALSE(command.getRequireResolution()); - props = commands[3].getPropertyMap(); + props = command.getPropertyMap(); ASSERT_EQ(4, props.size()); ASSERT_TRUE(IsEqual(true, props.at("foo").required)); ASSERT_TRUE(IsEqual(64, props.at("foo").defvalue)); @@ -928,37 +975,75 @@ TEST_F(ExtensionMediatorTest, ExtensionParseCommands) { TEST_F(ExtensionMediatorTest, ExtensionParseEventHandlers) { loadExtensions(EXT_DOC); - auto handlers = config->getExtensionEventHandlers(); + // We do not rely on RootConfig to store extension commands + ASSERT_EQ(0, config->getExtensionEventHandlers().size()); + + inflate(); + auto handlers = context->extensionManager().getEventHandlerDefinitions(); + ASSERT_EQ(6, handlers.size()); - ASSERT_EQ("aplext:hello:10", handlers[0].getURI()); - ASSERT_EQ("onEntityAdded", handlers[0].getName()); - ASSERT_EQ("aplext:hello:10", handlers[1].getURI()); - ASSERT_EQ("onEntityChanged", handlers[1].getName()); - ASSERT_EQ("aplext:hello:10", handlers[2].getURI()); - ASSERT_EQ("onEntityLost", handlers[2].getName()); - ASSERT_EQ("aplext:hello:10", handlers[3].getURI()); - ASSERT_EQ("onDeviceUpdate", handlers[3].getName()); - ASSERT_EQ("aplext:hello:10", handlers[4].getURI()); - ASSERT_EQ("onDeviceRemove", handlers[4].getName()); - ASSERT_EQ("aplext:hello:10", handlers[5].getURI()); - ASSERT_EQ("onGenericExternallyComingEvent", handlers[5].getName()); + + auto it = handlers.find("Hello:onEntityAdded"); + ASSERT_NE(handlers.end(), it); + auto handler = it->second; + ASSERT_EQ("aplext:hello:10", handler.getURI()); + ASSERT_EQ("onEntityAdded", handler.getName()); + + it = handlers.find("Hello:onEntityChanged"); + ASSERT_NE(handlers.end(), it); + handler = it->second; + ASSERT_EQ("aplext:hello:10", handler.getURI()); + ASSERT_EQ("onEntityChanged", handler.getName()); + + it = handlers.find("Hello:onEntityLost"); + ASSERT_NE(handlers.end(), it); + handler = it->second; + ASSERT_EQ("aplext:hello:10", handler.getURI()); + ASSERT_EQ("onEntityLost", handler.getName()); + + it = handlers.find("Hello:onDeviceUpdate"); + ASSERT_NE(handlers.end(), it); + handler = it->second; + ASSERT_EQ("aplext:hello:10", handler.getURI()); + ASSERT_EQ("onDeviceUpdate", handler.getName()); + + it = handlers.find("Hello:onDeviceRemove"); + ASSERT_NE(handlers.end(), it); + handler = it->second; + ASSERT_EQ("aplext:hello:10", handler.getURI()); + ASSERT_EQ("onDeviceRemove", handler.getName()); + + it = handlers.find("Hello:onGenericExternallyComingEvent"); + ASSERT_NE(handlers.end(), it); + handler = it->second; + ASSERT_EQ("aplext:hello:10", handler.getURI()); + ASSERT_EQ("onGenericExternallyComingEvent", handler.getName()); } TEST_F(ExtensionMediatorTest, ExtensionParseEventDataBindings) { loadExtensions(EXT_DOC); - auto ext = config->getSupportedExtensions(); - ASSERT_EQ(2, ext.size()); - auto ex = ext.find("aplext:hello:10"); - ASSERT_NE(ext.end(), ex); + // We do not rely on RootConfig to store extension-owned live data + ASSERT_EQ(0, config->getSupportedExtensions().size()); + ASSERT_EQ(0, config->getLiveObjectMap().size()); - auto liveDataMap = config->getLiveObjectMap(); - ASSERT_EQ(2, liveDataMap.size()); - auto arr = liveDataMap.at("entityList"); - auto map = liveDataMap.at("deviceState"); - ASSERT_EQ(LiveObject::ObjectType::kArrayType, arr->getType()); - ASSERT_EQ(LiveObject::ObjectType::kMapType, map->getType()); + inflate(); + + auto trackers = context->dataManager().trackers(); + ASSERT_EQ(2, trackers.size()); + + auto it = std::find_if(trackers.begin(), trackers.end(), [&](const std::shared_ptr& liveData) { + return liveData->getKey() == "entityList"; + }); + ASSERT_NE(trackers.end(), it); + ASSERT_EQ(LiveObject::ObjectType::kArrayType, (*it)->getType() ); + + it = std::find_if(trackers.begin(), trackers.end(), [&](const std::shared_ptr& liveData) { + return liveData->getKey() == "deviceState"; + }); + ASSERT_NE(trackers.end(), it); + ASSERT_EQ(LiveObject::ObjectType::kMapType, (*it)->getType() ); } @@ -1090,7 +1175,7 @@ TEST_F(ExtensionMediatorTest, CommandResolve) { // We have all we need. Inflate. inflate(); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); // Tap happened! @@ -1120,7 +1205,7 @@ void ExtensionMediatorTest::testLifecycle() { ASSERT_TRUE(hello->registered); ASSERT_TRUE(IsEqual(Object::TRUE_OBJECT(), evaluate(*context, "${environment.extension.Hello}"))); - auto text = component->findComponentById("label"); + auto text = root->findComponentById("label"); ASSERT_EQ(kComponentTypeText, text->getType()); auto canvas = root->findComponentById("MyCanvas"); @@ -1398,26 +1483,25 @@ TEST_F(ExtensionMediatorTest, AudioPlayerIntegration) { extensionProvider->registerExtension(std::make_shared(extension)); loadExtensions(AUDIO_PLAYER); - // The extension was registered - auto uris = config->getSupportedExtensions(); - ASSERT_EQ(1, uris.size()); - ASSERT_EQ(1, uris.count("aplext:audioplayer:10")); - - auto commands = config->getExtensionCommands(); - ASSERT_EQ(11, commands.size()); - - auto events = config->getExtensionEventHandlers(); - ASSERT_EQ(1, events.size()); - - auto liveDataMap = config->getLiveObjectMap(); - ASSERT_EQ(1, liveDataMap.size()); - inflate(); + // Validate the Extension environment + auto env = context->extensionManager().getEnvironment(); + ASSERT_EQ(1, env->size()); + ASSERT_EQ(1, env->count("AudioPlayer")); + ASSERT_TRUE(env->at("AudioPlayer").truthy()); ASSERT_TRUE(evaluate(*context, "${environment.extension.AudioPlayer}").isMap()); ASSERT_TRUE(IsEqual("APLAudioPlayerExtension-1.0", evaluate(*context, "${environment.extension.AudioPlayer.version}"))); + // Validate presence of command and event handler definitions + auto commands = context->extensionManager().getCommandDefinitions(); + ASSERT_EQ(11, commands.size()); + auto handlers = context->extensionManager().getEventHandlerDefinitions(); + ASSERT_EQ(1, handlers.size()); + // Validate Live Data + auto trackers = context->dataManager().trackers(); + ASSERT_EQ(1, trackers.size()); extension->updatePlayerActivity("PLAYING", 123); ASSERT_FALSE(ConsoleMessage()); root->clearPending(); @@ -3757,60 +3841,60 @@ class ComponentExtension : public alexaext::ExtensionBase { const char* ComponentExtension::URI = "test:component:1.0"; const char* COMPONENT_DOC = R"({ -"type": "APL", -"version": "1.9", -"theme": "dark", -"extensions": [ - { - "uri": "test:component:1.0", - "name": "Component" - } -], -"mainTemplate": { - "item": { - "type": "Container", - "width": "100%", - "height": "100%", - "items": [ - { - "type": "Component:Simple", - "id": "simple", - "width": 100, - "height": 100 - }, - { - "type": "Component:ResourceType", - "id": "resourceType", - "width": 100, - "height": 100, - "entities": [ "foo" ] - }, - { - "type": "Component:Properties", - "id": "properties", - "width": 100, - "height": 100, - "propA": true, - "propB": 42 - }, - { - "type": "Component:Events", - "id": "events", - "width": 100, - "height": 100, - "EventA": { - "type": "SetValue", - "property": "disabled", - "value": true + "type": "APL", + "version": "1.9", + "theme": "dark", + "extensions": [ + { + "uri": "test:component:1.0", + "name": "Component" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Component:Simple", + "id": "simple", + "width": 100, + "height": 100 + }, + { + "type": "Component:ResourceType", + "id": "resourceType", + "width": 100, + "height": 100, + "entities": [ "foo" ] }, - "EventB": { - "type": "SendEvent", - "arguments": [ "do it" ] + { + "type": "Component:Properties", + "id": "properties", + "width": 100, + "height": 100, + "propA": true, + "propB": 42 + }, + { + "type": "Component:Events", + "id": "events", + "width": 100, + "height": 100, + "EventA": { + "type": "SetValue", + "property": "disabled", + "value": true + }, + "EventB": { + "type": "SendEvent", + "arguments": [ "do it" ] + } } - } - ] + ] + } } -} })"; TEST_F(ExtensionMediatorTest, ExtensionComponentSchema) { @@ -3897,4 +3981,498 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentSchema) { ASSERT_EQ("do it", args[0].asString()); } -#endif \ No newline at end of file +const char* REQUIRED_URI = "test:required:1.0"; + +class RequiredExtension : public alexaext::ExtensionBase { +public: + RequiredExtension(bool fail) : ExtensionBase(REQUIRED_URI), + mActivityDescriptor(REQUIRED_URI, nullptr, ""), mFail(fail) {}; + + rapidjson::Document createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) override { + mActivityDescriptor = activity; + const auto& uri = activity.getURI(); + auto flags = RegistrationRequest::FLAGS().Get(registrationRequest); + if (flags->IsString()) + mFlags = flags->GetString(); + if (mFail) { + return RegistrationFailure("1.0").uri(uri).errorCode(1).errorMessage("Broke"); + } + return RegistrationSuccess("1.0") + .uri(uri) + .token("") + .schema("1.0", [uri](ExtensionSchema schema) { + schema.uri(uri); + }); + } + + std::string getFlags() { + return mFlags; + } + +private: + ActivityDescriptor mActivityDescriptor; + bool mFail; + std::string mFlags; +}; + +const char* REQUIRED_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "extensions": [ + { + "uri": "test:required:1.0", + "name": "Required", + "required": true + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "width": "100%", + "height": "100%", + "text": "${environment.extension.Required}" + } + } +})"; + +TEST_F(ExtensionMediatorTest, RequiredExtension) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto proxy = std::make_shared(std::make_shared(false)); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_TRUE(loaded); + ASSERT_EQ("true", component->getCalculated(apl::kPropertyText).asString()); +} + +TEST_F(ExtensionMediatorTest, RequiredExtensionWithFlags) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(false); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + ObjectMap flagMap{{REQUIRED_URI, "sampleflag"}}; + mediator->initializeExtensions(flagMap, content); + bool loaded = false; + mediator->loadExtensions(flagMap, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_TRUE(loaded); + ASSERT_EQ("true", component->getCalculated(apl::kPropertyText).asString()); + + ASSERT_EQ("sampleflag", extension->getFlags()); +} + +TEST_F(ExtensionMediatorTest, RequiredExtensionRegistrationFail) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto proxy = std::make_shared(std::make_shared(true)); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); +} + +TEST_F(ExtensionMediatorTest, RequiredExtensionDenied) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto proxy = std::make_shared(std::make_shared(false)); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content, [](const std::string& uri, + ExtensionMediator::ExtensionGrantResult grant, + ExtensionMediator::ExtensionGrantResult deny) { + deny(REQUIRED_URI); + }); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); +} + +class QuasiRemoteRequiredExtension : public alexaext::ExtensionProxy { +public: + QuasiRemoteRequiredExtension(bool failInitialization, bool failRegistrationRequest, bool failRegistration) + : mFailInitialization(failInitialization), + mFailRegistrationRequest(failRegistrationRequest), + mFailRegistration(failRegistration) {} + + std::set getURIs() const override { return std::set({REQUIRED_URI}); } + bool initializeExtension(const std::string &uri) override { + if (mFailInitialization) { + mInitialized = false; + return false; + } + mInitialized = true; + return true; + } + bool isInitialized(const std::string &uri) const override { return mInitialized; } + bool getRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest, + RegistrationSuccessActivityCallback&& success, + RegistrationFailureActivityCallback&& error) override { + mActivityDescriptor = activity; + mRegistrationSuccessCallback = std::move(success); + mRegistrationFailureCallback = std::move(error); + + return !mFailRegistrationRequest; + } + + void processRegistration() { + if (mFailRegistration) { + mRegistrationFailureCallback( + mActivityDescriptor, + RegistrationFailure("1.0").uri(REQUIRED_URI).errorCode(1).errorMessage("Broke")); + return; + } + + mRegistrationSuccessCallback( + mActivityDescriptor, + RegistrationSuccess("1.0") + .uri(REQUIRED_URI) + .token("") + .schema("1.0", [](ExtensionSchema schema) { + schema.uri(REQUIRED_URI); + })); + } + +private: + bool mFailInitialization; + bool mFailRegistrationRequest; + bool mFailRegistration; + ActivityDescriptor mActivityDescriptor = {REQUIRED_URI, nullptr, ""}; + bool mInitialized = false; + RegistrationSuccessActivityCallback mRegistrationSuccessCallback; + RegistrationFailureActivityCallback mRegistrationFailureCallback; +}; + +TEST_F(ExtensionMediatorTest, RequiredExtensionRemote) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto proxy = std::make_shared(false, false, false); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + proxy->processRegistration(); + + inflate(); + + ASSERT_TRUE(loaded); + ASSERT_EQ("true", component->getCalculated(apl::kPropertyText).asString()); +} + +const char* DOUBLE_REQUIRED_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "extensions": [ + { + "uri": "test:required:1.0", + "name": "Required", + "required": false + }, + { + "uri": "test:required:1.0", + "name": "Required", + "required": true + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "width": "100%", + "height": "100%", + "text": "${environment.extension.Required}" + } + } +})"; + +TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteDouble) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + auto proxy = std::make_shared(false, true, false); + extensionProvider->registerExtension(proxy); + + createContent(DOUBLE_REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); + + session->checkAndClear("Extension registration failure - code: 200 message: Invalid or malformed message.test:required:1.0"); +} + +const char* DOUBLE_NAME_REQUIRED_EXTENSION = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "extensions": [ + { + "uri": "test:required:1.0", + "name": "NotRequired", + "required": false + }, + { + "uri": "test:required:1.0", + "name": "Required", + "required": true + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "width": "100%", + "height": "100%", + "text": "${environment.extension.Required}" + } + } +})"; + +TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteDoubleNamed) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + auto proxy = std::make_shared(false, true, false); + extensionProvider->registerExtension(proxy); + + createContent(DOUBLE_NAME_REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); + + session->checkAndClear("Extension registration failure - code: 200 message: Invalid or malformed message.test:required:1.0"); +} + +TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteInitFail) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + auto proxy = std::make_shared(true, false, false); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); + + session->checkAndClear("Failed to retrieve proxy for extension: test:required:1.0"); +} + +TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteRequestFail) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + auto proxy = std::make_shared(false, true, false); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); + + session->checkAndClear("Extension registration failure - code: 200 message: Invalid or malformed message.test:required:1.0"); +} + +TEST_F(ExtensionMediatorTest, RequiredExtensionRemoteRegistrationFail) { + auto extSession = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + resourceProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + resourceProvider, + alexaext::Executor::getSynchronousExecutor(), + extSession); + + auto proxy = std::make_shared(false, false, true); + extensionProvider->registerExtension(proxy); + + createContent(REQUIRED_EXTENSION, nullptr); + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + ASSERT_TRUE(content->isReady()); + + mediator->initializeExtensions(config, content); + bool loaded = false; + mediator->loadExtensions(config, content, [&](bool result){ loaded = result; }); + proxy->processRegistration(); + + inflate(); + + ASSERT_FALSE(loaded); + ASSERT_EQ("false", component->getCalculated(apl::kPropertyText).asString()); + + session->checkAndClear("Extension registration failure - code: 200 message: Invalid or malformed message.test:required:1.0"); +} + +#endif diff --git a/unit/extension/unittest_extension_session.cpp b/aplcore/unit/extension/unittest_extension_session.cpp similarity index 100% rename from unit/extension/unittest_extension_session.cpp rename to aplcore/unit/extension/unittest_extension_session.cpp diff --git a/unit/extension/unittest_requested_extension.cpp b/aplcore/unit/extension/unittest_requested_extension.cpp similarity index 100% rename from unit/extension/unittest_requested_extension.cpp rename to aplcore/unit/extension/unittest_requested_extension.cpp diff --git a/aplcore/unit/faketextcomponent.h b/aplcore/unit/faketextcomponent.h new file mode 100644 index 0000000..3d30b84 --- /dev/null +++ b/aplcore/unit/faketextcomponent.h @@ -0,0 +1,52 @@ +/** +* Copyright 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 "apl/component/component.h" + +using namespace apl; + +class FakeTextComponent : public Component { +public: + FakeTextComponent(const ContextPtr& context, const std::string& id, const std::string& text) + : Component(context, id) + { + mCalculated.set(kPropertyText, text); + } + + void release() override {} + size_t getChildCount() const override {return 0;} + ComponentPtr getChildAt(size_t index) const override { return nullptr; } + bool appendChild(const ComponentPtr& child) override { return false; } + bool insertChild(const ComponentPtr& child, size_t index) override { return false; } + bool remove() override { return false; } + bool canInsertChild() const override { return false; } + bool canRemoveChild() const override { return false; } + ComponentType getType() const override { return kComponentTypeText; } + ComponentPtr getParent() const override { return nullptr; } + void update(UpdateType type, float value) override {} + void update(UpdateType type, const std::string& value) override {} + void ensureLayout(bool useDirtyFlag) override {} + size_t getDisplayedChildCount() const override{return 0;} + Point localToGlobal(Point) const override { return {0,0}; } + ComponentPtr getDisplayedChildAt(size_t drawIndex) const override { return nullptr; } + std::string getHierarchySignature() const override { return std::string(); } + rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override { return rapidjson::Value(); } + rapidjson::Value serializeAll(rapidjson::Document::AllocatorType& allocator) const override { return rapidjson::Value(); } + rapidjson::Value serializeDirty(rapidjson::Document::AllocatorType& allocator) override { return rapidjson::Value(); } + std::string provenance() const override { return std::string(); } + rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) override { return rapidjson::Value(); } + ComponentPtr findComponentById(const std::string& id) const override { return nullptr; } + ComponentPtr findComponentAtPosition(const Point& position) const override { return nullptr; } +}; \ No newline at end of file diff --git a/unit/focus/CMakeLists.txt b/aplcore/unit/focus/CMakeLists.txt similarity index 100% rename from unit/focus/CMakeLists.txt rename to aplcore/unit/focus/CMakeLists.txt diff --git a/unit/focus/unittest_focus_manager.cpp b/aplcore/unit/focus/unittest_focus_manager.cpp similarity index 100% rename from unit/focus/unittest_focus_manager.cpp rename to aplcore/unit/focus/unittest_focus_manager.cpp diff --git a/unit/focus/unittest_native_focus.cpp b/aplcore/unit/focus/unittest_native_focus.cpp similarity index 99% rename from unit/focus/unittest_native_focus.cpp rename to aplcore/unit/focus/unittest_native_focus.cpp index d980bf1..d976381 100644 --- a/unit/focus/unittest_native_focus.cpp +++ b/aplcore/unit/focus/unittest_native_focus.cpp @@ -6598,35 +6598,35 @@ TEST_F(NativeFocusTest, JumpBetweenTheRows) auto toFocus = focusableAreas.begin(); ASSERT_TRUE(root->setFocus(kFocusDirectionForward, toFocus->second, toFocus->first)); - auto child = component->findComponentById("row1"); + auto child = root->findComponentById("row1"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); root->nextFocus(kFocusDirectionDown); root->clearPending(); - child = component->findComponentById("row2"); + child = root->findComponentById("row2"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); root->nextFocus(kFocusDirectionRight); root->clearPending(); - child = component->findComponentById("button2"); + child = root->findComponentById("button2"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); root->nextFocus(kFocusDirectionDown); root->clearPending(); - child = component->findComponentById("row3"); + child = root->findComponentById("row3"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); root->nextFocus(kFocusDirectionRight); root->clearPending(); - child = component->findComponentById("button3"); + child = root->findComponentById("button3"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); @@ -6634,7 +6634,7 @@ TEST_F(NativeFocusTest, JumpBetweenTheRows) root->handleKeyboard(kKeyUp, Keyboard::ENTER_KEY()); root->clearPending(); - child = component->findComponentById("row4"); + child = root->findComponentById("row4"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); } @@ -6693,7 +6693,7 @@ TEST_F(NativeFocusTest, JustATest) { auto targetId = "1000"; auto origin = apl::Rect(top, left, width, height); ASSERT_TRUE(root->setFocus(static_cast(direction), origin, targetId)); - auto child = component->findComponentById("1000"); + auto child = root->findComponentById("1000"); ASSERT_EQ(child, fm.getFocus()); ASSERT_TRUE(verifyFocusSwitchEvent(child, root->popEvent())); } diff --git a/unit/graphic/CMakeLists.txt b/aplcore/unit/graphic/CMakeLists.txt similarity index 100% rename from unit/graphic/CMakeLists.txt rename to aplcore/unit/graphic/CMakeLists.txt diff --git a/unit/graphic/unittest_dependant_graphic.cpp b/aplcore/unit/graphic/unittest_dependant_graphic.cpp similarity index 99% rename from unit/graphic/unittest_dependant_graphic.cpp rename to aplcore/unit/graphic/unittest_dependant_graphic.cpp index e316186..b7d0eb3 100644 --- a/unit/graphic/unittest_dependant_graphic.cpp +++ b/aplcore/unit/graphic/unittest_dependant_graphic.cpp @@ -14,7 +14,6 @@ */ #include "../testeventloop.h" -#include "apl/graphic/graphicdependant.h" using namespace apl; diff --git a/unit/graphic/unittest_graphic.cpp b/aplcore/unit/graphic/unittest_graphic.cpp similarity index 99% rename from unit/graphic/unittest_graphic.cpp rename to aplcore/unit/graphic/unittest_graphic.cpp index 6958827..31772c1 100644 --- a/unit/graphic/unittest_graphic.cpp +++ b/aplcore/unit/graphic/unittest_graphic.cpp @@ -32,7 +32,7 @@ class GraphicTest : public DocumentWrapper { gc = GraphicContent::create(session, str); ASSERT_TRUE(gc); auto jr = JsonResource(&gc->get(), Path()); - auto context = Context::createTestContext(metrics, *config); + auto context = Context::createTestContext(metrics, *config, session); Properties properties; properties.emplace(propertyValues); graphic = Graphic::create(context, jr, std::move(properties), nullptr); @@ -950,10 +950,10 @@ TEST_F(GraphicTest, InvalidUpdateWithInvalidJson) { loadDocument(PILL_DOCUMENT); ASSERT_TRUE(component); - auto none = component->findComponentById("none"); + auto none = root->findComponentById("none"); ASSERT_TRUE(none); ASSERT_EQ(Object::NULL_OBJECT(), none->getCalculated(kPropertyGraphic)); - auto stretch = component->findComponentById("stretch"); + auto stretch = root->findComponentById("stretch"); ASSERT_TRUE(stretch); ASSERT_EQ(Object::NULL_OBJECT(), stretch->getCalculated(kPropertyGraphic)); @@ -962,13 +962,13 @@ TEST_F(GraphicTest, InvalidUpdateWithInvalidJson) { ASSERT_EQ(nullptr, graphicContent); ASSERT_TRUE(session->checkAndClear()); - none = component->findComponentById("none"); + none = root->findComponentById("none"); ASSERT_EQ(Object::NULL_OBJECT(), none->getCalculated(kPropertyGraphic)); auto result = stretch->updateGraphic(graphicContent); ASSERT_FALSE(result); - stretch = component->findComponentById("stretch"); + stretch = root->findComponentById("stretch"); ASSERT_EQ(Object::NULL_OBJECT(), stretch->getCalculated(kPropertyGraphic)); } @@ -976,10 +976,10 @@ TEST_F(GraphicTest, InvalidUpdateWithValidJson) { loadDocument(PILL_DOCUMENT); ASSERT_TRUE(component); - auto none = component->findComponentById("none"); + auto none = root->findComponentById("none"); ASSERT_TRUE(none); ASSERT_EQ(Object::NULL_OBJECT(), none->getCalculated(kPropertyGraphic)); - auto stretch = component->findComponentById("stretch"); + auto stretch = root->findComponentById("stretch"); ASSERT_TRUE(stretch); ASSERT_EQ(Object::NULL_OBJECT(), stretch->getCalculated(kPropertyGraphic)); @@ -987,9 +987,9 @@ TEST_F(GraphicTest, InvalidUpdateWithValidJson) { auto graphicContent = GraphicContent::create(session, std::move(json)); stretch->updateGraphic(graphicContent); - none = component->findComponentById("none"); + none = root->findComponentById("none"); ASSERT_EQ(Object::NULL_OBJECT(), none->getCalculated(kPropertyGraphic)); - stretch = component->findComponentById("stretch"); + stretch = root->findComponentById("stretch"); auto graphic = stretch->getCalculated(kPropertyGraphic).get(); ASSERT_TRUE(graphic); diff --git a/unit/graphic/unittest_graphic_bind.cpp b/aplcore/unit/graphic/unittest_graphic_bind.cpp similarity index 99% rename from unit/graphic/unittest_graphic_bind.cpp rename to aplcore/unit/graphic/unittest_graphic_bind.cpp index eed10ca..4a3a8d8 100644 --- a/unit/graphic/unittest_graphic_bind.cpp +++ b/aplcore/unit/graphic/unittest_graphic_bind.cpp @@ -14,7 +14,6 @@ */ #include "../testeventloop.h" -#include "apl/graphic/graphicdependant.h" using namespace apl; diff --git a/unit/graphic/unittest_graphic_component.cpp b/aplcore/unit/graphic/unittest_graphic_component.cpp similarity index 96% rename from unit/graphic/unittest_graphic_component.cpp rename to aplcore/unit/graphic/unittest_graphic_component.cpp index 3b56d2e..a33354d 100644 --- a/unit/graphic/unittest_graphic_component.cpp +++ b/aplcore/unit/graphic/unittest_graphic_component.cpp @@ -289,7 +289,7 @@ TEST_F(GraphicComponentTest, FitAndScale) auto content = Content::create(doc, makeDefaultSession()); ASSERT_TRUE(content && content->isReady()) << "Test case #" << index; - root = RootContext::create(Metrics().size(1024, 800), content); + root = std::static_pointer_cast(RootContext::create(Metrics().size(1024, 800), content)); ASSERT_TRUE(root) << "test case " << index; component = CoreComponent::cast(root->topComponent()); ASSERT_TRUE(component) << "test case " << index; @@ -380,7 +380,7 @@ TEST_F(GraphicComponentTest, StretchAndGrow) auto content = Content::create(doc, session); ASSERT_TRUE(content && content->isReady()) << "Test case #" << index; - root = RootContext::create(Metrics().size(1024, 800), content); + root = std::static_pointer_cast(RootContext::create(Metrics().size(1024, 800), content)); ASSERT_TRUE(root) << "test case " << index; component = CoreComponent::cast(root->topComponent()); ASSERT_TRUE(component) << "test case " << index; @@ -2733,4 +2733,137 @@ TEST_F(GraphicComponentTest, EnumParameterBinding) ASSERT_EQ(kGraphicLineJoinMiter, path->getValue(kGraphicPropertyStrokeLineJoin).asInt()); ASSERT_EQ(kGraphicLineCapRound, path->getValue(kGraphicPropertyStrokeLineCap).asInt()); +} + +static const char * VECTOR_GRAPHIC_CHARACTERISTICS = R"({ + "type": "APL", + "version": "2023.2", + "graphics": { + "box": { + "type": "AVG", + "version": "1.0", + "height": 100, + "width": 100, + "lang": "en-US", + "items": { + "type": "path", + "pathData": "M0,0 h100 v100 h-100 z", + "fill": "red" + } + } + }, + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "VectorGraphic", + "source": "box" + }, + { + "type": "VectorGraphic", + "source": "box", + "onFocus": { "type": "SendEvent" } + }, + { + "type": "VectorGraphic", + "source": "box", + "onBlur": { "type": "SendEvent" } + }, + { + "type": "VectorGraphic", + "source": "box", + "handleKeyDown": [ + { + "when": "${event.keyboard.code == 'KeyW'}", + "commands": { "type": "SendEvent" } + } + ] + }, + { + "type": "VectorGraphic", + "source": "box", + "handleKeyUp": [ + { + "when": "${event.keyboard.code == 'KeyW'}", + "commands": { "type": "SendEvent" } + } + ] + }, + { + "type": "VectorGraphic", + "source": "box", + "onDown": { "type": "SendEvent" } + }, + { + "type": "VectorGraphic", + "source": "box", + "onPress": { "type": "SendEvent" } + }, + { + "type": "VectorGraphic", + "source": "box", + "gestures": [ + { + "type": "DoublePress", + "onSinglePress": [ { "type": "SendEvent" } ] + } + ] + } + ] + } + } +})"; + +TEST_F(GraphicComponentTest, Characteristics) +{ + loadDocument(VECTOR_GRAPHIC_CHARACTERISTICS); + + auto vg = component->getCoreChildAt(0); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_FALSE(vg->isFocusable()); + ASSERT_FALSE(vg->isTouchable()); + ASSERT_FALSE(vg->isActionable()); + + vg = component->getCoreChildAt(1); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_FALSE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); + + vg = component->getCoreChildAt(2); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_FALSE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); + + vg = component->getCoreChildAt(3); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_FALSE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); + + vg = component->getCoreChildAt(4); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_FALSE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); + + vg = component->getCoreChildAt(5); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_TRUE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); + + vg = component->getCoreChildAt(6); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_TRUE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); + + vg = component->getCoreChildAt(7); + ASSERT_EQ(kComponentTypeVectorGraphic, vg->getType()); + ASSERT_TRUE(vg->isFocusable()); + ASSERT_TRUE(vg->isTouchable()); + ASSERT_TRUE(vg->isActionable()); } \ No newline at end of file diff --git a/unit/graphic/unittest_graphic_data.cpp b/aplcore/unit/graphic/unittest_graphic_data.cpp similarity index 99% rename from unit/graphic/unittest_graphic_data.cpp rename to aplcore/unit/graphic/unittest_graphic_data.cpp index c7f40a5..f123b73 100644 --- a/unit/graphic/unittest_graphic_data.cpp +++ b/aplcore/unit/graphic/unittest_graphic_data.cpp @@ -14,7 +14,6 @@ */ #include "../testeventloop.h" -#include "apl/graphic/graphicdependant.h" using namespace apl; diff --git a/unit/graphic/unittest_graphic_filters.cpp b/aplcore/unit/graphic/unittest_graphic_filters.cpp similarity index 100% rename from unit/graphic/unittest_graphic_filters.cpp rename to aplcore/unit/graphic/unittest_graphic_filters.cpp diff --git a/unit/livedata/CMakeLists.txt b/aplcore/unit/livedata/CMakeLists.txt similarity index 100% rename from unit/livedata/CMakeLists.txt rename to aplcore/unit/livedata/CMakeLists.txt diff --git a/unit/livedata/unittest_livearray_change.cpp b/aplcore/unit/livedata/unittest_livearray_change.cpp similarity index 100% rename from unit/livedata/unittest_livearray_change.cpp rename to aplcore/unit/livedata/unittest_livearray_change.cpp diff --git a/unit/livedata/unittest_livearray_rebuild.cpp b/aplcore/unit/livedata/unittest_livearray_rebuild.cpp similarity index 98% rename from unit/livedata/unittest_livearray_rebuild.cpp rename to aplcore/unit/livedata/unittest_livearray_rebuild.cpp index 17c4000..fdd2041 100644 --- a/unit/livedata/unittest_livearray_rebuild.cpp +++ b/aplcore/unit/livedata/unittest_livearray_rebuild.cpp @@ -99,35 +99,7 @@ class LiveArrayRebuildTest : public DocumentWrapper { */ ::testing::AssertionResult CheckUpdatedComponentsNotification(const std::vector& change) { - auto dirty = root->getDirty(); - if (!dirty.count(component)) - return ::testing::AssertionFailure() - << "No dirty property set."; - - if (!component->getDirty().count(kPropertyNotifyChildrenChanged)) - return ::testing::AssertionFailure() - << "No NotifyChildrenChanged property set."; - - auto notify = component->getCalculated(kPropertyNotifyChildrenChanged); - const auto& changed = notify.getArray(); - - if (changed.size() != change.size()) - return ::testing::AssertionFailure() - << "Inserted components count is wrong. Expected: " << change.size() - << ", actual: " << changed.size(); - - for (int i = 0; i(change.at(i))).toDebugString() - << ", actual: " << changed.at(i).toDebugString(); - } - } - - root->clearDirty(); - - return ::testing::AssertionSuccess(); + return CheckUpdatedChildrenNotification(root, component, change); } // Few commodity functions to perform scrolling to handle special change cases @@ -146,7 +118,7 @@ class LiveArrayRebuildTest : public DocumentWrapper { cmd.AddMember("componentId", rapidjson::Value(component.c_str(), alloc).Move(), alloc); cmd.AddMember("distance", distance, alloc); doc.SetArray().PushBack(cmd, alloc); - root->executeCommands(doc, false); + executeCommands(doc, false); } rapidjson::Document doc; diff --git a/unit/livedata/unittest_livemap_change.cpp b/aplcore/unit/livedata/unittest_livemap_change.cpp similarity index 100% rename from unit/livedata/unittest_livemap_change.cpp rename to aplcore/unit/livedata/unittest_livemap_change.cpp diff --git a/unit/media/CMakeLists.txt b/aplcore/unit/media/CMakeLists.txt similarity index 100% rename from unit/media/CMakeLists.txt rename to aplcore/unit/media/CMakeLists.txt diff --git a/unit/media/fakeplayer.cpp b/aplcore/unit/media/fakeplayer.cpp similarity index 99% rename from unit/media/fakeplayer.cpp rename to aplcore/unit/media/fakeplayer.cpp index 3ed5de1..04d6bda 100644 --- a/unit/media/fakeplayer.cpp +++ b/aplcore/unit/media/fakeplayer.cpp @@ -204,6 +204,12 @@ FakePlayer::seek(int offset) return true; } +bool +FakePlayer::seekTo(int offset) +{ + return seek(offset); +} + bool FakePlayer::clearRepeat() { diff --git a/unit/media/fakeplayer.h b/aplcore/unit/media/fakeplayer.h similarity index 95% rename from unit/media/fakeplayer.h rename to aplcore/unit/media/fakeplayer.h index d585bfa..8cba6bf 100644 --- a/unit/media/fakeplayer.h +++ b/aplcore/unit/media/fakeplayer.h @@ -223,6 +223,15 @@ class FakePlayer { */ bool seek(int offset); + /** + * Change the play head position. If the video has no more repeats, seeking to the end of + * the video will change the internal state to DONE. If the video was already done, seeking + * to an earlier spot will set the internal state to IDLE. + * @param offset The position to seek to (this is absolute to the offset) + * @return True if the play head moved. + */ + bool seekTo(int offset); + /** * Clear the repeat counter (used by the "setTrack" ControlMedia command). * @return True if the track was DONE with at least one repeat and now has been reset to IDLE. diff --git a/unit/media/testmediaplayer.cpp b/aplcore/unit/media/testmediaplayer.cpp similarity index 95% rename from unit/media/testmediaplayer.cpp rename to aplcore/unit/media/testmediaplayer.cpp index 15ad69c..6ab29cb 100644 --- a/unit/media/testmediaplayer.cpp +++ b/aplcore/unit/media/testmediaplayer.cpp @@ -222,6 +222,25 @@ TestMediaPlayer::seek(int offset) doCallback(kMediaPlayerEventTimeUpdate); // Always runs in fast mode } +/** + * ControlMedia.seekTo + * Can be called from normal or fast mode + */ +void +TestMediaPlayer::seekTo(int offset) +{ + if (mReleased || !mPlayer) + return; + + LOG_IF(DEBUG_MP) << "offset=" << offset << " " << mPlayer->toDebugString(); + + resolveExistingAction(); + + mPlayer->pause(); + if (mPlayer->seekTo(offset)) // TODO: Should we execute the onDone callback here? + doCallback(kMediaPlayerEventTimeUpdate); // Always runs in fast mode +} + /** * ControlMedia.setTrack * Can be called from normal or fast mode diff --git a/unit/media/testmediaplayer.h b/aplcore/unit/media/testmediaplayer.h similarity index 98% rename from unit/media/testmediaplayer.h rename to aplcore/unit/media/testmediaplayer.h index 83e3c4d..42d5bf1 100644 --- a/unit/media/testmediaplayer.h +++ b/aplcore/unit/media/testmediaplayer.h @@ -51,6 +51,7 @@ class TestMediaPlayer : public MediaPlayer, public Counter { void previous() override; void rewind() override; void seek(int offset) override; + void seekTo(int offset) override; void setTrackIndex(int trackIndex) override; void setMute(bool mute) override; diff --git a/unit/media/testmediaplayerfactory.h b/aplcore/unit/media/testmediaplayerfactory.h similarity index 100% rename from unit/media/testmediaplayerfactory.h rename to aplcore/unit/media/testmediaplayerfactory.h diff --git a/unit/media/unittest_fake_player.cpp b/aplcore/unit/media/unittest_fake_player.cpp similarity index 100% rename from unit/media/unittest_fake_player.cpp rename to aplcore/unit/media/unittest_fake_player.cpp diff --git a/unit/media/unittest_media_manager.cpp b/aplcore/unit/media/unittest_media_manager.cpp similarity index 99% rename from unit/media/unittest_media_manager.cpp rename to aplcore/unit/media/unittest_media_manager.cpp index 0d92198..7b64f17 100644 --- a/unit/media/unittest_media_manager.cpp +++ b/aplcore/unit/media/unittest_media_manager.cpp @@ -602,7 +602,7 @@ TEST_F(MediaManagerTest, OverrideManager) ASSERT_EQ(kMediaStateReady, root->findComponentById("myImage")->getCalculated(kPropertyMediaState).getInteger()); ASSERT_EQ(kMediaStateReady, root->findComponentById("myAVG")->getCalculated(kPropertyMediaState).getInteger()); - CheckDirty(root); // Nothing should be dirty because we loaded them immediately + ASSERT_TRUE(CheckDirty(root)); // Nothing should be dirty because we loaded them immediately } diff --git a/unit/media/unittest_media_player.cpp b/aplcore/unit/media/unittest_media_player.cpp similarity index 90% rename from unit/media/unittest_media_player.cpp rename to aplcore/unit/media/unittest_media_player.cpp index a90a570..db1d0ff 100644 --- a/unit/media/unittest_media_player.cpp +++ b/aplcore/unit/media/unittest_media_player.cpp @@ -295,6 +295,90 @@ TEST_F(MediaPlayerTest, BasicPlayback) "TrackIndex: 0 (0)", "TrackState: ready (ready)")); + // Seek in the video (this pauses the video as well) + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "seek"}, {"value", 100}}, false); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: TimeUpdate", + "URL: track1", + "Position: 100 (100)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: YES (YES)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // Start playing (again!) + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: Play", + "URL: track1", + "Position: 100 (100)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: NO (NO)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // Move forward 650 milliseconds + mediaPlayerFactory->advanceTime(650); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: TimeUpdate", + "URL: track1", + "Position: 750 (750)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: NO (NO)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // SeekTo in the video (this pauses the video as well) + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "seekTo"}, {"value", 100}}, false); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: TimeUpdate", + "URL: track1", + "Position: 100 (100)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: YES (YES)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // Start playing (again!) + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: Play", + "URL: track1", + "Position: 100 (100)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: NO (NO)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // Move forward 650 milliseconds + mediaPlayerFactory->advanceTime(650); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: TimeUpdate", + "URL: track1", + "Position: 750 (750)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: NO (NO)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + // Pause the video executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "pause"}}, false); ASSERT_TRUE(CheckSendEvent(root, @@ -543,6 +627,48 @@ TEST_F(MediaPlayerTest, BasicPlaybackNested) "TrackIndex: 0 (0)", "TrackState: ready (ready)")); + // SeekTo in the video (this pauses the video as well) + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "seekTo"}, {"value", 100}}, false); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: TimeUpdate", + "URL: track1", + "Position: 100 (100)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: YES (YES)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // Start playing (again!) + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: Play", + "URL: track1", + "Position: 100 (100)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: NO (NO)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + + // Move forward 650 milliseconds + mediaPlayerFactory->advanceTime(650); + ASSERT_TRUE(CheckSendEvent(root, + "Handler: TimeUpdate", + "URL: track1", + "Position: 750 (750)", + "Duration: 0 (0)", + "Ended: NO (NO)", + "Paused: NO (NO)", + "Muted: NO (NO)", + "TrackCount: 1 (1)", + "TrackIndex: 0 (0)", + "TrackState: ready (ready)")); + // Pause the video executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "pause"}}, false); ASSERT_TRUE(CheckSendEvent(root, diff --git a/unit/media/unittest_media_preservation.cpp b/aplcore/unit/media/unittest_media_preservation.cpp similarity index 100% rename from unit/media/unittest_media_preservation.cpp rename to aplcore/unit/media/unittest_media_preservation.cpp diff --git a/unit/media/unittest_media_utils.cpp b/aplcore/unit/media/unittest_media_utils.cpp similarity index 100% rename from unit/media/unittest_media_utils.cpp rename to aplcore/unit/media/unittest_media_utils.cpp diff --git a/unit/primitives/CMakeLists.txt b/aplcore/unit/primitives/CMakeLists.txt similarity index 100% rename from unit/primitives/CMakeLists.txt rename to aplcore/unit/primitives/CMakeLists.txt diff --git a/unit/primitives/unittest_color.cpp b/aplcore/unit/primitives/unittest_color.cpp similarity index 100% rename from unit/primitives/unittest_color.cpp rename to aplcore/unit/primitives/unittest_color.cpp diff --git a/unit/primitives/unittest_dimension.cpp b/aplcore/unit/primitives/unittest_dimension.cpp similarity index 100% rename from unit/primitives/unittest_dimension.cpp rename to aplcore/unit/primitives/unittest_dimension.cpp diff --git a/unit/primitives/unittest_filters.cpp b/aplcore/unit/primitives/unittest_filters.cpp similarity index 100% rename from unit/primitives/unittest_filters.cpp rename to aplcore/unit/primitives/unittest_filters.cpp diff --git a/unit/primitives/unittest_keyboard.cpp b/aplcore/unit/primitives/unittest_keyboard.cpp similarity index 100% rename from unit/primitives/unittest_keyboard.cpp rename to aplcore/unit/primitives/unittest_keyboard.cpp diff --git a/unit/primitives/unittest_object.cpp b/aplcore/unit/primitives/unittest_object.cpp similarity index 92% rename from unit/primitives/unittest_object.cpp rename to aplcore/unit/primitives/unittest_object.cpp index c14b004..025829d 100644 --- a/unit/primitives/unittest_object.cpp +++ b/aplcore/unit/primitives/unittest_object.cpp @@ -22,22 +22,20 @@ #include "../testeventloop.h" #include "apl/animation/easing.h" -#include "apl/engine/context.h" #include "apl/component/component.h" #include "apl/component/componenteventsourcewrapper.h" #include "apl/content/metrics.h" -#include "apl/livedata/livedataobject.h" +#include "apl/engine/arrayify.h" +#include "apl/engine/context.h" #include "apl/livedata/livearrayobject.h" +#include "apl/livedata/livedataobject.h" #include "apl/livedata/livemapobject.h" -#include "apl/primitives/object.h" #include "apl/primitives/gradient.h" +#include "apl/primitives/object.h" #include "apl/primitives/rect.h" #include "apl/primitives/transform.h" -#include "apl/engine/arrayify.h" #include "apl/utils/session.h" - - using namespace apl; TEST(ObjectTest, Constants) @@ -772,6 +770,57 @@ TEST_F(DocumentObjectTest, WhenNumberIsNotFiniteSerializeReturnsNull) ASSERT_TRUE(IsEqual(expectedObject, args.at(0))); } + +enum SerializedType { + kInt32, + kInt64, + kDouble, + kOther +}; +struct SerializedTestCase { + double value; + SerializedType type; +}; + +static const auto SERIALIZED_CASES = std::vector{ + { 0, kInt32 }, + { 2, kInt32 }, + { -23, kInt32 }, + { 2147483647, kInt32 }, // 2^31-1 + { -2147483648, kInt32 }, // -2^31 + { 2147483648, kInt64 }, // Just a little too large + { -2147483649, kInt64 }, // Just a little too small + { 9007199254740990, kInt64 }, // 2^53 - 2 + { 9007199254740991, kInt64 }, // 2^53 - 1 + { 9007199254740992, kInt64 }, // 2^53 + { -9007199254740990, kInt64 }, // 2^53 - 2 + { -9007199254740991, kInt64 }, // 2^53 - 1 + { -9007199254740992, kInt64 }, // 2^53 + { 2e54, kDouble }, + { -2e54, kDouble }, + { 0.0000001, kDouble }, + { -0.0000001, kDouble }, + { 2147483647.01, kDouble }, + { -2147483647.01, kDouble }, + { INFINITY, kOther }, + { -INFINITY, kOther }, + { NAN, kOther }, +}; + +TEST_F(DocumentObjectTest, SerializeDoubles) +{ + rapidjson::Document doc; + + for (const auto& m : SERIALIZED_CASES) { + auto value = Object(m.value).serialize(doc.GetAllocator()); + ASSERT_TRUE(value.IsInt() == (m.type == kInt32)) << m.value; + ASSERT_TRUE(value.IsInt64() == (m.type == kInt32 || m.type == kInt64)) << m.value; + ASSERT_TRUE(value.IsDouble() == (m.type == kDouble)) << m.value; + ASSERT_TRUE(value.IsNull() == (m.type == kOther)); + } +} + + TEST_F(DocumentObjectTest, ArrayComparison) { rapidjson::Document jsonArrays; @@ -888,3 +937,31 @@ TEST_F(DocumentObjectTest, LiveDataAccess) ASSERT_TRUE(liveArray.isArray()); ASSERT_TRUE(liveArray.getLiveDataObject()); } + +struct EqualityStruct { + Object first; + Object second; + bool result; +}; + +static const std::vector EQUALITY{ + {0, 0, true}, + {0, 1, false}, + {true, true, true}, + {true, false, false}, + {"first", "first", true}, + {"first", "second", false}, + {Rect(0, 0, 100, 100), Rect(0, 0, 100, 100), true}, + {Rect(0, 0, 100, 100), Rect(0, 0, 100, 150), false}, + {Color(1), Color(1), true}, + {Color(1), Color(2), false}, + {Dimension(1), Dimension(1), true}, + {Dimension(1), Dimension(2), false}, +}; + +TEST(ObjectTest, Equality) +{ + for (const auto& m : EQUALITY) { + ASSERT_EQ(m.result, m.first == m.second) << "'" << m.first << "' : " << m.second; + } +} diff --git a/unit/primitives/unittest_radii.cpp b/aplcore/unit/primitives/unittest_radii.cpp similarity index 100% rename from unit/primitives/unittest_radii.cpp rename to aplcore/unit/primitives/unittest_radii.cpp diff --git a/unit/primitives/unittest_range.cpp b/aplcore/unit/primitives/unittest_range.cpp similarity index 100% rename from unit/primitives/unittest_range.cpp rename to aplcore/unit/primitives/unittest_range.cpp diff --git a/unit/primitives/unittest_rect.cpp b/aplcore/unit/primitives/unittest_rect.cpp similarity index 94% rename from unit/primitives/unittest_rect.cpp rename to aplcore/unit/primitives/unittest_rect.cpp index b7fe597..5668c0c 100644 --- a/unit/primitives/unittest_rect.cpp +++ b/aplcore/unit/primitives/unittest_rect.cpp @@ -219,4 +219,17 @@ TEST_F(RectTest, Inset) { // Test going to zero width/height ASSERT_EQ(Rect(0,0,0,20), Rect(-10, -20, 20, 60).inset(10, 20)); ASSERT_EQ(Rect(25,40,0,0), Rect(10,20,30,40).inset(100)); +} + +TEST_F(RectTest, Object) { + Object object = Rect(3,4,3,4); + ASSERT_TRUE(object.is()); + ASSERT_EQ(Rect(3,4,3,4), object.get()); + //ASSERT_TRUE(object.type() == Rect::ObjectType::instance()); + ASSERT_FALSE(object.empty()); + + object = Rect(); + ASSERT_TRUE(object.is()); + ASSERT_EQ(Rect(), object.get()); + ASSERT_TRUE(object.empty()); } \ No newline at end of file diff --git a/unit/primitives/unittest_roundedrect.cpp b/aplcore/unit/primitives/unittest_roundedrect.cpp similarity index 100% rename from unit/primitives/unittest_roundedrect.cpp rename to aplcore/unit/primitives/unittest_roundedrect.cpp diff --git a/unit/primitives/unittest_styledtext.cpp b/aplcore/unit/primitives/unittest_styledtext.cpp similarity index 100% rename from unit/primitives/unittest_styledtext.cpp rename to aplcore/unit/primitives/unittest_styledtext.cpp diff --git a/unit/primitives/unittest_symbols.cpp b/aplcore/unit/primitives/unittest_symbols.cpp similarity index 68% rename from unit/primitives/unittest_symbols.cpp rename to aplcore/unit/primitives/unittest_symbols.cpp index fdd21c5..2b6066e 100644 --- a/unit/primitives/unittest_symbols.cpp +++ b/aplcore/unit/primitives/unittest_symbols.cpp @@ -13,11 +13,13 @@ * permissions and limitations under the License. */ +#include #include #include "../testeventloop.h" -#include "apl/primitives/symbolreferencemap.h" +#include "apl/primitives/boundsymbolset.h" +#include "apl/datagrammar/bytecode.h" using namespace apl; @@ -58,61 +60,71 @@ class SymbolTest : public MemoryWrapper { std::map>> BASIC_TESTS = { - {"${a}", {{"CONTEXT_1", "a/"}}}, - {"${ a }", {{"CONTEXT_1", "a/"}}}, - {"${a+b}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${27+b}", {{"CONTEXT_2", "b/"}}}, - {"${a ? b : -1}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${a ? -1 : b}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${0 ? a : b}", {{"CONTEXT_2", "b/"}}}, - {"${1 ? a : b}", {{"CONTEXT_1", "a/"}}}, - {"${c[0] ? a : b}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}, {"CONTEXT_1", "c/0/"}}}, - {"${0||b}", {{"CONTEXT_2", "b/"}}}, - {"${a||b}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${1&&b}", {{"CONTEXT_2", "b/"}}}, - {"${a&&b}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${Math.min(a,b)}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${c}", {{"CONTEXT_1", "c/"}}}, - {"${c[0]}", {{"CONTEXT_1", "c/0/"}}}, - {"${c[a]}", {{"CONTEXT_1", "c/"}, {"CONTEXT_1", "a/"}}}, - {"${c[b]}", {{"CONTEXT_1", "c/"}, {"CONTEXT_2", "b/"}}}, - {"${c[23 + c[b]]}", {{"CONTEXT_2", "b/"}, {"CONTEXT_1", "c/"}}}, - {"${b} ${Math.min(24,c[a+1])}", {{"CONTEXT_1", "c/"}, {"CONTEXT_1", "a/"}, {"CONTEXT_2", "b/"}}}, - {"${c[-1]}", {{"CONTEXT_1", "c/-1/"}}}, - {"${d}", {{"CONTEXT_2", "d/"}}}, - {"${d.name}", {{"CONTEXT_2", "d/name/"}}}, - {"${d['age']}", {{"CONTEXT_2", "d/age/"}}}, - {"${d.friends[-1]}", {{"CONTEXT_2", "d/friends/-1/"}}}, + {"${a}", {{"CONTEXT_1", "a"}}}, + {"${ a }", {{"CONTEXT_1", "a"}}}, + {"${a+b}", {{"CONTEXT_1", "a"}, {"CONTEXT_2", "b"}}}, + {"${27+b}", {{"CONTEXT_2", "b"}}}, + {"${a ? b : -1}", {{"CONTEXT_1", "a"}, {"CONTEXT_2", "b"}}}, + {"${a ? -1 : b}", {{"CONTEXT_1", "a"}}}, + {"${0 ? a : b}", {{"CONTEXT_2", "b"}}}, + {"${1 ? a : b}", {{"CONTEXT_1", "a"}}}, + {"${c[0] ? a : b}", {{"CONTEXT_1", "a"}, {"CONTEXT_1", "c"}}}, + {"${0||b}", {{"CONTEXT_2", "b"}}}, + {"${a||b}", {{"CONTEXT_1", "a"}}}, + {"${1&&b}", {{"CONTEXT_2", "b"}}}, + {"${a&&b}", {{"CONTEXT_1", "a"}, {"CONTEXT_2", "b"}}}, + {"${Math.min(a,b)}", {{"CONTEXT_1", "a"}, {"CONTEXT_2", "b"}}}, + {"${c}", {{"CONTEXT_1", "c"}}}, + {"${c[0]}", {{"CONTEXT_1", "c"}}}, + {"${c[a]}", {{"CONTEXT_1", "c"}, {"CONTEXT_1", "a"}}}, + {"${c[b]}", {{"CONTEXT_1", "c"}, {"CONTEXT_2", "b"}}}, + {"${c[23 + c[b]]}", {{"CONTEXT_2", "b"}, {"CONTEXT_1", "c"}}}, + {"${b} ${Math.min(24,c[a+1])}", {{"CONTEXT_1", "c"}, {"CONTEXT_1", "a"}, {"CONTEXT_2", "b"}}}, + {"${c[-1]}", {{"CONTEXT_1", "c"}}}, + {"${d}", {{"CONTEXT_2", "d"}}}, + {"${d.name}", {{"CONTEXT_2", "d"}}}, + {"${d['age']}", {{"CONTEXT_2", "d"}}}, + {"${d.friends[-1]}", {{"CONTEXT_2", "d"}}}, {"${Math.random() + 1}", {}}, - {"${d.friends[2+3]}", {{"CONTEXT_2", "d/friends/5/"}}}, + {"${d.friends[2+3]}", {{"CONTEXT_2", "d"}}}, {"${Math.random() * Math.random()}", {}}, - {"${d.friends[c[2]]}", {{"CONTEXT_1", "c/2/"}, {"CONTEXT_2", "d/friends/"}}}, - {"${c[d.friends.length - 2]}", {{"CONTEXT_1", "c/"}, {"CONTEXT_2", "d/friends/length/"}}}, - {"${c[Math.round(2.3)]}", {{"CONTEXT_1", "c/2/"}}}, - {"${c[Math.random()]}", {{"CONTEXT_1", "c/"}}}, + {"${d.friends[c[2]]}", {{"CONTEXT_1", "c"}, {"CONTEXT_2", "d"}}}, + {"${c[d.friends.length - 2]}", {{"CONTEXT_1", "c"}, {"CONTEXT_2", "d"}}}, + {"${c[Math.round(2.3)]}", {{"CONTEXT_1", "c"}}}, + {"${c[Math.random()]}", {{"CONTEXT_1", "c"}}}, {"${Math.max(Math.random(), Math.random())}", {}}, - {"${c[Math.min(Math.random(), Math.random())]}", {{"CONTEXT_1", "c/"}}}, - {"${d.friends[Math.random()]}", {{"CONTEXT_2", "d/friends/"}}}, - {"${d.friends[Math.random()*d.friends.length]}", {{"CONTEXT_2", "d/friends/"}}}, - {"${String.toUpperCase(d.friends[d.friends.length-1])}", {{"CONTEXT_2", "d/friends/"}}}, - {"${c[2] + c.length + c[Math.random()]}", {{"CONTEXT_1", "c/"}}}, - {"${c[Math.random()] + c.length + c[2]}", {{"CONTEXT_1", "c/"}}}, - {"${Math.max(Math.min(1,a), Math.min(d.friends[2], b))}", {{"CONTEXT_1", "a/"}, {"CONTEXT_2", "d/friends/2/"}, {"CONTEXT_2", "b/"}}}, + {"${c[Math.min(Math.random(), Math.random())]}", {{"CONTEXT_1", "c"}}}, + {"${d.friends[Math.random()]}", {{"CONTEXT_2", "d"}}}, + {"${d.friends[Math.random()*d.friends.length]}", {{"CONTEXT_2", "d"}}}, + {"${String.toUpperCase(d.friends[d.friends.length-1])}", {{"CONTEXT_2", "d"}}}, + {"${c[2] + c.length + c[Math.random()]}", {{"CONTEXT_1", "c"}}}, + {"${c[Math.random()] + c.length + c[2]}", {{"CONTEXT_1", "c"}}}, + {"${Math.max(Math.min(1,a), Math.min(d.friends[2], b))}", {{"CONTEXT_1", "a"}, {"CONTEXT_2", "d"}, {"CONTEXT_2", "b"}}}, }; TEST_F(SymbolTest, Basic) { for (const auto& m : BASIC_TESTS) { - auto node = parseDataBinding(*base, m.first); - ASSERT_TRUE(node.isEvaluable()); - - SymbolReferenceMap map; - node.symbols(map); - - // Convert the data structure to a map and compare - std::map other; - for (const auto& p : m.second) - other.emplace(p.second, contexts.at(p.first)); - EXPECT_EQ(other, map.get()) << m.first << " " << map.toDebugString(); + auto result = parseAndEvaluate(*base, m.first); + ASSERT_EQ(m.second.size(), result.symbols.size()) << m.first; + for (const auto& p : m.second) { + auto it = std::find(result.symbols.begin(), + result.symbols.end(), + BoundSymbol{contexts.at(p.first), p.second}); + ASSERT_TRUE(it != result.symbols.end()) << m.first; + } } } + +TEST_F(SymbolTest, BoundSymbol) +{ + auto bs = BoundSymbol{base, "a"}; + ASSERT_TRUE(bs.truthy()); + ASSERT_FALSE(bs.empty()); + ASSERT_TRUE(IsEqual(bs.toDebugString(), "BoundSymbol")); + + bs = BoundSymbol{base, "missing"}; + ASSERT_FALSE(bs.truthy()); + ASSERT_TRUE(bs.empty()); + ASSERT_TRUE(IsEqual(bs.toDebugString(), "BoundSymbol")); +} diff --git a/unit/primitives/unittest_time_grammar.cpp b/aplcore/unit/primitives/unittest_time_grammar.cpp similarity index 100% rename from unit/primitives/unittest_time_grammar.cpp rename to aplcore/unit/primitives/unittest_time_grammar.cpp diff --git a/unit/primitives/unittest_transform.cpp b/aplcore/unit/primitives/unittest_transform.cpp similarity index 100% rename from unit/primitives/unittest_transform.cpp rename to aplcore/unit/primitives/unittest_transform.cpp diff --git a/unit/primitives/unittest_unicode.cpp b/aplcore/unit/primitives/unittest_unicode.cpp similarity index 76% rename from unit/primitives/unittest_unicode.cpp rename to aplcore/unit/primitives/unittest_unicode.cpp index c0320a2..bd135ff 100644 --- a/unit/primitives/unittest_unicode.cpp +++ b/aplcore/unit/primitives/unittest_unicode.cpp @@ -23,7 +23,6 @@ using namespace apl; class UnicodeTest : public ::testing::Test {}; - struct LengthTest { std::string s; int bytes; @@ -58,7 +57,7 @@ static auto STRING_LENGTH_TESTS = std::vector{ {u8"\u0800\uffff", 6, 2 }, // Two three-byte characters {u8"\U00010000\U0010ffff", 8, 2}, // Two four-byte characters {u8"a\u00a3\u0939\U00010349", 10, 4}, // One of each type - {u8"hétérogénéité", 18, 13}, + {u8"hétérogénéité", 18, 13}, {"\x80", 1, -1}, // Invalid (this should be a trailing byte) {"\xbf", 1, -1}, // Invalid (this should be a trailing byte) {"\x20\x90", 2, -1}, // Trailing byte does not follow a two-byte header @@ -73,6 +72,61 @@ TEST_F(UnicodeTest, StringLength) { } } +struct LengthWithRangeTest { + std::string s; + int start; + int count; + int codepoints; + + std::string toString() const { + auto result = std::string("'") + s + "'"; + result += " start=" + std::to_string(start); + result += " count=" + std::to_string(count); + result += " cp=" + std::to_string(codepoints); + result += " raw="; + for (const auto& m : s) { + char hex[10]; + snprintf(hex, sizeof(hex), "%02x", (uint8_t)m); + result += std::string(hex); + } + return result; + } +}; + +static auto STRING_LENGTH_WITH_RANGE_TESTS = std::vector{ + // Test cases that start at the string boundary. + {u8"fuzzy", 0, 2, 2}, + {u8"\u007f\u0001", 0, 1, 1}, // Two single byte characters + {u8"\u0080\u07ff", 0, 2, 1}, // Two two-byte characters + {u8"\u0800\uffff", 0, 3, 1}, // Two three-byte characters + {u8"\U00010000\U0010ffff", 0, 4, 1}, // Two four-byte characters + + // Test cases that start at a codepoint boundary. + {u8"fuzzy", 2, 2, 2}, + {u8"\u007f\u0001", 1, 1, 1}, // Two single byte characters + {u8"\u0080\u07ff", 2, 2, 1}, // Two two-byte characters + {u8"\u0800\uffff", 3, 3, 1}, // Two three-byte characters + {u8"\U00010000\U0010ffff", 4, 4, 1}, // Two four-byte characters + + // Test cases that start or end in the middle of a codepoint boundary. + {u8"\u0080\u07ff", 1, 2, -1}, // Start in the middle of a utf8 codepoint + {u8"\u0800\uffff", 1, 3, -1}, // Start in the middle of a utf8 codepoint + {u8"\U00010000\U0010ffff", 1, 4, -1}, // Start in the middle of a utf8 codepoint + {u8"\u0080\u07ff", 0, 3, -1}, // End in the middle of a utf8 codepoint + {u8"\u0800\uffff", 0, 4, -1}, // End in the middle of a utf8 codepoint + {u8"\U00010000\U0010ffff", 0, 5, -1}, // End in the middle of a utf8 codepoint + + // Length Overflow protection test. + {u8"fuzzy", 0, 10, 5}, +}; + +TEST_F(UnicodeTest, StringLengthWithRange) { + for (const auto& m : STRING_LENGTH_WITH_RANGE_TESTS) { + const uint8_t* ptr = (uint8_t*)m.s.data() + m.start; + ASSERT_EQ(m.codepoints, utf8StringLength(ptr, m.count)) << m.toString(); + } +} + struct SubstringTest { std::string original; int start; diff --git a/unit/scaling/CMakeLists.txt b/aplcore/unit/scaling/CMakeLists.txt similarity index 94% rename from unit/scaling/CMakeLists.txt rename to aplcore/unit/scaling/CMakeLists.txt index 9e80ed4..171ce91 100644 --- a/unit/scaling/CMakeLists.txt +++ b/aplcore/unit/scaling/CMakeLists.txt @@ -13,5 +13,6 @@ target_sources_local(unittest PRIVATE + unittest_auto_size.cpp unittest_scaling.cpp ) \ No newline at end of file diff --git a/aplcore/unit/scaling/unittest_auto_size.cpp b/aplcore/unit/scaling/unittest_auto_size.cpp new file mode 100644 index 0000000..474aec9 --- /dev/null +++ b/aplcore/unit/scaling/unittest_auto_size.cpp @@ -0,0 +1,163 @@ +/* + * Copyright 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 "../testeventloop.h" + +using namespace apl; + +class AutoSizeTest : public apl::DocumentWrapper {}; + +static const char *BASIC_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "width": 123, + "height": 345 + } + } +} +)apl"; + +TEST_F(AutoSizeTest, Basic) +{ + metrics.size(300, 300).autoSizeHeight(true).autoSizeWidth(true); + loadDocument(BASIC_TEST); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Rect(0,0,123,345), component->getCalculated(apl::kPropertyBounds))); +} + +static const char *EMBEDDED_TEST = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "item": { + "type": "Frame", + "width": 100, + "height": 200 + } + } + } +} +)apl"; + +TEST_F(AutoSizeTest, Embedded) +{ + metrics.size(300, 300).autoSizeWidth(true); + loadDocument(EMBEDDED_TEST); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Rect(0,0,100,300), component->getCalculated(apl::kPropertyBounds))); + + metrics.size(500,500).autoSizeWidth(false).autoSizeHeight(true); + loadDocument(EMBEDDED_TEST); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Rect(0,0,500,200), component->getCalculated(apl::kPropertyBounds))); + + metrics.size(400,400).autoSizeWidth(true).autoSizeHeight(true); + loadDocument(EMBEDDED_TEST); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Rect(0,0,100,200), component->getCalculated(apl::kPropertyBounds))); +} + +static const char *SCROLL_VIEW = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "ScrollView", + "item": { + "type": "Frame", + "width": 300, + "height": 1000 + } + } + } +} +)apl"; + +TEST_F(AutoSizeTest, ScrollView) +{ + // The ScrollView defaults to an auto-sized width and a height of 100. + metrics.autoSizeWidth(true).autoSizeHeight(true); + loadDocument(SCROLL_VIEW); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(Rect(0,0,300,100), component->getCalculated(apl::kPropertyBounds))); +} + +static const char *RESIZING = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Frame", + "borderWidth": 1, + "item": { + "type": "Frame", + "id": "FOO", + "width": 10, + "height": 20 + } + } + } +} +)apl"; + +TEST_F(AutoSizeTest, Resizing) +{ + auto doInitialize = [&](const char *document, float width, float height) -> ::testing::AssertionResult { + loadDocument(document); + if (!component) + return ::testing::AssertionFailure() << "Failed to load document"; + return IsEqual(Rect(0, 0, width, height), component->getCalculated(kPropertyBounds)); + }; + + auto doTest = [&](const char *property, int value, float width, float height) -> ::testing::AssertionResult { + executeCommand("SetValue", {{"componentId", "FOO"}, {"property", property}, {"value", value}}, true); + root->clearPending(); + return IsEqual(Rect(0,0,width,height), component->getCalculated(kPropertyBounds)); + }; + + // Allow resizing in both direction + metrics.size(100,200).autoSizeWidth(true).autoSizeHeight(true); + ASSERT_TRUE(doInitialize(RESIZING, 12, 22)); + ASSERT_TRUE(doTest("width", 40, 42, 22)); + ASSERT_TRUE(doTest("height", 70, 42, 72)); + + // Auto-size width + metrics.size(100,200).autoSizeWidth(true).autoSizeHeight(false); + ASSERT_TRUE(doInitialize(RESIZING, 12, 200)); + ASSERT_TRUE(doTest("width", 40, 42, 200)); + ASSERT_TRUE(doTest("height", 70, 42, 200)); + + // Auto-size height + metrics.size(100,200).autoSizeWidth(false).autoSizeHeight(true); + ASSERT_TRUE(doInitialize(RESIZING, 100, 22)); + ASSERT_TRUE(doTest("width", 40, 100, 22)); + ASSERT_TRUE(doTest("height", 70, 100, 72)); + + // No auto-sizing + metrics.size(100,200).autoSizeWidth(false).autoSizeHeight(false); + ASSERT_TRUE(doInitialize(RESIZING, 100, 200)); + ASSERT_TRUE(doTest("width", 40, 100, 200)); + ASSERT_TRUE(doTest("height", 70, 100, 200)); +} \ No newline at end of file diff --git a/unit/scaling/unittest_scaling.cpp b/aplcore/unit/scaling/unittest_scaling.cpp similarity index 100% rename from unit/scaling/unittest_scaling.cpp rename to aplcore/unit/scaling/unittest_scaling.cpp diff --git a/unit/scenegraph/CMakeLists.txt b/aplcore/unit/scenegraph/CMakeLists.txt similarity index 100% rename from unit/scenegraph/CMakeLists.txt rename to aplcore/unit/scenegraph/CMakeLists.txt diff --git a/unit/scenegraph/test_sg.cpp b/aplcore/unit/scenegraph/test_sg.cpp similarity index 100% rename from unit/scenegraph/test_sg.cpp rename to aplcore/unit/scenegraph/test_sg.cpp diff --git a/unit/scenegraph/test_sg.h b/aplcore/unit/scenegraph/test_sg.h similarity index 100% rename from unit/scenegraph/test_sg.h rename to aplcore/unit/scenegraph/test_sg.h diff --git a/unit/scenegraph/testedittext.cpp b/aplcore/unit/scenegraph/testedittext.cpp similarity index 100% rename from unit/scenegraph/testedittext.cpp rename to aplcore/unit/scenegraph/testedittext.cpp diff --git a/unit/scenegraph/testedittext.h b/aplcore/unit/scenegraph/testedittext.h similarity index 100% rename from unit/scenegraph/testedittext.h rename to aplcore/unit/scenegraph/testedittext.h diff --git a/unit/scenegraph/unittest_sg_accessibility.cpp b/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_accessibility.cpp rename to aplcore/unit/scenegraph/unittest_sg_accessibility.cpp diff --git a/unit/scenegraph/unittest_sg_edit_text.cpp b/aplcore/unit/scenegraph/unittest_sg_edit_text.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_edit_text.cpp rename to aplcore/unit/scenegraph/unittest_sg_edit_text.cpp diff --git a/unit/scenegraph/unittest_sg_edit_text_config.cpp b/aplcore/unit/scenegraph/unittest_sg_edit_text_config.cpp similarity index 96% rename from unit/scenegraph/unittest_sg_edit_text_config.cpp rename to aplcore/unit/scenegraph/unittest_sg_edit_text_config.cpp index ba4445a..88d8cf5 100644 --- a/unit/scenegraph/unittest_sg_edit_text_config.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_edit_text_config.cpp @@ -30,12 +30,12 @@ TEST_F(SGEditTextConfig, Basic) {"Arial", "Helvetica"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900); auto config = sg::EditTextConfig::create(Color::BLUE, Color::RED, KeyboardType::kKeyboardTypeEmailAddress, - "en-US", 20, false, SubmitKeyType::kSubmitKeyTypeNext, @@ -51,7 +51,6 @@ TEST_F(SGEditTextConfig, Basic) "highlightColor": "#ff0000ff", "keyboardType": "emailAddress", "keyboardBehaviorOnFocus": "openKeyboard", - "language": "en-US", "maxLength": 20, "secureInput": false, "selectOnFocus": true, @@ -61,6 +60,7 @@ TEST_F(SGEditTextConfig, Basic) "fontFamily": ["Arial","Helvetica"], "fontSize": 22.0, "fontStyle": "normal", + "lang": "en-US", "fontWeight": 900, "letterSpacing": 0.0, "lineHeight": 1.25, @@ -79,12 +79,12 @@ TEST_F(SGEditTextConfig, ValidateAndStrip) {"Arial", "Helvetica"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900); auto config = sg::EditTextConfig::create(Color::BLUE, Color::RED, KeyboardType::kKeyboardTypeEmailAddress, - "en-US", 10, false, SubmitKeyType::kSubmitKeyTypeNext, diff --git a/unit/scenegraph/unittest_sg_filter.cpp b/aplcore/unit/scenegraph/unittest_sg_filter.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_filter.cpp rename to aplcore/unit/scenegraph/unittest_sg_filter.cpp diff --git a/unit/scenegraph/unittest_sg_frame.cpp b/aplcore/unit/scenegraph/unittest_sg_frame.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_frame.cpp rename to aplcore/unit/scenegraph/unittest_sg_frame.cpp diff --git a/unit/scenegraph/unittest_sg_graphic.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_graphic.cpp rename to aplcore/unit/scenegraph/unittest_sg_graphic.cpp diff --git a/unit/scenegraph/unittest_sg_graphic_component.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic_component.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_graphic_component.cpp rename to aplcore/unit/scenegraph/unittest_sg_graphic_component.cpp diff --git a/unit/scenegraph/unittest_sg_graphic_layers.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic_layers.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_graphic_layers.cpp rename to aplcore/unit/scenegraph/unittest_sg_graphic_layers.cpp diff --git a/unit/scenegraph/unittest_sg_graphic_loading.cpp b/aplcore/unit/scenegraph/unittest_sg_graphic_loading.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_graphic_loading.cpp rename to aplcore/unit/scenegraph/unittest_sg_graphic_loading.cpp diff --git a/unit/scenegraph/unittest_sg_image.cpp b/aplcore/unit/scenegraph/unittest_sg_image.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_image.cpp rename to aplcore/unit/scenegraph/unittest_sg_image.cpp diff --git a/unit/scenegraph/unittest_sg_layer.cpp b/aplcore/unit/scenegraph/unittest_sg_layer.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_layer.cpp rename to aplcore/unit/scenegraph/unittest_sg_layer.cpp diff --git a/unit/scenegraph/unittest_sg_line_highlighting.cpp b/aplcore/unit/scenegraph/unittest_sg_line_highlighting.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_line_highlighting.cpp rename to aplcore/unit/scenegraph/unittest_sg_line_highlighting.cpp diff --git a/unit/scenegraph/unittest_sg_node.cpp b/aplcore/unit/scenegraph/unittest_sg_node.cpp similarity index 98% rename from unit/scenegraph/unittest_sg_node.cpp rename to aplcore/unit/scenegraph/unittest_sg_node.cpp index b807a0b..b3e8e41 100644 --- a/unit/scenegraph/unittest_sg_node.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_node.cpp @@ -79,7 +79,7 @@ TEST_F(SGNodeTest, TextNode) sg::TextPropertiesCache cache; auto chunk = sg::TextChunk::create(StyledText::createRaw("hello, world")); auto properties = - sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, 500); + sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, "en-US", 500); auto textLayout = measure->layout(chunk, properties, 100, // Width MeasureMode::AtMost, @@ -276,6 +276,7 @@ class TrivialMediaPlayer : public MediaPlayer { void previous() override {} void rewind() override {} void seek( int offset ) override {} + void seekTo( int offset ) override {} void setTrackIndex( int trackIndex ) override {} void setAudioTrack( AudioTrack audioTrack ) override {} @@ -386,13 +387,12 @@ TEST_F(SGNodeTest, EditTextNode) auto editText = std::make_shared(); auto editTextBox = std::make_shared(); auto properties = - sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, 500); + sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, "klingon", 500); auto editTextConfig = sg::EditTextConfig::create( Color(Color::RED), Color(Color::BLUE), KeyboardType::kKeyboardTypeEmailAddress, - "klingon", 23, false, // Secure input SubmitKeyType::kSubmitKeyTypeGo, @@ -420,7 +420,6 @@ TEST_F(SGNodeTest, EditTextNode) "highlightColor": "#0000ffff", "keyboardType": "emailAddress", "keyboardBehaviorOnFocus": "systemDefault", - "language": "klingon", "maxLength": 23, "secureInput": false, "selectOnFocus": false, @@ -430,6 +429,7 @@ TEST_F(SGNodeTest, EditTextNode) "fontFamily": ["Arial"], "fontSize": 12, "fontStyle": "normal", + "lang": "klingon", "fontWeight": 500, "letterSpacing": 0, "lineHeight": 1.25, @@ -457,7 +457,6 @@ TEST_F(SGNodeTest, EditTextNode) Color(Color::RED), Color(Color::BLUE), KeyboardType::kKeyboardTypeEmailAddress, - "klingon", 23, false, // Secure input SubmitKeyType::kSubmitKeyTypeGo, diff --git a/unit/scenegraph/unittest_sg_nodebounds.cpp b/aplcore/unit/scenegraph/unittest_sg_nodebounds.cpp similarity index 99% rename from unit/scenegraph/unittest_sg_nodebounds.cpp rename to aplcore/unit/scenegraph/unittest_sg_nodebounds.cpp index 02c68cd..7557be8 100644 --- a/unit/scenegraph/unittest_sg_nodebounds.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_nodebounds.cpp @@ -68,6 +68,7 @@ class SGNodeBoundsTestMediaPlayer : public MediaPlayer { void previous() override {} void rewind() override {} void seek(int offset) override {} + void seekTo(int offset) override {} void setTrackIndex(int trackIndex) override {} void setAudioTrack(AudioTrack audioTrack) override {} }; @@ -107,7 +108,7 @@ TEST_F(SGNodeBoundsTest, TextNode) sg::TextPropertiesCache cache; auto chunk = sg::TextChunk::create(StyledText::createRaw("hello, world")); auto properties = - sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, 500); + sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, "en-US", 500); auto textLayout = measure->layout(chunk, properties, 100, // Width MeasureMode::AtMost, @@ -171,13 +172,12 @@ TEST_F(SGNodeBoundsTest, EditNode) auto editTextBox = std::make_shared(); sg::TextPropertiesCache cache; auto properties = - sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, 500); + sg::TextProperties::create(cache, {"Arial"}, 12, FontStyle::kFontStyleNormal, "klingon", 500); auto editTextConfig = sg::EditTextConfig::create( Color(Color::RED), Color(Color::BLUE), KeyboardType::kKeyboardTypeEmailAddress, - "klingon", 23, false, // Secure input SubmitKeyType::kSubmitKeyTypeGo, diff --git a/unit/scenegraph/unittest_sg_pager.cpp b/aplcore/unit/scenegraph/unittest_sg_pager.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_pager.cpp rename to aplcore/unit/scenegraph/unittest_sg_pager.cpp diff --git a/unit/scenegraph/unittest_sg_paint.cpp b/aplcore/unit/scenegraph/unittest_sg_paint.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_paint.cpp rename to aplcore/unit/scenegraph/unittest_sg_paint.cpp diff --git a/unit/scenegraph/unittest_sg_path.cpp b/aplcore/unit/scenegraph/unittest_sg_path.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_path.cpp rename to aplcore/unit/scenegraph/unittest_sg_path.cpp diff --git a/unit/scenegraph/unittest_sg_pathbounds.cpp b/aplcore/unit/scenegraph/unittest_sg_pathbounds.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_pathbounds.cpp rename to aplcore/unit/scenegraph/unittest_sg_pathbounds.cpp diff --git a/unit/scenegraph/unittest_sg_pathop.cpp b/aplcore/unit/scenegraph/unittest_sg_pathop.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_pathop.cpp rename to aplcore/unit/scenegraph/unittest_sg_pathop.cpp diff --git a/unit/scenegraph/unittest_sg_pathparser.cpp b/aplcore/unit/scenegraph/unittest_sg_pathparser.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_pathparser.cpp rename to aplcore/unit/scenegraph/unittest_sg_pathparser.cpp diff --git a/unit/scenegraph/unittest_sg_text.cpp b/aplcore/unit/scenegraph/unittest_sg_text.cpp similarity index 99% rename from unit/scenegraph/unittest_sg_text.cpp rename to aplcore/unit/scenegraph/unittest_sg_text.cpp index 8b53dd8..6c36bec 100644 --- a/unit/scenegraph/unittest_sg_text.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_text.cpp @@ -54,7 +54,7 @@ TEST_F(SGTextTest, SplitString) config->set(RootProperty::kDefaultFontFamily, "sans-serif"); for (const auto& m : SPLIT_TEST_CASES) - ASSERT_EQ(m.expected, sg::splitFontString(*config, m.input)) << m.input; + ASSERT_EQ(m.expected, sg::splitFontString(*config, session, m.input)) << m.input; } @@ -71,7 +71,7 @@ TEST_F(SGTextTest, SplitStringBad) const std::vector expected{"fail"}; for (const auto& m : BAD_TEST_CASES) { - ASSERT_EQ(expected, sg::splitFontString(*config, m)) << m; + ASSERT_EQ(expected, sg::splitFontString(*config, session, m)) << m; ASSERT_TRUE(ConsoleMessage()); } } diff --git a/unit/scenegraph/unittest_sg_text_properties.cpp b/aplcore/unit/scenegraph/unittest_sg_text_properties.cpp similarity index 92% rename from unit/scenegraph/unittest_sg_text_properties.cpp rename to aplcore/unit/scenegraph/unittest_sg_text_properties.cpp index 25b8d2c..574916f 100644 --- a/unit/scenegraph/unittest_sg_text_properties.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_text_properties.cpp @@ -32,6 +32,7 @@ TEST_F(SGTextPropertiesTest, Basic) {"Arial", "Helvetica"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900); rapidjson::Document doc; @@ -42,6 +43,7 @@ TEST_F(SGTextPropertiesTest, Basic) "fontFamily": ["Arial", "Helvetica"], "fontSize": 22, "fontStyle": "normal", + "lang": "en-US", "fontWeight": 900, "letterSpacing": 0, "lineHeight": 1.25, @@ -62,6 +64,7 @@ TEST_F(SGTextPropertiesTest, Duplicate) {"Arial", "Helvetica"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900); ASSERT_EQ(1, cache.size()); @@ -70,6 +73,7 @@ TEST_F(SGTextPropertiesTest, Duplicate) {"Arial", "Helvetica"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900, 0, // Letter spacing 1.25f // Line height @@ -88,6 +92,7 @@ TEST_F(SGTextPropertiesTest, NotDuplicate) {"Arial"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900); ASSERT_EQ(1, cache.size()); @@ -96,6 +101,7 @@ TEST_F(SGTextPropertiesTest, NotDuplicate) {"Arial", "Helvetica"}, 22.0f, FontStyle::kFontStyleNormal, + "en-US", 900, 0, // Letter spacing 1.25f // Line height diff --git a/unit/scenegraph/unittest_sg_touch.cpp b/aplcore/unit/scenegraph/unittest_sg_touch.cpp similarity index 100% rename from unit/scenegraph/unittest_sg_touch.cpp rename to aplcore/unit/scenegraph/unittest_sg_touch.cpp diff --git a/unit/test_comparisons.h b/aplcore/unit/test_comparisons.h similarity index 100% rename from unit/test_comparisons.h rename to aplcore/unit/test_comparisons.h diff --git a/unit/testeventloop.cpp b/aplcore/unit/testeventloop.cpp similarity index 94% rename from unit/testeventloop.cpp rename to aplcore/unit/testeventloop.cpp index ea88744..a7de5a0 100644 --- a/unit/testeventloop.cpp +++ b/aplcore/unit/testeventloop.cpp @@ -15,13 +15,14 @@ #include "testeventloop.h" -#include "apl/livedata/livearrayobject.h" -#include "apl/livedata/livemapobject.h" #include "apl/graphic/graphicelementcontainer.h" #include "apl/graphic/graphicelementgroup.h" #include "apl/graphic/graphicelementpath.h" #include "apl/graphic/graphicelementtext.h" +#include "apl/livedata/livearrayobject.h" +#include "apl/livedata/livemapobject.h" #include "apl/touch/gesture.h" +#include "apl/time/executionresourceholder.h" namespace apl { @@ -64,6 +65,8 @@ getMemoryCounterMap() { {"Context", Counter::itemsDelta}, {"DataSourceConnection", Counter::itemsDelta}, {"Dependant", Counter::itemsDelta}, + {"ExecutionResourceHolder", Counter::itemsDelta}, + {"DocumentContext", Counter::itemsDelta}, {"ExtensionClient", Counter::itemsDelta}, {"Gesture", Counter::itemsDelta}, {"Graphic", Counter::itemsDelta}, @@ -78,7 +81,8 @@ getMemoryCounterMap() { {"Node", Counter::itemsDelta}, #endif // SCENEGRAPH {"Package", Counter::itemsDelta}, - {"RootContextData", Counter::itemsDelta}, + {"SharedContextData", Counter::itemsDelta}, + {"ContextData", Counter::itemsDelta}, {"Sequencer", Counter::itemsDelta}, {"Styles", Counter::itemsDelta}, {"LayoutRebuilder", Counter::itemsDelta}, diff --git a/unit/testeventloop.h b/aplcore/unit/testeventloop.h similarity index 94% rename from unit/testeventloop.h rename to aplcore/unit/testeventloop.h index 6f2678d..2c98814 100644 --- a/unit/testeventloop.h +++ b/aplcore/unit/testeventloop.h @@ -33,6 +33,7 @@ #include "apl/apl.h" #include "apl/command/commandfactory.h" #include "apl/command/corecommand.h" +#include "apl/engine/corerootcontext.h" #include "apl/primitives/range.h" #include "apl/time/coretimemanager.h" #include "apl/utils/make_unique.h" @@ -125,8 +126,16 @@ class MixinCounter { // Check for the existing of message. To simplify testing, we // strip all whitespace. - bool checkAndClear(const std::string& msg) { - auto result = check(msg); + bool checkAndClear(const std::string& messages...) { + //auto result = check(msg); + auto result = false; + for (const std::string& msg : { messages }) { + result = check(msg); + if (!result){ + break; + } + } + if (!result) for (const auto& m : mMessages) fprintf(stderr, "SESSION %s\n", m.c_str()); @@ -310,7 +319,6 @@ class DocumentWrapper : public ActionWrapper { metrics.size(1024,800).dpi(160).theme("dark"); config->set(RootProperty::kAgentName, "Unit tests") .timeManager(loop) - .session(session) .measure(std::make_shared()); } @@ -318,6 +326,7 @@ class DocumentWrapper : public ActionWrapper { createContent(docName, dataName); inflate(); ASSERT_TRUE(root); + rootDocument = root->topDocument(); } void loadDocumentExpectFailure(const char *docName, const char *dataName = nullptr) { @@ -345,6 +354,8 @@ class DocumentWrapper : public ActionWrapper { inflate(); ASSERT_TRUE(root); advanceTime(10); + + rootDocument = root->topDocument(); } template @@ -373,6 +384,8 @@ class DocumentWrapper : public ActionWrapper { inflate(); ASSERT_TRUE(root); advanceTime(10); + + rootDocument = root->topDocument(); } /* @@ -382,6 +395,7 @@ class DocumentWrapper : public ActionWrapper { */ void TearDown() override { component = nullptr; + rootDocument = nullptr; clearDirty(); @@ -409,6 +423,10 @@ class DocumentWrapper : public ActionWrapper { } } + ActionPtr executeCommands(const Object& commands, bool fastMode) { + return rootDocument->executeCommands(commands, fastMode); + } + ActionPtr executeCommand(const std::string& name, const std::map& values, bool fastMode) { rapidjson::Value cmd(rapidjson::kObjectType); auto& alloc = command.GetAllocator(); @@ -416,7 +434,7 @@ class DocumentWrapper : public ActionWrapper { for (auto& m : values) cmd.AddMember(rapidjson::StringRef(m.first.c_str()), m.second.serialize(alloc), alloc); command.SetArray().PushBack(cmd, alloc); - return root->executeCommands(command, fastMode); + return executeCommands(command, fastMode); } /// Given a provenance path, return the JSON that created this @@ -481,7 +499,7 @@ class DocumentWrapper : public ActionWrapper { ASSERT_TRUE(content); ASSERT_TRUE(content->isReady()); - root = RootContext::create(metrics, content, *config, createCallback); + root = std::static_pointer_cast(RootContext::create(metrics, content, *config, createCallback)); if (root) { context = root->contextPtr(); @@ -526,7 +544,8 @@ class DocumentWrapper : public ActionWrapper { public: CoreComponentPtr component; - RootContextPtr root; + CoreRootContextPtr root; + DocumentContextPtr rootDocument; ContextPtr context; Metrics metrics; RootConfigPtr config; @@ -577,8 +596,10 @@ class CommandWrapper : public Command { * parameter for passing additional data. ***********************************************************************/ -class TestEventCommand : public CoreCommand { +class TestEventCommand : public TemplatedCommand { public: + COMMAND_CONSTRUCTOR(TestEventCommand); + static const char *COMMAND; static const char *EVENT; @@ -596,19 +617,6 @@ class TestEventCommand : public CoreCommand { return static_cast(sCommandNameBimap.at(COMMAND)); } - static CommandPtr create(const ContextPtr& context, - Properties&& properties, - const CoreComponentPtr& base, - const std::string& parentSequencer) { - auto ptr = std::make_shared(context, std::move(properties), base, parentSequencer); - return ptr->validate() ? ptr : nullptr; - } - - TestEventCommand(const ContextPtr& context, Properties&& properties, const CoreComponentPtr& base, - const std::string& parentSequencer) - : CoreCommand(context, std::move(properties), base, parentSequencer) - {} - const CommandPropDefSet& propDefSet() const override { static CommandPropDefSet sTestEventCommandProperties(CoreCommand::propDefSet(), { {kCommandPropertyArguments, Object::EMPTY_ARRAY(), asArray}, @@ -673,10 +681,11 @@ class CommandTest : public DocumentWrapper { mOld.emplace(name, CommandFactory::instance().get(name)); CommandFactory::instance().set(name, [&, index](const ContextPtr& context, + CommandData&& commandData, Properties&& props, const CoreComponentPtr& base, const std::string& parentSequencer) { - auto ptr = sCommandCreatorMap.at(index)(context, std::move(props), base, parentSequencer); + auto ptr = sCommandCreatorMap.at(index)(context, std::move(commandData), std::move(props), base, parentSequencer); if (!ptr) return ptr; @@ -896,7 +905,7 @@ ::testing::AssertionResult CheckState(const ComponentPtr& component, Args... arg template ::testing::AssertionResult TransformComponent(const RootContextPtr& root, const std::string& id, Args... args) { - auto component = root->topComponent()->findComponentById(id); + auto component = root->findComponentById(id); if (!component) return ::testing::AssertionFailure() << "Unable to find component " << id; @@ -919,7 +928,7 @@ ::testing::AssertionResult TransformComponent(const RootContextPtr& root, const event->emplace("property", "transform"); event->emplace("value", std::move(transforms)); - root->executeCommands(ObjectArray{event}, true); + root->topDocument()->executeCommands(ObjectArray{event}, true); root->clearPending(); return ::testing::AssertionSuccess(); @@ -931,7 +940,7 @@ ::testing::AssertionResult MatchEvent(const Event& event, EventBag&& bag, const ComponentPtr& component ) { - if (event.matches(Event(eventType, std::move(bag), component))) + if (event == Event(eventType, std::move(bag), component)) return ::testing::AssertionSuccess(); return ::testing::AssertionFailure() << event.getType() << " doesn't match " << eventType; @@ -1035,7 +1044,8 @@ CheckChildrenLaidOutDirtyFlagsWithNotify(const ComponentPtr& component, Range ra template -::testing::AssertionResult CheckDirtyVisualContext(const RootContextPtr& root, Args... args) +::testing::AssertionResult +CheckDirtyVisualContext(const RootContextPtr& root, Args... args) { static const bool value = (sizeof...(Args) != 0); @@ -1050,6 +1060,46 @@ ::testing::AssertionResult CheckDirtyVisualContext(const RootContextPtr& root, A return ::testing::AssertionSuccess(); } +/** + * Check content of kPropertyNotifyChildrenChanged + */ +inline +::testing::AssertionResult +CheckUpdatedChildrenNotification( + const RootContextPtr& root, + const CoreComponentPtr& comp, + const std::vector& change) { + auto dirty = root->getDirty(); + if (!dirty.count(comp)) + return ::testing::AssertionFailure() + << "No dirty property set."; + + if (!comp->getDirty().count(kPropertyNotifyChildrenChanged)) + return ::testing::AssertionFailure() + << "No NotifyChildrenChanged property set."; + + auto notify = comp->getCalculated(kPropertyNotifyChildrenChanged); + const auto& changed = notify.getArray(); + + if (changed.size() != change.size()) + return ::testing::AssertionFailure() + << "Inserted components count is wrong. Expected: " << change.size() + << ", actual: " << changed.size(); + + for (int i = 0; i(change.at(i))).toDebugString() + << ", actual: " << changed.at(i).toDebugString(); + } + } + + root->clearDirty(); + + return ::testing::AssertionSuccess(); +} + template ::testing::AssertionResult CheckSendEvent(const RootContextPtr& root, Args... args) { diff --git a/aplcore/unit/testlogbridge.h b/aplcore/unit/testlogbridge.h new file mode 100644 index 0000000..6354309 --- /dev/null +++ b/aplcore/unit/testlogbridge.h @@ -0,0 +1,41 @@ +/** +* Copyright 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 "gtest/gtest.h" + +#include "apl/utils/log.h" + +using namespace apl; + +class TestLogBridge : public LogBridge { +public: + void transport(LogLevel level, const std::string& log) override + { + mLevel = level; + mLog = log; + mCalls++; + } + + void reset() + { + mLevel = LogLevel::kNone; + mLog = ""; + mCalls = 0; + } + + LogLevel mLevel; + std::string mLog; + int mCalls; +}; \ No newline at end of file diff --git a/unit/time/CMakeLists.txt b/aplcore/unit/time/CMakeLists.txt similarity index 100% rename from unit/time/CMakeLists.txt rename to aplcore/unit/time/CMakeLists.txt diff --git a/unit/time/unittest_sequencer.cpp b/aplcore/unit/time/unittest_sequencer.cpp similarity index 96% rename from unit/time/unittest_sequencer.cpp rename to aplcore/unit/time/unittest_sequencer.cpp index 4561dcb..bc09c26 100644 --- a/unit/time/unittest_sequencer.cpp +++ b/aplcore/unit/time/unittest_sequencer.cpp @@ -23,13 +23,13 @@ class SequencerTest : public CommandTest { public: ActionPtr execute(const std::string& cmds, bool fastMode) { command.Parse(cmds.c_str()); - return root->executeCommands(command, fastMode); + return executeCommands(command, fastMode); } }; static const char *BASIC = R"({ "type": "APL", - "version": "1.4", + "version": "2022.1", "mainTemplate": { "item": { "type": "Container" @@ -1137,3 +1137,41 @@ TEST_F(SequencerTest, SequentialOnSequencer13) ASSERT_TRUE(sequencer.empty("magic")); ASSERT_TRUE(sequencer.empty(MAIN_SEQUENCER_NAME)); } + +static const char *DELAYED_ON_SEQUENCER = R"([ +{ + "type": "Sequential", + "sequencer": "magic", + "delay": 500, + "commands": [ + { + "type": "SendEvent", + "arguments": ["DELAYED","screensaver_open_animation","4"] + } + ] + } +])"; + +TEST_F(SequencerTest, ExecuteCommandsLifecycleMoved) +{ + loadDocument(BASIC); + + rapidjson::Document doc; + doc.Parse(DELAYED_ON_SEQUENCER); + apl::Object obj = apl::Object(std::move(doc)); + auto action = root->topDocument()->executeCommands(obj, false); + + ASSERT_TRUE(action); + ASSERT_FALSE(action->isPending()); + + advanceTime(50); + + // Remove reference to outer action + action = nullptr; + + ASSERT_FALSE(root->hasEvent()); + + advanceTime(450); + + ASSERT_TRUE(CheckSendEvent(root, "DELAYED","screensaver_open_animation","4")); +} diff --git a/unit/touch/CMakeLists.txt b/aplcore/unit/touch/CMakeLists.txt similarity index 100% rename from unit/touch/CMakeLists.txt rename to aplcore/unit/touch/CMakeLists.txt diff --git a/unit/touch/unittest_gestures.cpp b/aplcore/unit/touch/unittest_gestures.cpp similarity index 80% rename from unit/touch/unittest_gestures.cpp rename to aplcore/unit/touch/unittest_gestures.cpp index 497dae7..cdcb547 100644 --- a/unit/touch/unittest_gestures.cpp +++ b/aplcore/unit/touch/unittest_gestures.cpp @@ -688,7 +688,7 @@ TEST_F(GesturesTest, SwipeAwayUnfinished) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -739,7 +739,7 @@ TEST_F(GesturesTest, SwipeAwayUnfinishedMiddle) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -790,7 +790,7 @@ TEST_F(GesturesTest, SwipeAwayCancelled) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -826,7 +826,7 @@ TEST_F(GesturesTest, SwipeAwayWrongDirection) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -844,7 +844,7 @@ TEST_F(GesturesTest, SwipeAwayLeftReveal) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -920,7 +920,7 @@ TEST_F(GesturesTest, SwipeAwayLeftCover) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "cover", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -969,7 +969,7 @@ TEST_F(GesturesTest, SwipeAwayLeftCover) void GesturesTest::swipeAwayLeftSlide() { - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1036,7 +1036,7 @@ TEST_F(GesturesTest, SwipeAwayLeftRightLeftSlide) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1092,7 +1092,7 @@ TEST_F(GesturesTest, SwipeAwayLeftRightLeftSlideUnfinished) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1148,7 +1148,7 @@ TEST_F(GesturesTest, SwipeAwayFlickLeftSlide) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1194,7 +1194,7 @@ TEST_F(GesturesTest, SwipeAwayUnfinishedFlickLeftSlide) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1241,7 +1241,7 @@ TEST_F(GesturesTest, SwipeAwayFlickTooFast) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1269,7 +1269,7 @@ TEST_F(GesturesTest, SwipeAwayLeftSlideNotEnoughDistance) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1286,7 +1286,7 @@ TEST_F(GesturesTest, SwipeAwayLeftSlideNotEnoughDistance) void GesturesTest::swipeAwayRightSlide() { - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1356,7 +1356,7 @@ TEST_F(GesturesTest, SwipeAwayUpSlide) { loadDocument(SWIPE_AWAY, R"({ "direction": "up", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1412,7 +1412,7 @@ TEST_F(GesturesTest, SwipeAwayDownSlide) { loadDocument(SWIPE_AWAY, R"({ "direction": "down", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1468,7 +1468,7 @@ TEST_F(GesturesTest, SwipeAwayOverSwipe) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "cover", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); ASSERT_EQ(Rect(0, 0, 100, 100), tw->getChildAt(0)->getCalculated(kPropertyBounds).get()); @@ -1512,7 +1512,7 @@ TEST_F(GesturesTest, SwipeAwayLeftPointerMovementTooVertical) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1531,7 +1531,7 @@ TEST_F(GesturesTest, SwipeAwayRightPointerMovementTooVertical) { loadDocument(SWIPE_AWAY, R"({ "direction": "right", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1550,7 +1550,7 @@ TEST_F(GesturesTest, SwipeAwayUpPointerMovementTooHorizontal) { loadDocument(SWIPE_AWAY, R"({ "direction": "up", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1569,7 +1569,7 @@ TEST_F(GesturesTest, SwipeAwayDownPointerMovementTooHorizontal) { loadDocument(SWIPE_AWAY, R"({ "direction": "down", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1589,7 +1589,7 @@ TEST_F(GesturesTest, SwipeAwayMaxDurationEnforced) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 400, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1664,7 +1664,7 @@ TEST_F(GesturesTest, SwipeAwayContext) ASSERT_FALSE(child.HasMember("tags")); ////////////////// - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -1740,7 +1740,7 @@ TEST_F(GesturesTest, SwipeAwayLeftDisabled) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "slide", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); tw->setState(StateProperty::kStateDisabled, true); ASSERT_EQ(1, tw->getChildCount()); @@ -1922,7 +1922,7 @@ TEST_F(GesturesTest, GestureCombo) { loadDocument(TOUCH_ALL); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); auto text = tw->getChildAt(0); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ("Lorem ipsum dolor", text->getCalculated(kPropertyText).asString()); @@ -2027,7 +2027,7 @@ TEST_F(GesturesTest, SwipeAwayMiddle) { loadDocument(TOUCH_ALL); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); auto text = tw->getChildAt(0); ASSERT_EQ(kComponentTypeText, text->getType()); ASSERT_EQ("Lorem ipsum dolor", text->getCalculated(kPropertyText).asString()); @@ -2309,7 +2309,7 @@ TEST_F(GesturesTest, DoublePressDisabledAVG) { loadDocument(ALL_AVG); - auto myGraphic = CoreComponent::cast(component->findComponentById("MyGraphic")); + auto myGraphic = CoreComponent::cast(root->findComponentById("MyGraphic")); myGraphic->setState(StateProperty::kStateDisabled, true); ASSERT_EQ(kComponentTypeVectorGraphic, component->getType()); @@ -2368,7 +2368,7 @@ TEST_F(GesturesTest, LongPressDisabledAVG) { loadDocument(ALL_AVG); - auto myGraphic = CoreComponent::cast(component->findComponentById("MyGraphic")); + auto myGraphic = CoreComponent::cast(root->findComponentById("MyGraphic")); myGraphic->setState(StateProperty::kStateDisabled, true); ASSERT_EQ(kComponentTypeVectorGraphic, component->getType()); @@ -2980,7 +2980,7 @@ TEST_F(GesturesTest, LongPressSingularTransformAfterStart) { TEST_F(GesturesTest, SwipeAwayScaled) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -3032,7 +3032,7 @@ TEST_F(GesturesTest, SwipeAwayScaled) TEST_F(GesturesTest, SwipeAwayRotated) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_EQ(1, tw->getChildCount()); ASSERT_EQ("texty", tw->getChildAt(0)->getId()); @@ -3083,7 +3083,7 @@ TEST_F(GesturesTest, SwipeAwayRotated) TEST_F(GesturesTest, SwipeAwayTransformedDuringSwipe) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_TRUE(HandleAndCheckPointerEvent(PointerEventType::kPointerDown, Point(200,100), "onDown")); advanceTime(100); @@ -3126,7 +3126,7 @@ TEST_F(GesturesTest, SwipeAwayTransformedDuringSwipe) { TEST_F(GesturesTest, SwipeAwaySingularTransformDuringSwipe) { loadDocument(SWIPE_AWAY, R"({ "direction": "left", "mode": "reveal", "w": 100, "h": 100 })"); - auto tw = TouchWrapperComponent::cast(component->findComponentById("tw")); + auto tw = TouchWrapperComponent::cast(root->findComponentById("tw")); ASSERT_TRUE(HandleAndCheckPointerEvent(PointerEventType::kPointerDown, Point(200,100), "onDown")); advanceTime(100); @@ -3237,8 +3237,8 @@ static const char *SWIPE_RTL = R"( TEST_F(GesturesTest, SwipeAwayRTL) { loadDocument(SWIPE_RTL); - auto f1 = CoreComponent::cast(component->findComponentById("forwardSwipe")); - auto f2 = CoreComponent::cast(component->findComponentById("backwardSwipe")); + auto f1 = CoreComponent::cast(root->findComponentById("forwardSwipe")); + auto f2 = CoreComponent::cast(root->findComponentById("backwardSwipe")); // Up after fulfilled ASSERT_TRUE(HandleAndCheckPointerEvent(PointerEventType::kPointerDown, Point(20,40))); @@ -3272,8 +3272,8 @@ TEST_F(GesturesTest, SwipeAwayRTL) { TEST_F(GesturesTest, SwipeAwayWrongDirectionRTL) { loadDocument(SWIPE_RTL); - auto f1 = CoreComponent::cast(component->findComponentById("forwardSwipe")); - auto f2 = CoreComponent::cast(component->findComponentById("backwardSwipe")); + auto f1 = CoreComponent::cast(root->findComponentById("forwardSwipe")); + auto f2 = CoreComponent::cast(root->findComponentById("backwardSwipe")); // Up after fulfilled ASSERT_TRUE(HandleAndCheckPointerEvent(PointerEventType::kPointerDown, Point(20,40))); @@ -3470,3 +3470,656 @@ TEST_F(GesturesTest, SwipeAndStay) ASSERT_TRUE(IsEqual(Color(Color::RED), redFrame->getCalculated(apl::kPropertyBackgroundColor))); ASSERT_TRUE(IsEqual(Rect{10,10,480,80}, redFrame->getCalculated(kPropertyBounds))); } + +static const char * SWIPE_CHILD_THEN_SWIPE_PARENT = R"apl({ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": [{ + "type": "TouchWrapper", + "width": 800, + "height": 200, + "gestures": [{ + "type": "SwipeAway", + "direction": "right", + "items": { + "type": "Frame", + "backgroundColor": "red" + } + }], + "items": [{ + "type": "Frame", + "backgroundColor": "blue", + "items": [{ + "type": "Container", + "items": [{ + "type": "TouchWrapper", + "gestures": [{ + "type": "SwipeAway", + "direction": "right", + "items": { + "type": "Container" + } + }], + "items": [{ + "type": "Frame", + "width": 400, + "height": 200, + "backgroundColor": "yellow" + }] + }] + }] + }] + }] + } +})apl"; + +TEST_F(GesturesTest, SwipeChildThenSwipParent) +{ + loadDocument(SWIPE_CHILD_THEN_SWIPE_PARENT); + ASSERT_TRUE(component); + + auto yellowFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::YELLOW), yellowFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,400,200}, yellowFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(10,20)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(10,20)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(300,20)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(300,20)))); + advanceTime(1000); + + // Child's yellow frame is gone + auto yellowFrameGone = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_FALSE(IsEqual(Color(Color::YELLOW), yellowFrameGone->getCalculated(apl::kPropertyBackgroundColor))); + + auto blueFrame = component->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), blueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,200}, blueFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe at the same place + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(10,20)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(10,20)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(300,20)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(300,20)))); + advanceTime(1000); + + // Parent's blue frame is gone + auto redFrame = component->getChildAt(0); + ASSERT_NE(redFrame, blueFrame); + ASSERT_TRUE(IsEqual(Color(Color::RED), redFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,200}, redFrame->getCalculated(kPropertyBounds))); +} + +static const char * PARENT_CHILD_BOTH_HAVE_DOUBLEPRESS = R"apl({ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": [{ + "type": "Container", + "width": 800, + "height": 400, + "items": [{ + "type": "TouchWrapper", + "gestures": [{ + "type": "DoublePress", + "onDoublePress": [{ + "type": "SetValue", + "componentId": "parentText", + "property": "text", + "value": "Parent detect DoublePress." + }] + }], + "items": [{ + "type": "Frame", + "items": [{ + "type": "Container", + "wrap": "noWrap", + "items": [{ + "type": "Text", + "id": "parentText", + "height": 50, + "text": "Lorem ipsum dolor" + }, { + "type": "Text", + "id": "childText", + "height": 50, + "text": "Lorem ipsum dolor" + }, { + "type": "Container", + "alignItems": "start", + "direction": "row", + "items": [{ + "type": "TouchWrapper", + "gestures": [{ + "type": "DoublePress", + "onDoublePress": [{ + "type": "SetValue", + "componentId": "childText", + "property": "text", + "value": "Kid detect DoublePress." + }] + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 400 + }] + }] + }] + }] + }] + }] + }] + } +})apl"; + +TEST_F(GesturesTest, OnlyChildGetDoublePress) +{ + loadDocument(PARENT_CHILD_BOTH_HAVE_DOUBLEPRESS); + ASSERT_TRUE(component); + + auto parentText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, parentText->getType()); + ASSERT_EQ("Lorem ipsum dolor", parentText->getCalculated(kPropertyText).asString()); + + auto childText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(1); + ASSERT_EQ(kComponentTypeText, childText->getType()); + ASSERT_EQ("Lorem ipsum dolor", childText->getCalculated(kPropertyText).asString()); + + // Trigger a DoublePress + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(100,300)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(100,300)))); + advanceTime(400); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(100,300)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(100,300)))); + + // Verify only the kid get the DoublePress + parentText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, parentText->getType()); + ASSERT_EQ("Lorem ipsum dolor", parentText->getCalculated(kPropertyText).asString()); + + childText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(1); + ASSERT_EQ(kComponentTypeText, childText->getType()); + ASSERT_EQ("Kid detect DoublePress.", childText->getCalculated(kPropertyText).asString()); + +} + +static const char * PARENT_CHILD_BOTH_HAVE_ONDOWN = R"apl({ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": [{ + "type": "Container", + "width": 800, + "height": 400, + "items": [{ + "type": "TouchWrapper", + "onDown": [{ + "type": "SetValue", + "componentId": "parentText", + "property": "text", + "value": "Parent detect onDown." + }], + "items": [{ + "type": "Frame", + "backgroundColor": "blue", + "items": [{ + "type": "Container", + "wrap": "noWrap", + "items": [{ + "type": "Text", + "id": "parentText", + "height": 50, + "text": "Lorem ipsum dolor" + }, { + "type": "Text", + "id": "childText", + "height": 50, + "text": "Lorem ipsum dolor" + }, { + "type": "Container", + "alignItems": "start", + "direction": "row", + "items": [{ + "type": "TouchWrapper", + "onDown": [{ + "type": "SetValue", + "componentId": "childText", + "property": "text", + "value": "Kid detect onDown." + }], + "items": [{ + "type": "Frame", + "width": 400, + "height": 400, + "backgroundColor": "yellow" + }] + }, { + "type": "Container", + "items": [{ + "backgroundColor": "orange", + "type": "Frame", + "width": 400, + "height": 400 + }] + }] + }] + }] + }] + }] + }] + } +})apl"; + +TEST_F(GesturesTest, OnlyChildGetOnDown) +{ + loadDocument(PARENT_CHILD_BOTH_HAVE_ONDOWN); + ASSERT_TRUE(component); + + auto parentText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, parentText->getType()); + ASSERT_EQ("Lorem ipsum dolor", parentText->getCalculated(kPropertyText).asString()); + + auto childText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(1); + ASSERT_EQ(kComponentTypeText, childText->getType()); + ASSERT_EQ("Lorem ipsum dolor", childText->getCalculated(kPropertyText).asString()); + + // Trigger an onDown on overlapping area + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(200,300)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(200,300)))); + advanceTime(10); + + // Only kid received the onDown + parentText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, parentText->getType()); + ASSERT_EQ("Lorem ipsum dolor", parentText->getCalculated(kPropertyText).asString()); + + childText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(1); + ASSERT_EQ(kComponentTypeText, childText->getType()); + ASSERT_EQ("Kid detect onDown.", childText->getCalculated(kPropertyText).asString()); + + // Trigger an onDown on parent where child doesn't have any overlap. + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(600,300)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(600,300)))); + advanceTime(10); + + // Parent received the onDown + parentText = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, parentText->getType()); + ASSERT_EQ("Parent detect onDown.", parentText->getCalculated(kPropertyText).asString()); +} + +static const char * DOUBLEPRESS_CHILD_THEN_SWIPE_PARENT = R"apl({ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": [{ + "type": "TouchWrapper", + "width": 800, + "height": 200, + "gestures": [{ + "type": "SwipeAway", + "direction": "right", + "items": { + "type": "Frame", + "backgroundColor": "red" + } + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 200, + "backgroundColor": "blue", + "items": [{ + "type": "Container", + "items": [{ + "type": "Text", + "height": 50, + "id": "maintext", + "text": "Lorem ipsum dolor" + }, { + "type": "TouchWrapper", + "gestures": [{ + "type": "DoublePress", + "onDoublePress": [{ + "type": "SetValue", + "componentId": "maintext", + "property": "text", + "value": "Detect DoublePress." + }, { + "type": "SendEvent", + "arguments": ["onDoublePress", "${event.component.x}"] + }] + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 150 + }] + }] + }] + }] + }] + } +})apl"; + +TEST_F(GesturesTest, DoublePressChildThenSwipParent) +{ + loadDocument(DOUBLEPRESS_CHILD_THEN_SWIPE_PARENT); + ASSERT_TRUE(component); + + auto text = component->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, text->getType()); + ASSERT_EQ("Lorem ipsum dolor", text->getCalculated(kPropertyText).asString()); + + // Trigger a DoublePress + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(15,100)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(15,100)))); + advanceTime(400); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(15,100)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(15,100)))); + + // Verify the DoublePress + ASSERT_TRUE(CheckEvent("onDoublePress", 15)); + ASSERT_EQ("Detect DoublePress.", text->getCalculated(kPropertyText).asString()); + + auto blueFrame = component->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), blueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,200}, blueFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(10,100)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(10,100)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(300,100)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(300,100)))); + advanceTime(1000); + + // Parent's blue frame is gone + auto redFrame = component->getChildAt(0); + ASSERT_NE(redFrame, blueFrame); + ASSERT_TRUE(IsEqual(Color(Color::RED), redFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,200}, redFrame->getCalculated(kPropertyBounds))); +} + +static const char * ONDOWN_CHILD_THEN_SWIPE_PARENT = R"apl({ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": [{ + "type": "TouchWrapper", + "width": 800, + "height": 200, + "gestures": [{ + "type": "SwipeAway", + "direction": "right", + "items": { + "type": "Frame", + "backgroundColor": "red" + } + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 200, + "backgroundColor": "blue", + "items": [{ + "type": "Container", + "items": [{ + "type": "Text", + "height": 50, + "id": "maintext", + "text": "Lorem ipsum dolor" + }, { + "type": "TouchWrapper", + "onDown": [{ + "type": "SetValue", + "componentId": "maintext", + "property": "text", + "value": "Kid detect onDown." + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 150 + }] + }] + }] + }] + }] + } +})apl"; + +TEST_F(GesturesTest, OnDownChildThenSwipeParent) +{ + loadDocument(ONDOWN_CHILD_THEN_SWIPE_PARENT); + ASSERT_TRUE(component); + + auto text = component->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(kComponentTypeText, text->getType()); + ASSERT_EQ("Lorem ipsum dolor", text->getCalculated(kPropertyText).asString()); + + // Trigger a onDown + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(15,100)))); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(15,100)))); + advanceTime(400); + + // Verify the kid has got the onDown + ASSERT_EQ("Kid detect onDown.", text->getCalculated(kPropertyText).asString()); + + auto blueFrame = component->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), blueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,200}, blueFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(10,100)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(10,100)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(300,100)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(300,100)))); + advanceTime(1000); + + // Parent's blue frame is gone + auto redFrame = component->getChildAt(0); + ASSERT_NE(redFrame, blueFrame); + ASSERT_TRUE(IsEqual(Color(Color::RED), redFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,200}, redFrame->getCalculated(kPropertyBounds))); +} + +static const char * THREE_LAYERS_SWIPE_WITH_DIFFERENT_DIRECTIONS = R"apl({ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "items": [{ + "type": "TouchWrapper", + "width": 800, + "height": 400, + "gestures": [{ + "type": "SwipeAway", + "direction": "left", + "items": { + "type": "Frame", + "backgroundColor": "red" + } + }], + "items": [{ + "type": "Frame", + "backgroundColor": "blue", + "items": [{ + "type": "Container", + "items": [{ + "type": "TouchWrapper", + "gestures": [{ + "type": "SwipeAway", + "direction": "right", + "items": { + "type": "Container" + } + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 400, + "backgroundColor": "yellow", + "items": [{ + "type": "Container", + "items": [{ + "type": "TouchWrapper", + "gestures": [{ + "type": "SwipeAway", + "direction": "up", + "items": { + "type": "Container" + } + }], + "items": [{ + "type": "Frame", + "width": 800, + "height": 400, + "backgroundColor": "green" + }] + }] + }] + }] + }] + }] + }] + }] + } +})apl"; + +TEST_F(GesturesTest, TopLayerGetSwipeLeft) +{ + loadDocument(THREE_LAYERS_SWIPE_WITH_DIFFERENT_DIRECTIONS); + ASSERT_TRUE(component); + + auto greenFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::GREEN), greenFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, greenFrame->getCalculated(kPropertyBounds))); + + auto yellowFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::YELLOW), yellowFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, yellowFrame->getCalculated(kPropertyBounds))); + + auto blueFrame = component->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), blueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, blueFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe left + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(300,20)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(300,20)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(10,20)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(10,20)))); + advanceTime(1000); + + // blue frame's child are gone + auto blueFrameChildCount = component->getChildAt(0)->getChildCount(); + ASSERT_TRUE(IsEqual(0, blueFrameChildCount)); + + // blue frame is gone + auto redFrame = component->getChildAt(0); + ASSERT_NE(redFrame, blueFrame); + ASSERT_TRUE(IsEqual(Color(Color::RED), redFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, redFrame->getCalculated(kPropertyBounds))); +} + +TEST_F(GesturesTest, MidLayerGetSwipeRight) +{ + loadDocument(THREE_LAYERS_SWIPE_WITH_DIFFERENT_DIRECTIONS); + ASSERT_TRUE(component); + + auto greenFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::GREEN), greenFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, greenFrame->getCalculated(kPropertyBounds))); + + auto yellowFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::YELLOW), yellowFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, yellowFrame->getCalculated(kPropertyBounds))); + + auto blueFrame = component->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), blueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, blueFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe right + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(10,20)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(10,20)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(300,20)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(300,20)))); + advanceTime(1000); + + // yellow frame's child is gone + auto yellowFrameChildCount = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildCount(); + ASSERT_TRUE(IsEqual(0, yellowFrameChildCount)); + + // yellow frame is gone + auto yellowFrameGone = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_FALSE(IsEqual(Color(Color::YELLOW), yellowFrameGone->getCalculated(apl::kPropertyBackgroundColor))); + + // blue frame is still here + auto stillBlueFrame = component->getChildAt(0); + ASSERT_EQ(stillBlueFrame, blueFrame); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), stillBlueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, stillBlueFrame->getCalculated(kPropertyBounds))); +} + +TEST_F(GesturesTest, BottonLayerGetSwipeUp) +{ + loadDocument(THREE_LAYERS_SWIPE_WITH_DIFFERENT_DIRECTIONS); + ASSERT_TRUE(component); + + auto greenFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::GREEN), greenFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, greenFrame->getCalculated(kPropertyBounds))); + + auto yellowFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::YELLOW), yellowFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, yellowFrame->getCalculated(kPropertyBounds))); + + auto blueFrame = component->getChildAt(0); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), blueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, blueFrame->getCalculated(kPropertyBounds))); + + // Execute a swipe up + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(50,300)))); + advanceTime(10); + ASSERT_FALSE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(50,300)))); + advanceTime(250); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(50,50)))); + advanceTime(10); + ASSERT_TRUE(root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(50,50)))); + advanceTime(1000); + + // green Frame is gone + auto greenFrameGone = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_FALSE(IsEqual(Color(Color::GREEN), greenFrameGone->getCalculated(apl::kPropertyBackgroundColor))); + + // yellow Frame is still here + auto stillYellowFrame = component->getChildAt(0)->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ(stillYellowFrame, yellowFrame); + ASSERT_TRUE(IsEqual(Color(Color::YELLOW), stillYellowFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, stillYellowFrame->getCalculated(kPropertyBounds))); + + // blue frame is still here + auto stillBlueFrame = component->getChildAt(0); + ASSERT_EQ(stillBlueFrame, blueFrame); + ASSERT_TRUE(IsEqual(Color(Color::BLUE), stillBlueFrame->getCalculated(apl::kPropertyBackgroundColor))); + ASSERT_TRUE(IsEqual(Rect{0,0,800,400}, stillBlueFrame->getCalculated(kPropertyBounds))); +} \ No newline at end of file diff --git a/unit/touch/unittest_native_gestures_pager.cpp b/aplcore/unit/touch/unittest_native_gestures_pager.cpp similarity index 94% rename from unit/touch/unittest_native_gestures_pager.cpp rename to aplcore/unit/touch/unittest_native_gestures_pager.cpp index d37f7a3..7a064ba 100644 --- a/unit/touch/unittest_native_gestures_pager.cpp +++ b/aplcore/unit/touch/unittest_native_gestures_pager.cpp @@ -3419,3 +3419,186 @@ TEST_F(NativeGesturesPagerTest, PageFlingLeftForwardDirectionCustom) ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); } +static const char *LIVE_DATA_PAGER = R"({ + "type": "APL", + "version": "1.6", + "mainTemplate": { + "items": { + "type": "Pager", + "id": "pagers", + "width": 500, + "height": 500, + "data": "${colors}", + "onPageChanged": [ + { + "type": "SendEvent", + "sequencer": "SET_PAGE" + } + ], + "items": [ + { + "type": "Frame", + "id": "${data}${index}", + "backgroundColor": "${data}", + "width": "100%", + "height": "100%", + "item": { + "id": "touchWrapper${index}", + "type": "TouchWrapper", + "item": { + "type": "Text", + "text": "Focusable Component ${index}" + } + } + } + ] + } + } +})"; + +TEST_F(NativeGesturesPagerTest, LiveDataPagerFullReplacement) +{ + auto liveArray = LiveArray::create({"red", "green", "yellow", "blue", "purple", "gray", "red", "green", "yellow", "blue", "purple", "gray"}); + config->liveData("colors", liveArray); + loadDocument(LIVE_DATA_PAGER); + + // We are positioned at origin and first of 12 pages is shown + ASSERT_EQ(Point(), component->scrollPosition()); + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); + ASSERT_EQ(12, component->getChildCount()); + + // No transform + ASSERT_EQ(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + + // Start paging a bit + auto ptr = executeCommand("AutoPage", {{"componentId", "pagers"}, {"count", 1}, {"duration", 100}}, false); + advanceTime(200); + + // We are partway between the first and second page + ASSERT_FALSE(ptr->isResolved()); + ASSERT_EQ(2, component->getDisplayedChildCount()); + ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); + ASSERT_EQ("green1", component->getDisplayedChildAt(1)->getId()); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(1)->getCalculated(kPropertyTransform)); + + // We simulate the VH holding strong references to these pages (although in practice there are other strong references currently) + auto red = component->getDisplayedChildAt(0); + auto green = component->getDisplayedChildAt(1); + + // Fully replace the live data powering the pager + liveArray->remove(0, liveArray->size()); + liveArray->insert(0, "black"); + liveArray->insert(0, "white"); + ASSERT_EQ(2, liveArray->size()); + + // A little more time passes, which is not enough to complete the page transition + advanceTime(200); + + // There is no event and for a moment, no children are displayed + // TODO: Should the page transition be fast-forwarded to the next existing page? + ASSERT_FALSE(ptr->isResolved()); + ASSERT_FALSE(root->hasEvent()); + ASSERT_EQ(0, component->getDisplayedChildCount()); + + // Eventually, the pagination completes + advanceTime(1000); + + // Now we have an event + ASSERT_TRUE(ptr->isResolved()); + ASSERT_TRUE(root->hasEvent()); + root->popEvent(); + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_EQ("black1", component->getDisplayedChildAt(0)->getId()); + ASSERT_EQ(2, component->getChildCount()); + ASSERT_EQ(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); +} + +TEST_F(NativeGesturesPagerTest, LiveDataTargetOrCurrentRemove) +{ + auto liveArray = LiveArray::create({"red", "green", "yellow", "blue", "purple", "gray", "red", "green", "yellow", "blue", "purple", "gray"}); + config->liveData("colors", liveArray); + loadDocument(LIVE_DATA_PAGER); + + ASSERT_EQ(12, component->getChildCount()); + + // Start paging a bit + auto ptr = executeCommand("AutoPage", {{"componentId", "pagers"}, {"count", 1}, {"duration", 100}}, false); + advanceTime(200); + + // We are partway between the first and second page + ASSERT_FALSE(ptr->isResolved()); + ASSERT_EQ(2, component->getDisplayedChildCount()); + ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); + ASSERT_EQ("green1", component->getDisplayedChildAt(1)->getId()); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(1)->getCalculated(kPropertyTransform)); + + // Remove the target page (green) + liveArray->remove(1); + ASSERT_EQ(11, liveArray->size()); + + // A little more time passes, which is not enough to complete the page transition + advanceTime(200); + + // There is no event and the current page is still visible + ASSERT_FALSE(ptr->isResolved()); + ASSERT_FALSE(root->hasEvent()); + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + + // Eventually, the transition completes + advanceTime(1000); + + // Now we have an event + ASSERT_TRUE(ptr->isResolved()); + ASSERT_TRUE(root->hasEvent()); + root->popEvent(); + ASSERT_EQ(1, component->getDisplayedChildCount()); + ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); + ASSERT_EQ(11, component->getChildCount()); + ASSERT_EQ(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + + // Try again + ptr = executeCommand("AutoPage", {{"componentId", "pagers"}, {"count", 1}, {"duration", 100}}, false); + advanceTime(200); + + // We are partway between the first and second page + ASSERT_FALSE(ptr->isResolved()); + ASSERT_EQ(2, component->getDisplayedChildCount()); + ASSERT_EQ("red0", component->getDisplayedChildAt(0)->getId()); + // TODO: This should be "yellow1" because the second item is gone + ASSERT_EQ("yellow2", component->getDisplayedChildAt(1)->getId()); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(1)->getCalculated(kPropertyTransform)); + + // This time, remove the current page + liveArray->remove(0); + ASSERT_EQ(10, liveArray->size()); + + // A little more time passes, which is not enough to complete the page transition + advanceTime(200); + + // There is no event and the target page is still visible + ASSERT_FALSE(ptr->isResolved()); + ASSERT_FALSE(root->hasEvent()); + ASSERT_EQ(1, component->getDisplayedChildCount()); + // TODO: Again this should be "yellow1" + ASSERT_EQ("yellow2", component->getDisplayedChildAt(0)->getId()); + ASSERT_NE(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); + + // Eventually, the transition completes + advanceTime(1000); + + // Now we have an event + ASSERT_TRUE(ptr->isResolved()); + ASSERT_TRUE(root->hasEvent()); + root->popEvent(); + ASSERT_EQ(1, component->getDisplayedChildCount()); + // TODO: Again this should be "yellow1" + ASSERT_EQ("yellow2", component->getDisplayedChildAt(0)->getId()); + ASSERT_EQ(10, component->getChildCount()); + ASSERT_EQ(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); +} \ No newline at end of file diff --git a/unit/touch/unittest_native_gestures_scrollable.cpp b/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp similarity index 100% rename from unit/touch/unittest_native_gestures_scrollable.cpp rename to aplcore/unit/touch/unittest_native_gestures_scrollable.cpp diff --git a/unit/touch/unittest_pointer.cpp b/aplcore/unit/touch/unittest_pointer.cpp similarity index 99% rename from unit/touch/unittest_pointer.cpp rename to aplcore/unit/touch/unittest_pointer.cpp index 6b6ba63..e7ee815 100644 --- a/unit/touch/unittest_pointer.cpp +++ b/aplcore/unit/touch/unittest_pointer.cpp @@ -209,7 +209,7 @@ TEST_F(PointerTest, Overlapping) { // Now shift the TopFrame out of the way ASSERT_TRUE(TransformComponent(root, "HidingContainer", "translateX", 200)); - auto hiding = root->topComponent()->findComponentById("HidingContainer"); + auto hiding = root->findComponentById("HidingContainer"); auto transform = hiding->getCalculated(kPropertyTransform).get(); ASSERT_EQ(Point(200,0), transform * Point(0,0)); @@ -255,7 +255,7 @@ static const char *SCROLLING_CONTAINER = TEST_F(PointerTest, ScrollView) { loadDocument(SCROLLING_CONTAINER); - auto touch = CoreComponent::cast(component->findComponentById("MyTouch")); + auto touch = CoreComponent::cast(root->findComponentById("MyTouch")); // Verify you can hit the target at the starting location ASSERT_TRUE(MouseClick(root, touch, 25, 25)); diff --git a/unit/touch/unittest_velocity_tracking.cpp b/aplcore/unit/touch/unittest_velocity_tracking.cpp similarity index 100% rename from unit/touch/unittest_velocity_tracking.cpp rename to aplcore/unit/touch/unittest_velocity_tracking.cpp diff --git a/unit/unittest_simpletextmeasurement.cpp b/aplcore/unit/unittest_simpletextmeasurement.cpp similarity index 70% rename from unit/unittest_simpletextmeasurement.cpp rename to aplcore/unit/unittest_simpletextmeasurement.cpp index e9607e4..7f3a80d 100644 --- a/unit/unittest_simpletextmeasurement.cpp +++ b/aplcore/unit/unittest_simpletextmeasurement.cpp @@ -14,6 +14,7 @@ */ #include "testeventloop.h" +#include "faketextcomponent.h" using namespace apl; @@ -27,42 +28,6 @@ using namespace apl; * only serves to return an Object containing the text. */ - -class FakeComponent : public Component { -public: - FakeComponent(const ContextPtr& context, const std::string& id, const std::string& text) - : Component(context, id) - { - mCalculated.set(kPropertyText, text); - } - - void release() override {} - size_t getChildCount() const override {return 0;} - ComponentPtr getChildAt(size_t index) const override { return nullptr; } - bool appendChild(const ComponentPtr& child) override { return false; } - bool insertChild(const ComponentPtr& child, size_t index) override { return false; } - bool remove() override { return false; } - bool canInsertChild() const override { return false; } - bool canRemoveChild() const override { return false; } - ComponentType getType() const override { return kComponentTypeText; } - ComponentPtr getParent() const override { return nullptr; } - void update(UpdateType type, float value) override {} - void update(UpdateType type, const std::string& value) override {} - void ensureLayout(bool useDirtyFlag) override {} - size_t getDisplayedChildCount() const override{return 0;} - Point localToGlobal(Point) const override { return {0,0}; } - ComponentPtr getDisplayedChildAt(size_t drawIndex) const override { return nullptr; } - std::string getHierarchySignature() const override { return std::string(); } - rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override { return rapidjson::Value(); } - rapidjson::Value serializeAll(rapidjson::Document::AllocatorType& allocator) const override { return rapidjson::Value(); } - rapidjson::Value serializeDirty(rapidjson::Document::AllocatorType& allocator) override { return rapidjson::Value(); } - std::string provenance() const override { return std::string(); } - rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) override { return rapidjson::Value(); } - ComponentPtr findComponentById(const std::string& id) const override { return nullptr; } - ComponentPtr findComponentAtPosition(const Point& position) const override { return nullptr; } -}; - - class SimpleText : public ::testing::Test { public: SimpleText() @@ -99,13 +64,13 @@ class SimpleText : public ::testing::Test { TEST_F(SimpleText, Basic) { // Empty text string should return size 0,0 whenever possible - auto a = std::make_shared(context, "ID", ""); + auto a = std::make_shared(context, "ID", ""); ASSERT_TRUE(CheckSize(a, 100, MeasureMode::Exactly, 100, MeasureMode::Exactly, 100, 100)); ASSERT_TRUE(CheckSize(a, -1, MeasureMode::Undefined, -1, MeasureMode::Undefined, 0, 0)); ASSERT_TRUE(CheckSize(a, 100, MeasureMode::AtMost, 100, MeasureMode::AtMost, 0, 0)); // Assign a larger block of text. - a = std::make_shared(context, "ID", "123456789A"); + a = std::make_shared(context, "ID", "123456789A"); // When the width is fixed, the other modes depend on how much wrapper occurs ASSERT_TRUE(CheckSize(a, 37, MeasureMode::Exactly, 23, MeasureMode::Exactly, 37, 23)); diff --git a/unit/unittest_testeventloop.cpp b/aplcore/unit/unittest_testeventloop.cpp similarity index 100% rename from unit/unittest_testeventloop.cpp rename to aplcore/unit/unittest_testeventloop.cpp diff --git a/unit/utils/CMakeLists.txt b/aplcore/unit/utils/CMakeLists.txt similarity index 93% rename from unit/utils/CMakeLists.txt rename to aplcore/unit/utils/CMakeLists.txt index 63bb216..68ad2c1 100644 --- a/unit/utils/CMakeLists.txt +++ b/aplcore/unit/utils/CMakeLists.txt @@ -19,6 +19,8 @@ target_sources_local(unittest unittest_lrucache.cpp unittest_path.cpp unittest_ringbuffer.cpp + unittest_scopeddequeue.cpp + unittest_scopedset.cpp unittest_session.cpp unittest_stringfunctions.cpp unittest_url.cpp diff --git a/unit/utils/unittest_encoding.cpp b/aplcore/unit/utils/unittest_encoding.cpp similarity index 100% rename from unit/utils/unittest_encoding.cpp rename to aplcore/unit/utils/unittest_encoding.cpp diff --git a/unit/utils/unittest_hash.cpp b/aplcore/unit/utils/unittest_hash.cpp similarity index 100% rename from unit/utils/unittest_hash.cpp rename to aplcore/unit/utils/unittest_hash.cpp diff --git a/unit/utils/unittest_log.cpp b/aplcore/unit/utils/unittest_log.cpp similarity index 85% rename from unit/utils/unittest_log.cpp rename to aplcore/unit/utils/unittest_log.cpp index d3dd01a..9f37910 100644 --- a/unit/utils/unittest_log.cpp +++ b/aplcore/unit/utils/unittest_log.cpp @@ -15,34 +15,13 @@ #include "gtest/gtest.h" -#include "apl/utils/log.h" +#include "../testlogbridge.h" #include "apl/utils/session.h" using namespace apl; class LogTest : public ::testing::Test { protected: - class TestLogBridge : public LogBridge { - public: - void transport(LogLevel level, const std::string& log) override - { - mLevel = level; - mLog = log; - mCalls++; - } - - void reset() - { - mLevel = LogLevel::kNone; - mLog = ""; - mCalls = 0; - } - - LogLevel mLevel; - std::string mLog; - int mCalls; - }; - void reset() { mLogBridge->reset(); } LogLevel level() const { return mLogBridge->mLevel; } std::string log() const { return mLogBridge->mLog; } diff --git a/unit/utils/unittest_lrucache.cpp b/aplcore/unit/utils/unittest_lrucache.cpp similarity index 100% rename from unit/utils/unittest_lrucache.cpp rename to aplcore/unit/utils/unittest_lrucache.cpp diff --git a/unit/utils/unittest_path.cpp b/aplcore/unit/utils/unittest_path.cpp similarity index 100% rename from unit/utils/unittest_path.cpp rename to aplcore/unit/utils/unittest_path.cpp diff --git a/unit/utils/unittest_ringbuffer.cpp b/aplcore/unit/utils/unittest_ringbuffer.cpp similarity index 100% rename from unit/utils/unittest_ringbuffer.cpp rename to aplcore/unit/utils/unittest_ringbuffer.cpp diff --git a/aplcore/unit/utils/unittest_scopeddequeue.cpp b/aplcore/unit/utils/unittest_scopeddequeue.cpp new file mode 100644 index 0000000..4049447 --- /dev/null +++ b/aplcore/unit/utils/unittest_scopeddequeue.cpp @@ -0,0 +1,150 @@ +/* + * Copyright 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 "gtest/gtest.h" + +#include + +#include "apl/utils/scopeddequeue.h" + +using namespace apl; + +class ScopedDequeueTest : public ::testing::Test {}; + +TEST_F(ScopedDequeueTest, Basic) +{ + auto scopedDequeue = std::make_shared>(); + scopedDequeue->emplace(1, 1); + scopedDequeue->emplace(2, 3); + scopedDequeue->emplace(1, 2); + scopedDequeue->emplace(2, 4); + scopedDequeue->emplace(1, 2); + + ASSERT_FALSE(scopedDequeue->empty()); + ASSERT_EQ(5, scopedDequeue->size()); + + auto comp = std::deque>(); + comp.emplace_back(1, 1); + comp.emplace_back(2, 3); + comp.emplace_back(1, 2); + comp.emplace_back(2, 4); + comp.emplace_back(1, 2); + + ASSERT_EQ(comp, scopedDequeue->getAll()); + + + ASSERT_EQ(std::vector({1,2,2}), scopedDequeue->getScoped(1)); + ASSERT_EQ(std::vector({3,4}), scopedDequeue->getScoped(2)); +} + +TEST_F(ScopedDequeueTest, Clear) +{ + auto scopedDequeue = std::make_shared>(); + scopedDequeue->emplace(1, 1); + scopedDequeue->emplace(2, 3); + scopedDequeue->emplace(1, 2); + scopedDequeue->emplace(2, 4); + scopedDequeue->emplace(1, 2); + + ASSERT_FALSE(scopedDequeue->empty()); + ASSERT_EQ(5, scopedDequeue->size()); + + ASSERT_EQ(2, scopedDequeue->eraseScope(2)); + ASSERT_EQ(3, scopedDequeue->size()); + + scopedDequeue->clear(); + + ASSERT_TRUE(scopedDequeue->empty()); +} + +TEST_F(ScopedDequeueTest, ExtractScope) +{ + auto scopedDequeue = std::make_shared>(); + scopedDequeue->emplace(1, 1); + scopedDequeue->emplace(2, 3); + scopedDequeue->emplace(1, 2); + scopedDequeue->emplace(2, 4); + scopedDequeue->emplace(1, 2); + + ASSERT_FALSE(scopedDequeue->empty()); + ASSERT_EQ(5, scopedDequeue->size()); + + ASSERT_EQ(std::vector({1, 2, 2}), scopedDequeue->extractScope(1)); + + auto comp = std::deque>(); + comp.emplace_back(2, 3); + comp.emplace_back(2, 4); + + std::remove_if(comp.begin(), comp.end(), [](const std::pair& item) { + return item.first == 1; + }); + + ASSERT_EQ(comp, scopedDequeue->getAll()); + + ASSERT_EQ(2, scopedDequeue->size()); + ASSERT_EQ(3, scopedDequeue->pop()); + ASSERT_EQ(4, scopedDequeue->pop()); + + ASSERT_TRUE(scopedDequeue->empty()); +} + +TEST_F(ScopedDequeueTest, TestPushFrontPopEmpty) +{ + auto scopedDequeue = std::make_shared>(); + ASSERT_TRUE(scopedDequeue->empty()); + scopedDequeue->emplace(0, 1); + ASSERT_FALSE(scopedDequeue->empty()); + ASSERT_EQ(1, scopedDequeue->front()); + ASSERT_FALSE(scopedDequeue->empty()); + scopedDequeue->pop(); + ASSERT_TRUE(scopedDequeue->empty()); +} + +TEST_F(ScopedDequeueTest, TestPushFrontPopEmptyConst) +{ + auto scopedDequeue = std::make_shared>(); + ASSERT_TRUE(scopedDequeue->empty()); + scopedDequeue->emplace(0, 1); + ASSERT_FALSE(scopedDequeue->empty()); + ASSERT_EQ(1, scopedDequeue->front()); + ASSERT_FALSE(scopedDequeue->empty()); + scopedDequeue->pop(); + ASSERT_TRUE(scopedDequeue->empty()); +} + +TEST_F(ScopedDequeueTest, TestPushClearEmpty) +{ + auto scopedDequeue = std::make_shared>(); + ASSERT_TRUE(scopedDequeue->empty()); + scopedDequeue->emplace(0, 1); + scopedDequeue->emplace(0, 1); + ASSERT_FALSE(scopedDequeue->empty()); + scopedDequeue->clear(); + ASSERT_TRUE(scopedDequeue->empty()); +} + +TEST_F(ScopedDequeueTest, TestFIFO) +{ + auto scopedDequeue = std::make_shared>(); + ASSERT_TRUE(scopedDequeue->empty()); + scopedDequeue->emplace(0,1); + scopedDequeue->emplace(0,2); + + ASSERT_EQ(1, scopedDequeue->front()); + scopedDequeue->pop(); + ASSERT_EQ(2, scopedDequeue->front()); + scopedDequeue->pop(); + ASSERT_TRUE(scopedDequeue->empty()); +} diff --git a/aplcore/unit/utils/unittest_scopedset.cpp b/aplcore/unit/utils/unittest_scopedset.cpp new file mode 100644 index 0000000..7971375 --- /dev/null +++ b/aplcore/unit/utils/unittest_scopedset.cpp @@ -0,0 +1,87 @@ +/* + * Copyright 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 "gtest/gtest.h" + +#include + +#include "apl/utils/scopedset.h" + +using namespace apl; + +class ScopedSetTest : public ::testing::Test {}; + +TEST_F(ScopedSetTest, Basic) +{ + auto scopedSet = std::make_shared>(); + scopedSet->emplace(1, 1); + scopedSet->emplace(2, 3); + scopedSet->emplace(1, 2); + scopedSet->emplace(2, 4); + scopedSet->emplace(1, 2); + + ASSERT_FALSE(scopedSet->empty()); + ASSERT_EQ(4, scopedSet->size()); + ASSERT_EQ(std::set({1,2,3,4}), scopedSet->getAll()); + ASSERT_EQ(std::set({1,2}), scopedSet->getScoped(1)); + ASSERT_EQ(std::set({3,4}), scopedSet->getScoped(2)); +} + +TEST_F(ScopedSetTest, Clear) +{ + auto scopedSet = std::make_shared>(); + scopedSet->emplace(1, 1); + scopedSet->emplace(2, 3); + scopedSet->emplace(1, 2); + scopedSet->emplace(2, 4); + scopedSet->emplace(1, 2); + + ASSERT_FALSE(scopedSet->empty()); + ASSERT_EQ(4, scopedSet->size()); + ASSERT_EQ(2, scopedSet->eraseScope(2)); + ASSERT_EQ(2, scopedSet->size()); + + scopedSet->clear(); + + ASSERT_TRUE(scopedSet->empty()); +} + +TEST_F(ScopedSetTest, Erase) +{ + auto scopedSet = std::make_shared>(); + scopedSet->emplace(1, 1); + scopedSet->emplace(2, 3); + scopedSet->emplace(1, 2); + scopedSet->emplace(2, 4); + scopedSet->emplace(1, 2); + + ASSERT_FALSE(scopedSet->empty()); + ASSERT_EQ(4, scopedSet->size()); + + ASSERT_EQ(std::set({1,2}), scopedSet->extractScope(1)); + ASSERT_EQ(std::set({3,4}), scopedSet->getAll()); + + ASSERT_EQ(2, scopedSet->size()); + + scopedSet->eraseValue(3); + + ASSERT_EQ(std::set({4}), scopedSet->getAll()); + + ASSERT_EQ(1, scopedSet->size()); + + ASSERT_EQ(4, scopedSet->pop()); + + ASSERT_TRUE(scopedSet->empty()); +} diff --git a/unit/utils/unittest_session.cpp b/aplcore/unit/utils/unittest_session.cpp similarity index 100% rename from unit/utils/unittest_session.cpp rename to aplcore/unit/utils/unittest_session.cpp diff --git a/unit/utils/unittest_stringfunctions.cpp b/aplcore/unit/utils/unittest_stringfunctions.cpp similarity index 100% rename from unit/utils/unittest_stringfunctions.cpp rename to aplcore/unit/utils/unittest_stringfunctions.cpp diff --git a/unit/utils/unittest_synchronizedweakcache.cpp b/aplcore/unit/utils/unittest_synchronizedweakcache.cpp similarity index 100% rename from unit/utils/unittest_synchronizedweakcache.cpp rename to aplcore/unit/utils/unittest_synchronizedweakcache.cpp diff --git a/unit/utils/unittest_url.cpp b/aplcore/unit/utils/unittest_url.cpp similarity index 100% rename from unit/utils/unittest_url.cpp rename to aplcore/unit/utils/unittest_url.cpp diff --git a/unit/utils/unittest_userdata.cpp b/aplcore/unit/utils/unittest_userdata.cpp similarity index 100% rename from unit/utils/unittest_userdata.cpp rename to aplcore/unit/utils/unittest_userdata.cpp diff --git a/unit/utils/unittest_weakcache.cpp b/aplcore/unit/utils/unittest_weakcache.cpp similarity index 100% rename from unit/utils/unittest_weakcache.cpp rename to aplcore/unit/utils/unittest_weakcache.cpp diff --git a/bin/apl-header-inclusion-validation.sh b/bin/apl-header-inclusion-validation.sh index 98f413d..725c428 100644 --- a/bin/apl-header-inclusion-validation.sh +++ b/bin/apl-header-inclusion-validation.sh @@ -34,11 +34,13 @@ public_apl_headers=( "apl/content/aplversion.h" "apl/content/configurationchange.h" "apl/content/content.h" + "apl/content/documentconfig.h" "apl/content/extensioncommanddefinition.h" "apl/content/extensioncomponentdefinition.h" "apl/content/extensioneventhandler.h" "apl/content/extensionfilterdefinition.h" "apl/content/extensionproperty.h" + "apl/content/extensionrequest.h" "apl/content/importref.h" "apl/content/importrequest.h" "apl/content/jsondata.h" @@ -49,14 +51,17 @@ public_apl_headers=( "apl/content/settings.h" "apl/datasource/datasourceconnection.h" "apl/datasource/datasourceprovider.h" + "apl/datasource/dynamicindexlistdatasourceprovider.h" "apl/datasource/dynamiclistdatasourcecommon.h" "apl/datasource/dynamiclistdatasourceprovider.h" - "apl/datasource/dynamicindexlistdatasourceprovider.h" "apl/datasource/dynamictokenlistdatasourceprovider.h" "apl/datasource/offsetindexdatasourceconnection.h" "apl/document/displaystate.h" + "apl/document/documentcontext.h" "apl/document/documentproperties.h" "apl/dynamicdata.h" + "apl/embed/documentmanager.h" + "apl/embed/embedrequest.h" "apl/engine/binding.h" "apl/engine/dependant.h" "apl/engine/event.h" @@ -71,6 +76,7 @@ public_apl_headers=( "apl/engine/styledefinition.h" "apl/engine/styleinstance.h" "apl/engine/styles.h" + "apl/engine/uid.h" "apl/engine/uidobject.h" "apl/extension/extensionclient.h" "apl/extension/extensionmediator.h" @@ -78,20 +84,22 @@ public_apl_headers=( "apl/focus/focusdirection.h" "apl/graphic/graphic.h" "apl/graphic/graphiccontent.h" - "apl/graphic/graphicfilter.h" "apl/graphic/graphicelement.h" + "apl/graphic/graphicfilter.h" "apl/graphic/graphicpattern.h" "apl/graphic/graphicproperties.h" "apl/livedata/livearray.h" + "apl/livedata/livedataobjectwatcher.h" "apl/livedata/livemap.h" "apl/livedata/liveobject.h" - "apl/livedata/livedataobjectwatcher.h" - "apl/media/mediatrack.h" "apl/media/mediamanager.h" "apl/media/mediaobject.h" "apl/media/mediaplayer.h" "apl/media/mediaplayerfactory.h" + "apl/media/mediatrack.h" "apl/primitives/accessibilityaction.h" + "apl/primitives/boundsymbol.h" + "apl/primitives/boundsymbolset.h" "apl/primitives/color.h" "apl/primitives/dimension.h" "apl/primitives/filter.h" diff --git a/bin/find-forbidden-functions b/bin/find-forbidden-functions index 3052238..895bc73 100755 --- a/bin/find-forbidden-functions +++ b/bin/find-forbidden-functions @@ -109,7 +109,7 @@ functions_pattern=$(buildPattern) # - specify the classic locale (for locale-dependent functions) # - are followed by '// disable_forbidden_check' -violations=$(find . \( -iname "*.h" -or -iname "*.cpp" \) -exec 'grep' '-Hn' "$functions_pattern" '{}' \; | grep -v 'std::locale::classic\(\)') +violations=$(find include src \( -iname "*.h" -or -iname "*.cpp" \) -exec 'grep' '-Hn' "$functions_pattern" '{}' \; | grep -v 'std::locale::classic\(\)') if [ -z "$violations" ]; then exit 0 diff --git a/clang.cmake b/clang.cmake index 6c2afa2..dc3da05 100644 --- a/clang.cmake +++ b/clang.cmake @@ -57,7 +57,7 @@ if(COVERAGE) -instr-profile=${TARGET_NAME}.profdata -output-dir=${CMAKE_BINARY_DIR}/coverage ${CMAKE_SOURCE_DIR}/aplcore - DEPENDS ${TARGET_NAME} ${EXEC_NAME} + DEPENDS ${EXEC_NAME} ) add_custom_command(TARGET coverage-${TARGET_NAME} POST_BUILD COMMAND ; diff --git a/components.cmake b/components.cmake index a81783f..8d7363b 100644 --- a/components.cmake +++ b/components.cmake @@ -85,12 +85,6 @@ endif() # Do not move. It requires WASM_FLAGS while defining targets and generates part of environment required by next target. include(thirdparty/thirdparty.cmake) -# We treat enumgen as an external project because it needs to be built using the host toolchain -include(tools.cmake) - -# The core library -add_subdirectory(aplcore) - if(BUILD_DOC) include(doxygen.cmake) endif(BUILD_DOC) @@ -104,23 +98,13 @@ endif (BUILD_TESTS) if (BUILD_ALEXAEXTENSIONS) add_subdirectory(extensions) target_compile_definitions(alexaext PUBLIC ALEXAEXTENSIONS BUILD_UNIT_TESTS=${BUILD_UNIT_TESTS}) - if (NOT USE_SYSTEM_RAPIDJSON AND NOT HAS_FETCH_CONTENT) - add_dependencies(alexaext rapidjson-build) - endif() endif(BUILD_ALEXAEXTENSIONS) -# Test cases are built conditionally. Only affect core do not build them for everything else. -if (BUILD_UNIT_TESTS) - include(CTest) - include_directories(${GTEST_INCLUDE}) - add_subdirectory(unit) - - set(MEMCHECK_OPTIONS "--tool=memcheck --leak-check=full --show-reachable=no --error-exitcode=1 --errors-for-leak-kinds=definite,possible") - add_custom_target(unittest_memcheck - COMMAND ${CMAKE_CTEST_COMMAND} -VV - --overwrite MemoryCheckCommandOptions=${MEMCHECK_OPTIONS} - -T memcheck) -endif (BUILD_UNIT_TESTS) +# The core library +add_subdirectory(aplcore) + +# We treat enumgen as an external project because it needs to be built using the host toolchain +include(tools.cmake) if (BUILD_TEST_PROGRAMS) add_subdirectory(test) diff --git a/doc/scene_graph.puml b/doc/scene_graph.puml index b419a6a..4a30a16 100644 --- a/doc/scene_graph.puml +++ b/doc/scene_graph.puml @@ -280,6 +280,7 @@ MediaPlayer : next() MediaPlayer : previous() MediaPlayer : rewind() MediaPlayer : seek(int offset) +MediaPlayer : seekTo(int offset) MediaPlayer : setTrackIndex(int index) MediaPlayer : setAudioTrack( AudioTrack ) MediaPlayer : callback : function diff --git a/extensions/alexaext/CMakeLists.txt b/extensions/alexaext/CMakeLists.txt index 0a11576..a631eb7 100644 --- a/extensions/alexaext/CMakeLists.txt +++ b/extensions/alexaext/CMakeLists.txt @@ -27,9 +27,11 @@ project(AlexaExt add_library(alexaext STATIC src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp - src/APLWebflowExtension/AplWebflowBase.cpp - src/APLWebflowExtension/AplWebflowExtension.cpp - src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp + src/APLMetricsExtension/AplMetricsExtension.cpp + src/APLWebflowExtension/AplWebflowBase.cpp + src/APLWebflowExtension/AplWebflowExtension.cpp + src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp + src/APLAttentionSystemExtension/AplAttentionSystemExtension.cpp src/executor.cpp src/extensionmessage.cpp src/extensionregistrar.cpp @@ -40,9 +42,9 @@ add_library(alexaext STATIC if (BUILD_SHARED OR ENABLE_PIC) set_target_properties(alexaext - PROPERTIES - POSITION_INDEPENDENT_CODE ON - ) + PROPERTIES + POSITION_INDEPENDENT_CODE ON +) endif() set_target_properties(alexaext @@ -54,9 +56,10 @@ target_include_directories(alexaext PUBLIC $ $ - $ ) +target_link_libraries(alexaext PUBLIC rapidjson-apl) + target_compile_options(alexaext PRIVATE -Werror @@ -98,8 +101,20 @@ install( DESTINATION lib/cmake/alexaext FILE - alexaextConfig.cmake + alexaextTargets.cmake ) add_library(alexa::extensions ALIAS alexaext) +include(CMakePackageConfigHelpers) +configure_package_config_file(alexaextConfig.cmake.in ${CMAKE_CURRENT_BINARY_DIR}/alexaextConfig.cmake + INSTALL_DESTINATION + lib/cmake/alexaext + NO_CHECK_REQUIRED_COMPONENTS_MACRO) +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/alexaextConfig.cmake + DESTINATION + lib/cmake/alexaext +) + diff --git a/extensions/alexaext/alexaextConfig.cmake.in b/extensions/alexaext/alexaextConfig.cmake.in new file mode 100644 index 0000000..ac390ce --- /dev/null +++ b/extensions/alexaext/alexaextConfig.cmake.in @@ -0,0 +1,29 @@ +@PACKAGE_INIT@ + +set(USE_SYSTEM_RAPIDJSON @USE_SYSTEM_RAPIDJSON@) + +# For backwards-compatibility with the old build logic, try to locate RapidJSON on the system if the +# new CMake package is not found +if (NOT TARGET rapidjson-apl) + if (USE_SYSTEM_RAPIDJSON) + find_package(aplrapidjson QUIET) + if (NOT aplrapidjson_FOUND) + # Try to locate RapidJSON on the system + find_package(RapidJSON QUIET) + + if (NOT RapidJSON_FOUND) + # Try to find the headers directly on the system + find_path(RAPIDJSON_INCLUDE_DIRS + NAMES rapidjson/document.h + REQUIRED) + endif() + + add_library(rapidjson-apl INTERFACE IMPORTED) + target_include_directories(rapidjson-apl INTERFACE ${RAPIDJSON_INCLUDE_DIRS}) + endif() + else() + find_package(aplrapidjson REQUIRED) + endif() +endif() + +include("${CMAKE_CURRENT_LIST_DIR}/alexaextTargets.cmake") \ No newline at end of file diff --git a/extensions/alexaext/include/alexaext/APLAttentionSystemExtension/AplAttentionSystemExtension.h b/extensions/alexaext/include/alexaext/APLAttentionSystemExtension/AplAttentionSystemExtension.h new file mode 100644 index 0000000..6752cd5 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLAttentionSystemExtension/AplAttentionSystemExtension.h @@ -0,0 +1,92 @@ +/* + * Copyright 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 APL_APLATTENTIONSYSTEMEXTENSION_H +#define APL_APLATTENTIONSYSTEMEXTENSION_H + +#include + +#include + +#include + +namespace alexaext { +namespace attention { + +static const std::string URI = "aplext:attentionsystem:10"; +static const std::string ENVIRONMENT_VERSION = "APLAttentionSystemExtension-1.0"; + +enum AttentionState { + IDLE, + LISTENING, + THINKING, + SPEAKING +}; + +class AplAttentionSystemExtension + : public alexaext::ExtensionBase, + public std::enable_shared_from_this { + +public: + AplAttentionSystemExtension( + alexaext::ExecutorPtr executor, + alexaext::uuid::UUIDFunction uuidGenerator = alexaext::uuid::generateUUIDV4); + + ~AplAttentionSystemExtension() override = default; + + /// @name alexaext::Extension Functions + /// @{ + + rapidjson::Document createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value ®istrationRequest) override; + + void onActivityUnregistered(const ActivityDescriptor& activity) override; + /// @} + + // Updates the livedata map and sends the event. Should be called on every state change. + virtual void updateAttentionState(const AttentionState& newState); + + void applySettings(const ActivityDescriptor& activity, const rapidjson::Value &settings); + + // Publishes a LiveDataUpdate + void publishLiveData(const ActivityDescriptor& activity); + +private: + static std::string attentionStateToString(const AttentionState& state) { + switch (state) { + case AttentionState::LISTENING: + return "LISTENING"; + case AttentionState::THINKING: + return "THINKING"; + case AttentionState::SPEAKING: + return "SPEAKING"; + case AttentionState::IDLE: + default: + return "IDLE"; + } + }; + + std::weak_ptr mExecutor; + alexaext::uuid::UUIDFunction mUuidGenerator; + AttentionState mAttentionState = AttentionState::IDLE; + std::recursive_mutex mAttentionStateNameMutex; + std::map mAttentionStateNameMap; +}; + +using AplAttentionSystemExtensionPtr = std::shared_ptr; + +} // namespace attention +} // namespace alexaext + +#endif // APL_APLATTENTIONSYSTEMEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h index 908b41f..2d47a4c 100644 --- a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h +++ b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h @@ -13,13 +13,14 @@ * permissions and limitations under the License. */ -#ifndef ALEXA_SMART_SCREEN_SDK_APPLICATIONUTILITIES_APL_EXTENSIONS_AUDIOPLAYER_APLAUDIOPLAYEREXTENSION_H -#define ALEXA_SMART_SCREEN_SDK_APPLICATIONUTILITIES_APL_EXTENSIONS_AUDIOPLAYER_APLAUDIOPLAYEREXTENSION_H +#ifndef APL_APLAUDIOPLAYEREXTENSION_H +#define APL_APLAUDIOPLAYEREXTENSION_H -#include #include -#include +#include +#include #include +#include #include @@ -27,7 +28,8 @@ #include "AplAudioPlayerExtensionObserverInterface.h" -namespace AudioPlayer { +namespace alexaext { +namespace audioplayer { static const std::string URI = "aplext:audioplayer:10"; static const std::string ENVIRONMENT_VERSION = "APLAudioPlayerExtension-1.0"; @@ -37,22 +39,28 @@ static const std::string ENVIRONMENT_VERSION = "APLAudioPlayerExtension-1.0"; * to allow for control and command of audio stream and APL UI. */ class AplAudioPlayerExtension - : public alexaext::ExtensionBase, public std::enable_shared_from_this { + : public alexaext::ExtensionBase, + public std::enable_shared_from_this { public: + // forward declare + struct ActivityState; /** * Constructor */ explicit AplAudioPlayerExtension(std::shared_ptr observer); - virtual ~AplAudioPlayerExtension() = default; + ~AplAudioPlayerExtension() override = default; /// @name alexaext::Extension Functions /// @{ - rapidjson::Document createRegistration(const std::string &uri, + rapidjson::Document createRegistration(const ActivityDescriptor& activity, const rapidjson::Value ®istrationRequest) override; - bool invokeCommand(const std::string &uri, const rapidjson::Value &command) override; + bool invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value &command) override; + + void onActivityRegistered(const ActivityDescriptor& activity) override; + void onActivityUnregistered(const ActivityDescriptor& activity) override; /// @} @@ -79,108 +87,19 @@ class AplAudioPlayerExtension void updatePlaybackProgress(int offset); /** - * Used to inform the extension of the active @c AudioPlayer.Presentation.APL presentationSession. + * This method will do nothing * @deprecated The extension generates it's own token on extension registration. * @param id The identifier of the active presentation session. * @param skillId The identifiery of the active Skill / Speechlet who owns the session. */ void setActivePresentationSession(const std::string& id, const std::string& skillId); - /** - * Utility object for tracking lyrics viewed data. - */ - struct LyricsViewedData { - /** - * Default Constructor. - */ - LyricsViewedData() = default; - - /** - * Constructor. - * - * @param token the identifier of the track displaying lyrics. - */ - explicit LyricsViewedData(std::string token) : token{std::move(token)} - { - lyricData = std::make_shared(); - lyricData->SetArray(); - }; - - /// The identifier of the track displaying lyrics. - std::string token; - - /// The total time in milliseconds that lyrics were viewed. - long durationInMilliseconds{}; - - /// The lyrics viewed data array. - std::shared_ptr lyricData; - - /** - * Add Lyric lines to the data array. - * @param lines The lines of lyrics to append. - */ - void addLyricLinesData(const rapidjson::Value &lyricLines) - { - using namespace rapidjson; - if (!lyricLines.IsArray()) - return; - - auto &alloc = lyricData->GetAllocator(); - for (auto &line: lyricLines.GetArray()) { - // verify data integrity and adjust - // received line data from double format, store the int values - std::string text = alexaext::GetWithDefault("text", line, ""); - auto startTime = alexaext::GetWithDefault("startTime", line, -1); - auto endTime = alexaext::GetWithDefault("endTime", line, -1); - if (text.empty() || startTime < 0 || endTime < 0 || endTime < startTime) - continue; - Value value(kObjectType); - value.AddMember("text", Value(text.c_str(), alloc).Move(), alloc) - .AddMember("startTime", startTime, alloc) - .AddMember("endTime", endTime, alloc); - lyricData->PushBack(value.Move(), alloc); - } - } - - /** - * Resets the LyricsData object - */ - void reset() - { - token = ""; - durationInMilliseconds = 0; - lyricData = std::make_shared(); - lyricData->SetArray(); - } - - /** - * Returns string payload of the lyricData object. - * @return the lyricData object payload. - */ - std::string getLyricDataPayload() const - { - return alexaext::AsString(*lyricData); - } - }; - - /** - * An internal function to retrieve the active @c LyricsViewedData object from the m_lyricsViewedData map - * based on the m_activeSkillId; - * @param initIfNull If true this will initialize a @c LyricsViewedData object for the m_activeSkillId if none exists. - * @param token - The token for the track actively displaying lyrics. - */ - std::shared_ptr getActiveLyricsViewedData(bool initIfNull = false, const std::string &token = ""); - /** - * Flushes the provided @c LyricsViewedData and notifies the observer. - * @param lyricsViewedData The @c LyricsViewedData to flush. - */ - void flushLyricData(const std::shared_ptr &lyricsViewedData); protected: // Applies the settings from a RegistrationRequest - void applySettings(const rapidjson::Value &settings); + void applySettings(const ActivityDescriptor &activity, const rapidjson::Value &settings); // Publishes a LiveDataUpdate void publishLiveData(); @@ -190,23 +109,33 @@ class AplAudioPlayerExtension /// The @c AplAudioPlayerExtensionObserverInterface observer std::shared_ptr mObserver; - /// The document settings defined 'name' for the playbackState data object - std::string mPlaybackStateName; - - /// The @c apl::LiveMap ativity and offset for AudioPlayer playbackState data. + std::mutex mStateMutex; + /// The @c apl::LiveMap activity and offset for AudioPlayer playbackState data. std::string mPlaybackStateActivity; - int mPlaybackStateOffset; + int mPlaybackStateOffset = 0; + /// The map of activity to activity state + std::unordered_map, + ActivityDescriptor::Hash> mActivityStateMap; - /// The id of the active skill in session. - std::string mActiveClientToken; +private: + /** + * Flushes the provided @c ActivityState and notifies the observer. + * @param activityState The @c ActivityState to flush. + */ + void flushLyricData(const std::shared_ptr &activityState); - /// The map of @c LyricsViewedData objects per skill Id. - std::unordered_map> mLyricsViewedData; + /** + * An internal function to retrieve the @c ActivityState object from the mActivityStateMap + * based on the @c ActivityDescriptor. Creates a new ActivityState object if not already created. + * @param descriptor The activity descriptor + */ + std::shared_ptr getOrCreateActivityState(const ActivityDescriptor& activity); }; using AplAudioPlayerExtensionPtr = std::shared_ptr; -} // namespace AudioPlayer - +} // namespace audioplayer +} // namespace alexaext -#endif // ALEXA_SMART_SCREEN_SDK_APPLICATIONUTILITIES_APL_EXTENSIONS_AUDIOPLAYER_APLAUDIOPLAYEREXTENSION_H \ No newline at end of file +#endif // APL_APLAUDIOPLAYEREXTENSION_H \ No newline at end of file diff --git a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h index 9c80a50..3883187 100644 --- a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h +++ b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h @@ -13,10 +13,11 @@ * permissions and limitations under the License. */ -#ifndef ALEXA_SMART_SCREEN_SDK_APPLICATIONUTILITIES_APL_EXTENSIONS_AUDIOPLAYER_APLAUDIOPLAYEREXTENSIONOBSERVERINTERFACE_H -#define ALEXA_SMART_SCREEN_SDK_APPLICATIONUTILITIES_APL_EXTENSIONS_AUDIOPLAYER_APLAUDIOPLAYEREXTENSIONOBSERVERINTERFACE_H +#ifndef APL_APLAUDIOPLAYEREXTENSIONOBSERVERINTERFACE_H +#define APL_APLAUDIOPLAYEREXTENSIONOBSERVERINTERFACE_H -namespace AudioPlayer { +namespace alexaext { +namespace audioplayer { /** * This class allows a @c AplAudioPlayerExtensionObserverInterface observer to be notified of changes in the @@ -100,7 +101,10 @@ class AplAudioPlayerExtensionObserverInterface { virtual void onAudioPlayerSkipBackward() = 0; }; -} // namespace AudioPlayer +} // namespace audioplayer +} // namespace alexaext +// TODO Remove this: https://tiny.amazon.com/asvt1s36 +namespace AudioPlayer = alexaext::audioplayer; -#endif // ALEXA_SMART_SCREEN_SDK_APPLICATIONUTILITIES_APL_EXTENSIONS_AUDIOPLAYER_APLAUDIOPLAYEREXTENSIONOBSERVERINTERFACE_H \ No newline at end of file +#endif // APL_APLAUDIOPLAYEREXTENSIONOBSERVERINTERFACE_H \ No newline at end of file diff --git a/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h index 44ee6ba..146c594 100644 --- a/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h +++ b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h @@ -22,13 +22,15 @@ #include "AplE2eEncryptionExtensionObserverInterface.h" -namespace E2EEncryption { +namespace alexaext { +namespace e2eencryption { static const std::string URI = "aplext:e2eencryption:10"; static const std::string ENVIRONMENT_VERSION = "APLE2EEncryptionExtension-1.0"; class AplE2eEncryptionExtension - : public alexaext::ExtensionBase, public std::enable_shared_from_this { + : public alexaext::ExtensionBase, + public std::enable_shared_from_this { public: @@ -37,7 +39,7 @@ class AplE2eEncryptionExtension alexaext::ExecutorPtr executor, alexaext::uuid::UUIDFunction uuidGenerator = alexaext::uuid::generateUUIDV4); - virtual ~AplE2eEncryptionExtension() = default; + ~AplE2eEncryptionExtension() override = default; /// @name alexaext::Extension Functions /// @{ @@ -58,6 +60,7 @@ class AplE2eEncryptionExtension using AplE2eEncryptionExtensionPtr = std::shared_ptr; -} // namespace E2EEncryption +} // namespace e2eencryption +} // namespace alexaext #endif // APL_APLE2EENCRYPTIONEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h index 3735205..1028fc2 100644 --- a/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h +++ b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h @@ -18,7 +18,8 @@ #include #include -namespace E2EEncryption { +namespace alexaext { +namespace e2eencryption { /** * Callback to run when the encryption of a value finishes successfully @@ -102,6 +103,10 @@ class AplE2eEncryptionExtensionObserverInterface { using AplE2eEncryptionExtensionObserverInterfacePtr = std::shared_ptr; -} // namespace E2EEncryption +} // namespace e2eencryption +} // namespace alexaext + +// TODO Remove this: https://tiny.amazon.com/asvt1s36 +namespace E2EEncryption = alexaext::e2eencryption; #endif // APL_APLE2EENCRYPTIONEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtension/AplMetricsExtension.h b/extensions/alexaext/include/alexaext/APLMetricsExtension/AplMetricsExtension.h new file mode 100644 index 0000000..5a715f4 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtension/AplMetricsExtension.h @@ -0,0 +1,241 @@ +/* + * Copyright 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 APL_APLMETRICSEXTENSION_H +#define APL_APLMETRICSEXTENSION_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "AplMetricsExtensionObserverInterface.h" + +namespace alexaext { +namespace metrics { + +using Timestamp = std::chrono::steady_clock::time_point; + +static const std::string URI = "aplext:metrics:10"; +static const std::string ENVIRONMENT_VERSION = "APLMetricsExtension-1.0"; + +/** + * The metrics extension that enables generating metrics from APL document. + * + * This extension follows the observer model, where a common logic delegates to an observer + * the underlying behavior. + */ +class AplMetricsExtension + : public alexaext::ExtensionBase, + public std::enable_shared_from_this { +public: + /** + * Constructor + * @param observer AplMetricsExtensionObserverInterface instance to report report metrics + * @param executor Extension task executor, observer API are invoked as asynchronous tasks on this. + * @param maxMetricIdAllowed Optional max unique number of metricId allowed for an experience. + */ + AplMetricsExtension( + AplMetricsExtensionObserverInterfacePtr observer, + alexaext::ExecutorPtr executor, + int maxMetricIdAllowed = INT_MAX); + + ~AplMetricsExtension() override = default; + + /// @name alexaext::Extension Functions + /// @{ + + rapidjson::Document createRegistration(const alexaext::ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) override; + + bool invokeCommand(const alexaext::ActivityDescriptor& activity, const rapidjson::Value& command) override; + + void onSessionEnded(const alexaext::SessionDescriptor& session) override; + + /// @} + +private: + /** + * Timer for tracking start time for timer metrics. + */ + struct Timer { + + /** + * Mark the timer as started and note the current timestamp as start time. + */ + void start() { + startTime = std::chrono::steady_clock::now(); + started = true; + } + + /** + * Stop the timer. + * + * @return duration from start time to now, zero if timer is not started or already stopped. + */ + std::chrono::milliseconds stop() { + if (!started) return std::chrono::milliseconds(0); + + started = false; + return std::chrono::duration_cast( + std::chrono::steady_clock::now() - startTime); + } + + bool started; + Timestamp startTime; + }; + + /** + * Utility to track metric data for an experience within an application registered. + */ + struct MetricData { + public: + MetricData(const std::string& applicationId, const std::string& experienceId) + : applicationId(applicationId), experienceId(experienceId) {} + + void incrementCounter(const std::string& metricId, int amount) { + std::lock_guard lock(mMetricDataMutex); + auto counterMetricItr = mCounterMetricsMap.find(metricId); + if (counterMetricItr == mCounterMetricsMap.end()) { + mMetricIdSet.insert(metricId); + mCounterMetricsMap.insert(std::pair(metricId, amount)); + } else { + counterMetricItr->second += amount; + } + } + + std::shared_ptr getOrCreateTimer(const std::string& metricId) { + std::lock_guard lock(mMetricDataMutex); + auto timerMetricItr = mMetricTimerMap.find(metricId); + if (timerMetricItr == mMetricTimerMap.end()) { + auto timer = std::make_shared(); + mMetricIdSet.insert(metricId); + mMetricTimerMap.insert(std::pair>(metricId, timer)); + return timer; + } else { + return timerMetricItr->second; + } + } + + bool isMaxLimitExceeded(const std::string& metricId, const int maxLimit) { + std::lock_guard lock(mMetricDataMutex); + int newCount = mMetricIdSet.size(); + newCount += mMetricIdSet.find(metricId) == mMetricIdSet.end() ? 1 : 0; + return newCount > maxLimit; + } + + std::map& getCounters() { + return mCounterMetricsMap; + } + + public: + std::string applicationId; + std::string experienceId; + private: + std::set mMetricIdSet; + std::map mCounterMetricsMap; + std::unordered_map> mMetricTimerMap; + std::mutex mMetricDataMutex; + }; + + /** + * Track metric data within a session. A session can have multiple activities and each activity + * is associated with an application id experience id. + * + * Metrics within a session are tracked for a unique combination of {applicationId, experienceId} + * and thus can span across activities within a session. For e.g. a timer metric can be started + * in one activity and stopped in another, similarly a counter metric can be incremented by + * multiple activities in a session, the final count is reported when session ends. + */ + struct SessionMetricData { + public: + bool createMetricData(const alexaext::ActivityDescriptor& activity, + const std::string& applicationId, + const std::string& experienceId) + { + std::lock_guard lock(mSessionMutex); + auto metricKey = applicationId + "." + experienceId; + if (mActivityMetricKeysMap.find(activity) != mActivityMetricKeysMap.end()) { + // Activity already registered + return false; + } + + auto metricData = std::make_shared(applicationId, experienceId); + mApplicationMetricMap.insert(std::make_pair(metricKey, metricData)); + mActivityMetricKeysMap.insert(std::make_pair(activity, metricKey)); + return true; + + } + + std::shared_ptr + getActivityMetrics(const alexaext::ActivityDescriptor& activity) + { + std::lock_guard lock(mSessionMutex); + auto itr = mActivityMetricKeysMap.find(activity); + if (itr != mActivityMetricKeysMap.end()) { + auto applicationMetricItr = mApplicationMetricMap.find(itr->second); + if (applicationMetricItr != mApplicationMetricMap.end()) + return applicationMetricItr->second; + } + return nullptr; + } + + std::vector> getAllMetrics() + { + std::lock_guard lock(mSessionMutex); + std::vector> sessionMetrics; + for (auto itr = mApplicationMetricMap.begin(); itr != mApplicationMetricMap.end(); itr++) { + sessionMetrics.emplace_back(itr->second); + } + return sessionMetrics; + } + private: + std::unordered_map> mApplicationMetricMap; + std::unordered_map mActivityMetricKeysMap; + std::mutex mSessionMutex; + }; + +private: + std::shared_ptr getSessionMetrics(const alexaext::SessionDescriptor& session); + std::shared_ptr getActivityMetrics(const alexaext::ActivityDescriptor& activity); + bool incrementCounter(const alexaext::ActivityDescriptor& activity, + const std::string metricId, + const int amount); + bool startTimer(const alexaext::ActivityDescriptor& activity, const std::string metricId); + bool stopTimer(const alexaext::ActivityDescriptor& activity, const std::string metricId); + +private: + std::unordered_map, + alexaext::SessionDescriptor::Hash> mSessionMetricsMap; + AplMetricsExtensionObserverInterfacePtr mObserver; + std::weak_ptr mExecutor; + std::recursive_mutex mSessionMetricsMutex; + const int mMaxMetricIdAllowed; +}; + +using AplMetricsExtensionPtr = std::shared_ptr; + +} // namespace metrics +} // namespace alexaext + +#endif // APL_APLMETRICSEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtension/AplMetricsExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLMetricsExtension/AplMetricsExtensionObserverInterface.h new file mode 100644 index 0000000..e9f3de9 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtension/AplMetricsExtensionObserverInterface.h @@ -0,0 +1,74 @@ +/* + * Copyright 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 APL_APLMETRICSEXTENSIONOBSERVERINTERFACE_H +#define APL_APLMETRICSEXTENSIONOBSERVERINTERFACE_H + +#include +#include +#include + +namespace alexaext { +namespace metrics { + +using Timestamp = std::chrono::steady_clock::time_point; + +/** + * The observer interface for metrics extension. To enable metrics extension, metric publisher need + * to implement this interface and create @c AplMetricsExtension with it. + * + * Metric is created with applicationId, experienceId and metricId. These metrics are to be recorded + * with an identifier in the form @c .. + */ +class AplMetricsExtensionObserverInterface { +public: + explicit AplMetricsExtensionObserverInterface(int maxMetricIdAllowed) {} + + virtual ~AplMetricsExtensionObserverInterface() = default; + + /** + * Records a specific counter metric. + * + * @param applicationId property identifies the skill for which metric is created. + * @param experienceId identifies the experience within the skill for which metric is created. + * @param metricId The timer metric identifier. + * @param count The duration for the timer metric. + * @return true if metric is recorded, false otherwise + */ + virtual bool recordCounter(const std::string& applicationId, + const std::string& experienceId, + const std::string& metricId, + const int count) = 0; + + /** + * Records a specific timer metric. + * + * @param applicationId property identifies the skill for which metric is created. + * @param experienceId identifies the experience within the skill for which metric is created. + * @param metricId The timer metric identifier. + * @param duration The duration for the timer metric. + * @return true if metric is recorded, false otherwise + */ + virtual bool recordTimer(const std::string& applicationId, + const std::string& experienceId, + const std::string& metricId, + const std::chrono::milliseconds& duration) = 0; +}; + +using AplMetricsExtensionObserverInterfacePtr = std::shared_ptr; + +} // namespace metrics +} // namespace alexaext + +#endif // APL_APLMETRICSEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h index e6ef21f..2d9e4e0 100644 --- a/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h +++ b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h @@ -22,7 +22,8 @@ #include "AplMusicAlarmExtensionObserverInterface.h" -namespace MusicAlarm { +namespace alexaext { +namespace musicalarm { static const std::string URI = "aplext:musicalarm:10"; @@ -40,7 +41,7 @@ class AplMusicAlarmExtension : alexaext::ExecutorPtr executor, alexaext::uuid::UUIDFunction uuidGenerator = alexaext::uuid::generateUUIDV4); - virtual ~AplMusicAlarmExtension() = default; + ~AplMusicAlarmExtension() override = default; /// @name alexaext::Extension Functions /// @{ @@ -58,6 +59,7 @@ class AplMusicAlarmExtension : alexaext::uuid::UUIDFunction mUuidGenerator; }; -} // MusicAlarm +} // namespace musicalarm +} // namespace alexaext #endif // APL_APLMUSICALARMEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.h index 80868e8..d8b4bb7 100644 --- a/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.h +++ b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.h @@ -18,7 +18,8 @@ #include #include -namespace MusicAlarm { +namespace alexaext { +namespace musicalarm { class AplMusicAlarmExtensionObserverInterface { public: @@ -40,6 +41,10 @@ class AplMusicAlarmExtensionObserverInterface { using AplMusicAlarmExtensionObserverInterfacePtr = std::shared_ptr; -} // MusicAlarm +} // namespace musicalarm +} // namespace alexaext + +// TODO Remove this: https://tiny.amazon.com/asvt1s36 +namespace MusicAlarm = alexaext::musicalarm; #endif // APL_APLMUSICALARMEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h index ecd6db0..81271bf 100644 --- a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h +++ b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h @@ -18,7 +18,8 @@ #include #include -namespace Webflow { +namespace alexaext { +namespace webflow { /** * Webflow base class. It handles the launch and event processing. @@ -31,10 +32,10 @@ class AplWebflowBase { * Constructor of the webflow * * @param token Meta information about the webflow - * @param uri URI we want to connect + * @param url URL we want to open * @param flowId flow identier of this object */ - AplWebflowBase(std::string token, std::string uri, std::string flowId); + AplWebflowBase(std::string token, std::string url, std::string flowId); /** * Destructor @@ -47,11 +48,11 @@ class AplWebflowBase { virtual bool launch() = 0; /** - * Gets the Uri of the webflow + * Gets the Url of the webflow * - * @return string with the uri of the webflow + * @return string with the url of the webflow */ - const std::string& getUri() const; + const std::string& getUrl() const; /** @@ -70,12 +71,16 @@ class AplWebflowBase { const std::string& getToken() const; protected: std::string mToken; - std::string mUri; + std::string mUrl; std::string mFlowId; }; using AplWebflowBasePtr = std::shared_ptr; -} +} // namespace webflow +} // namespace alexaext + +// TODO Remove this: https://tiny.amazon.com/asvt1s36 +namespace Webflow = alexaext::webflow; #endif // APL_APLWEBFLOW_H diff --git a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h index 3d4888e..5d15b08 100644 --- a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h +++ b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h @@ -26,7 +26,8 @@ #include "AplWebflowBase.h" #include "AplWebflowExtensionObserverInterface.h" -namespace Webflow { +namespace alexaext { +namespace webflow { static const std::string URI = "aplext:webflow:10"; static const std::string ENVIRONMENT_VERSION = "APLWebflowExtension-1.0"; @@ -36,7 +37,7 @@ static const std::string ENVIRONMENT_VERSION = "APLWebflowExtension-1.0"; * to a URL. This is useful for authentication and verification flows. * * This extension follows the observer model, where a common logic delegates to an observer - * the underlaying behavior. + * the underlying behavior. * * Because of the flow nature of the webflow extension, flows can be runtime dependant. The current model * allows two level of indirection. @@ -44,7 +45,8 @@ static const std::string ENVIRONMENT_VERSION = "APLWebflowExtension-1.0"; * Extension->Observer->Flow where Observer and Flow need to implement their interfaces. */ class AplWebflowExtension - : public alexaext::ExtensionBase, public std::enable_shared_from_this { + : public alexaext::ExtensionBase, + public std::enable_shared_from_this { public: /** @@ -55,16 +57,27 @@ class AplWebflowExtension std::shared_ptr observer, const std::shared_ptr& executor); - virtual ~AplWebflowExtension() = default; + ~AplWebflowExtension() override = default; /// @name alexaext::Extension Functions /// @{ - rapidjson::Document createRegistration(const std::string &uri, + rapidjson::Document createRegistration(const ActivityDescriptor &activity, const rapidjson::Value ®istrationRequest) override; - bool invokeCommand(const std::string &uri, const rapidjson::Value &command) override; + bool invokeCommand(const ActivityDescriptor &activity, const rapidjson::Value &command) override; + void onForeground(const ActivityDescriptor& activity) override { + mObserver->onForeground(activity); + } + + void onBackground(const ActivityDescriptor& activity) override { + mObserver->onBackground(activity); + } + + void onHidden(const ActivityDescriptor& activity) override { + mObserver->onHidden(activity); + } /// @} private: @@ -80,6 +93,7 @@ class AplWebflowExtension using AplWebflowExtensionPtr = std::shared_ptr; -} // Webflow +} // namespace webflow +} // namespace alexaext #endif // APL_APLWEBFLOWEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h index 0df36e5..37c0a9c 100644 --- a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h +++ b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h @@ -18,7 +18,8 @@ #include "AplWebflowBase.h" -namespace Webflow { +namespace alexaext { +namespace webflow { /** * This class allows a @c AplWebflowExtensionObserverInterface observer to be notified of changes in the @@ -35,21 +36,42 @@ class AplWebflowExtensionObserverInterface { /** * Used to notify the observer when the extension has issued a StartFlow command. * + * @param activity Descriptor to give information about the activity. * @param token Meta-information about the webflow client. * @param url The https url to open in the webflow. * @param flowId An optional id that will be returned in OnFlowEnd event. * @param onFlowEndEvent when flowId is passed as parameter to the StartFlow command, EndEvent gets sent */ virtual void onStartFlow( + const ActivityDescriptor &activity, const std::string& token, const std::string& url, const std::string& flowId, std::function onFlowEndEvent = [](const std::string&, const std::string&){}) = 0; + + /** + * Notifies observer when the document has come to the foreground. + * @param activity gives information about the activity + */ + virtual void onForeground(const ActivityDescriptor &activity) {} + + /** + * Notifies observer when the document has gone to the background. + * @param activity gives information about the activity + */ + virtual void onBackground(const ActivityDescriptor &activity) {} + + /** + * Notifies observer when the document has been hidden. + * @param activity gives information about the activity + */ + virtual void onHidden(const ActivityDescriptor &activity) {} }; using AplWebflowExtensionObserverInterfacePtr = std::shared_ptr; -} // namespace Webflow +} // namespace webflow +} // namespace alexaext #endif // APL_APLWEBFLOWEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/alexaext.h b/extensions/alexaext/include/alexaext/alexaext.h index ad49033..cbfdf51 100644 --- a/extensions/alexaext/include/alexaext/alexaext.h +++ b/extensions/alexaext/include/alexaext/alexaext.h @@ -50,5 +50,7 @@ #include "APLE2EEncryptionExtension/AplE2eEncryptionExtension.h" #include "APLWebflowExtension/AplWebflowExtension.h" #include "APLMusicAlarmExtension/AplMusicAlarmExtension.h" +#include "APLMetricsExtension/AplMetricsExtension.h" +#include "APLAttentionSystemExtension/AplAttentionSystemExtension.h" #endif //_ALEXAEXT_H diff --git a/extensions/alexaext/include/alexaext/extensionschema.h b/extensions/alexaext/include/alexaext/extensionschema.h index 194d832..c903233 100644 --- a/extensions/alexaext/include/alexaext/extensionschema.h +++ b/extensions/alexaext/include/alexaext/extensionschema.h @@ -503,6 +503,11 @@ class LiveDataSchema : public SchemaBuilder { return *this; } + LiveDataSchema& data(const rapidjson::Value& value) { + DATA().Set(*mValue, rapidjson::Value().CopyFrom(value, *mAllocator), *mAllocator); + return *this; + } + static const rapidjson::Pointer& DATA_TYPE() { static const rapidjson::Pointer ptr("/type"); return ptr; @@ -539,6 +544,10 @@ class LiveDataSchema : public SchemaBuilder { return ptr; } + static const rapidjson::Pointer& DATA() { + static const rapidjson::Pointer ptr("/data"); + return ptr; + } private: bool mIsDataArray; diff --git a/extensions/alexaext/src/APLAttentionSystemExtension/AplAttentionSystemExtension.cpp b/extensions/alexaext/src/APLAttentionSystemExtension/AplAttentionSystemExtension.cpp new file mode 100644 index 0000000..51a2718 --- /dev/null +++ b/extensions/alexaext/src/APLAttentionSystemExtension/AplAttentionSystemExtension.cpp @@ -0,0 +1,141 @@ +/* + * Copyright 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 "alexaext/APLAttentionSystemExtension/AplAttentionSystemExtension.h" + +#include + +using namespace alexaext; +using namespace alexaext::attention; + +// version of the extension definition Schema +static const char *SCHEMA_VERSION = "1.0"; + +// Settings read on registration +static const char *SETTING_ATTENTION_SYSTEM_STATE_NAME = "attentionSystemStateName"; + +// Events +static const char* ON_ATTENTION_STATE_CHANGED = "OnAttentionStateChanged"; + +// Data Types +static const char *DATA_TYPE_ATTENTION_STATE = "AttentionSystemState"; +static const char *PROPERTY_ATTENTION_STATE = "attentionState"; + +AplAttentionSystemExtension::AplAttentionSystemExtension( + alexaext::ExecutorPtr executor, + alexaext::uuid::UUIDFunction uuidGenerator) + : alexaext::ExtensionBase(URI), + mExecutor(executor), + mUuidGenerator(std::move(uuidGenerator)) {} + + +rapidjson::Document +AplAttentionSystemExtension::createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) +{ + if (activity.getURI() != URI) + return RegistrationFailure::forUnknownURI(activity.getURI()); + + const auto *settingsValue = RegistrationRequest::SETTINGS().Get(registrationRequest); + if (settingsValue) { + applySettings(activity, *settingsValue); + } + + return RegistrationSuccess(SCHEMA_VERSION) + .uri(URI) + .token(mUuidGenerator()) + .environment([](Environment& environment) { + environment.version(ENVIRONMENT_VERSION); + }) + .schema(SCHEMA_VERSION, [&](ExtensionSchema& schema) { + schema.uri(URI) + .dataType(DATA_TYPE_ATTENTION_STATE, [](TypeSchema &dataTypeSchema) { + dataTypeSchema + .property(PROPERTY_ATTENTION_STATE, "string"); + }) + .event(ON_ATTENTION_STATE_CHANGED); + std::lock_guard lock(mAttentionStateNameMutex); + auto entry = mAttentionStateNameMap.find(activity); + if (entry != mAttentionStateNameMap.end()) { + schema.liveDataMap(entry->second, [] (LiveDataSchema &liveDataSchema) { + liveDataSchema.dataType(DATA_TYPE_ATTENTION_STATE); + }); + } + }); +} + +void +AplAttentionSystemExtension::updateAttentionState(const AttentionState& newState) +{ + mAttentionState = newState; + std::lock_guard lock(mAttentionStateNameMutex); + + for (auto entry : mAttentionStateNameMap) { + auto event = Event(SCHEMA_VERSION).uri(URI).target(URI) + .name(ON_ATTENTION_STATE_CHANGED) + .property(PROPERTY_ATTENTION_STATE, attentionStateToString(newState)); + + publishLiveData(entry.first); + invokeExtensionEventHandler(entry.first, event); + } +} + +void +AplAttentionSystemExtension::applySettings(const ActivityDescriptor& activity, const rapidjson::Value &settings) +{ + if (!settings.IsObject()) + return; + + /// Apply document assigned settings + auto attentionSystemStateName = settings.FindMember(SETTING_ATTENTION_SYSTEM_STATE_NAME); + if (attentionSystemStateName != settings.MemberEnd() && attentionSystemStateName->value.IsString()) { + mAttentionStateNameMap.emplace(activity, settings[SETTING_ATTENTION_SYSTEM_STATE_NAME].GetString()); + } +} + +void +AplAttentionSystemExtension::publishLiveData(const ActivityDescriptor& activity) +{ + + std::string attentionStateName; + { + std::lock_guard lock(mAttentionStateNameMutex); + auto entry = mAttentionStateNameMap.find(activity); + + // live data is not used if it has not been named + if (entry == mAttentionStateNameMap.end()) + return; + + attentionStateName = entry->second; + } + + // Publish attention state + auto liveDataUpdate = LiveDataUpdate(SCHEMA_VERSION) + .uri(URI) + .objectName(attentionStateName) + .target(URI) + .liveDataMapUpdate([&](LiveDataMapOperation &operation) { + operation + .type("Set") + .key(PROPERTY_ATTENTION_STATE) + .item(attentionStateToString(mAttentionState)); + }); + invokeLiveDataUpdate(activity, liveDataUpdate); +} + +void +AplAttentionSystemExtension::onActivityUnregistered(const ActivityDescriptor& activity) +{ + mAttentionStateNameMap.erase(activity); +} \ No newline at end of file diff --git a/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp b/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp index 91e2753..5901e78 100644 --- a/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp +++ b/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp @@ -84,6 +84,81 @@ static const std::vector PLAYER_ACTIVITY = { "BUFFER_UNDERRUN" }; +/** + * Utility object for tracking ActivityState + */ +struct AplAudioPlayerExtension::ActivityState { + /** + * Constructor. + * + * @param token the identifier of the track displaying lyrics. + */ + explicit ActivityState(std::string token = "") : token{std::move(token)} + { + lyricData = std::make_shared(); + lyricData->SetArray(); + }; + + /// The playback state name + std::string playbackStateName; + + /// The identifier of the track displaying lyrics. + std::string token; + + /// The total time in milliseconds that lyrics were viewed. + long durationInMilliseconds = 0; + + /// The lyrics viewed data array. + std::shared_ptr lyricData; + + /** + * Add Lyric lines to the data array. + * @param lines The lines of lyrics to append. + */ + void addLyricLinesData(const rapidjson::Value &lyricLines) + { + using namespace rapidjson; + if (!lyricLines.IsArray()) + return; + + auto &alloc = lyricData->GetAllocator(); + for (auto &line: lyricLines.GetArray()) { + // verify data integrity and adjust + // received line data from double format, store the int values + std::string text = alexaext::GetWithDefault("text", line, ""); + auto startTime = alexaext::GetWithDefault("startTime", line, -1); + auto endTime = alexaext::GetWithDefault("endTime", line, -1); + if (text.empty() || startTime < 0 || endTime < 0 || endTime < startTime) + continue; + Value value(kObjectType); + value.AddMember("text", Value(text.c_str(), alloc).Move(), alloc) + .AddMember("startTime", startTime, alloc) + .AddMember("endTime", endTime, alloc); + lyricData->PushBack(value.Move(), alloc); + } + } + + /** + * Clear the Lyrics object + */ + void clearLyrics() + { + token = ""; + durationInMilliseconds = 0; + lyricData = std::make_shared(); + lyricData->SetArray(); + } + + /** + * Returns string payload of the lyricData object. + * @return the lyricData object payload. + */ + std::string getLyricDataPayload() const + { + return alexaext::AsString(*lyricData); + } +}; + // Registration token for unique client identifier // TODO use UID gen static std::atomic_int sToken(53); @@ -91,31 +166,28 @@ static std::atomic_int sToken(53); AplAudioPlayerExtension::AplAudioPlayerExtension(std::shared_ptr observer) : alexaext::ExtensionBase(URI), mObserver(std::move(observer)) { - mPlaybackStateName = ""; - mActiveClientToken = ""; mPlaybackStateActivity = "STOPPED"; mPlaybackStateOffset = 0; } void -AplAudioPlayerExtension::applySettings(const rapidjson::Value &settings) +AplAudioPlayerExtension::applySettings(const ActivityDescriptor &activity, const rapidjson::Value &settings) { - // Reset to defaults - mPlaybackStateName = ""; if (!settings.IsObject()) return; /// Apply document assigned settings auto playbackStateName = settings.FindMember(SETTING_PLAYBACK_STATE_NAME); if (playbackStateName != settings.MemberEnd() && playbackStateName->value.IsString()) { - mPlaybackStateName = settings[SETTING_PLAYBACK_STATE_NAME].GetString(); + getOrCreateActivityState(activity)->playbackStateName = settings[SETTING_PLAYBACK_STATE_NAME].GetString(); } } rapidjson::Document -AplAudioPlayerExtension::createRegistration(const std::string &uri, +AplAudioPlayerExtension::createRegistration(const ActivityDescriptor& activity, const rapidjson::Value ®istrationRequest) { + auto uri = activity.getURI(); if (uri != URI) { return RegistrationFailure::forUnknownURI(uri); } @@ -123,11 +195,10 @@ AplAudioPlayerExtension::createRegistration(const std::string &uri, // extract document assigned settings const auto *settingsValue = RegistrationRequest::SETTINGS().Get(registrationRequest); if (settingsValue) - applySettings(*settingsValue); + applySettings(activity, *settingsValue); // session token auto clientToken = TAG + std::to_string(sToken++); - setActivePresentationSession(clientToken, clientToken); // return success with the schema and environment return RegistrationSuccess(SCHEMA_VERSION) @@ -226,9 +297,11 @@ AplAudioPlayerExtension::createRegistration(const std::string &uri, .dataType(DATA_TYPE_ADD_LYRICS_DURATION); }) .command(COMMAND_FLUSH_LYRIC_DATA); + // live data is not used if it has not been named - if (!mPlaybackStateName.empty()) { - schema.liveDataMap(mPlaybackStateName, [](LiveDataSchema &liveDataSchema) { + auto playbackStateName = getOrCreateActivityState(activity)->playbackStateName; + if (!playbackStateName.empty()) { + schema.liveDataMap(playbackStateName, [](LiveDataSchema &liveDataSchema) { liveDataSchema.dataType(DATA_TYPE_PLAYBACK_STATE); }); } @@ -236,8 +309,10 @@ AplAudioPlayerExtension::createRegistration(const std::string &uri, } bool -AplAudioPlayerExtension::invokeCommand(const std::string &uri, const rapidjson::Value &command) +AplAudioPlayerExtension::invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value &command) { + auto uri = activity.getURI(); + // unknown URI if (uri != URI) return false; @@ -313,12 +388,16 @@ AplAudioPlayerExtension::invokeCommand(const std::string &uri, const rapidjson:: std::string token = GetWithDefault(PROPERTY_TOKEN, *params, ""); if (token.empty()) return false; - auto lyricData = getActiveLyricsViewedData(true, token); - if (!lyricData) - return false; + auto state = getOrCreateActivityState(activity); + auto &activeToken = state->token; + // Flush lyric data if tokens are changing + if (token != activeToken) { + flushLyricData(state); + } + state->token = token; // Validation of lines is handled by lyricData, invalid values are ignored const auto &lines = (*params)[PROPERTY_LINES]; - lyricData->addLyricLinesData(lines); + state->addLyricLinesData(lines); return true; } @@ -329,59 +408,58 @@ AplAudioPlayerExtension::invokeCommand(const std::string &uri, const rapidjson:: std::string token = GetWithDefault(PROPERTY_TOKEN, *params, ""); if (token.empty()) return false; - auto lyricData = getActiveLyricsViewedData(true, token); - if (!lyricData) - return false; auto duration = GetWithDefault(PROPERTY_DURATION_IN_MILLISECONDS, *params, -1); if (duration < 0) return false; - lyricData->durationInMilliseconds += duration; + getOrCreateActivityState(activity)->durationInMilliseconds += duration; return true; } if (COMMAND_FLUSH_LYRIC_DATA == name) { - if (auto lyricData = getActiveLyricsViewedData()) { - flushLyricData(lyricData); - } + flushLyricData(getOrCreateActivityState(activity)); return true; } return false; } -std::shared_ptr -AplAudioPlayerExtension::getActiveLyricsViewedData(bool initIfNull, const std::string &token) +void +AplAudioPlayerExtension::onActivityRegistered(const ActivityDescriptor &activity) { - if (!mActiveClientToken.empty()) { - auto lvdi = mLyricsViewedData.find(mActiveClientToken); - if (lvdi != mLyricsViewedData.end()) { - auto lyricsViewedData = lvdi->second; - // If token has changed for the active skill's lyric data, flush the data and set the new token. - if (!token.empty() && lyricsViewedData->token != token) { - flushLyricData(lyricsViewedData); - lyricsViewedData->token = token; - } - return lyricsViewedData; - } - } + getOrCreateActivityState(activity); +} - if (initIfNull) { - mLyricsViewedData[mActiveClientToken] = std::make_shared(token); - return mLyricsViewedData[mActiveClientToken]; - } +void +AplAudioPlayerExtension::onActivityUnregistered(const ActivityDescriptor &activity) +{ + flushLyricData(getOrCreateActivityState(activity)); + std::lock_guard lock(mStateMutex); + mActivityStateMap.erase(activity); +} - return nullptr; +std::shared_ptr +AplAudioPlayerExtension::getOrCreateActivityState(const ActivityDescriptor &activity) +{ + std::lock_guard lock(mStateMutex); + auto it = mActivityStateMap.find(activity); + if (it != mActivityStateMap.end()) { + return it->second; + } else { + auto state = std::make_shared(); + mActivityStateMap.emplace(activity, state); + return state; + } } void -AplAudioPlayerExtension::flushLyricData(const std::shared_ptr &lyricsViewedData) +AplAudioPlayerExtension::flushLyricData(const std::shared_ptr &activityState) { - if (!lyricsViewedData->lyricData->Empty()) { - mObserver->onAudioPlayerLyricDataFlushed(lyricsViewedData->token, - lyricsViewedData->durationInMilliseconds, - lyricsViewedData->getLyricDataPayload()); + if (!activityState->lyricData->Empty()) { + mObserver->onAudioPlayerLyricDataFlushed(activityState->token, + activityState->durationInMilliseconds, + activityState->getLyricDataPayload()); } - lyricsViewedData->reset(); + activityState->clearLyrics(); } void @@ -391,57 +469,78 @@ AplAudioPlayerExtension::updatePlayerActivity(const std::string &state, int offs return; } - mPlaybackStateActivity = state; - mPlaybackStateOffset = offset; + { + std::lock_guard lock(mStateMutex); + mPlaybackStateActivity = state; + mPlaybackStateOffset = offset; + } auto event = Event("1.0").uri(URI).target(URI) .name(EVENTHANDLER_ON_PLAYER_ACTIVITY_UPDATED_NAME) .property(PROPERTY_PLAYER_ACTIVITY, state) .property(PROPERTY_OFFSET, offset); publishLiveData(); - invokeExtensionEventHandler(URI, event); + + // Make a list of activities to update with the lock + std::vector activitiesToUpdate; + { + std::lock_guard lock(mStateMutex); + for (const auto &it: mActivityStateMap) { + activitiesToUpdate.emplace_back(it.first); + } + } + + for (const auto &activity: activitiesToUpdate) { + invokeExtensionEventHandler(activity, event); + } } void AplAudioPlayerExtension::updatePlaybackProgress(int offset) { - mPlaybackStateOffset = offset; + { + std::lock_guard lock(mStateMutex); + mPlaybackStateOffset = offset; + } publishLiveData(); } void AplAudioPlayerExtension::setActivePresentationSession(const std::string &id, const std::string &skillId) { - mActiveClientToken = skillId; - /// If there's available lyricsViewedData for the newly active skillId, report it immediately - if (auto lyricsViewedData = getActiveLyricsViewedData()) { - flushLyricData(lyricsViewedData); - } + // no-op } void AplAudioPlayerExtension::publishLiveData() { - // live data is not used if it has not been named - if (mPlaybackStateName.empty()) - return; + // Make a list of updates with the lock + std::unordered_map, ActivityDescriptor::Hash> updates; + { + std::lock_guard lock(mStateMutex); + for (const auto &it: mActivityStateMap) { + const auto playbackStateName = it.second->playbackStateName; + // Publish live data for activities that set the playback state name + if (!playbackStateName.empty()) { + auto liveDataUpdate = std::make_shared("1.0"); + liveDataUpdate->uri(URI) + .objectName(playbackStateName) + .target(URI) + .liveDataMapUpdate([&](LiveDataMapOperation& operation) { + operation.type("Set") + .key(PROPERTY_PLAYER_ACTIVITY) + .item(mPlaybackStateActivity); + }) + .liveDataMapUpdate([&](LiveDataMapOperation& operation) { + operation.type("Set").key(PROPERTY_OFFSET).item(mPlaybackStateOffset); + }); + + updates.emplace(it.first, liveDataUpdate); + } + } + } - // Publish playback state - auto liveDataUpdate = LiveDataUpdate("1.0") - .uri(URI) - .objectName(mPlaybackStateName) - .target(URI) - .liveDataMapUpdate([&](LiveDataMapOperation &operation) { - operation - .type("Set") - .key(PROPERTY_PLAYER_ACTIVITY) - .item(mPlaybackStateActivity); - }) - .liveDataMapUpdate([&](LiveDataMapOperation &operation) { - operation - .type("Set") - .key(PROPERTY_OFFSET) - .item(mPlaybackStateOffset); - }); - invokeLiveDataUpdate(URI, liveDataUpdate); + for (const auto& it: updates) { + invokeLiveDataUpdate(it.first, it.second->getDocument()); + } } diff --git a/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp b/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp index fac3898..be3d006 100644 --- a/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp +++ b/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp @@ -69,7 +69,8 @@ AplE2eEncryptionExtension::AplE2eEncryptionExtension( rapidjson::Document AplE2eEncryptionExtension::createRegistration(const std::string& uri, - const rapidjson::Value& registrationRequest) { + const rapidjson::Value& registrationRequest) +{ if (uri != URI) return RegistrationFailure::forUnknownURI(uri); @@ -133,7 +134,8 @@ AplE2eEncryptionExtension::createRegistration(const std::string& uri, } bool -AplE2eEncryptionExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) { +AplE2eEncryptionExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) +{ if (uri != URI) return false; diff --git a/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp b/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp new file mode 100644 index 0000000..dfba0fd --- /dev/null +++ b/extensions/alexaext/src/APLMetricsExtension/AplMetricsExtension.cpp @@ -0,0 +1,280 @@ +/* + * Copyright 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 "alexaext/APLMetricsExtension/AplMetricsExtension.h" + +using namespace alexaext; +using namespace alexaext::metrics; + +static const std::string MAX_METRIC_ID_ALLOWED = "maxMetricIdAllowed"; + +static const char* APPLICATION_ID = "applicationId"; +static const char* EXPERIENCE_ID = "experienceId"; + +static const std::string COMMAND_INCREMENTCOUNTER_NAME = "IncrementCounter"; +static const std::string COMMAND_STARTTIMER_NAME = "StartTimer"; +static const std::string COMMAND_STOPTIMER_NAME = "StopTimer"; + +static const char* PROPERTY_METRIC_ID = "metricId"; +static const char* PROPERTY_AMOUNT = "amount"; + +static const std::string INCREMENT_COUNTER_DATA_TYPE = "IncrementCounterDataType"; +static const std::string START_TIMER_DATA_TYPE = "StartTimerDataType"; +static const std::string STOP_TIMER_DATA_TYPE = "StopTimerDataType"; + +static const std::string STRING_TYPE = "String"; +static const std::string INTEGER_TYPE = "Integer"; + +static const std::string SCHEMA_VERSION = "1.0"; + +AplMetricsExtension::AplMetricsExtension(AplMetricsExtensionObserverInterfacePtr observer, + ExecutorPtr executor, + int maxMetricIdAllowed) + : ExtensionBase(URI), + mObserver(std::move(observer)), + mExecutor(executor), + mMaxMetricIdAllowed(maxMetricIdAllowed) {} + +rapidjson::Document +AplMetricsExtension::createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) +{ + if (activity.getURI() != URI) + return RegistrationFailure::forUnknownURI(activity.getURI()); + + std::string applicationId; + std::string experienceId; + const auto *settings = RegistrationRequest::SETTINGS().Get(registrationRequest); + if (!settings || !settings->IsObject()) + return RegistrationFailure::forInvalidMessage(activity.getURI()); + + auto settingsObject = settings->GetObject(); + if (!settingsObject.HasMember(APPLICATION_ID)) + return RegistrationFailure::forInvalidMessage(activity.getURI()); + + applicationId = settingsObject[APPLICATION_ID].GetString(); + if (applicationId.empty()) + return RegistrationFailure::forInvalidMessage(activity.getURI()); + + if (settingsObject.HasMember(EXPERIENCE_ID)) + experienceId = settingsObject[EXPERIENCE_ID].GetString(); + + { + std::lock_guard lock(mSessionMetricsMutex); + auto sessionMetrics = getSessionMetrics(*activity.getSession()); + if (!sessionMetrics) { + sessionMetrics = std::make_shared(); + mSessionMetricsMap.insert( + std::make_pair(*activity.getSession(), sessionMetrics)); + } + + if (!sessionMetrics->createMetricData(activity, applicationId, experienceId)) + return RegistrationFailure::forException(activity.getURI(), "Activity already registered"); + + } + + return RegistrationSuccess(SCHEMA_VERSION) + .uri(URI) + .token("") + .environment([&](Environment& environment) { + environment.version(ENVIRONMENT_VERSION); + environment.property(MAX_METRIC_ID_ALLOWED, mMaxMetricIdAllowed); + }) + .schema(SCHEMA_VERSION, [&](ExtensionSchema& schema) { + schema.uri(URI) + .command(COMMAND_INCREMENTCOUNTER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(INCREMENT_COUNTER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .command(COMMAND_STARTTIMER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(START_TIMER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .command(COMMAND_STOPTIMER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(STOP_TIMER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .dataType(INCREMENT_COUNTER_DATA_TYPE, [](TypeSchema& typeSchema) { + typeSchema + .property( + PROPERTY_METRIC_ID, [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }) + .property( + PROPERTY_AMOUNT, [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(INTEGER_TYPE).defaultValue(1); + }); + }) + .dataType(START_TIMER_DATA_TYPE, [](TypeSchema& typeSchema) { + typeSchema + .property( + PROPERTY_METRIC_ID, [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }); + }) + .dataType(STOP_TIMER_DATA_TYPE, [](TypeSchema& typeSchema) { + typeSchema + .property( + PROPERTY_METRIC_ID, [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }); + }); + }); +} + +bool +AplMetricsExtension::invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value& command) +{ + if (!mObserver) + return false; + + auto sessionMetrics = getSessionMetrics(*activity.getSession()); + if (!sessionMetrics) + return false; + + const std::string commandName = GetWithDefault(Command::NAME(), command, ""); + const rapidjson::Value* params = Command::PAYLOAD().Get(command); + + if (!params || !params->HasMember(PROPERTY_METRIC_ID)) + return false; + + std::string metricId = GetWithDefault(PROPERTY_METRIC_ID, params, ""); + + if (metricId.empty()) + return false; + + if (COMMAND_INCREMENTCOUNTER_NAME == commandName) { + auto amount = GetWithDefault(PROPERTY_AMOUNT, params, 1); + return incrementCounter(activity, metricId, amount); + } + + if (COMMAND_STARTTIMER_NAME == commandName) + return startTimer(activity, metricId); + + if (COMMAND_STOPTIMER_NAME == commandName) + return stopTimer(activity, metricId); + + return false; +} + +bool +AplMetricsExtension::incrementCounter(const ActivityDescriptor& activity, + const std::string metricId, + const int amount) +{ + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) + return false; + + if (activityMetricData->isMaxLimitExceeded(metricId, mMaxMetricIdAllowed)) + return false; + + activityMetricData->incrementCounter(metricId, amount); + return true; +} + +bool +AplMetricsExtension::startTimer(const ActivityDescriptor& activity, + const std::string metricId) +{ + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) + return false; + + if (activityMetricData->isMaxLimitExceeded(metricId, mMaxMetricIdAllowed)) + return false; + + auto timer = activityMetricData->getOrCreateTimer(metricId); + timer->start(); + return true; +} + +bool +AplMetricsExtension::stopTimer(const ActivityDescriptor& activity, + const std::string metricId) +{ + auto executor = mExecutor.lock(); + if (!executor) + return false; + + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) + return false; + + auto timer = activityMetricData->getOrCreateTimer(metricId); + + if (timer->started) { + auto applicationId = activityMetricData->applicationId; + auto experienceId = activityMetricData->experienceId; + auto duration = timer->stop(); + executor->enqueueTask( + [=]() { mObserver->recordTimer(applicationId, experienceId, metricId, duration); }); + return true; + } + return false; +} + +std::shared_ptr +AplMetricsExtension::getSessionMetrics(const SessionDescriptor& session) +{ + std::lock_guard lock(mSessionMetricsMutex); + auto sessionMetricDataItr = mSessionMetricsMap.find(session); + if (sessionMetricDataItr == mSessionMetricsMap.end()) + return nullptr; + return sessionMetricDataItr->second; +} + +std::shared_ptr +AplMetricsExtension::getActivityMetrics(const ActivityDescriptor& activity) +{ + auto sessionMetrics = getSessionMetrics(*activity.getSession()); + if (!sessionMetrics) + return nullptr; + return sessionMetrics->getActivityMetrics(activity); +} + +void +AplMetricsExtension::onSessionEnded(const SessionDescriptor& session) +{ + std::lock_guard lock(mSessionMetricsMutex); + auto sessionMetrics = getSessionMetrics(session); + if (!sessionMetrics) + return; + + auto executor = mExecutor.lock(); + if (!executor) + return; + + for (auto metricData : sessionMetrics->getAllMetrics()) { + auto counterMetrics = metricData->getCounters(); + for (auto counterMetric = counterMetrics.begin(); counterMetric != counterMetrics.end(); + counterMetric++) { + auto applicationId = metricData->applicationId; + auto experienceId = metricData->experienceId; + auto metricId = counterMetric->first; + auto count = counterMetric->second; + executor->enqueueTask([=]() { + mObserver->recordCounter(applicationId, experienceId, metricId, count); + }); + } + } + + mSessionMetricsMap.erase(session); +} \ No newline at end of file diff --git a/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp b/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp index d820f85..5518fe7 100644 --- a/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp +++ b/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp @@ -37,7 +37,8 @@ AplMusicAlarmExtension::AplMusicAlarmExtension(AplMusicAlarmExtensionObserverInt rapidjson::Document AplMusicAlarmExtension::createRegistration(const std::string& uri, - const rapidjson::Value& registrationRequest) { + const rapidjson::Value& registrationRequest) +{ if (uri != URI) return RegistrationFailure::forUnknownURI(uri); @@ -55,7 +56,8 @@ AplMusicAlarmExtension::createRegistration(const std::string& uri, } bool -AplMusicAlarmExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) { +AplMusicAlarmExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) +{ if (!mObserver) return false; diff --git a/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp b/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp index e933fee..305836c 100644 --- a/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp +++ b/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp @@ -15,24 +15,27 @@ #include "alexaext/APLWebflowExtension/AplWebflowBase.h" -using namespace Webflow; +using namespace alexaext::webflow; -AplWebflowBase::AplWebflowBase(std::string token, std::string uri, std::string flowId) +AplWebflowBase::AplWebflowBase(std::string token, std::string url, std::string flowId) : mToken(std::move(token)), - mUri(std::move(uri)), + mUrl(std::move(url)), mFlowId(std::move(flowId)) {} const std::string& -AplWebflowBase::getUri() const { - return mUri; +AplWebflowBase::getUrl() const +{ + return mUrl; } const std::string& -AplWebflowBase::getFlowId() const { +AplWebflowBase::getFlowId() const +{ return mFlowId; } const std::string& -AplWebflowBase::getToken() const { +AplWebflowBase::getToken() const +{ return mToken; } \ No newline at end of file diff --git a/extensions/alexaext/src/APLWebflowExtension/AplWebflowExtension.cpp b/extensions/alexaext/src/APLWebflowExtension/AplWebflowExtension.cpp index 3396bbc..bf52812 100644 --- a/extensions/alexaext/src/APLWebflowExtension/AplWebflowExtension.cpp +++ b/extensions/alexaext/src/APLWebflowExtension/AplWebflowExtension.cpp @@ -22,8 +22,7 @@ #include "alexaext/APLWebflowExtension/AplWebflowExtension.h" -using namespace Webflow; -using namespace alexaext; +using namespace alexaext::webflow; // Data types static const char* PAYLOAD_START_FLOW = "StartFlowPayload"; @@ -52,10 +51,11 @@ AplWebflowExtension::AplWebflowExtension( mExecutor(executor) {} rapidjson::Document -AplWebflowExtension::createRegistration(const std::string& uri, - const rapidjson::Value& registrationRequest) { - if (uri != URI) { - return RegistrationFailure::forUnknownURI(uri); +AplWebflowExtension::createRegistration(const ActivityDescriptor &activity, + const rapidjson::Value& registrationRequest) +{ + if (activity.getURI() != URI) { + return RegistrationFailure::forUnknownURI(activity.getURI()); } // return success with the schema and environment @@ -85,9 +85,10 @@ AplWebflowExtension::createRegistration(const std::string& uri, } bool -AplWebflowExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) { +AplWebflowExtension::invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value& command) +{ // unknown URI - if (uri != URI) + if (activity.getURI() != URI) return false; if (!mObserver) @@ -112,7 +113,7 @@ AplWebflowExtension::invokeCommand(const std::string& uri, const rapidjson::Valu std::string flowId = GetWithDefault(PROPERTY_FLOW_ID, *params, ""); if (flowId.empty()) { - executor->enqueueTask([&]() { mObserver->onStartFlow(token, url, flowId); }); + executor->enqueueTask([&]() { mObserver->onStartFlow(activity, token, url, flowId); }); } else { std::weak_ptr thisWeak = shared_from_this(); @@ -128,7 +129,7 @@ AplWebflowExtension::invokeCommand(const std::string& uri, const rapidjson::Valu lockPtr->invokeExtensionEventHandler(URI, event); } }; - executor->enqueueTask([&]() { mObserver->onStartFlow(token, url, flowId, onFlowEnd); }); + executor->enqueueTask([&]() { mObserver->onStartFlow(activity, token, url, flowId, onFlowEnd); }); } return true; } diff --git a/extensions/unit/CMakeLists.txt b/extensions/unit/CMakeLists.txt index 7e0094b..e594cf1 100644 --- a/extensions/unit/CMakeLists.txt +++ b/extensions/unit/CMakeLists.txt @@ -16,16 +16,13 @@ set(CMAKE_CXX_STANDARD 11) ## It is expected that the enclosing project provides GTest dependency. -find_path(RAPIDJSON_INCLUDE - NAMES rapidjson/document.h - REQUIRED) -include_directories(${RAPIDJSON_INCLUDE}) - add_executable(alexaext-unittest unittest_apl_audio_player.cpp unittest_apl_e2e_encryption.cpp + unittest_apl_metric.cpp unittest_apl_webflow.cpp unittest_apl_music_alarm.cpp + unittest_apl_attention_system.cpp unittest_activity_descriptor.cpp unittest_extension_lifecycle.cpp unittest_extension_message.cpp diff --git a/extensions/unit/unittest_apl_attention_system.cpp b/extensions/unit/unittest_apl_attention_system.cpp new file mode 100644 index 0000000..8074ad4 --- /dev/null +++ b/extensions/unit/unittest_apl_attention_system.cpp @@ -0,0 +1,496 @@ +/* + * Copyright 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 "alexaext/extensionmessage.h" +#include "alexaext/APLAttentionSystemExtension/AplAttentionSystemExtension.h" + +#include "gtest/gtest.h" + +using namespace alexaext; +using namespace alexaext::attention; +using namespace rapidjson; + + +// Inject the UUID generator so we can reproduce tests +static int uuidValue = 0; + +class TestAttentionSystemExtension : public AplAttentionSystemExtension { +public: + explicit TestAttentionSystemExtension() + : AplAttentionSystemExtension(Executor::getSynchronousExecutor(), testUuid) + {} + + void updateLiveData(const ActivityDescriptor& activity) { + AplAttentionSystemExtension::publishLiveData(activity); + } + + static std::string testUuid() { + return "AplAttentionSystemUuid-" + std::to_string(uuidValue); + } +}; + +class AplAttentionSystemExtensionTest : public ::testing::Test { +public: + + void SetUp() override + { + mExtension = std::make_shared(); + } + + static + std::shared_ptr createActivityDescriptor(std::string uri = "aplext:attentionsystem:10") { + auto session = SessionDescriptor::create(); + return ActivityDescriptor::create(uri, session, uri); + } + + /** + * Simple registration for testing event/command/data. + */ + ::testing::AssertionResult registerExtension(std::shared_ptr activity = createActivityDescriptor()) + { + Document settings(kObjectType); + settings.AddMember("attentionSystemStateName", Value("MyAttentionState"), settings.GetAllocator()); + + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10").settings(settings); + + auto registration = mExtension->createRegistration(*activity, regReq); + auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) + return testing::AssertionFailure() << "Failed Registration:" << method; + mClientToken = GetWithDefault(RegistrationSuccess::METHOD(), registration, ""); + if (mClientToken.length() == 0) + return testing::AssertionFailure() << "Failed Token:" << mClientToken; + + return ::testing::AssertionSuccess(); + } + + inline + ::testing::AssertionResult CheckLiveData(const Value &update, const std::string &operation, + const std::string &key) + { + using namespace testing; + + if (!update.IsObject()) + return AssertionFailure() << "Invalid json object" << update.GetType(); + + std::string op = GetWithDefault(LiveDataMapOperation::TYPE(), update, ""); + if (op != operation) + return AssertionFailure() << "Invalid operation - expected:" << operation << " actual:" << op; + + std::string kk = GetWithDefault(LiveDataMapOperation::KEY(), update, ""); + if (kk != key) + return AssertionFailure() << "Invalid key - expected:" << key << " actual:" << kk; + + return AssertionSuccess(); + } + + ::testing::AssertionResult CheckLiveData(const Value &update, const std::string &operation, + const std::string &key, const char *item) + { + using namespace testing; + + const auto &preCheck = CheckLiveData(update, operation, key); + if (!preCheck) + return preCheck; + + const rapidjson::Value *itm = LiveDataMapOperation::ITEM().Get(update); + if (!itm || !itm->IsString()) + return AssertionFailure() << "Invalid item type"; + + // string compare + const char *value = itm->GetString(); + if (std::strcmp(value, item) != 0) + return AssertionFailure() << "Invalid item - expected:" << item << " actual:" << value; + + return AssertionSuccess(); + } + + template + ::testing::AssertionResult CheckLiveData(const Value &update, const std::string &operation, + const std::string &key, T item) + { + + using namespace testing; + + const auto &preCheck = CheckLiveData(update, operation, key); + if (!preCheck) + return preCheck; + + const rapidjson::Value *itm = LiveDataMapOperation::ITEM().Get(update); + if (!itm || itm->IsNull() || !itm->Is()) + return AssertionFailure() << "Invalid item type"; + + T value = itm->Get(); + if (item != value) + return AssertionFailure() << "Invalid item - extected:" << item << " actual:" << value; + + return AssertionSuccess(); + } + + Value* findDataType (Value *types, const std::string& typeName) { + for (auto& v : types->GetArray()) { + auto name = GetWithDefault(TypePropertySchema::NAME(), v, ""); + if (typeName.compare(name) == 0) { + return &v; + } + } + return nullptr; + } + + inline + ::testing::AssertionResult IsEqual(const Value &lhs, const Value &rhs) + { + + if (lhs != rhs) { + return ::testing::AssertionFailure() << "Documents not equal\n" + << "lhs:\n" << AsPrettyString(lhs) + << "\nrhs:\n" << AsPrettyString(rhs) << "\n"; + } + return ::testing::AssertionSuccess(); + } + + std::shared_ptr mExtension; + std::string mClientToken; +}; + +/** + * Simple create test for sanity. + */ +TEST_F(AplAttentionSystemExtensionTest, CreateExtension) +{ + ASSERT_TRUE(mExtension); + auto supported = mExtension->getURIs(); + ASSERT_EQ(1, supported.size()); + ASSERT_NE(supported.end(), supported.find("aplext:attentionsystem:10")); +} + +/** + * Registration request with bad URI. + */ +TEST_F(AplAttentionSystemExtensionTest, RegistrationURIBad) +{ + Document regReq = RegistrationRequest("aplext:attentionsystem:BAD"); + auto activity = createActivityDescriptor("aplext:attentionsystem:BAD"); + + auto registration = mExtension->createRegistration(*activity, regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +/** + * Registration Success has required fields + */ +TEST_F(AplAttentionSystemExtensionTest, RegistrationSuccess) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10"); + auto activity = createActivityDescriptor(); + + auto registration = mExtension->createRegistration(*activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + ASSERT_STREQ("aplext:attentionsystem:10", + GetWithDefault(RegistrationSuccess::URI(), registration, "")); + auto schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + ASSERT_STREQ("aplext:attentionsystem:10", GetWithDefault("uri", *schema, "")); +} + +/** + * Environment registration has best practice of version + */ +TEST_F(AplAttentionSystemExtensionTest, RegistrationEnvironmentVersion) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10"); + auto activity = createActivityDescriptor(); + + auto registration = mExtension->createRegistration(*activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *environment = RegistrationSuccess::ENVIRONMENT().Get(registration); + ASSERT_TRUE(environment); + ASSERT_STREQ("APLAttentionSystemExtension-1.0", + GetWithDefault(Environment::VERSION(), *environment, "")); +} + +/** + * Events are defined + */ +TEST_F(AplAttentionSystemExtensionTest, RegistrationEvents) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10"); + auto activity = createActivityDescriptor(); + + auto registration = mExtension->createRegistration(*activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value *events = ExtensionSchema::EVENTS().Get(*schema); + ASSERT_TRUE(events); + + auto expectedHandlerSet = std::set(); + expectedHandlerSet.insert("OnAttentionStateChanged"); + ASSERT_TRUE(events->IsArray() && events->Size() == expectedHandlerSet.size()); + + // should have all event handlers defined + for (const Value &evt : events->GetArray()) { + ASSERT_TRUE(evt.IsObject()); + auto name = GetWithDefault(Event::NAME(), evt, "attentionState"); + ASSERT_TRUE(expectedHandlerSet.count(name) == 1); + expectedHandlerSet.erase(name); + } + ASSERT_TRUE(expectedHandlerSet.empty()); +} + +/** + * LiveData registration is not defined without settings. + */ +TEST_F(AplAttentionSystemExtensionTest, RegistrationSettingsEmpty) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10"); + auto activity = createActivityDescriptor(); + + auto registration = mExtension->createRegistration(*activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + // no live data + Value *liveData = ExtensionSchema::LIVE_DATA().Get(*schema); + ASSERT_TRUE(liveData); + ASSERT_TRUE(liveData->IsArray() && liveData->Empty()); +} + +/** + * LiveData registration is defined with settings. + */ +TEST_F(AplAttentionSystemExtensionTest, RegistrationSettingsHasLiveData) +{ + Document settings(kObjectType); + settings.AddMember("attentionSystemStateName", Value("MyAttentionState"), settings.GetAllocator()); + + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10").settings(settings); + auto activity = createActivityDescriptor(); + + auto registration = mExtension->createRegistration(*activity, regReq); + + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + // live data defined + Value *liveData = ExtensionSchema::LIVE_DATA().Get(*schema); + ASSERT_TRUE(liveData); + ASSERT_TRUE(liveData->IsArray() && liveData->Size() == 1); + + const Value& data = (*liveData)[0]; + ASSERT_TRUE(data.IsObject()); + auto name = GetWithDefault(LiveDataSchema::NAME(), data, ""); + ASSERT_STREQ("MyAttentionState", name); + auto type = GetWithDefault(LiveDataSchema::DATA_TYPE(), data, ""); + ASSERT_STREQ("AttentionSystemState", type); + + Value *types = ExtensionSchema::TYPES().Get(*schema); + ASSERT_TRUE(types); + ASSERT_TRUE(liveData->IsArray()); + + Value *stateType = findDataType(types, "AttentionSystemState"); + ASSERT_NE(nullptr, stateType); + ASSERT_TRUE(stateType->IsObject()); + + rapidjson::Document expected; + expected.Parse(R"( + { + "name": "AttentionSystemState", + "properties": { + "attentionState": "string" + } + } + )"); + ASSERT_FALSE(expected.HasParseError()); + ASSERT_TRUE(IsEqual(expected, *stateType)); + +} + +/** +* Invalid settings on registration are handled and defaults are used. +**/ +TEST_F(AplAttentionSystemExtensionTest, RegistrationSettingsBad) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:attentionsystem:10").settings(Value()); + auto activity = createActivityDescriptor(); + + auto registration = mExtension->createRegistration(*activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + // live data available + Value *liveData = ExtensionSchema::LIVE_DATA().Get(*schema); + ASSERT_TRUE(liveData); + ASSERT_TRUE(liveData->IsArray() && liveData->Empty()); +} + +/** + * LiveData is published when settings assigned. + */ +TEST_F(AplAttentionSystemExtensionTest, GetLiveDataObjectsSuccess) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + bool gotUpdate = false; + mExtension->registerLiveDataUpdateCallback( + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + gotUpdate = true; + ASSERT_STREQ("LiveDataUpdate", + GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); + const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); + ASSERT_TRUE(ops); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 1); + ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "attentionState", "IDLE")); + }); + + mExtension->updateLiveData(*activity); + ASSERT_TRUE(gotUpdate); +} + +/** + * Attention state change updates live data. + */ +TEST_F(AplAttentionSystemExtensionTest, UpdateAttentionState) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + bool gotUpdate = false; + mExtension->registerLiveDataUpdateCallback( + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + gotUpdate = true; + ASSERT_STREQ("LiveDataUpdate", + GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); + ASSERT_STREQ("aplext:attentionsystem:10", + GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); + const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); + ASSERT_TRUE(ops); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 1); + ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "attentionState", "LISTENING")); + }); + + mExtension->updateAttentionState(AttentionState::LISTENING); + ASSERT_TRUE(gotUpdate); +} + +/** + * Extension instance can handle multiple concurrent activities. + */ +TEST_F(AplAttentionSystemExtensionTest, MultipleActivitiesLiveData) +{ + // Set up two activities with different attentionSystemStateName values + Document settings1(kObjectType); + settings1.AddMember("attentionSystemStateName", Value("FirstAttentionState"), settings1.GetAllocator()); + Document settings2(kObjectType); + settings2.AddMember("attentionSystemStateName", Value("SecondAttentionState"), settings2.GetAllocator()); + + Document regReq1 = RegistrationRequest("1.0").uri("aplext:attentionsystem:10").settings(settings1); + auto activity1 = createActivityDescriptor(); + + Document regReq2 = RegistrationRequest("1.0").uri("aplext:attentionsystem:10").settings(settings2); + auto activity2 = createActivityDescriptor(); + + auto registration1 = mExtension->createRegistration(*activity1, regReq1); + auto registration2 = mExtension->createRegistration(*activity2, regReq2); + + ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration1, "")); + Value *schema1 = RegistrationSuccess::SCHEMA().Get(registration1); + ASSERT_TRUE(schema1); + + ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration2, "")); + Value *schema2 = RegistrationSuccess::SCHEMA().Get(registration2); + ASSERT_TRUE(schema2); + + // Check LiveData schema exists and matches the correct name + Value *liveData1 = ExtensionSchema::LIVE_DATA().Get(*schema1); + const Value& data1 = (*liveData1)[0]; + ASSERT_TRUE(data1.IsObject()); + + auto name1 = GetWithDefault(LiveDataSchema::NAME(), data1, ""); + ASSERT_STREQ("FirstAttentionState", name1); + + Value *liveData2 = ExtensionSchema::LIVE_DATA().Get(*schema2); + const Value& data2 = (*liveData2)[0]; + ASSERT_TRUE(data2.IsObject()); + + auto name2 = GetWithDefault(LiveDataSchema::NAME(), data2, ""); + ASSERT_STREQ("SecondAttentionState", name2); + + // Update attention state and see both are updated + bool gotUpdate1 = false; + bool gotUpdate2 = false; + mExtension->registerLiveDataUpdateCallback( + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + if (activity == *activity1) { + gotUpdate1 = true; + } else if (activity == *activity2) { + gotUpdate2 = true; + } + + ASSERT_STREQ("LiveDataUpdate", + GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); + ASSERT_STREQ("aplext:attentionsystem:10", + GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); + const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); + ASSERT_TRUE(ops); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 1); + ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "attentionState", "THINKING")); + }); + + mExtension->updateAttentionState(AttentionState::THINKING); + ASSERT_TRUE(gotUpdate1); + ASSERT_TRUE(gotUpdate2); +} + +/** + * Once an activity is unregistered, ensure it does not get new updates. + */ +TEST_F(AplAttentionSystemExtensionTest, StateDoesNotUpdateAfterUnregister) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + int numUpdates = 0; + mExtension->registerLiveDataUpdateCallback( + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + numUpdates++; + ASSERT_STREQ("LiveDataUpdate", + GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); + ASSERT_STREQ("aplext:attentionsystem:10", + GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); + }); + + mExtension->updateAttentionState(AttentionState::LISTENING); + mExtension->updateAttentionState(AttentionState::SPEAKING); + ASSERT_EQ(numUpdates, 2); + + mExtension->onActivityUnregistered(*activity); + mExtension->updateAttentionState(AttentionState::THINKING); + + // should not have updated again + ASSERT_EQ(numUpdates, 2); +} diff --git a/extensions/unit/unittest_apl_audio_player.cpp b/extensions/unit/unittest_apl_audio_player.cpp index 3ecdb82..3a50eaa 100644 --- a/extensions/unit/unittest_apl_audio_player.cpp +++ b/extensions/unit/unittest_apl_audio_player.cpp @@ -93,17 +93,30 @@ class AplAudioPlayerExtensionTest : public ::testing::Test { mExtension = std::make_shared(mObserver); } + /** + * Simple utility to create activity descriptors accross the tests + */ + ActivityDescriptor createActivityDescriptor(std::string uri = URI) { + // Create Activity + SessionDescriptorPtr sessionPtr = SessionDescriptor::create("TestSessionId"); + ActivityDescriptor activityDescriptor( + uri, + sessionPtr); + return activityDescriptor; + } + /** * Simple registration for testing event/command/data. */ - ::testing::AssertionResult registerExtension() + ::testing::AssertionResult registerExtension(const alexaext::ActivityDescriptor& activity) { Document settings(kObjectType); settings.AddMember("playbackStateName", Value("MyPlayBackState"), settings.GetAllocator()); - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10") + Document regReq = RegistrationRequest("1.0").uri(URI) .settings(settings); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto registration = mExtension->createRegistration(activity, regReq); + mExtension->onActivityRegistered(activity); auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); if (std::strcmp("RegisterSuccess", method) != 0) return testing::AssertionFailure() << "Failed Registration:" << method; @@ -213,7 +226,7 @@ TEST_F(AplAudioPlayerExtensionTest, CreateExtension) ASSERT_TRUE(mExtension); auto supported = mExtension->getURIs(); ASSERT_EQ(1, supported.size()); - ASSERT_NE(supported.end(), supported.find("aplext:audioplayer:10")); + ASSERT_NE(supported.end(), supported.find(URI)); } /** @@ -221,8 +234,10 @@ TEST_F(AplAudioPlayerExtensionTest, CreateExtension) */ TEST_F(AplAudioPlayerExtensionTest, RegistrationURIBad) { + SessionDescriptorPtr sessionPtr = SessionDescriptor::create("TestSessionId"); + ActivityDescriptor badActivity("aplext:audioplayer:BAD", sessionPtr); Document regReq = RegistrationRequest("aplext:audioplayer:BAD"); - auto registration = mExtension->createRegistration("aplext:audioplayer:BAD", regReq); + auto registration = mExtension->createRegistration(badActivity, regReq); ASSERT_FALSE(registration.HasParseError()); ASSERT_FALSE(registration.IsNull()); ASSERT_STREQ("RegisterFailure", @@ -234,8 +249,9 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationURIBad) */ TEST_F(AplAudioPlayerExtensionTest, RegistrationSuccess) { - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10"); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); ASSERT_STREQ("aplext:audioplayer:10", @@ -252,8 +268,9 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationSuccess) */ TEST_F(AplAudioPlayerExtensionTest, RegistrationEnvironmentVersion) { - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10"); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value *environment = RegistrationSuccess::ENVIRONMENT().Get(registration); @@ -267,9 +284,9 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationEnvironmentVersion) */ TEST_F(AplAudioPlayerExtensionTest, RegistrationCommands) { - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10"); - - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value *schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -306,8 +323,9 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationCommands) */ TEST_F(AplAudioPlayerExtensionTest, RegistrationEvents) { - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10"); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value *schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -336,8 +354,9 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationEvents) */ TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsEmpty) { - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10"); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value *schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -357,9 +376,10 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsHasLiveData) Document settings(kObjectType); settings.AddMember("playbackStateName", Value("MyPlayBackState"), settings.GetAllocator()); - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10") + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI) .settings(settings); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value *schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -405,9 +425,10 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsHasLiveData) **/ TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsBad) { - Document regReq = RegistrationRequest("1.0").uri("aplext:audioplayer:10") + auto activity = createActivityDescriptor(); + Document regReq = RegistrationRequest("1.0").uri(URI) .settings(Value()); - auto registration = mExtension->createRegistration("aplext:audioplayer:10", regReq); + auto registration = mExtension->createRegistration(activity, regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value *schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -423,11 +444,12 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsBad) */ TEST_F(AplAudioPlayerExtensionTest, GetLiveDataObjectsSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); bool gotUpdate = false; mExtension->registerLiveDataUpdateCallback( - [&](const std::string &uri, const rapidjson::Value &liveDataUpdate) { + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { gotUpdate = true; ASSERT_STREQ("LiveDataUpdate", GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); @@ -446,12 +468,13 @@ TEST_F(AplAudioPlayerExtensionTest, GetLiveDataObjectsSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandPlaySuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Play"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("PLAY", mObserver->mCommand); } @@ -461,12 +484,13 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandPlaySuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandPauseSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Pause"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("PAUSE", mObserver->mCommand); } @@ -476,12 +500,13 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandPauseSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandPreviousSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Previous"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("PREVIOUS", mObserver->mCommand); } @@ -491,12 +516,13 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandPreviousSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandNextSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Next"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("NEXT", mObserver->mCommand); } @@ -506,12 +532,13 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandNextSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionMissingParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("SeekToPosition"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); ASSERT_EQ("", mObserver->mCommand); } @@ -521,13 +548,14 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionMissingParamFailu */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionBadParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("SeekToPosition") .property("offset", "wrong"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); ASSERT_EQ("", mObserver->mCommand); } @@ -537,26 +565,27 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionBadParamFailure) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("SeekToPosition") .property("offset", 42); bool gotUpdate = false; mExtension->registerLiveDataUpdateCallback( - [&](const std::string &uri, const rapidjson::Value &liveDataUpdate) { + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { gotUpdate = true; ASSERT_STREQ("LiveDataUpdate", GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); - ASSERT_STREQ("aplext:audioplayer:10", - GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); + ASSERT_STREQ("aplext:audioplayer:10", + GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); ASSERT_TRUE(ops->IsArray() && ops->Size() == 2); ASSERT_TRUE(CheckLiveData(ops->GetArray()[1], "Set", "offset", 42)); }); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(gotUpdate); ASSERT_TRUE(invoke); @@ -569,12 +598,13 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSkipForwardSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("SkipForward"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("FORWARD", mObserver->mCommand); } @@ -584,12 +614,13 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSkipForwardSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSkipBackwardSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("SkipBackward"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("BACKWARD", mObserver->mCommand); } @@ -599,23 +630,24 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSkipBackwardSuccess) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandToggleMissingParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); // missing checked param auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Toggle") .property("name", "value"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); ASSERT_EQ("", mObserver->mCommand); // missing name param command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Toggle") .property("checked", true); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); ASSERT_EQ("", mObserver->mCommand); } @@ -625,25 +657,26 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandToggleMissingParamFailure) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandToggleBadParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); // missing checked param auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Toggle") .property("name", 0) .property("checked", true); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); ASSERT_EQ("", mObserver->mCommand); // missing name param command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Toggle") .property("name", "thumbsUp") .property("checked", -10); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); ASSERT_EQ("", mObserver->mCommand); } @@ -653,14 +686,15 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandToggleBadParamFailure) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandToggleSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("Toggle") .property("name", "thumbsUp") .property("checked", true); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); ASSERT_EQ("TOGGLE", mObserver->mCommand); ASSERT_EQ("thumbsUp", mObserver->mParamString); @@ -689,7 +723,8 @@ const char *LINES = R"( */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedMissingParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); Document lines; lines.Parse(LINES); @@ -697,18 +732,18 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedMissingParamFail // missing "lines" auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("token", "SONG-TOKEN"); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); // missing "token" command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("lines", lines["lines"].Move()); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); } @@ -717,7 +752,8 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedMissingParamFail */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedBadParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); Document lines; lines.Parse(LINES); @@ -725,11 +761,11 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedBadParamFailure) // bad token auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("token", "") .property("lines", lines["lines"]); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); // bad lines param handled in folllow-on test @@ -740,26 +776,27 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedBadParamFailure) */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsViewedSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); Document lines; lines.Parse(LINES); ASSERT_FALSE(lines.HasParseError()); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("token", "SONG-TOKEN") .property("lines", lines["lines"]); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); - // verify line data - auto data = mExtension->getActiveLyricsViewedData()->lyricData; - ASSERT_TRUE(data->IsArray()); - ASSERT_EQ(2, data->Size()); + mExtension->onActivityUnregistered(activity); lines.Parse(LINES); - ASSERT_TRUE(IsEqual(lines["lines"], *data)); + // Observer is notified of flush. + ASSERT_EQ("FLUSHED", mObserver->mCommand); + ASSERT_EQ(AsString(lines["lines"]), mObserver->mParamJson); + ASSERT_EQ(0, mObserver->mParaNum); } // Invalid line data in additon to valid data from LINES @@ -804,86 +841,145 @@ const char *BAD_LINES = R"( */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsIgnoreBadLines) { - ASSERT_TRUE(registerExtension()); - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); Document lines; lines.Parse(BAD_LINES); ASSERT_FALSE(lines.HasParseError()); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("token", "SONG-TOKEN") .property("lines", lines["lines"]); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); - // verify only good line data recorded - auto data = mExtension->getActiveLyricsViewedData()->lyricData; - ASSERT_TRUE(data->IsArray()); - ASSERT_EQ(2, data->Size()); lines.Parse(LINES); - ASSERT_TRUE(IsEqual(lines["lines"], *data)); + // verify only good line data recorded + mExtension->onActivityUnregistered(activity); + ASSERT_EQ("FLUSHED", mObserver->mCommand); + ASSERT_EQ(AsString(lines["lines"]), mObserver->mParamJson); + ASSERT_EQ(0, mObserver->mParaNum); } TEST_F(AplAudioPlayerExtensionTest, InvokeFlushLyricsSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); Document lines; lines.Parse(LINES); ASSERT_FALSE(lines.HasParseError()); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("token", "SONG-TOKEN") .property("lines", lines["lines"]); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); - // verify line data - auto data = mExtension->getActiveLyricsViewedData()->lyricData; - ASSERT_TRUE(data->IsArray()); - ASSERT_EQ(2, data->Size()); - lines.Parse(LINES); - ASSERT_TRUE(IsEqual(lines["lines"], *data)); // Flush data command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("FlushLyricData") .property("token", "SONG-TOKEN"); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); + lines.Parse(LINES); // Observer is notified of flush. ASSERT_EQ("FLUSHED", mObserver->mCommand); ASSERT_EQ(AsString(lines["lines"]), mObserver->mParamJson); ASSERT_EQ(0, mObserver->mParaNum); } +TEST_F(AplAudioPlayerExtensionTest, ShouldFlushLyricsOnUnregister) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + Document lines; + lines.Parse(LINES); + ASSERT_FALSE(lines.HasParseError()); + + auto command = Command("1.0").target(mClientToken) + .uri(URI) + .name("AddLyricsViewed") + .property("token", "SONG-TOKEN") + .property("lines", lines["lines"]); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + // Unregister activity + mExtension->onActivityUnregistered(activity); + + lines.Parse(LINES); + // Observer is notified of flush. + ASSERT_EQ("FLUSHED", mObserver->mCommand); + ASSERT_EQ(AsString(lines["lines"]), mObserver->mParamJson); + ASSERT_EQ(0, mObserver->mParaNum); +} + +TEST_F(AplAudioPlayerExtensionTest, ShouldFlushLyricsOnTokenChange) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + Document lines; + lines.Parse(LINES); + ASSERT_FALSE(lines.HasParseError()); + + auto command = Command("1.0").target(mClientToken) + .uri(URI) + .name("AddLyricsViewed") + .property("token", "SONG-TOKEN") + .property("lines", lines["lines"]); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + // Changing token causes lyrics to get flushed + lines.Parse(LINES); + auto newCommand = Command("1.0").target(mClientToken) + .uri(URI) + .name("AddLyricsViewed") + .property("token", "OTHER_TOKEN") + .property("lines", lines["lines"]); + invoke = mExtension->invokeCommand(activity, newCommand); + ASSERT_TRUE(invoke); + + lines.Parse(LINES); + // Observer is notified of flush. + ASSERT_EQ("FLUSHED", mObserver->mCommand); + ASSERT_EQ(AsString(lines["lines"]), mObserver->mParamJson); + ASSERT_EQ(0, mObserver->mParaNum); +} + + /** * Command AddLyricsDurationInMilliseconds handles missing params and properly fails. */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMillisecondsMissingParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); // missing token auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsDurationInMilliseconds") .property("durationInMilliseconds", 100); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); // missing duration command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsDurationInMilliseconds") .property("token", "SONG-TOKEN"); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); } @@ -892,24 +988,25 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMilliseconds */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMillisecondsBadParamFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); // bad token auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsDurationInMilliseconds") .property("token", "") .property("durationInMilliseconds", 100); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); // bad duration command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsDurationInMilliseconds") .property("token", "SONG-TOKEN") .property("durationInMilliseconds", -1); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_FALSE(invoke); } @@ -918,41 +1015,37 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMilliseconds */ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMillisecondsSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); Document lines; lines.Parse(LINES); ASSERT_FALSE(lines.HasParseError()); auto command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsViewed") .property("token", "SONG-TOKEN") .property("lines", lines["lines"]); - auto invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + auto invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); - // verify line data - auto data = mExtension->getActiveLyricsViewedData()->lyricData; - ASSERT_TRUE(data->IsArray()); - ASSERT_EQ(2, data->Size()); lines.Parse(LINES); - ASSERT_TRUE(IsEqual(lines["lines"], *data)); // add duration command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("AddLyricsDurationInMilliseconds") .property("token", "SONG-TOKEN") .property("durationInMilliseconds", 53); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); // Flush data command = Command("1.0").target(mClientToken) - .uri("aplext:audioplayer:10") + .uri(URI) .name("FlushLyricData") .property("token", "SONG-TOKEN"); - invoke = mExtension->invokeCommand("aplext:audioplayer:10", command); + invoke = mExtension->invokeCommand(activity, command); ASSERT_TRUE(invoke); @@ -967,15 +1060,16 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMilliseconds */ TEST_F(AplAudioPlayerExtensionTest, UpdatePlaybackProgressSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); bool gotUpdate = false; mExtension->registerLiveDataUpdateCallback( - [&](const std::string &uri, const rapidjson::Value &liveDataUpdate) { + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { gotUpdate = true; ASSERT_STREQ("LiveDataUpdate", GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); - ASSERT_STREQ("aplext:audioplayer:10", + ASSERT_STREQ("aplext:audioplayer:10", GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); @@ -993,15 +1087,16 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlaybackProgressSuccess) */ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityLiveDataSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); bool gotUpdate = false; mExtension->registerLiveDataUpdateCallback( - [&](const std::string &uri, const rapidjson::Value &liveDataUpdate) { + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { gotUpdate = true; ASSERT_STREQ("LiveDataUpdate", GetWithDefault(LiveDataUpdate::METHOD(), liveDataUpdate, "")); - ASSERT_STREQ("aplext:audioplayer:10", + ASSERT_STREQ("aplext:audioplayer:10", GetWithDefault(LiveDataUpdate::TARGET(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); @@ -1019,21 +1114,22 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityLiveDataSuccess) */ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityEventSuccess) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); bool gotUpdate = false; mExtension->registerEventCallback( [&](const std::string &uri, const rapidjson::Value &event) { - gotUpdate = true; - ASSERT_STREQ("Event", - GetWithDefault(Event::METHOD(), event, "")); - ASSERT_STREQ("aplext:audioplayer:10", - GetWithDefault(Event::TARGET(), event, "")); - auto payload = Event::PAYLOAD().Get(event); - ASSERT_STREQ("PLAYING", - GetWithDefault("playerActivity", payload, "")); - ASSERT_EQ(100, - GetWithDefault("offset", payload, -1)); + gotUpdate = true; + ASSERT_STREQ("Event", + GetWithDefault(Event::METHOD(), event, "")); + ASSERT_STREQ("aplext:audioplayer:10", + GetWithDefault(Event::TARGET(), event, "")); + auto payload = Event::PAYLOAD().Get(event); + ASSERT_STREQ("PLAYING", + GetWithDefault("playerActivity", payload, "")); + ASSERT_EQ(100, + GetWithDefault("offset", payload, -1)); }); mExtension->updatePlayerActivity("PLAYING", 100); @@ -1045,11 +1141,12 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityEventSuccess) */ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityFailure) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); bool gotUpdate = false; mExtension->registerLiveDataUpdateCallback( - [&](const std::string &uri, const rapidjson::Value &liveDataUpdate) { + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { gotUpdate = true; }); @@ -1066,11 +1163,12 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityFailure) */ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityStateChange) { - ASSERT_TRUE(registerExtension()); + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); int updateCount = 0; mExtension->registerLiveDataUpdateCallback( - [&](const std::string &uri, const rapidjson::Value &liveDataUpdate) { + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { updateCount++; }); diff --git a/extensions/unit/unittest_apl_metric.cpp b/extensions/unit/unittest_apl_metric.cpp new file mode 100644 index 0000000..b89bf47 --- /dev/null +++ b/extensions/unit/unittest_apl_metric.cpp @@ -0,0 +1,625 @@ +/* + * Copyright 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 "gtest/gtest.h" +#include + +#include "alexaext/APLMetricsExtension/AplMetricsExtension.h" +#include "alexaext/extensionmessage.h" + +using namespace alexaext; +using namespace alexaext::metrics; +using namespace rapidjson; + +// Metric Commands +enum MetricCommand { + NONE, + RECORD_COUNTER, + RECORD_TIMER +}; + +static const char* METRIC_ID = "metricId"; +static const char* AMOUNT = "amount"; + +static const int MAX_METRIC_ID_ALLOWED = 5; + +class TestMetricObserver : public AplMetricsExtensionObserverInterface { +public: + TestMetricObserver() : AplMetricsExtensionObserverInterface(10) {} + + bool recordCounter(const std::string& applicationId, const std::string& experienceId, + const std::string& metricId, const int amount) override { + mCommand = RECORD_COUNTER; + mRecordedCounter = amount; + return true; + } + + bool recordTimer(const std::string& applicationId, + const std::string& experienceId, + const std::string& metricId, + const std::chrono::milliseconds& duration) override { + mCommand = RECORD_TIMER; + return true; + } + + MetricCommand mCommand = NONE; + int mRecordedCounter = 0; +}; + +class AplMetricsExtensionTest : public ::testing::Test { +public: + void SetUp() override { + mObserver = std::make_shared(); + mExtension = std::make_shared(mObserver, Executor::getSynchronousExecutor(), MAX_METRIC_ID_ALLOWED); + } + + /** + * Simple registration for testing event/command/data. + */ + ::testing::AssertionResult registerExtension(const alexaext::ActivityDescriptor& activity) + { + Document metricsSettings(kObjectType); + metricsSettings.AddMember("applicationId", "TestApplication", metricsSettings.GetAllocator()); + metricsSettings.AddMember("experienceId", "TestExperience", metricsSettings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(metricsSettings); + auto registration = mExtension->createRegistration(activity, regReq); + auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) + return testing::AssertionFailure() << "Registration failed:" << method; + + return ::testing::AssertionSuccess(); + } + + /** + * Simple utility to create activity descriptors accross the tests + */ + ActivityDescriptor createActivityDescriptor(std::string uri = URI) { + // Create Activity + SessionDescriptorPtr sessionPtr = SessionDescriptor::create("TestSessionId"); + ActivityDescriptor activityDescriptor( + uri, + sessionPtr); + return activityDescriptor; + } + + /** + * Create activity descriptors with a specific session + */ + ActivityDescriptor createActivityDescriptor(SessionDescriptorPtr session, + std::string uri = URI) { + ActivityDescriptor activityDescriptor( + uri, + session); + return activityDescriptor; + } + + std::shared_ptr mObserver; + std::shared_ptr mExtension; +}; + + +TEST_F(AplMetricsExtensionTest, RegistrationTest) +{ + Document settings(kObjectType); + settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); + settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + + auto activity = createActivityDescriptor(); + auto registration = mExtension->createRegistration(activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + ASSERT_STREQ(URI.c_str(), + GetWithDefault(RegistrationSuccess::URI(), registration, "")); + std::string token = GetWithDefault(RegistrationSuccess::TOKEN(), registration, ""); + ASSERT_EQ("",token); + + // Validate that fails to register again with same + registration = mExtension->createRegistration(activity, regReq); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, InvalidURI) +{ + Document regReq = RegistrationRequest("aplext:metrics:INVALID"); + auto registration = mExtension->createRegistration(createActivityDescriptor("aplext:metrics:INVALID"), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, RegistrationWithoutSettings) { + Document regReq = RegistrationRequest(URI); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, RegistrationWithoutApplicationId) { + Document settings(kObjectType); + settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, RegistrationWithoutEmptyApplicationId) { + Document settings(kObjectType); + settings.AddMember("applicationId", "", settings.GetAllocator()); + settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, RegistrationWithoutExperienceId) { + Document settings(kObjectType); + settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, RegistrationWithEmptyExperienceId) { + Document settings(kObjectType); + settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); + settings.AddMember("experienceId", "", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +TEST_F(AplMetricsExtensionTest, RegistrationCommands) +{ + Document settings(kObjectType); + settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); + settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value *commands = ExtensionSchema::COMMANDS().Get(*schema); + ASSERT_TRUE(commands); + + auto expectedCommandSet = std::set(); + expectedCommandSet.insert("IncrementCounter"); + expectedCommandSet.insert("StartTimer"); + expectedCommandSet.insert("StopTimer"); + ASSERT_TRUE(commands->IsArray() && commands->Size() == expectedCommandSet.size()); + + for (const Value &com : commands->GetArray()) { + ASSERT_TRUE(com.IsObject()); + auto name = GetWithDefault(Command::NAME(), com, "MissingName"); + ASSERT_TRUE(expectedCommandSet.count(name) == 1) << "Unknown Command:" << name; + expectedCommandSet.erase(name); + } + ASSERT_TRUE(expectedCommandSet.empty()); +} + +TEST_F(AplMetricsExtensionTest, RegistrationEvents) +{ + Document settings(kObjectType); + settings.AddMember("applicationId", "TestApplication", settings.GetAllocator()); + settings.AddMember("experienceId", "TestExperience", settings.GetAllocator()); + Document regReq = RegistrationRequest("1.0") + .uri(URI) + .settings(settings); + + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value *events = ExtensionSchema::EVENTS().Get(*schema); + ASSERT_TRUE(events); + + ASSERT_TRUE(events->IsArray() && events->Size() == 0); +} + +TEST_F(AplMetricsExtensionTest, TestCommands) { + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + mObserver->mCommand = NONE; + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 3); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + // Test IncrementCounter without amount property + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); + + command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(RECORD_TIMER, mObserver->mCommand); + + mExtension->onSessionEnded(*activity.getSession()); + ASSERT_EQ(4, mObserver->mRecordedCounter); + ASSERT_EQ(RECORD_COUNTER, mObserver->mCommand); +} + +TEST_F(AplMetricsExtensionTest, TestMetricIdLimit) { + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + for (int i = 0; i < MAX_METRIC_ID_ALLOWED; i++) { + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId" + std::to_string(i)) + .property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + } + + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "NewTestId") + .property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "NewTestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); +} + +TEST_F(AplMetricsExtensionTest, TestCommandsWithInvalidActivity) { + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + mObserver->mCommand = NONE; + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId"); + auto session = SessionDescriptor::create("TestSessionId"); + auto invalidActivity = createActivityDescriptor(session, "aplext:metrics:INVALID"); + auto invoke = mExtension->invokeCommand(invalidActivity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(invalidActivity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(invalidActivity, command); + ASSERT_FALSE(invoke); +} + +TEST_F(AplMetricsExtensionTest, TestCommandsWithInvalidSession) { + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + mObserver->mCommand = NONE; + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId"); + auto session = SessionDescriptor::create("Session1"); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(session), command); + ASSERT_FALSE(invoke); +} + +TEST_F(AplMetricsExtensionTest, TestInvalidCommands) { + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + // Invalid command name + auto command = Command("1.0") + .uri(URI) + .name("InvalidCommand") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + // MetricId property missing + command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(AMOUNT, 1); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .name("StartTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .name("StopTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + // MetricId is empty + command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "") + .property(AMOUNT, 1); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .property(METRIC_ID, "") + .name("StartTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + command = Command("1.0") + .uri(URI) + .property(METRIC_ID, "") + .name("StopTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + +} + +TEST_F(AplMetricsExtensionTest, TestTimerMetricCommand) { + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + // Test stop without start + mObserver->mCommand = NONE; + auto command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId"); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); + + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); + + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(RECORD_TIMER, mObserver->mCommand); + + // Stop again + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); + + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); +} + +TEST_F(AplMetricsExtensionTest, TestTimerMetricWithinSession) { + auto session1 = SessionDescriptor::create("Session1"); + auto activity1 = createActivityDescriptor(session1); + ASSERT_TRUE(registerExtension(activity1)); + + mObserver->mCommand = NONE; + auto command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "TestId1"); + auto invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); + + // Register another activity with same session + auto activity2 = createActivityDescriptor(session1); + ASSERT_TRUE(registerExtension(activity2)); + + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId1"); + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(RECORD_TIMER, mObserver->mCommand); + + // Start another timer in activity1 + mObserver->mCommand = NONE; + command = Command("1.0") + .uri(URI) + .name("StartTimer") + .property(METRIC_ID, "TestId2"); + invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); + + // Register another activity with different session and try to stop timer + auto activity3 = createActivityDescriptor(SessionDescriptor::create("Session2")); + ASSERT_TRUE(registerExtension(activity3)); + command = Command("1.0") + .uri(URI) + .name("StopTimer") + .property(METRIC_ID, "TestId2"); + invoke = mExtension->invokeCommand(activity3, command); + ASSERT_FALSE(invoke); + ASSERT_EQ(NONE, mObserver->mCommand); +} + +TEST_F(AplMetricsExtensionTest, TestCounterMetricWithinSession) { + auto session1 = SessionDescriptor::create("Session1"); + auto activity1 = createActivityDescriptor(session1); + ASSERT_TRUE(registerExtension(activity1)); + + // Increment counter in activity1 + mObserver->mCommand = NONE; + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId"); + auto invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + + // Register activity2 with same session + auto activity2 = createActivityDescriptor(session1); + ASSERT_TRUE(registerExtension(activity2)); + + // Increment counter in activity2 + command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + + // Increment counter again in activity1 by amount 2. + command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 2); + invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + + // Register another activity with different session and increment counter + auto session2 = SessionDescriptor::create("Session2"); + auto activity3 = createActivityDescriptor(session2); + ASSERT_TRUE(registerExtension(activity3)); + command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 10); + invoke = mExtension->invokeCommand(activity3, command); + ASSERT_TRUE(invoke); + + // Observer not invoked before session end + ASSERT_EQ(NONE, mObserver->mCommand); + + // End session1 + mExtension->onSessionEnded(*session1); + ASSERT_EQ(RECORD_COUNTER, mObserver->mCommand); + ASSERT_EQ(4, mObserver->mRecordedCounter); + + mObserver->mCommand = NONE; + + // End session2 + mExtension->onSessionEnded(*session2); + ASSERT_EQ(RECORD_COUNTER, mObserver->mCommand); + ASSERT_EQ(10, mObserver->mRecordedCounter); +} + +TEST_F(AplMetricsExtensionTest, TestCommandWithInvalidActivity) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + auto command = Command("1.0") + .uri(URI) + .name("IncrementCounter") + .property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(), command); + ASSERT_FALSE(invoke); +} + +TEST_F(AplMetricsExtensionTest, TestCommandWithInvalidURI) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + auto command = Command("1.0") + .uri("aplext:metrics:INVALID") + .name("IncrementCounter") + .property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); +} \ No newline at end of file diff --git a/extensions/unit/unittest_apl_webflow.cpp b/extensions/unit/unittest_apl_webflow.cpp index d6857b6..1f4fd91 100644 --- a/extensions/unit/unittest_apl_webflow.cpp +++ b/extensions/unit/unittest_apl_webflow.cpp @@ -26,7 +26,7 @@ #include "alexaext/extensionmessage.h" using namespace alexaext; -using namespace Webflow; +using namespace alexaext::webflow; using namespace rapidjson; static int uuidValue = 1; @@ -42,7 +42,7 @@ class SimpleTestWebflowObserver : public AplWebflowExtensionObserverInterface { ~SimpleTestWebflowObserver() override = default; - void onStartFlow(const std::string& token, const std::string& url, const std::string& flowId, + void onStartFlow(const ActivityDescriptor &activity, const std::string& token, const std::string& url, const std::string& flowId, std::function onFlowEndEvent) override { mCommand = "START_FLOW"; mUrl = url; @@ -64,6 +64,31 @@ class SimpleTestWebflowObserver : public AplWebflowExtensionObserverInterface { std::string mToken; }; +class SimpleLifecycleTestWebflowObserver : public AplWebflowExtensionObserverInterface { +public: + SimpleLifecycleTestWebflowObserver() : AplWebflowExtensionObserverInterface() {} + + ~SimpleLifecycleTestWebflowObserver() override = default; + + // no-op + void onStartFlow(const ActivityDescriptor &activity, const std::string& token, const std::string& url, const std::string& flowId, + std::function onFlowEndEvent) override {} + + void onForeground(const ActivityDescriptor &activity) override { + mLifecycleState = "FOREGROUND"; + } + + void onBackground(const ActivityDescriptor &activity) override { + mLifecycleState = "BACKGROUND"; + } + + void onHidden(const ActivityDescriptor &activity) override { + mLifecycleState = "HIDDEN"; + } + + std::string mLifecycleState = "CREATED"; +}; + class SimpleTestWebflowExtension : public AplWebflowExtension { public: explicit SimpleTestWebflowExtension( @@ -100,7 +125,7 @@ class SimpleAplWebflowExtensionTest : public ::testing::Test { */ ::testing::AssertionResult registerExtension() { Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); - auto registration = mExtension->createRegistration("aplext:webflow:10", regReq); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); if (std::strcmp("RegisterSuccess", method) != 0) @@ -112,6 +137,18 @@ class SimpleAplWebflowExtensionTest : public ::testing::Test { return ::testing::AssertionSuccess(); } + /** + * Simple utility to create activity descriptors accross the tests + */ + ActivityDescriptor createActivityDescriptor(std::string uri = URI) { + // Create Activity + SessionDescriptorPtr sessionPtr = SessionDescriptor::create("TestSessionId"); + ActivityDescriptor activityDescriptor( + uri, + sessionPtr); + return activityDescriptor; + } + void resetTestData() { mClientToken.clear(); mEventFlow.clear(); @@ -141,7 +178,7 @@ TEST_F(SimpleAplWebflowExtensionTest, CreateExtension) { */ TEST_F(SimpleAplWebflowExtensionTest, RegistrationURIBad) { Document regReq = RegistrationRequest("aplext:webflow:BAD"); - auto registration = mExtension->createRegistration("aplext:webflow:BAD", regReq); + auto registration = mExtension->createRegistration(createActivityDescriptor("aplext:webflow:BAD"), regReq); ASSERT_FALSE(registration.HasParseError()); ASSERT_FALSE(registration.IsNull()); ASSERT_STREQ("RegisterFailure", @@ -153,7 +190,7 @@ TEST_F(SimpleAplWebflowExtensionTest, RegistrationURIBad) { */ TEST_F(SimpleAplWebflowExtensionTest, RegistrationSuccess) { Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); - auto registration = mExtension->createRegistration("aplext:webflow:10", regReq); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); ASSERT_STREQ("aplext:webflow:10", @@ -171,7 +208,7 @@ TEST_F(SimpleAplWebflowExtensionTest, RegistrationSuccess) { TEST_F(SimpleAplWebflowExtensionTest, RegistrationCommands) { Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); - auto registration = mExtension->createRegistration("aplext:webflow:10", regReq); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value* schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -198,7 +235,7 @@ TEST_F(SimpleAplWebflowExtensionTest, RegistrationCommands) { */ TEST_F(SimpleAplWebflowExtensionTest, RegistrationEvents) { Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); - auto registration = mExtension->createRegistration("aplext:webflow:10", regReq); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); ASSERT_STREQ("RegisterSuccess", GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); Value* schema = RegistrationSuccess::SCHEMA().Get(registration); @@ -233,7 +270,7 @@ TEST_F(SimpleAplWebflowExtensionTest, InvokeCommandStartFlowSuccess) { .uri("aplext:webflow:10") .name("StartFlow") .property("url", "test_url"); - auto invoke = mExtension->invokeCommand("aplext:webflow:10", command); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(), command); ASSERT_TRUE(invoke); ASSERT_EQ("START_FLOW", mObserver->mCommand); ASSERT_EQ("test_url", mObserver->mUrl); @@ -252,10 +289,48 @@ TEST_F(SimpleAplWebflowExtensionTest, InvokeCommandStartFlowWithFlowIdSuccess) { .name("StartFlow") .property("url", "test_url") .property("flowId", "test_flow"); - auto invoke = mExtension->invokeCommand("aplext:webflow:10", command); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(), command); ASSERT_TRUE(invoke); ASSERT_EQ("START_FLOW", mObserver->mCommand); ASSERT_EQ("test_url", mObserver->mUrl); ASSERT_EQ("test_flow", mObserver->mFlowId); ASSERT_EQ("test_flow", mEventFlow); -} \ No newline at end of file +} + +/** + * Ensure base implementation of lifecycle callbacks in the observer run with no effect + */ + TEST_F(SimpleAplWebflowExtensionTest, VerifyLifecycleCallbacksRun) { + auto activity = createActivityDescriptor(); + + ASSERT_TRUE(registerExtension()); + + mExtension->onForeground(activity); + mExtension->onBackground(activity); + mExtension->onHidden(activity); + + ASSERT_TRUE(mObserver->mCommand.empty()); + ASSERT_TRUE(mObserver->mUrl.empty()); + ASSERT_TRUE(mObserver->mFlowId.empty()); + } + +/** + * Lifecycle callbacks are forwarded to observer. + */ +TEST_F(SimpleAplWebflowExtensionTest, LifecycleCallbacksForwardToObserver) { + auto activity = createActivityDescriptor(); + + auto lifecycleObserver = std::make_shared(); + auto extension = std::make_shared(testGenUuid, lifecycleObserver); + + ASSERT_EQ("CREATED", lifecycleObserver->mLifecycleState); + + extension->onForeground(activity); + ASSERT_EQ("FOREGROUND", lifecycleObserver->mLifecycleState); + + extension->onBackground(activity); + ASSERT_EQ("BACKGROUND", lifecycleObserver->mLifecycleState); + + extension->onHidden(activity); + ASSERT_EQ("HIDDEN", lifecycleObserver->mLifecycleState); +} diff --git a/extensions/unit/unittest_extension_provider.cpp b/extensions/unit/unittest_extension_provider.cpp index 8ca6554..03aeba4 100644 --- a/extensions/unit/unittest_extension_provider.cpp +++ b/extensions/unit/unittest_extension_provider.cpp @@ -24,7 +24,6 @@ using namespace rapidjson; class SimpleExtension final : public ExtensionBase { public: - explicit SimpleExtension(const std::string& uri) : ExtensionBase(uri) {}; explicit SimpleExtension(const std::set& uris) : ExtensionBase(uris) {}; @@ -52,6 +51,8 @@ class SimpleExtension final : public ExtensionBase { settingA = obj["A"].GetInt(); if (obj.HasMember("B")) settingsB = obj["B"].GetString(); + if (obj.HasMember("WantsInitialLiveData") && obj["WantsInitialLiveData"].GetBool()) + needsInitialLiveData = true; } Document environment; @@ -59,10 +60,25 @@ class SimpleExtension final : public ExtensionBase { auto registration = RegistrationSuccess("1.0").uri(uri).token("SessionToken1") .environment(environment) - .schema("1.0", [uri](ExtensionSchema schema) { + .schema("1.0", [&, uri](ExtensionSchema schema) { schema.uri(uri).event("boo"); + if (needsInitialLiveData) { + schema + .dataType("SampleData", [](TypeSchema &dataTypeSchema) { + dataTypeSchema + .property("label", "string"); + }) + .liveDataMap("IAmData", [](LiveDataSchema &liveDataSchema) { + liveDataSchema.dataType("SampleData"); + Document doc; + doc.Parse(R"({"label": "example"})"); + liveDataSchema.data(doc); + }); + } }); + + return registration; } @@ -78,6 +94,7 @@ class SimpleExtension final : public ExtensionBase { int settingA; std::string settingsB; + bool needsInitialLiveData = false; }; /** @@ -331,6 +348,80 @@ TEST_F(ExtensionProviderTest, RegistrationSuccessSettings) { ASSERT_EQ("hello", simple->settingsB); } +static const char* REGISTRATION_SUCCESS_WITH_LIVE_DATA = R"({ + "version": "1.0", + "method": "RegisterSuccess", + "uri": "aplext:foo:10", + "target": "aplext:foo:10", + "token": "SessionToken1", + "environment": { + "WantsInitialLiveData": true + }, + "schema": { + "type": "Schema", + "version": "1.0", + "events": [ + { + "name": "boo" + } + ], + "types": [ + { + "name": "SampleData", + "properties": { + "label": "string" + } + } + ], + "commands": [], + "liveData": [ + { + "name": "IAmData", + "events": [], + "type": "SampleData", + "data": { + "label": "example" + } + } + ], + "components": [], + "uri": "aplext:foo:10" + } +})"; + +/** + * Example of settings for LiveData + */ +TEST_F(ExtensionProviderTest, RegistrationSuccessWithLiveSettings) { + ASSERT_TRUE(extPro->hasExtension("aplext:foo:10")); + auto foo = extPro->getExtension("aplext:foo:10"); + ASSERT_TRUE(foo); + + document.Parse(R"({ + "WantsInitialLiveData": true + })"); + Document req = RegistrationRequest("aplext:foo:10").settings(document); + + bool gotsuccess = false; + auto invoke = foo->getRegistration( + "aplext:foo:10", req, + [this, &gotsuccess](const std::string& uri, const rapidjson::Value& registerSuccess) { + gotsuccess = true; + ASSERT_EQ("aplext:foo:10", uri); + AssertMessage(uri, "RegisterSuccess", registerSuccess); + + rapidjson::Document document(rapidjson::kObjectType); + rapidjson::Document result; + rapidjson::ParseResult ok = result.Parse(REGISTRATION_SUCCESS_WITH_LIVE_DATA); + ASSERT_TRUE(ok); + + ASSERT_TRUE(registerSuccess == result); + }, + nullptr); + ASSERT_TRUE(invoke); + ASSERT_TRUE(gotsuccess); +} + /** * Test registration success callback. */ diff --git a/fedora30.Dockerfile b/fedora30.Dockerfile index 088e2d4..89e2460 100644 --- a/fedora30.Dockerfile +++ b/fedora30.Dockerfile @@ -38,7 +38,9 @@ RUN cd apl-core \ # RUN APL Core Tests RUN cd apl-core/build \ - && unit/unittest + && aplcore/unit/unittest \ + && tools/unit/tools-unittest \ + && extensions/unit/alexaext-unittest # Make APL Core ADD . /apl-core @@ -51,4 +53,6 @@ RUN cd apl-core \ # RUN APL Core Tests RUN cd apl-core/build \ - && unit/unittest + && aplcore/unit/unittest \ + && tools/unit/tools-unittest \ + && extensions/unit/alexaext-unittest diff --git a/gcc.cmake b/gcc.cmake index 04d4d12..964f134 100644 --- a/gcc.cmake +++ b/gcc.cmake @@ -37,7 +37,7 @@ if(COVERAGE) COMMAND ${LCOV_PATH} --remove ${COVERAGE_INFO} '**/unit/*' '/usr/*' '**/*build/*' '**/thirdparty/*' --output-file ${COVERAGE_CLEANED} COMMAND ${GENHTML_PATH} -o ${CMAKE_BINARY_DIR}/coverage ${COVERAGE_CLEANED} COMMAND ${CMAKE_COMMAND} -E remove ${COVERAGE_INFO} - DEPENDS ${TARGET_NAME} ${EXEC_NAME} + DEPENDS ${EXEC_NAME} ) add_custom_command(TARGET coverage-${TARGET_NAME} POST_BUILD COMMAND ; diff --git a/options.cmake b/options.cmake index 75871d1..d80d685 100644 --- a/options.cmake +++ b/options.cmake @@ -34,6 +34,15 @@ option(BUILD_ALEXAEXTENSIONS "Build Alexa Extensions library as part of the proj option(ENABLE_SCENEGRAPH "Build and enable Scene Graph support" OFF) + +# Enumgen is only built by default for certain platforms +if (NOT APPLE) + set(BUILD_ENUMGEN_DEFAULT ON) +else() + set(BUILD_ENUMGEN_DEFAULT OFF) +endif() +option(BUILD_ENUMGEN "Build the enumgen tool" ${BUILD_ENUMGEN_DEFAULT}) + # Test options option(BUILD_TESTS "Build unit tests and test programs." OFF) option(BUILD_TEST_PROGRAMS "Build test programs. Included if BUILD_TESTS=ON" OFF) @@ -58,13 +67,6 @@ if(ENABLE_SCENEGRAPH) set(SCENEGRAPH ON) endif(ENABLE_SCENEGRAPH) -# Enumgen is only built for certain platforms -if (NOT APPLE) - set(BUILD_ENUMGEN ON) -else() - set(BUILD_ENUMGEN OFF) -endif() - # Building Yoga inline depends on having the FetchContent module if(USE_PROVIDED_YOGA_INLINE AND NOT HAS_FETCH_CONTENT) message(FATAL_ERROR "The FetchContent module is needed to build yoga inline") @@ -86,6 +88,14 @@ elseif(NOT USE_PROVIDED_YOGA_INLINE) set(USE_PROVIDED_YOGA_AS_LIB ON) endif() +# Check for evaluation depth limit. If not set, pick a default +# To override this value, set it on the command line. For example: +# cmake .. -DEVALUATION_DEPTH_LIMIT=10 +if(NOT EVALUATION_DEPTH_LIMIT) + set(EVALUATION_DEPTH_LIMIT 5) +endif() +message(STATUS "Evaluation depth limit set to ${EVALUATION_DEPTH_LIMIT}") + # Capture the compile-time options to apl_config.h so that headers can be distributed configure_file(${APL_CORE_DIR}/aplcore/include/apl/apl_config.h.in aplcore/include/apl/apl_config.h @ONLY) diff --git a/test/CMakeLists.txt b/test/CMakeLists.txt index de1da0d..01108c3 100644 --- a/test/CMakeLists.txt +++ b/test/CMakeLists.txt @@ -14,7 +14,6 @@ set(CMAKE_CXX_STANDARD 11) include_directories(../aplcore/include) -include_directories(${RAPIDJSON_INCLUDE}) include_directories(${PEGTL_INCLUDE}) include_directories(${YOGA_INCLUDE}) @@ -22,20 +21,6 @@ if (ANDROID) set(OTHER_LIBS log) endif (ANDROID) -if (ENABLE_ALEXAEXTENSIONS) - if (NOT BUILD_ALEXAEXTENSIONS) - # Check to see if it's available from the system. - find_package(alexaext REQUIRED) - endif (NOT BUILD_ALEXAEXTENSIONS) - - if(OTHER_LIBS) - set(OTHER_LIBS "${OTHER_LIBS} alexa::extensions") - else() - set(OTHER_LIBS alexa::extensions) - endif(OTHER_LIBS) - -endif(ENABLE_ALEXAEXTENSIONS) - add_executable(parseColor parseColor.cpp) target_link_libraries(parseColor apl ${OTHER_LIBS}) diff --git a/test/parseExpression.cpp b/test/parseExpression.cpp index 5e01b4b..9b02912 100644 --- a/test/parseExpression.cpp +++ b/test/parseExpression.cpp @@ -22,11 +22,22 @@ #include "apl/datagrammar/bytecode.h" #include "apl/engine/evaluate.h" -#include "apl/primitives/symbolreferencemap.h" #include "apl/utils/dump_object.h" static const char *USAGE_STRING = "parseExpression [OPTIONS] EXPRESSION*"; +void showSymbols(const apl::BoundSymbolSet& symbols) +{ + if (symbols.empty()) + std::cout << "No symbols referenced"; + else { + std::cout << "Symbols referenced:"; + for (const auto& symbol : symbols) + std::cout << " " << symbol.getName(); + std::cout << std::endl; + } +} + int main(int argc, char *argv[]) { @@ -36,6 +47,8 @@ main(int argc, char *argv[]) bool optimize = false; long repetitions = 0; bool verbose = false; + bool show_symbols = false; + bool decompile = false; argumentSet.add({ Argument("-o", @@ -63,7 +76,23 @@ main(int argc, char *argv[]) "", [&](const std::vector&) { verbose = true; - }) + }), + Argument("-S", + "--symbols", + Argument::NONE, + "Show referenced symbols used when evaluating the expression", + "", + [&](const std::vector&) { + show_symbols = true; + }), + Argument("-D", + "--decompile", + Argument::NONE, + "Decompile the byte code and display", + "", + [&](const std::vector&) { + decompile = true; + }), }); std::vector args(argv + 1, argv + argc); @@ -79,22 +108,9 @@ main(int argc, char *argv[]) auto start = std::chrono::high_resolution_clock::now(); for (const auto& m : args) { - // When the optimize flag is turned on, we optimize the expression once - // and evaluate it "N" times - if (optimize) { - auto cbc = apl::getDataBinding(*c, m); - if (cbc.is()) { - apl::SymbolReferenceMap map; - cbc.symbols(map); - } - - for (long i = 0 ; i < repetitions ; i++) - cbc.eval(); - } - else { // When the optimize flag is turned off, we evaluated the expression "N" times. - for (long i = 0 ; i < repetitions ; i++) - apl::evaluate(*c, m); - } + auto result = apl::parseAndEvaluate(*c, m, optimize); + for (long i = 0; i < repetitions; i++) + result.expression.eval(); } auto stop = std::chrono::high_resolution_clock::now(); @@ -108,20 +124,17 @@ main(int argc, char *argv[]) if (verbose) std::cout << "parsing '" << m << "'" << std::endl; - auto cbc = apl::getDataBinding(*c, m); - - if (verbose && cbc.is()) - cbc.get()->dump(); - - std::cout << "Evaluates to " << cbc.eval().asString() << std::endl; + auto result = apl::parseAndEvaluate(*c, m, optimize); - if (optimize && cbc.is()) { - apl::SymbolReferenceMap map; - cbc.symbols(map); - std::cout << std::endl << "Optimized version" << std::endl; - cbc.get()->dump(); - std::cout << "optimized version evaluates to " << cbc.eval().asString() << std::endl; + if (decompile && result.expression.is()) { + for (const auto& m : result.expression.get()->disassemble()) { + std::cout << m << std::endl; + } } + + std::cout << "Evaluates to " << result.value.toDebugString() << std::endl; + if (show_symbols) + showSymbols(result.symbols); } } } diff --git a/thirdparty/aplrapidjsonConfig.cmake.in b/thirdparty/aplrapidjsonConfig.cmake.in new file mode 100644 index 0000000..4001bc4 --- /dev/null +++ b/thirdparty/aplrapidjsonConfig.cmake.in @@ -0,0 +1,17 @@ +@PACKAGE_INIT@ + +set(USE_RAPIDJSON_PACKAGE @USE_RAPIDJSON_PACKAGE@) + +if (USE_RAPIDJSON_PACKAGE) + if (NOT TARGET rapidjson-apl) # Guard against multiple inclusion + # Short circuit the usual mechanism and instead proxy the installed package. + # This way we don't accidentally capture the exact path of the system rapidjson library + find_package(RapidJSON REQUIRED) + + add_library(rapidjson-apl INTERFACE IMPORTED) + target_include_directories(rapidjson-apl INTERFACE ${RAPIDJSON_INCLUDE_DIRS}) + endif() +else() + include("${CMAKE_CURRENT_LIST_DIR}/aplrapidjsonTargets.cmake") +endif() + diff --git a/thirdparty/rapidjson-apl.cmake b/thirdparty/rapidjson-apl.cmake new file mode 100644 index 0000000..cf2344e --- /dev/null +++ b/thirdparty/rapidjson-apl.cmake @@ -0,0 +1,109 @@ +############################################################ +# RapidJSON is a header-only library +############################################################ + +set(RAPIDJSON_SOURCE_URL "${APL_PROJECT_DIR}/thirdparty/rapidjson-v1.1.0.tar.gz") +set(RAPIDJSON_SOURCE_MD5 "badd12c511e081fec6c89c43a7027bce") +set(USE_RAPIDJSON_PACKAGE FALSE) + +if (USE_SYSTEM_RAPIDJSON) + find_package(RapidJSON QUIET) + if (RapidJSON_FOUND) + set(RAPIDJSON_INCLUDE ${RAPIDJSON_INCLUDE_DIRS}) + set(USE_RAPIDJSON_PACKAGE TRUE) + else() + find_path(RAPIDJSON_INCLUDE + NAMES rapidjson/document.h + REQUIRED) + endif() +elseif (HAS_FETCH_CONTENT) + FetchContent_Declare(rapidjson + URL ${RAPIDJSON_SOURCE_URL} + URL_MD5 ${RAPIDJSON_SOURCE_MD5} + PATCH_COMMAND patch ${PATCH_FLAGS} -p1 < ${APL_PATCH_DIR}/rapidjson.patch + ) + FetchContent_Populate(rapidjson) + set(RAPIDJSON_INCLUDE ${rapidjson_SOURCE_DIR}/include) +else() + ExternalProject_Add(rapidjson + URL ${RAPIDJSON_SOURCE_URL} + URL_MD5 ${RAPIDJSON_SOURCE_MD5} + PATCH_COMMAND patch ${PATCH_FLAGS} -p1 < ${APL_PATCH_DIR}/rapidjson.patch + STEP_TARGETS build + EXCLUDE_FROM_ALL TRUE + CONFIGURE_COMMAND "" + BUILD_COMMAND "" + CMAKE_ARGS ${CMAKE_ARGS} + ) + ExternalProject_Get_Property(rapidjson install_dir) + set(RAPIDJSON_INCLUDE ${install_dir}/src/rapidjson/include) +endif() + +message(VERBOSE "Rapidjson include directory ${RAPIDJSON_INCLUDE}") + +add_library(rapidjson-apl INTERFACE) +target_include_directories(rapidjson-apl INTERFACE + # When we're building against RapidJSON, just use the include directory we discovered above + $ +) + +if (USE_SYSTEM_RAPIDJSON) + # If we're using the system rapidjson, then use the full include path in our generated config + target_include_directories(rapidjson-apl INTERFACE + $ + ) +else() + # If we're using the bundled rapidjson, use the relative path to "include" in our generated config + target_include_directories(rapidjson-apl INTERFACE + $ + ) +endif() + +if (NOT USE_SYSTEM_RAPIDJSON AND NOT HAS_FETCH_CONTENT) + add_dependencies(rapidjson-apl rapidjson-build) +endif() + +if (NOT USE_SYSTEM_RAPIDJSON) + # If we're using the bundled RapidJSON, make sure to install it along with the rest of the APL Core files + install(DIRECTORY ${RAPIDJSON_INCLUDE}/rapidjson + DESTINATION include + FILES_MATCHING PATTERN "*.h") +endif() + +include(CMakePackageConfigHelpers) +configure_package_config_file( + "${CMAKE_CURRENT_LIST_DIR}/aplrapidjsonConfig.cmake.in" + "${CMAKE_CURRENT_BINARY_DIR}/aplrapidjsonConfig.cmake" + INSTALL_DESTINATION + lib/cmake/aplrapidjson + NO_CHECK_REQUIRED_COMPONENTS_MACRO) + +install( + FILES + ${CMAKE_CURRENT_BINARY_DIR}/aplrapidjsonConfig.cmake + DESTINATION + lib/cmake/aplrapidjson +) + +# Create an export for rapidjson-apl so that other exported modules can depend on it +install( + TARGETS rapidjson-apl + EXPORT aplrapidjson-targets + ARCHIVE DESTINATION lib + LIBRARY DESTINATION lib + PUBLIC_HEADER DESTINATION include +) + +export( + EXPORT + aplrapidjson-targets +) + +install( + EXPORT + aplrapidjson-targets + DESTINATION + lib/cmake/aplrapidjson + FILE + aplrapidjsonTargets.cmake +) \ No newline at end of file diff --git a/thirdparty/thirdparty.cmake b/thirdparty/thirdparty.cmake index 5bffbb0..6bef0d4 100644 --- a/thirdparty/thirdparty.cmake +++ b/thirdparty/thirdparty.cmake @@ -28,6 +28,9 @@ list(APPEND CMAKE_ARGS -DCMAKE_CXX_FLAGS=${EXT_CXX_ARGS}) list(APPEND CMAKE_ARGS -DCMAKE_INSTALL_PREFIX=${CMAKE_BINARY_DIR}/lib) list(APPEND CMAKE_ARGS -DCMAKE_TOOLCHAIN_FILE=${CMAKE_TOOLCHAIN_FILE}) + +include(${CMAKE_CURRENT_LIST_DIR}/rapidjson-apl.cmake) + ############################################################ # Build yoga locally or use an external library ############################################################ @@ -130,41 +133,9 @@ endif() message(VERBOSE "PEGTL include directory ${PEGTL_INCLUDE}") -############################################################ -# RapidJSON is a header-only library -############################################################ - -set(RAPIDJSON_SOURCE_URL "${APL_PROJECT_DIR}/thirdparty/rapidjson-v1.1.0.tar.gz") -set(RAPIDJSON_SOURCE_MD5 "badd12c511e081fec6c89c43a7027bce") - -if (USE_SYSTEM_RAPIDJSON) - find_path(RAPIDJSON_INCLUDE - NAMES rapidjson/document.h - REQUIRED) -elseif (HAS_FETCH_CONTENT) - FetchContent_Declare(rapidjson - URL ${RAPIDJSON_SOURCE_URL} - URL_MD5 ${RAPIDJSON_SOURCE_MD5} - PATCH_COMMAND patch ${PATCH_FLAGS} -p1 < ${APL_PATCH_DIR}/rapidjson.patch - ) - FetchContent_Populate(rapidjson) - set(RAPIDJSON_INCLUDE ${rapidjson_SOURCE_DIR}/include) -else() - ExternalProject_Add(rapidjson - URL ${RAPIDJSON_SOURCE_URL} - URL_MD5 ${RAPIDJSON_SOURCE_MD5} - PATCH_COMMAND patch ${PATCH_FLAGS} -p1 < ${APL_PATCH_DIR}/rapidjson.patch - STEP_TARGETS build - EXCLUDE_FROM_ALL TRUE - CONFIGURE_COMMAND "" - BUILD_COMMAND "" - CMAKE_ARGS ${CMAKE_ARGS} - ) - ExternalProject_Get_Property(rapidjson install_dir) - set(RAPIDJSON_INCLUDE ${install_dir}/src/rapidjson/include) -endif() - -message(VERBOSE "Rapidjson include directory ${RAPIDJSON_INCLUDE}") +# Prevent overriding the parent project's compiler/linker +# settings on Windows +set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) ############################################################ # Unpack googletest at configure time. This is copied from the googletest README.md file @@ -185,6 +156,8 @@ if(result) message(FATAL_ERROR "Build step for googletest failed: ${result}") endif() -# Prevent overriding the parent project's compiler/linker -# settings on Windows -set(gtest_force_shared_crt ON CACHE BOOL "" FORCE) +# Add googletest directly to our build. This defines +# the gtest and gtest_main targets. +add_subdirectory(${CMAKE_BINARY_DIR}/googletest-src + ${CMAKE_BINARY_DIR}/googletest-build + EXCLUDE_FROM_ALL) diff --git a/tools.cmake b/tools.cmake index 4550c6c..1d8c86a 100644 --- a/tools.cmake +++ b/tools.cmake @@ -12,6 +12,8 @@ # permissions and limitations under the License. if (BUILD_ENUMGEN) + message("Building enumgen") + set(ENUMGEN_INSTALL_DIR "${CMAKE_BINARY_DIR}/tools") set(ENUMGEN_BIN "${ENUMGEN_INSTALL_DIR}/enumgen") @@ -20,4 +22,10 @@ if (BUILD_ENUMGEN) SOURCE_DIR ${APL_PROJECT_DIR}/tools CMAKE_ARGS "-DCMAKE_INSTALL_PREFIX=${ENUMGEN_INSTALL_DIR}" ) -endif() \ No newline at end of file + + if (BUILD_UNIT_TESTS) + message("tools - Unit test build enabled.") + enable_testing() + add_subdirectory(tools/unit) + endif() +endif() diff --git a/tools/CMakeLists.txt b/tools/CMakeLists.txt index 0374da6..21f84e5 100644 --- a/tools/CMakeLists.txt +++ b/tools/CMakeLists.txt @@ -34,4 +34,8 @@ endif() include_directories(${PEGTL_INCLUDE}) +if(MSVC) + SET_TARGET_PROPERTIES(enumgen PROPERTIES LINK_FLAGS "/link setargv.obj") +endif() + install(TARGETS enumgen RUNTIME DESTINATION ${CMAKE_INSTALL_PREFIX}) diff --git a/tools/unit/CMakeLists.txt b/tools/unit/CMakeLists.txt new file mode 100644 index 0000000..d1a0a10 --- /dev/null +++ b/tools/unit/CMakeLists.txt @@ -0,0 +1,26 @@ +message("Adding tools unit test target") + +add_executable(tools-unittest + ../src/enumparser.cpp + unittest_enumgen.cpp +) + +target_include_directories(tools-unittest PRIVATE "../../tools/src") +target_include_directories(tools-unittest PRIVATE "${PEGTL_INCLUDE}") + +target_link_libraries(tools-unittest gtest gtest_main) + +if (COVERAGE) + target_add_code_coverage(tools-unittest tools) +endif() + +if(CTEST_INDIVIDUALLY) + # NOTE: discovered ctest targets below are much slower than their direct counterparts. Ctest loads + # tests individually instead of just running all in a class. This makes it take much + # longer for execution. This is somewhat useful if you want to execute tests using ctest scripts, but is + # completely unusable on dev machine: each test takes 800ms vs 20ms, and valgrind takes 4-5s per test. + gtest_discover_tests(tools-unittest) +else() + # Adds the entire unittest executable as a single ctest. Great for speed. + add_test(all-tests tools-unittest) +endif() \ No newline at end of file diff --git a/unit/unittest_enumgen.cpp b/tools/unit/unittest_enumgen.cpp similarity index 99% rename from unit/unittest_enumgen.cpp rename to tools/unit/unittest_enumgen.cpp index 860ffaf..ccda3e7 100644 --- a/unit/unittest_enumgen.cpp +++ b/tools/unit/unittest_enumgen.cpp @@ -13,7 +13,7 @@ * permissions and limitations under the License. */ -#include "../tools/src/enumparser.h" +#include "../../tools/src/enumparser.h" #include "gtest/gtest.h" diff --git a/ubuntu16.04.Dockerfile b/ubuntu16.04.Dockerfile index 4d5474d..eb1d519 100644 --- a/ubuntu16.04.Dockerfile +++ b/ubuntu16.04.Dockerfile @@ -32,7 +32,9 @@ RUN cd apl-core \ # RUN APL Core Tests with gcc RUN cd apl-core/build \ - && unit/unittest + && aplcore/unit/unittest \ + && tools/unit/tools-unittest \ + && extensions/unit/alexaext-unittest # Make APL Core with clang ADD . /apl-core @@ -45,4 +47,6 @@ RUN cd apl-core \ # RUN APL Core Tests with clang RUN cd apl-core/build-clang \ - && unit/unittest + && aplcore/unit/unittest \ + && tools/unit/tools-unittest \ + && extensions/unit/alexaext-unittest diff --git a/ubuntu18.04.Dockerfile b/ubuntu18.04.Dockerfile index 385872c..9df7f45 100644 --- a/ubuntu18.04.Dockerfile +++ b/ubuntu18.04.Dockerfile @@ -32,7 +32,9 @@ RUN cd apl-core \ # RUN APL Core Tests RUN cd apl-core/build \ - && unit/unittest + && aplcore/unit/unittest \ + && tools/unit/tools-unittest \ + && extensions/unit/alexaext-unittest # Make APL Core with clang @@ -46,4 +48,6 @@ RUN cd apl-core \ # RUN APL Core Tests RUN cd apl-core/build-clang \ - && unit/unittest + && aplcore/unit/unittest \ + && tools/unit/tools-unittest \ + && extensions/unit/alexaext-unittest diff --git a/unit/datagrammar/unittest_decompile.cpp b/unit/datagrammar/unittest_decompile.cpp deleted file mode 100644 index 84df28a..0000000 --- a/unit/datagrammar/unittest_decompile.cpp +++ /dev/null @@ -1,94 +0,0 @@ -/** - * Copyright 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 "../testeventloop.h" -#include "apl/datagrammar/bytecode.h" - -using namespace apl; - -class DecompileTest : public ::testing::Test { -public: - DecompileTest() { - context = Context::createTestContext(Metrics(), makeDefaultSession()); - } - - ContextPtr context; -}; - -// Static method for splitting strings on whitespace -static std::vector -splitStringWS(const std::string& text, int maxCount=3) -{ - std::vector lines; - auto it = text.begin(); - while (it != text.end()) { - it = std::find_if_not(it, text.end(), isspace); - if (it != text.end()) { - auto it2 = std::find_if(it, text.end(), isspace); - lines.emplace_back(std::string(it, it2)); - it = it2; - if (lines.size() == maxCount) - return lines; - } - } - - return lines; -} - -::testing::AssertionResult -CheckByteCode(const std::vector& lines, const std::shared_ptr& bc) -{ - if (lines.size() != bc->instructionCount()) - return ::testing::AssertionFailure() << "Mismatched line count expected=" << lines.size() - << " actual=" << bc->instructionCount(); - - for (int lineNumber = 0 ; lineNumber < lines.size() ; lineNumber++) { - auto expected = splitStringWS(lines.at(lineNumber)); - auto actual = splitStringWS(bc->instructionAsString(lineNumber)); - auto result = IsEqual(expected, actual); - if (!result) - return result; - } - - return ::testing::AssertionSuccess(); -} - -struct DecompileTestCase { - std::string expression; - std::vector instructions; -}; - -static const auto DECOMPILE_TEST_CASES = std::vector{ - {"${}", {"0 LOAD_CONSTANT (3) empty_string"}}, - {"${3}", {"0 LOAD_IMMEDIATE (3) "}}, - {"${'foo'}", {"0 LOAD_DATA (0) ['foo'] "}}, - {"${1 < 2}", {"0 LOAD_IMMEDIATE (1)", "1 LOAD_IMMEDIATE (2)", "2 COMPARE_OP (0) <"}}, - {"${true ? 2 : 3}", - {"0 LOAD_CONSTANT (2) true", "1 POP_JUMP_IF_FALSE (2) GOTO 4", "2 LOAD_IMMEDIATE (2)", - "3 JUMP (1) GOTO 5", "4 LOAD_IMMEDIATE (3)"}}, - {"${Math.min(1,2)}", - {"0 LOAD_DATA (0) ['Math']", "1 ATTRIBUTE_ACCESS (1) ['min']", "2 LOAD_IMMEDIATE (1)", - "3 LOAD_IMMEDIATE (2)", "4 CALL_FUNCTION (2) argument_count=2"}} -}; - -TEST_F(DecompileTest, Basic) -{ - for (const auto& m : DECOMPILE_TEST_CASES) { - auto v = getDataBinding(*context, m.expression); - ASSERT_TRUE(v.isEvaluable()); - auto bc = v.get(); - ASSERT_TRUE(CheckByteCode(m.instructions, bc)) << "Test case '" << m.expression << "'"; - } -} \ No newline at end of file diff --git a/unit/datagrammar/unittest_optimize.cpp b/unit/datagrammar/unittest_optimize.cpp deleted file mode 100644 index ed56560..0000000 --- a/unit/datagrammar/unittest_optimize.cpp +++ /dev/null @@ -1,114 +0,0 @@ -/** - * Copyright 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 "../testeventloop.h" -#include "apl/datagrammar/bytecode.h" -#include "apl/primitives/symbolreferencemap.h" - -using namespace apl; - -class OptimizeTest : public ::testing::Test { -public: - OptimizeTest() { - Metrics m; - context = Context::createTestContext(m, makeDefaultSession()); - } - - ContextPtr context; -}; - -static std::vector> BASIC = { - {"${1+2+a}", 4}, - {"${a || b}", 1}, - {"${false || a}", 1}, - {"${b || 100 || a}", 100}, - {"${a && b}", 0}, - {"${c[0]}", 1}, - {"${d.y}", "foobar"}, - {"${d.x}", 1}, - {"${c[0] - d.x}", 0}, - {"${c[0] - d.x ? d['y'] : d['z'][0]}", -1}, - {"${Math.min( a, b, c.length, d.x, d.z[0] ) }", -1}, - {"${Math.max( a , b , c.length , d.x , d.z[3-3] ) }", 3}, - {"${+2+a}", 3}, - {"${!(aputUserWriteable("a", 1); - context->putUserWriteable("b", 0); - - auto array = JsonData("[1,2,3]"); - ASSERT_TRUE(array); - context->putUserWriteable("c", array.get()); - - auto map = JsonData(R"({"x": 1, "y": "foobar", "z": [-1, 0, false]})"); - ASSERT_TRUE(map); - context->putUserWriteable("d", map.get()); - - for (const auto& m : BASIC) { - auto result = parseDataBinding(*context, m.first); - ASSERT_TRUE(result.isEvaluable()); - ASSERT_TRUE(IsEqual(m.second, result.eval())) << m.first; - ASSERT_FALSE(result.get()->isOptimized()); - - SymbolReferenceMap symbols; - result.symbols(symbols); - ASSERT_TRUE(IsEqual(m.second, result.eval())) << m.first; - ASSERT_TRUE(result.get()->isOptimized()); - } -} - -static std::vector> MERGE_STRINGS = { - {"This value is ${23}", "This value is 23"}, - {"${1+1} is the value", "2 is the value"}, - {"Where are ${1+1} tigers?", "Where are 2 tigers?"}, - {"A ${null ?? 'friendly'} tiger is not ${3-1} easy ${4/2} find", "A friendly tiger is not 2 easy 2 find"} -}; - -TEST_F(OptimizeTest, MergeStrings) -{ - context->putUserWriteable("a", 23); - - for (const auto& m : MERGE_STRINGS) { - auto result = parseDataBinding(*context, m.first); - ASSERT_FALSE(result.isEvaluable()); - ASSERT_TRUE(IsEqual(m.second, result)) << m.first; - } -} - - -TEST_F(OptimizeTest, DeadCodeRemoval) -{ - context->putUserWriteable("a", 23); - auto result = parseDataBinding(*context, "${a?(1!=2? 10:3):4}"); - ASSERT_TRUE(result.is()); - ASSERT_TRUE(IsEqual(10, result.eval())); - - context->userUpdateAndRecalculate("a", 0, false); - ASSERT_TRUE(IsEqual(4, result.eval())); - - // Now optimize - SymbolReferenceMap symbols; - result.symbols(symbols); - ASSERT_TRUE(IsEqual(4, result.eval())); - - context->userUpdateAndRecalculate("a", 23, false); - ASSERT_TRUE(IsEqual(10, result.eval())); -} \ No newline at end of file