diff --git a/CHANGELOG.md b/CHANGELOG.md index 4ef07b7..f79fad1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [2022.1.0] + +This release adds support for version 2022.1 of the APL specification. + +### Added + +- Alpha feature: New APL Extensions lifecycle. +- Alpha feature: Added new encryption and webflow interfaces into the extension library. +- Added log id to help to track logs between multiple experiences. +- Video now can be muted and unmuted dynamically. + +### Changed + +- Bug fixes. +- Build improvements. +- Fixed typos and cleanup formatting. +- Improved localization handling within core. +- Performance improvements. +- Update Yoga dependency to 1.19. + ## [1.9.1] ### Changed diff --git a/CMakeLists.txt b/CMakeLists.txt index f1c2f86..2177bbb 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -11,6 +11,10 @@ # express or implied. See the License for the specific language governing # permissions and limitations under the License. +# Use the FetchContent cmake module to unpack Yoga. This may not be available +# on older systems +include(FetchContent OPTIONAL RESULT_VARIABLE HAS_FETCH_CONTENT) + cmake_minimum_required(VERSION 3.5) project(APLCoreEngine diff --git a/THIRD-PARTY-NOTICES.txt b/THIRD-PARTY-NOTICES.txt index 3d0739d..76d434f 100644 --- a/THIRD-PARTY-NOTICES.txt +++ b/THIRD-PARTY-NOTICES.txt @@ -145,7 +145,7 @@ SOFTWARE ------ -** yoga 1.16; version 1.16.0 -- https://yogalayout.com/ +** yoga 1.19; version 1.19.0 -- https://yogalayout.com/ Copyright (c) Facebook, Inc. and its affiliates. MIT License diff --git a/aplcore/CMakeLists.txt b/aplcore/CMakeLists.txt index 8e79f07..9268452 100644 --- a/aplcore/CMakeLists.txt +++ b/aplcore/CMakeLists.txt @@ -52,7 +52,8 @@ endif() if (TARGET yoga) add_dependencies(apl yoga) endif() - add_dependencies(apl pegtl-build) + +add_dependencies(apl pegtl-build) # When not using the system rapidjson build the library, add a dependency on the build step if (NOT USE_SYSTEM_RAPIDJSON) @@ -110,9 +111,13 @@ target_include_directories(apl $ ) -target_link_libraries(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 if (ENABLE_ALEXAEXTENSIONS) @@ -158,12 +163,13 @@ install(DIRECTORY ${RAPIDJSON_INCLUDE}/rapidjson FILES_MATCHING PATTERN "*.h") endif() -if (NOT YOGA_EXTERNAL_LIB AND NOT USE_SYSTEM_YOGA) +if (USE_PROVIDED_YOGA_AS_LIB) # We built the bundled yoga lib, install it install(FILES ${YOGA_LIB} DESTINATION lib) endif() +if (NOT USE_PROVIDED_YOGA_INLINE) set_target_properties(apl PROPERTIES EXPORT_NAME core @@ -171,6 +177,7 @@ set_target_properties(apl PROPERTIES # Only set this for builds, the find module will handle the other cases $ ) +endif() export( EXPORT diff --git a/aplcore/include/apl/action/action.h b/aplcore/include/apl/action/action.h index ba9aba7..7cb5752 100644 --- a/aplcore/include/apl/action/action.h +++ b/aplcore/include/apl/action/action.h @@ -58,8 +58,8 @@ class Action : public std::enable_shared_from_this, /** * Make a generic action. The StartFunc runs immediately. If you don't pass a starting function, * the action is resolved immediately. - * @param timers - * @param func + * @param timers Timer reference + * @param func The starting function to execute immediately. * @return The action */ static ActionPtr make(const TimersPtr& timers, StartFunc func = nullptr); @@ -67,16 +67,16 @@ class Action : public std::enable_shared_from_this, /** * Make an action that fires after a delay. If you don't pass a starting function, * the action resolves after the delay. - * @param timers - * @param delay - * @param func + * @param timers Timer reference + * @param delay Delay in milliseconds to wait before running the starting function. + * @param func The starting function to execute. * @return The action */ static ActionPtr makeDelayed(const TimersPtr& timers, apl_duration_t delay, StartFunc func = nullptr); /** * Make an action that resolves after all of the child actions resolve. - * @param timers + * @param timers Timer reference * @param actionList A list of actions. * @return The tail of this action */ @@ -85,7 +85,7 @@ class Action : public std::enable_shared_from_this, /** * Make an action that resolves after any of the child actions resolve. * The other child actions are terminated. - * @param timers + * @param timers Timer reference * @param actionList A list of actions * @return The tail of this action. */ @@ -94,7 +94,7 @@ class Action : public std::enable_shared_from_this, /** * Make an action that runs an animation. The animator function is called as time is advanced * up to and including when the duration is reached. It is _not_ called for a time of zero. - * @param timers + * @param timers Timer reference * @param duration The duration of the animation. * @param animator The function to call up to and including when the duration time is reached. * @return The animation action @@ -106,7 +106,7 @@ class Action : public std::enable_shared_from_this, /** * Wrap provided action with another one that will call a callback when provided resolved. If * provided action is terminated wrap action will also be terminated. - * @param timers + * @param timers Timer reference * @param action action to wrap. * @param callback Callback to call. * @return Wrap action. @@ -151,7 +151,7 @@ class Action : public std::enable_shared_from_this, /** * Resolve with a rect. Used to pass back a bounds for the first line of a text component * during line karaoke. - * @param argument + * @param argument A rectangle */ void resolve(const Rect& argument); @@ -230,7 +230,7 @@ class ActionRef { /** * Resolve the action with a union - * @param argument + * @param argument A rectangle */ void resolve(const Rect& argument) const { if (mPtr) @@ -293,7 +293,7 @@ class ActionRef { * @deprecated * @return True if there is no action associated with this action ref. */ - bool isEmpty() const { return empty(); } + APL_DEPRECATED bool isEmpty() const { return empty(); } /** * Attach a chunk of user data to this action diff --git a/aplcore/include/apl/action/scrollaction.h b/aplcore/include/apl/action/scrollaction.h index 4d1e04d..20d4888 100644 --- a/aplcore/include/apl/action/scrollaction.h +++ b/aplcore/include/apl/action/scrollaction.h @@ -37,7 +37,7 @@ class ScrollAction : public AnimatedScrollAction { /** * @param timers Timer reference. * @param command Command that spawned this action. - * @return + * @return A scroll action or null if not needed. */ static std::shared_ptr make(const TimersPtr& timers, const std::shared_ptr& command); @@ -48,7 +48,7 @@ class ScrollAction : public AnimatedScrollAction { * @param target component to perform action on. * @param targetDistance Object containing Dimension representing distance to be scrolled. * @param duration scrolling duration. - * @return + * @return The scroll action or null if it is not needed. */ static std::shared_ptr make(const TimersPtr& timers, const ContextPtr& context, diff --git a/aplcore/include/apl/action/scrolltoaction.h b/aplcore/include/apl/action/scrolltoaction.h index f0042f3..c5bd434 100644 --- a/aplcore/include/apl/action/scrolltoaction.h +++ b/aplcore/include/apl/action/scrolltoaction.h @@ -40,7 +40,7 @@ class ScrollToAction : public AnimatedScrollAction { * @param timers Timer reference. * @param command Command that spawned this action. * @param target Component to scroll to. - * @return + * @return The scroll to action or null if it is not needed. */ static std::shared_ptr make(const TimersPtr& timers, const std::shared_ptr& command, @@ -51,7 +51,7 @@ class ScrollToAction : public AnimatedScrollAction { * @param command Command that spawned this action. * @param subBounds Bounds within the target to scroll to. * @param target Component to scroll to. - * @return + * @return The scroll to action or null if it is not needed. */ static std::shared_ptr make(const TimersPtr& timers, const std::shared_ptr& command, @@ -66,7 +66,7 @@ class ScrollToAction : public AnimatedScrollAction { * @param subBounds Bounds within the target to scroll to. * @param context Target context. * @param target Component to scroll to. - * @return + * @return The scroll to action or null if it is not needed. */ static std::shared_ptr make(const TimersPtr& timers, const CommandScrollAlign& align, @@ -79,7 +79,7 @@ class ScrollToAction : public AnimatedScrollAction { * @param timers Timer reference. * @param target Component to scroll to. * @param duration scrolling duration. - * @return + * @return The scroll to action or null if it is not needed. */ static std::shared_ptr makeUsingSnap(const TimersPtr& timers, const CoreComponentPtr& target, diff --git a/aplcore/include/apl/animation/easing.h b/aplcore/include/apl/animation/easing.h index 9b928c3..2ac3c47 100644 --- a/aplcore/include/apl/animation/easing.h +++ b/aplcore/include/apl/animation/easing.h @@ -48,7 +48,7 @@ class Easing : public ObjectData { /** * Evaluate the easing curve at a given time between 0 and 1. - * @param time + * @param time The parameterized time * @return The value */ virtual float calc(float time) = 0; diff --git a/aplcore/include/apl/animation/easinggrammar.h b/aplcore/include/apl/animation/easinggrammar.h index ffb6ea7..2192eaa 100644 --- a/aplcore/include/apl/animation/easinggrammar.h +++ b/aplcore/include/apl/animation/easinggrammar.h @@ -21,6 +21,7 @@ #include "apl/animation/coreeasing.h" #include "apl/utils/log.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -29,6 +30,10 @@ namespace easinggrammar { namespace pegtl = tao::TAO_PEGTL_NAMESPACE; using namespace pegtl; +/** + * \cond ShowColorGrammar + */ + /** * This grammar assumes that all space characters have been removed. * @@ -93,7 +98,7 @@ template<> struct action { template< typename Input > static void apply(const Input& in, easing_state& state) { - state.args.push_back(std::stof(in.string())); + state.args.push_back(sutil::stof(in.string())); } }; @@ -309,6 +314,9 @@ template<> struct action< scurve > state.segments.emplace_back(EasingSegment(kSCurveSegment, state.startIndex)); } }; -} +/** + * \endcond + */ +} // namespace easinggrammar } // namespace apl #endif //_APL_EASING_GRAMMAR_H diff --git a/aplcore/include/apl/apl.h b/aplcore/include/apl/apl.h index bc5b268..85d6095 100644 --- a/aplcore/include/apl/apl.h +++ b/aplcore/include/apl/apl.h @@ -25,9 +25,10 @@ #include "rapidjson/document.h" #include "apl/apl_config.h" +#include "apl/common.h" + #include "apl/action/action.h" #include "apl/buildTimeConstants.h" -#include "apl/common.h" #include "apl/component/component.h" #include "apl/component/textmeasurement.h" #include "apl/content/configurationchange.h" @@ -60,6 +61,8 @@ #include "apl/primitives/mediasource.h" #include "apl/primitives/mediastate.h" #include "apl/primitives/object.h" +#include "apl/primitives/range.h" +#include "apl/primitives/roundedrect.h" #include "apl/primitives/styledtext.h" #include "apl/scaling/metricstransform.h" #include "apl/touch/pointerevent.h" @@ -69,6 +72,7 @@ #ifdef ALEXAEXTENSIONS #include "apl/extension/extensionmediator.h" +#include "apl/extension/extensionsession.h" #endif #endif // _APL_H diff --git a/aplcore/include/apl/colorgrammar/colorfunctions.h b/aplcore/include/apl/colorgrammar/colorfunctions.h index 1f770a5..236bbcd 100644 --- a/aplcore/include/apl/colorgrammar/colorfunctions.h +++ b/aplcore/include/apl/colorgrammar/colorfunctions.h @@ -100,9 +100,9 @@ inline uint32_t colorFromHSL(double hue, double sat, double light) { /** * Calculate a color value from a hexidecimal string - * @param hex - * @param color - * @return + * @param hex The color as a hexidecimal string value + * @param color The calculated color will be stored in this unsigned integer. + * @return True if the string parsed correctly */ inline bool colorFromHex(std::string hex, uint32_t& color) { switch (hex.length()) { @@ -135,4 +135,4 @@ inline bool colorFromHex(std::string hex, uint32_t& color) { } // namespace colorgrammar } // namespace apl -#endif // _COLORGRAMMAR_COLOR_FUNCTIONS_H \ No newline at end of file +#endif // _COLORGRAMMAR_COLOR_FUNCTIONS_H diff --git a/aplcore/include/apl/colorgrammar/colorgrammar.h b/aplcore/include/apl/colorgrammar/colorgrammar.h index e103ad8..9c1b413 100644 --- a/aplcore/include/apl/colorgrammar/colorgrammar.h +++ b/aplcore/include/apl/colorgrammar/colorgrammar.h @@ -28,6 +28,7 @@ #include "colorfunctions.h" #include "apl/utils/log.h" +#include "apl/utils/stringfunctions.h" namespace apl { namespace colorgrammar { @@ -115,9 +116,9 @@ namespace colorgrammar { double value; if (s.back() == '%') { size_t len = s.length(); - value = stod(s.substr(0, len - 1)) * 0.01; + value = sutil::stod(s.substr(0, len - 1)) * 0.01; } else { - value = stod(s); + value = sutil::stod(s); } LOGF_IF(DEBUG_GRAMMAR, "Number: '%s' -> %lf", in.string().c_str(), value); diff --git a/aplcore/include/apl/common.h b/aplcore/include/apl/common.h index a60f1ac..1ceca52 100644 --- a/aplcore/include/apl/common.h +++ b/aplcore/include/apl/common.h @@ -47,7 +47,6 @@ using apl_duration_t = double; // here so they can be conveniently used from any source file. class Action; -class CharacterRanges; class Command; class Component; class Content; @@ -83,9 +82,11 @@ class Timers; using ActionPtr = std::shared_ptr; using CommandPtr = std::shared_ptr; +using ConstCommandPtr = std::shared_ptr; using ComponentPtr = std::shared_ptr; using ContentPtr = std::shared_ptr; using ContextPtr = std::shared_ptr; +using ConstContextPtr = std::shared_ptr; using CoreComponentPtr = std::shared_ptr; using DataSourcePtr = std::shared_ptr; using DataSourceProviderPtr = std::shared_ptr; @@ -114,7 +115,6 @@ using StyleDefinitionPtr = std::shared_ptr; using StyleInstancePtr = std::shared_ptr; using TextMeasurementPtr = std::shared_ptr; using TimersPtr = std::shared_ptr; -using CharacterRangesPtr = std::shared_ptr; // Convenience templates for creating sets of weak and strong pointers diff --git a/aplcore/include/apl/component/actionablecomponent.h b/aplcore/include/apl/component/actionablecomponent.h index 7d90ade..05a4b1c 100644 --- a/aplcore/include/apl/component/actionablecomponent.h +++ b/aplcore/include/apl/component/actionablecomponent.h @@ -40,9 +40,9 @@ class ActionableComponent : public CoreComponent { void enableGestures(); /** - * Get the touch event specific properties - * @param point Properties of the component segment of the event. - * @return The event data-binding context. + * Get the touch event-specific properties + * @param localPoint The coordinates of the local touch event + * @return A map of event-specific properties to be added to the data-binding context. */ virtual ObjectMapPtr createTouchEventProperties(const Point& localPoint) const; diff --git a/aplcore/include/apl/component/component.h b/aplcore/include/apl/component/component.h index 9fa6afe..ba7a7d9 100644 --- a/aplcore/include/apl/component/component.h +++ b/aplcore/include/apl/component/component.h @@ -26,6 +26,7 @@ #include "apl/engine/propertymap.h" #include "apl/primitives/rect.h" #include "apl/engine/state.h" +#include "apl/utils/deprecated.h" #include "apl/utils/noncopyable.h" #include "apl/utils/visitor.h" #include "apl/utils/userdata.h" @@ -247,7 +248,7 @@ class Component : public Counter, /** * @return This component's context */ - std::shared_ptr getContext() const { return mContext; } + ConstContextPtr getContext() const { return mContext; } /** * @return True if this component was properly created with all required @@ -342,7 +343,7 @@ class Component : public Counter, * } * } * - * @param index The zero-based display index of the child. + * @param displayIndex The zero-based display index of the child. * @return The child. */ virtual ComponentPtr getDisplayedChildAt(size_t displayIndex) const = 0; @@ -352,7 +353,7 @@ class Component : public Counter, * children of a sequence to before retrieving the layout bounds. * @deprecated Should not be used. No-op. */ - virtual void ensureLayout(bool useDirtyFlag = false) {} + APL_DEPRECATED virtual void ensureLayout(bool useDirtyFlag = false) {} /** * The bounds of this component within an ancestor. @@ -416,8 +417,8 @@ class Component : public Counter, /** * Serialize a component and its children into a rapidjson object. - * @param allocator - * @return + * @param allocator RapidJSON memory allocator + * @return The component as a RapidJSON object */ virtual rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const = 0; @@ -425,16 +426,16 @@ class Component : public Counter, * Convert this component and all of its properties into a human-readable JSON object. * This method is intended to be used by debugging and testing tools; it is not intended * for viewhosts. - * @param allocator - * @return The object + * @param allocator RapidJSON memory allocator + * @return The component and all of its properties as a RapidJSON object. */ virtual rapidjson::Value serializeAll(rapidjson::Document::AllocatorType& allocator) const = 0; /** - * Serialize all dirty component parameters into a rapidjson array. This clears the dirty + * Serialize all dirty component parameters into an object. This clears the dirty * flags. - * @param allocator - * @return + * @param allocator RapidJSON memory allocator + * @return All dirty properties as a RapidJSON object */ virtual rapidjson::Value serializeDirty(rapidjson::Document::AllocatorType& allocator) = 0; @@ -442,7 +443,7 @@ class Component : public Counter, * @return The descriptive path of the source that created this component * @deprecated Replace with provenance */ - virtual std::string getPath() const { return provenance(); }; + APL_DEPRECATED virtual std::string getPath() const { return provenance(); }; /** * @return The descriptive path of the source that created this component @@ -455,7 +456,7 @@ class Component : public Counter, * @param allocator Allocator for allocating memory for the DOM * @return a json representation of the visual context. */ - virtual rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) = 0; + APL_DEPRECATED virtual rapidjson::Value serializeVisualContext(rapidjson::Document::AllocatorType& allocator) = 0; /** * Find a component at or below this point in the hierarchy with the given id or uniqueId. diff --git a/aplcore/include/apl/component/componentproperties.h b/aplcore/include/apl/component/componentproperties.h index aeb32d2..77c0428 100644 --- a/aplcore/include/apl/component/componentproperties.h +++ b/aplcore/include/apl/component/componentproperties.h @@ -374,6 +374,8 @@ enum PropertyKey { kPropertyAudioTrack, /// VideoComponent autoplay kPropertyAutoplay, + /// VideoComponent muted + kPropertyMuted, /// FrameComponent background color kPropertyBackgroundColor, /// FrameComponent border bottom-left radius (input only) diff --git a/aplcore/include/apl/component/corecomponent.h b/aplcore/include/apl/component/corecomponent.h index 603e1b6..7a9ee52 100644 --- a/aplcore/include/apl/component/corecomponent.h +++ b/aplcore/include/apl/component/corecomponent.h @@ -106,7 +106,7 @@ class CoreComponent : public Component, * that the user can see/interact with. Overrides that have knowledge about which children are off screen or otherwise * invalid/unattached should use that knowledge to reduce the number of nodes walked or avoid walking otherwise invalid * components they may have stashed in their children. - * @param visitor + * @param visitor The component visitor */ virtual void accept(Visitor& visitor) const; @@ -115,7 +115,7 @@ class CoreComponent : public Component, * that the user can see/interact with. Overrides that have knowledge about which children are off screen or otherwise * invalid/unattached should use that knowledge to reduce the number of nodes walked or avoid walking otherwise invalid * components they may have stashed in their children. - * @param visitor + * @param visitor The component visitor */ virtual void raccept(Visitor& visitor) const; @@ -389,21 +389,21 @@ class CoreComponent : public Component, /** * Convert this component into a JSON object - * @param allocator + * @param allocator RapidJSON memory allocator * @return The object. */ rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override; /** * Convert this component and all of its properties into a human-readable JSON object - * @param allocator + * @param allocator RapidJSON memory allocator * @return The object */ rapidjson::Value serializeAll(rapidjson::Document::AllocatorType& allocator) const override; /** * Convert the dirty properties of this component into a JSON object. - * @param allocator + * @param allocator RapidJSON memory allocator * @return The obje */ rapidjson::Value serializeDirty(rapidjson::Document::AllocatorType& allocator) override; @@ -571,7 +571,7 @@ class CoreComponent : public Component, * Attach Component's visual context tags to provided Json object. * NOTE: Required to be called explicitly from overriding methods. * @param outMap object to add tags to. - * @param allocator + * @param allocator RapidJSON memory allocator * @return true if actionable, false otherwise */ virtual bool getTags(rapidjson::Value& outMap, rapidjson::Document::AllocatorType& allocator); @@ -766,8 +766,8 @@ class CoreComponent : public Component, /** * This inline function casts YGMeasureMode enum to MeasureMode enum. - * @param YGMeasureMode - * @return MeasureMode + * @param ygMeasureMode Yoga definition of the measuring mode + * @return APL definition of the measuring mode */ static inline MeasureMode toMeasureMode(YGMeasureMode ygMeasureMode) { @@ -784,21 +784,21 @@ class CoreComponent : public Component, /** * This inline function used in TextComponent and EditTextComponent class for TextMeasurement. - * @param YGNodeRef node - * @param float width - * @param YGMeasureMode widthMode - * @param float height - * @param YGMeasureMode heightMode - * @return YGSize + * @param node The yoga node + * @param width Width in dp + * @param widthMode Width measuring mode - at most, exactly, or undefined + * @param height Height in dp + * @param heightMode Height measuring mode - at most, exactly, or undefined + * @return Size of measured text, in dp */ static YGSize textMeasureFunc(YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode); /** * This inline function used in TextComponent and EditTextComponent class for TextMeasurement. - * @param YGNodeRef node - * @param float width - * @param float height - * @return float + * @param node The yoga node + * @param width The width + * @param height The height + * @return The baseline of the text in dp */ static float textBaselineFunc(YGNodeRef node, float width, float height); diff --git a/aplcore/include/apl/component/edittextcomponent.h b/aplcore/include/apl/component/edittextcomponent.h index 197f3bf..2a649cd 100644 --- a/aplcore/include/apl/component/edittextcomponent.h +++ b/aplcore/include/apl/component/edittextcomponent.h @@ -42,16 +42,11 @@ class EditTextComponent : public ActionableComponent { bool isCharacterValid(const wchar_t wc) const override; - void parseValidCharactersProperty(); - protected: const ComponentPropDefSet& propDefSet() const override; const EventPropertyMap& eventPropertyMap() const override; PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp) override; void executeOnFocus() override; - -private: - CharacterRangesPtr mCharacterRangesPtr; }; } // namespace apl diff --git a/aplcore/include/apl/component/multichildscrollablecomponent.h b/aplcore/include/apl/component/multichildscrollablecomponent.h index 296ab66..15c13f2 100644 --- a/aplcore/include/apl/component/multichildscrollablecomponent.h +++ b/aplcore/include/apl/component/multichildscrollablecomponent.h @@ -16,7 +16,7 @@ #define _APL_MULTICHILD_SCROLLABLE_COMPONENT_H #include "apl/component/scrollablecomponent.h" -#include "apl/utils/range.h" +#include "apl/primitives/range.h" namespace apl { @@ -181,6 +181,7 @@ class MultiChildScrollableComponent : public ScrollableComponent { void fixScrollPosition(const Rect& oldAnchorRect, const Rect& anchorRect); Point getPaddedScrollPosition(LayoutDirection layoutDirection) const; void processLayoutChangesInternal(bool useDirtyFlag, bool first, bool delayed, bool needsFullReProcess); + void scheduleDelayedLayout(); private: Range mIndexesSeen; diff --git a/aplcore/include/apl/component/textmeasurement.h b/aplcore/include/apl/component/textmeasurement.h index 311b9b0..be6b493 100644 --- a/aplcore/include/apl/component/textmeasurement.h +++ b/aplcore/include/apl/component/textmeasurement.h @@ -43,7 +43,7 @@ class TextMeasurement { /** * Install a TextMeasurement object. This will be used for all future * layout calculations. - * @param textMeasurement + * @param textMeasurement The TextMeasurement object */ static void install(const TextMeasurementPtr& textMeasurement); static const TextMeasurementPtr& instance(); diff --git a/aplcore/include/apl/content/aplversion.h b/aplcore/include/apl/content/aplversion.h index ef294df..9e347c7 100644 --- a/aplcore/include/apl/content/aplversion.h +++ b/aplcore/include/apl/content/aplversion.h @@ -24,28 +24,30 @@ class APLVersion { public: enum Value : uint32_t { kAPLVersionIgnore = 0x0, /// Ignore version numbers - kAPLVersion10 = 0x1, /// Support version 1.0 - kAPLVersion11 = 0x1U << 1, /// Support version 1.1 - kAPLVersion12 = 0x1U << 2, /// Support version 1.2 - kAPLVersion13 = 0x1U << 3, /// Support version 1.3 - kAPLVersion14 = 0x1U << 4, /// Support version 1.4 - kAPLVersion15 = 0x1U << 5, /// Support version 1.5 - kAPLVersion16 = 0x1U << 6, /// Support version 1.6 - kAPLVersion17 = 0x1U << 7, /// Support version 1.7 - kAPLVersion18 = 0x1U << 8, /// Support version 1.8 - kAPLVersion19 = 0x1U << 9, /// Support version 1.9 - kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, - kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, - kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, - kAPLVersion10to14 = kAPLVersion10to13 | kAPLVersion14, - kAPLVersion10to15 = kAPLVersion10to14 | kAPLVersion15, - kAPLVersion10to16 = kAPLVersion10to15 | kAPLVersion16, - kAPLVersion10to17 = kAPLVersion10to16 | kAPLVersion17, - kAPLVersion10to18 = kAPLVersion10to17 | kAPLVersion18, - kAPLVersion10to19 = kAPLVersion10to18 | kAPLVersion19, - kAPLVersionLatest = kAPLVersion10to19, /// Support the most recent engine version - kAPLVersionDefault = kAPLVersion10to19, /// Default value - kAPLVersionReported = kAPLVersion19, /// Default reported version + kAPLVersion10 = 0x1, /// Support version 1.0 + kAPLVersion11 = 0x1U << 1, /// Support version 1.1 + kAPLVersion12 = 0x1U << 2, /// Support version 1.2 + kAPLVersion13 = 0x1U << 3, /// Support version 1.3 + kAPLVersion14 = 0x1U << 4, /// Support version 1.4 + kAPLVersion15 = 0x1U << 5, /// Support version 1.5 + kAPLVersion16 = 0x1U << 6, /// Support version 1.6 + kAPLVersion17 = 0x1U << 7, /// Support version 1.7 + kAPLVersion18 = 0x1U << 8, /// Support version 1.8 + kAPLVersion19 = 0x1U << 9, /// Support version 1.9 + kAPLVersion20221 = 0x1U << 10, /// Support version 2022.1 + kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, + kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, + kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, + kAPLVersion10to14 = kAPLVersion10to13 | kAPLVersion14, + kAPLVersion10to15 = kAPLVersion10to14 | kAPLVersion15, + kAPLVersion10to16 = kAPLVersion10to15 | kAPLVersion16, + kAPLVersion10to17 = kAPLVersion10to16 | kAPLVersion17, + kAPLVersion10to18 = kAPLVersion10to17 | kAPLVersion18, + kAPLVersion10to19 = kAPLVersion10to18 | kAPLVersion19, + kAPLVersion10to20221 = kAPLVersion10to19 | kAPLVersion20221, + kAPLVersionLatest = kAPLVersion10to20221, /// Support the most recent engine version + kAPLVersionDefault = kAPLVersion10to20221, /// Default value + kAPLVersionReported = kAPLVersion20221, /// 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 4263464..ed02f31 100644 --- a/aplcore/include/apl/content/configurationchange.h +++ b/aplcore/include/apl/content/configurationchange.h @@ -37,8 +37,8 @@ class ConfigurationChange { /** * Convenience constructor that sets the pixel width and height immediately. - * @param pixelWidth - * @param pixelHeight + * @param pixelWidth The pixel width of the screen + * @param pixelHeight The pixel height of the screen */ ConfigurationChange(int pixelWidth, int pixelHeight) : mFlags(kConfigurationChangeSize), @@ -48,8 +48,8 @@ class ConfigurationChange { /** * Update the size - * @param pixelWidth - * @param pixelHeight + * @param pixelWidth The pixel width of the screen + * @param pixelHeight The pixel height of the screen * @return This object for chaining */ ConfigurationChange& size(int pixelWidth, int pixelHeight) { @@ -143,28 +143,28 @@ class ConfigurationChange { /** * Merge this configuration change into a metrics object. - * @param oldMetrics + * @param oldMetrics The old metrics to merge with this change * @return An new metrics object with these changes */ Metrics mergeMetrics(const Metrics& oldMetrics) const; /** * Merge this configuration change into a root config object - * @param oldRootConfig + * @param oldRootConfig The old root config to merge with this change * @return A new root config with these changes */ RootConfig mergeRootConfig(const RootConfig& oldRootConfig) const; /** * Merge this configuration change into a new size object - * @param oldSize + * @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 + * @param other The old configuration change to merge with this change */ void mergeConfigurationChange(const ConfigurationChange& other); diff --git a/aplcore/include/apl/content/content.h b/aplcore/include/apl/content/content.h index 37568b3..cac003b 100644 --- a/aplcore/include/apl/content/content.h +++ b/aplcore/include/apl/content/content.h @@ -118,7 +118,7 @@ class Content : public Counter { /** * Add a requested package to the document. - * @param info The requested package import structure. + * @param request The requested package import structure. * @param raw Parsed data for the package. */ void addPackage(const ImportRequest& request, JsonData&& raw); @@ -203,10 +203,9 @@ class Content : public Counter { public: /** * Internal constructor. Do not call this directly. - * @param session - * @param mainPackagePtr - * @param mainTemplate - * @param parameterNames + * @param session The APL session + * @param mainPackagePtr The main package + * @param mainTemplate The RapidJSON main template object */ Content(SessionPtr session, PackagePtr mainPackagePtr, diff --git a/aplcore/include/apl/content/directive.h b/aplcore/include/apl/content/directive.h index bf89fff..9837aed 100644 --- a/aplcore/include/apl/content/directive.h +++ b/aplcore/include/apl/content/directive.h @@ -86,7 +86,8 @@ class Directive : public NonCopyable { /** * Basic constructor. Use the create(JsonData&&) function instead. - * @param directive + * @param session The APL session + * @param directive The JsonData of the directive to parse */ Directive(const SessionPtr& session, JsonData&& directive); @@ -119,4 +120,4 @@ class Directive : public NonCopyable { } // namespace apl -#endif // _APL_DIRECTIVE_H \ No newline at end of file +#endif // _APL_DIRECTIVE_H diff --git a/aplcore/include/apl/content/extensioncommanddefinition.h b/aplcore/include/apl/content/extensioncommanddefinition.h index 25ab28d..c931984 100644 --- a/aplcore/include/apl/content/extensioncommanddefinition.h +++ b/aplcore/include/apl/content/extensioncommanddefinition.h @@ -90,7 +90,7 @@ class ExtensionCommandDefinition { /** * Add a named property. The property names "when" and "type" are reserved. - * @param property The property to add + * @param name The property to add * @param defvalue The default value to use for this property when it is not provided. * @param required If true and the property is not provided, the command will not execute. * @return This object for chaining. @@ -131,7 +131,6 @@ class ExtensionCommandDefinition { * Add a named array-ified property. The property will be converted into an array of values. The names "when" * and "type" are reserved. * @param property The property to add - * @param defvalue The default value to use for this property when it is not provided. * @param required If true and the property is not provided, the command will not execute. * @return This object for chaining. */ diff --git a/aplcore/include/apl/content/extensionfilterdefinition.h b/aplcore/include/apl/content/extensionfilterdefinition.h index eb2ee3e..ab782d2 100644 --- a/aplcore/include/apl/content/extensionfilterdefinition.h +++ b/aplcore/include/apl/content/extensionfilterdefinition.h @@ -90,7 +90,7 @@ class ExtensionFilterDefinition { /** * Add a named property. The property names "when", "type", "source", and "destination" are reserved. - * @param property The property name + * @param name The property name * @param defvalue The default value to use for this property when it is not provided. * @param bindingType Binding type. * @return This object for chaining. diff --git a/aplcore/include/apl/content/jsondata.h b/aplcore/include/apl/content/jsondata.h index d2ff03c..6923792 100644 --- a/aplcore/include/apl/content/jsondata.h +++ b/aplcore/include/apl/content/jsondata.h @@ -38,7 +38,7 @@ class JsonData { public: /** * Initialize by moving an existing JSON document. - * @param document + * @param document A RapidJSON document */ JsonData(rapidjson::Document&& document) : mDocument(std::move(document)), @@ -49,7 +49,7 @@ class JsonData { * Initialize by reference to an existing JSON document. The document * is not copied, so another agent must keep it alive during the * lifespan of this object. - * @param value + * @param value A RapidJSON value */ JsonData(const rapidjson::Value& value) : mValuePtr(&value), @@ -58,7 +58,7 @@ class JsonData { /** * Initialize by parsing a std::string. - * @param raw + * @param raw The string */ JsonData(const std::string& raw) : mType(kDocument) @@ -70,7 +70,7 @@ class JsonData { /** * Initialize by parsing a raw string. The string may be released * immediately. - * @param raw + * @param raw The string */ JsonData(const char *raw) : mType(raw ? kDocument : kNullPtr) @@ -84,7 +84,7 @@ class JsonData { * Initialize by parsing a raw string in situ. The string may be * modified. Another agent must keep the raw string in memory until * this object is destroyed. - * @param raw + * @param raw The string */ JsonData(char *raw) : mType(raw ? kDocument : kNullPtr) diff --git a/aplcore/include/apl/content/rootconfig.h b/aplcore/include/apl/content/rootconfig.h index 1806e03..5b16870 100644 --- a/aplcore/include/apl/content/rootconfig.h +++ b/aplcore/include/apl/content/rootconfig.h @@ -32,6 +32,7 @@ #include "apl/livedata/liveobject.h" #include "apl/primitives/color.h" #include "apl/primitives/dimension.h" +#include "apl/utils/deprecated.h" #include "apl/utils/stringfunctions.h" #ifdef ALEXAEXTENSIONS @@ -131,23 +132,6 @@ class RootConfig { */ const Context& evaluationContext(); - /** - * Configure the agent name and version - * @deprecated Use set({ - {RootProperty::kAgentName, agentName}, - {RootProperty::kAgentVersion, agentVersion} - }) instead - * @param agentName - * @param agentVersion - * @return This object for chaining - */ - RootConfig& agent(const std::string& agentName, const std::string& agentVersion) { - return set({ - {RootProperty::kAgentName, agentName}, - {RootProperty::kAgentVersion, agentVersion} - }); - } - /** * Add a text measurement object for calculating the size of blocks * of text and calculating the baseline of text. @@ -199,50 +183,9 @@ class RootConfig { return *this; } - /** - * Set if the OpenURL command is supported - * @deprecated Use set(RootProperty::kAllowOpenUrl, allowed) instead - * @param allowed If true, the OpenURL command is supported. - * @return This object for chaining - */ - RootConfig& allowOpenUrl(bool allowed) { - return set(RootProperty::kAllowOpenUrl, allowed); - } - - /** - * Set if video is supported - * @deprecated Use set(RootProperty::kDisallowVideo, disallowed) instead - * @param disallowed If true, the Video component is disabled - * @return This object for chaining - */ - RootConfig& disallowVideo(bool disallowed) { - return set(RootProperty::kDisallowVideo, disallowed); - } - - /** - * Set the quality of animation expected. If set to kAnimationQualityNone, - * all animation commands are disabled (include onMount). - * @deprecated Use set(RootProperty::kAnimationQuality, quality) instead - * @param quality The expected quality of animation playback. - * @return This object for chaining - */ - RootConfig& animationQuality(AnimationQuality quality) { - return set(RootProperty::kAnimationQuality, quality); - } - - /** - * Set the default idle timeout. - * @deprecated Use set(RootProperty::kDefaultIdleTimeout, idleTimeout) instead - * @param idleTimeout Device wide idle timeout. - * @return This object for chaining - */ - RootConfig& defaultIdleTimeout(int idleTimeout) { - return set(RootProperty::kDefaultIdleTimeout, idleTimeout); - } - /** * Set how APL spec version check should be enforced. - * @param version @see APLVersion::Value. + * @param version The require APL version. @see APLVersion::Value. * @return This object for chaining */ RootConfig& enforceAPLVersion(APLVersion::Value version) { @@ -250,73 +193,6 @@ class RootConfig { return *this; } - /** - * Set the reported APL version of the specification that is supported - * by this application. This value will be reported in the data-binding - * context under "environment.aplVersion". - * @deprecated Use set(RootProperty::kReportedVersion, version) instead - * @param version The version string to report. - * @return This object for chaining. - */ - RootConfig& reportedAPLVersion(const std::string& version) { - return set(RootProperty::kReportedVersion, version); - } - - /** - * Sets whether the "type" field of an APL document should be enforced. - * Type should always be "APL", but for backwards compatibility, this is - * optionally ignored. - * @deprecated Use set(RootProperty::kEnforceTypeField, enforce) instead - * @param enforce `true` to enforce that the "type" field is set to "APL" - * @return This object for chaining - */ - RootConfig& enforceTypeField(bool enforce) { - return set(RootProperty::kEnforceTypeField, enforce); - } - - /** - * Set the default font color. This is the fallback color for all themes. - * This color will only be applied if there is not a theme-defined default color. - * @deprecated Use set(RootProperty::kDefaultFontColor, color) instead - * @param color The font color - * @return This object for chaining - */ - RootConfig& defaultFontColor(Color color) { - return set(RootProperty::kDefaultFontColor, color); - } - - /** - * Set the default font color for a particular theme. - * @deprecated Use set(RootProperty::kAllowOpenUrl, allowed) instead - * @param theme The named theme (must match exactly) - * @param color The font color - * @return This object for chaining - */ - RootConfig& defaultFontColor(const std::string& theme, Color color) { - mDefaultThemeFontColor[theme] = color; - return *this; - } - - /** - * Set the default font family. This is usually locale-based. - * @deprecated Use set(RootProperty::kDefaultFontFamily, fontFamily) instead - * @param fontFamily The font family. - * @return This object for chaining. - */ - RootConfig& defaultFontFamily(const std::string& fontFamily) { - return set(RootProperty::kDefaultFontFamily, fontFamily); - } - - /** - * Enable or disable tracking of resource, style, and component provenance - * @deprecated Use set(RootProperty::kTrackProvenance, trackProvenance) instead - * @param trackProvenance True if provenance should be tracked. - * @return This object for chaining. - */ - RootConfig& trackProvenance(bool trackProvenance) { - return set(RootProperty::kTrackProvenance, trackProvenance); - } - /** * Set the default size of a built-in component. This applies to both horizontal and vertical components * @param type The component type. @@ -343,28 +219,6 @@ class RootConfig { return *this; } - /** - * Set pager layout cache in both directions. 1 is default and results in 1 page ensured before and one after - * current one. - * @deprecated Use set(RootProperty::kPagerChildCache, cache) instead - * @param cache Number of pages to ensure before and after current one. - * @return This object for chaining. - */ - RootConfig& pagerChildCache(int cache) { - return set(RootProperty::kPagerChildCache, cache); - } - - /** - * Set sequence layout cache in both directions. 1 is default and results in 1 page of children ensured before and - * one after current one. - * @deprecated Use set(RootProperty::kSequenceChildCache, cache) instead - * @param cache Number of pages to ensure before and after current one. - * @return This object for chaining. - */ - RootConfig& sequenceChildCache(int cache) { - return set(RootProperty::kSequenceChildCache, cache); - } - /** * Add DataSource provider implementation. * @param type Type name of DataSource. @@ -383,27 +237,6 @@ class RootConfig { */ RootConfig& session(const SessionPtr& session); - /** - * Set the current UTC time in milliseconds since the epoch. - * @deprecated Use set(RootProperty::kUTCTime, time) instead - * @param time Milliseconds. - * @return This object for chaining. - */ - RootConfig& utcTime(apl_time_t time) { - return set(RootProperty::kUTCTime, time); - } - - /** - * Set the local time zone adjustment in milliseconds. When added to the current UTC time, - * this will give the local time. This includes any daylight saving time adjustment. - * @deprecated Use set(RootProperty::kLocalTimeAdjustment, adjustment) instead - * @param adjustment Milliseconds - * @return This object for chaining - */ - RootConfig& localTimeAdjustment(apl_duration_t adjustment) { - return set(RootProperty::kLocalTimeAdjustment, adjustment); - } - /** * Assign a LiveObject to the top-level context * @param name The name of the LiveObject @@ -491,8 +324,7 @@ class RootConfig { /** * Register an extension event handler. The name should be something like 'onDomainAction'. * This method will also register the extension as a supported extension. - * @param uri The extension URI this handler is registered to - * @param name The name of the handler to support. + * @param handler The name of the handler to support. * @return This object for chaining. */ RootConfig& registerExtensionEventHandler(ExtensionEventHandler handler) { @@ -567,7 +399,7 @@ class RootConfig { /** * Register a supported extension. Any previously registered configuration is overwritten. * @param uri The URI of the extension - * @param optional configuration value(s) supported by this extension. + * @param config Configuration value(s) supported by this extension. * @return This object for chaining */ RootConfig& registerExtension(const std::string& uri, const Object& config = Object::TRUE_OBJECT()) { @@ -605,13 +437,194 @@ class RootConfig { */ RootConfig& setEnvironmentValue(const std::string& name, const Object& value); + /** + * Enable experimental feature. @see enum ExperimentalFeatures for available set. + * None of the features enabled by default. + * @experimental Not guaranteed to work for any of available features and can change Engine behaviors drastically. + * @param feature experimental feature to enable. + * @return This object for chaining + */ + RootConfig& enableExperimentalFeature(ExperimentalFeature feature) { + mEnabledExperimentalFeatures.emplace(feature); + return *this; + } + + /** + * Set the default font color for a particular theme. + * @param theme The named theme (must match exactly) + * @param color The font color + * @return This object for chaining + */ + RootConfig& defaultFontColor(const std::string& theme, Color color) { + mDefaultThemeFontColor[theme] = color; + return *this; + } + + ///////////////////////////////////////////////// + + /** + * Configure the agent name and version + * @deprecated Use set({ + {RootProperty::kAgentName, agentName}, + {RootProperty::kAgentVersion, agentVersion} + }) instead + * @param agentName The name of the APL agent + * @param agentVersion The version of the APL agent + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& agent(const std::string& agentName, const std::string& agentVersion) { + return set({ + {RootProperty::kAgentName, agentName}, + {RootProperty::kAgentVersion, agentVersion} + }); + } + + /** + * Set if the OpenURL command is supported + * @deprecated Use set(RootProperty::kAllowOpenUrl, allowed) instead + * @param allowed If true, the OpenURL command is supported. + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& allowOpenUrl(bool allowed) { + return set(RootProperty::kAllowOpenUrl, allowed); + } + + /** + * Set if video is supported + * @deprecated Use set(RootProperty::kDisallowVideo, disallowed) instead + * @param disallowed If true, the Video component is disabled + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& disallowVideo(bool disallowed) { + return set(RootProperty::kDisallowVideo, disallowed); + } + + /** + * Set the quality of animation expected. If set to kAnimationQualityNone, + * all animation commands are disabled (include onMount). + * @deprecated Use set(RootProperty::kAnimationQuality, quality) instead + * @param quality The expected quality of animation playback. + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& animationQuality(AnimationQuality quality) { + return set(RootProperty::kAnimationQuality, quality); + } + + /** + * Set the default idle timeout. + * @deprecated Use set(RootProperty::kDefaultIdleTimeout, idleTimeout) instead + * @param idleTimeout Device wide idle timeout. + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& defaultIdleTimeout(int idleTimeout) { + return set(RootProperty::kDefaultIdleTimeout, idleTimeout); + } + + /** + * Set the reported APL version of the specification that is supported + * by this application. This value will be reported in the data-binding + * context under "environment.aplVersion". + * @deprecated Use set(RootProperty::kReportedVersion, version) instead + * @param version The version string to report. + * @return This object for chaining. + */ + APL_DEPRECATED RootConfig& reportedAPLVersion(const std::string& version) { + return set(RootProperty::kReportedVersion, version); + } + + /** + * Sets whether the "type" field of an APL document should be enforced. + * Type should always be "APL", but for backwards compatibility, this is + * optionally ignored. + * @deprecated Use set(RootProperty::kEnforceTypeField, enforce) instead + * @param enforce `true` to enforce that the "type" field is set to "APL" + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& enforceTypeField(bool enforce) { + return set(RootProperty::kEnforceTypeField, enforce); + } + + /** + * Set the default font color. This is the fallback color for all themes. + * This color will only be applied if there is not a theme-defined default color. + * @deprecated Use set(RootProperty::kDefaultFontColor, color) instead + * @param color The font color + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& defaultFontColor(Color color) { + return set(RootProperty::kDefaultFontColor, color); + } + + /** + * Set the default font family. This is usually locale-based. + * @deprecated Use set(RootProperty::kDefaultFontFamily, fontFamily) instead + * @param fontFamily The font family. + * @return This object for chaining. + */ + APL_DEPRECATED RootConfig& defaultFontFamily(const std::string& fontFamily) { + return set(RootProperty::kDefaultFontFamily, fontFamily); + } + + /** + * Enable or disable tracking of resource, style, and component provenance + * @deprecated Use set(RootProperty::kTrackProvenance, trackProvenance) instead + * @param trackProvenance True if provenance should be tracked. + * @return This object for chaining. + */ + APL_DEPRECATED RootConfig& trackProvenance(bool trackProvenance) { + return set(RootProperty::kTrackProvenance, trackProvenance); + } + + /** + * Set pager layout cache in both directions. 1 is default and results in 1 page ensured before and one after + * current one. + * @deprecated Use set(RootProperty::kPagerChildCache, cache) instead + * @param cache Number of pages to ensure before and after current one. + * @return This object for chaining. + */ + APL_DEPRECATED RootConfig& pagerChildCache(int cache) { + return set(RootProperty::kPagerChildCache, cache); + } + + /** + * Set sequence layout cache in both directions. 1 is default and results in 1 page of children ensured before and + * one after current one. + * @deprecated Use set(RootProperty::kSequenceChildCache, cache) instead + * @param cache Number of pages to ensure before and after current one. + * @return This object for chaining. + */ + APL_DEPRECATED RootConfig& sequenceChildCache(int cache) { + return set(RootProperty::kSequenceChildCache, cache); + } + + /** + * Set the current UTC time in milliseconds since the epoch. + * @deprecated Use set(RootProperty::kUTCTime, time) instead + * @param time Milliseconds. + * @return This object for chaining. + */ + APL_DEPRECATED RootConfig& utcTime(apl_time_t time) { + return set(RootProperty::kUTCTime, time); + } + + /** + * Set the local time zone adjustment in milliseconds. When added to the current UTC time, + * this will give the local time. This includes any daylight saving time adjustment. + * @deprecated Use set(RootProperty::kLocalTimeAdjustment, adjustment) instead + * @param adjustment Milliseconds + * @return This object for chaining + */ + APL_DEPRECATED RootConfig& localTimeAdjustment(apl_duration_t adjustment) { + return set(RootProperty::kLocalTimeAdjustment, adjustment); + } + /** * Set double press timeout. * @deprecated Use set(RootProperty::kDoublePressTimeout, timeout) instead * @param timeout new double press timeout. Default is 500 ms. * @return This object for chaining */ - RootConfig& doublePressTimeout(apl_duration_t timeout) { + APL_DEPRECATED RootConfig& doublePressTimeout(apl_duration_t timeout) { return set(RootProperty::kDoublePressTimeout, timeout); } @@ -621,7 +634,7 @@ class RootConfig { * @param timeout new long press timeout. Default is 1000 ms. * @return This object for chaining */ - RootConfig& longPressTimeout(apl_duration_t timeout) { + APL_DEPRECATED RootConfig& longPressTimeout(apl_duration_t timeout) { return set(RootProperty::kLongPressTimeout, timeout); } @@ -632,7 +645,7 @@ class RootConfig { * @param timeout Duration in milliseconds. Default is 64 ms. * @return This object for chaining */ - RootConfig& pressedDuration(apl_duration_t timeout) { + APL_DEPRECATED RootConfig& pressedDuration(apl_duration_t timeout) { return set(RootProperty::kPressedDuration, timeout); } @@ -643,7 +656,7 @@ class RootConfig { * @param timeout Duration in milliseconds. Default is 100 ms. * @return This object for chaining */ - RootConfig& tapOrScrollTimeout(apl_duration_t timeout) { + APL_DEPRECATED RootConfig& tapOrScrollTimeout(apl_duration_t timeout) { return set(RootProperty::kTapOrScrollTimeout, timeout); } @@ -654,7 +667,7 @@ class RootConfig { * @param distance threshold distance. * @return This object for chaining */ - RootConfig& swipeAwayTriggerDistanceThreshold(float distance) { + APL_DEPRECATED RootConfig& swipeAwayTriggerDistanceThreshold(float distance) { return set(RootProperty::kPointerSlopThreshold, distance); } @@ -665,7 +678,7 @@ class RootConfig { * @param distance threshold distance. * @return This object for chaining */ - RootConfig& swipeAwayFulfillDistancePercentageThreshold(float distance) { + APL_DEPRECATED RootConfig& swipeAwayFulfillDistancePercentageThreshold(float distance) { return set(RootProperty::kSwipeAwayFulfillDistancePercentageThreshold, distance); } @@ -675,7 +688,7 @@ class RootConfig { * @param easing Easing string to use for gesture animation. Should be according to current APL spec. * @return This object for chaining */ - RootConfig& swipeAwayAnimationEasing(const std::string& easing) { + APL_DEPRECATED RootConfig& swipeAwayAnimationEasing(const std::string& easing) { return set(RootProperty::kSwipeAwayAnimationEasing, easing); } @@ -685,7 +698,7 @@ class RootConfig { * @param velocity swipe velocity threshold in dp per second. * @return This object for chaining */ - RootConfig& swipeVelocityThreshold(float velocity) { + APL_DEPRECATED RootConfig& swipeVelocityThreshold(float velocity) { return set(RootProperty::kSwipeVelocityThreshold, velocity); } @@ -695,7 +708,7 @@ class RootConfig { * @param velocity max swipe velocity in dp per second. * @return This object for chaining */ - RootConfig& swipeMaxVelocity(float velocity) { + APL_DEPRECATED RootConfig& swipeMaxVelocity(float velocity) { return set(RootProperty::kSwipeMaxVelocity, velocity); } @@ -705,7 +718,7 @@ class RootConfig { * @param degrees swipe direction tolerance, in degrees. * @return This object for chaining */ - RootConfig& swipeAngleTolerance(float degrees) { + APL_DEPRECATED RootConfig& swipeAngleTolerance(float degrees) { return set(RootProperty::kSwipeAngleTolerance, degrees); } @@ -715,7 +728,7 @@ class RootConfig { * @param duration the default duration for animations, in ms. * @return This object for chaining */ - RootConfig& defaultSwipeAnimationDuration(apl_duration_t duration) { + APL_DEPRECATED RootConfig& defaultSwipeAnimationDuration(apl_duration_t duration) { return set(RootProperty::kDefaultSwipeAnimationDuration, duration); } @@ -725,7 +738,7 @@ class RootConfig { * @param duration the maximum duration for animations, in ms. * @return This object for chaining */ - RootConfig& maxSwipeAnimationDuration(apl_duration_t duration) { + APL_DEPRECATED RootConfig& maxSwipeAnimationDuration(apl_duration_t duration) { return set(RootProperty::kMaxSwipeAnimationDuration, duration); } @@ -735,7 +748,7 @@ class RootConfig { * @param velocity Fling velocity in dp per second. * @return This object for chaining */ - RootConfig& minimumFlingVelocity(float velocity) { + APL_DEPRECATED RootConfig& minimumFlingVelocity(float velocity) { return set(RootProperty::kMinimumFlingVelocity, velocity); } @@ -745,7 +758,7 @@ class RootConfig { * @param updateLimit update limit in ms. Should be > 0. * @return This object for chaining */ - RootConfig& tickHandlerUpdateLimit(apl_duration_t updateLimit) { + APL_DEPRECATED RootConfig& tickHandlerUpdateLimit(apl_duration_t updateLimit) { return set(RootProperty::kTickHandlerUpdateLimit, updateLimit); } @@ -755,7 +768,7 @@ class RootConfig { * @param scale The scaling factor. Default is 1.0 * @return This object for chaining */ - RootConfig& fontScale(float scale) { + APL_DEPRECATED RootConfig& fontScale(float scale) { return set(RootProperty::kFontScale, scale); } @@ -765,7 +778,7 @@ class RootConfig { * @param screenMode The screen display mode * @return This object for chaining */ - RootConfig& screenMode(ScreenMode screenMode) { + APL_DEPRECATED RootConfig& screenMode(ScreenMode screenMode) { return set(RootProperty::kScreenMode, screenMode); } @@ -775,7 +788,7 @@ class RootConfig { * @param enabled True if the screen reader is enabled * @return This object for chaining */ - RootConfig& screenReader(bool enabled) { + APL_DEPRECATED RootConfig& screenReader(bool enabled) { return set(RootProperty::kScreenReader, enabled); } @@ -785,7 +798,7 @@ class RootConfig { * @param timeout inactivity timeout in ms. * @return This object for chaining */ - RootConfig& pointerInactivityTimeout(apl_duration_t timeout) { + APL_DEPRECATED RootConfig& pointerInactivityTimeout(apl_duration_t timeout) { return set(RootProperty::kPointerInactivityTimeout, timeout); } @@ -795,7 +808,7 @@ class RootConfig { * @param velocity fling gesture velocity in dp per second. * @return This object for chaining */ - RootConfig& maximumFlingVelocity(float velocity) { + APL_DEPRECATED RootConfig& maximumFlingVelocity(float velocity) { return set(RootProperty::kMaximumFlingVelocity, velocity); } @@ -806,7 +819,7 @@ class RootConfig { * @param distance threshold distance. * @return This object for chaining */ - RootConfig& pointerSlopThreshold(float distance) { + APL_DEPRECATED RootConfig& pointerSlopThreshold(float distance) { return set(RootProperty::kPointerSlopThreshold, distance); } @@ -816,7 +829,7 @@ class RootConfig { * @param duration duration in ms. * @return This object for chaining */ - RootConfig& scrollCommandDuration(apl_duration_t duration) { + APL_DEPRECATED RootConfig& scrollCommandDuration(apl_duration_t duration) { return set(RootProperty::kScrollCommandDuration, duration); } @@ -826,7 +839,7 @@ class RootConfig { * @param duration duration in ms. * @return This object for chaining */ - RootConfig& scrollSnapDuration(apl_duration_t duration) { + APL_DEPRECATED RootConfig& scrollSnapDuration(apl_duration_t duration) { return set(RootProperty::kScrollSnapDuration, duration); } @@ -836,21 +849,11 @@ class RootConfig { * @param duration duration in ms. * @return This object for chaining */ - RootConfig& defaultPagerAnimationDuration(apl_duration_t duration) { + APL_DEPRECATED RootConfig& defaultPagerAnimationDuration(apl_duration_t duration) { return set(RootProperty::kDefaultPagerAnimationDuration, duration); } - /** - * Enable experimental feature. @see enum ExperimentalFeatures for available set. - * None of the features enabled by default. - * @experimental Not guaranteed to work for any of available features and can change Engine behaviors drastically. - * @param feature experimental feature to enable. - * @return This object for chaining - */ - RootConfig& enableExperimentalFeature(ExperimentalFeature feature) { - mEnabledExperimentalFeatures.emplace(feature); - return *this; - } + /// Specific getters /** * Get RootConfig property @@ -1029,7 +1032,8 @@ class RootConfig { const std::map& getLiveObjectMap() const { return mLiveObjectMap; } /** - * @param LiveData name. + * Retrieve all registered LiveDataWatches for a provided name. + * @param name The name to look up * @return Registered LiveDataWatcher for provided name. */ const std::vector getLiveDataWatchers(const std::string& name) const { diff --git a/aplcore/include/apl/content/settings.h b/aplcore/include/apl/content/settings.h index 2c242a2..dab02df 100644 --- a/aplcore/include/apl/content/settings.h +++ b/aplcore/include/apl/content/settings.h @@ -23,6 +23,7 @@ #include "apl/primitives/object.h" #include "apl/content/rootconfig.h" #include "apl/content/package.h" +#include "apl/utils/deprecated.h" namespace apl { @@ -59,7 +60,7 @@ class Settings { * @return Recommended time in milliseconds that the document should be kept on the screen * before closing due to inactivity. */ - int idleTimeout() const { + APL_DEPRECATED int idleTimeout() const { Object value = getValue("idleTimeout"); if (value.isNumber()) { auto idle = value.getInteger(); diff --git a/aplcore/include/apl/datagrammar/databindingerrors.h b/aplcore/include/apl/datagrammar/databindingerrors.h index 4df1d73..34b9d5b 100644 --- a/aplcore/include/apl/datagrammar/databindingerrors.h +++ b/aplcore/include/apl/datagrammar/databindingerrors.h @@ -29,7 +29,7 @@ namespace datagrammar { * if a problem occurs. The static "error_value" defined in this template converts * from a templated action to a numbered error message. The "errorToString" method * further converts the error message into a human-readable string. - * @tparam Rule + * @tparam Rule The base type for the rules */ template< typename Rule > struct error_control : tao::pegtl::normal< Rule > { @@ -44,8 +44,8 @@ struct error_control : tao::pegtl::normal< Rule > { /** * Convenience routine for printing out the current character being processed * by the PEGTL grammar. - * @tparam Input - * @param in + * @tparam Input PEGTL input + * @param in Input data * @return A string showing the character (if printable) and the numeric value of the character. */ template< typename Input > std::string @@ -90,7 +90,7 @@ const bool TRACED_ERROR_CONTROL_SHOW_FAILURE = false; // Log failed blocks /** * Fancing PEGTL parsing error controller. This is enabled with DEBUG_DATA_BINDING. * The messages are output as the PEGTL grammar is parsed. - * @tparam Rule + * @tparam Rule The base type for the rules */ template< typename Rule > struct traced_error_control : error_control diff --git a/aplcore/include/apl/datagrammar/databindingrules.h b/aplcore/include/apl/datagrammar/databindingrules.h index 2f4d414..1135c1f 100644 --- a/aplcore/include/apl/datagrammar/databindingrules.h +++ b/aplcore/include/apl/datagrammar/databindingrules.h @@ -24,6 +24,7 @@ #include "apl/primitives/object.h" +#include "apl/utils/stringfunctions.h" #ifdef APL_CORE_UWP #undef TRUE @@ -53,7 +54,7 @@ template<> struct action< number > { template< typename Input > static void apply(const Input& in, ByteCodeAssembler& assembler) { - double value = std::stod(in.string()); + double value = sutil::stod(in.string()); if (fitsInBCI(value)) assembler.loadImmediate(asBCI(value)); else diff --git a/aplcore/include/apl/datagrammar/databindingstack.h b/aplcore/include/apl/datagrammar/databindingstack.h index 47430e7..4526272 100644 --- a/aplcore/include/apl/datagrammar/databindingstack.h +++ b/aplcore/include/apl/datagrammar/databindingstack.h @@ -351,7 +351,7 @@ class Stacks void dump() { - LOG(LogLevel::kDebug) << "Stacks=" << mStack.size(); + LOG(LogLevel::kDebug).session(mContext) << "Stacks=" << mStack.size(); for (auto& m : mStack) m.dump(); } diff --git a/aplcore/include/apl/datasource/datasourceprovider.h b/aplcore/include/apl/datasource/datasourceprovider.h index 7f6c09f..facb8ed 100644 --- a/aplcore/include/apl/datasource/datasourceprovider.h +++ b/aplcore/include/apl/datasource/datasourceprovider.h @@ -38,7 +38,9 @@ class DataSourceProvider : public NonCopyable { * @param liveArray pointer to base LiveArray. * @return connection if succeeded, nullptr otherwise. */ - virtual std::shared_ptr create(const Object& dataSourceDefinition, std::weak_ptr, std::weak_ptr liveArray) = 0; + virtual std::shared_ptr create(const Object& dataSourceDefinition, + std::weak_ptr context, + std::weak_ptr liveArray) = 0; /** * Parse update payload and pass it to relevant connection. diff --git a/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h b/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h index 7fc3b4b..dc2ff48 100644 --- a/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h +++ b/aplcore/include/apl/datasource/dynamicindexlistdatasourceprovider.h @@ -190,7 +190,7 @@ class DynamicIndexListDataSourceProvider : public DynamicListDataSourceProvider, * @param type DataSource type. * @param cacheChunkSize size of cache chunk. Effectively means how many items around ensured one should be available. */ - DynamicIndexListDataSourceProvider(const std::string& type, size_t cacheChunkSize); + APL_DEPRECATED DynamicIndexListDataSourceProvider(const std::string& type, size_t cacheChunkSize); /** * @param config Full configuration object. diff --git a/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h b/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h index a4cca24..0ccf351 100644 --- a/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h +++ b/aplcore/include/apl/datasource/dynamiclistdatasourceprovider.h @@ -92,7 +92,14 @@ class DynamicListDataSourceConnection : public OffsetIndexDataSourceConnection, /** * @return context object */ - std::shared_ptr getContext() { return mContext.lock(); } + ContextPtr getContext() { return mContext.lock(); } + + /** + * Replace attached context. + * + * @param context context object + */ + void setContext(const ContextPtr& context) { mContext = context; } protected: /** @@ -105,7 +112,8 @@ 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 std::string& reason, const Object& operationIndex, const std::string& message); + void constructAndReportError(const SessionPtr& session, const std::string& reason, const Object& operationIndex, + const std::string& message); std::weak_ptr mContext; DynamicListConfiguration mConfiguration; @@ -162,14 +170,23 @@ class DynamicListDataSourceProvider : public DataSourceProvider { DLConnectionPtr getConnection(const std::string& listId, const Object& correlationToken); void constructAndReportError( + const SessionPtr& session, const std::string& reason, const std::string& listId, const Object& listVersion, const Object& operationIndex, const std::string& message); - void constructAndReportError(const std::string& reason, const std::string& listId, const std::string& message); void constructAndReportError( - const std::string& reason, const DLConnectionPtr& connection, const Object& operationIndex, const std::string& message); + const SessionPtr& session, + const std::string& reason, + const std::string& listId, + const std::string& message); + void constructAndReportError( + const SessionPtr& session, + const std::string& reason, + const DLConnectionPtr& connection, + const Object& operationIndex, + const std::string& message); bool canFetch(const Object& correlationToken, const DLConnectionPtr& connection); diff --git a/aplcore/include/apl/engine/arrayify.h b/aplcore/include/apl/engine/arrayify.h index fb56052..295321b 100644 --- a/aplcore/include/apl/engine/arrayify.h +++ b/aplcore/include/apl/engine/arrayify.h @@ -132,8 +132,8 @@ arrayifyProperty(const rapidjson::Value& value) * * This function does NOT perform any data-binding. * - * @tparam T - * @tparam Types + * @tparam T The type of the first argument. Normally std::string + * @tparam Types The type of additional arguments. * @param value The object to extract the named property from. * @param name The name of the first child property to look for * @param other The names of the other child properties to look or @@ -215,8 +215,8 @@ arrayifyProperty(const Context& context, const Object& value) * This function performs data-binding and interpolates arrays as per the function * arrayify(const Context&, const Object&). * - * @tparam T - * @tparam Types + * @tparam T The type of the first property. Normally a std::string. + * @tparam Types The type of the additional properties. * @param context The data-binding context * @param value The object to extract the named property from. * @param name The name of the property to extract and array-ify. @@ -234,11 +234,10 @@ arrayifyProperty(const Context& context, const Object& value, T name, Types... o /** * These routines do arrayification, but they return the result as a single Object that contains an array. - * @param context - * @param value - * @return + * @param context The data-binding context. + * @param value The value to arrayify. + * @return The result of arrayification, stored in a single Object */ - extern Object arrayifyAsObject(const Context& context, const Object& value); inline Object diff --git a/aplcore/include/apl/engine/context.h b/aplcore/include/apl/engine/context.h index 8bc912d..695c8b8 100644 --- a/aplcore/include/apl/engine/context.h +++ b/aplcore/include/apl/engine/context.h @@ -351,7 +351,6 @@ class Context : public RecalculateTarget, * @param key The string key name * @param value The value to store * @param path The path data to associate with this key - * @return True if the key already exists in this context. */ void putResource(const std::string& key, const Object& value, const Path& path) { // Toss away a resource if it already exists (we overwrite it) @@ -418,13 +417,13 @@ class Context : public RecalculateTarget, /** * @return The parent of this context or nullptr if there is no parent */ - std::shared_ptr parent() const { return mParent; } + ConstContextPtr parent() const { return mParent; } ContextPtr parent() { return mParent; } /** * @return The top context for data evaluation */ - std::shared_ptr top() const { return mTop ? mTop : shared_from_this(); } + ConstContextPtr top() const { return mTop ? mTop : shared_from_this(); } ContextPtr top() { return mTop ? mTop : shared_from_this(); } /** @@ -533,19 +532,31 @@ class Context : public RecalculateTarget, /** * Internal routine used by components to mark themselves as changed. - * @param id The id of the component. + * @param ptr The component to mark */ void setDirty(const ComponentPtr& ptr); + + /** + * Internal routine used by components to mark themselves as no longer dirty + * @param ptr The component to unmark + */ void clearDirty(const ComponentPtr& ptr); /** - * Internal routine used by components to mark/unmark/test when the visual context may have changed. + * Internal routine used by components to mark when the visual context may have changed. + * @param ptr The component to mark */ void setDirtyVisualContext(const ComponentPtr& ptr); + + /** + * Internal routine used by components to check the dirty state of the visual context + * @param ptr The component to check + */ bool isVisualContextDirty(const ComponentPtr& ptr); /** * Internal routine used by dynamic datasources to mark/unmark/test when the datasource context may have changed. + * @param ptr The data source connection to mark */ void setDirtyDataSourceContext(const DataSourceConnectionPtr& ptr); diff --git a/aplcore/include/apl/engine/contextdependant.h b/aplcore/include/apl/engine/contextdependant.h index 3a675f0..da85af0 100644 --- a/aplcore/include/apl/engine/contextdependant.h +++ b/aplcore/include/apl/engine/contextdependant.h @@ -55,12 +55,11 @@ class ContextDependant : public Dependant { /** * Internal constructor - do not call. Use ContextDependant::create instead. - * @param downstreamContext - * - * @param evaluationContext - * @param name - * @param node - * @param func + * @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, diff --git a/aplcore/include/apl/engine/contextwrapper.h b/aplcore/include/apl/engine/contextwrapper.h index da9a4b3..51a03f3 100644 --- a/aplcore/include/apl/engine/contextwrapper.h +++ b/aplcore/include/apl/engine/contextwrapper.h @@ -32,17 +32,17 @@ namespace apl { */ class ContextWrapper : public ObjectData { public: - static std::shared_ptr create(const std::shared_ptr& context) { + static std::shared_ptr create(const ConstContextPtr& context) { return std::make_shared(context); } - ContextWrapper(const std::shared_ptr &context) : mContext(context) {} + ContextWrapper(const ConstContextPtr& context) : mContext(context) {} std::string toDebugString() const override { return "Context<>"; } - Object get(const std::string &key) const override { + Object get(const std::string& key) const override { auto context = mContext.lock(); if (context) return context->opt(key); @@ -50,7 +50,7 @@ class ContextWrapper : public ObjectData { return Object::NULL_OBJECT(); } - bool has(const std::string &key) const override { + bool has(const std::string& key) const override { auto context = mContext.lock(); if (context) return context->has(key); @@ -58,7 +58,7 @@ class ContextWrapper : public ObjectData { return false; } - Object opt(const std::string &key, const Object &def) const override { + Object opt(const std::string& key, const Object& def) const override { auto context = mContext.lock(); if (context) { auto cr = context->find(key); @@ -79,12 +79,12 @@ class ContextWrapper : public ObjectData { bool empty() const override { return mContext.expired(); } - bool operator==(const ContextWrapper &rhs) const { + bool operator==(const ContextWrapper& rhs) const { return !mContext.owner_before(rhs.mContext) && !rhs.mContext.owner_before(mContext); } // Context wrappers intentionally return an empty object - rapidjson::Value serialize(rapidjson::Document::AllocatorType &allocator) const override { + rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override { return rapidjson::Value(rapidjson::kObjectType); } diff --git a/aplcore/include/apl/engine/evaluate.h b/aplcore/include/apl/engine/evaluate.h index 7d88fe2..90c4332 100644 --- a/aplcore/include/apl/engine/evaluate.h +++ b/aplcore/include/apl/engine/evaluate.h @@ -87,7 +87,7 @@ Object reevaluate(const Context& context, const Object& equation); * be evaluated for data-binding. * @param context The data-binding context. * @param object The object to evaluate. - * @return + * @return The result of recursive evaluation. */ Object evaluateRecursive(const Context& context, const Object& object); diff --git a/aplcore/include/apl/engine/event.h b/aplcore/include/apl/engine/event.h index 659cc50..4b57b9f 100644 --- a/aplcore/include/apl/engine/event.h +++ b/aplcore/include/apl/engine/event.h @@ -323,8 +323,8 @@ class Event : public UserData { /** * Serialize this event into a JSON object - * @param allocator - * @return + * @param allocator RapidJSON memory allocator + * @return The event serialized as a RapidJSON object */ rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; diff --git a/aplcore/include/apl/engine/keyboardmanager.h b/aplcore/include/apl/engine/keyboardmanager.h index b11e110..4140700 100644 --- a/aplcore/include/apl/engine/keyboardmanager.h +++ b/aplcore/include/apl/engine/keyboardmanager.h @@ -29,9 +29,9 @@ class KeyboardManager { /** * Handle a keyboard update on a component. + * @param type The keyboard handler type * @param component The component receiving the key press. If null, ignored. * @param keyboard The key press definition. - * @param document The Content document. * @result True, if the key was consumed. */ bool handleKeyboard(KeyHandlerType type, const CoreComponentPtr& component, const Keyboard& keyboard, diff --git a/aplcore/include/apl/engine/layoutmanager.h b/aplcore/include/apl/engine/layoutmanager.h index 2eaedaf..d9be218 100644 --- a/aplcore/include/apl/engine/layoutmanager.h +++ b/aplcore/include/apl/engine/layoutmanager.h @@ -109,7 +109,7 @@ class LayoutManager { /** * Unmark this component as the top of a Yoga hierarchy. - * @param component + * @param component The component to remove */ void removeAsTopNode(const CoreComponentPtr& component); diff --git a/aplcore/include/apl/engine/propdef.h b/aplcore/include/apl/engine/propdef.h index c375760..bfd3cb6 100644 --- a/aplcore/include/apl/engine/propdef.h +++ b/aplcore/include/apl/engine/propdef.h @@ -232,7 +232,7 @@ inline Object asStyledText(const Context& context, const Object& object) { } inline Object asFilteredText(const Context& context, const Object& object) { - return StyledText::create(context, object).getStyledText().getText(); + return StyledText::create(context, object).getText(); } inline Object asTransformOrArray(const Context& context, const Object& object) { @@ -323,7 +323,6 @@ struct PropDef { * @param defvalue The default value for the property. This will be used if it is not specified by the end user. * @param func A conversion function that takes an Object and converts it into the correct type for this property. * @param flags A collection of flags specifying how to handle this property. - * @param trigger An optional trigger function to execute whenever this property changes value. */ PropDef(K key, const Object& defvalue, BindingFunction func, int flags=0) : key(key), @@ -340,7 +339,6 @@ struct PropDef { * @param defvalue The default value for the property. This will be used if it is not specified by the end user. * @param map A bi-map between the property value (which is a string) and the integer value to store. * @param flags A collection of flags specifying how to handle this property. - * @param trigger An optional trigger function to execute whenever this property changes value. */ PropDef(K key, int defvalue, Bimap& map, int flags=0) : key(key), diff --git a/aplcore/include/apl/engine/recalculatetarget.h b/aplcore/include/apl/engine/recalculatetarget.h index 3f0a095..95d0ab1 100644 --- a/aplcore/include/apl/engine/recalculatetarget.h +++ b/aplcore/include/apl/engine/recalculatetarget.h @@ -49,7 +49,7 @@ class RecalculateTarget { /** * Search and remove all dependants that are associated with this downstream key. - * @param key + * @param key The key */ void removeUpstream(T key) { for (auto it = mUpstream.begin() ; it != mUpstream.end() ; ) diff --git a/aplcore/include/apl/engine/rootcontext.h b/aplcore/include/apl/engine/rootcontext.h index ea67747..997ea8d 100644 --- a/aplcore/include/apl/engine/rootcontext.h +++ b/aplcore/include/apl/engine/rootcontext.h @@ -172,7 +172,7 @@ class RootContext : public std::enable_shared_from_this, /** * Public constructor. Use the ::create method instead. * @param metrics Display metrics - * @param json Processed APL document file + * @param content Processed APL content data * @param config Configuration information */ RootContext(const Metrics& metrics, const ContentPtr& content, const RootConfig& config); @@ -257,8 +257,8 @@ class RootContext : public std::enable_shared_from_this, /** * Execute an externally-driven command - * @param commands - * @param fastMode + * @param commands The commands to execute + * @param fastMode If true this handler will be invoked in fast mode */ ActionPtr executeCommands(const Object& commands, bool fastMode); @@ -339,7 +339,7 @@ class RootContext : public std::enable_shared_from_this, * @deprecated Use Content->getDocumentSettings() * @return document-wide properties. */ - const Settings& settings(); + APL_DEPRECATED const Settings& settings(); /** * @return The content @@ -371,10 +371,10 @@ class RootContext : public std::enable_shared_from_this, /** * Update cursor position. - * @param cursor Cursor positon. + * @param cursorPosition Cursor positon. * @deprecated use handlePointerEvent instead */ - void updateCursorPosition(Point cursorPosition); + APL_DEPRECATED void updateCursorPosition(Point cursorPosition); /** * Handle a given PointerEvent with coordinates relative to the viewport. @@ -484,6 +484,16 @@ class RootContext : public std::enable_shared_from_this, 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); bool setup(const CoreComponentPtr& top); bool verifyAPLVersionCompatibility(const std::vector>& ordered, diff --git a/aplcore/include/apl/engine/state.h b/aplcore/include/apl/engine/state.h index 47b77a8..144c7fa 100644 --- a/aplcore/include/apl/engine/state.h +++ b/aplcore/include/apl/engine/state.h @@ -56,15 +56,14 @@ class State { /** * Construct a state object. All properties are set to false. - * @param disabled The setting for the disabled property. */ State() : mStateMap(kStatePropertyCount, false) {} /** * Constructor that takes a variable number of arguments. * Used to initialize the state to a random set of - * @tparam Args - * @param args + * @tparam Args The type of the arguments + * @param args The set of arguments */ template State(Args... args) : mStateMap(kStatePropertyCount, false) { @@ -132,4 +131,4 @@ class State { } // namespace apl -#endif // _APL_STATE_H \ No newline at end of file +#endif // _APL_STATE_H diff --git a/aplcore/include/apl/extension/extensionclient.h b/aplcore/include/apl/extension/extensionclient.h index 21dc1ab..582f934 100644 --- a/aplcore/include/apl/extension/extensionclient.h +++ b/aplcore/include/apl/extension/extensionclient.h @@ -183,6 +183,12 @@ class ExtensionClient : public Counter, */ bool registered(); + /** + * @return True if extension failed to register (i.e. registration was processed but failed). + * False otherwise. + */ + bool registrationFailed(); + /** * @return The assigned connection token. */ @@ -213,20 +219,20 @@ class ExtensionClient : public Counter, /** * @deprecated @c createComponentChange */ - rapidjson::Value processComponentRequest(rapidjson::Document::AllocatorType& allocator, - ExtensionComponent& component); + APL_DEPRECATED rapidjson::Value processComponentRequest(rapidjson::Document::AllocatorType& allocator, + ExtensionComponent& component); /** * @deprecated @c createComponentChange */ - rapidjson::Value processComponentUpdate(rapidjson::Document::AllocatorType& allocator, - ExtensionComponent& component); + APL_DEPRECATED rapidjson::Value processComponentUpdate(rapidjson::Document::AllocatorType& allocator, + ExtensionComponent& component); /** * @deprecated @c createComponentChange */ - rapidjson::Value processComponentRelease(rapidjson::Document::AllocatorType& allocator, - ExtensionComponent& component); + APL_DEPRECATED rapidjson::Value processComponentRelease(rapidjson::Document::AllocatorType& allocator, + ExtensionComponent& component); /** * Handle disconnection from an extension. It could either be a graceful disconnection or diff --git a/aplcore/include/apl/extension/extensionmediator.h b/aplcore/include/apl/extension/extensionmediator.h index 832a22f..a03f02c 100644 --- a/aplcore/include/apl/extension/extensionmediator.h +++ b/aplcore/include/apl/extension/extensionmediator.h @@ -17,20 +17,23 @@ #ifndef _APL_EXTENSION_MEDIATOR_H #define _APL_EXTENSION_MEDIATOR_H +#include +#include +#include +#include + #include #include "apl/content/content.h" #include "apl/content/rootconfig.h" #include "apl/engine/rootcontext.h" -#include "extensionclient.h" - -#include -#include -#include +#include "apl/extension/extensionclient.h" +#include "apl/extension/extensionsession.h" namespace apl { class RootContext; +class ExtensionSessionState; using ExtensionsLoadedCallback = std::function; @@ -61,7 +64,7 @@ class ExtensionMediator : public std::enable_shared_from_this * alexaext::ExtensionProvider. * @param provider The extension provider. */ - static ExtensionMediatorPtr create(const alexaext::ExtensionProviderPtr& provider) { + APL_DEPRECATED static ExtensionMediatorPtr create(const alexaext::ExtensionProviderPtr& provider) { return std::make_shared(provider, nullptr, alexaext::Executor::getSynchronousExecutor()); } @@ -95,6 +98,24 @@ class ExtensionMediator : public std::enable_shared_from_this return std::make_shared(provider, resourceProvider, messageExecutor); } + /** + * Create a message mediator for the alexaext:Extensions registered with given + * alexaext::ExtensionProvider. + * + * @param provider The extension provider. + * @param resourceProvider The provider for resources shared with the extension. + * @param messageExecutor Process an extension message in a manner consistent with the APL + * execution model. + * @param extensionSession Extension session for this mediator + */ + static ExtensionMediatorPtr + create(const alexaext::ExtensionProviderPtr& provider, + const alexaext::ExtensionResourceProviderPtr& resourceProvider, + const alexaext::ExecutorPtr& messageExecutor, + const ExtensionSessionPtr extensionSession) { + return std::make_shared(provider, resourceProvider, messageExecutor, extensionSession); + } + /** * Signal the grant or deny of a requested extension. @@ -179,15 +200,22 @@ class ExtensionMediator : public std::enable_shared_from_this */ void notifyComponentUpdate(const ExtensionComponentPtr& component, bool resourceNeeded); + /** + * Use create(...) + * + * @deprecated Use the extension session variant + */ + explicit ExtensionMediator(const alexaext::ExtensionProviderPtr& provider, + const alexaext::ExtensionResourceProviderPtr& resourceProvider, + const alexaext::ExecutorPtr& messageExecutor); + /** * Use create(...) */ explicit ExtensionMediator(const alexaext::ExtensionProviderPtr& provider, const alexaext::ExtensionResourceProviderPtr& resourceProvider, - const alexaext::ExecutorPtr& messageExecutor) - : mProvider(provider), - mResourceProvider(resourceProvider), - mMessageExecutor(messageExecutor) {} + const alexaext::ExecutorPtr& messageExecutor, + const ExtensionSessionPtr& extensionSession); /** * Destructor. @@ -210,12 +238,24 @@ class ExtensionMediator : public std::enable_shared_from_this */ void enable(bool enabled) { mEnabled = enabled; } - /** * Clear the internal state and unregister all extensions. */ void finish(); + /** + * Invoked by a viewhost when the session associated with this mediator (if it has been + * previously set) has ended. + */ + void onSessionEnded(); + + /** + * Invoked when the display state associated with the current APL document changes. + * + * @param displayState The new display state. + */ + void onDisplayStateChanged(DisplayState displayState); + private: friend class RootContext; @@ -227,7 +267,7 @@ class ExtensionMediator : public std::enable_shared_from_this /** * Stop initialization on a denied extension. */ - void denyExtension(const std::string& uri); + void denyExtension(const RootConfigPtr& rootConfig, const std::string& uri); /** * Perform extension registration requests. @@ -243,19 +283,20 @@ class ExtensionMediator : public std::enable_shared_from_this * Registers the extensions found in the ExtensionProvider by calling * RootConfig::registerExtensionXXX(). */ - void registerExtension(const std::string& uri, const alexaext::ExtensionProxyPtr& extension, + void registerExtension(const std::string& uri, + const alexaext::ExtensionProxyPtr& extension, const ExtensionClientPtr& client); /** * Enqueue a message with the executor in response to an extension callback. */ - void enqueueResponse(const std::string& uri, const rapidjson::Value& message); + void enqueueResponse(const alexaext::ActivityDescriptorPtr& activity, const rapidjson::Value& message); /** * Delegate a message to the extension client for processing. * @return true if the message was processed. */ - void processMessage(const std::string& uri, JsonData&& message); + void processMessage(const alexaext::ActivityDescriptorPtr& activity, JsonData&& message); /** * Get Proxy corresponding to requested uri. @@ -286,20 +327,52 @@ class ExtensionMediator : public std::enable_shared_from_this * @param errorCode Failure error code. * @param error Message associated with error code. */ - void resourceFail(const ExtensionComponentPtr& component, int errorCode, const std::string& error); + void resourceFail(const ExtensionComponentPtr& component, int errorCode, const std::string& error); + + /** + * @return The current session state object, if a session is present + */ + std::shared_ptr getExtensionSessionState() const; + /** + * Returns the activity associated with the specified extension URI. If no activity was + * previously associated with the URI, one is created and returned. + * + * @param uri The extension URI + * @return The activity descriptor for the specified URI + */ + alexaext::ActivityDescriptorPtr getActivity(const std::string& uri); + + /** + * Updates the display state for the given activity. + * + * @param activity The activity to update + * @param displayState The new display state + */ + void updateDisplayState(const alexaext::ActivityDescriptorPtr& activity, DisplayState displayState); + + /** + * Causes the specified activity to be unregistered. + * + * @param activity The extension activity + */ + void unregister(const alexaext::ActivityDescriptorPtr& activity); + + private: // access to the extensions std::weak_ptr mProvider; // access to the extension resources std::weak_ptr mResourceProvider; + // executor to enqueue/sequence message processing + alexaext::ExecutorPtr mMessageExecutor; + // 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; // retro extension wrapper used for message passing std::map> mClients; - // executor to enqueue/sequence message processing - alexaext::ExecutorPtr mMessageExecutor; // Determines whether incoming messages from extensions should be processed. bool mEnabled = true; // Pending Extension grants @@ -308,6 +381,7 @@ class ExtensionMediator : public std::enable_shared_from_this std::set mPendingRegistrations; // Extensions loaded callback ExtensionsLoadedCallback mLoadedCallback; + std::unordered_map mActivitiesByURI; }; } // namespace apl diff --git a/aplcore/include/apl/extension/extensionsession.h b/aplcore/include/apl/extension/extensionsession.h new file mode 100644 index 0000000..f4d0853 --- /dev/null +++ b/aplcore/include/apl/extension/extensionsession.h @@ -0,0 +1,106 @@ +/** + * 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 +#ifndef _APL_EXTENSION_SESSION_H +#define _APL_EXTENSION_SESSION_H + +#include + +#include + +#include "apl/utils/noncopyable.h" + +namespace apl { + +class ExtensionMediator; +class ExtensionSessionState; + +/** + * Represents an extension session, as exposed to an APL viewhost. This class primarily exists in + * order to associate state with a session descriptor. + */ +class ExtensionSession final : public NonCopyable { +public: + using SessionEndedCallback = std::function; + + /** + * Use create + */ + ExtensionSession(const alexaext::SessionDescriptorPtr& sessionDescriptor) + : mSessionDescriptor(sessionDescriptor) + {} + + /** + * @return A new extension session with a unique descriptor + */ + static std::shared_ptr create(); + + /** + * Creates a new extension session from the specificied descriptor. This returns @c nullptr + * if the provided descriptor is null. + * + * @return A new extension session with the specified descriptor + */ + static std::shared_ptr create(const alexaext::SessionDescriptorPtr& sessionDescriptor); + + /** + * @return The ID of the underlying session descriptor, for convenience. + */ + const alexaext::SessionId& getId() const { return mSessionDescriptor->getId(); } + + /** + * @return the session descriptor associated with this instance. + */ + alexaext::SessionDescriptorPtr getSessionDescriptor() const { return mSessionDescriptor; } + + /** + * @return @true if the session has been marked as ended, @c false otherwise + */ + bool hasEnded() const { return mEnded; } + + /** + * Marks the session as ended. If a callback has been registered, it will be invoked before + * this call returns. + */ + void end(); + + /** + * Registers a callback to be invoked when the session has ended. If the session has already + * ended, the callback is invoked immediately, before this method returns. + * + * @param callback The callback to be invoked. + */ + void onSessionEnded(SessionEndedCallback&& callback); + +private: + friend class ExtensionMediator; + + void setSessionState(const std::shared_ptr& state) { mState = state; } + std::shared_ptr getSessionState() const { return mState; } + +private: + alexaext::SessionDescriptorPtr mSessionDescriptor; + bool mEnded = false; + SessionEndedCallback mSessionEndedCallback = nullptr; + std::shared_ptr mState; +}; + +using ExtensionSessionPtr = std::shared_ptr; + +} // namespace apl + +#endif //_APL_EXTENSION_SESSION_H +#endif //ALEXAEXTENSIONS diff --git a/aplcore/include/apl/graphic/graphicbuilder.h b/aplcore/include/apl/graphic/graphicbuilder.h index 8e79dce..ba03939 100644 --- a/aplcore/include/apl/graphic/graphicbuilder.h +++ b/aplcore/include/apl/graphic/graphicbuilder.h @@ -44,7 +44,7 @@ class GraphicBuilder { /** * Internal constructor - don't use this - * @param graphic + * @param graphic The container graphic */ explicit GraphicBuilder(const GraphicPtr& graphic); diff --git a/aplcore/include/apl/graphic/graphiccontent.h b/aplcore/include/apl/graphic/graphiccontent.h index 7ab2fc4..d8786cd 100644 --- a/aplcore/include/apl/graphic/graphiccontent.h +++ b/aplcore/include/apl/graphic/graphiccontent.h @@ -20,6 +20,7 @@ #include "apl/common.h" #include "apl/content/jsondata.h" +#include "apl/utils/deprecated.h" namespace apl { @@ -37,7 +38,7 @@ class GraphicContent { * @param data The raw data * @return Null if the graphic data is invalid */ - static GraphicContentPtr create(JsonData&& data); + APL_DEPRECATED static GraphicContentPtr create(JsonData&& data); /** * Construct a shared pointer for this JSON data. diff --git a/aplcore/include/apl/graphic/graphicelement.h b/aplcore/include/apl/graphic/graphicelement.h index 4ca03c9..1f19f82 100644 --- a/aplcore/include/apl/graphic/graphicelement.h +++ b/aplcore/include/apl/graphic/graphicelement.h @@ -104,6 +104,16 @@ class GraphicElement : public std::enable_shared_from_this, */ virtual GraphicElementType getType() const = 0; + /** + * @return The language as a BCP-47 string (e.g., en-US) + */ + virtual std::string getLang() const; + + /** + * @return The layoutDirection of the AVG (either LTR or RTL) + */ + virtual GraphicLayoutDirection getLayoutDirection() const; + /** * Update any assigned style state. */ diff --git a/aplcore/include/apl/livedata/layoutrebuilder.h b/aplcore/include/apl/livedata/layoutrebuilder.h index 0c97517..5a43ee6 100644 --- a/aplcore/include/apl/livedata/layoutrebuilder.h +++ b/aplcore/include/apl/livedata/layoutrebuilder.h @@ -91,7 +91,7 @@ class LayoutRebuilder : public std::enable_shared_from_this, /** * Notify rebuilder that particular data index is on screen. - * @param idx + * @param idx The index of the item that is on screen. */ void notifyItemOnScreen(int idx); diff --git a/aplcore/include/apl/livedata/livemap.h b/aplcore/include/apl/livedata/livemap.h index 1582692..234b8c9 100644 --- a/aplcore/include/apl/livedata/livemap.h +++ b/aplcore/include/apl/livedata/livemap.h @@ -66,7 +66,7 @@ class LiveMap : public LiveObject, public Counter { /** * Create a LiveMap with an initial Object - * @param object The initial Object + * @param map The initial object map * @return The LiveMap */ static LiveMapPtr create(ObjectMap&& map) { @@ -75,7 +75,7 @@ class LiveMap : public LiveObject, public Counter { /** * Default constructor. Do not call this; use the create() method. - * @param object + * @param map The initial object map */ explicit LiveMap(ObjectMap&& map) : mMap(std::move(map)) {} diff --git a/aplcore/include/apl/media/mediaplayer.h b/aplcore/include/apl/media/mediaplayer.h index 2cb6405..336d246 100644 --- a/aplcore/include/apl/media/mediaplayer.h +++ b/aplcore/include/apl/media/mediaplayer.h @@ -29,7 +29,7 @@ struct MediaTrack { std::string url; // Source of the video clip HeaderArray headers; // HeaderArray required for the track int offset; // Starting offset within the media object, in milliseconds - int duration; // Duration from the starting offset to play. Set this to a large number to play the whole track. + int duration; // Duration from the starting offset to play. If non-positive, play the entire track int repeatCount; // Number of times to repeat this track before moving to the next. Negative numbers repeat forever. }; @@ -47,7 +47,7 @@ extern std::map sMediaPlayerEventTypeMap; /** * The media player callback should be executed by the view host in a thread-safe manner. - * Pass in the event type, the current state of the media object, and a fast mode flag. + * Pass in the event type and the current state of the media object. */ using MediaPlayerCallback = std::function; @@ -84,7 +84,7 @@ class MediaPlayer { * * @param tracks An array of media tracks */ - virtual void setTrackList(std::vector) = 0; + virtual void setTrackList(std::vector tracks) = 0; /** * Start or resume playing at the current track and offset @@ -92,8 +92,8 @@ class MediaPlayer { * player is at the end of the final track and has no repeats or the player has been released. * * Events: onPlay - * @param action An optional action reference to resolve when finished. Resolve this immediately - * if not using foreground audio. + * @param actionRef An optional action reference to resolve when finished. Resolve this immediately + * if not using foreground audio. */ virtual void play(ActionRef actionRef) = 0; @@ -154,7 +154,7 @@ class MediaPlayer { * * Events: onPause, onTrackUpdate, onTimeUpdate(?) * - * @param trackIndex + * @param trackIndex The index of the track to change to. */ virtual void setTrackIndex( int trackIndex ) = 0; diff --git a/aplcore/include/apl/media/mediaplayerfactory.h b/aplcore/include/apl/media/mediaplayerfactory.h index 1fa60ca..46a1556 100644 --- a/aplcore/include/apl/media/mediaplayerfactory.h +++ b/aplcore/include/apl/media/mediaplayerfactory.h @@ -32,6 +32,10 @@ class MediaPlayerFactory { virtual ~MediaPlayerFactory() = default; /** + * Construct a media player. This media player occupies space on the screen and may be + * used to play audio or video files. + * + * @param callback Invoked as the media player changes state. * @return A new media player */ virtual MediaPlayerPtr createPlayer( MediaPlayerCallback callback ) = 0; diff --git a/aplcore/include/apl/primitives/characterrange.h b/aplcore/include/apl/primitives/characterrange.h deleted file mode 100644 index 5a71a5b..0000000 --- a/aplcore/include/apl/primitives/characterrange.h +++ /dev/null @@ -1,71 +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 -#include - -#include "apl/common.h" - -#ifndef _APL_CHARACTERRANGE_H -#define _APL_CHARACTERRANGE_H - -namespace apl { - -class CharacterRangeData { -public: - CharacterRangeData(wchar_t first, wchar_t second) : - mLower(first < second ? first : second), - mUpper(first < second ? second : first) {} - bool isCharacterValid(const wchar_t& wc) const { - return (wc <= mUpper && wc >= mLower); - } -private: - const wchar_t mLower; - const wchar_t mUpper; -}; - -class CharacterRanges { -public: - /** - * Build a character ranges holder from a string expression - * - * e.g - - * "a-zA-Z0-9" expresses that the characters in the ranges - * a-z, A-Z, and 0-9 are valid - * "--=" expresses that characters in the range of '-' through '=' are valid. - * A dash can be represented as a valid character, but only in the first term of the expression. - * - * @param session The logging session - * @param rangeExpression The character range expression. - */ - CharacterRanges(const SessionPtr &session, const char *rangeExpression) : - mRanges(parse(session, rangeExpression)) {} - - /** - * Build a character ranges holder from a string expression - * @param session The logging session - * @param rangeExpression The character range expression. - */ - CharacterRanges(const SessionPtr &session, const std::string& rangeExpression) : - CharacterRanges(session, rangeExpression.data()) {} - - const std::vector& getRanges() const { return mRanges; } - -private: - static std::vector parse(const SessionPtr &session, const char* rangeExpression); - const std::vector mRanges; -}; -} // namespace apl - -#endif //_APL_CHARACTERRANGE_H diff --git a/aplcore/include/apl/primitives/characterrangegrammar.h b/aplcore/include/apl/primitives/characterrangegrammar.h deleted file mode 100644 index 89b9ff1..0000000 --- a/aplcore/include/apl/primitives/characterrangegrammar.h +++ /dev/null @@ -1,119 +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_CHARACTERRANGEGRAMMAR_H -#define _APL_CHARACTERRANGEGRAMMAR_H - -#include -#include - -#include -#include -#include -#include - -#include "apl/primitives/characterrange.h" -#include "apl/utils/log.h" - -/** - * Character converter to be used for multi-byte sized characters. - */ -std::wstring_convert, wchar_t> wchar_converter; - -namespace apl { -namespace character_range_grammar { - - namespace pegtl = tao::TAO_PEGTL_NAMESPACE; - using namespace pegtl; - - struct firstTerm : utf8::not_one {}; - struct dash : utf8::one {}; - struct leadingDash : utf8::one {}; - struct secondTerm : utf8::not_one {}; - struct rangeExpression : seq {}; - struct singleCharTerm : utf8::not_one {}; - struct grammar : must< opt, plus>, eof > {}; - - // ******************** ACTIONS ********************* - template - struct action - : pegtl::nothing { - }; - - // state struct - struct character_range_state - { - std::vector mRanges; - wchar_t firstTerm; - - void push(CharacterRangeData value) { mRanges.push_back(value); } - const std::vector getRanges() const { return mRanges; } - }; - - inline void pushToRangeState(character_range_state& state, wchar_t first, wchar_t second) - { - CharacterRangeData range(first, second); - state.push(range); - } - - /* - * When we parse the first character in the range expression we update our state - */ - template<> struct action< firstTerm > - { - template< typename Input > - static void apply(const Input& in, character_range_state& state) { - wchar_t rangeChar = wchar_converter.from_bytes(in.string().data()).at(0); - state.firstTerm = rangeChar; - } - }; - - /* - * When we parse the second character in the range expression create a CharacterRangeData and push it - */ - template<> struct action< secondTerm > - { - template< typename Input > - static void apply(const Input& in, character_range_state& state) { - wchar_t rangeChar = wchar_converter.from_bytes(in.string().data()).at(0); - pushToRangeState(state, state.firstTerm, rangeChar); - } - }; - - /* - * When we parse single character in the expression create a CharacterRangeData with single(same) value and push it - */ - template<> struct action< singleCharTerm > - { - template< typename Input > - static void apply(const Input& in, character_range_state& state) { - wchar_t rangeChar = wchar_converter.from_bytes(in.string().data()).at(0); - pushToRangeState(state, rangeChar, rangeChar); - } - }; - - /* - * When we parse starting dash in the expression create a CharacterRangeData with single(same) value and push it - */ - template<> struct action< leadingDash > - { - template< typename Input > - static void apply(const Input& in, character_range_state& state) { - wchar_t rangeChar = wchar_converter.from_bytes(in.string().data()).at(0); - pushToRangeState(state, rangeChar, rangeChar); - } - }; -} // namespace character_range_grammar -} // namespace apl -#endif //_APL_CHARACTERRANGEGRAMMAR_H diff --git a/aplcore/include/apl/primitives/color.h b/aplcore/include/apl/primitives/color.h index c7057ef..2c227b1 100644 --- a/aplcore/include/apl/primitives/color.h +++ b/aplcore/include/apl/primitives/color.h @@ -93,22 +93,27 @@ class Color { /** * @return The red component [0-255] */ - int red() const { return (mColor >> 24) & 0xff; } + int red() const { return (mColor >> 24u) & 0xffu; } /** * @return The green component [0-255] */ - int green() const { return (mColor >> 16) & 0xff; } + int green() const { return (mColor >> 16u) & 0xffu; } /** * @return The blue component [0-255] */ - int blue() const { return (mColor >> 8) & 0xff; } + int blue() const { return (mColor >> 8u) & 0xffu; } /** * @return The alpha component [0-255] */ - int alpha() const { return (mColor & 0xff); } + int alpha() const { return (mColor & 0xffu); } + + /** + * @return True if the color is transparent + */ + bool transparent() const { return alpha() == 0; } friend streamer& operator<<(streamer& os, const Color& color) { return os << color.asString(); @@ -118,10 +123,10 @@ class Color { * @return This color in '#RRGGBBAA' format */ std::string asString() const { - uint32_t a = mColor & 0xff; - uint32_t b = (mColor >> 8) & 0xff; - uint32_t g = (mColor >> 16) & 0xff; - uint32_t r = (mColor >> 24) & 0xff; + uint32_t a = mColor & 0xffu; + uint32_t b = (mColor >> 8u) & 0xffu; + uint32_t g = (mColor >> 16u) & 0xffu; + uint32_t r = (mColor >> 24u) & 0xffu; char hex[10]; snprintf(hex, sizeof(hex), "#%02x%02x%02x%02x", r, g, b, a); return std::string(hex); @@ -151,7 +156,7 @@ class Color { /** * Convert from a color string representation to a color * @param color The color string - * @return + * @return An color as a 32 bit unsigned integer in the form RGBA */ static uint32_t parse(const SessionPtr& session, const char *color); @@ -161,4 +166,4 @@ class Color { } // namespace apl -#endif // _APL_COLOR_H \ No newline at end of file +#endif // _APL_COLOR_H diff --git a/aplcore/include/apl/primitives/gradient.h b/aplcore/include/apl/primitives/gradient.h index 58f81b0..97521e7 100644 --- a/aplcore/include/apl/primitives/gradient.h +++ b/aplcore/include/apl/primitives/gradient.h @@ -17,12 +17,15 @@ #define _APL_GRADIENT_H #include +#include -#include "color.h" +#include "rapidjson/document.h" + +#include "apl/primitives/color.h" +#include "apl/primitives/object.h" namespace apl { -class Object; class Context; enum GradientProperty { @@ -97,7 +100,7 @@ class Gradient { * to linear gradients. 0 is up, 90 is to the right, 180 is down * and 270 is to the left. */ - double getAngle() const { + APL_DEPRECATED double getAngle() const { if (getType() != LINEAR) { return 0; } @@ -105,13 +108,11 @@ class Gradient { } /** - * @deprecated use getProperty(kGradientPropertyColorRange) instead. * @return The vector of color stops. */ const std::vector getColorRange() const { return mColorRange; } /** - * @deprecated use getProperty(kGradientPropertyInputRange) instead. * @return The vector of input stops. These are the values of the color * stops. They are guaranteed to be in ascending numerical order in * the range [0,1]. @@ -140,7 +141,7 @@ class Gradient { bool truthy() const { return true; } private: - Gradient(std::map&& properties); + Gradient(const Context& context, std::map&& properties); static Object create(const Context& context, const Object& object, bool avg); diff --git a/aplcore/include/apl/primitives/keyboard.h b/aplcore/include/apl/primitives/keyboard.h index 867f01f..73c128d 100644 --- a/aplcore/include/apl/primitives/keyboard.h +++ b/aplcore/include/apl/primitives/keyboard.h @@ -111,7 +111,8 @@ class Keyboard { /** * Creates a representation of a non-repeating key, without modifier keys. - * @param code + * @param code The physical key code + * @param key The string representation of the key */ Keyboard(std::string code, std::string key) : mCode(std::move(code)), mKey(std::move(key)) {} @@ -148,7 +149,7 @@ class Keyboard { /** * Set the Ctrl key state. - * @param CtrlKey The pressed state of the Ctrl key + * @param ctrlKey The pressed state of the Ctrl key * @return This Keyboard. */ Keyboard& ctrl(bool ctrlKey) { @@ -214,14 +215,13 @@ class Keyboard { /** * Serialize into JSON format - * @param allocator + * @param allocator The RapidJSON memory allocator * @return The serialized rectangle */ rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; /** * Serialize into ObjectMap format - * @param allocator * @return The serialized rectangle */ std::shared_ptr serialize() const; @@ -286,4 +286,4 @@ class Keyboard { } -#endif // _APL_KEYBOARD_H \ No newline at end of file +#endif // _APL_KEYBOARD_H diff --git a/aplcore/include/apl/primitives/mediastate.h b/aplcore/include/apl/primitives/mediastate.h index a169d04..01135a0 100644 --- a/aplcore/include/apl/primitives/mediastate.h +++ b/aplcore/include/apl/primitives/mediastate.h @@ -22,11 +22,12 @@ class MediaState { MediaState() : mTrackIndex(0), mTrackCount(0), mCurrentTime(0), mDuration(0), - mPaused(true), mEnded(false), mTrackState(kTrackNotReady), mErrorCode(0) {} - MediaState(int trackIndex, int trackCount, int currentTime, int duration, bool paused, bool ended) : + mPaused(true), mEnded(false), mMuted(false), mTrackState(kTrackNotReady), + mErrorCode(0) {} + MediaState(int trackIndex, int trackCount, int currentTime, int duration, bool paused, bool ended, bool muted = false) : mTrackIndex(trackIndex), mTrackCount(trackCount), mCurrentTime(currentTime), - mDuration(duration), mPaused(paused), mEnded(ended), mTrackState(kTrackNotReady), - mErrorCode(0) {} + mDuration(duration), mPaused(paused), mEnded(ended), mMuted(muted), + mTrackState(kTrackNotReady), mErrorCode(0) {} int getTrackIndex() const { return mTrackIndex; } int getTrackCount() const { return mTrackCount; } @@ -34,6 +35,7 @@ class MediaState { int getDuration() const { return mDuration; } bool isPaused() const { return mPaused; } bool isEnded() const { return mEnded; } + bool isMuted() const { return mMuted; } TrackState getTrackState() const { return mTrackState; } int getErrorCode() const { return mErrorCode; } bool isError() const { return mTrackState == kTrackFailed; } @@ -56,6 +58,7 @@ class MediaState { int mDuration; bool mPaused; bool mEnded; + bool mMuted; TrackState mTrackState; int mErrorCode; diff --git a/aplcore/include/apl/primitives/object.h b/aplcore/include/apl/primitives/object.h index 145477e..121bcf2 100644 --- a/aplcore/include/apl/primitives/object.h +++ b/aplcore/include/apl/primitives/object.h @@ -74,6 +74,7 @@ class GraphicPattern; class Transformation; class MediaSource; class LiveDataObject; +class Range; class RangeGenerator; class URLRequest; class SliceGenerator; @@ -118,6 +119,7 @@ using SharedVectorPtr = ObjectArrayPtr; * - Filters * - Gradients * - Media sources + * - Range * - Rectangles * - Radii * - Sources @@ -154,6 +156,7 @@ class Object kRectType, kRadiiType, kStyledTextType, + kRangeType, kGraphicType, kTransformType, kTransform2DType, @@ -196,6 +199,7 @@ class Object Object(Rect&& rect); Object(Radii&& radii); Object(StyledText&& styledText); + Object(Range range); Object(const GraphicPtr& graphic); Object(const GraphicPatternPtr& graphicPattern); Object(const std::shared_ptr& transform); @@ -270,6 +274,7 @@ class Object bool isRadii() const { return mType == kRadiiType; } bool isURLRequest() const { return mType == kURLRequestType; } bool isStyledText() const { return mType == kStyledTextType; } + bool isRange() const { return mType == kRangeType; } bool isGraphic() const { return mType == kGraphicType; } bool isGraphicPattern() const { return mType == kGraphicPatternType; } bool isTransform() const { return mType == kTransformType; } @@ -285,6 +290,7 @@ class Object std::string asString() const; bool asBoolean() const { return truthy(); } double asNumber() const; + float asFloat() const; int asInt(int base=10) const; int64_t asInt64(int base = 10) const; Dimension asDimension(const Context& ) const; @@ -293,7 +299,7 @@ class Object Dimension asNonAutoRelativeDimension(const Context&) const; URLRequest asURLRequest() const; /// @deprecated This method will be removed soon. - Color asColor() const; + APL_DEPRECATED Color asColor() const; Color asColor(const SessionPtr&) const; Color asColor(const Context&) const; @@ -327,6 +333,7 @@ class Object Radii getRadii() const; const URLRequest& getURLRequest() const; const StyledText& getStyledText() const; + const Range& getRange() const; std::shared_ptr getTransformation() const; Transform2D getTransform2D() const; EasingPtr getEasing() const; @@ -383,6 +390,7 @@ class Object rapidjson::Value serializeDirty(rapidjson::Document::AllocatorType& allocator) const; template const T& as() const; + template T asEnum() const { return static_cast(getInteger()); } private: ObjectType mType; diff --git a/aplcore/include/apl/primitives/objectdata.h b/aplcore/include/apl/primitives/objectdata.h index b5e761d..5ed2c58 100644 --- a/aplcore/include/apl/primitives/objectdata.h +++ b/aplcore/include/apl/primitives/objectdata.h @@ -636,7 +636,7 @@ class TransformData : public ObjectData { * bool empty() const; * rapidjson::Value serialize(rapidjson::document::AllocatorType& allocator) const; * - * @tparam T + * @tparam T The type of the stored object. */ template class DirectObjectData : public ObjectData { @@ -650,7 +650,7 @@ class DirectObjectData : public ObjectData { /** * Internal method for accessing the inner object stored here. * Eventually we will shift this to return const T& - * @return + * @return a pointer to the raw data stored in this object */ const void *inner() const override { return &mData; } diff --git a/aplcore/include/apl/primitives/point.h b/aplcore/include/apl/primitives/point.h index a891ed5..7362434 100644 --- a/aplcore/include/apl/primitives/point.h +++ b/aplcore/include/apl/primitives/point.h @@ -16,11 +16,12 @@ #ifndef _APL_POINT_H #define _APL_POINT_H -#include "apl/utils/streamer.h" - #include #include +#include "apl/utils/streamer.h" +#include "apl/utils/stringfunctions.h" + namespace apl { /** @@ -79,7 +80,7 @@ class tPoint } std::string toString() const { - return std::to_string(mX) + "," + std::to_string(mY); + return sutil::to_string(mX) + "," + sutil::to_string(mY); } bool isFinite() { return std::isfinite(mX) && std::isfinite(mY); } diff --git a/aplcore/include/apl/primitives/radii.h b/aplcore/include/apl/primitives/radii.h index 5af64ac..c211f3f 100644 --- a/aplcore/include/apl/primitives/radii.h +++ b/aplcore/include/apl/primitives/radii.h @@ -17,8 +17,13 @@ #define _APL_RADII_H #include +#include +#include + #include "rapidjson/document.h" +#include "apl/utils/deprecated.h" + namespace apl { class streamer; @@ -44,7 +49,10 @@ class Radii { * Assign the same radius to each corner * @param radius The radius to assign */ - Radii(float radius) : mData{radius, radius, radius, radius} {} + Radii(float radius) : mData{radius, radius, radius, radius} { + sanitize(); + } + /** * Define specific values for each corner @@ -55,14 +63,18 @@ class Radii { */ Radii(float topLeft, float topRight, float bottomLeft, float bottomRight) : mData{topLeft, topRight, bottomLeft, bottomRight} - {} + { + sanitize(); + } /** * Construct from a fixed set of values. The order is top-left, top-right, * bottom-left, bottom-right. * @param values The radius values to assign. */ - Radii(std::array&& values) : mData(std::move(values)) {} + Radii(std::array&& values) : mData(std::move(values)) { + sanitize(); + } Radii(const Radii& rhs) = default; Radii& operator=(const Radii& rhs) = default; @@ -107,7 +119,23 @@ class Radii { * @deprecated Use "empty()" instead * @return True if all of the corners have been set to zero */ - bool isEmpty() const { return empty(); } + APL_DEPRECATED bool isEmpty() const { return empty(); } + + /** + * @return True if all the corners have the same radius + */ + bool isRegular() const { + return mData[0] == mData[1] && + mData[0] == mData[2] && + mData[0] == mData[3]; + } + + /** + * Subtract a value from each radius and return a new Radii. + * @param value The amount to subtract. + * @return The new, shrunken Radii. + */ + Radii subtract(float value) const; /** * @return The array of radii. These are guaranteed to be in the order @@ -132,6 +160,8 @@ class Radii { bool truthy() const { return !empty(); } private: + void sanitize(); + std::array mData; }; diff --git a/aplcore/include/apl/utils/range.h b/aplcore/include/apl/primitives/range.h similarity index 67% rename from aplcore/include/apl/utils/range.h rename to aplcore/include/apl/primitives/range.h index fab37b8..7b90474 100644 --- a/aplcore/include/apl/utils/range.h +++ b/aplcore/include/apl/primitives/range.h @@ -16,6 +16,11 @@ #ifndef _APL_RANGE_H #define _APL_RANGE_H +#include +#include +#include +#include + namespace apl { /** @@ -37,8 +42,12 @@ class Range assert(mLowerBound <= mUpperBound); } - bool operator==(const Range& rhs) const { - return mLowerBound == rhs.mLowerBound && mUpperBound == rhs.mUpperBound; + friend bool operator==(const Range& lhs, const Range& rhs) { + return lhs.mLowerBound == rhs.mLowerBound && lhs.mUpperBound == rhs.mUpperBound; + } + + friend bool operator!=(const Range& lhs, const Range& rhs) { + return lhs.mLowerBound != rhs.mLowerBound || lhs.mUpperBound != rhs.mUpperBound; } /** @@ -46,6 +55,11 @@ class Range */ bool empty() const { return mUpperBound < mLowerBound; } + /** + * @return true if there is at least one item in this range + */ + bool truthy() const { return !empty(); } + /** * @return number of elements contained in the range. */ @@ -210,6 +224,75 @@ class Range return to; } + /** + * Calculate the intersection of two ranges + * @param other The other range + * @return The range that is contained in each of the other ranges + */ + Range intersectWith(const Range& other) const { + if (empty() || other.empty() || mLowerBound > other.mUpperBound || mUpperBound < other.mLowerBound) + return {}; + + return Range{ std::max(mLowerBound, other.mLowerBound), + std::min(mUpperBound, other.mUpperBound) }; + } + + /** + * Calculate what part of this range is strictly below a value + * @param value The value + * @return The range that is strictly below the value + */ + Range subsetBelow(int value) const { + if (empty() || mLowerBound >= value) + return {}; + + return { mLowerBound, std::min(mUpperBound, value - 1) }; + } + + /** + * Calculate the part of this range that is strictly above a value. + * @param value The value + * @return The subset of this range strictly above the value + */ + Range subsetAbove(int value) const { + if (empty() || mUpperBound <= value) + return {}; + return { std::max(mLowerBound, value + 1), mUpperBound }; + } + + /* + * Construct an iterator. Ranges can't be modified once constructed, so we don't need + * to write a complicated iterator. + */ + class iterator { + public: + using iterator_category = std::input_iterator_tag; + using difference_type = int; + using value_type = int; + using pointer = value_type*; + using reference = value_type&; + + explicit iterator(int value) : mValue(value) {} + + int operator*() const { return mValue; } + + iterator& operator++() { mValue++; return *this; } + iterator operator++(int) { iterator result = *this; mValue++; return result; } + + friend bool operator==(const iterator& lhs, const iterator& rhs) { return lhs.mValue == rhs.mValue; } + friend bool operator!=(const iterator& lhs, const iterator& rhs) { return lhs.mValue != rhs.mValue; } + + private: + int mValue; + }; + + iterator begin() const { return iterator(mLowerBound); } + iterator end() const { return iterator(mUpperBound + 1); } + + std::string toDebugString() const; + + rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; + private: int mLowerBound; int mUpperBound; diff --git a/aplcore/include/apl/primitives/rangegenerator.h b/aplcore/include/apl/primitives/rangegenerator.h index d2b1aa8..a4bec45 100644 --- a/aplcore/include/apl/primitives/rangegenerator.h +++ b/aplcore/include/apl/primitives/rangegenerator.h @@ -19,6 +19,7 @@ #include #include "apl/primitives/generator.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -60,7 +61,9 @@ class RangeGenerator : public Generator { } std::string toDebugString() const override { - return "RangeGenerator<"+std::to_string(mMinimum)+","+std::to_string(mStep)+","+std::to_string(mSize)+">"; + return "RangeGenerator<" + sutil::to_string(mMinimum) + + "," + sutil::to_string(mStep) + + "," + std::to_string(mSize) + ">"; } private: diff --git a/aplcore/include/apl/primitives/rect.h b/aplcore/include/apl/primitives/rect.h index 546a5f8..e4514be 100644 --- a/aplcore/include/apl/primitives/rect.h +++ b/aplcore/include/apl/primitives/rect.h @@ -20,6 +20,7 @@ #include "apl/primitives/point.h" #include "apl/primitives/size.h" +#include "apl/utils/deprecated.h" namespace apl { @@ -84,17 +85,17 @@ class Rect * @deprecated Remove this in favor of "empty()" * @return True if this rectangle has zero or undefined width and height. */ - bool isEmpty() const; + APL_DEPRECATED bool isEmpty() const { return empty(); } /** * @return True if this rectangle has zero or undefined width and height. */ - bool empty() const { return isEmpty(); } + bool empty() const; /** * @return True if this rectangle is not empty */ - bool truthy() const { return !isEmpty(); } + bool truthy() const { return !empty(); } /** * @return The x-value of the top-left corner @@ -182,6 +183,13 @@ class Rect */ void offset(const Point& p) { mX += p.getX(); mY += p.getY(); } + /** + * Offset this rectangle by a distance + * @param dx The distance in the x-direction + * @param dy The distance in the y-direction + */ + void offset(float dx, float dy) { mX += dx; mY += dy; } + /** * Get rect intersection with other rect. * @param other rect to intersect with. @@ -189,6 +197,13 @@ class Rect */ Rect intersect(const Rect& other) const; + /** + * Create a rectangle that contains both rectangles. Empty rectangles are ignored. + * @param other Rectangle to extend over. + * @return union rectangle + */ + Rect extend(const Rect& other) const; + /** * Check whether point is within this rectangle. The point must in the region [left, right] and [top, bottom]. * @param point The point to check @@ -210,6 +225,26 @@ class Rect */ float area() const { return mWidth * mHeight; } + /** + * Inset the rectangle and return a new rectangle. Rectangles do not + * turn inside-out. If you inset more than half the width/height, the + * rectangle will pin to a zero width/height. + * @param dx Distance to inset horizontally + * @param dy Distance to inset vertically + * @return The new rectangle (may be empty). + */ + Rect inset(float dx, float dy) const; + + /** + * Inset the rectangle and return a new rectangle. Rectangles do not + * turn inside-out. If you inset more than half the width/height, the + * rectangle will pin to a zero width/height. + * @param dx Distance to inset horizontally + * @param dy Distance to inset vertically + * @return The new rectangle (may be empty). + */ + Rect inset(float dist) const { return inset(dist, dist); } + /** * Serialize into a string. * @@ -222,7 +257,7 @@ class Rect /** * Serialize into JSON format - * @param allocator + * @param allocator RapidJSON memory allocator * @return The serialized rectangle */ rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; diff --git a/aplcore/include/apl/primitives/roundedrect.h b/aplcore/include/apl/primitives/roundedrect.h new file mode 100644 index 0000000..5a4452e --- /dev/null +++ b/aplcore/include/apl/primitives/roundedrect.h @@ -0,0 +1,102 @@ +/** + * 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_ROUNDED_RECT_H +#define _APL_ROUNDED_RECT_H + +#include "apl/primitives/rect.h" +#include "apl/primitives/radii.h" + +namespace apl { + +/** + * A rectangle with radius values at each corner + */ +class RoundedRect { +public: + RoundedRect() = default; + RoundedRect(Rect rect, float radius); + RoundedRect(Rect rect, Radii radii); + + bool empty() const { + return mRect.empty(); + } + + /** + * @return True if this rounded rectangle is actually a rectangle + */ + bool isRect() const { + return mRadii.empty(); + } + + /** + * @return True if this rectangle has the same radius for each corner + */ + bool isRegular() const { + return mRadii.isRegular(); + } + + /** + * @return The bounding rectangle + */ + const Rect& rect() const { return mRect; } + + /** + * @return The defined radii + */ + const Radii& radii() const { return mRadii; } + + /** + * @return The top-left corner of the bounding rectangle + */ + Point getTopLeft() const { return mRect.getTopLeft(); } + + /** + * @return The dimensions of the bounding rectangle + */ + Size getSize() const { return mRect.getSize(); } + + /** + * Inset the rounded rectangle by an equal amount in X and Y. The inset + * reduces the corner radii by an equal amount. + * @param inset The amount to inset (may be negative) + * @return A new rounded rectangle + */ + RoundedRect inset(float inset) const; + + /** + * Offset the bounding rectangle by a point. + * @param p The point to offset by. + */ + void offset(const Point& p) { mRect.offset(p); } + + friend bool operator==(const RoundedRect& lhs, const RoundedRect& rhs) { + return lhs.mRect == rhs.mRect && lhs.mRadii == rhs.mRadii; + } + + friend bool operator!=(const RoundedRect& lhs, const RoundedRect& rhs) { + return lhs.mRect != rhs.mRect || lhs.mRadii != rhs.mRadii; + } + + std::string toDebugString() const; + +private: + Rect mRect; + Radii mRadii; +}; + +} // namespace apl + +#endif // _APL_ROUNDED_RECT_H diff --git a/aplcore/include/apl/primitives/size.h b/aplcore/include/apl/primitives/size.h index facfa57..474d014 100644 --- a/aplcore/include/apl/primitives/size.h +++ b/aplcore/include/apl/primitives/size.h @@ -17,6 +17,7 @@ #define _APL_SIZE_H #include "apl/utils/streamer.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -62,8 +63,13 @@ class tSize return os; } + // @deprecated std::string toString() const { - return std::to_string(mWidth) + "x" + std::to_string(mHeight); + return sutil::to_string(mWidth) + "x" + sutil::to_string(mHeight); + } + + std::string toDebugString() const { + return toString(); } private: diff --git a/aplcore/include/apl/primitives/styledtext.h b/aplcore/include/apl/primitives/styledtext.h index 0c7336d..430990e 100644 --- a/aplcore/include/apl/primitives/styledtext.h +++ b/aplcore/include/apl/primitives/styledtext.h @@ -24,6 +24,10 @@ #include #include +#include + +#include "apl/primitives/object.h" + namespace apl { /** @@ -96,7 +100,7 @@ class StyledText { /** * @param start style span starting index. * @param type style type. - * @param attributes style attributes. + * @param attributeMap style attributes. */ Span(size_t start, SpanType type, const std::map& attributeMap) : type(type), @@ -183,6 +187,8 @@ class StyledText { std::string getString() const { return mString; }; + size_t spanCount(); + private: const int START = -1; @@ -200,15 +206,29 @@ class StyledText { * Build StyledText from object. * @param context The data-binding context. * @param object The source of the text. - * @return An object containing a StyledText or null. + * @return An existing StyledText from the object or a new StyledText object. + */ + static StyledText create(const Context& context, const Object& object); + + /** + * Build StyledText from a string which should *not* be parsed + * @param raw The raw string to display + * @return The StyledText object */ - static Object create(const Context& context, const Object& object); + static StyledText createRaw(const std::string& raw); /** * Empty styled text object. Useful as default value. * @return empty StyledText. */ - static Object EMPTY() { return Object(StyledText()); } + static StyledText EMPTY() { return StyledText(); } + + /** + * Assignment constructor + * @param other Styled text object + * @return This object + */ + StyledText& operator=(const StyledText& other); /** * @return Raw text filtered of not-allowed characters and styles. @@ -226,9 +246,9 @@ class StyledText { * underlying string representation, which is both complex and error-prone. * @return Vector of style spans. */ - const std::vector& getSpans() const { return mSpans; } + APL_DEPRECATED const std::vector& getSpans() const { return mSpans; } - const std::string asString() const { + std::string asString() const { return getRawText(); } @@ -244,10 +264,10 @@ class StyledText { bool operator==(const StyledText& rhs) const { return mRawText == rhs.mRawText; } - StyledText(const Context& context, const std::string& raw); - private: - StyledText() {} + StyledText() = default; + explicit StyledText(const std::string& raw); + StyledText(const Context& context, const std::string& raw); std::string mRawText; std::string mText; diff --git a/aplcore/include/apl/primitives/styledtextstate.h b/aplcore/include/apl/primitives/styledtextstate.h index c6fe419..54c6b4c 100644 --- a/aplcore/include/apl/primitives/styledtextstate.h +++ b/aplcore/include/apl/primitives/styledtextstate.h @@ -81,7 +81,6 @@ class StyledTextState { /** * End style span on current text position. In case if tag was not opened it will close current one and move up to "parent". This implementation effectively replicates html behavior. - * @param tag style tag. */ void end(); @@ -93,7 +92,6 @@ class StyledTextState { /** * Record non-parameterized tag, for example line break. - * @param type style type. */ void single(); diff --git a/aplcore/include/apl/primitives/textmeasurerequest.h b/aplcore/include/apl/primitives/textmeasurerequest.h index e0a8ff8..80d0ce5 100644 --- a/aplcore/include/apl/primitives/textmeasurerequest.h +++ b/aplcore/include/apl/primitives/textmeasurerequest.h @@ -22,6 +22,7 @@ #include "apl/utils/hash.h" #include "apl/utils/streamer.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -62,9 +63,9 @@ struct TextMeasureRequest { std::string toString() const { std::string result = "TextMeasureRequest<"; - result += "width=" + std::to_string(width) + ","; + result += "width=" + sutil::to_string(width) + ","; result += "widthMode=" + std::to_string(widthMode) + ","; - result += "height=" + std::to_string(height) + ","; + result += "height=" + sutil::to_string(height) + ","; result += "heightMode=" + std::to_string(heightMode) + ","; result += "paramHash=" + paramHash + ">"; return result; diff --git a/aplcore/include/apl/primitives/timegrammar.h b/aplcore/include/apl/primitives/timegrammar.h index cb98438..3f0c0fb 100644 --- a/aplcore/include/apl/primitives/timegrammar.h +++ b/aplcore/include/apl/primitives/timegrammar.h @@ -64,6 +64,9 @@ namespace timegrammar { namespace pegtl = tao::TAO_PEGTL_NAMESPACE; using namespace pegtl; +/** + * \cond ShowTimeGrammar + */ struct other : any {}; struct year_four : string<'Y', 'Y', 'Y', 'Y'> {}; @@ -323,6 +326,10 @@ template<> struct action extern std::string timeToString(const std::string& format, double time); +/** + * \endcond + */ + } // namespace timegrammar } // namespace apl diff --git a/aplcore/include/apl/primitives/transform.h b/aplcore/include/apl/primitives/transform.h index e3ce08d..e764960 100644 --- a/aplcore/include/apl/primitives/transform.h +++ b/aplcore/include/apl/primitives/transform.h @@ -60,7 +60,7 @@ class Transformation { /** * Create a transformation from a context and an array of transformation items * @param context The context to evaluate the transformation in. - * @param object The transformations + * @param array An array of transformation items * @return The calculated transformation */ static std::shared_ptr create(const Context& context, const std::vector& array); diff --git a/aplcore/include/apl/primitives/transform2d.h b/aplcore/include/apl/primitives/transform2d.h index 2417ed2..dbb51f4 100644 --- a/aplcore/include/apl/primitives/transform2d.h +++ b/aplcore/include/apl/primitives/transform2d.h @@ -136,7 +136,8 @@ class Transform2D { /** * Scale in both the x- and y-direction - * @param s Scaling factor. Should satisfy s > 0 + * @param sx Scaling factor in x + * @param sy Scaling factor in y * @return The transform */ static Transform2D scale(float sx, float sy) { @@ -313,8 +314,8 @@ class Transform2D { /** * Serialize this transform into a 6 element array. - * @param allocator - * @return + * @param allocator RapidJSON memory allocator + * @return A RapidJSON array value */ rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value v(rapidjson::kArrayType); diff --git a/aplcore/include/apl/primitives/unicode.h b/aplcore/include/apl/primitives/unicode.h index 6396e16..188d20f 100644 --- a/aplcore/include/apl/primitives/unicode.h +++ b/aplcore/include/apl/primitives/unicode.h @@ -37,6 +37,44 @@ int utf8StringLength(const std::string& utf8String); */ std::string utf8StringSlice(const std::string& utf8String, int start, int end = std::numeric_limits::max()); +/** + * Strip invalid characters out of a UTF-8 string. The "validCharacters" property in the EditText + * component defines the schema for the valid character string. + * + * @param utf8String The starting string + * @param validCharacters The valid set of characters + * @return The stripped string + */ +std::string utf8StripInvalid(const std::string& utf8String, const std::string& validCharacters); + +/** + * Check if all characters in a string are valid. The "validCharacters" property in the EditText +* component defines the schema for the valid character string. +* + * @param utf8String A UTF-8 string + * @param validCharacters The set of valid characters + * @return True if all of the characters in utf8String are valid + */ +bool utf8ValidCharacters(const std::string& utf8String, const std::string& validCharacters); + +/** + * Check a single wchar_t character to see if it is a valid character. The "validCharacters" property + * in the EditText component defines the schema for the valid character string. + * + * @param wc The wchar_t character + * @param validCharacters The valid set of characters + * @return True if this is a valid character + */ +bool wcharValidCharacter(wchar_t wc, const std::string& validCharacters); + +/** + * Trim the length of a UTF-8 string to a maximum number of code points (not bytes). + * @param utf8String The string to trim in place. + * @param maxLength The maximum number of allowed code points. If zero, no trimming occurs. + * @return True if the string was trimmed + */ +bool utf8StringTrim(std::string& utf8String, int maxLength); + } // namespace apl #endif // _APL_UNICODE_H diff --git a/aplcore/include/apl/scaling/metricstransform.h b/aplcore/include/apl/scaling/metricstransform.h index 98464c0..513b043 100644 --- a/aplcore/include/apl/scaling/metricstransform.h +++ b/aplcore/include/apl/scaling/metricstransform.h @@ -223,7 +223,7 @@ class MetricsTransform { /** * Get the metrics - * @return + * @return A copy of the Metrics */ Metrics getMetrics() const { return mMetrics; } diff --git a/aplcore/include/apl/scaling/scalingcalculator.h b/aplcore/include/apl/scaling/scalingcalculator.h index 5d47d33..6aa359a 100644 --- a/aplcore/include/apl/scaling/scalingcalculator.h +++ b/aplcore/include/apl/scaling/scalingcalculator.h @@ -48,8 +48,8 @@ class ScalingCalculator { /** * Calculates the scale factor at the given size - * @param size - * @return The scale + * @param size The size + * @return The scale factor */ double scaleFactor(const Size& size) { return scaleFactor(size.w, size.h); } double scaleFactor(double w, double h) { diff --git a/aplcore/include/apl/time/sequencer.h b/aplcore/include/apl/time/sequencer.h index 99334dc..b8bfaa4 100644 --- a/aplcore/include/apl/time/sequencer.h +++ b/aplcore/include/apl/time/sequencer.h @@ -155,7 +155,7 @@ class Sequencer : public Counter { /** * Release all claimed resources associated with this holder - * @param holder + * @param holder An execution resource holder */ void releaseRelatedResources(const ExecutionResourceHolderPtr& holder); diff --git a/aplcore/include/apl/time/timemanager.h b/aplcore/include/apl/time/timemanager.h index ec0278f..39da460 100644 --- a/aplcore/include/apl/time/timemanager.h +++ b/aplcore/include/apl/time/timemanager.h @@ -34,7 +34,7 @@ class TimeManager : public Timers { /** * Move forward in time - * @param updatedTime + * @param updatedTime The new time value (absolute, not relative) */ virtual void updateTime(apl_time_t updatedTime) = 0; diff --git a/aplcore/include/apl/touch/pointermanager.h b/aplcore/include/apl/touch/pointermanager.h index bb6a839..71950ee 100644 --- a/aplcore/include/apl/touch/pointermanager.h +++ b/aplcore/include/apl/touch/pointermanager.h @@ -73,13 +73,14 @@ class PointerManager { * removed. For touch type pointers, the pointer will be removed from the manager. * * @param pointerEvent The PointerEvent to handle. - * @param event timestamp. + * @param timestamp The time of the event * @return true if was consumed and should not be passed through any platform handling. */ bool handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t timestamp); /** * Function to notify all interested parties about pointer related time updates. + * @param timestamp The time of the event */ void handleTimeUpdate(apl_time_t timestamp); diff --git a/aplcore/include/apl/utils/counter.h b/aplcore/include/apl/utils/counter.h index 6f80346..74acfef 100644 --- a/aplcore/include/apl/utils/counter.h +++ b/aplcore/include/apl/utils/counter.h @@ -93,4 +93,4 @@ CounterPair::size_type Counter::sItemsCreated = 0; } // namespace apl -#endif //_APL_CORE_MEMORY_TRACKER_H +#endif //_APL_CORE_COUNTER_H diff --git a/aplcore/include/apl/utils/deprecated.h b/aplcore/include/apl/utils/deprecated.h new file mode 100644 index 0000000..86157d4 --- /dev/null +++ b/aplcore/include/apl/utils/deprecated.h @@ -0,0 +1,28 @@ +/** + * 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_DEPRECATED_H +#define _APL_DEPRECATED_H + +#if defined(__GNUC__) || defined(__clang__) +#define APL_DEPRECATED __attribute__((deprecated)) +#elif defined(_MSC_VER) +#define APL_DEPRECATED __declspec(deprecated) +#else +#pragma message("WARNING: Missing compiler implementation for DEPRECATED") +#define APL_DEPRECATED +#endif + +#endif //_APL_DEPRECATED_H \ No newline at end of file diff --git a/aplcore/include/apl/utils/log.h b/aplcore/include/apl/utils/log.h index 6993cbc..3ee2410 100644 --- a/aplcore/include/apl/utils/log.h +++ b/aplcore/include/apl/utils/log.h @@ -26,6 +26,7 @@ #include #include +#include "apl/common.h" #include "apl/utils/streamer.h" namespace apl { @@ -66,8 +67,7 @@ class LogBridge { class Logger { public: - Logger(std::shared_ptr bridge, LogLevel level, const std::string& file, const std::string& function); - + Logger(const std::shared_ptr& bridge, LogLevel level, const std::string& file, const std::string& function); ~Logger(); /** @@ -82,6 +82,20 @@ class Logger */ void log(const char *format, va_list args); + // Session binding + Logger& session(const Session& session); + Logger& session(const SessionPtr& session); + 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); + template friend Logger& operator<<(Logger& os, T&& value) { @@ -100,6 +114,7 @@ class Logger const bool mUncaught; const std::shared_ptr mBridge; const LogLevel mLevel; + std::string mLogId; apl::streamer mStringStream; }; @@ -123,7 +138,7 @@ class LoggerFactory { * Set consumer specific logger configuration. * @param bridge Consumer logging bridge. */ - void initialize(std::shared_ptr bridge); + void initialize(const std::shared_ptr& bridge); /** * Reset Loggers state. Logging to reset to console. @@ -175,8 +190,7 @@ class LoggerFactory { #define LOG(LEVEL) apl::LoggerFactory::instance().getLogger(LEVEL,__FILENAME__,__func__) #define LOGF(LEVEL,FORMAT,...) apl::LoggerFactory::instance().getLogger(LEVEL,__FILENAME__,__func__).log(FORMAT,__VA_ARGS__) #define LOG_IF(CONDITION) !(CONDITION) ? (void)0 : apl::LogVoidify() & LOG(apl::LogLevel::kDebug) -#define LOGF_IF(CONDITION,FORMAT,...) \ - !(CONDITION) ? (void) 0 : LOGF(apl::LogLevel::kDebug,FORMAT,__VA_ARGS__) +#define LOGF_IF(CONDITION,FORMAT,...) !(CONDITION) ? (void) 0 : LOGF(apl::LogLevel::kDebug,FORMAT,__VA_ARGS__) } // namespace apl #endif // _APL_LOG_H diff --git a/aplcore/include/apl/utils/path.h b/aplcore/include/apl/utils/path.h index de2ce08..e2204b7 100644 --- a/aplcore/include/apl/utils/path.h +++ b/aplcore/include/apl/utils/path.h @@ -45,7 +45,7 @@ class Path { /** * Add an object segment to the path - * @param segment The name of the object. + * @param segment The name of the segment. * @return A new Path object */ Path addObject(const std::string& segment) const { @@ -58,6 +58,11 @@ class Path { return Path(mPath + '/' + segment); } + /** + * Add an object segment to the path + * @param segment The name of the segment + * @return a new Path object + */ Path addObject(const char *segment) const { return addObject(std::string(segment)); } @@ -65,7 +70,7 @@ class Path { /** * Add an array segment to the path * @param segment The name of the array - * @return + * @return A new Path object */ Path addArray(const std::string& segment) const { if (mPath.empty()) @@ -77,6 +82,11 @@ class Path { return Path(mPath + '/' + segment + '/'); } + /** + * Add an array segment to the path + * @param segment The name of the array + * @return A new Path object + */ Path addArray(const char *segment) const { return addArray(std::string(segment)); } diff --git a/aplcore/include/apl/utils/random.h b/aplcore/include/apl/utils/random.h index 9e7f2fa..db51d0c 100644 --- a/aplcore/include/apl/utils/random.h +++ b/aplcore/include/apl/utils/random.h @@ -61,6 +61,20 @@ class Random return ss.str(); } + static std::string + generateSimpleToken(size_t length = 10) + { + static auto gen = Random::mt32Generator(); + static std::uniform_int_distribution<> dis('A', 'Z'); + + streamer ss; + int i; + for (i = 0; i < length; i++) { + ss << (char)dis(gen); + } + + return ss.str(); + } }; } // namespace apl diff --git a/aplcore/include/apl/utils/session.h b/aplcore/include/apl/utils/session.h index 06c19e1..cb8ff7f 100644 --- a/aplcore/include/apl/utils/session.h +++ b/aplcore/include/apl/utils/session.h @@ -35,6 +35,7 @@ namespace apl { */ class Session { public: + Session(); virtual ~Session() = default; /** @@ -53,18 +54,35 @@ class Session { * Write a string in the session log, including the filename and function where * the log was generated. This is a convenience method for std::string * - * @param filename - * @param func - * @param value + * @param filename The filename where the log entry was generated. + * @param func The function mame where the log entry was generated. + * @param value The message to write */ void write(const char *filename, const char *func, const std::string& value) { write(filename, func, value.c_str()); } + + /** + * Set log prefix to be used. + * @param prefix [A-Z], truncated to 6 characters. Padded with '_' + */ + void setLogIdPrefix(const std::string& prefix); + + /** + * @return Generated Log ID. + */ + std::string getLogId() const { + return mLogId; + } + +private: + std::string mLogId; }; /** - * Construct a default session which passes console messages to the log - * @return + * Construct a default session which passes console messages to the log. The default session + * writes session messages to the log as warnings. + * @return A default session pointer. */ extern SessionPtr makeDefaultSession(); @@ -123,17 +141,7 @@ class SessionMessage { streamer mStringStream; }; -/// Report content errors using a session object -#define CONSOLE_S(SESSION) SessionMessage(SESSION,__FILENAME__,__func__) - -/// Report content errors using a context object pointer (which contains a session) -#define CONSOLE_CTP(CONTEXT_PTR) SessionMessage(CONTEXT_PTR,__FILENAME__,__func__) - -/// Report content errors using a context object (which contains a session) -#define CONSOLE_CTX(CONTEXT) SessionMessage(CONTEXT,__FILENAME__,__func__) - -/// Report content errors using a config object pointer (which contains a session) -#define CONSOLE_CFGP(CONFIG_PTR) SessionMessage(CONFIG_PTR,__FILENAME__,__func__) +#define CONSOLE(SESSION_HODER) SessionMessage(SESSION_HODER,__FILENAME__,__func__) } // namespace apl diff --git a/aplcore/include/apl/utils/streamer.h b/aplcore/include/apl/utils/streamer.h index cdb44c6..4bc7976 100644 --- a/aplcore/include/apl/utils/streamer.h +++ b/aplcore/include/apl/utils/streamer.h @@ -21,6 +21,8 @@ #include #include +#include "apl/utils/stringfunctions.h" + namespace apl { class streamer { public: @@ -116,17 +118,17 @@ class streamer { } streamer& operator<<(float __f) { - mString += std::to_string(__f); + mString += sutil::to_string(__f); return *this; } streamer& operator<<(double __f) { - mString += std::to_string(__f); + mString += sutil::to_string(__f); return *this; } streamer& operator<<(long double __f) { - mString += std::to_string(__f); + mString += sutil::to_string(__f); return *this; } diff --git a/aplcore/include/apl/utils/stringfunctions.h b/aplcore/include/apl/utils/stringfunctions.h index ac3965c..565ddce 100644 --- a/aplcore/include/apl/utils/stringfunctions.h +++ b/aplcore/include/apl/utils/stringfunctions.h @@ -16,7 +16,9 @@ #ifndef APL_STRINGFUNCTIONS_H #define APL_STRINGFUNCTIONS_H +#include #include +#include namespace apl { @@ -44,17 +46,212 @@ std::string ltrim(const std::string &str); */ std::string trim(const std::string &str); +/** + * Pads the end of the specified string, if needed, with the padding character until the resulting + * string reaches the minimum length. No padding is applied if the input string already has the + * requested minimum length. + * + * @param str The string to pad + * @param minWidth The minimal resulting string length + * @param padChar The character to use when padding, defaults to ' '. + * @return The padding string + */ +std::string rpad(const std::string &str, std::size_t minWidth, char padChar = ' '); + +/** + * Pads the beginning of the specified string, if needed, with the padding character until the + * resulting string reaches the minimum length. No padding is applied if the input string already + * has the requested minimum length. + * + * @param str The string to pad + * @param minWidth The minimal resulting string length + * @param padChar The character to use when padding, defaults to ' '. + * @return The padding string + */ +std::string lpad(const std::string &str, std::size_t minWidth, char padChar = ' '); /** * Internal utility to convert string to lowercase. - * Applicable only to latin characters. It must not used instead of corelocalemethods. + * Applicable only to latin characters. It must not be used instead of corelocalemethods. * @param str string to process. * @return lowercase version of str. */ std::string tolower(const std::string& str); +/** + * sutil:: functions are intended to be locale-independent versions of std:: functions of the same + * name that are affected by the C locale. + */ +namespace sutil { + +constexpr char DECIMAL_POINT = '.'; + +/** + * Internal utility to convert parse the textual representation of a floating-point number with + * a known format without risking being affected by the current C locale. Intended to be used + * as a locale-invariant alternative to std::stof. + * + * @param str The string value to parse + * @param pos If non-null, will be set to the index of the first non-parsed character in the input + * string. + * @return the parsed value, or NaN if the string could not be parsed. + */ +float stof(const std::string& str, std::size_t* pos = nullptr); + +/** + * Internal utility to convert parse the textual representation of a floating-point number with + * a known format without risking being affected by the current C locale. Intended to be used + * as a locale-invariant alternative to std::stod. + * + * @param str The string value to parse + * @param pos If non-null, will be set to the index of the first non-parsed character in the input + * string. + * @return the parsed value, or NaN if the string could not be parsed. + */ +double stod(const std::string& str, std::size_t* pos = nullptr); + +/** + * Internal utility to convert parse the textual representation of a floating-point number with + * a known format without risking being affected by the current C locale. Intended to be used + * as a locale-invariant alternative to std::stold. + * + * @param str The string value to parse + * @param pos If non-null, will be set to the index of the first non-parsed character in the input + * string. + * @return the parsed value, or NaN if the string could not be parsed. + */ +long double stold(const std::string& str, std::size_t* pos = nullptr); + + +/** + * Internal utility to format a single-precision value as a string. Intended to be used as a + * locale-invariant alternative to std::to_string(double). + * + * @param value The value to format + * @return the value formatted as a string + */ +std::string to_string(float value); + +/** + * Internal utility to format a double-precision value as a string. Intended to be used as a + * locale-invariant alternative to std::to_string(double). + * + * @param value The value to format + * @return the value formatted as a string + */ +std::string to_string(double value); + +/** + * Internal utility to format a extended precision value as a string. Intended to be used as a + * locale-invariant alternative to std::to_string(long double). + * + * @param value The value to format + * @return the value formatted as a string + */ +std::string to_string(long double value); + + +/** + * Determines if a character is alphanumeric in the classic C locale. + * + * @param c The character to check + * @return @c true if the provided character is alphanumeric in the classic C locale + */ +bool isalnum(char c); + +/** + * Determines if a character is alphanumeric in the classic C locale. + * + * @param c The character to check + * @return @c true if the provided character is alphanumeric in the classic C locale + */ +bool isalnum(unsigned char c); + +/** + * Determines if a character is a whitespace character in the classic C locale. + * + * @param c The chacater to check + * @return @c true of the character is a whitespace character, @c false otherwise + */ +bool isspace(char c); + +/** + * Determines if a character is a whitespace character in the classic C locale. + * + * @param c The chacater to check + * @return @c true of the character is a whitespace character, @c false otherwise + */ +bool isspace(unsigned char c); + +/** + * Determines if a character is an uppercase character in the classic C locale. + * + * @param c The chacater to check + * @return @c true of the character is an uppercase character, @c false otherwise + */ +bool isupper(char c); + +/** + * Determines if a character is an uppercase character in the classic C locale. + * + * @param c The chacater to check + * @return @c true of the character is an uppercase character, @c false otherwise + */ +bool isupper(unsigned char c); + +/** + * Determines if a character is a lowercase character in the classic C locale. + * + * @param c The chacater to check + * @return @c true of the character is a lowercase character, @c false otherwise + */ +bool islower(char c); + +/** + * Determines if a character is a lowercase character in the classic C locale. + * + * @param c The chacater to check + * @return @c true of the character is a lowercase character, @c false otherwise + */ +bool islower(unsigned char c); + +/** + * Converts a character to lowercase according to the classic C locale. + * + * @param c The character to convert + * @return The lowercase version of the character + */ +char tolower(char c); + +/** + * Converts a character to lowercase according to the classic C locale. + * + * @param c The character to convert + * @return The lowercase version of the character + */ +unsigned char tolower(unsigned char c); + +/** + * Converts a character to uppercase according to the classic C locale. + * + * @param c The character to convert + * @return The uppercase version of the character + */ +char toupper(char c); + +/** + * Converts a character to uppercase according to the classic C locale. + * + * @param c The character to convert + * @return The uppercase version of the character + */ +unsigned char toupper(unsigned char c); + + +} // namespace sutil + } #endif // APL_STRINGFUNCTIONS_H diff --git a/aplcore/include/apl/utils/weakcache.h b/aplcore/include/apl/utils/weakcache.h index 3393199..063bd36 100644 --- a/aplcore/include/apl/utils/weakcache.h +++ b/aplcore/include/apl/utils/weakcache.h @@ -26,7 +26,7 @@ namespace apl { * As strong pointers are released the entries in the cache become invalid. * Periodically running the "clean()" method will remove those invalid entries. * - * @tparam T + * @tparam T The templated type stored in the cache */ template class WeakCache { @@ -40,8 +40,8 @@ class WeakCache { /** * Find an item in the cache and return it, if it exists. - * @param key - * @return + * @param key The key to look up in the cache + * @return The value if it exists or nullptr */ std::shared_ptr find(const std::string& key) { auto it = mCache.find(key); @@ -56,8 +56,8 @@ class WeakCache { /** * Insert a new item in the weak cache - * @param key - * @param value + * @param key The key to add to the cache. + * @param value The value to add */ void insert(std::string key, const std::shared_ptr& value) { mCache.emplace(key, value); diff --git a/aplcore/src/action/controlmediaaction.cpp b/aplcore/src/action/controlmediaaction.cpp index cad7ff8..8c1365b 100644 --- a/aplcore/src/action/controlmediaaction.cpp +++ b/aplcore/src/action/controlmediaaction.cpp @@ -35,7 +35,7 @@ ControlMediaAction::make(const TimersPtr& timers, const std::shared_ptr& command) { if (kComponentTypeVideo != command->target()->getType()) { - CONSOLE_CTP(command->context()) << "ControlMedia targeting non-Video component"; + CONSOLE(command->context()) << "ControlMedia targeting non-Video component"; // TODO: Check if we actually sanitize commands for target component type. return nullptr; } @@ -48,7 +48,7 @@ ControlMediaAction::make(const TimersPtr& timers, int maxIndex = mediaSource.isArray() ? mediaSource.size() - 1 : 0; if (value.asInt() > maxIndex) { - CONSOLE_CTP(command->context()) << "ControlMedia track index out of bounds"; + CONSOLE(command->context()) << "ControlMedia track index out of bounds"; return nullptr; } } diff --git a/aplcore/src/action/scrolltoaction.cpp b/aplcore/src/action/scrolltoaction.cpp index 660e6b3..3db2a21 100644 --- a/aplcore/src/action/scrolltoaction.cpp +++ b/aplcore/src/action/scrolltoaction.cpp @@ -104,7 +104,7 @@ ScrollToAction::make(const TimersPtr& timers, auto resultingAlign = align; if (useSnap) { - LOG_IF(DEBUG_SCROLL_TO) << "Ignoring provided align and using component defined snap."; + LOG_IF(DEBUG_SCROLL_TO).session(context) << "Ignoring provided align and using component defined snap."; auto snapObject = container->getCalculated(kPropertySnap); if (!snapObject.isNull()) { auto snap = static_cast(snapObject.getInteger()); @@ -169,7 +169,7 @@ ScrollToAction::start() { void ScrollToAction::scrollTo() { - LOG_IF(DEBUG_SCROLL_TO) << "Constructing scroll to action"; + LOG_IF(DEBUG_SCROLL_TO).session(mTarget) << "Constructing scroll to action"; // Calculate how far we need to scroll Rect childBoundsInParent; @@ -223,9 +223,9 @@ ScrollToAction::scrollTo() afterParentEnd = childEnd - scrollTo < parentEnd; } - LOG_IF(DEBUG_SCROLL_TO) << "parent start=" << parentStart << " end=" << parentEnd; - LOG_IF(DEBUG_SCROLL_TO) << "child start=" << childStart << " end=" << childEnd; - LOG_IF(DEBUG_SCROLL_TO) << "scrollPosition=" << scrollTo; + LOG_IF(DEBUG_SCROLL_TO).session(mTarget) << "parent start=" << parentStart << " end=" << parentEnd; + LOG_IF(DEBUG_SCROLL_TO).session(mTarget) << "child start=" << childStart << " end=" << childEnd; + LOG_IF(DEBUG_SCROLL_TO).session(mTarget) << "scrollPosition=" << scrollTo; switch (mAlign) { case kCommandScrollAlignFirst: @@ -251,7 +251,7 @@ ScrollToAction::scrollTo() // Calculate the new position by trimming the old position plus the distance auto p = mContainer->trimScroll(Point(scrollTo, scrollTo)); - LOG_IF(DEBUG_SCROLL_TO) << "...distance=" << scrollTo << " position=" << p; + LOG_IF(DEBUG_SCROLL_TO).session(mTarget) << "...distance=" << scrollTo << " position=" << p; scroll(vertical, p); } @@ -259,7 +259,7 @@ ScrollToAction::scrollTo() void ScrollToAction::pageTo() { - LOG_IF(DEBUG_SCROLL_TO) << mContainer; + LOG_IF(DEBUG_SCROLL_TO).session(mContainer) << mContainer; // We have a target component to show and a pager component that (eventually) holds the target. // First, we need to figure out which page the target component is on. This requires finding @@ -277,7 +277,7 @@ ScrollToAction::pageTo() } if (targetPage == -1) { - LOG(LogLevel::kError) << "Unrecoverable error in pageTo"; + LOG(LogLevel::kError).session(mTarget) << "Unrecoverable error in pageTo"; resolve(); return; } diff --git a/aplcore/src/action/sequentialaction.cpp b/aplcore/src/action/sequentialaction.cpp index 19777a9..133a754 100644 --- a/aplcore/src/action/sequentialaction.cpp +++ b/aplcore/src/action/sequentialaction.cpp @@ -32,7 +32,7 @@ SequentialAction::SequentialAction(const TimersPtr& timers, mRepeatCounter(0) { addTerminateCallback([this](const TimersPtr&) { - LOG_IF(DEBUG_SEQUENTIAL) << "terminating " << *this; + LOG_IF(DEBUG_SEQUENTIAL).session(mCommand) << "terminating " << *this; if (mCurrentAction) { mCurrentAction->terminate(); mCurrentAction = nullptr; @@ -66,7 +66,7 @@ SequentialAction::SequentialAction(const TimersPtr& timers, */ void SequentialAction::advance() { - LOG_IF(DEBUG_SEQUENTIAL) << *this << " state=" << mStateFinally; + LOG_IF(DEBUG_SEQUENTIAL).session(mCommand) << *this << " state=" << mStateFinally; if (isTerminated()) return; diff --git a/aplcore/src/animation/animatedproperty.cpp b/aplcore/src/animation/animatedproperty.cpp index ae3abb2..5a89907 100644 --- a/aplcore/src/animation/animatedproperty.cpp +++ b/aplcore/src/animation/animatedproperty.cpp @@ -27,20 +27,20 @@ AnimatedProperty::create(const ContextPtr& context, const Object& object) { if (!object.isMap()) { - CONSOLE_CTP(context) << "Unrecognized animation command" << object; + CONSOLE(context) << "Unrecognized animation command" << object; return nullptr; } auto property = propertyAsString(*context, object, "property"); if (!object.has("to")) { - CONSOLE_CTP(context) << "Animation property has no 'to' value '" << property << "'"; + CONSOLE(context) << "Animation property has no 'to' value '" << property << "'"; return nullptr; } auto propRef = component->getPropertyAndWriteableState(property); if (!propRef.second) { - CONSOLE_CTP(context) << "Unusable animation property '" << property << "'"; + CONSOLE(context) << "Unusable animation property '" << property << "'"; return nullptr; } @@ -48,7 +48,7 @@ AnimatedProperty::create(const ContextPtr& context, auto key = static_cast(sComponentPropertyBimap.get(property, -1)); if (key == kPropertyTransformAssigned) { if (!object.has("from")) { - CONSOLE_CTP(context) << "Animated transforms need a 'from' property"; + CONSOLE(context) << "Animated transforms need a 'from' property"; return nullptr; } @@ -59,12 +59,12 @@ AnimatedProperty::create(const ContextPtr& context, // The only other assigned key we can animate is opacity if (key != static_cast(-1) && key != kPropertyOpacity) { - CONSOLE_CTP(context) << "Unable to animate property '" << property << "'"; + CONSOLE(context) << "Unable to animate property '" << property << "'"; return nullptr; } if (!propRef.first.isNumber()) { - CONSOLE_CTP(context) << "Only numbers and transforms can be animated '" << property << "'"; + CONSOLE(context) << "Only numbers and transforms can be animated '" << property << "'"; return nullptr; } diff --git a/aplcore/src/animation/coreeasing.cpp b/aplcore/src/animation/coreeasing.cpp index 32c8c73..beb3260 100644 --- a/aplcore/src/animation/coreeasing.cpp +++ b/aplcore/src/animation/coreeasing.cpp @@ -18,6 +18,7 @@ #include "apl/animation/coreeasing.h" #include "apl/animation/easingapproximation.h" +#include "apl/utils/stringfunctions.h" #include "apl/utils/weakcache.h" namespace apl { @@ -26,9 +27,9 @@ static WeakCache sEasingAppoxCache; std::string dofSig(int dof, const float* array) { - std::string result = "x" + std::to_string(array[0]); + std::string result = "x" + sutil::to_string(array[0]); for (int i = 1; i < dof; i++) - result += "," + std::to_string(array[i]); + result += "," + sutil::to_string(array[i]); return result; } @@ -54,10 +55,7 @@ f(float a, float b, float t) { * For a given value of x find the matching value of y. We restrict ourselves * to the case where a1=b1=0 and a4=b4=1. * - * @param a The first x-control point parameter (a2) - * @param b The first y-control point parameter (b2) - * @param c The second x-control point parameter (a3) - * @param d The second y-control point parameter (b3) + * @param a An array ofthe four control points [a2,b2,a3,b3] * @param x The target value * @return The calculated value y */ @@ -92,8 +90,8 @@ CoreEasing::bezier(float a, float b, float c, float d) noexcept return create( std::vector{EasingSegment(kCurveSegment, 0), EasingSegment(kEndSegment, 6)}, std::vector{0, 0, a, b, c, d, 1, 1}, - "cubic-bezier(" + std::to_string(a) + "," + std::to_string(b) + "," + std::to_string(c) + - "," + std::to_string(d) + ")"); + "cubic-bezier(" + sutil::to_string(a) + "," + sutil::to_string(b) + "," + + sutil::to_string(c) + "," + sutil::to_string(d) + ")"); } EasingPtr diff --git a/aplcore/src/animation/easing.cpp b/aplcore/src/animation/easing.cpp index 1f8bd53..8ab43e7 100644 --- a/aplcore/src/animation/easing.cpp +++ b/aplcore/src/animation/easing.cpp @@ -61,7 +61,7 @@ Easing::parse(const SessionPtr& session, const std::string& easing) std::move(state.args), s); if (!easingCurve) { - CONSOLE_S(session) << "Unable to create easing curve " << easing; + CONSOLE(session) << "Unable to create easing curve " << easing; } else { if (sEasingCacheDirty) { sEasingCache.clean(); @@ -73,7 +73,7 @@ Easing::parse(const SessionPtr& session, const std::string& easing) } } catch (pegtl::parse_error& e) { - CONSOLE_S(session) << "Parse error in " << easing << " - " << e.what(); + CONSOLE(session) << "Parse error in " << easing << " - " << e.what(); } return Easing::linear(); diff --git a/aplcore/src/command/autopagecommand.cpp b/aplcore/src/command/autopagecommand.cpp index 2f0e2e0..4c1a585 100644 --- a/aplcore/src/command/autopagecommand.cpp +++ b/aplcore/src/command/autopagecommand.cpp @@ -33,7 +33,7 @@ AutoPageCommand::propDefSet() const { ActionPtr AutoPageCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring AutoPage in fast mode"; + CONSOLE(mContext) << "Ignoring AutoPage in fast mode"; return nullptr; } diff --git a/aplcore/src/command/commandfactory.cpp b/aplcore/src/command/commandfactory.cpp index a7d9014..8ed4b31 100644 --- a/aplcore/src/command/commandfactory.cpp +++ b/aplcore/src/command/commandfactory.cpp @@ -99,7 +99,7 @@ CommandFactory::expandMacro(const ContextPtr& context, const std::string& parentSequencer) { assert(definition.IsObject()); - LOG_IF(DEBUG_COMMAND_FACTORY) << "Expanding macro"; + LOG_IF(DEBUG_COMMAND_FACTORY).session(context) << "Expanding macro"; // Build a new context for this command macro ContextPtr cptr = Context::createFromParent(context); @@ -108,7 +108,7 @@ CommandFactory::expandMacro(const ContextPtr& context, // the matching named property that was passed in. ParameterArray params(definition); for (const auto& param : params) { - LOG_IF(DEBUG_COMMAND_FACTORY) << "Parsing parameter: " << param.name; + LOG_IF(DEBUG_COMMAND_FACTORY).session(context) << "Parsing parameter: " << param.name; properties.addToContext(cptr, param, false); } @@ -140,7 +140,7 @@ CommandFactory::inflate(const ContextPtr& context, auto type = propertyAsString(*context, command, "type"); if (type.empty()) { - CONSOLE_CTP(context) << "No type defined for command"; + CONSOLE(context) << "No type defined for command"; return nullptr; } @@ -167,7 +167,7 @@ CommandFactory::inflate(const ContextPtr& context, if (!resource.empty()) return expandMacro(context, props, resource.json(), base, parentSequencer); - CONSOLE_CTP(context) << "Unable to find command '" << type << "'"; + CONSOLE(context) << "Unable to find command '" << type << "'"; return nullptr; } @@ -176,7 +176,6 @@ CommandFactory::inflate(const ContextPtr& context, * @param context The context in which the command should be expanded. * @param command The command definition. Should be a map. * @param base The base component in which the command was defined. - * @param fastMode True if the command is executing in fast mode. * @return The inflated command or nullptr if it is invalid. */ CommandPtr @@ -195,4 +194,4 @@ CommandFactory::inflate(const Object& command, const std::shared_ptrcontext(), command, properties, parent->base(), parent->sequencer()); } -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/command/controlmediacommand.cpp b/aplcore/src/command/controlmediacommand.cpp index b62ac66..2653354 100644 --- a/aplcore/src/command/controlmediacommand.cpp +++ b/aplcore/src/command/controlmediacommand.cpp @@ -36,14 +36,14 @@ ControlMediaCommand::execute(const TimersPtr& timers, bool fastMode) { return nullptr; if (mTarget->getType() != ComponentType::kComponentTypeVideo) { - CONSOLE_CTP(mContext) << "Target of ControlMedia must be a video component"; + CONSOLE(mContext) << "Target of ControlMedia must be a video component"; return nullptr; } // All commands except "Play" are allowed in fast mode auto command = getValue(kCommandPropertyCommand); if (fastMode && command.getInteger() == kCommandControlMediaPlay) { - CONSOLE_CTP(mContext) << "Ignoring ControlMedia.play in fast mode"; + CONSOLE(mContext) << "Ignoring ControlMedia.play in fast mode"; return nullptr; } diff --git a/aplcore/src/command/corecommand.cpp b/aplcore/src/command/corecommand.cpp index 10450c1..aadcc94 100644 --- a/aplcore/src/command/corecommand.cpp +++ b/aplcore/src/command/corecommand.cpp @@ -153,7 +153,7 @@ CoreCommand::validate() auto p = mProperties.find(cpd.second.names); if (p == mProperties.end()) { - CONSOLE_CTP(mContext) << "Missing required property '" << cpd.second.names << "' for " << name(); + CONSOLE(mContext) << "Missing required property '" << cpd.second.names << "' for " << name(); return false; } } @@ -185,12 +185,12 @@ CoreCommand::calculateProperties() if (mTarget == nullptr && (cpd->second.flags & kPropRequired)) { // TODO: Try full inflation, we may be missing deep component inflated. Quite inefficient, especially if done // in onMount, revisit. - LOG(LogLevel::kWarn) << "Trying to scroll to uninflated component. Flushing pending layouts."; + LOG(LogLevel::kWarn).session(mContext) << "Trying to scroll to uninflated component. Flushing pending layouts."; auto& lm = mContext->layoutManager(); lm.flushLazyInflation(); mTarget = std::dynamic_pointer_cast(mContext->findComponentById(id)); if (mTarget == nullptr) { - CONSOLE_CTP(mContext) << "Illegal command " << name() << " - need to specify a target componentId"; + CONSOLE(mContext) << "Illegal command " << name() << " - need to specify a target componentId"; return false; } } @@ -219,7 +219,7 @@ CoreCommand::calculateProperties() // Enumerated properties must be valid if (!result.first) { - CONSOLE_CTP(context) << "Invalid enumerated property for '" << it.second.names << "'"; + CONSOLE(context) << "Invalid enumerated property for '" << it.second.names << "'"; return false; } @@ -229,7 +229,7 @@ CoreCommand::calculateProperties() if (DEBUG_COMMAND_VALUES) { for (const auto& m : mValues) { - LOG(LogLevel::kDebug) << "Property: " << sCommandPropertyBimap.at(m.first) << "(" + LOG(LogLevel::kDebug).session(mContext) << "Property: " << sCommandPropertyBimap.at(m.first) << "(" << m.first << ")"; DumpVisitor::dump(m.second); } diff --git a/aplcore/src/command/extensioneventcommand.cpp b/aplcore/src/command/extensioneventcommand.cpp index ff44471..b85c956 100644 --- a/aplcore/src/command/extensioneventcommand.cpp +++ b/aplcore/src/command/extensioneventcommand.cpp @@ -23,7 +23,7 @@ ActionPtr ExtensionEventCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode && !mDefinition.getAllowFastMode()) { - CONSOLE_CTP(mContext) << "Ignoring extension " << mDefinition.getName() << " command in fast mode"; + CONSOLE(mContext) << "Ignoring extension " << mDefinition.getName() << " command in fast mode"; return nullptr; } @@ -37,7 +37,7 @@ ExtensionEventCommand::execute(const TimersPtr& timers, bool fastMode) auto it = mProperties.find(m.first); if (it == mProperties.end()) { if (m.second.required) { - CONSOLE_CTP(mContext) << "Missing required property '" << m.first << "' for extension command '" + CONSOLE(mContext) << "Missing required property '" << m.first << "' for extension command '" << mDefinition.getName() << "': dropping command"; return nullptr; } diff --git a/aplcore/src/command/openurlcommand.cpp b/aplcore/src/command/openurlcommand.cpp index e0b3dad..4947224 100644 --- a/aplcore/src/command/openurlcommand.cpp +++ b/aplcore/src/command/openurlcommand.cpp @@ -33,7 +33,7 @@ OpenURLCommand::propDefSet() const { ActionPtr OpenURLCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring OpenURL in fast mode"; + CONSOLE(mContext) << "Ignoring OpenURL in fast mode"; return nullptr; } diff --git a/aplcore/src/command/playmediacommand.cpp b/aplcore/src/command/playmediacommand.cpp index 855284f..d3690e5 100644 --- a/aplcore/src/command/playmediacommand.cpp +++ b/aplcore/src/command/playmediacommand.cpp @@ -34,7 +34,7 @@ ActionPtr PlayMediaCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring PlayMedia command in fast mode"; + CONSOLE(mContext) << "Ignoring PlayMedia command in fast mode"; return nullptr; } @@ -42,7 +42,7 @@ PlayMediaCommand::execute(const TimersPtr& timers, bool fastMode) return nullptr; if (mTarget->getType() != ComponentType::kComponentTypeVideo) { - CONSOLE_CTP(mContext) << "Target of PlayMedia must be a video component"; + CONSOLE(mContext) << "Target of PlayMedia must be a video component"; return nullptr; } diff --git a/aplcore/src/command/scrollcommand.cpp b/aplcore/src/command/scrollcommand.cpp index a514ab6..ba4d487 100644 --- a/aplcore/src/command/scrollcommand.cpp +++ b/aplcore/src/command/scrollcommand.cpp @@ -32,7 +32,7 @@ ScrollCommand::propDefSet() const { ActionPtr ScrollCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring Scroll in fast mode"; + CONSOLE(mContext) << "Ignoring Scroll in fast mode"; return nullptr; } @@ -40,7 +40,7 @@ ScrollCommand::execute(const TimersPtr& timers, bool fastMode) { return nullptr; if (!mTarget || mTarget->scrollType() == kScrollTypeNone) { - CONSOLE_CTP(mContext) << "Attempting to scroll non-scrollable component"; + CONSOLE(mContext) << "Attempting to scroll non-scrollable component"; return nullptr; } diff --git a/aplcore/src/command/scrolltocomponentcommand.cpp b/aplcore/src/command/scrolltocomponentcommand.cpp index 8c666ee..caa1c09 100644 --- a/aplcore/src/command/scrolltocomponentcommand.cpp +++ b/aplcore/src/command/scrolltocomponentcommand.cpp @@ -31,7 +31,7 @@ ScrollToComponentCommand::propDefSet() const { ActionPtr ScrollToComponentCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring ScrollTo command in fast mode"; + CONSOLE(mContext) << "Ignoring ScrollTo command in fast mode"; return nullptr; } diff --git a/aplcore/src/command/scrolltoindexcommand.cpp b/aplcore/src/command/scrolltoindexcommand.cpp index e239e96..ddcc1b1 100644 --- a/aplcore/src/command/scrolltoindexcommand.cpp +++ b/aplcore/src/command/scrolltoindexcommand.cpp @@ -33,7 +33,7 @@ ScrollToIndexCommand::propDefSet() const { ActionPtr ScrollToIndexCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring ScrollToIndex in fast mode"; + CONSOLE(mContext) << "Ignoring ScrollToIndex in fast mode"; return nullptr; } @@ -45,7 +45,7 @@ ScrollToIndexCommand::execute(const TimersPtr& timers, bool fastMode) { auto childCount = mTarget->getChildCount(); childIndex = childIndex < 0 ? childIndex + childCount : childIndex; if (childIndex >= childCount || childIndex < 0) { - CONSOLE_CTP(mContext) << "ScrollToIndex invalid child index=" << childIndex; + CONSOLE(mContext) << "ScrollToIndex invalid child index=" << childIndex; return nullptr; } diff --git a/aplcore/src/command/sendeventcommand.cpp b/aplcore/src/command/sendeventcommand.cpp index f022e37..e83ab23 100644 --- a/aplcore/src/command/sendeventcommand.cpp +++ b/aplcore/src/command/sendeventcommand.cpp @@ -37,7 +37,7 @@ SendEventCommand::propDefSet() const { ActionPtr SendEventCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring SendEvent command in fast mode"; + CONSOLE(mContext) << "Ignoring SendEvent command in fast mode"; return nullptr; } @@ -84,9 +84,9 @@ SendEventCommand::execute(const TimersPtr& timers, bool fastMode) { } if (DEBUG_SEND_EVENT) { - LOG(LogLevel::kDebug) << "SendEvent Bag"; - for (auto m : bag) { - LOG(LogLevel::kDebug) << "Property: " << sEventPropertyBimap.at(m.first) << "(" + LOG(LogLevel::kDebug).session(mContext) << "SendEvent Bag"; + for (auto& m : bag) { + LOG(LogLevel::kDebug).session(mContext) << "Property: " << sEventPropertyBimap.at(m.first) << "(" << m.first << ")"; DumpVisitor::dump(m.second); } diff --git a/aplcore/src/command/setpagecommand.cpp b/aplcore/src/command/setpagecommand.cpp index 9027062..e489b72 100644 --- a/aplcore/src/command/setpagecommand.cpp +++ b/aplcore/src/command/setpagecommand.cpp @@ -33,7 +33,7 @@ SetPageCommand::propDefSet() const { ActionPtr SetPageCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring SetPage command in fast mode"; + CONSOLE(mContext) << "Ignoring SetPage command in fast mode"; return nullptr; } diff --git a/aplcore/src/command/setvaluecommand.cpp b/aplcore/src/command/setvaluecommand.cpp index a5a2d26..4d2c54b 100644 --- a/aplcore/src/command/setvaluecommand.cpp +++ b/aplcore/src/command/setvaluecommand.cpp @@ -38,7 +38,7 @@ SetValueCommand::execute(const TimersPtr& timers, bool fastMode) { std::string property = mValues.at(kCommandPropertyProperty).asString(); Object value = mValues.at(kCommandPropertyValue); - LOG_IF(DEBUG_SET_VALUE) << "SetValue - property: "<< property << " value: "<< value; + LOG_IF(DEBUG_SET_VALUE).session(mContext) << "SetValue - property: "<< property << " value: "<< value; if (sComponentPropertyBimap.has(property)) { auto propKey = static_cast(sComponentPropertyBimap.at(property)); diff --git a/aplcore/src/command/speakitemcommand.cpp b/aplcore/src/command/speakitemcommand.cpp index 60d4bc9..ab14302 100644 --- a/aplcore/src/command/speakitemcommand.cpp +++ b/aplcore/src/command/speakitemcommand.cpp @@ -35,7 +35,7 @@ SpeakItemCommand::propDefSet() const { ActionPtr SpeakItemCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring SpeakItem command in fast mode"; + CONSOLE(mContext) << "Ignoring SpeakItem command in fast mode"; return nullptr; } @@ -43,7 +43,7 @@ SpeakItemCommand::execute(const TimersPtr& timers, bool fastMode) { return nullptr; if (mContext->getRootConfig().getProperty(RootProperty::kDisallowDialog).getBoolean()) { - CONSOLE_CTP(mContext) << "Ignoring SpeakItem command because disallowDialog is true"; + CONSOLE(mContext) << "Ignoring SpeakItem command because disallowDialog is true"; return nullptr; } diff --git a/aplcore/src/command/speaklistcommand.cpp b/aplcore/src/command/speaklistcommand.cpp index f7d9793..9c8affd 100644 --- a/aplcore/src/command/speaklistcommand.cpp +++ b/aplcore/src/command/speaklistcommand.cpp @@ -37,7 +37,7 @@ SpeakListCommand::propDefSet() const { ActionPtr SpeakListCommand::execute(const TimersPtr& timers, bool fastMode) { if (fastMode) { - CONSOLE_CTP(mContext) << "Ignoring SpeakList command in fast mode"; + CONSOLE(mContext) << "Ignoring SpeakList command in fast mode"; return nullptr; } @@ -45,7 +45,7 @@ SpeakListCommand::execute(const TimersPtr& timers, bool fastMode) { return nullptr; if (mContext->getRootConfig().getProperty(RootProperty::kDisallowDialog).getBoolean()) { - CONSOLE_CTP(mContext) << "Ignoring SpeakList command because disallowDialog is true"; + CONSOLE(mContext) << "Ignoring SpeakList command because disallowDialog is true"; return nullptr; } diff --git a/aplcore/src/component/component.cpp b/aplcore/src/component/component.cpp index 9d4b2f0..f31467d 100644 --- a/aplcore/src/component/component.cpp +++ b/aplcore/src/component/component.cpp @@ -39,19 +39,19 @@ Component::name() const void Component::updateMediaState(const MediaState& state, bool fromEvent) { - LOG(LogLevel::kError) << "updateMediaState called for component that does not support it."; + LOG(LogLevel::kError).session(mContext) << "updateMediaState called for component that does not support it."; } bool Component::updateGraphic(const GraphicContentPtr& json) { - LOG(LogLevel::kError) << "updateGraphic called for component that does not support it."; + LOG(LogLevel::kError).session(mContext) << "updateGraphic called for component that does not support it."; return false; } void Component::updateResourceState(const ExtensionComponentResourceState& state, int errorCode, const std::string& error) { - LOG(LogLevel::kError) << "updateResourceState called for component that does not support it."; + LOG(LogLevel::kError).session(mContext) << "updateResourceState called for component that does not support it."; } void @@ -106,7 +106,7 @@ Component::toDebugSimpleString() const bool Component::isCharacterValid(const wchar_t wc) const { - LOG(LogLevel::kError) << "isCharacterValid called for component that does not support it."; + LOG(LogLevel::kError).session(mContext) << "isCharacterValid called for component that does not support it."; return false; } diff --git a/aplcore/src/component/componentproperties.cpp b/aplcore/src/component/componentproperties.cpp index 67c445c..a415f25 100644 --- a/aplcore/src/component/componentproperties.cpp +++ b/aplcore/src/component/componentproperties.cpp @@ -295,6 +295,7 @@ Bimap sComponentPropertyBimap = { {kPropertyAlignSelf, "alignSelf"}, {kPropertyAudioTrack, "audioTrack"}, {kPropertyAutoplay, "autoplay"}, + {kPropertyMuted, "muted"}, {kPropertyBackgroundColor, "backgroundColor"}, {kPropertyBorderBottomLeftRadius, "borderBottomLeftRadius"}, {kPropertyBorderBottomRightRadius, "borderBottomRightRadius"}, diff --git a/aplcore/src/component/corecomponent.cpp b/aplcore/src/component/corecomponent.cpp index 210532d..c5250d7 100644 --- a/aplcore/src/component/corecomponent.cpp +++ b/aplcore/src/component/corecomponent.cpp @@ -216,7 +216,7 @@ CoreComponent::release() /** * Accept a visitor pattern - * @param visitor + * @param visitor The visitor for core components. */ void CoreComponent::accept(Visitor& visitor) const @@ -233,7 +233,7 @@ CoreComponent::accept(Visitor& visitor) const * that the user can see/interact with. Child classes that have knowledge about which children are off screen or otherwise * invalid/unattached should use that knowledge to reduce the number of nodes walked or avoid walking otherwise invalid * components they may have stashed in their children. - * @param visitor + * @param visitor The visitor for core components */ void CoreComponent::raccept(Visitor& visitor) const @@ -308,7 +308,7 @@ CoreComponent::update(UpdateType type, float value) setState(kStatePressed, value != 0); break; default: - LOG(LogLevel::kWarn) << "Unexpected update command type " << type << " value=" << value; + LOG(LogLevel::kWarn).session(mContext) << "Unexpected update command type " << type << " value=" << value; break; } } @@ -452,7 +452,7 @@ CoreComponent::ensureDisplayedChildren() childBounds = transform.calculateAxisAlignedBoundingBox(Rect{0, 0, childBounds.getWidth(), childBounds.getHeight()}); childBounds.offset(childBoundsTopLeft); - if (!viewportRect.intersect(childBounds).isEmpty()) { + if (!viewportRect.intersect(childBounds).empty()) { if (child->getCalculated(kPropertyPosition) == kPositionSticky) { sticky.emplace_back(child); } else { @@ -631,7 +631,7 @@ CoreComponent::markAdded() void CoreComponent::ensureLayoutInternal(bool useDirtyFlag) { - LOG_IF(DEBUG_ENSURE) << toDebugSimpleString() << " useDirtyFlag=" << useDirtyFlag; + LOG_IF(DEBUG_ENSURE).session(mContext) << toDebugSimpleString() << " useDirtyFlag=" << useDirtyFlag; APL_TRACE_BLOCK("CoreComponent:ensureLayout"); auto& lm = mContext->layoutManager(); if (lm.ensure(shared_from_corecomponent())) @@ -742,7 +742,7 @@ CoreComponent::assignProperties(const ComponentPropDefSet& propDefSet) // Make sure this wasn't a required property if ((pd.flags & kPropRequired) != 0) { mFlags |= kComponentFlagInvalid; - CONSOLE_CTP(mContext) << "Missing required property: " << pd.names; + CONSOLE(mContext) << "Missing required property: " << pd.names; } // Check for a styled property @@ -922,7 +922,7 @@ CoreComponent::find(PropertyKey key) const /** * A property has been set on the component. - * @param id The property iterator. Contains the key of the property being changed and the definition. + * @param it The property iterator. Contains the key of the property being changed and the definition. * @param value The new value of the property. * @return True if the property could be set (it must be dynamic). */ @@ -981,7 +981,7 @@ CoreComponent::setProperty(PropertyKey key, const Object& value) if (findRef.first && setPropertyInternal(findRef.second, value)) return true; - CONSOLE_CTP(mContext) << "Invalid property key '" << sComponentPropertyBimap.at(key) << "' for this component"; + CONSOLE(mContext) << "Invalid property key '" << sComponentPropertyBimap.at(key) << "' for this component"; return false; } @@ -1000,7 +1000,7 @@ CoreComponent::setProperty( const std::string& key, const Object& value ) if (setPropertyInternal(key, value)) return; - CONSOLE_CTP(mContext) << "Unknown property name " << key; + CONSOLE(mContext) << "Unknown property name " << key; } std::pair @@ -1023,7 +1023,7 @@ CoreComponent::getPropertyAndWriteableState(const std::string& key) const if (internal.second) return internal; - CONSOLE_CTP(mContext) << "Unknown property name " << key; + CONSOLE(mContext) << "Unknown property name " << key; return { Object::NULL_OBJECT(), false }; } @@ -1095,7 +1095,7 @@ CoreComponent::updateProperty(PropertyKey key, const Object& value) } // We should not reach this point. Only an assigned equation calls updateProperty - CONSOLE_CTP(mContext) << "Property " << sComponentPropertyBimap.at(key) + CONSOLE(mContext) << "Property " << sComponentPropertyBimap.at(key) << " is not dynamic and can't be updated."; } @@ -1163,7 +1163,7 @@ void CoreComponent::setState( StateProperty stateProperty, bool value ) { if (mInheritParentState) { - CONSOLE_CTP(mContext) << "Cannot assign state properties to a child that inherits parent state"; + CONSOLE(mContext) << "Cannot assign state properties to a child that inherits parent state"; return; } @@ -1444,7 +1444,7 @@ CoreComponent::processLayoutChanges(bool useDirtyFlag, bool first) setDirty(kPropertyInnerBounds); } - if (!mCalculated.get(kPropertyLaidOut).asBoolean() && !mCalculated.get(kPropertyBounds).getRect().isEmpty()) { + if (!mCalculated.get(kPropertyLaidOut).asBoolean() && !mCalculated.get(kPropertyBounds).getRect().empty()) { mCalculated.set(kPropertyLaidOut, true); if (useDirtyFlag) setDirty(kPropertyLaidOut); @@ -1576,7 +1576,7 @@ CoreComponent::getHierarchySignature() const void CoreComponent::fixTransform(bool useDirtyFlag) { - LOG_IF(DEBUG_TRANSFORM) << mCalculated.get(kPropertyTransform).getTransform2D(); + LOG_IF(DEBUG_TRANSFORM).session(mContext) << mCalculated.get(kPropertyTransform).getTransform2D(); Transform2D updated; @@ -1605,7 +1605,7 @@ CoreComponent::fixTransform(bool useDirtyFlag) if (useDirtyFlag) setDirty(kPropertyTransform); - LOG_IF(DEBUG_TRANSFORM) << "updated to " << mCalculated.get(kPropertyTransform).getTransform2D(); + LOG_IF(DEBUG_TRANSFORM).session(mContext) << "updated to " << mCalculated.get(kPropertyTransform).getTransform2D(); } } @@ -1622,7 +1622,7 @@ setPaddingIfKeyFound(PropertyKey key, YGEdge edge, CalculatedPropertyMap& map, Y void CoreComponent::fixPadding() { - LOG_IF(DEBUG_PADDING) << mCalculated.get(kPropertyPadding); + LOG_IF(DEBUG_PADDING).session(mContext) << mCalculated.get(kPropertyPadding); static std::vector> EDGES = { {kPropertyPaddingLeft, YGEdgeLeft}, @@ -1653,7 +1653,7 @@ CoreComponent::fixPadding() void CoreComponent::fixLayoutDirection(bool useDirtyFlag) { - LOG_IF(DEBUG_LAYOUTDIRECTION) << mCalculated.get(kPropertyLayoutDirection); + LOG_IF(DEBUG_LAYOUTDIRECTION).session(mContext) << mCalculated.get(kPropertyLayoutDirection); auto reportedLayoutDirection = static_cast(mCalculated.get(kPropertyLayoutDirection).asInt()); auto currentLayoutDirection = getLayoutDirection() == YGDirectionLTR ? kLayoutDirectionLTR : kLayoutDirectionRTL; if (reportedLayoutDirection != currentLayoutDirection) { @@ -1662,7 +1662,7 @@ CoreComponent::fixLayoutDirection(bool useDirtyFlag) setDirty(kPropertyLayoutDirection); } handleLayoutDirectionChange(useDirtyFlag); - LOG_IF(DEBUG_LAYOUTDIRECTION) << "updated to " << mCalculated.get(kPropertyLayoutDirection); + LOG_IF(DEBUG_LAYOUTDIRECTION).session(mContext) << "updated to " << mCalculated.get(kPropertyLayoutDirection); } } @@ -1670,7 +1670,7 @@ void CoreComponent::setHeight(const Dimension& height) { if (mYGNodeRef) yn::setHeight(mYGNodeRef, height, *mContext); else - LOG(LogLevel::kError) << "setHeight: Missing yoga node for component id '" << getId() << "'"; + LOG(LogLevel::kError).session(mContext) << "setHeight: Missing yoga node for component id '" << getId() << "'"; mCalculated.set(kPropertyHeight, height); } @@ -1678,7 +1678,7 @@ void CoreComponent::setWidth(const Dimension& width) { if (mYGNodeRef) yn::setWidth(mYGNodeRef, width, *mContext); else - LOG(LogLevel::kError) << "setWidth: Missing yoga node for component id '" << getId() << "'"; + LOG(LogLevel::kError).session(mContext) << "setWidth: Missing yoga node for component id '" << getId() << "'"; mCalculated.set(kPropertyWidth, width); } @@ -1773,7 +1773,7 @@ CoreComponent::serializeDirty(rapidjson::Document::AllocatorType& allocator) { } rapidjson::Value -CoreComponent:: serializeVisualContext(rapidjson::Document::AllocatorType& allocator) { +CoreComponent::serializeVisualContext(rapidjson::Document::AllocatorType& allocator) { float viewportWidth = mContext->width(); float viewportHeight = mContext->height(); Rect viewportRect(0, 0, viewportWidth, viewportHeight); @@ -2117,27 +2117,16 @@ void CoreComponent::resolveDrawnBorder(Component& component) { void CoreComponent::calculateDrawnBorder(bool useDirtyFlag ) { - auto strokeWidthProp = getCalculated(kPropertyBorderStrokeWidth); - float borderWidth = getCalculated(kPropertyBorderWidth).asAbsoluteDimension(*mContext).getValue(); - float drawnWidth = borderWidth; // default the drawn width to the border width + auto strokeWidth = getCalculated(kPropertyBorderStrokeWidth); + auto borderWidth = getCalculated(kPropertyBorderWidth); - if (strokeWidthProp == Object::NULL_OBJECT()) { - // no stroke width - default draw border width to border width - // initialize stroke width to border width - mCalculated.set(kPropertyBorderStrokeWidth, Object(Dimension(borderWidth))); - } else { - // stroke width - clamp the drawn border to the border width - float strokeWidth = strokeWidthProp.getAbsoluteDimension(); - if (strokeWidth < borderWidth) - drawnWidth = strokeWidth; - } - - Dimension dimension(drawnWidth); + auto drawnBorderWidth = borderWidth; + if (!strokeWidth.isNull()) + drawnBorderWidth = Object(Dimension( + std::min(strokeWidth.getAbsoluteDimension(), borderWidth.getAbsoluteDimension()))); - auto drawnWidthProp = getCalculated(kPropertyDrawnBorderWidth); - if (drawnWidthProp == Object::NULL_OBJECT() || - dimension != mCalculated.get(kPropertyDrawnBorderWidth).asAbsoluteDimension(*mContext)) { - mCalculated.set(kPropertyDrawnBorderWidth, Object(std::move(dimension))); + if (drawnBorderWidth != getCalculated(kPropertyDrawnBorderWidth)) { + mCalculated.set(kPropertyDrawnBorderWidth, drawnBorderWidth); if (useDirtyFlag) setDirty(kPropertyDrawnBorderWidth); } @@ -2167,7 +2156,7 @@ CoreComponent::textMeasureInternal(float width, YGMeasureMode widthMode, float h fixVisualHash(true); auto componentHash = textMeasurementHash(); - LOG_IF(DEBUG_MEASUREMENT) + LOG_IF(DEBUG_MEASUREMENT).session(mContext) << "Measuring: " << getUniqueId() << " hash: " << componentHash << " width: " << width @@ -2186,7 +2175,7 @@ CoreComponent::textMeasureInternal(float width, YGMeasureMode widthMode, float h this, width, toMeasureMode(widthMode), height, toMeasureMode(heightMode)); auto size = YGSize({layoutSize.width, layoutSize.height}); measuresCache.put(tmr, size); - LOG_IF(DEBUG_MEASUREMENT) << "Size: " << size.width << "x" << size.height; + LOG_IF(DEBUG_MEASUREMENT).session(mContext) << "Size: " << size.width << "x" << size.height; APL_TRACE_END("CoreComponent:textMeasureInternal:runtimeMeasure"); return size; } @@ -2385,7 +2374,7 @@ CoreComponent::inParentViewport() const { // Shift by scroll position if any parentBounds.offset(mParent->scrollPosition()); - return !parentBounds.intersect(bounds).isEmpty(); + return !parentBounds.intersect(bounds).empty(); } PointerCaptureStatus diff --git a/aplcore/src/component/edittextcomponent.cpp b/aplcore/src/component/edittextcomponent.cpp index ed16ac8..d7c6fd1 100644 --- a/aplcore/src/component/edittextcomponent.cpp +++ b/aplcore/src/component/edittextcomponent.cpp @@ -20,7 +20,7 @@ #include "apl/content/rootconfig.h" #include "apl/engine/event.h" #include "apl/focus/focusmanager.h" -#include "apl/primitives/characterrange.h" +#include "apl/primitives/unicode.h" #include "apl/time/sequencer.h" #include "apl/touch/pointerevent.h" @@ -57,7 +57,13 @@ EditTextComponent::assignProperties(const ComponentPropDefSet& propDefSet) { ActionableComponent::assignProperties(propDefSet); calculateDrawnBorder(false); - parseValidCharactersProperty(); + + // Force the text to match the valid characters + const auto& current = getCalculated(kPropertyText).getString(); + const auto& valid = getCalculated(kPropertyValidCharacters).getString(); + auto text = utf8StripInvalid(current, valid); + if (text != current) + mCalculated.set(kPropertyText, text); // Calculate initial measurement hash. fixTextMeasurementHash(); @@ -96,9 +102,21 @@ static inline Object defaultHighlightColor(Component& component, const RootConfi const ComponentPropDefSet& EditTextComponent::propDefSet() const { + // This is only called from 'setProperty()' + static auto checkText = [](Component& component) { + auto& coreComp = dynamic_cast(component); + const auto& current = component.getCalculated(kPropertyText).getString(); + const auto& valid = component.getCalculated(kPropertyValidCharacters).getString(); + auto text = utf8StripInvalid(current, valid); + if (text != current) { + coreComp.mCalculated.set(kPropertyText, text); + coreComp.setDirty(kPropertyText); + } + }; + static ComponentPropDefSet sEditTextComponentProperties(ActionableComponent::propDefSet(), { {kPropertyBorderColor, Color(), asColor, kPropInOut | kPropStyled | kPropDynamic | kPropVisualHash}, - {kPropertyBorderWidth, Dimension(0), asNonNegativeAbsoluteDimension, kPropInOut | kPropStyled | kPropDynamic, yn::setBorder}, + {kPropertyBorderWidth, Dimension(0), asNonNegativeAbsoluteDimension, kPropInOut | kPropStyled | kPropDynamic, yn::setBorder, resolveDrawnBorder}, {kPropertyColor, Color(), asColor, kPropInOut | kPropStyled | kPropDynamic | kPropVisualHash, defaultFontColor}, {kPropertyFontFamily, "", asString, kPropInOut | kPropLayout | kPropStyled | kPropDynamic | kPropTextHash | kPropVisualHash, defaultFontFamily}, {kPropertyFontSize, Dimension(40), asAbsoluteDimension, kPropInOut | kPropLayout | kPropStyled | kPropDynamic | kPropTextHash | kPropVisualHash}, @@ -119,8 +137,8 @@ EditTextComponent::propDefSet() const {kPropertySelectOnFocus, false, asBoolean, kPropInOut | kPropStyled}, {kPropertySize, 8, asPositiveInteger, kPropInOut | kPropStyled | kPropLayout}, {kPropertySubmitKeyType, kSubmitKeyTypeDone, sSubmitKeyTypeMap, kPropInOut | kPropStyled}, - {kPropertyText, "", asString, kPropInOut | kPropDynamic | kPropVisualContext | kPropTextHash | kPropVisualHash}, - {kPropertyValidCharacters, "", asString, kPropIn | kPropStyled}, + {kPropertyText, "", asString, kPropInOut | kPropDynamic | kPropVisualContext | kPropTextHash | kPropVisualHash, checkText}, + {kPropertyValidCharacters, "", asString, kPropIn | kPropStyled, checkText}, // The width of the drawn border. If borderStrokeWith is set, the drawn border is the min of borderWidth // and borderStrokeWidth. If borderStrokeWidth is unset, the drawn border defaults to borderWidth @@ -185,21 +203,7 @@ EditTextComponent::update(UpdateType type, const std::string& value) bool EditTextComponent::isCharacterValid(const wchar_t wc) const { - if (mCharacterRangesPtr == nullptr) return true; - - std::vector validRanges = mCharacterRangesPtr->getRanges(); - if (validRanges.empty()) return true; - - for (auto& range : validRanges) { - if (range.isCharacterValid(wc)) return true; - } - return false; -} - -void EditTextComponent::parseValidCharactersProperty() -{ - mCharacterRangesPtr = std::make_shared(CharacterRanges(getContext()->session(), - mCalculated.get(kPropertyValidCharacters).asString())); + return wcharValidCharacter(wc, getCalculated(kPropertyValidCharacters).getString()); } PointerCaptureStatus diff --git a/aplcore/src/component/framecomponent.cpp b/aplcore/src/component/framecomponent.cpp index 6102629..3b35a91 100644 --- a/aplcore/src/component/framecomponent.cpp +++ b/aplcore/src/component/framecomponent.cpp @@ -62,7 +62,8 @@ FrameComponent::propDefSet() const {kPropertyBorderWidth, Dimension(0), asNonNegativeAbsoluteDimension, kPropInOut | kPropStyled | kPropDynamic | - kPropVisualHash, yn::setBorder}, + kPropVisualHash, + yn::setBorder, resolveDrawnBorder}, // These are input-only properties that trigger the calculation of the output properties {kPropertyBorderBottomLeftRadius, Object::NULL_OBJECT(), asAbsoluteDimension, kPropIn | diff --git a/aplcore/src/component/multichildscrollablecomponent.cpp b/aplcore/src/component/multichildscrollablecomponent.cpp index 99d3e20..19783d3 100644 --- a/aplcore/src/component/multichildscrollablecomponent.cpp +++ b/aplcore/src/component/multichildscrollablecomponent.cpp @@ -65,14 +65,14 @@ template void setScrollAlignId(CoreComponent& component, const Object& value) { if (!value.isArray() || value.size() != 2) { - CONSOLE_CTP(component.getContext()) << "Invalid " << value.toDebugString(); + CONSOLE(component.getContext()) << "Invalid " << value.toDebugString(); return; } auto id = value.at(0).asString(); auto child = std::dynamic_pointer_cast(component.findComponentById(id)); if (!child) { - CONSOLE_CTP(component.getContext()) << "Unable to find child with id " << id; + CONSOLE(component.getContext()) << "Unable to find child with id " << id; return; } @@ -84,13 +84,13 @@ template void setScrollAlignIndex(CoreComponent& component, const Object& value) { if (!value.isArray() || value.size() != 2) { - CONSOLE_CTP(component.getContext()) << "Invalid " << value.toDebugString(); + CONSOLE(component.getContext()) << "Invalid " << value.toDebugString(); return; } auto index = value.at(0).asInt(); if (index < 0 || index >= component.getChildCount()) { - CONSOLE_CTP(component.getContext()) << "Child index out of range " << index; + CONSOLE(component.getContext()) << "Child index out of range " << index; return; } @@ -368,7 +368,7 @@ MultiChildScrollableComponent::accept(Visitor& visitor) const i <= mEnsuredChildren.upperBound() && !visitor.isAborted(); i++) { auto child = std::dynamic_pointer_cast(mChildren.at(i)); if (child != nullptr && child->isAttached() && - !child->getCalculated(kPropertyBounds).getRect().isEmpty()) + !child->getCalculated(kPropertyBounds).getRect().empty()) child->accept(visitor); } } @@ -385,7 +385,7 @@ MultiChildScrollableComponent::raccept(Visitor& visitor) const i >= mEnsuredChildren.lowerBound() && !visitor.isAborted(); i--) { auto child = std::dynamic_pointer_cast(mChildren.at(i)); if (child != nullptr && child->isAttached() && - !child->getCalculated(kPropertyBounds).getRect().isEmpty()) + !child->getCalculated(kPropertyBounds).getRect().empty()) child->raccept(visitor); } } @@ -522,6 +522,7 @@ MultiChildScrollableComponent::trimScroll(const Point& point) auto idx = mEnsuredChildren.lowerBound() - 1; layoutChildIfRequired(mChildren.at(idx), idx, true, false); zeroAnchorPos = zeroAnchor->getCalculated(kPropertyBounds).getRect().getTopLeft() - innerBounds.getTopLeft(); + reportLoaded(idx); } fixScrollPosition(oldZeroAnchorPos, zeroAnchor->getCalculated(kPropertyBounds).getRect()); @@ -538,8 +539,10 @@ MultiChildScrollableComponent::trimScroll(const Point& point) const auto& child = mChildren.at(i); layoutChildIfRequired(child, i, false, false); maxY = std::max(maxY, nonNegative(child->getCalculated(kPropertyBounds).getRect().getBottom() - bottom)); - if (y <= maxY) - return Point(0,y); + if (y <= maxY) { + reportLoaded(i); + return Point(0, y); + } } return Point(0, maxY); @@ -550,6 +553,7 @@ MultiChildScrollableComponent::trimScroll(const Point& point) auto idx = mEnsuredChildren.lowerBound() - 1; layoutChildIfRequired(mChildren.at(idx), idx, true, false); zeroAnchorPos = zeroAnchor->getCalculated(kPropertyBounds).getRect().getTopLeft() - innerBounds.getTopLeft(); + reportLoaded(idx); } fixScrollPosition(oldZeroAnchorPos, zeroAnchor->getCalculated(kPropertyBounds).getRect()); @@ -566,8 +570,10 @@ MultiChildScrollableComponent::trimScroll(const Point& point) const auto& child = mChildren.at(i); layoutChildIfRequired(child, i, true, false); maxX = std::max(maxX, nonNegative(child->getCalculated(kPropertyBounds).getRect().getRight() - right)); - if (x <= maxX) - return Point(x,0); + if (x <= maxX) { + reportLoaded(i); + return Point(x, 0); + } } return Point(maxX, 0); @@ -579,6 +585,7 @@ MultiChildScrollableComponent::trimScroll(const Point& point) auto idx = mEnsuredChildren.lowerBound() - 1; layoutChildIfRequired(mChildren.at(idx), idx, true, false); zeroAnchorPos = zeroAnchor->getCalculated(kPropertyBounds).getRect().getTopRight() - innerBounds.getTopRight(); + reportLoaded(idx); } fixScrollPosition(oldZeroAnchorPos, zeroAnchor->getCalculated(kPropertyBounds).getRect()); @@ -595,8 +602,10 @@ MultiChildScrollableComponent::trimScroll(const Point& point) const auto& child = mChildren.at(i); layoutChildIfRequired(child, i, true, false); maxX = std::min(maxX, nonPositive(child->getCalculated(kPropertyBounds).getRect().getLeft() - left)); - if (x >= maxX) - return Point(x,0); + if (x >= maxX) { + reportLoaded(i); + return Point(x, 0); + } } return Point(maxX, 0); @@ -730,7 +739,7 @@ MultiChildScrollableComponent::relayoutInPlace(bool useDirtyFlag, bool first) auto root = getLayoutRoot(); auto rootBounds = root->getCalculated(kPropertyBounds).getRect(); APL_TRACE_BEGIN("MultiChildScrollableComponent:YGNodeCalculateLayout:root"); - YGNodeCalculateLayout(root->getNode(), rootBounds.getWidth(), rootBounds.getHeight(), getLayoutDirection()); + YGNodeCalculateLayout(root->getNode(), rootBounds.getWidth(), rootBounds.getHeight(), root->getLayoutDirection()); APL_TRACE_END("MultiChildScrollableComponent:YGNodeCalculateLayout:root"); auto oldBounds = getCalculated(kPropertyBounds); CoreComponent::processLayoutChanges(useDirtyFlag, first); @@ -806,8 +815,18 @@ MultiChildScrollableComponent::removeChild(const CoreComponentPtr& child, size_t void MultiChildScrollableComponent::runLayoutHeuristics(size_t anchorIdx, float childCache, float pageSize, bool useDirtyFlag, bool first) { - // Estimate how many children is actually required based on available anchor dimensions - auto toCover = estimateChildrenToCover(first ? pageSize : (childCache + 1) * pageSize, anchorIdx); + APL_TRACE_BLOCK("MultiChildScrollableComponent:runLayoutHeuristics"); + // Estimate how many children is actually required based on available anchor dimensions. + // In cases when firstChild used it's main use is for padding or "headers". Size of such item is likely quite + // different from "normal" data-inflated items and may lead to aroximation which will layout much more items than + // actually required. To avoid such cases - use 2nd item as approximation reference. + auto coverReferenceIdx = anchorIdx; + if (coverReferenceIdx <= 0 && mChildren.size() >= 2) { + coverReferenceIdx++; + auto child = mChildren.at(coverReferenceIdx); + layoutChildIfRequired(child, coverReferenceIdx, useDirtyFlag, first); + } + auto toCover = estimateChildrenToCover(first ? pageSize : (childCache + 1) * pageSize, coverReferenceIdx); auto attached = false; for (int i = mEnsuredChildren.upperBound(); i < std::min(anchorIdx + toCover, mChildren.size()); i++) { auto child = mChildren.at(i); @@ -821,7 +840,7 @@ MultiChildScrollableComponent::runLayoutHeuristics(size_t anchorIdx, float child } if (!first) { - toCover = estimateChildrenToCover(childCache * pageSize, anchorIdx); + toCover = estimateChildrenToCover(childCache * pageSize, coverReferenceIdx); for (int i = mEnsuredChildren.lowerBound(); i >= std::max(0, static_cast(anchorIdx - toCover)); i--) { auto child = mChildren.at(i); if (!child->isAttached() || child->getCalculated(kPropertyBounds).empty()) { @@ -852,6 +871,20 @@ MultiChildScrollableComponent::processLayoutChanges(bool useDirtyFlag, bool firs processLayoutChangesInternal(useDirtyFlag, first, false, true); } +void +MultiChildScrollableComponent::scheduleDelayedLayout() { + mDelayLayoutAction = Action::makeDelayed(getRootConfig().getTimeManager(), 1); + auto weak_self = std::weak_ptr( + std::static_pointer_cast(shared_from_corecomponent())); + mDelayLayoutAction->then([weak_self](const ActionPtr &) { + auto self = weak_self.lock(); + if (self) { + self->processLayoutChangesInternal(true, false, true, false); + self->mDelayLayoutAction = nullptr; + } + }); +} + void MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, bool first, bool delayed, bool needsFullReProcess) { @@ -931,7 +964,18 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b float pageSize = horizontal ? sequenceBounds.getWidth() : sequenceBounds.getHeight(); // Try to figure majority of layout as a bulk - runLayoutHeuristics(anchorIdx, childCache, pageSize, useDirtyFlag, first); + // + // TODO: Layout heuristics are good for performance but not essential. In + // an earlier version, the heuristic looked at the size of the first child + // to estimate how many children need to be laid out. In a later version we + // looked at the second child instead, to avoid cases where a narrow first + // child resulted in over-estimation of the number of children that needed + // to be laid out. This change had unintended consequences for certain + // layouts that counted on the original heuristic. We need to re-engineer + // the heuristic and in the mean time, we can disable it. + // + // runLayoutHeuristics(anchorIdx, childCache, pageSize, useDirtyFlag, first); + // // Anchor bounds may have shifted Rect anchorBounds = anchor->getCalculated(kPropertyBounds).getRect(); float anchorPosition = horizontal @@ -940,9 +984,9 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b // Lay out children in positive order until we hit cache limit. auto distanceToCover = first ? pageSize : (childCache + 1) * pageSize; - float positionToCover = layoutDirection == kLayoutDirectionLTR - ? anchorPosition + distanceToCover - : anchorPosition - distanceToCover; + float positionToCover = (layoutDirection == kLayoutDirectionRTL && horizontal) + ? anchorPosition - distanceToCover : anchorPosition + distanceToCover; + bool targetCovered = false; int lastLoaded = anchorIdx; for (; lastLoaded < mChildren.size(); lastLoaded++) { @@ -953,8 +997,9 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b ? (layoutDirection == kLayoutDirectionLTR ? childBounds.getRight() : childBounds.getLeft()) : childBounds.getBottom(); - targetCovered = layoutDirection == kLayoutDirectionLTR ? childCoveredPosition > positionToCover - : childCoveredPosition < positionToCover; + targetCovered = (layoutDirection == kLayoutDirectionRTL && horizontal) + ? childCoveredPosition < positionToCover : childCoveredPosition > positionToCover; + if (targetCovered) { break; } @@ -969,9 +1014,9 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b } // Lay out children in negative order until we hit cache limit. - positionToCover = layoutDirection == kLayoutDirectionLTR - ? childCache * pageSize - : childCache * pageSize * -1.0f; + positionToCover = (layoutDirection == kLayoutDirectionRTL && horizontal) + ? childCache * pageSize * -1.0f + : childCache * pageSize; int firstLoaded = anchorIdx; targetCovered = false; for (; firstLoaded >= 0; firstLoaded--) { @@ -981,7 +1026,8 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b anchorBounds = anchor->getCalculated(kPropertyBounds).getRect(); float distance = (horizontal ? anchorBounds.getLeft() : anchorBounds.getTop()) - (horizontal ? childBounds.getLeft() : childBounds.getTop()); - targetCovered = layoutDirection == kLayoutDirectionLTR ? distance > positionToCover : distance < positionToCover; + targetCovered = (layoutDirection == kLayoutDirectionRTL && horizontal) + ? distance < positionToCover : distance > positionToCover; if (targetCovered) { break; } @@ -1004,18 +1050,9 @@ MultiChildScrollableComponent::processLayoutChangesInternal(bool useDirtyFlag, b // Avoid yoga initiated re-layout that may be caused by attaching components that were already laid-out mContext->layoutManager().remove(getLayoutRoot()); - // Postpone to the next frame, if any if (mEnsuredChildren.upperBound() + 1 >= mChildren.size()) return; - mDelayLayoutAction = Action::makeDelayed(getRootConfig().getTimeManager(), 1); - auto weak_self = std::weak_ptr( - std::static_pointer_cast(shared_from_corecomponent())); - mDelayLayoutAction->then([weak_self](const ActionPtr &) { - auto self = weak_self.lock(); - if (self) { - self->processLayoutChangesInternal(true, false, true, false); - self->mDelayLayoutAction = nullptr; - } - }); + // Postpone to the next frame, if any + scheduleDelayedLayout(); } // Record current range of available motion to avoid re-calculating during scroll trimming. @@ -1043,8 +1080,8 @@ MultiChildScrollableComponent::onScrollPositionUpdated() mChildrenVisibilityStale = true; - // Force figuring out what is on screen. - processLayoutChangesInternal(true, false, false, false); + // Force figuring out what is on screen on next "free" frame. + scheduleDelayedLayout(); } float @@ -1253,7 +1290,7 @@ MultiChildScrollableComponent::getSnapOffset() const } if (!targetChild) { - LOG(LogLevel::kWarn) << "Can't snap on scroll offset " << scrollOffset; + LOG(LogLevel::kWarn).session(mContext) << "Can't snap on scroll offset " << scrollOffset; return {}; } diff --git a/aplcore/src/component/pagercomponent.cpp b/aplcore/src/component/pagercomponent.cpp index 80f0959..c682663 100644 --- a/aplcore/src/component/pagercomponent.cpp +++ b/aplcore/src/component/pagercomponent.cpp @@ -155,7 +155,7 @@ PagerComponent::setPage(int page) /** * Immediately change the current page in the pager. This method can be invoked using "SetValue" on pageIndex or pageId. - * @param page The index of the page to change to + * @param pageIndex The index of the page to change to */ void PagerComponent::setPageImmediate(int pageIndex) @@ -201,7 +201,7 @@ PagerComponent::handleSetPage(int index, PageDirection direction, const ActionRe setPage(targetPageIndex); } mCurrentAnimation->resolve(); - if (!ref.isEmpty() && ref.isPending()) ref.resolve(); + if (!ref.empty() && ref.isPending()) ref.resolve(); return; } @@ -241,7 +241,7 @@ PagerComponent::handleSetPage(int index, PageDirection direction, const ActionRe } }); - if (!ref.isEmpty() && ref.isPending()) { + if (!ref.empty() && ref.isPending()) { ref.addTerminateCallback([weak_ptr](const TimersPtr&) { auto self = weak_ptr.lock(); if (self) { @@ -336,7 +336,7 @@ PagerComponent::endPageMove(bool fulfilled, const ActionRef& ref, bool fast) } auto event = executePageChangeEvent(fast); - if (!ref.isEmpty()) { + if (!ref.empty()) { if (event && event->isPending()) { event->then([ref](const ActionPtr& ptr) { ref.resolve(); }); } else { @@ -344,7 +344,7 @@ PagerComponent::endPageMove(bool fulfilled, const ActionRef& ref, bool fast) } } } else { - if (!ref.isEmpty()) { + if (!ref.empty()) { ref.resolve(); } } @@ -674,7 +674,7 @@ PagerComponent::finalizePopulate() void PagerComponent::attachPageAndReportLoaded(int page) { - LOG_IF(DEBUG_PAGER) << this->toDebugSimpleString(); + LOG_IF(DEBUG_PAGER).session(getContext()) << this->toDebugSimpleString(); if (mChildren.empty() && mRebuilder) { // Force loading if possible mRebuilder->notifyStartEdgeReached(); @@ -715,7 +715,7 @@ PagerComponent::attachPageAndReportLoaded(int page) { break; } - LOG_IF(DEBUG_PAGER) << " start=" << start << " count=" << count; + LOG_IF(DEBUG_PAGER).session(getContext()) << " start=" << start << " count=" << count; auto& lm = mContext->layoutManager(); for (int i = 0 ; i < count ; i++) { auto index = (start + i) % childCount; diff --git a/aplcore/src/component/videocomponent.cpp b/aplcore/src/component/videocomponent.cpp index 93614e4..ff06b54 100644 --- a/aplcore/src/component/videocomponent.cpp +++ b/aplcore/src/component/videocomponent.cpp @@ -27,7 +27,7 @@ namespace apl { * Convert a media state object into event properties that will be passed to the event handler. * Note that this method copies more properties than are strictly needed according to the APL * documentation. - * @param mediaState + * @param mediaState The media state to convert * @return Shared object map */ ObjectMapPtr @@ -41,6 +41,7 @@ mediaStateToEventProperties(const MediaState& mediaState) eventProps->emplace("duration", mediaState.getDuration()); eventProps->emplace("paused", mediaState.isPaused()); eventProps->emplace("ended", mediaState.isEnded()); + eventProps->emplace("muted", mediaState.isMuted()); eventProps->emplace("errorCode", mediaState.getErrorCode()); return eventProps; } @@ -167,13 +168,14 @@ VideoComponent::remove() const ComponentPropDefSet& VideoComponent::propDefSet() const { - static const std::array PLAYING_STATE = { + static const std::array PLAYING_STATE = { kPropertyTrackCount, kPropertyTrackCurrentTime, kPropertyTrackDuration, kPropertyTrackIndex, kPropertyTrackPaused, - kPropertyTrackEnded + kPropertyTrackEnded, + kPropertyMuted }; // Save the current playing state of the component @@ -189,7 +191,7 @@ VideoComponent::propDefSet() const // and decide how to handle them. static auto setPlayingState = [](CoreComponent& component, const Object& value ) -> void { if (!value.isArray() || value.size() != PLAYING_STATE.size()) { - CONSOLE_CTP(component.getContext()) << "setPlayingState: Invalid " << value.toDebugString(); + CONSOLE(component.getContext()) << "setPlayingState: Invalid " << value.toDebugString(); return; } @@ -211,6 +213,7 @@ VideoComponent::propDefSet() const CoreComponent::propDefSet(), MediaComponentTrait::propDefList()).add({ { kPropertyAudioTrack, kAudioTrackForeground, sAudioTrackMap, kPropInOut }, { kPropertyAutoplay, false, asOldBoolean, kPropInOut }, + { kPropertyMuted, false, asOldBoolean, kPropDynamic | kPropInOut }, { kPropertyScale, kVideoScaleBestFit, sVideoScaleMap, kPropInOut }, { kPropertySource, Object::EMPTY_ARRAY(), asMediaSourceArray, kPropDynamic | kPropInOut | kPropVisualContext | kPropVisualHash | kPropEvaluated, resetMediaState }, { kPropertyOnEnd, Object::EMPTY_ARRAY(), asCommand, kPropIn }, @@ -271,6 +274,7 @@ VideoComponent::saveMediaState(const MediaState& state) mCalculated.set(kPropertyTrackIndex, state.getTrackIndex()); mCalculated.set(kPropertyTrackPaused, state.isPaused()); mCalculated.set(kPropertyTrackEnded, state.isEnded()); + mCalculated.set(kPropertyMuted, state.isMuted()); mCalculated.set(kPropertyTrackState, state.getTrackState()); } @@ -380,7 +384,7 @@ VideoComponent::createDefaultEventProperties() eventProps->emplace("duration", getCalculated(kPropertyTrackDuration).asInt()); eventProps->emplace("paused", getCalculated(kPropertyTrackPaused).asBoolean()); eventProps->emplace("ended", getCalculated(kPropertyTrackEnded).asBoolean()); - + eventProps->emplace("muted", getCalculated(kPropertyMuted).asBoolean()); return eventProps; } @@ -438,6 +442,7 @@ VideoComponent::eventPropertyMap() const {"duration", [](const CoreComponent* c) { return c->getCalculated(kPropertyTrackDuration); }}, {"paused", [](const CoreComponent* c) { return c->getCalculated(kPropertyTrackPaused); }}, {"ended", [](const CoreComponent* c) { return c->getCalculated(kPropertyTrackEnded); }}, + {"muted", [](const CoreComponent* c) { return c->getCalculated(kPropertyMuted); }}, {"source", &inlineGetCurrentURL}, {"url", &inlineGetCurrentURL}, {"trackState", [](const CoreComponent* c) { return sTrackStateMap.at(c->getCalculated(kPropertyTrackState).asInt()); }}, @@ -494,4 +499,4 @@ VideoComponent::getVisualContextType() const return getCalculated(kPropertySource).empty() ? VISUAL_CONTEXT_TYPE_EMPTY : VISUAL_CONTEXT_TYPE_VIDEO; } -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/component/yogaproperties.cpp b/aplcore/src/component/yogaproperties.cpp index 804c66e..a6e24b0 100644 --- a/aplcore/src/component/yogaproperties.cpp +++ b/aplcore/src/component/yogaproperties.cpp @@ -25,20 +25,20 @@ namespace yn { const static bool DEBUG_FLEXBOX = false; void -setPropertyGrow(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; +setPropertyGrow(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; YGNodeStyleSetFlexGrow(nodeRef, value.asNumber()); } void -setPropertyShrink(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; +setPropertyShrink(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; YGNodeStyleSetFlexShrink(nodeRef, value.asNumber()); } void -setPositionType(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; +setPositionType(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; auto positionType = static_cast(value.asInt()); if (positionType == kPositionRelative || positionType == kPositionSticky) YGNodeStyleSetPositionType(nodeRef, YGPositionTypeRelative); @@ -48,7 +48,7 @@ setPositionType(YGNodeRef nodeRef, const Object& value, const Context&) { void setWidth(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; if (value.isNull()) return; @@ -63,7 +63,7 @@ setWidth(YGNodeRef nodeRef, const Object& value, const Context& context) { void setMinWidth(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; if (value.isNull()) return; @@ -76,7 +76,7 @@ setMinWidth(YGNodeRef nodeRef, const Object& value, const Context& context) { void setMaxWidth(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; if (value.isNull()) return; @@ -89,7 +89,7 @@ setMaxWidth(YGNodeRef nodeRef, const Object& value, const Context& context) { void setHeight(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; if (value.isNull()) return; @@ -104,7 +104,7 @@ setHeight(YGNodeRef nodeRef, const Object& value, const Context& context) { void setMinHeight(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; if (value.isNull()) return; @@ -117,7 +117,7 @@ setMinHeight(YGNodeRef nodeRef, const Object& value, const Context& context) { void setMaxHeight(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; if (value.isNull()) return; @@ -141,7 +141,7 @@ static const std::array sEdgeToString = { }; void setPadding(YGNodeRef nodeRef, YGEdge edge, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << sEdgeToString[edge] << "->" << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << sEdgeToString[edge] << "->" << value << " [" << nodeRef << "]"; Dimension padding = value.asDimension(context); if (padding.isRelative()) YGNodeStyleSetPaddingPercent(nodeRef, edge, padding.getValue()); @@ -150,14 +150,14 @@ void setPadding(YGNodeRef nodeRef, YGEdge edge, const Object& value, const Conte } void setBorder(YGNodeRef nodeRef, YGEdge edge, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << sEdgeToString[edge] << "->" << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << sEdgeToString[edge] << "->" << value << " [" << nodeRef << "]"; Dimension border = value.asDimension(context); if (border.isAbsolute()) YGNodeStyleSetBorder(nodeRef, edge, border.getValue()); } void setPosition(YGNodeRef nodeRef, YGEdge edge, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << sEdgeToString[edge] << "->" << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << sEdgeToString[edge] << "->" << value << " [" << nodeRef << "]"; CoreComponent *component = static_cast(nodeRef->getContext()); if (component && component->getCalculated(kPropertyPosition) == kPositionSticky) { @@ -175,8 +175,8 @@ void setPosition(YGNodeRef nodeRef, YGEdge edge, const Object& value, const Cont } void -setFlexDirection(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; +setFlexDirection(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; auto flexDirection = static_cast(value.asInt()); switch (flexDirection) { case kContainerDirectionColumn: @@ -197,7 +197,7 @@ setFlexDirection(YGNodeRef nodeRef, const Object& value, const Context&) { // Note: In the future if we allow Container to change layout direction, we'll need to reset all the margins carefully. void setSpacing(YGNodeRef nodeRef, const Object& value, const Context& context) { - LOG_IF(DEBUG_FLEXBOX) << value << " [" << nodeRef << "]"; + LOG_IF(DEBUG_FLEXBOX).session(context) << value << " [" << nodeRef << "]"; CoreComponent *component = static_cast(nodeRef->getContext()); auto spacing = value.asDimension(context); @@ -233,8 +233,8 @@ static const YGJustify JUSTIFY_LOOKUP[] = { }; void -setJustifyContent(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sFlexboxJustifyContentMap.at(value.asInt()) << " [" << nodeRef << "]"; +setJustifyContent(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sFlexboxJustifyContentMap.at(value.asInt()) << " [" << nodeRef << "]"; auto justify = static_cast(value.asInt()); if (justify >= kFlexboxJustifyContentStart && justify <= kFlexboxJustifyContentSpaceAround) YGNodeStyleSetJustifyContent(nodeRef, JUSTIFY_LOOKUP[justify]); @@ -248,8 +248,8 @@ static const YGWrap WRAP_LOOKUP[] = { }; void -setWrap(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sFlexboxWrapMap.at(value.asInt()) << " [" << nodeRef << "]"; +setWrap(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sFlexboxWrapMap.at(value.asInt()) << " [" << nodeRef << "]"; auto wrap = static_cast(value.asInt()); if (wrap >= kFlexboxWrapNoWrap && wrap <= kFlexboxWrapWrapReverse) YGNodeStyleSetFlexWrap(nodeRef, WRAP_LOOKUP[wrap]); @@ -266,16 +266,16 @@ static const YGAlign ALIGN_LOOKUP[] = { }; void -setAlignSelf(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sFlexboxAlignMap.at(value.asInt()) << " [" << nodeRef << "]"; +setAlignSelf(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sFlexboxAlignMap.at(value.asInt()) << " [" << nodeRef << "]"; auto alignSelf = static_cast(value.asInt()); if (alignSelf >= kFlexboxAlignStretch && alignSelf <= kFlexboxAlignAuto) YGNodeStyleSetAlignSelf(nodeRef, ALIGN_LOOKUP[alignSelf]); } void -setAlignItems(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sFlexboxAlignMap.at(value.asInt()) << " [" << nodeRef << "]"; +setAlignItems(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sFlexboxAlignMap.at(value.asInt()) << " [" << nodeRef << "]"; auto alignItems = static_cast(value.asInt()); if (alignItems >= kFlexboxAlignStretch && alignItems <= kFlexboxAlignAuto) YGNodeStyleSetAlignItems(nodeRef, ALIGN_LOOKUP[alignItems]); @@ -295,8 +295,8 @@ scrollDirectionLookup(ScrollDirection direction) { } void -setScrollDirection(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; +setScrollDirection(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; auto scrollDirection = static_cast(value.asInt()); YGNodeStyleSetFlexDirection(nodeRef, scrollDirectionLookup(scrollDirection)); } @@ -315,22 +315,22 @@ gridScrollDirectionLookup(ScrollDirection direction) { } void -setGridScrollDirection(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; +setGridScrollDirection(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sScrollDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; auto scrollDirection = static_cast(value.asInt()); YGNodeStyleSetFlexDirection(nodeRef, gridScrollDirectionLookup(scrollDirection)); } void -setDisplay(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sDisplayMap.at(value.asInt()) << " [" << nodeRef << "]"; +setDisplay(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sDisplayMap.at(value.asInt()) << " [" << nodeRef << "]"; auto display = static_cast(value.asInt()); YGNodeStyleSetDisplay(nodeRef, display == kDisplayNone ? YGDisplayNone : YGDisplayFlex); } void -setLayoutDirection(YGNodeRef nodeRef, const Object& value, const Context&) { - LOG_IF(DEBUG_FLEXBOX) << sLayoutDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; +setLayoutDirection(YGNodeRef nodeRef, const Object& value, const Context& context) { + LOG_IF(DEBUG_FLEXBOX).session(context) << sLayoutDirectionMap.at(value.asInt()) << " [" << nodeRef << "]"; auto layoutDirection = static_cast(value.asInt()); switch (layoutDirection) { case kLayoutDirectionLTR: diff --git a/aplcore/src/content/aplversion.cpp b/aplcore/src/content/aplversion.cpp index 84a30bf..de2a6a8 100644 --- a/aplcore/src/content/aplversion.cpp +++ b/aplcore/src/content/aplversion.cpp @@ -21,16 +21,17 @@ namespace apl { static Bimap sVersionMap = { - { APLVersion::kAPLVersion10, "1.0" }, - { APLVersion::kAPLVersion11, "1.1" }, - { APLVersion::kAPLVersion12, "1.2" }, - { APLVersion::kAPLVersion13, "1.3" }, - { APLVersion::kAPLVersion14, "1.4" }, - { APLVersion::kAPLVersion15, "1.5" }, - { APLVersion::kAPLVersion16, "1.6" }, - { APLVersion::kAPLVersion17, "1.7" }, - { APLVersion::kAPLVersion18, "1.8" }, - { APLVersion::kAPLVersion19, "1.9" } + { APLVersion::kAPLVersion10, "1.0" }, + { APLVersion::kAPLVersion11, "1.1" }, + { APLVersion::kAPLVersion12, "1.2" }, + { APLVersion::kAPLVersion13, "1.3" }, + { APLVersion::kAPLVersion14, "1.4" }, + { APLVersion::kAPLVersion15, "1.5" }, + { APLVersion::kAPLVersion16, "1.6" }, + { APLVersion::kAPLVersion17, "1.7" }, + { APLVersion::kAPLVersion18, "1.8" }, + { APLVersion::kAPLVersion19, "1.9" }, + { APLVersion::kAPLVersion20221, "2022.1" }, }; bool diff --git a/aplcore/src/content/configurationchange.cpp b/aplcore/src/content/configurationchange.cpp index 6fc21fa..4e26d17 100644 --- a/aplcore/src/content/configurationchange.cpp +++ b/aplcore/src/content/configurationchange.cpp @@ -43,13 +43,13 @@ ConfigurationChange::mergeRootConfig(const RootConfig& oldRootConfig) const auto rootConfig = oldRootConfig; if ((mFlags & kConfigurationChangeScreenMode) != 0) - rootConfig.screenMode(mScreenMode); + rootConfig.set(RootProperty::kScreenMode, mScreenMode); if ((mFlags & kConfigurationChangeFontScale) != 0) - rootConfig.fontScale(mFontScale); + rootConfig.set(RootProperty::kFontScale, mFontScale); if ((mFlags & kConfigurationChangeScreenReader) != 0) - rootConfig.screenReader(mScreenReaderEnabled); + rootConfig.set(RootProperty::kScreenReader, mScreenReaderEnabled); if ((mFlags & kConfigurationChangeDisallowVideo) != 0) rootConfig.set(RootProperty::kDisallowVideo, mDisallowVideo); diff --git a/aplcore/src/content/content.cpp b/aplcore/src/content/content.cpp index b6c6a07..2d2045f 100644 --- a/aplcore/src/content/content.cpp +++ b/aplcore/src/content/content.cpp @@ -15,6 +15,7 @@ #include +#include "apl/buildTimeConstants.h" #include "apl/engine/arrayify.h" #include "apl/engine/parameterarray.h" #include "apl/content/package.h" @@ -46,7 +47,7 @@ Content::create(JsonData&& document) { ContentPtr Content::create(JsonData&& document, const SessionPtr& session) { if (!document) { - CONSOLE_S(session).log("Document parse error offset=%u: %s.", document.offset(), document.error()); + CONSOLE(session).log("Document parse error offset=%u: %s.", document.offset(), document.error()); return nullptr; } @@ -57,7 +58,7 @@ Content::create(JsonData&& document, const SessionPtr& session) { const rapidjson::Value& json = ptr->json(); auto it = json.FindMember(DOCUMENT_MAIN_TEMPLATE); if (it == json.MemberEnd()) { - CONSOLE_S(session) << "Document does not contain a mainTemplate property"; + CONSOLE(session) << "Document does not contain a mainTemplate property"; return nullptr; } @@ -72,6 +73,11 @@ Content::Content(SessionPtr session, mState(LOADING), mMainTemplate(mainTemplate) { + // First chance where we can extract settings. Set up the session. + auto diagnosticLabel = getDocumentSettings()->getValue("-diagnosticLabel").asString(); + mSession->setLogIdPrefix(diagnosticLabel); + LOG(LogLevel::kInfo).session(mSession) << "Initializing experience using " << std::string(sCoreRepositoryVersion); + addImportList(*mMainPackage); addExtensions(*mMainPackage); @@ -124,7 +130,7 @@ Content::addPackage(const ImportRequest& request, JsonData&& raw) { // If the package data is invalid, set the error state if (!raw) { - CONSOLE_S(mSession).log("Package %s (%s) parse error offset=%u: %s", + CONSOLE(mSession).log("Package %s (%s) parse error offset=%u: %s", request.reference().name().c_str(), request.reference().version().c_str(), raw.offset(), raw.error()); @@ -134,7 +140,7 @@ Content::addPackage(const ImportRequest& request, JsonData&& raw) { // We expect packages to be objects, erase from the requested set if (!raw.get().IsObject()) { - CONSOLE_S(mSession).log("Package %s (%s) is not a JSON object", + CONSOLE(mSession).log("Package %s (%s) is not a JSON object", request.reference().name().c_str(), request.reference().version().c_str()); mState = ERROR; @@ -158,9 +164,8 @@ Content::addPackage(const ImportRequest& request, JsonData&& raw) { // Insert into the mLoaded list. Note that json has been moved auto ptr = Package::create(mSession, request.reference().toString(), std::move(raw)); if (!ptr) { - LOGF(LogLevel::kError, "Package %s (%s) could not be moved to the loaded list.", - request.reference().name().c_str(), - request.reference().version().c_str()); + LOG(LogLevel::kError).session(mSession) << "Package " << request.reference().name() + << " (" << request.reference().version() << ") could not be moved to the loaded list."; mState = ERROR; return; } @@ -177,14 +182,14 @@ void Content::addData(const std::string& name, JsonData&& raw) { return; if (!mPendingParameters.erase(name)) { - CONSOLE_S(mSession).log("Data parameter '%s' does not exist or is already assigned", + CONSOLE(mSession).log("Data parameter '%s' does not exist or is already assigned", name.c_str()); return; } // If the data is invalid, set the error state if (!raw) { - CONSOLE_S(mSession).log("Data '%s' parse error offset=%u: %s", + CONSOLE(mSession).log("Data '%s' parse error offset=%u: %s", name.c_str(), raw.offset(), raw.error()); mState = ERROR; return; @@ -204,9 +209,9 @@ Content::getMainProperties(Properties& out) const { out.emplace(m.name, mParameterValues.at(m.name).get()); if (DEBUG_CONTENT) { - LOG(LogLevel::kDebug) << "Main Properties:"; + LOG(LogLevel::kDebug).session(mSession) << "Main Properties:"; for (const auto& m : out) - LOG(LogLevel::kDebug) << " " << m.first << ": " << m.second.toDebugString(); + LOG(LogLevel::kDebug).session(mSession) << " " << m.first << ": " << m.second.toDebugString(); } return true; @@ -214,14 +219,14 @@ Content::getMainProperties(Properties& out) const { void Content::addImportList(Package& package) { - LOG_IF(DEBUG_CONTENT) << "addImportList " << &package; + LOG_IF(DEBUG_CONTENT).session(mSession) << "addImportList " << &package; const rapidjson::Value& value = package.json(); auto it = value.FindMember(DOCUMENT_IMPORT); if (it != value.MemberEnd()) { if (!it->value.IsArray()) { - CONSOLE_S(mSession).log("%s: Document import property should be an array", package.name().c_str()); + CONSOLE(mSession).log("%s: Document import property should be an array", package.name().c_str()); mState = ERROR; return; } @@ -232,17 +237,17 @@ Content::addImportList(Package& package) { void Content::addImport(Package& package, const rapidjson::Value& value) { - LOG_IF(DEBUG_CONTENT) << "addImport " << &package; + LOG_IF(DEBUG_CONTENT).session(mSession) << "addImport " << &package; if (!value.IsObject()) { - CONSOLE_S(mSession).log("Invalid import record in document"); + CONSOLE(mSession).log("Invalid import record in document"); mState = ERROR; return; } ImportRequest request(value); if (!request.isValid()) { - CONSOLE_S(mSession).log("Malformed import record"); + CONSOLE(mSession).log("Malformed import record"); mState = ERROR; return; } @@ -277,7 +282,7 @@ Content::addExtensions(Package& package) { // The properties are required if (uri.empty() || name.empty()) { - CONSOLE_S(mSession).log("Illegal extension request in package '%s'", package.name().c_str()); + CONSOLE(mSession).log("Illegal extension request in package '%s'", package.name().c_str()); continue; } @@ -287,7 +292,7 @@ Content::addExtensions(Package& package) { if (eit->second == uri) // The same NAME->URI mapping is ignored continue; - CONSOLE_S(mSession).log("The extension name='%s' is referencing different URI values", name.c_str()); + CONSOLE(mSession).log("The extension name='%s' is referencing different URI values", name.c_str()); mState = ERROR; return; } else { @@ -349,7 +354,7 @@ Content::loadExtensionSettings() { continue; // no Settings in this package settings = std::make_shared(Settings(settingsValue)); sMap.emplace(pkg->name(), settings); - LOG_IF(DEBUG_CONTENT) << "created settings for pkg: " << pkg->name(); + LOG_IF(DEBUG_CONTENT).session(mSession) << "created settings for pkg: " << pkg->name(); } else { settings = sItr->second; } @@ -362,7 +367,7 @@ Content::loadExtensionSettings() { // override / augment existing settings for (auto v: val.getMap()) (*esMap)[v.first] = v.second; - LOG_IF(DEBUG_CONTENT) << "extension:" << name << " pkg:" << pkg << " inserting: " << val; + LOG_IF(DEBUG_CONTENT).session(mSession) << "extension:" << name << " pkg:" << pkg << " inserting: " << val; } } @@ -373,7 +378,7 @@ Content::loadExtensionSettings() { for (auto tm : tmpMap) { auto obj = (!tm.second->empty()) ? Object(tm.second) : Object::NULL_OBJECT(); mExtensionSettings->emplace(tm.first, obj); - LOG_IF(DEBUG_CONTENT) << "extension result: " << obj.toDebugString(); + LOG_IF(DEBUG_CONTENT).session(mSession) << "extension result: " << obj.toDebugString(); } } @@ -428,7 +433,7 @@ Content::getEnvironment(const RootConfig& config) const auto s = ldIter->value.GetString(); auto ld = static_cast(sLayoutDirectionMap.get(s, -1)); if (ld == static_cast(-1)) { - CONSOLE_S(mSession) + CONSOLE(mSession) << "Document 'layoutDirection' property is invalid. Falling back to system defaults"; } else if (ld != kLayoutDirectionInherit) { @@ -477,7 +482,7 @@ Content::getExtensionRequests() const { Object Content::getExtensionSettings(const std::string& uri) { if (!isReady()) { - CONSOLE_S(mSession).log("Settings for extension name='%s' cannot be returned. The document is not Ready.", + CONSOLE(mSession).log("Settings for extension name='%s' cannot be returned. The document is not Ready.", uri.c_str()); return Object::NULL_OBJECT(); } @@ -488,7 +493,7 @@ Content::getExtensionSettings(const std::string& uri) { const std::map::const_iterator& es = mExtensionSettings->find(uri); if (es != mExtensionSettings->end()) { - LOG_IF(DEBUG_CONTENT) << "getExtensionSettings " << uri << ":" << es->second.toDebugString() + LOG_IF(DEBUG_CONTENT).session(mSession) << "getExtensionSettings " << uri << ":" << es->second.toDebugString() << " mapaddr:" << &es->second; return es->second; } @@ -504,7 +509,7 @@ Content::orderDependencyList() { std::set inProgress; bool isOrdered = addToDependencyList(mOrderedDependencies, inProgress, mMainPackage); if (!isOrdered) - CONSOLE_S(mSession).log("Failure to order packages"); + CONSOLE(mSession).log("Failure to order packages"); return isOrdered; } @@ -516,19 +521,19 @@ bool Content::addToDependencyList(std::vector& ordered, std::set& inProgress, const PackagePtr& package) { - LOG_IF(DEBUG_CONTENT) << "addToDependencyList " << package << " dependency count=" + LOG_IF(DEBUG_CONTENT).session(mSession) << "addToDependencyList " << package << " dependency count=" << package->getDependencies().size(); inProgress.insert(package); // For dependency loop detection // Start with the package dependencies for (const auto& ref : package->getDependencies()) { - LOG_IF(DEBUG_CONTENT) << "checking child " << ref.toString(); + LOG_IF(DEBUG_CONTENT).session(mSession) << "checking child " << ref.toString(); // Convert the reference into a loaded PackagePtr const auto& pkg = mLoaded.find(ref); if (pkg == mLoaded.end()) { - LOGF(LogLevel::kError, "Missing package '%s' in the loaded set", ref.name().c_str()); + LOG(LogLevel::kError).session(mSession) << "Missing package '" << ref.name() << "' in the loaded set"; return false; } @@ -537,23 +542,23 @@ Content::addToDependencyList(std::vector& ordered, // Check if it is already in the dependency list (someone else included it first) auto it = std::find(ordered.begin(), ordered.end(), child); if (it != ordered.end()) { - LOG_IF(DEBUG_CONTENT) << "child package " << ref.toString() << " already in dependency list"; + LOG_IF(DEBUG_CONTENT).session(mSession) << "child package " << ref.toString() << " already in dependency list"; continue; } // Check for a circular dependency if (inProgress.count(child)) { - CONSOLE_S(mSession).log("Circular package dependency '%s'", ref.name().c_str()); + CONSOLE(mSession).log("Circular package dependency '%s'", ref.name().c_str()); return false; } if (!addToDependencyList(ordered, inProgress, child)) { - LOG_IF(DEBUG_CONTENT) << "returning false with child package " << child->name(); + LOG_IF(DEBUG_CONTENT).session(mSession) << "returning false with child package " << child->name(); return false; } } - LOG_IF(DEBUG_CONTENT) << "Pushing package " << package << " onto ordered list"; + LOG_IF(DEBUG_CONTENT).session(mSession) << "Pushing package " << package << " onto ordered list"; ordered.push_back(package); inProgress.erase(package); return true; diff --git a/aplcore/src/content/directive.cpp b/aplcore/src/content/directive.cpp index cf01902..bb0967d 100644 --- a/aplcore/src/content/directive.cpp +++ b/aplcore/src/content/directive.cpp @@ -31,7 +31,7 @@ std::shared_ptr Directive::create(JsonData&& directive, const SessionPtr& session) { if (!directive) { - CONSOLE_S(session).log("Directive parse error offset=%u: %s", directive.offset(), directive.error()); + CONSOLE(session).log("Directive parse error offset=%u: %s", directive.offset(), directive.error()); return nullptr; } @@ -56,14 +56,14 @@ Directive::Directive(const SessionPtr& session, JsonData &&directive) const auto& doc = payload->FindMember("document"); if (doc == payload->MemberEnd()) { - CONSOLE_S(session).log("Directive payload does not contain a document"); + CONSOLE(session).log("Directive payload does not contain a document"); return; } // Find the main template and count the parameters auto mainTemplate = doc->value.FindMember("mainTemplate"); if (mainTemplate == doc->value.MemberEnd()) { - CONSOLE_S(session).log("Directive document does not contain a mainTemplate"); + CONSOLE(session).log("Directive document does not contain a mainTemplate"); return; } @@ -71,13 +71,13 @@ Directive::Directive(const SessionPtr& session, JsonData &&directive) auto parameters = mainTemplate->value.FindMember("parameters"); if (parameters != mainTemplate->value.MemberEnd()) { if (!parameters->value.IsArray()) { - CONSOLE_S(session).log("Main template parameters is not an array"); + CONSOLE(session).log("Main template parameters is not an array"); return; } paramCount = parameters->value.Size(); if (paramCount > 1) { - CONSOLE_S(session).log("Main template can have at most one parameter"); + CONSOLE(session).log("Main template can have at most one parameter"); return; } } @@ -87,7 +87,7 @@ Directive::Directive(const SessionPtr& session, JsonData &&directive) auto hasDataSource = datasources != payload->MemberEnd(); if (paramCount == 1 && !hasDataSource) { - CONSOLE_S(session).log("Document missing datasources"); + CONSOLE(session).log("Document missing datasources"); return; } diff --git a/aplcore/src/content/package.cpp b/aplcore/src/content/package.cpp index 2c5b149..dc569e4 100644 --- a/aplcore/src/content/package.cpp +++ b/aplcore/src/content/package.cpp @@ -25,26 +25,26 @@ PackagePtr Package::create(const SessionPtr& session, const std::string& name, JsonData&& json) { if (!json) { - CONSOLE_S(session).log("Package %s parse error offset=%u: %s", name.c_str(), + CONSOLE(session).log("Package %s parse error offset=%u: %s", name.c_str(), json.offset(), json.error()); return nullptr; } const auto& value = json.get(); if (!value.IsObject()) { - CONSOLE_S(session).log("Package %s: not a valid JSON object", name.c_str()); + CONSOLE(session).log("Package %s: not a valid JSON object", name.c_str()); return nullptr; } auto it_type = value.FindMember(DOCUMENT_TYPE); if (it_type == value.MemberEnd()) { - CONSOLE_S(session).log("Package %s does not contain a type field", name.c_str()); + CONSOLE(session).log("Package %s does not contain a type field", name.c_str()); return nullptr; } auto it_version = value.FindMember(DOCUMENT_VERSION); if (it_version == value.MemberEnd() || !it_version->value.IsString()) { - CONSOLE_S(session).log("Package %s does not contain a valid version field", name.c_str()); + CONSOLE(session).log("Package %s does not contain a valid version field", name.c_str()); return nullptr; } diff --git a/aplcore/src/content/rootconfig.cpp b/aplcore/src/content/rootconfig.cpp index 3a4a3ed..140a877 100644 --- a/aplcore/src/content/rootconfig.cpp +++ b/aplcore/src/content/rootconfig.cpp @@ -210,7 +210,7 @@ RootConfig::set(const std::string& name, const Object& object) auto propertyKey = static_cast(it->second); return set(propertyKey, object); } else { - LOG(LogLevel::kInfo) << "Unable to find property " << name; + LOG(LogLevel::kInfo).session(mSession) << "Unable to find property " << name; } return *this; @@ -269,7 +269,7 @@ RootConfig::setEnvironmentValue(const std::string& name, const Object& value) { if (isAllowedEnvironmentName(name)) { mEnvironmentValues[name] = value; } else { - LOG(LogLevel::kWarn) << "Ignoring attempt to set environment value: " << name; + LOG(LogLevel::kWarn).session(mSession) << "Ignoring attempt to set environment value: " << name; } return *this; } diff --git a/aplcore/src/datagrammar/bytecode.cpp b/aplcore/src/datagrammar/bytecode.cpp index 91490f3..54687fc 100644 --- a/aplcore/src/datagrammar/bytecode.cpp +++ b/aplcore/src/datagrammar/bytecode.cpp @@ -43,7 +43,7 @@ ByteCode::eval() const return cmd.value; default: - CONSOLE_CTP(mContext) << "Unexpected trivial instruction " << cmd.type; + CONSOLE(mContext) << "Unexpected trivial instruction " << cmd.type; return Object::NULL_OBJECT(); } } @@ -54,7 +54,7 @@ ByteCode::eval() const if (evaluator.isDone()) return evaluator.getResult(); - CONSOLE_CTP(mContext) << "Unable to evaluate byte code data"; + CONSOLE(mContext) << "Unable to evaluate byte code data"; return Object::NULL_OBJECT(); } @@ -145,13 +145,13 @@ ByteCode::getContext() const void ByteCode::dump() const { - LOG(LogLevel::kDebug) << "Data"; + LOG(LogLevel::kDebug).session(getContext()) << "Data"; for (int i = 0; i < mData.size(); i++) - LOG(LogLevel::kDebug) << " [" << i << "] " << mData.at(i).toDebugString(); + LOG(LogLevel::kDebug).session(getContext()) << " [" << i << "] " << mData.at(i).toDebugString(); - LOG(LogLevel::kDebug) << "Instructions"; + LOG(LogLevel::kDebug).session(getContext()) << "Instructions"; for (int pc = 0; pc < mInstructions.size(); pc++) - LOG(LogLevel::kDebug) << instructionAsString(pc); + LOG(LogLevel::kDebug).session(getContext()) << instructionAsString(pc); } diff --git a/aplcore/src/datagrammar/bytecodeassembler.cpp b/aplcore/src/datagrammar/bytecodeassembler.cpp index d8cea62..a9ed458 100644 --- a/aplcore/src/datagrammar/bytecodeassembler.cpp +++ b/aplcore/src/datagrammar/bytecodeassembler.cpp @@ -79,9 +79,9 @@ ByteCodeAssembler::parse(const Context& context, const std::string& value) } catch (const pegtl::parse_error& e) { const auto p = e.positions.front(); - CONSOLE_CTX(context) << "Syntax error: " << e.what(); - CONSOLE_CTX(context) << in.line_at(p); - CONSOLE_CTX(context) << std::string(p.byte_in_line, ' ') << "^"; + CONSOLE(context) << "Syntax error: " << e.what(); + CONSOLE(context) << in.line_at(p); + CONSOLE(context) << std::string(p.byte_in_line, ' ') << "^"; } return value; @@ -407,10 +407,6 @@ ByteCodeAssembler::pushArrayAccessEnd() mOperatorsRef->pop_back(); } -/** - * The top of the operator stack should match "order" and can be reduced. - * @param order - */ void ByteCodeAssembler::reduceOneJump(ByteCodeOrder order) { @@ -493,4 +489,4 @@ ByteCodeAssembler::toString() const } } // namespace datagrammar -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/datagrammar/bytecodeevaluator.cpp b/aplcore/src/datagrammar/bytecodeevaluator.cpp index df946d6..c363dfe 100644 --- a/aplcore/src/datagrammar/bytecodeevaluator.cpp +++ b/aplcore/src/datagrammar/bytecodeevaluator.cpp @@ -58,7 +58,7 @@ ByteCodeEvaluator::advance() // For now, we'll consider a program done when it runs off the end of the code for (; mProgramCounter < number_of_commands; mProgramCounter++) { const auto &cmd = instructions.at(mProgramCounter); - LOG_IF(DEBUG_BYTE_CODE) << mByteCode.instructionAsString(mProgramCounter) + LOG_IF(DEBUG_BYTE_CODE).session(mByteCode.getContext()) << mByteCode.instructionAsString(mProgramCounter) << " stack={" << stackToString(mStack) << "}"; switch (cmd.type) { case BC_OPCODE_NOP: @@ -75,7 +75,7 @@ ByteCodeEvaluator::advance() mIsConstant = false; mStack.emplace_back(f.call(args)); } else { - CONSOLE_CTP(mByteCode.getContext()) << "Invalid function pc=" << mProgramCounter; + CONSOLE(mByteCode.getContext()) << "Invalid function pc=" << mProgramCounter; mStack.emplace_back(Object::NULL_OBJECT()); } } @@ -242,7 +242,7 @@ ByteCodeEvaluator::getResult() const return Object::NULL_OBJECT(); if (len > 1) - LOG(LogLevel::kError) << "Expected no items on stack; found " << len << " instead"; + LOG(LogLevel::kError).session(mByteCode.getContext()) << "Expected no items on stack; found " << len << " instead"; return mStack.back(); } diff --git a/aplcore/src/datagrammar/bytecodeoptimizer.cpp b/aplcore/src/datagrammar/bytecodeoptimizer.cpp index 8538ba0..f6dfe0b 100644 --- a/aplcore/src/datagrammar/bytecodeoptimizer.cpp +++ b/aplcore/src/datagrammar/bytecodeoptimizer.cpp @@ -145,13 +145,13 @@ ByteCodeOptimizer::simplifyOperations() case BC_OPCODE_LOAD_DATA: return operands.at(cmd.value); default: - LOG(LogLevel::kError) << "Illegal offset in stack at " << offset; + LOG(LogLevel::kError).session(mByteCode.getContext()) << "Illegal offset in stack at " << offset; assert(false); return Object::NULL_OBJECT(); } } - LOG(LogLevel::kError) << "Too many DUPLICATE commands on the stack for constant value retrieval"; + LOG(LogLevel::kError).session(mByteCode.getContext()) << "Too many DUPLICATE commands on the stack for constant value retrieval"; assert(false); return Object::NULL_OBJECT(); }; @@ -399,9 +399,9 @@ ByteCodeOptimizer::simplifyOperations() } if (DEBUG_OPTIMIZER) { - LOG(LogLevel::kDebug) << "Basic blocks located at: "; + LOG(LogLevel::kDebug).session(mByteCode.getContext()) << "Basic blocks located at: "; for (const auto& m : basicBlocks) - LOG(LogLevel::kDebug) << m.first << ": " << m.second.toString(); + LOG(LogLevel::kDebug).session(mByteCode.getContext()) << m.first << ": " << m.second.toString(); } LOG_IF(DEBUG_OPTIMIZER) << "Scanning for dead code blocks"; diff --git a/aplcore/src/datasource/datasource.cpp b/aplcore/src/datasource/datasource.cpp index 7c3a516..12c83e4 100644 --- a/aplcore/src/datasource/datasource.cpp +++ b/aplcore/src/datasource/datasource.cpp @@ -48,13 +48,13 @@ DataSource::create(const ContextPtr& context, const Object& object, const std::s std::string type = propertyAsString(*context, object, "type"); if (type.empty()) { - CONSOLE_CTP(context) << "Unrecognized type field in DataSource"; + CONSOLE(context) << "Unrecognized type field in DataSource"; return Object::NULL_OBJECT(); } auto dataSourceProvider = context->getRootConfig().getDataSourceProvider(type); if(!dataSourceProvider) { - CONSOLE_CTP(context) << "Unrecognized DataSource type"; + CONSOLE(context) << "Unrecognized DataSource type"; return Object::NULL_OBJECT(); } @@ -64,7 +64,7 @@ DataSource::create(const ContextPtr& context, const Object& object, const std::s auto liveDataSourceArray = items.isNull() ? LiveArray::create() : LiveArray::create(arrayify(*context, items)); auto dataSourceConnection = dataSourceProvider->create(object, context, liveDataSourceArray); if (!dataSourceConnection) { - CONSOLE_CTP(context) << "DataSourceConnection failed to initialize."; + CONSOLE(context) << "DataSourceConnection failed to initialize."; return Object::NULL_OBJECT(); } liveDataSourceArray = dataSourceConnection->getLiveArray(); diff --git a/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp b/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp index 240a1d4..9120452 100644 --- a/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamicindexlistdatasourceprovider.cpp @@ -94,12 +94,12 @@ DynamicIndexListDataSourceConnection::processLazyLoad(int index, const Object& d size_t idx = index - mMinimumInclusiveIndex; if (overlaps(idx, dataArray.size())) { - constructAndReportError(ERROR_REASON_OCCUPIED_LIST_INDEX, index, + constructAndReportError(context->session(), ERROR_REASON_OCCUPIED_LIST_INDEX, index, "Load range overlaps existing items. New items for existing range discarded."); } result = update(idx, dataArray, false); } else { - constructAndReportError(ERROR_REASON_MISSING_LIST_ITEMS, index, + constructAndReportError(context->session(), 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(ERROR_REASON_LOAD_INDEX_OUT_OF_RANGE, index, + constructAndReportError(context->session(), ERROR_REASON_LOAD_INDEX_OUT_OF_RANGE, index, "Requested index out of bounds."); } return result; @@ -118,17 +118,18 @@ DynamicIndexListDataSourceConnection::processLazyLoad(int index, const Object& d bool DynamicIndexListDataSourceConnection::processUpdate(DynamicIndexListUpdateType type, int index, const Object& data, int count) { - if (index < mMinimumInclusiveIndex) { - constructAndReportError(ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, - "Requested index out of bounds."); + auto context = mContext.lock(); + if (!context) { return false; } - size_t idx = index - mMinimumInclusiveIndex; - auto context = mContext.lock(); - if (!context) { + if (index < mMinimumInclusiveIndex) { + constructAndReportError(context->session(), 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); bool result = false; @@ -159,7 +160,7 @@ DynamicIndexListDataSourceConnection::processUpdate(DynamicIndexListUpdateType t } if (!items.isArray()) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, index, + constructAndReportError(context->session(), ERROR_REASON_INTERNAL_ERROR, index, "No array provided for range insert."); return false; } @@ -181,7 +182,7 @@ DynamicIndexListDataSourceConnection::processUpdate(DynamicIndexListUpdateType t break; } if (!result) { - constructAndReportError(ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, + constructAndReportError(context->session(), ERROR_REASON_LIST_INDEX_OUT_OF_RANGE, index, "Requested index out of bounds."); } return result; @@ -254,8 +255,10 @@ DynamicIndexListDataSourceProvider::createConnection( std::weak_ptr context, std::weak_ptr liveArray, const std::string& listId) { + auto ctx = context.lock(); + if (!ctx) return nullptr; if (!sourceDefinition.has(START_INDEX) || !sourceDefinition.get(START_INDEX).isNumber()) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); + constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); return nullptr; } @@ -270,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(ERROR_REASON_INTERNAL_ERROR, listId, "DataSource bounds configuration is wrong."); + constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, listId, "DataSource bounds configuration is wrong."); return nullptr; } @@ -304,20 +307,23 @@ DynamicIndexListDataSourceProvider::processLazyLoadInternal( auto minimumInclusiveIndex = responseMap.get(MINIMUM_INCLUSIVE_INDEX); auto maximumExclusiveIndex = responseMap.get(MAXIMUM_EXCLUSIVE_INDEX); + auto ctx = connection->getContext(); + if (!ctx) return false; + if(connection->updateBounds(minimumInclusiveIndex, maximumExclusiveIndex)) { - constructAndReportError(ERROR_REASON_INCONSISTENT_RANGE, connection, startIndex, + constructAndReportError(connection->getContext()->session(), ERROR_REASON_INCONSISTENT_RANGE, connection, startIndex, "Bounds were changed in runtime."); } if (!responseMap.has(ITEMS)) { - constructAndReportError(ERROR_REASON_MISSING_LIST_ITEMS, connection, Object::NULL_OBJECT(), - "No items defined."); + constructAndReportError(connection->getContext()->session(), ERROR_REASON_MISSING_LIST_ITEMS, connection, + Object::NULL_OBJECT(), "No items defined."); return true; } if (!connection->changesAllowed()) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, connection, Object::NULL_OBJECT(), - "Payload has unexpected fields."); + constructAndReportError(connection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, connection, + Object::NULL_OBJECT(), "Payload has unexpected fields."); return true; } @@ -329,8 +335,8 @@ bool DynamicIndexListDataSourceProvider::processUpdateInternal( const DILConnectionPtr& connection, const Object& responseMap) { if (connection->isLazyLoadingOnly()) { - constructAndReportError(ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, connection, Object::NULL_OBJECT(), - "List supports only lazy loading."); + constructAndReportError(connection->getContext()->session(), ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, + connection, Object::NULL_OBJECT(), "List supports only lazy loading."); connection->setFailed(); return false; } @@ -341,16 +347,16 @@ 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(ERROR_REASON_INVALID_OPERATION, connection, Object::NULL_OBJECT(), - "Operation malformed."); + constructAndReportError(connection->getContext()->session(), ERROR_REASON_INVALID_OPERATION, connection, + Object::NULL_OBJECT(), "Operation malformed."); result = false; break; } auto typeName = operation.get(UPDATE_TYPE).asString(); if (!sDatasourceUpdateType.count(typeName)) { - constructAndReportError(ERROR_REASON_INVALID_OPERATION, connection, Object::NULL_OBJECT(), - "Wrong update type."); + constructAndReportError(connection->getContext()->session(), ERROR_REASON_INVALID_OPERATION, connection, + Object::NULL_OBJECT(), "Wrong update type."); result = false; break; } @@ -377,7 +383,7 @@ bool DynamicIndexListDataSourceProvider::process(const Object& responseMap) { if (!responseMap.has(LIST_ID) || !responseMap.get(LIST_ID).isString()) { - constructAndReportError(ERROR_REASON_INVALID_LIST_ID, "N/A", "Missing listId."); + constructAndReportError(nullptr, ERROR_REASON_INVALID_LIST_ID, "N/A", "Missing listId."); return false; } @@ -390,9 +396,12 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { } auto connection = std::dynamic_pointer_cast(dataSourceConnection); + auto context = connection->getContext(); + if (!context) + return false; if(connection->inFailState()) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, listId, "List in fail state."); + constructAndReportError(context->session(), ERROR_REASON_INTERNAL_ERROR, listId, "List in fail state."); return false; } @@ -406,7 +415,7 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { } else if (responseMap.has(OPERATIONS) && responseMap.get(OPERATIONS).isArray()) { isLazyLoading = false; } else { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, connection, + constructAndReportError(context->session(), ERROR_REASON_INTERNAL_ERROR, connection, Object::NULL_OBJECT(), "Payload missing required fields."); return false; } @@ -418,11 +427,11 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { if (listVersion > currentListVersion + 1) { connection->putCacheUpdate(listVersion - 1, responseMap); } else if (listVersion < 0) { - constructAndReportError(ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, connection, - Object::NULL_OBJECT(), "Missing list version."); + constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_VERSION_IN_SEND_DATA, + connection, Object::NULL_OBJECT(), "Missing list version."); connection->setFailed(); } else { - constructAndReportError(ERROR_REASON_DUPLICATE_LIST_VERSION, connection, + constructAndReportError(context->session(), ERROR_REASON_DUPLICATE_LIST_VERSION, connection, Object::NULL_OBJECT(), "Duplicate list version."); } @@ -448,8 +457,7 @@ DynamicIndexListDataSourceProvider::process(const Object& responseMap) { processUpdate(cachedPayload); } - auto context = connection->getContext(); - if (result && context != nullptr) + if (result) context->setDirtyDataSourceContext( std::dynamic_pointer_cast(shared_from_this())); diff --git a/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp b/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp index fc3dc5a..0381c24 100644 --- a/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamiclistdatasourceprovider.cpp @@ -101,7 +101,7 @@ DynamicListDataSourceConnection::scheduleTimeout(const std::string& correlationT return; if (self->retryFetchRequest(correlationToken)) { - self->constructAndReportError(ERROR_REASON_LOAD_TIMEOUT, Object::NULL_OBJECT(), + self->constructAndReportError(ctx->session(), ERROR_REASON_LOAD_TIMEOUT, Object::NULL_OBJECT(), "Retrying timed out request: " + correlationToken); } }, mConfiguration.fetchTimeout); @@ -193,7 +193,7 @@ DynamicListDataSourceConnection::reportUpdateExpired(int version) { auto it = mUpdatesCache.find(version); if (it != mUpdatesCache.end()) { context->getRootConfig().getTimeManager()->clearTimeout(it->second.expiryTimeout); - constructAndReportError(ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), + constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), "Update to version " + std::to_string(version + 1) + " buffered longer than expected."); } } @@ -201,16 +201,17 @@ DynamicListDataSourceConnection::reportUpdateExpired(int version) { void DynamicListDataSourceConnection::putCacheUpdate(int version, const Object& payload) { auto provider = mProvider.lock(); - if (!provider) { + auto context = mContext.lock(); + if (!provider || !context) { // Provider dead. Should not happen. - LOG(LogLevel::kError) << "DataSource provider for " << mConfiguration.type + LOG(LogLevel::kError).session(context) << "DataSource provider for " << mConfiguration.type << " is dead while trying to process update cache."; return; } if (mUpdatesCache.size() >= mConfiguration.listUpdateBufferSize) { // Remove highest or discard current one if it's one. - constructAndReportError(ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), + constructAndReportError(context->session(), ERROR_REASON_MISSING_LIST_VERSION, Object::NULL_OBJECT(), "Too many updates buffered. Discarding highest version."); auto it = mUpdatesCache.rbegin(); if (it->first > version) { @@ -225,7 +226,7 @@ DynamicListDataSourceConnection::putCacheUpdate(int version, const Object& paylo Update update = {payload, timeoutId}; mUpdatesCache.emplace(version, update); } else { - constructAndReportError(ERROR_REASON_DUPLICATE_LIST_VERSION, Object::NULL_OBJECT(), + constructAndReportError(context->session(), ERROR_REASON_DUPLICATE_LIST_VERSION, Object::NULL_OBJECT(), "Trying to cache existing list version."); } } @@ -249,18 +250,19 @@ DynamicListDataSourceConnection::retrieveCachedUpdate(int version) { void DynamicListDataSourceConnection::constructAndReportError( + const SessionPtr& session, const std::string& reason, const Object& operationIndex, const std::string& message) { auto provider = mProvider.lock(); if (!provider) { // Provider dead. Should not happen. - LOG(LogLevel::kError) << "DataSource provider for " << mConfiguration.type + LOG(LogLevel::kError).session(mContext.lock()) << "DataSource provider for " << mConfiguration.type << " is dead while trying to report error."; return; } - provider->constructAndReportError(reason, shared_from_this(), operationIndex, message); + provider->constructAndReportError(session, reason, shared_from_this(), operationIndex, message); } DynamicListDataSourceProvider::DynamicListDataSourceProvider(const DynamicListConfiguration& config) @@ -272,8 +274,10 @@ DynamicListDataSourceProvider::create( std::weak_ptr context, std::weak_ptr liveArray) { clearStaleConnections(); + auto ctx = context.lock(); + if (!ctx) return nullptr; if (!sourceDefinition.has(LIST_ID) || !sourceDefinition.get(LIST_ID).isString()) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); + constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); return nullptr; } @@ -283,10 +287,13 @@ DynamicListDataSourceProvider::create( // this datasource allows for data reuse on reinflate. auto contextPtr = context.lock(); if (contextPtr && contextPtr->getReinflationFlag()) { + // If no valid context attached - we are likely reinflating. + existingConnection->setContext(ctx); return existingConnection; } // Trying to reuse existing listId/DataSource. Should not happen. - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, listId, "Trying to reuse existing listId."); + constructAndReportError(existingConnection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, listId, + "Trying to reuse existing listId."); return nullptr; } @@ -316,7 +323,7 @@ DynamicListDataSourceProvider::processUpdate(const Object &payload) { } else if (payload.isMap()) { result = process(payload); } else { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, "N/A", "Can't process payload."); + constructAndReportError(nullptr, ERROR_REASON_INTERNAL_ERROR, "N/A", "Can't process payload."); } return result; @@ -370,9 +377,9 @@ DynamicListDataSourceProvider::getConnection(const std::string& listId, const Ob } if (!hasValidListId && connectionIter != mConnections.end()) { - constructAndReportError(ERROR_REASON_INCONSISTENT_LIST_ID, listId, "Non-existing listId."); + constructAndReportError(nullptr, ERROR_REASON_INCONSISTENT_LIST_ID, listId, "Non-existing listId."); } else if (connectionIter == mConnections.end()) { - constructAndReportError(ERROR_REASON_INVALID_LIST_ID, listId, "Unexpected response."); + constructAndReportError(nullptr, ERROR_REASON_INVALID_LIST_ID, listId, "Unexpected response."); return nullptr; } @@ -380,7 +387,7 @@ DynamicListDataSourceProvider::getConnection(const std::string& listId, const Ob if (!connection) { // Link is dead. Clean it up. Unlikely to happen but clean anyway. mConnections.erase(connectionIter); - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, listId, "DataSource context lost."); + constructAndReportError(nullptr, ERROR_REASON_INTERNAL_ERROR, listId, "DataSource context lost."); return nullptr; } @@ -396,6 +403,7 @@ DynamicListDataSourceProvider::getPendingErrors() { void DynamicListDataSourceProvider::constructAndReportError( + const SessionPtr& session, const std::string& reason, const std::string& listId, const Object& listVersion, @@ -416,24 +424,26 @@ DynamicListDataSourceProvider::constructAndReportError( mPendingErrors.emplace_back(std::move(error)); // Throw errors into log to help debugging on device - LOG(LogLevel::kWarn) << "Datasource " << listId << "; Error: " << message; + LOG(LogLevel::kWarn).session(session) << "Datasource " << listId << "; Error: " << message; } void DynamicListDataSourceProvider::constructAndReportError( + const SessionPtr& session, const std::string& reason, const std::string& listId, const std::string& message) { - constructAndReportError(reason, listId, Object::NULL_OBJECT(), Object::NULL_OBJECT(), message); + constructAndReportError(session, reason, listId, Object::NULL_OBJECT(), Object::NULL_OBJECT(), message); } void DynamicListDataSourceProvider::constructAndReportError( + const SessionPtr& session, const std::string& reason, const DLConnectionPtr& connection, const Object& operationIndex, const std::string& message) { - constructAndReportError(reason, connection->getListId(), connection->getListVersion(), operationIndex, message); + constructAndReportError(session, reason, connection->getListId(), connection->getListVersion(), operationIndex, message); } bool @@ -447,6 +457,7 @@ DynamicListDataSourceProvider::canFetch(const Object& correlationToken, const DL return false; } - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, connection, Object::NULL_OBJECT(), "Wrong correlation token."); + constructAndReportError(connection->getContext()->session(), 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 1f51ed3..43e1f8d 100644 --- a/aplcore/src/datasource/dynamictokenlistdatasourceprovider.cpp +++ b/aplcore/src/datasource/dynamictokenlistdatasourceprovider.cpp @@ -62,7 +62,7 @@ DynamicTokenListDataSourceConnection::processLazyLoad( result = updateLiveArray(dataArray, pageToken, nextPageToken); } else { - constructAndReportError(ERROR_REASON_MISSING_LIST_ITEMS, Object::NULL_OBJECT(), + constructAndReportError(getContext()->session(), ERROR_REASON_MISSING_LIST_ITEMS, Object::NULL_OBJECT(), "No items provided to load."); retryFetchRequest(correlationToken.asString()); return result; @@ -131,9 +131,12 @@ DynamicTokenListDataSourceProvider::createConnection( const std::string& listId) { auto sourceMap = sourceDefinition.getMap(); + auto ctx = context.lock(); + if (!ctx) return nullptr; + if (!sourceDefinition.has(LIST_ID) || !sourceDefinition.get(LIST_ID).isString()|| !sourceDefinition.has(PAGE_TOKEN) || !sourceDefinition.get(PAGE_TOKEN).isString()) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); + constructAndReportError(ctx->session(), ERROR_REASON_INTERNAL_ERROR, "N/A", "Missing required fields."); return nullptr; } @@ -155,7 +158,7 @@ DynamicTokenListDataSourceProvider::processLazyLoadInternal( return false; if (!responseMap.has(ITEMS)) { - constructAndReportError(ERROR_REASON_INTERNAL_ERROR, connection->getListId(), + constructAndReportError(connection->getContext()->session(), ERROR_REASON_INTERNAL_ERROR, connection->getListId(), "Missing required fields."); return true; } @@ -169,7 +172,7 @@ DynamicTokenListDataSourceProvider::processLazyLoadInternal( bool DynamicTokenListDataSourceProvider::process(const Object& responseMap) { if (!responseMap.has(LIST_ID) || !responseMap.get(LIST_ID).isString()) { - constructAndReportError(ERROR_REASON_INVALID_LIST_ID, "N/A", "Missing listId."); + constructAndReportError(nullptr, ERROR_REASON_INVALID_LIST_ID, "N/A", "Missing listId."); return false; } diff --git a/aplcore/src/engine/builder.cpp b/aplcore/src/engine/builder.cpp index 52b9cb1..eaf06ec 100644 --- a/aplcore/src/engine/builder.cpp +++ b/aplcore/src/engine/builder.cpp @@ -78,7 +78,7 @@ Builder::populateSingleChildLayout(const ContextPtr& context, bool fullBuild, bool useDirtyFlag) { - LOG_IF(DEBUG_BUILDER) << "call"; + LOG_IF(DEBUG_BUILDER).session(context) << "call"; APL_TRACE_BLOCK("Builder:populateSingleChildLayout"); auto child = expandSingleComponentFromArray(context, arrayifyProperty(*context, item, "item", "items"), @@ -98,7 +98,7 @@ Builder::populateLayoutComponent(const ContextPtr& context, bool fullBuild, bool useDirtyFlag) { - LOG_IF(DEBUG_BUILDER) << path; + LOG_IF(DEBUG_BUILDER).session(context) << path; APL_TRACE_BLOCK("Builder:populateLayoutComponent"); auto child = expandSingleComponentFromArray(context, @@ -133,7 +133,7 @@ Builder::populateLayoutComponent(const ContextPtr& context, else { auto dataItems = evaluateRecursive(*context, data); if (!dataItems.empty()) { - LOG_IF(DEBUG_BUILDER) << "data size=" << dataItems.size(); + LOG_IF(DEBUG_BUILDER).session(context) << "data size=" << dataItems.size(); // Transform data into LiveData and use rebuilder to have more control over its content. auto rawArray = ObjectArray(); @@ -149,7 +149,7 @@ Builder::populateLayoutComponent(const ContextPtr& context, layoutBuilder->build(useDirtyFlag); } else { - LOG_IF(DEBUG_BUILDER) << "items size=" << items.size(); + LOG_IF(DEBUG_BUILDER).session(context) << "items size=" << items.size(); auto length = items.size(); for (int i = 0; i < length; i++) { const auto& element = items.at(i); @@ -234,17 +234,17 @@ Builder::expandSingleComponent(const ContextPtr& context, bool fullBuild, bool useDirtyFlag) { - LOG_IF(DEBUG_BUILDER) << path.toString(); + LOG_IF(DEBUG_BUILDER).session(context) << path.toString(); APL_TRACE_BLOCK("Builder:expandSingleComponent"); std::string type = propertyAsString(*context, item, "type"); if (type.empty()) { - CONSOLE_CTP(context) << "Invalid type in component"; + CONSOLE(context) << "Invalid type in component"; return nullptr; } if (auto method = findComponentBuilderFunc(context, type)) { - LOG_IF(DEBUG_BUILDER) << "Expanding primitive " << type; + LOG_IF(DEBUG_BUILDER).session(context) << "Expanding primitive " << type; // Copy the items into the properties map. properties.emplace(item); @@ -256,7 +256,7 @@ Builder::expandSingleComponent(const ContextPtr& context, // Construct the component CoreComponentPtr component = CoreComponentPtr(method(expanded, std::move(properties), path)); if (!component || !component->isValid()) { - CONSOLE_CTP(context) << "Unable to inflate component"; + CONSOLE(context) << "Unable to inflate component"; if (component) component->release(); return nullptr; @@ -284,25 +284,25 @@ Builder::expandSingleComponent(const ContextPtr& context, copyPreservedProperties(component, oldComponent); } - LOG_IF(DEBUG_BUILDER) << "Returning component " << *component; + LOG_IF(DEBUG_BUILDER).session(context) << "Returning component " << *component; return component; } - LOG_IF(DEBUG_BUILDER) << "Expanding layout '" << type << "'"; + LOG_IF(DEBUG_BUILDER).session(context) << "Expanding layout '" << type << "'"; auto resource = context->getLayout(type); if (!resource.empty()) { properties.emplace(item); return expandLayout(context, properties, resource.json(), parent, resource.path(), fullBuild, useDirtyFlag); } - CONSOLE_CTP(context) << "Unable to find layout or component '" << type << "'"; + CONSOLE(context) << "Unable to find layout or component '" << type << "'"; return nullptr; } /** * Process data bindings - * @param context - * @param item + * @param context The data-binding context in which to evaluate the item. + * @param item The item that contains a "bind" property. */ void Builder::attachBindings(const apl::ContextPtr& context, const apl::Object& item) @@ -315,7 +315,7 @@ Builder::attachBindings(const apl::ContextPtr& context, const apl::Object& item) continue; if (context->hasLocal(name)) { - CONSOLE_CTP(context) << "Attempted to bind to pre-existing property '" << name << "'"; + CONSOLE(context) << "Attempted to bind to pre-existing property '" << name << "'"; continue; } @@ -354,7 +354,7 @@ Builder::expandSingleComponentFromArray(const ContextPtr& context, bool fullBuild, bool useDirtyFlag) { - LOG_IF(DEBUG_BUILDER) << path; + LOG_IF(DEBUG_BUILDER).session(context) << path; for (int index = 0; index < items.size(); index++) { const auto& item = items.at(index); if (!item.isMap()) @@ -388,10 +388,10 @@ Builder::expandLayout(const ContextPtr& context, bool fullBuild, bool useDirtyFlag) { - LOG_IF(DEBUG_BUILDER) << path; + LOG_IF(DEBUG_BUILDER).session(context) << path; if (!layout.IsObject()) { std::string errorMessage = "Layout inflation for one of the components failed. Path: " + path.toString(); - CONSOLE_CTP(context) << errorMessage; + CONSOLE(context) << errorMessage; return nullptr; } APL_TRACE_BLOCK("Builder:expandLayout"); @@ -404,16 +404,16 @@ Builder::expandLayout(const ContextPtr& context, // the property map. ParameterArray params(layout); for (const auto& param : params) { - LOG_IF(DEBUG_BUILDER) << "Parsing parameter: " << param.name; + LOG_IF(DEBUG_BUILDER).session(context) << "Parsing parameter: " << param.name; properties.addToContext(cptr, param, true); } Builder::attachBindings(cptr, layout); if (DEBUG_BUILDER) { - for (std::shared_ptr p = cptr; p; p = p->parent()) { + for (ConstContextPtr p = cptr; p; p = p->parent()) { for (const auto& m : *p) - LOG(LogLevel::kDebug) << m.first << ": " << m.second; + LOG(LogLevel::kDebug).session(context) << m.first << ": " << m.second; } } return expandSingleComponentFromArray(cptr, @@ -471,7 +471,7 @@ void Builder::copyPreservedBindings(const CoreComponentPtr& newComponent, const CoreComponentPtr& originalComponent) { APL_TRACE_BLOCK("Builder:copyPreservedBindings"); - LOG_IF(DEBUG_BUILDER) << newComponent << " old=" << mOld; + LOG_IF(DEBUG_BUILDER).session(newComponent) << newComponent << " old=" << mOld; copyPreserved(newComponent, originalComponent, true, false); } @@ -485,7 +485,7 @@ void Builder::copyPreservedProperties(const CoreComponentPtr& newComponent, const CoreComponentPtr& originalComponent) { APL_TRACE_BLOCK("Builder:copyPreservedProperties"); - LOG_IF(DEBUG_BUILDER) << newComponent << " old=" << mOld; + LOG_IF(DEBUG_BUILDER).session(newComponent) << newComponent << " old=" << mOld; copyPreserved(newComponent, originalComponent, false, true); } diff --git a/aplcore/src/engine/context.cpp b/aplcore/src/engine/context.cpp index d3e666e..3cd84b2 100644 --- a/aplcore/src/engine/context.cpp +++ b/aplcore/src/engine/context.cpp @@ -456,7 +456,7 @@ bool Context::userUpdateAndRecalculate(const std::string& key, const Object& val if (it->second.set(value)) // If the value changes, recalculate downstream values recalculateDownstream(key, useDirtyFlag); } else { - CONSOLE_S(mCore->session()) << "Data-binding field '" << key << "' is read-only"; + CONSOLE(mCore->session()) << "Data-binding field '" << key << "' is read-only"; } return true; diff --git a/aplcore/src/engine/contextdependant.cpp b/aplcore/src/engine/contextdependant.cpp index 67ae4b8..cf9d4ae 100644 --- a/aplcore/src/engine/contextdependant.cpp +++ b/aplcore/src/engine/contextdependant.cpp @@ -16,6 +16,7 @@ #include "apl/engine/contextdependant.h" #include "apl/engine/evaluate.h" #include "apl/primitives/symbolreferencemap.h" +#include "apl/utils/session.h" namespace apl { @@ -28,7 +29,7 @@ ContextDependant::create(const ContextPtr& downstreamContext, const ContextPtr& bindingContext, BindingFunction bindingFunction) { - LOG_IF(DEBUG_CONTEXT_DEP) << "to '" << downstreamName << "' (" << downstreamContext.get() << ")"; + LOG_IF(DEBUG_CONTEXT_DEP).session(bindingContext) << "to '" << downstreamName << "' (" << downstreamContext.get() << ")"; SymbolReferenceMap symbols; equation.symbols(symbols); diff --git a/aplcore/src/engine/event.cpp b/aplcore/src/engine/event.cpp index b0bd3f9..814b3ed 100644 --- a/aplcore/src/engine/event.cpp +++ b/aplcore/src/engine/event.cpp @@ -49,6 +49,7 @@ Bimap sEventPropertyBimap = { {kEventPropertyExtensionURI, "extensionURI"}, {kEventPropertyExtensionResourceId, "resourceId"}, {kEventPropertyFlags, "flags"}, + {kEventPropertyHeaders, "headers"}, {kEventPropertyHighlightMode, "highlightMode"}, {kEventPropertyMediaType, "mediaType"}, {kEventPropertyName, "name"}, @@ -138,7 +139,7 @@ Event::serialize(rapidjson::Document::AllocatorType& allocator) const for (auto& m : mData->bag) { if (!sEventPropertyBimap.has(m.first)) { - LOG(LogLevel::kError) << "Unknown property enum: " << m.first; + LOG(LogLevel::kError).session(getComponent()) << "Unknown property enum: " << m.first; continue; } const std::string& prop = sEventPropertyBimap.at(m.first); diff --git a/aplcore/src/engine/hovermanager.cpp b/aplcore/src/engine/hovermanager.cpp index b180dcf..f5cff4a 100644 --- a/aplcore/src/engine/hovermanager.cpp +++ b/aplcore/src/engine/hovermanager.cpp @@ -61,16 +61,16 @@ HoverManager::setCursorPosition(const Point& cursorPosition) { if (previous && !previous->getState().get(kStateDisabled)) { previous->executeOnCursorExit(); - LOG_IF(DEBUG_HOVER) << "Execute OnCursorExit: " << previous->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorExit: " << previous->toDebugSimpleString(); } if (target && !target->getState().get(kStateDisabled)) { target->executeOnCursorEnter(); - LOG_IF(DEBUG_HOVER) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); } // store the new hover component, if any - LOG_IF(DEBUG_HOVER) << "hover change -\n\tfrom: " << previous << "\n\t to:" << target; + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "hover change -\n\tfrom: " << previous << "\n\t to:" << target; mHover = target; } @@ -112,10 +112,10 @@ HoverManager::componentToggledDisabled(const CoreComponentPtr& component) { // execute the OnCursor commands if (target->getState().get(kStateDisabled)) { target->executeOnCursorExit(); - LOG_IF(DEBUG_HOVER) << "Execute OnCursorExit: " << target->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorExit: " << target->toDebugSimpleString(); } else { target->executeOnCursorEnter(); - LOG_IF(DEBUG_HOVER) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Execute OnCursorEnter: " << target->toDebugSimpleString(); } } @@ -142,14 +142,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) << "Hover Previous: " << previous->toDebugSimpleString() << " state: " << previous->getState(); + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "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) << "Hover Target: " << target->toDebugSimpleString() << " state: " << target->getState(); + LOG_IF(DEBUG_HOVER).session(mCore.session()) << "Hover Target: " << target->toDebugSimpleString() << " state: " << target->getState(); } } diff --git a/aplcore/src/engine/keyboardmanager.cpp b/aplcore/src/engine/keyboardmanager.cpp index 52b7966..b7e0797 100644 --- a/aplcore/src/engine/keyboardmanager.cpp +++ b/aplcore/src/engine/keyboardmanager.cpp @@ -54,7 +54,7 @@ KeyboardManager::getHandlerPropertyKey(KeyHandlerType type) { bool KeyboardManager::handleKeyboard(KeyHandlerType type, const CoreComponentPtr& component, const Keyboard& keyboard, const RootContextPtr& rootContext) { - LOG_IF(DEBUG_KEYBOARD_MANAGER) << "type:" << type << ", keyboard:" << keyboard.toDebugString(); + LOG_IF(DEBUG_KEYBOARD_MANAGER).session(rootContext) << "type:" << type << ", keyboard:" << keyboard.toDebugString(); if (keyboard.isReservedKey()) { // do not process handlers when is key reserved for future use by APL @@ -68,7 +68,7 @@ KeyboardManager::handleKeyboard(KeyHandlerType type, const CoreComponentPtr& com while (!consumed && target) { consumed = target->processKeyPress(type, keyboard); if (consumed) { - LOG_IF(DEBUG_KEYBOARD_MANAGER) << target->getUniqueId() << " " << type << " consumed."; + LOG_IF(DEBUG_KEYBOARD_MANAGER).session(rootContext) << target->getUniqueId() << " " << type << " consumed."; } else { // propagate target = std::static_pointer_cast(target->getParent()); diff --git a/aplcore/src/engine/layoutmanager.cpp b/aplcore/src/engine/layoutmanager.cpp index a08a00a..f6c8969 100644 --- a/aplcore/src/engine/layoutmanager.cpp +++ b/aplcore/src/engine/layoutmanager.cpp @@ -28,10 +28,9 @@ static const bool DEBUG_LAYOUT_MANAGER = false; static void yogaNodeDirtiedCallback(YGNodeRef node) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << "dirty top node"; - auto component = static_cast(YGNodeGetContext(node)); assert(component); + LOG_IF(DEBUG_LAYOUT_MANAGER).session(*component) << "dirty top node"; component->getContext()->layoutManager().requestLayout(component->shared_from_corecomponent(), false); } @@ -60,7 +59,7 @@ LayoutManager::needsLayout() const void LayoutManager::firstLayout() { - LOG_IF(DEBUG_LAYOUT_MANAGER) << mTerminated; + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << mTerminated; if (mTerminated) return; @@ -108,7 +107,7 @@ compareComponents(const CoreComponentPtr& a, const CoreComponentPtr& b) void LayoutManager::layout(bool useDirtyFlag, bool first) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << mTerminated << " dirty_flag=" << useDirtyFlag; + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << mTerminated << " dirty_flag=" << useDirtyFlag; if (mTerminated || mInLayout) return; @@ -119,7 +118,7 @@ LayoutManager::layout(bool useDirtyFlag, bool first) mInLayout = true; while (needsLayout()) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << "Laying out " << mPendingLayout.size() << " component(s)"; + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << "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()); @@ -166,7 +165,7 @@ LayoutManager::flushLazyInflationInternal(const CoreComponentPtr& comp) void LayoutManager::setAsTopNode(const CoreComponentPtr& component) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString(); + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString(); assert(component); YGNodeSetDirtiedFunc(component->getNode(), yogaNodeDirtiedCallback); } @@ -174,7 +173,7 @@ LayoutManager::setAsTopNode(const CoreComponentPtr& component) void LayoutManager::removeAsTopNode(const CoreComponentPtr& component) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString(); + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString(); assert(component); YGNodeSetDirtiedFunc(component->getNode(), nullptr); } @@ -185,7 +184,7 @@ LayoutManager::layoutComponent(const CoreComponentPtr& component, bool useDirtyF APL_TRACE_BLOCK("LayoutManager:layoutComponent"); auto parent = component->getParent(); - LOG_IF(DEBUG_LAYOUT_MANAGER) << "component=" << component->toDebugSimpleString() + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << "component=" << component->toDebugSimpleString() << " dirty_flag=" << useDirtyFlag << " parent=" << (parent ? parent->toDebugSimpleString() : "none"); @@ -218,7 +217,7 @@ LayoutManager::layoutComponent(const CoreComponentPtr& component, bool useDirtyF void LayoutManager::requestLayout(const CoreComponentPtr& component, bool force) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString() << " force=" << force; + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString() << " force=" << force; assert(component); if (mTerminated) @@ -258,7 +257,7 @@ LayoutManager::remove(const CoreComponentPtr& component) bool LayoutManager::ensure(const CoreComponentPtr& component) { - LOG_IF(DEBUG_LAYOUT_MANAGER) << component->toDebugSimpleString(); + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << component->toDebugSimpleString(); // Walk up the component hierarchy and ensure that Yoga nodes are correctly attached bool result = false; @@ -277,7 +276,7 @@ LayoutManager::ensure(const CoreComponentPtr& component) attachedYogaNodeNeedsLayout = false; } } else { // This child needs to be attached to its parent - LOG_IF(DEBUG_LAYOUT_MANAGER) << "Attaching yoga node from: " << child->toDebugSimpleString(); + LOG_IF(DEBUG_LAYOUT_MANAGER).session(mCore.session()) << "Attaching yoga node from: " << child->toDebugSimpleString(); parent->attachYogaNode(child); attachedYogaNodeNeedsLayout = true; } diff --git a/aplcore/src/engine/properties.cpp b/aplcore/src/engine/properties.cpp index 88659ca..13f52b1 100644 --- a/aplcore/src/engine/properties.cpp +++ b/aplcore/src/engine/properties.cpp @@ -23,6 +23,7 @@ #include "apl/engine/evaluate.h" #include "apl/primitives/dimension.h" #include "apl/datasource/datasource.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -38,7 +39,9 @@ Properties::asLabel(const Context& context, const char *name) auto result = evaluate(context, s->second).asString(); result.erase(std::remove_if(result.begin(), result.end(), - [](unsigned char c) { return !std::isalnum(c) && (c != '_') && (c != '-'); }), result.end()); + [](unsigned char c) { + return !sutil::isalnum(c) && (c != '_') && (c != '-'); + }), result.end()); return result; } diff --git a/aplcore/src/engine/resources.cpp b/aplcore/src/engine/resources.cpp index c76f085..337c0af 100644 --- a/aplcore/src/engine/resources.cpp +++ b/aplcore/src/engine/resources.cpp @@ -52,15 +52,15 @@ addResourceBlock( const ResourceOperators& resourceOperators) { if (DEBUG_RESOURCES) { - LOG(LogLevel::kDebug) << path; + LOG(LogLevel::kDebug).session(context) << path; auto description = block.FindMember("description"); if (description != block.MemberEnd() && description->value.IsString()) - LOG(LogLevel::kDebug) << "Evaluating resource block: " << description->value.GetString(); + LOG(LogLevel::kDebug).session(context) << "Evaluating resource block: " << description->value.GetString(); } auto when = block.FindMember("when"); if (when != block.MemberEnd() && !evaluate(context, when->value).asBoolean()) { - LOG_IF(DEBUG_RESOURCES) << "...skipping"; + LOG_IF(DEBUG_RESOURCES).session(context) << "...skipping"; return; } @@ -82,7 +82,7 @@ addResourceBlock( auto resourceName = itemIter->name.GetString(); auto result = conversionFunc(context, evaluate(context, itemIter->value)); context.putResource(std::string("@")+resourceName, result, memberPath.addObject(resourceName)); - LOG_IF(DEBUG_RESOURCES) << " @" << resourceName << ": " << result + LOG_IF(DEBUG_RESOURCES).session(context) << " @" << resourceName << ": " << result << " [" << memberPath.addObject(resourceName).toString() << "]"; } } @@ -96,13 +96,13 @@ addOrderedResources( const ResourceOperators& resourceOperators) { if (value.IsArray()) { - LOG_IF(DEBUG_RESOURCES) << "addOrderedResources: " << value.GetArray().Size(); + LOG_IF(DEBUG_RESOURCES).session(context) << "addOrderedResources: " << value.GetArray().Size(); auto arraySize = value.Size(); for (rapidjson::SizeType i = 0; i < arraySize; i++) { const auto &item = value[i]; if (!item.IsObject()) { - LOG_IF(DEBUG_RESOURCES) << "addOrderedResources - item is not an object: " << path << i; + LOG_IF(DEBUG_RESOURCES).session(context) << "addOrderedResources - item is not an object: " << path << i; continue; } diff --git a/aplcore/src/engine/rootcontext.cpp b/aplcore/src/engine/rootcontext.cpp index 0a8deac..86d4930 100644 --- a/aplcore/src/engine/rootcontext.cpp +++ b/aplcore/src/engine/rootcontext.cpp @@ -72,7 +72,7 @@ RootContext::create(const Metrics& metrics, const ContentPtr& content, const RootConfig& config, std::function callback) { if (!content->isReady()) { - LOG(LogLevel::kError) << "Attempting to create root context with illegal content"; + LOG(LogLevel::kError).session(content->getSession()) << "Attempting to create root context with illegal content"; return nullptr; } @@ -121,7 +121,7 @@ void RootContext::updateDisplayState(DisplayState displayState) { if (!sDisplayStateMap.has(displayState)) { - LOG(LogLevel::kWarn) << "View specified an invalid display state, ignoring it"; + LOG(LogLevel::kWarn).session(getSession()) << "View specified an invalid display state, ignoring it"; return; } @@ -142,6 +142,13 @@ RootContext::updateDisplayState(DisplayState displayState) 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 @@ -157,8 +164,8 @@ RootContext::reinflate() auto config = mActiveConfigurationChanges.mergeRootConfig(mCore->mConfig); // Update the configuration with the current UTC time and time adjustment - config.utcTime(mUTCTime); - config.localTimeAdjustment(mLocalTimeAdjustment); + config.set(RootProperty::kUTCTime, mUTCTime); + config.set(RootProperty::kLocalTimeAdjustment, mLocalTimeAdjustment); // Stop any execution on the old core auto oldTop = mCore->halt(); @@ -320,7 +327,7 @@ RootContext::popEvent() } // This should never be reached. - LOG(LogLevel::kError) << "No events available"; + LOG(LogLevel::kError).session(getSession()) << "No events available"; std::exit(EXIT_FAILURE); } @@ -371,7 +378,7 @@ rapidjson::Value RootContext::serializeVisualContext(rapidjson::Document::AllocatorType& allocator) { clearVisualContextDirty(); - return topComponent()->serializeVisualContext(allocator); + return mCore->mTop->serializeVisualContext(allocator); } bool @@ -462,7 +469,7 @@ RootContext::invokeExtensionEventHandler(const std::string& uri, const std::stri if (comp) { handler = comp->findHandler(handlerDefinition); if (handler.isNull()) { - CONSOLE_S(getSession()) << "Extension Component " << comp->name() + CONSOLE(getSession()) << "Extension Component " << comp->name() << " can't execute event handler " << handlerDefinition.getName(); return nullptr; } @@ -473,7 +480,7 @@ RootContext::invokeExtensionEventHandler(const std::string& uri, const std::stri } else { handler = mCore->extensionManager().findHandler(handlerDefinition); if (handler.isNull()) { - CONSOLE_S(getSession()) << "Extension Handler " << handlerDefinition.getName() << " don't exist."; + CONSOLE(getSession()) << "Extension Handler " << handlerDefinition.getName() << " don't exist."; return nullptr; } @@ -718,7 +725,7 @@ RootContext::setup(const CoreComponentPtr& top) if (h != json.MemberEnd()) { auto oldHandler = em.findHandler(handler.second); if (!oldHandler.isNull()) - CONSOLE_CTP(mContext) << "Overwriting existing command handler " << handler.first; + CONSOLE(mContext) << "Overwriting existing command handler " << handler.first; em.addEventHandler(handler.second, asCommand(*mContext, evaluate(*mContext, h->value))); } } @@ -822,7 +829,7 @@ RootContext::verifyAPLVersionCompatibility(const std::vector& ordere { for(const auto& child : ordered) { if(!compatibilityVersion.isValid(child->version())) { - CONSOLE_CTP(mContext) << child->name() << " has invalid version: " << child->version(); + CONSOLE(mContext) << child->name() << " has invalid version: " << child->version(); return false; } } @@ -834,10 +841,10 @@ RootContext::verifyTypeField(const std::vector>& ordere { for(auto& child : ordered) { auto type = child->type(); - if (type.compare("APML") == 0) CONSOLE_CTP(mContext) + if (type.compare("APML") == 0) CONSOLE(mContext) << child->name() << ": Stop using the APML document format!"; else if (type.compare("APL") != 0) { - CONSOLE_CTP(mContext) << child->name() << ": Document type field should be \"APL\"!"; + CONSOLE(mContext) << child->name() << ": Document type field should be \"APL\"!"; if(enforce) { return false; } @@ -929,7 +936,7 @@ RootContext::setFocus(FocusDirection direction, const Rect& origin, const std::s auto target = std::dynamic_pointer_cast(findComponentById(targetId)); if (!target) { - LOG(LogLevel::kWarn) << "Don't have component: " << targetId; + LOG(LogLevel::kWarn).session(getSession()) << "Don't have component: " << targetId; return false; } diff --git a/aplcore/src/engine/styledefinition.cpp b/aplcore/src/engine/styledefinition.cpp index d6811ce..58e3e3e 100644 --- a/aplcore/src/engine/styledefinition.cpp +++ b/aplcore/src/engine/styledefinition.cpp @@ -47,12 +47,12 @@ StyleDefinition::extendWithStyle(const StyleDefinitionPtr& extend) const StyleInstancePtr StyleDefinition::get(const ContextPtr& context, const State& state) { - LOG_IF(DEBUG_STYLES) << "StyleDefinition::get " << state; + LOG_IF(DEBUG_STYLES).session(context) << "StyleDefinition::get " << state; auto it = mCache.find(state); if (it != mCache.end()) return it->second; - LOG_IF(DEBUG_STYLES) << "Constructing style"; + LOG_IF(DEBUG_STYLES).session(context) << "Constructing style"; StyleInstancePtr ptr = std::make_shared(mStyleProvenance); // Build extensions in order diff --git a/aplcore/src/engine/styles.cpp b/aplcore/src/engine/styles.cpp index 5d7cbf2..af880e5 100644 --- a/aplcore/src/engine/styles.cpp +++ b/aplcore/src/engine/styles.cpp @@ -55,10 +55,10 @@ class StyleProcessSet { } StyleDefinitionPtr addOne(const std::string& name) { - LOG_IF(DEBUG_STYLES) << "Styles::addOne " << name; + LOG_IF(DEBUG_STYLES).session(mSession) << "Styles::addOne " << name; if (mInProcess.find(name) != mInProcess.end()) { - CONSOLE_S(mSession) << "Loop in style specification with " << name; + CONSOLE(mSession) << "Loop in style specification with " << name; return nullptr; } @@ -78,12 +78,12 @@ class StyleProcessSet { } StyleDefinitionPtr buildStyle(const std::string& name) { - LOG_IF(DEBUG_STYLES) << name; + LOG_IF(DEBUG_STYLES).session(mSession) << name; const rapidjson::Value& value = mJson->GetObject()[name.c_str()]; StyleDefinitionPtr styledef = std::make_shared(value, mPath.addObject(name)); - LOG_IF(DEBUG_STYLES) << " extend, extends"; + LOG_IF(DEBUG_STYLES).session(mSession) << " extend, extends"; for (auto&& m : arrayifyProperty(value, "extend", "extends")) { if (!m.IsString()) continue; @@ -108,13 +108,13 @@ class StyleProcessSet { const StyleInstancePtr Styles::get(const ContextPtr& context, const std::string& name, const State& state) { - LOG_IF(DEBUG_STYLES) << "Styles::get " << name << " " << state; + LOG_IF(DEBUG_STYLES).session(context) << "Styles::get " << name << " " << state; auto definition = getStyleDefinition(name); if (definition) return definition->get(context, state); - LOG_IF(DEBUG_STYLES) << "Didn't find anything"; + LOG_IF(DEBUG_STYLES).session(context) << "Didn't find anything"; return nullptr; } diff --git a/aplcore/src/extension/CMakeLists.txt b/aplcore/src/extension/CMakeLists.txt index ecfd681..f675f67 100644 --- a/aplcore/src/extension/CMakeLists.txt +++ b/aplcore/src/extension/CMakeLists.txt @@ -17,4 +17,5 @@ target_sources_local(apl extensionmanager.cpp extensionmediator.cpp extensioncomponent.cpp + extensionsession.cpp ) \ No newline at end of file diff --git a/aplcore/src/extension/extensionclient.cpp b/aplcore/src/extension/extensionclient.cpp index d1f5101..ae695ad 100644 --- a/aplcore/src/extension/extensionclient.cpp +++ b/aplcore/src/extension/extensionclient.cpp @@ -93,7 +93,7 @@ rapidjson::Value ExtensionClient::createRegistrationRequest(rapidjson::Document::AllocatorType& allocator, Content& content) { auto rootConfig = mRootConfig.lock(); if (!rootConfig) { - LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; + LOG(LogLevel::kError).session(content.getSession()) << ROOT_CONFIG_MISSING; return rapidjson::Value(rapidjson::kNullType); } @@ -127,6 +127,12 @@ ExtensionClient::registered() return mRegistered; } +bool +ExtensionClient::registrationFailed() +{ + return mRegistrationProcessed && !mRegistered; +} + std::string ExtensionClient::getConnectionToken() const { @@ -135,7 +141,6 @@ ExtensionClient::getConnectionToken() const void ExtensionClient::bindContext(const RootContextPtr& rootContext) { - LOG_IF(DEBUG_EXTENSION_CLIENT) << "connection: " << mConnectionToken; if (rootContext) { mCachedContext = rootContext; flushPendingEvents(rootContext); @@ -143,21 +148,22 @@ ExtensionClient::bindContext(const RootContextPtr& rootContext) { LOG(LogLevel::kError) << "Can't bind Client to non-existent RootContext."; return; } + LOG_IF(DEBUG_EXTENSION_CLIENT).session(rootContext) << "connection: " << mConnectionToken; } bool ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& message) { - LOG_IF(DEBUG_EXTENSION_CLIENT) << "Connection: " << mConnectionToken << " message: " << message.toString(); - auto rootConfig = mRootConfig.lock(); if (!rootConfig) { LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; return false; } + LOG_IF(DEBUG_EXTENSION_CLIENT).session(rootContext) << "Connection: " << mConnectionToken << " message: " << message.toString(); + if (!message) { - CONSOLE_CFGP(rootConfig).log("Malformed offset=%u: %s.", message.offset(), message.error()); + CONSOLE(rootConfig).log("Malformed offset=%u: %s.", message.offset(), message.error()); return false; } @@ -167,10 +173,10 @@ ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& me if (!mRegistered) { if (mRegistrationProcessed) { - CONSOLE_CFGP(rootConfig).log("Can't process message after failed registration."); + CONSOLE(rootConfig).log("Can't process message after failed registration."); return false; } else if (method != kExtensionMethodRegisterSuccess && method != kExtensionMethodRegisterFailure) { - CONSOLE_CFGP(rootConfig).log("Can't process message before registration."); + CONSOLE(rootConfig).log("Can't process message before registration."); return false; } } @@ -181,7 +187,7 @@ ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& me auto version = propertyAsObject(context, evaluated, "version"); if (version.isNull() || version.getString() != IMPLEMENTED_INTERFACE_VERSION) { - CONSOLE_CFGP(rootConfig) << "Interface version is wrong. Expected=" << IMPLEMENTED_INTERFACE_VERSION + CONSOLE(rootConfig) << "Interface version is wrong. Expected=" << IMPLEMENTED_INTERFACE_VERSION << "; Actual=" << version.toDebugString(); return false; } @@ -190,6 +196,7 @@ ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& me switch (method) { case kExtensionMethodRegisterSuccess: result = processRegistrationResponse(context, evaluated); + // FALL_THROUGH case kExtensionMethodRegisterFailure: mRegistrationProcessed = true; break; @@ -211,7 +218,7 @@ ExtensionClient::processMessage(const RootContextPtr& rootContext, JsonData&& me result = processComponentResponse(context, evaluated); break; default: - CONSOLE_CFGP(rootConfig).log("Unknown method"); + CONSOLE(rootConfig).log("Unknown method"); result = false; break; } @@ -229,25 +236,27 @@ ExtensionClient::processRegistrationResponse(const Context& context, const Objec } if (mRegistered) { - CONSOLE_CFGP(rootConfig).log("Can't register extension twice."); + CONSOLE(rootConfig).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_CFGP(rootConfig).log("Malformed connection response message."); + CONSOLE(rootConfig).log("Malformed connection response message."); return false; } if (!readExtension(context, schema)) { - CONSOLE_CFGP(rootConfig).log("Malformed schema."); + CONSOLE(rootConfig).log("Malformed schema."); return false; } const auto& assignedToken = connectionToken.getString(); if (assignedToken == "") { - mConnectionToken = Random::generateToken(mUri); + if (mConnectionToken.empty()) { + mConnectionToken = Random::generateToken(mUri); + } } else { mConnectionToken = assignedToken; } @@ -274,20 +283,20 @@ ExtensionClient::processEvent(const Context& context, const Object& event) auto name = propertyAsObject(context, event, "name"); if (!name.isString() || name.empty() || (mEventModes.find(name.getString()) == mEventModes.end())) { - CONSOLE_CFGP(rootConfig) << "Invalid extension event name for extension=" << mUri + CONSOLE(rootConfig) << "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_CFGP(rootConfig) << "Invalid extension event target for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid extension event target for extension=" << mUri; return false; } auto payload = propertyAsRecursive(context, event, "payload"); if (!payload.isNull() && !payload.isMap()) { - CONSOLE_CFGP(rootConfig) << "Invalid extension event data for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid extension event data for extension=" << mUri; return false; } @@ -310,26 +319,26 @@ ExtensionClient::processCommand(rapidjson::Document::AllocatorType& allocator, c } if (kEventTypeExtension != event.getType()) { - CONSOLE_CFGP(rootConfig) << "Invalid extension command type for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid extension command type for extension=" << mUri; return rapidjson::Value(rapidjson::kNullType); } auto extensionURI = event.getValue(kEventPropertyExtensionURI); if (!extensionURI.isString() || extensionURI.getString() != mUri) { - CONSOLE_CFGP(rootConfig) << "Invalid extension command target for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid extension command target for extension=" << mUri; return rapidjson::Value(rapidjson::kNullType); } auto commandName = event.getValue(kEventPropertyName); if (!commandName.isString() || commandName.empty()) { - CONSOLE_CFGP(rootConfig) << "Invalid extension command name for extension=" << mUri + CONSOLE(rootConfig) << "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_CFGP(rootConfig) << "Invalid extension component handle for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid extension component handle for extension=" << mUri; return rapidjson::Value (rapidjson::kNullType); } @@ -344,7 +353,7 @@ ExtensionClient::processCommand(rapidjson::Document::AllocatorType& allocator, c result->emplace("id", id ); auto actionRef = event.getActionRef(); - if (!actionRef.isEmpty() && actionRef.isPending()) { + if (!actionRef.empty() && actionRef.isPending()) { actionRef.addTerminateCallback([this, id](const TimersPtr&) { mActionRefs.erase(id); }); @@ -372,7 +381,7 @@ ExtensionClient::processCommandResponse(const Context& context, const Object& re auto id = propertyAsObject(context, response, "id"); if (!id.isNumber() || id.getInteger() > sCommandIdGenerator) { - CONSOLE_CFGP(rootConfig) << "Invalid extension command response for extension=" << mUri << " id=" + CONSOLE(rootConfig) << "Invalid extension command response for extension=" << mUri << " id=" << id.toDebugString() << " total pending=" << mActionRefs.size(); return false; } @@ -398,12 +407,12 @@ void ExtensionClient::liveDataObjectFlushed(const std::string& key, LiveDataObject& liveDataObject) { if (!mLiveData.count(key)) { - LOG(LogLevel::kWarn) << "Received update for unhandled LiveData " << key; + LOG(LogLevel::kWarn).session(mRootConfig.lock()) << "Received update for unhandled LiveData " << key; return; } auto ref = mLiveData.at(key); - LOG_IF(DEBUG_EXTENSION_CLIENT) << " connection: " << mConnectionToken + LOG_IF(DEBUG_EXTENSION_CLIENT).session(mRootConfig.lock()) << " connection: " << mConnectionToken << ", key: " << key << ", ref.name: " << ref.name << ", type: " << ref.type; @@ -536,19 +545,19 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd auto name = propertyAsObject(context, update, "name"); if (!name.isString() || name.empty() || (mLiveData.find(name.getString()) == mLiveData.end())) { - CONSOLE_CFGP(rootConfig) << "Invalid LiveData name for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid LiveData name for extension=" << mUri; return false; } auto target = propertyAsObject(context, update, "target"); if (!target.isString() || target.empty() || target.getString() != mUri) { - CONSOLE_CFGP(rootConfig) << "Invalid LiveData target for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid LiveData target for extension=" << mUri; return false; } auto operations = propertyAsRecursive(context, update, "operations"); if (!operations.isArray()) { - CONSOLE_CFGP(rootConfig) << "Invalid LiveData operations for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid LiveData operations for extension=" << mUri; return false; } @@ -557,7 +566,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_CFGP(rootConfig) << "Wrong operation type for=" << name; + CONSOLE(rootConfig) << "Wrong operation type for=" << name; return false; } @@ -571,12 +580,12 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd break; default: result = false; - CONSOLE_CFGP(rootConfig) << "Unknown LiveObject type=" << dataRef.objectType << " for " << dataRef.name; + CONSOLE(rootConfig) << "Unknown LiveObject type=" << dataRef.objectType << " for " << dataRef.name; break; } if (!result) { - CONSOLE_CFGP(rootConfig) << "LiveMap operation failed=" << dataRef.name << " operation=" + CONSOLE(rootConfig) << "LiveMap operation failed=" << dataRef.name << " operation=" << sExtensionLiveDataUpdateTypeBimap.at(type); } else { dataRef.hasPendingUpdate = true; @@ -597,7 +606,7 @@ ExtensionClient::updateLiveMap(ExtensionLiveDataUpdateType type, const LiveDataR std::string triggerEvent; auto keyObj = operation.opt("key", ""); if (keyObj.empty()) { - CONSOLE_CFGP(rootConfig) << "Invalid LiveData key for=" << dataRef.name; + CONSOLE(rootConfig) << "Invalid LiveData key for=" << dataRef.name; return false; } const auto& key = keyObj.getString(); @@ -615,7 +624,7 @@ ExtensionClient::updateLiveMap(ExtensionLiveDataUpdateType type, const LiveDataR result = liveMap->remove(key); break; default: - CONSOLE_CFGP(rootConfig) << "Unknown operation for=" << dataRef.name; + CONSOLE(rootConfig) << "Unknown operation for=" << dataRef.name; return false; } @@ -635,13 +644,13 @@ ExtensionClient::updateLiveArray(ExtensionLiveDataUpdateType type, const LiveDat auto item = operation.get("item"); if (item.isNull() && (type != kExtensionLiveDataUpdateTypeRemove && type != kExtensionLiveDataUpdateTypeClear)) { - CONSOLE_CFGP(rootConfig) << "Malformed items on LiveData update for=" << dataRef.name; + CONSOLE(rootConfig) << "Malformed items on LiveData update for=" << dataRef.name; return false; } auto indexObj = operation.opt("index", -1); if (!indexObj.isNumber() && type != kExtensionLiveDataUpdateTypeClear) { - CONSOLE_CFGP(rootConfig) << "Invalid LiveData index for=" << dataRef.name; + CONSOLE(rootConfig) << "Invalid LiveData index for=" << dataRef.name; return false; } auto index = indexObj.getInteger(); @@ -671,7 +680,7 @@ ExtensionClient::updateLiveArray(ExtensionLiveDataUpdateType type, const LiveDat liveArray->clear(); break; default: - CONSOLE_CFGP(rootConfig) << "Unknown operation for=" << dataRef.name; + CONSOLE(rootConfig) << "Unknown operation for=" << dataRef.name; return false; } @@ -691,14 +700,14 @@ ExtensionClient::readExtension(const Context& context, const Object& extension) auto schema = propertyAsString(context, extension, "type"); auto version = propertyAsString(context, extension, "version"); if (schema != "Schema" || version.compare(MAX_SUPPORTED_SCHEMA_VERSION) > 0) { - CONSOLE_CFGP(rootConfig) << "Unsupported extension schema version:" << version; + CONSOLE(rootConfig) << "Unsupported extension schema version:" << version; return false; } // register extension based on URI auto uriObj = propertyAsObject(context, extension, "uri"); if (!uriObj.isString() || uriObj.empty()) { - CONSOLE_CFGP(rootConfig).log("Missing or invalid extension URI."); + CONSOLE(rootConfig).log("Missing or invalid extension URI."); return false; } const auto& uri = uriObj.getString(); @@ -746,7 +755,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) } if (!types.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); + CONSOLE(rootConfig).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); return false; } @@ -754,7 +763,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_CFGP(rootConfig).log("Invalid extension type for extension=%s", + CONSOLE(rootConfig).log("Invalid extension type for extension=%s", mUri.c_str()); continue; } @@ -767,7 +776,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) if (extendedType != mTypes.end()) { properties->insert(extendedType->second->begin(), extendedType->second->end()); } else { - CONSOLE_CFGP(rootConfig) << "Unknown type to extend=" << extended + CONSOLE(rootConfig) << "Unknown type to extend=" << extended << " for type=" << name.getString() << " for extension=" << mUri.c_str(); } @@ -784,7 +793,7 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) if (ps.isString()) { ptype = sBindingMap.get(ps.getString(), kBindingTypeAny); } else if (!ps.has("type")) { - CONSOLE_CFGP(rootConfig).log("Invalid extension property for type=%s extension=%s", + CONSOLE(rootConfig).log("Invalid extension property for type=%s extension=%s", name.getString().c_str(), mUri.c_str()); continue; } else { @@ -820,7 +829,7 @@ ExtensionClient::readCommandDefinitionsInternal(const Context& context,const Obj // create a command auto name = propertyAsObject(context, command, "name"); if (!name.isString() || name.empty()) { - CONSOLE_CFGP(rootConfig).log("Invalid extension command for extension=%s", mUri.c_str()); + CONSOLE(rootConfig).log("Invalid extension command for extension=%s", mUri.c_str()); continue; } auto commandName = name.asString(); @@ -844,7 +853,7 @@ ExtensionClient::readCommandDefinitionsInternal(const Context& context,const Obj } if (!mTypes.count(type)) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed `payload` block for command=%s", + CONSOLE(rootConfig).log("The extension name=%s has a malformed `payload` block for command=%s", mUri.c_str(), commandName.c_str()); continue; } @@ -872,7 +881,7 @@ ExtensionClient::readExtensionCommandDefinitions(const Context& context, const O } if (!commands.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); + CONSOLE(rootConfig).log("The extension name=%s has a malformed 'commands' block", mUri.c_str()); return false; } readCommandDefinitionsInternal(context, commands.getArray()); @@ -890,7 +899,7 @@ ExtensionClient::readExtensionComponentCommandDefinitions(const Context& context } if (!commands.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension component name=%s has a malformed 'commands' block", mUri.c_str()); + CONSOLE(rootConfig).log("The extension component name=%s has a malformed 'commands' block", mUri.c_str()); return; } // TODO: Remove when customers stopped using it. @@ -907,14 +916,14 @@ ExtensionClient::readExtensionEventHandlers(const Context& context, const Object } if (!handlers.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed 'events' block", mUri.c_str()); + CONSOLE(rootConfig).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_CFGP(rootConfig).log("Invalid extension event handler for extension=%s", mUri.c_str()); + CONSOLE(rootConfig).log("Invalid extension event handler for extension=%s", mUri.c_str()); return false; } else { auto mode = propertyAsMapped(context, handler, "mode", @@ -937,20 +946,20 @@ ExtensionClient::readExtensionLiveData(const Context& context, const Object& liv } if (!liveData.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed 'dataBindings' block", mUri.c_str()); + CONSOLE(rootConfig).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_CFGP(rootConfig).log("Invalid extension data binding for extension=%s", mUri.c_str()); + CONSOLE(rootConfig).log("Invalid extension data binding for extension=%s", mUri.c_str()); return false; } auto typeDef = propertyAsObject(context, binding, "type"); if (!typeDef.isString()) { - CONSOLE_CFGP(rootConfig).log("Invalid extension data binding type for extension=%s", mUri.c_str()); + CONSOLE(rootConfig).log("Invalid extension data binding type for extension=%s", mUri.c_str()); return false; } @@ -964,7 +973,7 @@ ExtensionClient::readExtensionLiveData(const Context& context, const Object& liv if (!(mTypes.count(type) // Any LiveData may use defined complex types || (isArray && sBindingMap.has(type)))) { // Arrays also may use primitive types - CONSOLE_CFGP(rootConfig).log("Data type=%s, for LiveData=%s is invalid", type.c_str(), name.getString().c_str()); + CONSOLE(rootConfig).log("Data type=%s, for LiveData=%s is invalid", type.c_str(), name.getString().c_str()); continue; } @@ -1076,7 +1085,7 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, if (!handlers.isNull()) { if (!handlers.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed 'events' block", + CONSOLE(rootConfig).log("The extension name=%s has a malformed 'events' block", mUri.c_str()); return false; } @@ -1084,7 +1093,7 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, for (const auto& handler : handlers.getArray()) { auto name = propertyAsObject(context, handler, "name"); if (!name.isString() || name.empty()) { - CONSOLE_CFGP(rootConfig).log("Invalid extension event handler for extension=%s", + CONSOLE(rootConfig).log("Invalid extension event handler for extension=%s", mUri.c_str()); return false; } @@ -1111,14 +1120,14 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const } if (!components.isArray()) { - CONSOLE_CFGP(rootConfig).log("The extension name=%s has a malformed 'components' block", mUri.c_str()); + CONSOLE(rootConfig).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_CFGP(rootConfig).log("Invalid extension component name for extension=%s", mUri.c_str()); + CONSOLE(rootConfig).log("Invalid extension component name for extension=%s", mUri.c_str()); continue; } auto componentName = name.asString(); @@ -1155,7 +1164,7 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const if (ps.isString()) { ptype = sBindingMap.get(ps.getString(), kBindingTypeAny); } else if (!ps.has("type")) { - CONSOLE_CFGP(rootConfig).log("Invalid extension property extension=%s", mUri.c_str()); + CONSOLE(rootConfig).log("Invalid extension property extension=%s", mUri.c_str()); continue; } else { defValue = propertyAsObject(context, ps, "default"); @@ -1193,7 +1202,7 @@ ExtensionClient::createComponentChange(rapidjson::MemoryPoolAllocator<>& allocat auto extensionURI = component.getUri(); if (extensionURI != mUri) { - CONSOLE_CFGP(rootConfig) << "Invalid extension command target for extension=" << mUri; + CONSOLE(rootConfig) << "Invalid extension command target for extension=" << mUri; return rapidjson::Value(rapidjson::kNullType); } @@ -1228,7 +1237,7 @@ ExtensionClient::createComponentChange(rapidjson::MemoryPoolAllocator<>& allocat result->emplace("payload", payload); } - LOG_IF(DEBUG_EXTENSION_CLIENT) << "Component: " << Object(result); + LOG_IF(DEBUG_EXTENSION_CLIENT).session(component.getContext()) << "Component: " << Object(result); return Object(result).serialize(allocator); } @@ -1255,7 +1264,7 @@ ExtensionClient::processComponentResponse(const Context& context, const Object& LOG(LogLevel::kError) << ROOT_CONFIG_MISSING; return false; } - CONSOLE_CFGP(rootConfig) << "Unable to find component associated with :" << componentId; + CONSOLE(rootConfig) << "Unable to find component associated with :" << componentId; } return true; } @@ -1315,7 +1324,7 @@ ExtensionClient::invokeExtensionHandler(const std::string& uri, const std::strin if (rootContext) { rootContext->invokeExtensionEventHandler(uri, name, data, fastMode, resourceId); } else { - LOG(LogLevel::kWarn) << "RootContext not available"; + LOG(LogLevel::kWarn).session(mRootConfig.lock()) << "RootContext not available"; ExtensionEvent event = {uri, name, data, fastMode, resourceId}; mPendingEvents.emplace_back(std::move(event)); } @@ -1324,7 +1333,7 @@ ExtensionClient::invokeExtensionHandler(const std::string& uri, const std::strin void ExtensionClient::flushPendingEvents(const RootContextPtr& rootContext) { - LOG_IF(DEBUG_EXTENSION_CLIENT && (mPendingEvents.size() > 0)) << "Flushing " << mPendingEvents.size() + LOG_IF(DEBUG_EXTENSION_CLIENT && (mPendingEvents.size() > 0)).session(rootContext) << "Flushing " << mPendingEvents.size() << " pending events for " << mConnectionToken; for (auto& event : mPendingEvents) { @@ -1337,7 +1346,7 @@ ExtensionClient::flushPendingEvents(const RootContextPtr& rootContext) auto& ref = kv.second; if (!ref.hasPendingUpdate) continue; - LOG_IF(DEBUG_EXTENSION_CLIENT) << "Simulate changes for " << ref.name << " in: " << mConnectionToken; + LOG_IF(DEBUG_EXTENSION_CLIENT).session(rootContext) << "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 2a37351..02e7e58 100644 --- a/aplcore/src/extension/extensionmanager.cpp +++ b/aplcore/src/extension/extensionmanager.cpp @@ -31,7 +31,7 @@ ExtensionManager::ExtensionManager(const std::vector(); for (const auto& m : requests) { uriToNamespace.emplace(m.second, m.first); - LOG_IF(DEBUG_EXTENSION_MANAGER) << "URI to Namespace: " << m.second << "->" << m.first; + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "URI to Namespace: " << m.second << "->" << m.first; } // Extensions that define custom commands @@ -41,7 +41,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(); mExtensionCommands.emplace(qualifiedName, m); - LOG_IF(DEBUG_EXTENSION_MANAGER) << "extension commands: " << qualifiedName << "->" + m.toDebugString(); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "extension commands: " << qualifiedName + << "->" + m.toDebugString(); } } @@ -52,7 +53,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(); mExtensionFilters.emplace(qualifiedName, m); - LOG_IF(DEBUG_EXTENSION_MANAGER) << "extension filters: " << qualifiedName << "->" + m.toDebugString(); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "extension filters: " << qualifiedName + << "->" + m.toDebugString(); } } @@ -61,7 +63,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(), m); - LOG_IF(DEBUG_EXTENSION_MANAGER) << "qualified handlers: " << it->second + ":" + m.getName() << "->" << m.toDebugString(); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "qualified handlers: " + << it->second + ":" + m.getName() << "->" << m.toDebugString(); } } @@ -74,10 +77,12 @@ ExtensionManager::ExtensionManager(const std::vectoremplace(m.first, cfg);// Add the NAME. The URI should already be there. - LOG_IF(DEBUG_EXTENSION_MANAGER) << "requestedEnvironment: " << m.first << "->" << cfg.toDebugString(); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "requestedEnvironment: " << m.first + << "->" << cfg.toDebugString(); } else { mEnvironment->emplace(m.first, Object::FALSE_OBJECT()); - LOG_IF(DEBUG_EXTENSION_MANAGER) << "requestedEnvironment: " << m.first << "->" << false; + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "requestedEnvironment: " << m.first + << "->" << false; } } @@ -88,7 +93,8 @@ ExtensionManager::ExtensionManager(const std::vectorsecond + ":" + m.getName(); mExtensionComponentDefs.emplace(qualifiedName, m); - LOG_IF(DEBUG_EXTENSION_MANAGER) << "extension component: " << qualifiedName << "->" + m.toDebugString(); + LOG_IF(DEBUG_EXTENSION_MANAGER).session(rootConfig) << "extension component: " << qualifiedName + << "->" + m.toDebugString(); } } } diff --git a/aplcore/src/extension/extensionmediator.cpp b/aplcore/src/extension/extensionmediator.cpp index 7169155..6bd0b20 100644 --- a/aplcore/src/extension/extensionmediator.cpp +++ b/aplcore/src/extension/extensionmediator.cpp @@ -15,8 +15,11 @@ #ifdef ALEXAEXTENSIONS +#include #include #include +#include +#include #include @@ -31,6 +34,281 @@ namespace apl { static const bool DEBUG_EXTENSION_MEDIATOR = false; +/** + * Possible lifecycle states for extensions. A session manages a state machine for each registered + * extension (expressed as an activity). State transitions are typically used to ensure that + * the appropriate notifications are sent to extensions exactly once. + */ +enum class ExtensionLifecycleStage { + /** + * Not part of the state machine. Only used to report the state of an unknown extension. + */ + kExtensionUnknown, + + /** + * Initial state for a requested extension + */ + kExtensionInitialized, + + /** + * Assigned to an extension once registration has successfully been completed. + */ + kExtensionRegistered, + + /** + * Assigned to an extension once it was notified that the current document unregistered. + * + * This is a terminal state. + */ + kExtensionUnregistered, + + /** + * Assigned to an extension that has reached terminal state without going through a successful + * registration. For example, the extension was denied, or an error was encountered before + * registration could be completed. + * + * This is a terminal state. + */ + kExtensionFinalized, +}; + +enum class SessionLifecycleStage { + kSessionStarted, + kSessionEnding, + kSessionEnded, +}; + +/** + * Describes the state of a specific extension within a session. + */ +struct ActivityState { + ActivityState(const ActivityDescriptorPtr& activity) + : activity(activity), + state(ExtensionLifecycleStage::kExtensionInitialized) + {} + + ActivityDescriptorPtr activity; + ExtensionLifecycleStage state; +}; + +struct ExtensionState { + ~ExtensionState() { + proxy = nullptr; + activities.clear(); + } + + SessionLifecycleStage state; + alexaext::ExtensionProxyPtr proxy; + std::vector activities; +}; + +/** + * Defines the state of all extensions within a session. + */ +class ExtensionSessionState final { +public: + ExtensionSessionState(const alexaext::SessionDescriptorPtr& sessionDescriptor) + : mSessionDescriptor(sessionDescriptor) + {} + ~ExtensionSessionState() = default; + + /** + * Ends the specified session + * + * @param session The session that ended + */ + void endSession() { + if (mSessionState == SessionLifecycleStage::kSessionEnded) { + /* The session has already ended, and the notification has been sent to all associated + * extensions, so simply ignore this duplicate signal */ + return; + } + + alexaext::ExtensionProxyPtr proxy = nullptr; + + mSessionState = SessionLifecycleStage::kSessionEnding; + + bool allEnded = true; + for (const auto& entry : mExtensionStateByURI) { + if (!tryEndSession(*entry.second)) { + allEnded = false; + } + } + + if (allEnded) { + mSessionState = SessionLifecycleStage::kSessionEnded; + } + } + + /** + * Returns the state of the extension with the specified URI. + * + * @param uri The extension URI + * @return The current state for this extension, or unknown if not found. + */ + ExtensionLifecycleStage getState(const ActivityDescriptorPtr& activity) { + auto it = mExtensionStateByURI.find(activity->getURI()); + if (it != mExtensionStateByURI.end()) { + for (const auto& entry : it->second->activities) { + if (*entry.activity == *activity) { + return entry.state; + } + } + } + + return ExtensionLifecycleStage::kExtensionUnknown; + } + + /** + * Initialize the specified extension + * @param activity The extension activity + * @param proxy The proxy used to communicate with the extension + */ + void initialize(const ActivityDescriptorPtr& activity, const alexaext::ExtensionProxyPtr& proxy) { + if (mSessionState == SessionLifecycleStage::kSessionEnding + || mSessionState == SessionLifecycleStage::kSessionEnded) { + // The session is ending or has ended, prevent new extensions from being initialized + LOG(LogLevel::kWarn) << "Ignoring attempt to initialize extension in inactive session, uri: " << activity->getURI(); + return; + } + + auto it = mExtensionStateByURI.find(activity->getURI()); + if (it == mExtensionStateByURI.end()) { + // This is the first activity for this extension, initialize the state + auto state = std::make_shared(); + state->proxy = proxy; + state->state = SessionLifecycleStage::kSessionStarted; + + state->activities.emplace_back(ActivityState(activity)); + + mExtensionStateByURI.emplace(activity->getURI(), state); + + // Notify the extension that a session has started. This is only done + // after at least one extension is requested during the session to cut down on + // unnecessary noise + proxy->onSessionStarted(*mSessionDescriptor); + } else { + for (const auto& entry : it->second->activities) { + if (*entry.activity == *activity) { + // This activity was already registered + LOG(LogLevel::kWarn) << "Ignoring attempt to re-initialize extension, uri: " << activity->getURI(); + return; + } + } + + it->second->activities.emplace_back(ActivityState(activity)); + } + } + + /** + * Attempts to update the state of the specified extension. + * + * @param uri The extension URI + * @param lifecycleStage The new extension state + * @return @c true if the update succeeded, @c false if the state transition isn't permitted. + */ + bool updateState(const alexaext::ActivityDescriptorPtr& activity, ExtensionLifecycleStage lifecycleStage) { + auto it = mExtensionStateByURI.find(activity->getURI()); + if (it != mExtensionStateByURI.end()) { + for (auto& entry : it->second->activities) { + if (*entry.activity == *activity && canUpdate(entry.state, lifecycleStage)) { + entry.state = lifecycleStage; + + if (lifecycleStage == ExtensionLifecycleStage::kExtensionFinalized + && mSessionState == SessionLifecycleStage::kSessionEnding) { + // An activity was just finalized, and there was a previous attempt to end + // the session that could not be successfully completed. Force a check to + // see if the session can now end + endSession(); + } + + return true; + } + } + } + + return false; + } + +private: + bool tryEndSession(ExtensionState& state) { + if (state.state == SessionLifecycleStage::kSessionEnded) { + // Already ended the session for this URI, nothing to do + return true; + } + + for (const auto& activity : state.activities) { + if (activity.state == ExtensionLifecycleStage::kExtensionInitialized + || activity.state == ExtensionLifecycleStage::kExtensionRegistered) { + // Found an extension that hasn't been unregistered yet, so don't end the session + return false; + } + } + + state.state = SessionLifecycleStage::kSessionEnded; + state.proxy->onSessionEnded(*mSessionDescriptor); + + return true; + } + + /** + * Determines if a state transition is permitted. + * + * @param fromState The current state + * @param toState The candidate state + * @return @c true if the state transition is allowed, @c false otherwise + */ + bool canUpdate(ExtensionLifecycleStage fromState, ExtensionLifecycleStage toState) { + switch (fromState) { + case ExtensionLifecycleStage::kExtensionInitialized: + return toState == ExtensionLifecycleStage::kExtensionRegistered + || toState == ExtensionLifecycleStage::kExtensionFinalized; + case ExtensionLifecycleStage::kExtensionRegistered: + return toState == ExtensionLifecycleStage::kExtensionUnregistered; + case ExtensionLifecycleStage::kExtensionUnregistered: // terminal state + case ExtensionLifecycleStage::kExtensionFinalized: // terminal state + default: + return false; + } + } + +private: + alexaext::SessionDescriptorPtr mSessionDescriptor; + SessionLifecycleStage mSessionState = SessionLifecycleStage::kSessionStarted; + std::unordered_map> mExtensionStateByURI; +}; + +ExtensionMediator::ExtensionMediator(const ExtensionProviderPtr& provider, + const ExtensionResourceProviderPtr& resourceProvider, + const ExecutorPtr& messageExecutor) + : mProvider(provider), + mResourceProvider(resourceProvider), + mMessageExecutor(messageExecutor) +{} + +ExtensionMediator::ExtensionMediator(const ExtensionProviderPtr& provider, + const ExtensionResourceProviderPtr& resourceProvider, + const ExecutorPtr& messageExecutor, + const ExtensionSessionPtr& extensionSession) + : mProvider(provider), + mResourceProvider(resourceProvider), + mMessageExecutor(messageExecutor), + mExtensionSession(extensionSession) +{ + if (mExtensionSession && mExtensionSession->getSessionDescriptor()) { + auto sessionState = extensionSession->getSessionState(); + if (!sessionState) { + sessionState = std::make_shared(extensionSession->getSessionDescriptor()); + extensionSession->setSessionState(sessionState); + extensionSession->onSessionEnded([](ExtensionSession& session) { + if (auto state = session.getSessionState()) { + state->endSession(); + } + }); + } + } +} + // TODO // TODO Experimental class. This represents the integration point between the Alexa Extension library, // TODO and the existing extension messaging client. This class will change significantly. @@ -45,6 +323,7 @@ ExtensionMediator::bindContext(const RootContextPtr& context) for (auto& client : mClients) { client.second->bindContext(context); } + onDisplayStateChanged(context->getDisplayState()); } void @@ -62,7 +341,7 @@ ExtensionMediator::initializeExtensions(const RootConfigPtr& rootConfig, const C for (const auto& uri : uris) { if (mPendingRegistrations.count(uri)) continue; - LOG_IF(DEBUG_EXTENSION_MEDIATOR) << "initialize extension: " << uri + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "initialize extension: " << uri << " has extension: " << extensionProvider->hasExtension(uri); mPendingGrants.insert(uri); @@ -74,9 +353,9 @@ ExtensionMediator::initializeExtensions(const RootConfigPtr& rootConfig, const C if (auto mediator = weak_this.lock()) mediator->grantExtension(weak_config.lock(), grantedUri); }, - [weak_this](const std::string& deniedUri) { + [weak_this, weak_config](const std::string& deniedUri) { if (auto mediator = weak_this.lock()) - mediator->denyExtension(deniedUri); + mediator->denyExtension(weak_config.lock(), deniedUri); }); } else { // auto-grant when no grant handler @@ -90,7 +369,7 @@ ExtensionMediator::grantExtension(const RootConfigPtr& rootConfig, const std::st if (!mPendingGrants.count(uri)) return; - LOG_IF(DEBUG_EXTENSION_MEDIATOR) << "Extension granted: " << uri; + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "Extension granted: " << uri; mPendingGrants.erase(uri); auto extensionProvider = mProvider.lock(); @@ -102,25 +381,30 @@ ExtensionMediator::grantExtension(const RootConfigPtr& rootConfig, const std::st // First get will call initialize. auto proxy = extensionProvider->getExtension(uri); if (!proxy) { - CONSOLE_S(rootConfig->getSession()) << "Failed to retrieve proxy for extension: " << uri; + CONSOLE(rootConfig) << "Failed to retrieve proxy for extension: " << uri; return; } // create a client for message processing auto client = ExtensionClient::create(rootConfig, uri); if (!client) { - CONSOLE_S(rootConfig->getSession()) << "Failed to create client for extension: " << uri; + CONSOLE(rootConfig) << "Failed to create client for extension: " << uri; return; } + auto activity = getActivity(uri); mClients.emplace(uri, client); mPendingRegistrations.insert(uri); + + if (auto sessionState = getExtensionSessionState()) { + sessionState->initialize(activity, proxy); + } } } void -ExtensionMediator::denyExtension(const std::string& uri) { +ExtensionMediator::denyExtension(const RootConfigPtr& rootConfig, const std::string& uri) { mPendingGrants.erase(uri); - LOG_IF(DEBUG_EXTENSION_MEDIATOR) << "Extension denied: " << uri; + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "Extension denied: " << uri; } void @@ -128,7 +412,7 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const { if (!mPendingGrants.empty()) { - LOG(LogLevel::kWarn) << "Loading extensions with pending grant requests. " + LOG(LogLevel::kWarn).session(content->getSession()) << "Loading extensions with pending grant requests. " << "Failure to grant extension use makes the extension unavailable for the session."; mPendingGrants.clear(); } @@ -145,13 +429,28 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const auto session = rootConfig->getSession(); auto pendingRegistrations = mPendingRegistrations; for (const auto& uri : pendingRegistrations) { - LOG_IF(DEBUG_EXTENSION_MEDIATOR) << "load extension: " << uri + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(rootConfig) << "load extension: " << uri << " has extension: " << extensionProvider->hasExtension(uri); + + auto activity = getActivity(uri); + auto sessionState = getExtensionSessionState(); + + if (sessionState && sessionState->getState(activity) != ExtensionLifecycleStage::kExtensionInitialized) { + LOG(LogLevel::kError) << "Ignoring registration for uninitialized extension: " << uri; + mPendingRegistrations.erase(uri); + continue; + } + // Get the extension from the registration auto proxy = extensionProvider->getExtension(uri); if (!proxy) { - CONSOLE_S(session) << "Failed to retrieve proxy for extension: " << uri; + CONSOLE(session) << "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); + } continue; } @@ -165,23 +464,27 @@ ExtensionMediator::loadExtensionsInternal(const RootConfigPtr& rootConfig, const regReq.Swap(RegistrationRequest("1.0").uri(uri).settings(settings).flags(flags).getDocument()); std::weak_ptr weak_this(shared_from_this()); - auto success = proxy->getRegistration(uri, regReq, - [weak_this](const std::string& uri, - const rapidjson::Value& registrationSuccess) { - if (auto mediator = weak_this.lock()) - mediator->enqueueResponse(uri, registrationSuccess); - }, - [weak_this](const std::string& uri, - const rapidjson::Value& registrationFailure) { - if (auto mediator = weak_this.lock()) - mediator->enqueueResponse(uri, registrationFailure); - }); + auto success = proxy->getRegistration(*activity, regReq, + [weak_this, activity](const ActivityDescriptor& descriptor, const rapidjson::Value& registrationSuccess) { + if (auto mediator = weak_this.lock()) + mediator->enqueueResponse(activity, registrationSuccess); + }, + [weak_this, activity](const ActivityDescriptor& descriptor, const rapidjson::Value& registrationFailure) { + if (auto mediator = weak_this.lock()) + mediator->enqueueResponse(activity, registrationFailure); + } + ); if (!success) { - mPendingRegistrations.erase(uri); // call to extension failed without failure callback - CONSOLE_S(session) << "Extension registration failure - code: " << kErrorInvalidMessage - << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; + CONSOLE(session) << "Extension registration failure - code: " << kErrorInvalidMessage + << " message: " << sErrorMessage[kErrorInvalidMessage] + 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); + } } } @@ -197,7 +500,7 @@ ExtensionMediator::loadExtensions(const RootConfigPtr& rootConfig, const Content { mRootConfig = rootConfig; if (!content->isReady()) { - CONSOLE_CFGP(rootConfig) << "Cannot load extensions when Content is not ready"; + CONSOLE(rootConfig) << "Cannot load extensions when Content is not ready"; return; } @@ -244,7 +547,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_S(root->getSession()) << "Attempt to execute command on unavailable extension - uri: " << uri; + CONSOLE(root->getSession()) << "Attempt to execute command on unavailable extension - uri: " << uri; return false; } auto client = itr->second; @@ -252,7 +555,7 @@ ExtensionMediator::invokeCommand(const apl::Event& event) // Get the Extension auto proxy = extPro->getExtension(uri); if (!proxy) { - CONSOLE_S(root->getSession()) << "Attempt to execute command on unavailable extension - uri: " << uri; + CONSOLE(root->getSession()) << "Attempt to execute command on unavailable extension - uri: " << uri; return false; } @@ -261,21 +564,20 @@ ExtensionMediator::invokeCommand(const apl::Event& event) auto cmd = client->processCommand(document.GetAllocator(), event); std::weak_ptr weak_this = shared_from_this(); + auto activity = getActivity(uri); // Forward to the extension - auto invoke = proxy->invokeCommand(uri, cmd, - [weak_this](const std::string& uri, - const rapidjson::Value& commandSuccess) { - if (auto mediator = weak_this.lock()) - mediator->enqueueResponse(uri, commandSuccess); - }, - [weak_this](const std::string& uri, - const rapidjson::Value& commandFailure) { - if (auto mediator = weak_this.lock()) - mediator->enqueueResponse(uri, commandFailure); - }); + auto invoke = proxy->invokeCommand(*activity, cmd, + [weak_this, activity](const ActivityDescriptor& descriptor, const rapidjson::Value& commandSuccess) { + if (auto mediator = weak_this.lock()) + mediator->enqueueResponse(activity, commandSuccess); + }, + [weak_this, activity](const ActivityDescriptor& descriptor, const rapidjson::Value& commandFailure) { + if (auto mediator = weak_this.lock()) + mediator->enqueueResponse(activity, commandFailure); + }); if (!invoke) { - CONSOLE_S(root->getSession()) << "Extension command failure - code: " << kErrorInvalidMessage + CONSOLE(root->getSession()) << "Extension command failure - code: " << kErrorInvalidMessage << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; } @@ -288,7 +590,7 @@ ExtensionMediator::getProxy(const std::string &uri) auto extPro = mProvider.lock(); if (extPro == nullptr || !extPro->hasExtension(uri)) { auto config = mRootConfig.lock(); - CONSOLE_CFGP(config) << "Proxy does not exist for uri: " << uri; + CONSOLE(config) << "Proxy does not exist for uri: " << uri; return nullptr; } return extPro->getExtension(uri); @@ -300,7 +602,7 @@ ExtensionMediator::getClient(const std::string &uri) auto itr = mClients.find(uri); if (itr == mClients.end()) { auto config = mRootConfig.lock(); - CONSOLE_CFGP(config) << "Attempt to use an unavailable extension - uri: " << uri; + CONSOLE(config) << "Attempt to use an unavailable extension - uri: " << uri; return nullptr; } return itr->second; @@ -316,15 +618,17 @@ ExtensionMediator::notifyComponentUpdate(const ExtensionComponentPtr& component, if (!proxy || !client) return; + auto activity = getActivity(uri); + rapidjson::Document document; rapidjson::Value message; message = client->createComponentChange(document.GetAllocator(), *component); // Notify the extension of the component change - auto sent = proxy->sendMessage(uri, message); + auto sent = proxy->sendComponentMessage(*activity, message); if (!sent) { auto config = mRootConfig.lock(); - CONSOLE_CFGP(config) << "Extension message failure - code: " << kErrorInvalidMessage + CONSOLE(config) << "Extension message failure - code: " << kErrorInvalidMessage << " message: " << sErrorMessage[kErrorInvalidMessage] + uri; return; } @@ -359,9 +663,10 @@ ExtensionMediator::notifyComponentUpdate(const ExtensionComponentPtr& component, void ExtensionMediator::sendResourceReady(const std::string& uri, const alexaext::ResourceHolderPtr& resourceHolder) { + auto activity = getActivity(uri); if (auto proxy = getProxy(uri)) - proxy->onResourceReady(uri, resourceHolder); + proxy->onResourceReady(*activity, resourceHolder); } void @@ -369,7 +674,7 @@ ExtensionMediator::resourceFail(const ExtensionComponentPtr& component, int erro auto root = mRootContext.lock(); if (!component) return; - CONSOLE_S(root->getSession()) << "Extension resource failure - uri:" + CONSOLE(root->getSession()) << "Extension resource failure - uri:" << component->getUri() << " resourceId:" << component->getResourceID(); component->updateResourceState(kResourceError, errorCode, error); } @@ -379,53 +684,98 @@ void ExtensionMediator::registerExtension(const std::string& uri, const ExtensionProxyPtr& extension, const ExtensionClientPtr& client) { + auto activity = getActivity(uri); // set up callbacks for extension messages std::weak_ptr weak_this = shared_from_this(); + extension->registerEventCallback(*activity, + [weak_this](const alexaext::ActivityDescriptor& activity, const rapidjson::Value& event) { + if (auto mediator = weak_this.lock()) { + if (mediator->isEnabled()) { + auto activityPtr = mediator->getActivity(activity.getURI()); + mediator->enqueueResponse(activityPtr, event); + } + } else if (DEBUG_EXTENSION_MEDIATOR) { + LOG(LogLevel::kDebug) << "Mediator expired for event callback."; + } + }); + // Legacy callback for backwards compatibility with older extensions / proxies extension->registerEventCallback( [weak_this](const std::string& uri, const rapidjson::Value& event) { if (auto mediator = weak_this.lock()) { - if (mediator->isEnabled()) - mediator->enqueueResponse(uri, event); + if (mediator->isEnabled()) { + auto activityPtr = mediator->getActivity(uri); + mediator->enqueueResponse(activityPtr, event); + } } else if (DEBUG_EXTENSION_MEDIATOR) { LOG(LogLevel::kDebug) << "Mediator expired for event callback."; } }); + extension->registerLiveDataUpdateCallback(*activity, + [weak_this](const alexaext::ActivityDescriptor& activity, const rapidjson::Value& liveDataUpdate) { + if (auto mediator = weak_this.lock()) { + if (mediator->isEnabled()) { + auto activityPtr = mediator->getActivity(activity.getURI()); + mediator->enqueueResponse(activityPtr, liveDataUpdate); + } + } else if (DEBUG_EXTENSION_MEDIATOR) { + LOG(LogLevel::kDebug) << "Mediator expired for live data callback."; + } + }); + // Legacy callback for backwards compatibility with older extensions / proxies extension->registerLiveDataUpdateCallback( [weak_this](const std::string& uri, const rapidjson::Value& liveDataUpdate) { if (auto mediator = weak_this.lock()) { - if (mediator->isEnabled()) - mediator->enqueueResponse(uri, liveDataUpdate); + if (mediator->isEnabled()) { + auto activityPtr = mediator->getActivity(uri); + mediator->enqueueResponse(activityPtr, liveDataUpdate); + } } else if (DEBUG_EXTENSION_MEDIATOR) { LOG(LogLevel::kDebug) << "Mediator expired for live data callback."; } }); mClients.emplace(uri, client); - extension->onRegistered(uri, client->getConnectionToken()); - LOG_IF(DEBUG_EXTENSION_MEDIATOR) << "registered: " << uri << " clients: " << mClients.size(); + + auto sessionState = getExtensionSessionState(); + if (sessionState) { + if (!sessionState->updateState(activity, ExtensionLifecycleStage::kExtensionRegistered)) { + LOG(LogLevel::kError) << "Ignoring extension registration due to incompatible state"; + return; + } + } + + extension->onRegistered(*activity); + + LOG_IF(DEBUG_EXTENSION_MEDIATOR).session(mRootContext.lock()) << "registered: " << uri << " clients: " << mClients.size(); } void -ExtensionMediator::enqueueResponse(const std::string& uri, const rapidjson::Value& message) +ExtensionMediator::enqueueResponse(const alexaext::ActivityDescriptorPtr& activity, const rapidjson::Value& message) { + if (!activity) return; + + const auto& uri = activity->getURI(); std::weak_ptr weak_this = shared_from_this(); // TODO optimize auto copy = std::make_shared(); copy->CopyFrom(message, copy->GetAllocator()); - bool enqueued = mMessageExecutor->enqueueTask([weak_this, uri, copy] () { + bool enqueued = mMessageExecutor->enqueueTask([weak_this, activity, copy] () { if (auto mediator = weak_this.lock()) { - mediator->processMessage(uri, *copy); + mediator->processMessage(activity, *copy); } }); if (!enqueued) - LOG(LogLevel::kWarn) << "failed to process message for extension, uri:" << uri; + LOG(LogLevel::kWarn).session(mRootContext.lock()) << "failed to process message for extension, uri:" << uri; } void -ExtensionMediator::processMessage(const std::string& uri, JsonData&& processMessage) +ExtensionMediator::processMessage(const alexaext::ActivityDescriptorPtr& activity, JsonData&& processMessage) { + if (!activity) return; + + const auto& uri = activity->getURI(); auto client = mClients.find(uri); if (client == mClients.end()) return; @@ -444,6 +794,13 @@ ExtensionMediator::processMessage(const std::string& uri, JsonData&& processMess auto proxy = provider->getExtension(uri); registerExtension(uri, proxy, client->second); } + } else if (client->second->registrationFailed()) { + // Registration failed, since registration was processed but th + if (auto state = getExtensionSessionState()) { + // The update shouldn't fail because we know that the activity is known and + // the transition should be allowed. + state->updateState(activity, ExtensionLifecycleStage::kExtensionFinalized); + } } if (mPendingRegistrations.count(uri)) { @@ -463,13 +820,113 @@ ExtensionMediator::finish() if (!provider) return; for (const auto& u2c : mClients) { - auto proxy = provider->getExtension(u2c.first); - if (proxy) { - proxy->onUnregistered(u2c.first, u2c.second->getConnectionToken()); - } + auto activity = getActivity(u2c.first); + unregister(activity); } mClients.clear(); + mActivitiesByURI.clear(); + + /** + * Check if the session has already ended. If it has, make sure we trigger the end of + * session logic since any active extension from the current mediator would have prevented + * end of session notifications from going out. If the session hasn't ended yet, there is + * nothing to do until it does. + */ + if (mExtensionSession && mExtensionSession->hasEnded()) { + onSessionEnded(); + } +} + +void +ExtensionMediator::onSessionEnded() { + auto sessionState = getExtensionSessionState(); + if (!sessionState) return; + + sessionState->endSession(); +} + +void +ExtensionMediator::onDisplayStateChanged(DisplayState displayState) { + for (const auto& entry : mActivitiesByURI) { + updateDisplayState(entry.second, displayState); + } +} + +void +ExtensionMediator::updateDisplayState(const ActivityDescriptorPtr& activity, + DisplayState displayState) { + if (!activity) return; + auto provider = mProvider.lock(); + if (!provider) return; + + if (auto sessionState = getExtensionSessionState()) { + if (sessionState->getState(activity) == ExtensionLifecycleStage::kExtensionRegistered) { + auto proxy = provider->getExtension(activity->getURI()); + if (!proxy) return; + + switch (displayState) { + case kDisplayStateForeground: + proxy->onForeground(*activity); + break; + case kDisplayStateBackground: + proxy->onBackground(*activity); + break; + case kDisplayStateHidden: + proxy->onHidden(*activity); + break; + default: + LOG(LogLevel::kWarn) << "Unknown display state, ignoring update"; + break; + } + } + } +} + +std::shared_ptr +ExtensionMediator::getExtensionSessionState() const { + if (!mExtensionSession) return nullptr; + return mExtensionSession->getSessionState(); +} + +alexaext::ActivityDescriptorPtr +ExtensionMediator::getActivity(const std::string& uri) { + auto it = mActivitiesByURI.find(uri); + if (it != mActivitiesByURI.end()) { + return it->second; + } + + auto activity = ActivityDescriptor::create(uri, + mExtensionSession ? mExtensionSession->getSessionDescriptor() : nullptr); + mActivitiesByURI.emplace(uri, activity); + return activity; +} + +void +ExtensionMediator::unregister(const alexaext::ActivityDescriptorPtr& activity) { + if (!activity) return; + auto provider = mProvider.lock(); + if (!provider) return; + + auto proxy = provider->getExtension(activity->getURI()); + if (!proxy) return; + + auto sessionState = getExtensionSessionState(); + + auto itr = mClients.find(activity->getURI()); + if (itr == mClients.end()) return; + if (!itr->second->registered()) return; // Nothing to do, the activity was never successfully registered + + proxy->onUnregistered(*activity); + + if (sessionState) { + sessionState->updateState(activity, ExtensionLifecycleStage::kExtensionUnregistered); + if (mExtensionSession->hasEnded()) { + // The session has already ended, so make sure we notify all extensions if the activity + // we just unregistered was the last one for the session. + sessionState->endSession(); + } + } } } // namespace apl diff --git a/aplcore/src/extension/extensionsession.cpp b/aplcore/src/extension/extensionsession.cpp new file mode 100644 index 0000000..e7d5553 --- /dev/null +++ b/aplcore/src/extension/extensionsession.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. + */ + +#ifdef ALEXAEXTENSIONS + +#include "apl/extension/extensionsession.h" + +namespace apl { + +void +ExtensionSession::end() { + if (mEnded) return; + + mEnded = true; + + if (mSessionEndedCallback) + mSessionEndedCallback(*this); +} + +void +ExtensionSession::onSessionEnded(ExtensionSession::SessionEndedCallback&& callback) { + mSessionEndedCallback = std::move(callback); + + if (hasEnded() && mSessionEndedCallback) { + mSessionEndedCallback(*this); + } +} + +ExtensionSessionPtr +ExtensionSession::create(const alexaext::SessionDescriptorPtr& sessionDescriptor) { + if (!sessionDescriptor) return nullptr; + + return std::make_shared(sessionDescriptor); +} + +std::shared_ptr +ExtensionSession::create() { + return create(alexaext::SessionDescriptor::create()); +} + +} // namespace apl + +#endif //ALEXAEXTENSIONS diff --git a/aplcore/src/focus/focusfinder.cpp b/aplcore/src/focus/focusfinder.cpp index 925e2a7..026a31d 100644 --- a/aplcore/src/focus/focusfinder.cpp +++ b/aplcore/src/focus/focusfinder.cpp @@ -58,7 +58,7 @@ FocusFinder::findNext(const CoreComponentPtr& focused, FocusDirection direction) } if (root != focused && root->isFocusable()) { - LOG_IF(DEBUG_FOCUS_FINDER) << "going with root"; + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "going with root"; return root->takeFocusFromChild(direction, focusedRect); } return nullptr; @@ -84,7 +84,7 @@ FocusFinder::findNext( } if (root != focused && root->isFocusable()) { - LOG_IF(DEBUG_FOCUS_FINDER) << "going with root"; + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "going with root"; return root->takeFocusFromChild(direction, focusedRect); } return nullptr; @@ -93,7 +93,7 @@ FocusFinder::findNext( CoreComponentPtr FocusFinder::findNextInternal(const CoreComponentPtr& root, const Rect& focusedRect, FocusDirection direction) { - LOG_IF(DEBUG_FOCUS_FINDER) << "Root:" << root->toDebugSimpleString() << " focusedRect:" << + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "Root:" << root->toDebugSimpleString() << " focusedRect:" << focusedRect.toDebugString() << " direction:" << direction; auto focusables = getFocusables(root, false); CoreComponentPtr bestCandidate; @@ -105,7 +105,7 @@ FocusFinder::findNextInternal(const CoreComponentPtr& root, const Rect& focusedR focusable->getBoundsInParent(root, candidateRect); if (isValidCandidate(root, focusedRect, focusable, candidateRect, direction)) { auto candidateIntersect = BeamIntersect::build(focusedRect, candidateRect, direction); - LOG_IF(DEBUG_FOCUS_FINDER) << "Candidate: " << focusable->toDebugSimpleString() + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "Candidate: " << focusable->toDebugSimpleString() << " intersect: " << candidateIntersect; if (bestIntersect.empty() || candidateIntersect > bestIntersect) { bestIntersect = candidateIntersect; @@ -115,7 +115,7 @@ FocusFinder::findNextInternal(const CoreComponentPtr& root, const Rect& focusedR } if (bestCandidate) { - LOG_IF(DEBUG_FOCUS_FINDER) << "Best: " << bestCandidate->toDebugSimpleString() + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "Best: " << bestCandidate->toDebugSimpleString() << " intersect: " << bestIntersect; auto offsetFocusRect = focusedRect; offsetFocusRect.offset(-bestIntersect.getCandidate().getTopLeft()); @@ -129,7 +129,7 @@ FocusFinder::findNextInternal(const CoreComponentPtr& root, const Rect& focusedR return bestCandidate; } - LOG_IF(DEBUG_FOCUS_FINDER) << "Nothing to focus."; + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "Nothing to focus."; return nullptr; } @@ -137,7 +137,7 @@ FocusFinder::findNextInternal(const CoreComponentPtr& root, const Rect& focusedR CoreComponentPtr FocusFinder::findNextByTabOrder(const CoreComponentPtr& focused, const CoreComponentPtr& root, FocusDirection direction) { - LOG_IF(DEBUG_FOCUS_FINDER) << "Root:" << root->getUniqueId() << " focused:" + LOG_IF(DEBUG_FOCUS_FINDER).session(root) << "Root:" << root->getUniqueId() << " focused:" << (focused ? focused->getUniqueId() : "N/A"); auto walkRoot = root; auto current = focused; diff --git a/aplcore/src/focus/focusmanager.cpp b/aplcore/src/focus/focusmanager.cpp index 04303f3..f152a03 100644 --- a/aplcore/src/focus/focusmanager.cpp +++ b/aplcore/src/focus/focusmanager.cpp @@ -75,7 +75,7 @@ FocusManager::setFocus(const CoreComponentPtr& component, bool notifyViewhost) auto focused = mFocused.lock(); - LOG_IF(DEBUG_FOCUS) << focused << " -> " << component; + LOG_IF(DEBUG_FOCUS).session(component) << focused << " -> " << component; // If you target the already focused component, we don't need to do any work if (focused == component) @@ -117,7 +117,7 @@ FocusManager::releaseFocus(const std::shared_ptr& component, { auto focused = mFocused.lock(); - LOG_IF(DEBUG_FOCUS) << focused << " -> " << component; + LOG_IF(DEBUG_FOCUS).session(component) << focused << " -> " << component; if (focused == component) clearFocus(notifyViewhost); @@ -130,9 +130,9 @@ void FocusManager::clearFocus(bool notifyViewhost, FocusDirection direction, bool force) { auto focused = mFocused.lock(); - LOG_IF(DEBUG_FOCUS) << focused; if (focused) { + LOG_IF(DEBUG_FOCUS).session(focused) << focused; if (!notifyViewhost) { clearFocusedComponent(); return; diff --git a/aplcore/src/graphic/graphic.cpp b/aplcore/src/graphic/graphic.cpp index f10f7c9..b9fffd0 100644 --- a/aplcore/src/graphic/graphic.cpp +++ b/aplcore/src/graphic/graphic.cpp @@ -68,15 +68,15 @@ Graphic::create(const ContextPtr& context, const Path& path, const StyleInstancePtr& styledPtr) { - LOG_IF(DEBUG_GRAPHIC) << "Creating graphic data=" << context->opt("data").toDebugString(); + LOG_IF(DEBUG_GRAPHIC).session(context) << "Creating graphic data=" << context->opt("data").toDebugString(); // Check and extract the version auto version = propertyAsMapped(*context, json, "version", -1, sGraphicVersionBimap); if (version == -1) { - CONSOLE_CTP(context) << "Illegal graphics version"; + CONSOLE(context) << "Illegal graphics version"; return nullptr; } - LOG_IF(DEBUG_GRAPHIC) << "Found version" << version; + LOG_IF(DEBUG_GRAPHIC).session(context) << "Found version" << version; auto graphic = std::make_shared(context, json, static_cast(version)); graphic->initialize(context, json, std::move(properties), component, path, styledPtr); @@ -167,7 +167,7 @@ Graphic::initialize(const ContextPtr& sourceContext, // Populate the data-binding context with parameters for (const auto& param : mParameterArray) { - LOG_IF(DEBUG_GRAPHIC) << "Parse parameter: " << param.name; + LOG_IF(DEBUG_GRAPHIC).session(sourceContext) << "Parse parameter: " << param.name; const auto& conversionFunc = sBindingFunctions.at(param.type); auto value = conversionFunc(*sourceContext, evaluate(*mInternalContext, param.defvalue)); Object parsed; @@ -193,7 +193,7 @@ Graphic::initialize(const ContextPtr& sourceContext, } // Store the calculated value in the data-binding context - LOG_IF(DEBUG_GRAPHIC) << "Storing parameter '" << param.name << "' = " << value; + LOG_IF(DEBUG_GRAPHIC).session(sourceContext) << "Storing parameter '" << param.name << "' = " << value; mInternalContext->putUserWriteable(param.name, value); // After storing the parameter we can wire up any necessary data dependant diff --git a/aplcore/src/graphic/graphicbuilder.cpp b/aplcore/src/graphic/graphicbuilder.cpp index 2af6a04..18ecdd8 100644 --- a/aplcore/src/graphic/graphicbuilder.cpp +++ b/aplcore/src/graphic/graphicbuilder.cpp @@ -80,9 +80,9 @@ GraphicBuilder::GraphicBuilder(const GraphicPtr& graphic) void GraphicBuilder::addChildren(GraphicElement& element, const Object& json) { - LOG_IF(DEBUG_GRAPHIC_BUILDER) << element.toDebugString(); - const auto& context = *element.mContext; + LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << element.toDebugString(); + const auto items = arrayifyProperty(context, json, "item", "items"); if (items.empty()) return; @@ -94,7 +94,7 @@ GraphicBuilder::addChildren(GraphicElement& element, const Object& json) const auto data = arrayifyPropertyAsObject(context, json, "data"); const auto dataItems = evaluateRecursive(context, data); if (!dataItems.empty()) { - LOG_IF(DEBUG_GRAPHIC_BUILDER) << "Data child inflation: " << dataItems; + LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << "Data child inflation: " << dataItems; const auto length = dataItems.size(); for (size_t dataIndex = 0; dataIndex < length; dataIndex++) { const auto& dataItem = dataItems.at(dataIndex); @@ -113,7 +113,7 @@ GraphicBuilder::addChildren(GraphicElement& element, const Object& json) } // If we get to this point, we are not doing multi-child inflation - LOG_IF(DEBUG_GRAPHIC_BUILDER) << "Normal child inflation"; + LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << "Normal child inflation"; const auto length = items.size(); for (size_t i = 0; i < length; i++) { const auto& item = items.at(i); @@ -124,7 +124,7 @@ GraphicBuilder::addChildren(GraphicElement& element, const Object& json) } auto child = createChildFromArray(childContext, arrayify(context, item)); if (child) { - LOG_IF(DEBUG_GRAPHIC_BUILDER) << "child [" << i << "]"; + LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << "child [" << i << "]"; element.mChildren.push_back(child); index++; } @@ -135,13 +135,13 @@ GraphicBuilder::addChildren(GraphicElement& element, const Object& json) GraphicElementPtr GraphicBuilder::createChild(const ContextPtr& context, const Object& json) { - LOG_IF(DEBUG_GRAPHIC_BUILDER) << ""; + LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << ""; // Check for a valid child type auto type = propertyAsString(*context, json, "type"); auto it = sGraphicElementMap.find(type); if (it == sGraphicElementMap.end()) { - CONSOLE_CTP(context) << "Invalid graphic child type '" << type << "'"; + CONSOLE(context) << "Invalid graphic child type '" << type << "'"; return nullptr; } @@ -179,7 +179,7 @@ GraphicElementPtr GraphicBuilder::createChildFromArray(const ContextPtr& context, const std::vector& items) { - LOG_IF(DEBUG_GRAPHIC_BUILDER) << items.size(); + LOG_IF(DEBUG_GRAPHIC_BUILDER).session(context) << items.size(); const auto length = items.size(); for (size_t i = 0; i < length; i++) { diff --git a/aplcore/src/graphic/graphiccontent.cpp b/aplcore/src/graphic/graphiccontent.cpp index 905d975..9307b5b 100644 --- a/aplcore/src/graphic/graphiccontent.cpp +++ b/aplcore/src/graphic/graphiccontent.cpp @@ -34,48 +34,48 @@ GraphicContent::create(const SessionPtr& session, JsonData&& data) // These checks don't guarantee that it could be drawn. if (!data) { - CONSOLE_S(session) << "Graphic Json could not be parsed: " << data.error(); + CONSOLE(session) << "Graphic Json could not be parsed: " << data.error(); return nullptr; } if (!json.IsObject()) { - CONSOLE_S(session) << "Unknown object type for graphic"; + CONSOLE(session) << "Unknown object type for graphic"; return nullptr; } auto type = json.FindMember("type"); if (type == json.MemberEnd() || !type->value.IsString()) { - CONSOLE_S(session) << "Missing 'type' property in graphic"; + CONSOLE(session) << "Missing 'type' property in graphic"; return nullptr; } auto typeString = type->value.GetString(); if (::strcmp(typeString, "AVG")) { - CONSOLE_S(session) << "Invalid 'type' property in graphic - must be 'AVG'"; + CONSOLE(session) << "Invalid 'type' property in graphic - must be 'AVG'"; return nullptr; } auto version = json.FindMember("version"); if (version == json.MemberEnd() || !version->value.IsString()) { - CONSOLE_S(session) << "Missing or invalid 'version' property in graphic"; + CONSOLE(session) << "Missing or invalid 'version' property in graphic"; return nullptr; } std::string v = version->value.GetString(); if (!sGraphicVersionBimap.has(v)) { - CONSOLE_S(session) << "Invalid AVG version '" << v << "'"; + CONSOLE(session) << "Invalid AVG version '" << v << "'"; return nullptr; } auto height = json.FindMember("height"); if (height == json.MemberEnd()) { - CONSOLE_S(session) << "Missing 'height' property in graphic"; + CONSOLE(session) << "Missing 'height' property in graphic"; return nullptr; } auto width = json.FindMember("width"); if (width == json.MemberEnd()) { - CONSOLE_S(session) << "Missing 'width' property in graphic"; + CONSOLE(session) << "Missing 'width' property in graphic"; return nullptr; } diff --git a/aplcore/src/graphic/graphicdependant.cpp b/aplcore/src/graphic/graphicdependant.cpp index 777c56b..bccc4af 100644 --- a/aplcore/src/graphic/graphicdependant.cpp +++ b/aplcore/src/graphic/graphicdependant.cpp @@ -31,7 +31,7 @@ GraphicDependant::create(const GraphicElementPtr& downstreamGraphicElement, const ContextPtr& bindingContext, BindingFunction bindingFunction) { - LOG_IF(DEBUG_GRAPHIC_DEP) << " to " << sGraphicPropertyBimap.at(downstreamKey) + LOG_IF(DEBUG_GRAPHIC_DEP).session(bindingContext) << " to " << sGraphicPropertyBimap.at(downstreamKey) << "(" << downstreamGraphicElement.get() << ")"; SymbolReferenceMap symbols; @@ -55,7 +55,7 @@ GraphicDependant::recalculate(bool useDirtyFlag) const auto bindingContext = mBindingContext.lock(); if (downstream && bindingContext) { auto value = mBindingFunction(*bindingContext, reevaluate(*bindingContext, mEquation)); - LOG_IF(DEBUG_GRAPHIC_DEP) << " new value " << value.toDebugString(); + LOG_IF(DEBUG_GRAPHIC_DEP).session(bindingContext) << " new value " << value.toDebugString(); downstream->setValue(mDownstreamKey, value, useDirtyFlag); } } diff --git a/aplcore/src/graphic/graphicelement.cpp b/aplcore/src/graphic/graphicelement.cpp index ec8e1d2..54bdae2 100644 --- a/aplcore/src/graphic/graphicelement.cpp +++ b/aplcore/src/graphic/graphicelement.cpp @@ -100,7 +100,7 @@ GraphicElement::initialize(const GraphicPtr& graphic, const Object& json) // If this was a required property, and not in style, abort if ((pd.flags & kPropRequired) != 0 && (value == defValue)) { - CONSOLE_CTP(mContext) << "Missing required graphic property: " << pd.names; + CONSOLE(mContext) << "Missing required graphic property: " << pd.names; return false; } @@ -114,6 +114,23 @@ GraphicElement::initialize(const GraphicPtr& graphic, const Object& json) return true; } +std::string +GraphicElement::getLang() const { + auto graphic = mGraphic.lock(); + if (!graphic) { return ""; } + + return graphic->getRoot()->getValue(kGraphicPropertyLang).asString(); +} + +GraphicLayoutDirection +GraphicElement::getLayoutDirection() const { + auto graphic = mGraphic.lock(); + if (!graphic) { return kGraphicLayoutDirectionLTR; } + + return static_cast( + graphic->getRoot()->getValue(kGraphicPropertyLayoutDirection).asInt()); +} + bool GraphicElement::setValue(GraphicPropertyKey key, const Object& value, bool useDirtyFlag) { diff --git a/aplcore/src/graphic/graphicelementcontainer.cpp b/aplcore/src/graphic/graphicelementcontainer.cpp index d75395b..9f13d03 100644 --- a/aplcore/src/graphic/graphicelementcontainer.cpp +++ b/aplcore/src/graphic/graphicelementcontainer.cpp @@ -65,13 +65,13 @@ GraphicElementContainer::initialize(const GraphicPtr& graphic, const Object& jso auto height = mValues.get(kGraphicPropertyHeightOriginal).getAbsoluteDimension(); if (height <= 0) { - CONSOLE_CTP(mContext) << "Invalid graphic height - must be positive"; + CONSOLE(mContext) << "Invalid graphic height - must be positive"; return false; } auto width = mValues.get(kGraphicPropertyWidthOriginal).getAbsoluteDimension(); if (width <= 0) { - CONSOLE_CTP(mContext) << "Invalid graphic width - must be positive"; + CONSOLE(mContext) << "Invalid graphic width - must be positive"; return false; } diff --git a/aplcore/src/graphic/graphicfilter.cpp b/aplcore/src/graphic/graphicfilter.cpp index 8256f95..5832930 100644 --- a/aplcore/src/graphic/graphicfilter.cpp +++ b/aplcore/src/graphic/graphicfilter.cpp @@ -67,7 +67,7 @@ calculateNormal(const GraphicFilterPropDef& def, if (value != -1) return value; - CONSOLE_CTX(context) << "Invalid value for graphic filter property " << def.names[0] << ": " << tmp.asString(); + CONSOLE(context) << "Invalid value for graphic filter property " << def.names[0] << ": " << tmp.asString(); return def.defvalue; } @@ -85,7 +85,7 @@ GraphicFilter::create(const Context& context, const Object& object) auto typeName = propertyAsString(context, object, "type"); if (typeName.empty()) { - CONSOLE_CTX(context) << "No 'type' property defined for graphic filter"; + CONSOLE(context) << "No 'type' property defined for graphic filter"; return Object::NULL_OBJECT(); } @@ -100,7 +100,7 @@ GraphicFilter::create(const Context& context, const Object& object) return Object(GraphicFilter(type, std::move(data))); } - CONSOLE_CTX(context) << "Unable to find graphic filter named '" << typeName << "'"; + CONSOLE(context) << "Unable to find graphic filter named '" << typeName << "'"; return Object::NULL_OBJECT(); } diff --git a/aplcore/src/graphic/graphicpattern.cpp b/aplcore/src/graphic/graphicpattern.cpp index 4b12bf4..dce0aa6 100644 --- a/aplcore/src/graphic/graphicpattern.cpp +++ b/aplcore/src/graphic/graphicpattern.cpp @@ -15,11 +15,12 @@ #include "apl/engine/evaluate.h" #include "apl/engine/arrayify.h" -#include "apl/utils/session.h" -#include "apl/primitives/object.h" #include "apl/graphic/graphicpattern.h" #include "apl/graphic/graphicelement.h" #include "apl/graphic/graphicbuilder.h" +#include "apl/primitives/object.h" +#include "apl/utils/session.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -46,12 +47,12 @@ GraphicPattern::create(const Context& context, const Object& object) std::string description = propertyAsString(context, object, "description"); double height = propertyAsDouble(context, object, "height", -1); if (height < 0) { - CONSOLE_CTX(context) << "GraphicPattern height is required."; + CONSOLE(context) << "GraphicPattern height is required."; return Object::NULL_OBJECT(); } double width = propertyAsDouble(context, object, "width", -1); if (width < 0) { - CONSOLE_CTX(context) << "GraphicPattern width is required."; + CONSOLE(context) << "GraphicPattern width is required."; return Object::NULL_OBJECT(); } @@ -71,8 +72,8 @@ std::string GraphicPattern::toDebugString() const { std::string result = "GraphicPattern< id=" + getId() + " description=" + getDescription() + - " width=" + std::to_string(getWidth()) + - " height=" + std::to_string(getHeight()) + + " width=" + sutil::to_string(getWidth()) + + " height=" + sutil::to_string(getHeight()) + " items=["; for (const auto& item : getItems()) result += " " + item->toDebugString(); diff --git a/aplcore/src/livedata/layoutrebuilder.cpp b/aplcore/src/livedata/layoutrebuilder.cpp index 96922eb..de10595 100644 --- a/aplcore/src/livedata/layoutrebuilder.cpp +++ b/aplcore/src/livedata/layoutrebuilder.cpp @@ -30,19 +30,20 @@ class ChildWalker { public: /** * Construct the walker and step over a "firstItem". - * @param layout + * @param layout The layout + * @param hasFirstItem True if a "firstItem" property was specified that needs to be skipped. */ ChildWalker(const CoreComponentPtr& layout, bool hasFirstItem) : mLayout(layout), mIndex(hasFirstItem ? 1 : 0) { - LOG_IF(DEBUG_WALKER) << "mIndex=" << mIndex << " total=" << mLayout->getChildCount(); + LOG_IF(DEBUG_WALKER).session(layout) << "mIndex=" << mIndex << " total=" << mLayout->getChildCount(); } /** * Throw away any remaining children, but save the "lastItem" child if it exists */ void finish(bool hasLastItem) { - LOG_IF(DEBUG_WALKER) << "mIndex=" << mIndex << " total=" << mLayout->getChildCount() << " hasLast=" << hasLastItem; + LOG_IF(DEBUG_WALKER).session(mLayout) << "mIndex=" << mIndex << " total=" << mLayout->getChildCount() << " hasLast=" << hasLastItem; int lastIndex = mLayout->getChildCount(); if (hasLastItem) @@ -55,32 +56,32 @@ class ChildWalker { /** * Throw away children until we reach one with "dataIndex" equal to oldIndex * If we pass it, that means it didn't inflate last time, so we can stop. - * @param oldIndex + * @param oldIndex The old index to search for * @return True if we found it; false if we've gone beyond */ bool advanceUntil(int oldIndex) { - LOG_IF(DEBUG_WALKER) << " oldIndex=" << oldIndex << " mIndex=" << mIndex << " total=" << mLayout->getChildCount(); + LOG_IF(DEBUG_WALKER).session(mLayout) << " oldIndex=" << oldIndex << " mIndex=" << mIndex << " total=" << mLayout->getChildCount(); while (mIndex < mLayout->getChildCount()) { auto child = mLayout->getCoreChildAt(mIndex); auto dataIndex = child->getContext()->opt("dataIndex"); if (dataIndex.isNumber()) { auto index = dataIndex.getInteger(); if (index >= oldIndex) { - LOG_IF(DEBUG_WALKER) << " matched index " << index; + LOG_IF(DEBUG_WALKER).session(mLayout) << " matched index " << index; return index == oldIndex; } - LOG_IF(DEBUG_WALKER) << " found data index of " << index << " but it didn't match"; + LOG_IF(DEBUG_WALKER).session(mLayout) << " found data index of " << index << " but it didn't match"; } - LOG_IF(DEBUG_WALKER) << " removing child at index " << mIndex; + LOG_IF(DEBUG_WALKER).session(mLayout) << " removing child at index " << mIndex; mLayout->removeChildAt(mIndex, true); } - LOG(LogLevel::kError) << "Failed to find child with dataIndex of " << oldIndex; + LOG(LogLevel::kError).session(mLayout) << "Failed to find child with dataIndex of " << oldIndex; return false; } void advance() { - LOG_IF(DEBUG_WALKER) << "mIndex=" << mIndex << " total=" << mLayout->getChildCount(); + LOG_IF(DEBUG_WALKER).session(mLayout) << "mIndex=" << mIndex << " total=" << mLayout->getChildCount(); mIndex++; } @@ -104,7 +105,7 @@ inline ContextPtr findToken(const CoreComponentPtr& component, int token) if (value.isNumber() && value.getInteger() == token) return p.context(); - LOG(LogLevel::kWarn) << "Unable to find token parent of context. Token=" << token; + LOG(LogLevel::kWarn).session(component) << "Unable to find token parent of context. Token=" << token; return nullptr; } @@ -177,7 +178,7 @@ LayoutRebuilder::build(bool useDirtyFlag) auto array = mArray.lock(); if (!layout || !array) { - LOG(LogLevel::kError) << "Attempting to build a layout without a layout or data array"; + LOG(LogLevel::kError).session(layout) << "Attempting to build a layout without a layout or data array"; return; } @@ -227,7 +228,7 @@ LayoutRebuilder::rebuild() auto array = mArray.lock(); if (!layout || !array) { - LOG(LogLevel::kError) << "Attempting to rebuild a layout without a layout or data array"; + LOG(LogLevel::kError).session(layout) << "Attempting to rebuild a layout without a layout or data array"; return; } diff --git a/aplcore/src/livedata/livearrayobject.cpp b/aplcore/src/livedata/livearrayobject.cpp index 3b2c8b0..89b7a0a 100644 --- a/aplcore/src/livedata/livearrayobject.cpp +++ b/aplcore/src/livedata/livearrayobject.cpp @@ -97,7 +97,7 @@ LiveArrayObject::flush() { * Return the index of the old item and a flag if that item has changed value. * The index is -1 if the item is completely new. * @param index The index in the new array. - * @return + * @return A pair containing the index of the old item and a boolean "true" if the item has changed value. */ std::pair LiveArrayObject::newToOld(ObjectArray::size_type index) @@ -134,4 +134,4 @@ LiveArrayObject::newToOld(ObjectArray::size_type index) return { index, changed }; } -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/livedata/livedataobject.cpp b/aplcore/src/livedata/livedataobject.cpp index 9e2e8ae..2a0bd62 100644 --- a/aplcore/src/livedata/livedataobject.cpp +++ b/aplcore/src/livedata/livedataobject.cpp @@ -36,7 +36,7 @@ LiveDataObject::create(const LiveObjectPtr& data, else if (type == Object::kMapType) element = std::make_shared(std::static_pointer_cast(data), context, key); else { - LOG(LogLevel::kError) << "Unexpected data type for live object key='" << key << "': " << data->getType(); + LOG(LogLevel::kError).session(context) << "Unexpected data type for live object key='" << key << "': " << data->getType(); return nullptr; } diff --git a/aplcore/src/primitives/CMakeLists.txt b/aplcore/src/primitives/CMakeLists.txt index 69550b4..aef6e40 100644 --- a/aplcore/src/primitives/CMakeLists.txt +++ b/aplcore/src/primitives/CMakeLists.txt @@ -14,8 +14,8 @@ target_sources_local(apl PRIVATE accessibilityaction.cpp - characterrange.cpp color.cpp + range.cpp dimension.cpp filter.cpp functions.cpp @@ -24,7 +24,7 @@ target_sources_local(apl mediasource.cpp radii.cpp rect.cpp - urlrequest.cpp + roundedrect.cpp symbolreferencemap.cpp styledtext.cpp styledtextstate.cpp @@ -35,4 +35,5 @@ target_sources_local(apl object.cpp objectdata.cpp unicode.cpp + urlrequest.cpp ) diff --git a/aplcore/src/primitives/accessibilityaction.cpp b/aplcore/src/primitives/accessibilityaction.cpp index b153f3e..dda6f2c 100644 --- a/aplcore/src/primitives/accessibilityaction.cpp +++ b/aplcore/src/primitives/accessibilityaction.cpp @@ -79,19 +79,19 @@ AccessibilityAction::create(const CoreComponentPtr& component, const Object& obj return object.getAccessibilityAction(); if (!object.isMap()) { - CONSOLE_CTP(context) << "Invalid accessibility action"; + CONSOLE(context) << "Invalid accessibility action"; return nullptr; } auto name = propertyAsString(*context, object, "name"); if (name.empty()) { - CONSOLE_CTP(context) << "Accessibility action missing name"; + CONSOLE(context) << "Accessibility action missing name"; return nullptr; } auto label = propertyAsString(*context, object, "label"); if (label.empty()) { - CONSOLE_CTP(context) << "Accessibility action name='" << name << "' missing label"; + CONSOLE(context) << "Accessibility action name='" << name << "' missing label"; return nullptr; } diff --git a/aplcore/src/primitives/characterrange.cpp b/aplcore/src/primitives/characterrange.cpp deleted file mode 100644 index 2346a88..0000000 --- a/aplcore/src/primitives/characterrange.cpp +++ /dev/null @@ -1,46 +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/characterrange.h" -#include "apl/primitives/characterrangegrammar.h" -#include "apl/utils/session.h" - -namespace apl { - -namespace pegtl = tao::TAO_PEGTL_NAMESPACE; - -/** - * Parse a rangeExpression and return a stack of CharacterRange objects - * @param rangeExpression String representing 0 or more character ranges - * @return a stack of CharacterRange objects - */ -std::vector CharacterRanges::parse(const apl::SessionPtr &session, const char* rangeExpression) -{ - if (rangeExpression != nullptr && - rangeExpression[0] != '\0') { - try { - character_range_grammar::character_range_state state; - pegtl::string_input<> in(rangeExpression, ""); - pegtl::parse(in, state); - return state.getRanges(); - } - catch (pegtl::parse_error e) { - CONSOLE_S(session) << "Error parsing character range '" << rangeExpression << "', " << e.what(); - } - } - return std::vector(); -} - -} diff --git a/aplcore/src/primitives/color.cpp b/aplcore/src/primitives/color.cpp index b51873f..53204b0 100644 --- a/aplcore/src/primitives/color.cpp +++ b/aplcore/src/primitives/color.cpp @@ -34,7 +34,7 @@ uint32_t Color::parse(const SessionPtr& session, const char *color) { return state.getColor(); } catch (pegtl::parse_error e) { - CONSOLE_S(session) << "Error parsing color '" << color << "', " << e.what(); + CONSOLE(session) << "Error parsing color '" << color << "', " << e.what(); } return TRANSPARENT; diff --git a/aplcore/src/primitives/dimension.cpp b/aplcore/src/primitives/dimension.cpp index 37ad0a5..f1d395c 100644 --- a/aplcore/src/primitives/dimension.cpp +++ b/aplcore/src/primitives/dimension.cpp @@ -18,6 +18,7 @@ #include "apl/primitives/dimension.h" #include "apl/engine/context.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -52,7 +53,7 @@ namespace apl { { template< typename Input > static void apply(const Input& in, std::string& unit, bool& isAuto, double& value) { - value = std::stod(in.string()); + value = sutil::stod(in.string()); } }; diff --git a/aplcore/src/primitives/filter.cpp b/aplcore/src/primitives/filter.cpp index 47caa4c..cbd1183 100644 --- a/aplcore/src/primitives/filter.cpp +++ b/aplcore/src/primitives/filter.cpp @@ -147,7 +147,7 @@ calculateNormal(const FilterPropDef& def, if (value != -1) return value; - CONSOLE_CTX(context) << "Invalid value for filter property " << def.names[0] << ": " << tmp.asString(); + CONSOLE(context) << "Invalid value for filter property " << def.names[0] << ": " << tmp.asString(); return def.defvalue; } @@ -179,7 +179,7 @@ Filter::create(const Context& context, const Object& object) auto typeName = propertyAsString(context, object, "type"); if (typeName.empty()) { - CONSOLE_CTX(context) << "No 'type' property defined for filter"; + CONSOLE(context) << "No 'type' property defined for filter"; return Object::NULL_OBJECT(); } @@ -223,7 +223,7 @@ Filter::create(const Context& context, const Object& object) return Object(Filter(kFilterTypeExtension, std::move(data))); } - CONSOLE_CTX(context) << "Unable to find filter named '" << typeName << "'"; + CONSOLE(context) << "Unable to find filter named '" << typeName << "'"; return Object::NULL_OBJECT(); } diff --git a/aplcore/src/primitives/gradient.cpp b/aplcore/src/primitives/gradient.cpp index 7f6de18..0afebea 100644 --- a/aplcore/src/primitives/gradient.cpp +++ b/aplcore/src/primitives/gradient.cpp @@ -98,13 +98,13 @@ Gradient::create(const Context& context, const Object& object, bool avg) return Object::NULL_OBJECT(); if (avg && !object.has("type")) { - CONSOLE_CTX(context) << "Type field is required in AVG gradient"; + CONSOLE(context) << "Type field is required in AVG gradient"; return Object::NULL_OBJECT(); } auto type = propertyAsMapped(context, object, "type", LINEAR, sGradientTypeMap); if (type < 0) { - CONSOLE_CTX(context) << "Unrecognized type field in gradient"; + CONSOLE(context) << "Unrecognized type field in gradient"; return Object::NULL_OBJECT(); } @@ -112,7 +112,7 @@ Gradient::create(const Context& context, const Object& object, bool avg) auto colorRange = arrayifyPropertyAsObject(context, object, "colorRange"); auto length = colorRange.size(); if (length < 2) { - CONSOLE_CTX(context) << "Gradient does not have suitable color range"; + CONSOLE(context) << "Gradient does not have suitable color range"; return Object::NULL_OBJECT(); } @@ -120,7 +120,7 @@ Gradient::create(const Context& context, const Object& object, bool avg) auto inputRange = arrayifyPropertyAsObject(context, object, "inputRange"); auto inputRangeLength = inputRange.size(); if (inputRangeLength != 0 && inputRangeLength != length) { - CONSOLE_CTX(context) << "Gradient input range must match the color range length"; + CONSOLE(context) << "Gradient input range must match the color range length"; return Object::NULL_OBJECT(); } @@ -140,7 +140,7 @@ Gradient::create(const Context& context, const Object& object, bool avg) for (const auto& m : inputRange.getArray()) { double value = m.asNumber(); if (value < last || value > 1) { - CONSOLE_CTX(context) << "Gradient input range not in ascending order within range [0,1]"; + CONSOLE(context) << "Gradient input range not in ascending order within range [0,1]"; return Object::NULL_OBJECT(); } @@ -204,7 +204,7 @@ Gradient::create(const Context& context, const Object& object, bool avg) properties.emplace(kGradientPropertyRadius, radius); } - return Object(Gradient(std::move(properties))); + return Object(Gradient(context, std::move(properties))); } std::string @@ -237,12 +237,12 @@ Gradient::operator==(const apl::Gradient &other) const { return mProperties == other.mProperties; } -Gradient::Gradient(std::map&& properties) : +Gradient::Gradient(const Context& context, std::map&& properties) : mProperties(std::move(properties)) { for(auto& m : mProperties.at(kGradientPropertyInputRange).getArray()) mInputRange.emplace_back(m.asNumber()); for(auto& m : mProperties.at(kGradientPropertyColorRange).getArray()) - mColorRange.emplace_back(m.asColor()); + mColorRange.emplace_back(m.asColor(context)); } } // namespace apl diff --git a/aplcore/src/primitives/mediasource.cpp b/aplcore/src/primitives/mediasource.cpp index b18fc93..e78b0d7 100644 --- a/aplcore/src/primitives/mediasource.cpp +++ b/aplcore/src/primitives/mediasource.cpp @@ -45,7 +45,7 @@ MediaSource::create(const Context& context, const Object& object) if (object.isString()) { std::string url = object.asString(); if (url.empty()) { - CONSOLE_CTX(context) << "Empty string for media source"; + CONSOLE(context) << "Empty string for media source"; return Object::NULL_OBJECT(); } return Object(MediaSource(URLRequest::create(context, object).getURLRequest(), @@ -61,7 +61,7 @@ MediaSource::create(const Context& context, const Object& object) std::string url = propertyAsString(context, object, "url"); if(url.empty()) { - CONSOLE_CTX(context) << "Media Source has no URL defined."; + CONSOLE(context) << "Media Source has no URL defined."; return Object::NULL_OBJECT(); } diff --git a/aplcore/src/primitives/object.cpp b/aplcore/src/primitives/object.cpp index 2e4983c..c2b1712 100644 --- a/aplcore/src/primitives/object.cpp +++ b/aplcore/src/primitives/object.cpp @@ -13,8 +13,6 @@ * permissions and limitations under the License. */ -#include - #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" @@ -34,11 +32,13 @@ #include "apl/primitives/mediasource.h" #include "apl/primitives/object.h" #include "apl/primitives/objectdata.h" +#include "apl/primitives/range.h" #include "apl/primitives/rangegenerator.h" #include "apl/primitives/slicegenerator.h" #include "apl/primitives/transform.h" #include "apl/primitives/urlrequest.h" #include "apl/utils/log.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -582,6 +582,13 @@ Object::Object(StyledText&& styledText) LOG_IF(OBJECT_DEBUG) << "Object StyledText constructor " << this; } +Object::Object(Range range) + : mType(DirectObjectData::sType), + mU(DirectObjectData::create(std::move(range))) +{ + LOG_IF(OBJECT_DEBUG) << "Object Range constructor " << this; +} + Object::Object(const GraphicPtr& graphic) : mType(kGraphicType), mU(std::make_shared(graphic)) @@ -673,6 +680,7 @@ Object::operator==(const Object& rhs) const case kURLRequestType: case kTransform2DType: case kStyledTextType: + case kRangeType: case kArrayType: case kMapType: return *(mU.data.get()) == *(rhs.mU.data.get()); @@ -715,10 +723,11 @@ Object::isJson() const } /** - * Return an attractively formatted double for display. - * We drop trailing zeros for decimal numbers. If the number is an integer or rounds - * to an integer, we drop the decimal point as well. + * Return a formatted double for display. The formatted double follows the APL syntax for + * floating-point numbers. Additionally, we drop trailing zeros for decimal numbers. If the number + * is an integer or rounds to an integer, we drop the decimal point as well. * Scientific notation numbers are not handled attractively. + * * @param value The value to format * @return A suitable string */ @@ -732,13 +741,9 @@ doubleToString(double value) return std::to_string(iValue); } - // TODO: Is this cheap enough to run each time? - // If so, we could unit test other languages more easily - static char *separator = std::localeconv()->decimal_point; - - auto s = std::to_string(value); + auto s = sutil::to_string(value); auto it = s.find_last_not_of('0'); - if (it != s.find(separator)) // Remove a trailing decimal point + if (it != s.find(sutil::DECIMAL_POINT)) // Remove a trailing decimal point it++; s.erase(it, std::string::npos); return s; @@ -776,6 +781,7 @@ Object::asString() const case kRadiiType: return ""; case kURLRequestType: return ""; case kStyledTextType: return as().asString(); + case kRangeType: return ""; case kGraphicType: return ""; case kGraphicPatternType: return ""; case kTransformType: return ""; @@ -796,7 +802,7 @@ stringToDouble(const std::string& string) try { auto len = string.size(); auto idx = len; - double result = std::stod(string, &idx); + double result = sutil::stod(string, &idx); // Handle percentages. We skip over whitespace and stop on any other character while (idx < len) { auto c = string[idx]; @@ -804,7 +810,7 @@ stringToDouble(const std::string& string) result *= 0.01; break; } - if (!std::isspace(c)) + if (!sutil::isspace(c)) break; idx++; } @@ -831,6 +837,12 @@ Object::asNumber() const } } +float +Object::asFloat() const +{ + return static_cast(asNumber()); +} + int Object::asInt(int base) const { @@ -1108,6 +1120,11 @@ Object::getStyledText() const { return as(); } +const Range& +Object::getRange() const { + return as(); +} + std::shared_ptr Object::getTransformation() const { assert(mType == kTransformType); return mU.data->getTransform(); @@ -1164,6 +1181,7 @@ Object::truthy() const case kTransform2DType: case kURLRequestType: case kStyledTextType: + case kRangeType: case kGraphicPatternType: return mU.data->truthy(); @@ -1486,6 +1504,7 @@ Object::serialize(rapidjson::Document::AllocatorType& allocator) const case kEasingType: case kTransform2DType: case kStyledTextType: + case kRangeType: case kGraphicPatternType: return mU.data->serialize(allocator); case kGraphicType: @@ -1531,7 +1550,7 @@ Object::toDebugString() const case Object::kBoolType: return (mU.value ? "true" : "false"); case Object::kNumberType: - return std::to_string(mU.value); + return sutil::to_string(mU.value); case Object::kStringType: return "'" + mU.string + "'"; case Object::kMapType: @@ -1540,9 +1559,9 @@ Object::toDebugString() const case Object::kFunctionType: return mU.data->toDebugString(); case Object::kAbsoluteDimensionType: - return "AbsDim<" + std::to_string(mU.value) + ">"; + return "AbsDim<" + sutil::to_string(mU.value) + ">"; case Object::kRelativeDimensionType: - return "RelDim<" + std::to_string(mU.value) + ">"; + return "RelDim<" + sutil::to_string(mU.value) + ">"; case Object::kAutoDimensionType: return "AutoDim"; case Object::kColorType: @@ -1555,6 +1574,7 @@ Object::toDebugString() const case Object::kRadiiType: case Object::kURLRequestType: case Object::kStyledTextType: + case Object::kRangeType: case Object::kGraphicType: case Object::kGraphicPatternType: case Object::kTransformType: diff --git a/aplcore/src/primitives/objectdata.cpp b/aplcore/src/primitives/objectdata.cpp index 1215b0b..e63f99f 100644 --- a/aplcore/src/primitives/objectdata.cpp +++ b/aplcore/src/primitives/objectdata.cpp @@ -19,6 +19,7 @@ #include "apl/primitives/gradient.h" #include "apl/primitives/mediasource.h" #include "apl/primitives/radii.h" +#include "apl/primitives/range.h" #include "apl/primitives/rect.h" #include "apl/primitives/styledtext.h" #include "apl/primitives/transform2d.h" @@ -38,5 +39,6 @@ template<> const Object::ObjectType DirectObjectData::sType = Object::kRa template<> const Object::ObjectType DirectObjectData::sType = Object::kURLRequestType; template<> const Object::ObjectType DirectObjectData::sType = Object::kTransform2DType; template<> const Object::ObjectType DirectObjectData::sType = Object::kStyledTextType; +template<> const Object::ObjectType DirectObjectData::sType = Object::kRangeType; } // namespace apl diff --git a/aplcore/src/primitives/radii.cpp b/aplcore/src/primitives/radii.cpp index b883ab9..3adff78 100644 --- a/aplcore/src/primitives/radii.cpp +++ b/aplcore/src/primitives/radii.cpp @@ -13,20 +13,32 @@ * permissions and limitations under the License. */ -#include +#include #include "apl/primitives/radii.h" #include "apl/utils/streamer.h" +#include "apl/utils/stringfunctions.h" namespace apl { +Radii +Radii::subtract(float value) const +{ + return { + std::max(0.0f, mData[0] - value), + std::max(0.0f, mData[1] - value), + std::max(0.0f, mData[2] - value), + std::max(0.0f, mData[3] - value) + }; +} + std::string Radii::toString() const { std::string result; - for (int i = 0 ; i < mData.size() ; i++) { + for (size_t i = 0 ; i < mData.size() ; i++) { if (i != 0) result += ", "; - result += std::to_string(mData[i]); + result += sutil::to_string(mData[i]); } return result; } @@ -42,9 +54,9 @@ std::string Radii::toDebugString() const { std::string result = "Radii<"; - for (int i = 0 ; i < mData.size() ; i++) { + for (size_t i = 0 ; i < mData.size() ; i++) { if (i != 0) result += ", "; - result += std::to_string(mData[i]); + result += sutil::to_string(mData[i]); } return result + ">"; } @@ -58,4 +70,11 @@ Radii::serialize(rapidjson::Document::AllocatorType& allocator) const { return v; } +void +Radii::sanitize() +{ + for (size_t i = 0 ; i < mData.size() ; i++) + mData[i] = std::max(0.0f, mData[i]); +} + } // namespace apl diff --git a/aplcore/src/primitives/range.cpp b/aplcore/src/primitives/range.cpp new file mode 100644 index 0000000..a1276a1 --- /dev/null +++ b/aplcore/src/primitives/range.cpp @@ -0,0 +1,39 @@ +/** + * 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/range.h" + +namespace apl { + +std::string +Range::toDebugString() const +{ + return std::string("Range<"+std::to_string(mLowerBound)+","+std::to_string(mUpperBound)+">"); +} + +rapidjson::Value +Range::serialize(rapidjson::Document::AllocatorType& allocator) const +{ + using rapidjson::Value; + using rapidjson::StringRef; + + Value span(rapidjson::kObjectType); + span.AddMember("lowerBound", mLowerBound, allocator); + span.AddMember("upperBound", mUpperBound, allocator); + return span; +} + + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/primitives/rect.cpp b/aplcore/src/primitives/rect.cpp index b97ea36..406d46d 100644 --- a/aplcore/src/primitives/rect.cpp +++ b/aplcore/src/primitives/rect.cpp @@ -63,7 +63,7 @@ Rect::toString() const { } bool -Rect::isEmpty() const { +Rect::empty() const { return (mWidth == 0 && mHeight == 0) || std::isnan(mWidth) || std::isnan(mHeight); } @@ -73,12 +73,26 @@ Rect::intersect(const Rect &other) const { getTop() >= other.getBottom() || other.getTop() >= getBottom()) return {}; - float x = std::max(other.getX(), getX()); - float y = std::max(other.getY(), getY()); - return {x, - y, - std::min(other.getRight(), getRight()) - x, - std::min(other.getBottom(), getBottom()) - y}; + auto left = std::max(getLeft(), other.getLeft()); + auto top = std::max(getTop(), other.getTop()); + auto right = std::min(getRight(), other.getRight()); + auto bottom = std::min(getBottom(), other.getBottom()); + + return { left, top, right - left, bottom - top }; +} + +Rect +Rect::extend(const Rect& other) const +{ + if (empty()) return other; + if (other.empty()) return *this; + + auto left = std::min(getLeft(), other.getLeft()); + auto top = std::min(getTop(), other.getTop()); + auto right = std::max(getRight(), other.getRight()); + auto bottom = std::max(getBottom(), other.getBottom()); + + return { left, top, right - left, bottom - top }; } bool @@ -86,7 +100,7 @@ Rect::contains(const Point& point) const { auto x = point.getX(); auto y = point.getY(); - return !isEmpty() && x >= mX && x <= mX + mWidth && y >= mY && y <= mY + mHeight; + return !empty() && x >= mX && x <= mX + mWidth && y >= mY && y <= mY + mHeight; } float @@ -100,6 +114,15 @@ Rect::distanceTo(const Point& point) const { return std::sqrt(dx*dx + dy*dy); } +Rect +Rect::inset(float dx, float dy) const { + auto w = std::max(0.0f, mWidth - 2 * dx); + auto h = std::max(0.0f, mHeight - 2 * dy); + auto x = w <= 0 ? mX + mWidth / 2 : mX + dx; + auto y = h <= 0 ? mY + mHeight / 2 : mY + dy; + return {x, y, w, h}; +} + rapidjson::Value Rect::serialize(rapidjson::Document::AllocatorType& allocator) const { rapidjson::Value v(rapidjson::kArrayType); diff --git a/aplcore/src/primitives/roundedrect.cpp b/aplcore/src/primitives/roundedrect.cpp new file mode 100644 index 0000000..7ba2b53 --- /dev/null +++ b/aplcore/src/primitives/roundedrect.cpp @@ -0,0 +1,81 @@ +/** + * 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/primitives/roundedrect.h" + +namespace apl { + +// Sum two radii. If they are larger than a maximum value, scale them down so they fit. +inline void scaleRadii(float& r1, float& r2, float sumMax) +{ + if (r1 + r2 > sumMax) { + float scale = sumMax / (r1 + r2); + r1 *= scale; + r2 *= scale; + } +} + +// Clip a radius value to between 0 and a maximum value; +inline float clipRadius(float r, float maxRadius) +{ + if (r < 0) return 0; + return r < maxRadius ? r : maxRadius; +} + +RoundedRect::RoundedRect(Rect rect, float radius) + : RoundedRect(rect, Radii{radius}) +{} + +RoundedRect::RoundedRect(Rect rect, Radii radii) + : mRect(rect) +{ + auto w = mRect.getWidth(); + auto h = mRect.getHeight(); + auto side = std::min(w, h); + + // Clip all radii to fit within the side length (and check for negative values) + auto tl = clipRadius(radii.topLeft(), side); + auto tr = clipRadius(radii.topRight(), side); + auto bl = clipRadius(radii.bottomLeft(), side); + auto br = clipRadius(radii.bottomRight(), side); + + // If the sum of the radii on any given side is greater than the side length, they need to be scaled down + scaleRadii(tl, bl, h); + scaleRadii(tr, br, h); + + // Repeat the scaling, this time for top and bottom + scaleRadii(tl, tr, w); + scaleRadii(bl, br, w); + + mRadii = {tl, tr, bl, br}; +} + +RoundedRect +RoundedRect::inset(float inset) const +{ + return { + mRect.inset(inset), + mRadii.subtract(inset) + }; +} + +std::string +RoundedRect::toDebugString() const +{ + return mRect.toDebugString() + ":" + mRadii.toDebugString(); +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/primitives/styledtext.cpp b/aplcore/src/primitives/styledtext.cpp index 5cffd66..d4cfcf3 100644 --- a/aplcore/src/primitives/styledtext.cpp +++ b/aplcore/src/primitives/styledtext.cpp @@ -74,6 +74,12 @@ static inline std::string stripControl(const std::string& str) return output; } +/** + * \cond ShowStyledTextGrammar + */ + +namespace styledtextgrammar { + struct quote : sor, one<'\''>> {}; struct attributename : plus{}; struct attributevalue : plus>{}; @@ -223,12 +229,41 @@ template<> struct action< hexentity > } }; -Object -StyledText::create(const Context& context, const Object& object) { +} // namespace styledtextgrammar + +/** + * \endcond + */ + +StyledText +StyledText::create(const Context& context, const Object& object) +{ if (object.isStyledText()) - return object; + return object.getStyledText(); + + return {context, object.asString()}; +} + +StyledText +StyledText::createRaw(const std::string& raw) +{ + return StyledText(raw); +} + +StyledText& +StyledText::operator=(const StyledText& other) { + if (this == &other) + return *this; - return Object(StyledText(context, object.asString())); + mRawText = other.mRawText; + mText = other.mText; + mSpans = other.mSpans; + return *this; +} + +StyledText::StyledText(const std::string& raw) + : mRawText(raw), mText(raw) +{ } StyledText::StyledText(const Context& context, const std::string& raw) { @@ -237,7 +272,7 @@ StyledText::StyledText(const Context& context, const std::string& raw) { auto state = StyledTextState(context); pegtl::string_input<> in(filtered, ""); - pegtl::parse(in, state); + pegtl::parse(in, state); mText = state.getText(); mSpans = state.finalize(); @@ -285,7 +320,7 @@ StyledText::Iterator::getSpanType() const { StyledText::Iterator::TokenType StyledText::Iterator::next() { - const auto& spans = mStyledText.getSpans(); + const auto& spans = mStyledText.mSpans; size_t nextStartSpanPosition = mSpanIndex < spans.size() ? spans[mSpanIndex].start : std::numeric_limits::max(); size_t nextEndSpanPosition = mStack.empty() ? std::numeric_limits::max() : mStack.top()->end; size_t next = std::min({nextStartSpanPosition, nextEndSpanPosition, codePointCount}); @@ -312,4 +347,9 @@ StyledText::Iterator::next() { return kEnd; } +size_t +StyledText::Iterator::spanCount() { + return mStyledText.mSpans.size(); +} + } // namespace apl diff --git a/aplcore/src/primitives/symbolreferencemap.cpp b/aplcore/src/primitives/symbolreferencemap.cpp index 8f8e122..3abc229 100644 --- a/aplcore/src/primitives/symbolreferencemap.cpp +++ b/aplcore/src/primitives/symbolreferencemap.cpp @@ -25,9 +25,9 @@ namespace apl { * 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 name - * @param map - * @return + * @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) @@ -82,4 +82,4 @@ SymbolReferenceMap::toDebugString() const -} // namespace apl \ No newline at end of file +} // namespace apl diff --git a/aplcore/src/primitives/transform.cpp b/aplcore/src/primitives/transform.cpp index 7ca5085..65cf3cb 100644 --- a/aplcore/src/primitives/transform.cpp +++ b/aplcore/src/primitives/transform.cpp @@ -124,7 +124,7 @@ class TranslateTransform : public Transform { static std::unique_ptr transformFromElement(const Context& context, const Object& element) { if (!element.isMap()) { - CONSOLE_CTX(context) << "Illegal transform element " << element; + CONSOLE(context) << "Illegal transform element " << element; return nullptr; } @@ -167,7 +167,7 @@ static std::unique_ptr transformFromElement(const Context& context, c return std::make_unique(tx, ty); } - CONSOLE_CTX(context) << "Transform element doesn't have a valid property" << element; + CONSOLE(context) << "Transform element doesn't have a valid property" << element; return nullptr; } @@ -237,7 +237,7 @@ class InterpolatedTransformationImpl : public InterpolatedTransformation { { auto len = std::min(from.size(), to.size()); if (len != from.size() || len != to.size()) - CONSOLE_CTX(context) << "Mismatched transformation lengths"; + CONSOLE(context) << "Mismatched transformation lengths"; for (int i = 0 ; i < len ; i++) { auto fromTransform = transformFromElement(context, from.at(i)); @@ -251,7 +251,7 @@ class InterpolatedTransformationImpl : public InterpolatedTransformation { auto fromType = fromTransform->getType(); auto toType = toTransform->getType(); if (fromType != toType) { - CONSOLE_CTX(context) << "Type mismatch between animation elements " << i << " from:" << fromType << " to:" << toType; + CONSOLE(context) << "Type mismatch between animation elements " << i << " from:" << fromType << " to:" << toType; continue; } diff --git a/aplcore/src/primitives/transform2d.cpp b/aplcore/src/primitives/transform2d.cpp index 82e2875..3fb3e09 100644 --- a/aplcore/src/primitives/transform2d.cpp +++ b/aplcore/src/primitives/transform2d.cpp @@ -15,8 +15,9 @@ #include -#include "apl/utils/session.h" #include "apl/primitives/transform2d.h" +#include "apl/utils/session.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -100,7 +101,7 @@ struct transform_state { for (int i = 0 ; i < arg_count ; i++) { if (i > 0) result += " "; - result += std::to_string(args[i]); + result += sutil::to_string(args[i]); } return result; } @@ -161,7 +162,7 @@ template<> struct action< number > template< typename Input > static void apply( const Input& in, transform_state& state) { std::string s(in.string()); - double value = stod(s); + double value = sutil::stod(s); LOGF_IF(DEBUG_GRAMMAR, "Number: '%s' -> %lf", in.string().c_str(), value); state.push(value); @@ -235,9 +236,9 @@ operator<<(streamer& os, const Transform2D& transform) std::string Transform2D::toDebugString() const { - auto result = "Transform2D<" + std::to_string(mData[0]); + auto result = "Transform2D<" + sutil::to_string(mData[0]); for (int i = 1 ; i < 6 ; i++) - result += ", " + std::to_string(mData[i]); + result += ", " + sutil::to_string(mData[i]); return result + ">"; } @@ -253,7 +254,7 @@ Transform2D::parse(const SessionPtr& session, const std::string& transform) return state.transform; } catch (pegtl::parse_error error) { - CONSOLE_S(session) << "Error parsing transform '" << transform << "'" << error.what(); + CONSOLE(session) << "Error parsing transform '" << transform << "'" << error.what(); } return Transform2D(); diff --git a/aplcore/src/primitives/unicode.cpp b/aplcore/src/primitives/unicode.cpp index 766f3e6..22fdb50 100644 --- a/aplcore/src/primitives/unicode.cpp +++ b/aplcore/src/primitives/unicode.cpp @@ -16,6 +16,8 @@ #include "apl/primitives/unicode.h" #include +#include +#include namespace apl { @@ -73,9 +75,30 @@ utf8AdvanceCodePointsUnsafe(uint8_t *ptr, int n) return ptr; } +/** + * Compare two UTF-8 characters for smaller, equal, or larger. + * The UTF-8 encoding conveniently allows for direct byte-to-byte comparisons. + * @param lhs The first character + * @param rhs The second character + * @param trailing The maximum number of trailing bytes to compare + * @return -1 if lhs < rhs, +1 if lhs > rhs, 0 if they are equal + */ +int +compareUTF8(const uint8_t *lhs, const uint8_t *rhs, const int trailing) +{ + for (int i = 0 ; i <= trailing ; lhs++, rhs++, i++) { + if (*lhs < *rhs) + return -1; + if (*lhs > *rhs) + return 1; + } + return 0; +} + + int utf8StringLength(const std::string& utf8String) { - uint8_t *ptr = (uint8_t *) utf8String.data(); + uint8_t *ptr = (uint8_t *) utf8String.c_str(); int length = 0; while (*ptr) { @@ -112,10 +135,140 @@ utf8StringSlice(const std::string& utf8String, int start, int end) if (end <= start) return ""; - auto startPtr = utf8AdvanceCodePointsUnsafe((uint8_t*)utf8String.data(), start); + auto startPtr = utf8AdvanceCodePointsUnsafe((uint8_t*)utf8String.c_str(), start); auto endPtr = utf8AdvanceCodePointsUnsafe(startPtr, end - start); return std::string((char *)startPtr, endPtr - startPtr); } +/** + * Internal method to check a single UTF-8 character and see if it appears in a + * string of valid characters. This method assumes that the inputs are valid UTF-8 strings + * and that the validation string is non-empty. + * + * The APL specification states that the format of the valid string is: + * + * VSTRING + * '' // Empty string + * ('-')? CODE* // Optional hyphen followed by zero or more CODES + * CODE + * CHAR '-' CHAR // Character, hyphen, character + * CHAR // Single unicode character + * CHAR + * '0001' . '10FFFF' // Any character in the range 0x01 to 0x10FFFF + * + * The APL specification states that an empty validation string accepts everything + * + * This implementation uses linear search to find UTF-8 code units in the valid character + * string. That makes the performance O(N^2), which could be improved with the construction of + * some lookup bit streams, valid character set sorting (into 1-4 code unit sets), etc. + * The advantage of this approach is that no intermediate data structures need to be + * constructed or stored. + * + * @param ptr Pointer to the UTF-8 character to test for validity + * @param pcount Number of trailing bytes in ptr + * @param vptr Pointer to the UTF-8 string of valid characters and character ranges + * @return True if this character is a valid character + */ +bool +utf8ValidCharacter(const uint8_t *ptr, const int pcount, const uint8_t *vptr) +{ + while (*vptr) { + auto lower = compareUTF8(ptr, vptr, pcount); + if (lower == 0) + return true; + + vptr += countUTF8TrailingBytes(*vptr) + 1; + + // Check for a hyphen + if (*vptr == '-') { + vptr++; + + if (!*vptr) + return false; // Trailing hyphen is ignored + + auto upper = compareUTF8(ptr, vptr, pcount); + if (lower == 1 && upper != 1) // Greater than lower and less than or equal to upper + return true; + + vptr += countUTF8TrailingBytes(*vptr) + 1; + } + } + + return false; +} + +std::string +utf8StripInvalid(const std::string& utf8String, const std::string& validCharacters) +{ + if (validCharacters.empty()) + return utf8String; + + uint8_t *ptr = (uint8_t *) utf8String.c_str(); + uint8_t *vptr = (uint8_t *) validCharacters.c_str(); + + // For now we'll just copy valid code points over + std::string result; + + while (*ptr) { + const auto pcount = countUTF8TrailingBytes(*ptr); + if (utf8ValidCharacter(ptr, pcount, vptr)) + result.append((char *) ptr, pcount + 1); + ptr += pcount + 1; + } + + return result; +} + +bool +utf8ValidCharacters(const std::string& utf8String, const std::string& validCharacters) +{ + if (validCharacters.empty()) + return true; + + uint8_t *ptr = (uint8_t *) utf8String.c_str(); + uint8_t *vptr = (uint8_t *) validCharacters.c_str(); + + while (*ptr) { + const auto pcount = countUTF8TrailingBytes(*ptr); + if (!utf8ValidCharacter(ptr, pcount, vptr)) + return false; + ptr += pcount + 1; + } + + return true; +} + +bool +wcharValidCharacter(wchar_t wc, const std::string& validCharacters) +{ + if (validCharacters.empty()) + return true; + + // Convert the single character into a UTF-8 string for checking. + static std::wstring_convert> converter; + auto result = converter.to_bytes(wc); + auto* ptr = result.c_str(); + return utf8ValidCharacter((uint8_t*)ptr, + countUTF8TrailingBytes(*ptr), + (uint8_t*)validCharacters.c_str()); +} + +bool +utf8StringTrim(std::string& utf8String, int maxLength) +{ + if (maxLength <= 0) + return false; + + auto it = utf8String.begin(); + for (int i = 0 ; i < maxLength ; i++) { + if (*it == 0) + return false; + it += countUTF8TrailingBytes(*it) + 1; + } + + utf8String.erase(it, utf8String.end()); + return true; +} + } // namespace apl diff --git a/aplcore/src/primitives/urlrequest.cpp b/aplcore/src/primitives/urlrequest.cpp index 6e7f484..ce696ba 100644 --- a/aplcore/src/primitives/urlrequest.cpp +++ b/aplcore/src/primitives/urlrequest.cpp @@ -77,7 +77,7 @@ URLRequest::create(const Context& context, const Object& object) { auto url = propertyAsString(context, object, "url"); if(url.empty()) { - CONSOLE_CTX(context) << "Source has no URL defined."; + CONSOLE(context) << "Source has no URL defined."; return Object::NULL_OBJECT(); } return URLRequest{ url, diff --git a/aplcore/src/time/sequencer.cpp b/aplcore/src/time/sequencer.cpp index b380fde..a224587 100644 --- a/aplcore/src/time/sequencer.cpp +++ b/aplcore/src/time/sequencer.cpp @@ -154,7 +154,7 @@ Sequencer::executeCommands(const Object& commands, return nullptr; if (!commands.isArray()) { - LOG(LogLevel::kError) << "executeCommands: invalid command list"; + LOG(LogLevel::kError).session(context) << "executeCommands: invalid command list"; return nullptr; } @@ -162,7 +162,7 @@ Sequencer::executeCommands(const Object& commands, return nullptr; if (!context->has("event") && !fastMode) - LOG(LogLevel::kWarn) << "missing event in context"; + LOG(LogLevel::kWarn).session(context) << "missing event in context"; Properties props; auto commandPtr = ArrayCommand::create(context, commands, baseComponent, props, ""); @@ -180,7 +180,7 @@ Sequencer::executeCommandsOnSequencer(const Object& commands, return nullptr; if (!commands.isArray()) { - LOG(LogLevel::kError) << "executeCommands: invalid command list"; + LOG(LogLevel::kError).session(context) << "executeCommands: invalid command list"; return nullptr; } @@ -188,7 +188,7 @@ Sequencer::executeCommandsOnSequencer(const Object& commands, return nullptr; if (!context->has("event")) - LOG(LogLevel::kWarn) << "missing event in context"; + LOG(LogLevel::kWarn).session(context) << "missing event in context"; Properties props; auto commandPtr = ArrayCommand::create(context, commands, baseComponent, props, ""); diff --git a/aplcore/src/touch/gesture.cpp b/aplcore/src/touch/gesture.cpp index ba8782f..4f7d465 100644 --- a/aplcore/src/touch/gesture.cpp +++ b/aplcore/src/touch/gesture.cpp @@ -58,7 +58,7 @@ Gesture::create(const ActionablePtr& actionable, const Object& object) { auto type = propertyAsMapped(*contextPtr, object, "type", kGestureTypeDoublePress, sGestureTypeBimap); if (type == static_cast(-1)) { - CONSOLE_CTX(*contextPtr) << "Unrecognized type field in gesture handler"; + CONSOLE(*contextPtr) << "Unrecognized type field in gesture handler"; return nullptr; } diff --git a/aplcore/src/touch/gestures/pagerflinggesture.cpp b/aplcore/src/touch/gestures/pagerflinggesture.cpp index 40f0ee3..a0e1c31 100644 --- a/aplcore/src/touch/gestures/pagerflinggesture.cpp +++ b/aplcore/src/touch/gestures/pagerflinggesture.cpp @@ -110,7 +110,7 @@ PagerFlingGesture::reset() { bool PagerFlingGesture::onDown(const PointerEvent& event, apl_time_t timestamp) { - LOG_IF(DEBUG_FLING_GESTURE) << "event: " << event.pointerEventPosition.toString() << ", timestamp: " << timestamp; + LOG_IF(DEBUG_FLING_GESTURE).session(mActionable) << "event: " << event.pointerEventPosition.toString() << ", timestamp: " << timestamp; // We don't change layout direction during a gesture mLayoutDirection = static_cast(mActionable->getCalculated(kPropertyLayoutDirection).asInt()); @@ -135,7 +135,7 @@ PagerFlingGesture::onDown(const PointerEvent& event, apl_time_t timestamp) mStartPosition += distanceShift; // Restore old start time to avoid move timeout mStartTime = startTime; - LOG_IF(DEBUG_FLING_GESTURE) << "Chaining. distanceShift:" << distanceShift + LOG_IF(DEBUG_FLING_GESTURE).session(mActionable) << "Chaining. distanceShift:" << distanceShift << ", mStartPosition: " << mStartPosition.toString() << ", mAmount: " << mAmount << ", mLastAnimationAmount: " << mLastAnimationAmount; return true; @@ -158,11 +158,11 @@ PagerFlingGesture::onMove(const PointerEvent& event, apl_time_t timestamp) ? (distance < 0 ? kPageDirectionBack : kPageDirectionForward) : (distance < 0 ? kPageDirectionForward : kPageDirectionBack); - LOG_IF(DEBUG_FLING_GESTURE) << "Distance: " << distance << ", direction: " << direction; + LOG_IF(DEBUG_FLING_GESTURE).session(mActionable) << "Distance: " << distance << ", direction: " << direction; if (mTriggered && direction != mPageDirection) { mTriggered = false; - LOG_IF(DEBUG_FLING_GESTURE) << "Reverse direction from: " << mPageDirection; + LOG_IF(DEBUG_FLING_GESTURE).session(mActionable) << "Reverse direction from: " << mPageDirection; } auto horizontal = mActionable->isHorizontal(); @@ -181,7 +181,7 @@ PagerFlingGesture::onMove(const PointerEvent& event, apl_time_t timestamp) } mTriggered = true; - LOG_IF(DEBUG_FLING_GESTURE) << "Triggered"; + LOG_IF(DEBUG_FLING_GESTURE).session(mActionable) << "Triggered"; mResourceHolder->takeResource(); mPageDirection = direction; mTargetPage = calculateTargetPage(mActionable, mPageDirection, mCurrentPage); @@ -192,7 +192,7 @@ PagerFlingGesture::onMove(const PointerEvent& event, apl_time_t timestamp) if (mTriggered) { mAmount = getTranslationAmount(mActionable, distance); if (mAmount > 1.0f) { - LOG_IF(DEBUG_FLING_GESTURE) << "Moved over 100%, restarting with new page."; + LOG_IF(DEBUG_FLING_GESTURE).session(mActionable) << "Moved over 100%, restarting with new page."; // Reset start of the gesture, we effectively switched the page pager->endPageMove(true, ActionRef(nullptr), false); @@ -302,7 +302,7 @@ PagerFlingGesture::finishUp() auto globalToLocal = mActionable->getGlobalToLocalTransform(); auto scaleFactor = (horizontal ? globalToLocal.getXScaling() : globalToLocal.getYScaling()); if (std::isnan(scaleFactor)) { - CONSOLE_CTP(mActionable->getContext()) + CONSOLE(mActionable->getContext()) << "Singular transform encountered during page switch. Animation impossible, resetting."; // Reset the state of the component auto pager = std::dynamic_pointer_cast(mActionable); @@ -327,7 +327,7 @@ PagerFlingGesture::finishUp() // In case if we don't get enough fling or distance or fling in opposite direction - snap back. if ((mAmount < 0.5f && std::abs(velocity) < (minFlingVelocity / time::MS_PER_SECOND)) || direction != mPageDirection) { - LOG(LogLevel::kDebug) << "Do not fling with velocity: " << velocity << ", amount :" << mAmount + LOG(LogLevel::kDebug).session(mActionable) << "Do not fling with velocity: " << velocity << ", amount :" << mAmount << ", expected direction: " << mPageDirection << ", direction: " << direction; fulfill = false; } diff --git a/aplcore/src/touch/gestures/scrollgesture.cpp b/aplcore/src/touch/gestures/scrollgesture.cpp index 046985b..fe02905 100644 --- a/aplcore/src/touch/gestures/scrollgesture.cpp +++ b/aplcore/src/touch/gestures/scrollgesture.cpp @@ -49,7 +49,7 @@ ScrollGesture::ScrollGesture(const ActionablePtr& actionable) void ScrollGesture::release() { - LOG_IF(DEBUG_SCROLL_GESTURE) << "Release"; + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << "Release"; mResourceHolder->release(); reset(); } @@ -57,7 +57,7 @@ ScrollGesture::release() void ScrollGesture::reset() { - LOG_IF(DEBUG_SCROLL_GESTURE) << "Reset"; + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << "Reset"; FlingGesture::reset(); if (isTriggered()) @@ -103,7 +103,7 @@ ScrollGesture::onMove(const PointerEvent& event, apl_time_t timestamp) bool hasPositionChanged = scrollPosition != getOrCalculateNewScrollPosition(); if (hasPositionChanged) { - LOG_IF(DEBUG_SCROLL_GESTURE) << "Triggering"; + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << "Triggering"; mTriggered = true; mResourceHolder->takeResource(); } @@ -145,7 +145,7 @@ ScrollGesture::getVelocityLimit(const Point& travel) { auto directionalTravel = std::abs(scrollable->isVertical() ? travel.getY() : travel.getX()); - LOG_IF(DEBUG_SCROLL_GESTURE) << "maxTravel " << maxTravel << " directionalTravel " << directionalTravel; + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << "maxTravel " << maxTravel << " directionalTravel " << directionalTravel; auto maxVelocity = toLocalThreshold(rootConfig.getProperty(RootProperty::kMaximumFlingVelocity).getDouble()); @@ -156,7 +156,7 @@ ScrollGesture::getVelocityLimit(const Point& travel) { // Just linear distribution from minVelocity to maxVelocity across 0 to longFlingStart as limit limit = maxVelocity * velocityEasing->calc(alpha); - LOG_IF(DEBUG_SCROLL_GESTURE) << " maxVelocity " << maxVelocity << " alpha " << alpha << " limit " << limit; + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << " maxVelocity " << maxVelocity << " alpha " << alpha << " limit " << limit; } return limit / time::MS_PER_SECOND; @@ -172,7 +172,7 @@ ScrollGesture::onUp(const PointerEvent& event, apl_time_t timestamp) auto velocities = toLocalVector(mVelocityTracker->getEstimatedVelocity()); auto velocity = scrollable->isVertical() ? velocities.getY() : velocities.getX(); if (std::isnan(velocity)) { - CONSOLE_CTP(scrollable->getContext()) << "Singularity encountered during scroll, resetting"; + CONSOLE(scrollable->getContext()) << "Singularity encountered during scroll, resetting"; reset(); return false; } @@ -186,7 +186,7 @@ ScrollGesture::onUp(const PointerEvent& event, apl_time_t timestamp) auto delta = position - mStartPosition; auto velocityLimit = getVelocityLimit(delta); if (std::abs(velocity) > velocityLimit) { - LOG_IF(DEBUG_SCROLL_GESTURE) + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << "Velocity " << velocity << " is too fast, reset to " << velocityLimit; velocity = velocity > 0 ? velocityLimit : -velocityLimit; velocities = Point(velocity, velocity); @@ -209,7 +209,7 @@ ScrollGesture::onUp(const PointerEvent& event, apl_time_t timestamp) } else if (scrollable->shouldForceSnap()) { scrollToSnap(); } else { - LOG_IF(DEBUG_SCROLL_GESTURE) << "Velocity " << velocity << " is too low, do not fling."; + LOG_IF(DEBUG_SCROLL_GESTURE).session(mActionable) << "Velocity " << velocity << " is too low, do not fling."; reset(); } return true; diff --git a/aplcore/src/touch/gestures/swipeawaygesture.cpp b/aplcore/src/touch/gestures/swipeawaygesture.cpp index 1f9b100..74fd2d2 100644 --- a/aplcore/src/touch/gestures/swipeawaygesture.cpp +++ b/aplcore/src/touch/gestures/swipeawaygesture.cpp @@ -50,13 +50,13 @@ SwipeAwayGesture::create(const ActionablePtr& actionable, const Context& context auto action = propertyAsMapped(context, object, "action", kSwipeAwayActionSlide, sSwipeAwayActionTypeBimap); if (action == static_cast(-1)) { - CONSOLE_CTX(context) << "Unrecognized action field in SwipeAway gesture handler"; + CONSOLE(context) << "Unrecognized action field in SwipeAway gesture handler"; return nullptr; } auto direction = propertyAsMapped(context, object, "direction", -1, sSwipeDirectionMap); if (direction < 0) { - CONSOLE_CTX(context) << "Unrecognized direction field in SwipeAway gesture handler"; + CONSOLE(context) << "Unrecognized direction field in SwipeAway gesture handler"; return nullptr; } @@ -414,7 +414,7 @@ SwipeAwayGesture::onUp(const PointerEvent& event, apl_time_t timestamp) { auto velocity = getCurrentVelocity(); if (std::isnan(velocity)) { - CONSOLE_CTP(mActionable->getContext()) << "Singular transform encountered during " + CONSOLE(mActionable->getContext()) << "Singular transform encountered during " "SwipeAway, aborting swipe"; animateRemainder(false, 0); } else { diff --git a/aplcore/src/touch/pointermanager.cpp b/aplcore/src/touch/pointermanager.cpp index 0753830..1b6e24c 100644 --- a/aplcore/src/touch/pointermanager.cpp +++ b/aplcore/src/touch/pointermanager.cpp @@ -133,7 +133,7 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t break; case kPointerTargetChanged: default: - LOG(LogLevel::kWarn) << "Unknown pointer event type ignored" + LOG(LogLevel::kWarn).session(mCore.session()) << "Unknown pointer event type ignored" << pointerEvent.pointerEventType; return false; } diff --git a/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp b/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp index 4e1e561..93f158d 100644 --- a/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp +++ b/aplcore/src/touch/utils/unidirectionaleasingscroller.cpp @@ -35,7 +35,7 @@ UnidirectionalEasingScroller::make( velocity.getY() : velocity.getX(); if (directionalVelocity == 0) { - LOG(LogLevel::kWarn) << "Can't create a scroller with 0 velocity"; + LOG(LogLevel::kWarn).session(scrollable) << "Can't create a scroller with 0 velocity"; return nullptr; } @@ -43,7 +43,7 @@ UnidirectionalEasingScroller::make( auto decelerationRate = rootConfig.getProperty(RootProperty::kUEScrollerDeceleration).getDouble(); auto deceleration = -directionalVelocity * decelerationRate; - LOG_IF(DEBUG_SCROLLER) + LOG_IF(DEBUG_SCROLLER).session(scrollable) << "Velocity: " << velocity.toString() << ", Deceleration" << decelerationRate; @@ -81,7 +81,7 @@ UnidirectionalEasingScroller::UnidirectionalEasingScroller( mEndTarget(target), mDuration(duration) { - LOG_IF(DEBUG_SCROLLER) + LOG_IF(DEBUG_SCROLLER).session(scrollable) << "StartPos: " << mScrollStartPosition.toString() << ", LastPos: " << mLastScrollPosition.toString() << ", TargetDistance: " << mEndTarget.toString(); @@ -131,8 +131,8 @@ UnidirectionalEasingScroller::update( if (offset <= 0) return; bool isFinished = (horizontal && isRTL) - ? (target >= 0 || (endTarget < target && availableTarget >= target) || offset == mDuration) - : (target <= 0 || (endTarget > target && availableTarget <= target) || offset == mDuration); + ? (target >= 0 || (endTarget < target && availableTarget > target) || offset == mDuration) + : (target <= 0 || (endTarget > target && availableTarget < target) || offset == mDuration); if (isFinished) { finish(); } @@ -149,7 +149,7 @@ UnidirectionalEasingScroller::fixFlingStartPosition(const std::shared_ptr #include "apl/utils/corelocalemethods.h" +#include "apl/utils/stringfunctions.h" namespace apl { @@ -25,7 +26,9 @@ CoreLocaleMethods::toUpperCase(const std::string& value, const std::string& loca result.resize(value.size()); std::transform(value.begin(), value.end(), result.begin(), - [](unsigned char c) -> unsigned char { return std::toupper(c); }); + [](char c) -> char { + return sutil::toupper(c); + }); return result; } @@ -36,7 +39,9 @@ CoreLocaleMethods::toLowerCase(const std::string& value, const std::string& loca result.resize(value.size()); std::transform(value.begin(), value.end(), result.begin(), - [](unsigned char c) -> unsigned char { return std::tolower(c); }); + [](char c) -> char { + return sutil::tolower(c); + }); return result; } diff --git a/aplcore/src/utils/log.cpp b/aplcore/src/utils/log.cpp index bfd7304..fa780e3 100644 --- a/aplcore/src/utils/log.cpp +++ b/aplcore/src/utils/log.cpp @@ -14,10 +14,20 @@ */ #include "apl/utils/log.h" +#include "apl/utils/session.h" +#include "apl/component/corecomponent.h" +#include "apl/command/corecommand.h" +#include "apl/engine/context.h" +#include "apl/engine/rootcontext.h" +#include "apl/content/rootconfig.h" namespace apl { -Logger::Logger(std::shared_ptr bridge, LogLevel level, const std::string& file, const std::string& function): +Logger::Logger( + const std::shared_ptr& bridge, + LogLevel level, + const std::string& file, + const std::string& function): mUncaught{std::uncaught_exception()}, mBridge{bridge}, mLevel{level} @@ -28,10 +38,80 @@ Logger::Logger(std::shared_ptr bridge, LogLevel level, const std::str Logger::~Logger() { if(mLevel > LogLevel::kNone) { - mBridge->transport(mLevel, (mUncaught != std::uncaught_exception()) ? "***" : "" + mStringStream.str()); + mBridge->transport(mLevel, + (mLogId.empty() ? "" : (mLogId + ":")) + + ((mUncaught != std::uncaught_exception()) ? "***" : "" + mStringStream.str())); } } +Logger& +Logger::session(const Session& session) +{ + mLogId = session.getLogId(); + return *this; +} + +Logger& +Logger::session(const SessionPtr& session) +{ + if (session) mLogId = session->getLogId(); + return *this; +} + +Logger& +Logger::session(const Context& context) +{ + return session(context.session()); +} + +Logger& +Logger::session(const ConstContextPtr& context) +{ + return context ? session(context->session()) : *this; +} + +Logger& +Logger::session(const ContextPtr& context) +{ + return context ? session(context->session()) : *this; +} + +Logger& +Logger::session(const RootConfigPtr& config) +{ + return config ? session(config->getSession()) : *this; +} + +Logger& +Logger::session(const RootConfig& config) +{ + return session(config.getSession()); +} + +Logger& +Logger::session(const RootContextPtr& root) +{ + return root ? session(root->getSession()) : *this; +} + +Logger& +Logger::session(const Component& component) +{ + return session(component.getContext()->session()); +} + +Logger& +Logger::session(const ComponentPtr& component) +{ + return component ? session(component->getContext()->session()) : *this; +} + +Logger& +Logger::session(const ConstCommandPtr& command) +{ + return command ? session(std::dynamic_pointer_cast(command)->context()->session()) : *this; +} + void Logger::log(const char *format, ...) { @@ -64,26 +144,31 @@ Logger::log(const char *format, va_list args) } LoggerFactory& -LoggerFactory::instance() { +LoggerFactory::instance() +{ static LoggerFactory instance; return instance; } void -LoggerFactory::initialize(std::shared_ptr bridge) { +LoggerFactory::initialize(const std::shared_ptr& bridge) +{ mLogBridge = bridge; mInitialized = true; + mWarned = false; } void -LoggerFactory::reset() { +LoggerFactory::reset() +{ mLogBridge = std::make_shared(); mInitialized = false; mWarned = false; } Logger -LoggerFactory::getLogger(LogLevel level, const std::string& file, const std::string& function) { +LoggerFactory::getLogger(LogLevel level, const std::string& file, const std::string& function) +{ if(!mInitialized && !mWarned) { Logger(mLogBridge, LogLevel::kWarn, __FILE__, __func__) << "Logs not initialized. Using default bridge."; mWarned = true; diff --git a/aplcore/src/utils/searchvisitor.cpp b/aplcore/src/utils/searchvisitor.cpp index 4079b55..a8bda02 100644 --- a/aplcore/src/utils/searchvisitor.cpp +++ b/aplcore/src/utils/searchvisitor.cpp @@ -35,7 +35,7 @@ SearchVisitor::visit(const CoreComponent& component) { return; } - LOG_IF(DEBUG_SEARCH) << "Checking " << component.toDebugSimpleString() + LOG_IF(DEBUG_SEARCH).session(component) << "Checking " << component.toDebugSimpleString() << " bounds=" << component.getCalculated(kPropertyBounds).toDebugString() << " point=" << pointInCurrent.toString() << " scrollPosition=" << component.scrollPosition(); @@ -49,7 +49,7 @@ SearchVisitor::visit(const CoreComponent& component) { // if the component satisfies the spot condition, cache it as a potential result so we can avoid a stack mPotentialResult = std::const_pointer_cast(component.shared_from_corecomponent()); - LOG_IF(DEBUG_SEARCH) << "Found potential result " << component.toDebugSimpleString(); + LOG_IF(DEBUG_SEARCH).session(component) << "Found potential result " << component.toDebugSimpleString(); } // If we reached the a leaf node, then keep the best result encountered so far. We specifically diff --git a/aplcore/src/utils/session.cpp b/aplcore/src/utils/session.cpp index a7e9f81..baa0b3d 100644 --- a/aplcore/src/utils/session.cpp +++ b/aplcore/src/utils/session.cpp @@ -15,10 +15,32 @@ #include "apl/content/rootconfig.h" #include "apl/engine/context.h" +#include "apl/utils/random.h" #include "apl/utils/session.h" namespace apl { +static const size_t LOG_PREFIX_SIZE = 6; +static const size_t LOG_ID_SIZE = 10; + +Session::Session() : mLogId(Random::generateSimpleToken(LOG_ID_SIZE)) {} + +void +Session::setLogIdPrefix(const std::string& prefix) { + mLogId = mLogId.substr(mLogId.size() - LOG_ID_SIZE); + auto resultPrefix = prefix; + if (!resultPrefix.empty()) { + resultPrefix.erase(std::remove_if(resultPrefix.begin(), resultPrefix.end(), + [](unsigned char c) { + return !sutil::isupper(c); + }), resultPrefix.end()); + if (!resultPrefix.empty()) { + resultPrefix.resize(LOG_PREFIX_SIZE, '_'); + mLogId = resultPrefix + "-" + mLogId; + } + } +} + SessionMessage::SessionMessage(const SessionPtr& session, const char *filename, const char *function) : mSession(session), mFilename(filename), @@ -86,7 +108,7 @@ SessionMessage& SessionMessage::log(const char *format, ...) class DefaultSession : public Session { public: void write(const char *filename, const char *func, const char *value) override { - LoggerFactory::instance().getLogger(LogLevel::kWarn, filename, func).log("%s", value); + LoggerFactory::instance().getLogger(LogLevel::kWarn, filename, func).session(*this).log("%s", value); } }; diff --git a/aplcore/src/utils/stringfunctions.cpp b/aplcore/src/utils/stringfunctions.cpp index e4c90c9..58f819c 100644 --- a/aplcore/src/utils/stringfunctions.cpp +++ b/aplcore/src/utils/stringfunctions.cpp @@ -14,43 +14,397 @@ */ #include "apl/utils/stringfunctions.h" + #include +#include +#include +#include +#include namespace apl { std::string -rtrim(const std::string &str) { +rtrim(const std::string &str) +{ std::string output(str); output.erase(std::find_if(output.rbegin(), output.rend(), [](unsigned char ch) { - return !std::isspace(ch); + return !sutil::isspace(ch); }).base(), output.end()); return output; } std::string -ltrim(const std::string &str) { +ltrim(const std::string &str) +{ std::string output(str); output.erase(output.begin(), std::find_if(output.begin(), output.end(), [](unsigned char ch) { - return !std::isspace(ch); + return !sutil::isspace(ch); })); return output; } std::string -trim(const std::string &str) { +trim(const std::string &str) +{ return rtrim(ltrim(str)); } std::string -tolower(const std::string& str) { +rpad(const std::string &str, std::size_t minWidth, char padChar) +{ + auto slen = str.length(); + if (slen >= minWidth) return str; + + return str + std::string(minWidth - slen, padChar); +} + +std::string +lpad(const std::string &str, std::size_t minWidth, char padChar) +{ + auto slen = str.length(); + if (slen >= minWidth) return str; + + return std::string(minWidth - slen, padChar) + str; +} + +std::string +tolower(const std::string& str) +{ std::string output = str; std::transform(output.begin(), output.end(), output.begin(), [](unsigned char ch) { - return std::tolower(ch); + return sutil::tolower(ch); }); return output; } +namespace sutil { + +// ---- Internal utilities for parsing/formatting floating-point values + +/** + * Tries to consume the candidate token characters (case-insensitive) from the input string starting + * at the specified offset position. If the candidate token matches the next characters, the offset + * is updated to point to the next offset after the match. If the candidate token does not match, + * the offset isn't updated. + * + * @param s The input string + * @param offset The offset into the input string to start matching + * @param candidate The candidate token to consume (must be all lowercase) + * @return @c true if the candidate token matches the next characters in the string, @c false otherwise. + */ +bool +consumeToken(const std::string& s, std::size_t* offset, const char* candidate) +{ + auto slen = s.length(); + auto clen = strlen(candidate); + for (std::size_t i = 0; i < clen; i++) { + std::size_t index = *offset + i; + if (index >= slen) + return false; + if (sutil::tolower(s.at(index)) != candidate[i]) + return false; + } + + *offset = *offset + clen; + return true; +} + +/** + * Tries to consume at least one digit from the input stream starting at the specified offset. + * If successful, the offset if update to point to the first non-digit character after the match. + * If no digit was matched, the offset isn't updated. + * + * @param s The input string + * @param offset The offset into the input string to start matching + * @param base The base in which the expected digits are expressed + * @return @c true if at least one digit was matched, @c false otherwise. + */ +bool +consumeDigits(const std::string& s, std::size_t* offset, int base = 10) +{ + static const char* digits = "0123456789ABCDEF"; + auto slen = s.length(); + auto index = *offset; + + while (index < slen) { + char normalizedChar = sutil::toupper(s.at(index)); + const char* digit = std::strchr(digits, normalizedChar); + auto charIndex = digit - digits; + if (digit && charIndex < base) { + index += 1; + } else { + break; + } + } + + if (index > *offset) { + *offset = index; + return true; + } else { + return false; + } +} + +/** + * Tries to consume a +/- sign from the specified string at the specified offset, and returns + * the sign as +1 or -1. If no sign could be read, returns +1. If a sign is read, the offset + * is updated to point to the character following the sign character. + * + * @param s The input string + * @param offset The offset from which to read. + * @return The sign that was read, or +1 if no sign character was present. + */ +int +consumeSign(const std::string& s, std::size_t* offset) +{ + auto index = *offset; + if (s[index] == '+') { + *offset += 1; + return 1; + } else if (s[index] == '-') { + *offset += 1; + return -1; + } + + return 1; +} + +template +T +parseFloatingPointLiteral(const std::string& str, std::size_t* pos) +{ + auto len = str.length(); + std::size_t firstChar = 0; + while (firstChar < len && sutil::isspace(str.at(firstChar))) { + firstChar += 1; + } + + if (firstChar >= len) { + if (pos) *pos = len; + return std::numeric_limits::quiet_NaN(); + } + + std::size_t current = firstChar; + + // Parse sign, if present + int sign = consumeSign(str, ¤t); + + if (consumeToken(str, ¤t, "infinity") || consumeToken(str, ¤t, "inf")) { + if (pos) *pos = current; + return sign * std::numeric_limits::infinity(); + } + + if (consumeToken(str, ¤t, "nan")) { + if (pos) *pos = current; + return std::numeric_limits::quiet_NaN(); + } + + int base; + if (consumeToken(str, ¤t, "0x")) { + base = 16; + } else { + base = 10; + } + + auto significandStart = current; + T value = 0; + bool needDecimalSep = false; + if (consumeDigits(str, ¤t, base)) { + // No digits found, invalid number + value = std::stoll(str.substr(significandStart, current - significandStart), nullptr, base); + } else { + // No digits found, the next character must be '.' + needDecimalSep = true; + } + + // Check for a fractional part + if (consumeToken(str, ¤t, ".")) { + auto fractionalStart = current; + if (consumeDigits(str, ¤t, base)) { + int count = current - fractionalStart; + auto fractional = std::stoll(str.substr(fractionalStart, count), nullptr, base); + value += fractional / std::pow(base, count); + } + } else if (needDecimalSep) { + return std::numeric_limits::quiet_NaN(); + } + + // Parse exponent. Base 10 uses 'e' whereas base 16 uses 'p' as a delimiter + if (base == 10 && consumeToken(str, ¤t, "e")) { + auto exponentSign = consumeSign(str, ¤t); + + auto exponentStart = current; + if (!consumeDigits(str, ¤t, base)) { + // exponent has to have at least one decimal digit + return std::numeric_limits::quiet_NaN(); + } + + auto exponent = std::stoll(str.substr(exponentStart, current - exponentStart)); + value = value * std::pow(10, exponentSign * exponent); + } else if (base == 16 && consumeToken(str, ¤t, "p")) { + auto exponentSign = consumeSign(str, ¤t); + + auto exponentStart = current; + if (!consumeDigits(str, ¤t, 10)) { + // exponent has to have at least one decimal digit + return std::numeric_limits::quiet_NaN(); + } + + // exponent is base 10 even when parsing as hex + auto exponent = std::stoll(str.substr(exponentStart, current - exponentStart), nullptr, 10); + value = value * std::pow(2, exponentSign * exponent); + } + + if (pos) *pos = current; + return sign * value; +} + +template +std::string +format(T value) { + if (std::isnan(value)) { + return "nan"; + } else if (std::isinf(value)) { + return value > 0 ? "inf" : "-inf"; + } + + auto normalized = std::abs(value); + auto integerPart = (std::int64_t) std::abs(normalized); + // We want 6 digits after the decimal point + static const char *digits = "0123456789"; + static const int numDigits = 6; + static const int multiplier = std::pow(10, numDigits); + auto fractionalPart = std::abs(std::llround((normalized - integerPart) * multiplier)); + if (fractionalPart >= multiplier) { + // Rounding caused overflow (e.g. 9.9999...9 -> "10.000000") + integerPart += 1; + fractionalPart -= multiplier; + } + std::string fractionalStr = ""; + for (int i = 0; i < numDigits; i++) { + auto digit = fractionalPart % 10; + fractionalStr = digits[digit] + fractionalStr; + fractionalPart = fractionalPart / 10; + } + auto sign = value < 0 ? "-" : ""; + return sign + std::to_string(integerPart) + "." + rpad(fractionalStr, numDigits, '0'); +} + +// ---- End of internal utilities + +float +stof(const std::string& str, std::size_t* pos) +{ + return parseFloatingPointLiteral(str, pos); +} + +double +stod(const std::string& str, std::size_t* pos) +{ + return parseFloatingPointLiteral(str, pos); +} + + +long double +stold(const std::string& str, std::size_t* pos) +{ + return parseFloatingPointLiteral(str, pos); +} + +std::string +to_string(float value) +{ + return format(value); +} + +std::string +to_string(double value) +{ + return format(value); +} + +std::string +to_string(long double value) +{ + return format(value); +} + +bool +isalnum(char c) +{ + return std::isalnum(c, std::locale::classic()); +} + +bool +isalnum(unsigned char c) +{ + return std::isalnum((char) c, std::locale::classic()); +} + +bool +isspace(char c) +{ + return std::isspace(c, std::locale::classic()); +} + +bool +isspace(unsigned char c) +{ + return std::isspace((char) c, std::locale::classic()); +} + +bool +isupper(char c) +{ + return std::isupper(c, std::locale::classic()); +} + +bool +isupper(unsigned char c) +{ + return std::isupper((char) c, std::locale::classic()); +} + +bool +islower(char c) +{ + return std::islower(c, std::locale::classic()); +} + +bool +islower(unsigned char c) +{ + return std::islower((char) c, std::locale::classic()); +} + +char +tolower(char c) +{ + return std::tolower(c, std::locale::classic()); +} + +unsigned char +tolower(unsigned char c) +{ + return std::tolower((char) c, std::locale::classic()); +} + +char +toupper(char c) +{ + return std::toupper(c, std::locale::classic()); +} + +unsigned char +toupper(unsigned char c) +{ + return std::toupper((char) c, std::locale::classic()); +} + + +} // namespace sutil + } \ No newline at end of file diff --git a/aplcore/src/utils/url.cpp b/aplcore/src/utils/url.cpp index 7792e92..2a17af8 100644 --- a/aplcore/src/utils/url.cpp +++ b/aplcore/src/utils/url.cpp @@ -16,23 +16,23 @@ #include #include "apl/utils/url.h" +#include "apl/utils/stringfunctions.h" namespace apl { // See https://tools.ietf.org/html/rfc3986#section-2.3 bool -isUsableRaw(unsigned char c) { - return std::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~'; +isUsableRaw(char c) { + return sutil::isalnum(c) || c == '-' || c == '.' || c == '_' || c == '~'; } - std::string encodeUrl(const std::string& url) { std::string encodedUrl; encodedUrl.reserve(url.length() * 3); char hexChar[4]; // "%20" type escapes are 4 characters with null termination for (char c : url) { - if (isUsableRaw(static_cast(c))) { + if (isUsableRaw(c)) { encodedUrl.append(1, c); } else { // format string decoding: %% -> '%' diff --git a/bin/apl-header-inclusion-validation.sh b/bin/apl-header-inclusion-validation.sh index 6097f1b..80a12f7 100644 --- a/bin/apl-header-inclusion-validation.sh +++ b/bin/apl-header-inclusion-validation.sh @@ -69,6 +69,7 @@ public_apl_headers=( "apl/engine/styles.h" "apl/extension/extensionclient.h" "apl/extension/extensionmediator.h" + "apl/extension/extensionsession.h" "apl/focus/focusdirection.h" "apl/graphic/graphic.h" "apl/graphic/graphiccontent.h" @@ -97,7 +98,9 @@ public_apl_headers=( "apl/primitives/objectdata.h" "apl/primitives/point.h" "apl/primitives/radii.h" + "apl/primitives/range.h" "apl/primitives/rect.h" + "apl/primitives/roundedrect.h" "apl/primitives/size.h" "apl/primitives/styledtext.h" "apl/primitives/transform2d.h" @@ -107,6 +110,7 @@ public_apl_headers=( "apl/touch/pointerevent.h" "apl/utils/bimap.h" "apl/utils/counter.h" + "apl/utils/deprecated.h" "apl/utils/localemethods.h" "apl/utils/log.h" "apl/utils/noncopyable.h" diff --git a/bin/find-forbidden-functions b/bin/find-forbidden-functions new file mode 100755 index 0000000..3052238 --- /dev/null +++ b/bin/find-forbidden-functions @@ -0,0 +1,120 @@ +#!/usr/bin/env bash +# 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. + +FORBIDDEN_FUNCTIONS=( + # Functions that depend on the C locale + "std::fprintf" + "std::fscanf" + "std::isalnum" + "std::isalpha" + "std::isblank" + "std::iscntrl" + "std::isdigit" + "std::isgraph" + "std::islower" + "std::isprint" + "std::ispunct" + "std::isspace" + "std::isupper" + "std::iswalnum" + "std::iswalpha" + "std::iswblank" + "std::iswcntrl" + "std::iswctype" + "std::iswdigit" + "std::iswgraph" + "std::iswlower" + "std::iswprint" + "std::iswpunct" + "std::iswspace" + "std::iswupper" + "std::iswxdigit" + "std::isxdigit" + "std::localeconv" + "std::mblen" + "std::mbstowcs" + "std::mbtowc" + "std::setlocale" + "std::stod" + "std::stof" + "std::stold" + "std::strcoll" + "std::strerror" + "std::strtod" + "std::strtof" + "std::strtold" + "std::tolower" + "std::toupper" + "std::towlower" + "std::towupper" + "std::wcscoll" + "std::wcstod" + "std::wcstof" + "std::wcstold" + "std::wcstombs" + "std::wcsxfrm" + + # Stream functions + "std::istream" + "std::ostream" + "std::ifstream" + "std::ofstream" + "std::fstream" + "std::wifstream" + "std::wofstream" + "std::wfstream" + "std::istringstream" + "std::ostringstream" + "std::stringstream" + "std::wistringstream" + "std::wostringstream" + "std::wstringstream" +) + +function toPattern() { + echo "$1[(]" +} + +function buildPattern() { + local pattern + + for fn in "${FORBIDDEN_FUNCTIONS[@]}"; do + if [ -z "$pattern" ]; then + pattern=$fn + else + pattern="$pattern\\|`toPattern $fn`" + fi + done + + echo $pattern +} + +APL_CORE_DIR=$(dirname $0)/../aplcore +cd $APL_CORE_DIR + +functions_pattern=$(buildPattern) + +# Find all occurrences of forbidden functions, and filter out those that: +# - 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\(\)') + +if [ -z "$violations" ]; then + exit 0 +fi + +echo "Found violations:" +echo "$violations" +exit 1 diff --git a/components.cmake b/components.cmake index 974f28a..ebe3e0d 100644 --- a/components.cmake +++ b/components.cmake @@ -114,10 +114,24 @@ if (BUILD_TEST_PROGRAMS) add_subdirectory(test) endif (BUILD_TEST_PROGRAMS) +if (VALIDATE_HEADERS OR VALIDATE_FORBIDDEN_FUNCTIONS) + add_custom_target(validation ALL) +endif () + if (VALIDATE_HEADERS) add_custom_command(OUTPUT include_validation COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/bin/apl-header-inclusion-validation.sh WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/) - add_custom_target(validation ALL - DEPENDS include_validation) + add_custom_target(target_validate_includes ALL DEPENDS include_validation) + add_dependencies(validation target_validate_includes) endif (VALIDATE_HEADERS) + +if (VALIDATE_FORBIDDEN_FUNCTIONS) + add_custom_command(OUTPUT forbidden_function_validation + COMMAND bash ${CMAKE_CURRENT_SOURCE_DIR}/bin/find-forbidden-functions + WORKING_DIRECTORY ${CMAKE_CURRENT_SOURCE_DIR}/) + add_custom_target(target_validate_forbidden_functions ALL DEPENDS forbidden_function_validation) + add_dependencies(validation target_validate_forbidden_functions) +endif (VALIDATE_FORBIDDEN_FUNCTIONS) + + diff --git a/extensions/alexaext/CMakeLists.txt b/extensions/alexaext/CMakeLists.txt index c20155e..f64822b 100644 --- a/extensions/alexaext/CMakeLists.txt +++ b/extensions/alexaext/CMakeLists.txt @@ -22,10 +22,16 @@ 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/executor.cpp src/extensionmessage.cpp src/extensionregistrar.cpp src/localextensionproxy.cpp + src/random.cpp + src/sessiondescriptor.cpp ) if (BUILD_SHARED OR ENABLE_PIC) diff --git a/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h new file mode 100644 index 0000000..44ee6ba --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h @@ -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. + */ +#ifndef APL_APLE2EENCRYPTIONEXTENSION_H +#define APL_APLE2EENCRYPTIONEXTENSION_H + +#include +#include + +#include + +#include "AplE2eEncryptionExtensionObserverInterface.h" + +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: + + AplE2eEncryptionExtension( + AplE2eEncryptionExtensionObserverInterfacePtr observer, + alexaext::ExecutorPtr executor, + alexaext::uuid::UUIDFunction uuidGenerator = alexaext::uuid::generateUUIDV4); + + virtual ~AplE2eEncryptionExtension() = default; + + /// @name alexaext::Extension Functions + /// @{ + + rapidjson::Document createRegistration(const std::string &uri, + const rapidjson::Value ®istrationRequest) override; + + bool invokeCommand(const std::string &uri, const rapidjson::Value &command) override; + + /// @} + +private: + + AplE2eEncryptionExtensionObserverInterfacePtr mObserver; + std::weak_ptr mExecutor; + alexaext::uuid::UUIDFunction mUuidGenerator; +}; + +using AplE2eEncryptionExtensionPtr = std::shared_ptr; + +} // namespace E2EEncryption + +#endif // APL_APLE2EENCRYPTIONEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h new file mode 100644 index 0000000..3735205 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtensionObserverInterface.h @@ -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. + */ +#ifndef APL_APLE2EENCRYPTIONEXTENSIONOBSERVERINTERFACE_H +#define APL_APLE2EENCRYPTIONEXTENSIONOBSERVERINTERFACE_H + +#include +#include + +namespace E2EEncryption { + +/** + * Callback to run when the encryption of a value finishes successfully + * + * @param token with the token to identify the caller + * @param base64EncryptedData result of the encoding process + * @param base64EncodedIV The base64 encoded value of the initialization vector if used + * by the given algorithm + * @param base64EncodedKey The base64 encoded value of the key used for the encryption. + */ +using EncryptionCallbackSuccess = + std::function; + +/** + * Callback to run when the encryption of a value finishes successfully + * + * @param token with the token to identify the caller + * @param reason reason why the encryption process failed + */ +using EncryptionCallbackError = + std::function; + + +/** + * Callback to run when the encryption of a value finishes successfully + * + * @param token with the token to identify the caller + * @param base64EncodedData result of the encoding process + */ +using EncodeCallbackSuccess = + std::function; + + +/** + * This class allows a @c AplE2eEncryptionExtensionObserverInterface observer to be notified of + * changes in the + * @c AplE2eEncryptionExtension. + */ +class AplE2eEncryptionExtensionObserverInterface { +public: + /** + * Destructor + */ + virtual ~AplE2eEncryptionExtensionObserverInterface() = default; + + /** + * Used to encrypt the value property. + * + * @param token metadata used to identify the caller of the command. This is needed for async + * purposes. + * @param key key to use to encrypt value + * @param algorithm the encryption algorithm used for encryption + * @param aad additional authentication data used by the encryption algorithm + * @param value text to encrypt + * @param base64Encoded when true, the value needs to be base64decode before encryption + * @param successCallback code to run when the encryption successfully encrypts value + * @param errorCallback code to run when the encryption can't encrypt value + */ + virtual void onBase64EncryptValue(const std::string& token, + const std::string& key, + const std::string& algorithm, + const std::string& aad, + const std::string& value, + bool base64Encoded, + EncryptionCallbackSuccess successCallback, + EncryptionCallbackError errorCallback) = 0; + + /** + * @param token metadata used to identify the caller of the command. This is needed for async + * purposes. + * @param value text to encode + */ + virtual void onBase64EncodeValue(const std::string& token, + const std::string& value, + EncodeCallbackSuccess successCallback) = 0; +}; + +using AplE2eEncryptionExtensionObserverInterfacePtr = + std::shared_ptr; + +} // namespace E2EEncryption + +#endif // APL_APLE2EENCRYPTIONEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h new file mode 100644 index 0000000..e6ef21f --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h @@ -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. + */ +#ifndef APL_APLMUSICALARMEXTENSION_H +#define APL_APLMUSICALARMEXTENSION_H + +#include +#include + +#include + +#include "AplMusicAlarmExtensionObserverInterface.h" + +namespace MusicAlarm { + +static const std::string URI = "aplext:musicalarm:10"; + +/** + * The MusicAlarm extension is an optional-use feature, which allows APL developers to dismiss/snooze + * the ringing music alarm from within the APL document. + */ +class AplMusicAlarmExtension : + public alexaext::ExtensionBase, + public std::enable_shared_from_this { + +public: + AplMusicAlarmExtension( + AplMusicAlarmExtensionObserverInterfacePtr observer, + alexaext::ExecutorPtr executor, + alexaext::uuid::UUIDFunction uuidGenerator = alexaext::uuid::generateUUIDV4); + + virtual ~AplMusicAlarmExtension() = default; + + /// @name alexaext::Extension Functions + /// @{ + + rapidjson::Document createRegistration(const std::string &uri, + const rapidjson::Value ®istrationRequest) override; + + bool invokeCommand(const std::string &uri, const rapidjson::Value &command) override; + + /// @} + +private: + AplMusicAlarmExtensionObserverInterfacePtr mObserver; + std::weak_ptr mExecutor; + alexaext::uuid::UUIDFunction mUuidGenerator; +}; + +} // MusicAlarm + +#endif // APL_APLMUSICALARMEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.h new file mode 100644 index 0000000..80868e8 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMusicAlarmExtension/AplMusicAlarmExtensionObserverInterface.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_APLMUSICALARMEXTENSIONOBSERVERINTERFACE_H +#define APL_APLMUSICALARMEXTENSIONOBSERVERINTERFACE_H + +#include +#include + +namespace MusicAlarm { + +class AplMusicAlarmExtensionObserverInterface { +public: + /** + * Destructor + */ + virtual ~AplMusicAlarmExtensionObserverInterface() = default; + + /** + * The DismissAlarm command is used to dismiss the current ringing alarm. + */ + virtual void dismissAlarm() = 0; + + /** + * The SnoozeAlarm command is used to snooze the current ringing alarm. + */ + virtual void snoozeAlarm() = 0; +}; + +using AplMusicAlarmExtensionObserverInterfacePtr = std::shared_ptr; + +} // MusicAlarm + +#endif // APL_APLMUSICALARMEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h new file mode 100644 index 0000000..ecd6db0 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowBase.h @@ -0,0 +1,81 @@ +/* + * 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_APLWEBFLOW_H +#define APL_APLWEBFLOW_H + +#include +#include + +namespace Webflow { + +/** + * Webflow base class. It handles the launch and event processing. + * + * This is a helper base class to encourage best practices. + */ +class AplWebflowBase { +public: + /** + * Constructor of the webflow + * + * @param token Meta information about the webflow + * @param uri URI we want to connect + * @param flowId flow identier of this object + */ + AplWebflowBase(std::string token, std::string uri, std::string flowId); + + /** + * Destructor + */ + virtual ~AplWebflowBase() = default; + + /** + * Starts a webflow + */ + virtual bool launch() = 0; + + /** + * Gets the Uri of the webflow + * + * @return string with the uri of the webflow + */ + const std::string& getUri() const; + + + /** + * Gets the flow of the webflow + * + * @return string with the flow of the webflow + */ + const std::string& getFlowId() const; + + + /** + * Gets the token of the webflow + * + * @return string with the token used to start this webflow + */ + const std::string& getToken() const; +protected: + std::string mToken; + std::string mUri; + std::string mFlowId; +}; + +using AplWebflowBasePtr = std::shared_ptr; + +} + +#endif // APL_APLWEBFLOW_H diff --git a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h new file mode 100644 index 0000000..3d4888e --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtension.h @@ -0,0 +1,85 @@ +/* + * 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_APLWEBFLOWEXTENSION_H +#define APL_APLWEBFLOWEXTENSION_H + + +#include +#include +#include + +#include + +#include "AplWebflowBase.h" +#include "AplWebflowExtensionObserverInterface.h" + +namespace Webflow { + +static const std::string URI = "aplext:webflow:10"; +static const std::string ENVIRONMENT_VERSION = "APLWebflowExtension-1.0"; + +/** + * An APL Extension designed to launch a feature restricted browser that is capable of navigating + * 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. + * + * Because of the flow nature of the webflow extension, flows can be runtime dependant. The current model + * allows two level of indirection. + * + * Extension->Observer->Flow where Observer and Flow need to implement their interfaces. + */ +class AplWebflowExtension + : public alexaext::ExtensionBase, public std::enable_shared_from_this { +public: + + /** + * Constructor + */ + explicit AplWebflowExtension( + std::function tokenGenerator, + std::shared_ptr observer, + const std::shared_ptr& executor); + + virtual ~AplWebflowExtension() = default; + + /// @name alexaext::Extension Functions + /// @{ + + rapidjson::Document createRegistration(const std::string &uri, + const rapidjson::Value ®istrationRequest) override; + + bool invokeCommand(const std::string &uri, const rapidjson::Value &command) override; + + /// @} + +private: + /// The @c AplWebflowExtensionObserverInterface observer + std::shared_ptr mObserver; + + /// The @c uuid token generator + std::function mTokenGenerator; + + /// The @c executor to run the observer + std::weak_ptr mExecutor; +}; + +using AplWebflowExtensionPtr = std::shared_ptr; + +} // Webflow + +#endif // APL_APLWEBFLOWEXTENSION_H diff --git a/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h new file mode 100644 index 0000000..0df36e5 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLWebflowExtension/AplWebflowExtensionObserverInterface.h @@ -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. + */ + +#ifndef APL_APLWEBFLOWEXTENSIONOBSERVERINTERFACE_H +#define APL_APLWEBFLOWEXTENSIONOBSERVERINTERFACE_H + +#include "AplWebflowBase.h" + +namespace Webflow { + +/** + * This class allows a @c AplWebflowExtensionObserverInterface observer to be notified of changes in the + * @c AplWebflowExtension + */ +class AplWebflowExtensionObserverInterface { +public: + + /** + * Destructor + */ + virtual ~AplWebflowExtensionObserverInterface() = default; + + /** + * Used to notify the observer when the extension has issued a StartFlow command. + * + * @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 std::string& token, + const std::string& url, + const std::string& flowId, + std::function onFlowEndEvent = + [](const std::string&, const std::string&){}) = 0; +}; + +using AplWebflowExtensionObserverInterfacePtr = std::shared_ptr; + +} // namespace Webflow + +#endif // APL_APLWEBFLOWEXTENSIONOBSERVERINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/activitydescriptor.h b/extensions/alexaext/include/alexaext/activitydescriptor.h new file mode 100644 index 0000000..0cf6890 --- /dev/null +++ b/extensions/alexaext/include/alexaext/activitydescriptor.h @@ -0,0 +1,169 @@ +/** + * 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 _ALEXAEXT_ACTIVITY_DESCRIPTOR_H +#define _ALEXAEXT_ACTIVITY_DESCRIPTOR_H + +#include + +#include "random.h" +#include "sessiondescriptor.h" +#include "types.h" + +namespace alexaext { + +/** + * Represents an activity that requests and uses functionality defined by a given extension. + * For example, a rendering task for an APL document is a common type of activity that requests + * APL extensions. Each activity belongs to a single extension session. + * + * Activity descriptors are immutable and hashable, so they are suitable to use as keys in + * unordered maps or other hashing data structures. + * + * @see ActivityId + * @see SessionId + */ +class ActivityDescriptor final { +public: + /** + * Constructs a new immutable activity descriptor. + * + * @param uri The URI of the extension as requested by the activity. + * @param session The session associated with this activity. + */ + ActivityDescriptor(const std::string& uri, + const SessionDescriptorPtr& session) + : mURI(uri), + mSession(session), + mActivityId(generateBase36Token("activity-")) + {} + + /** + * Constructs a new immutable activity descriptor. + * + * @param uri The URI of the extension as requested by the activity. + * @param session The session associated with this activity. + * @param uniqueId The unique activity ID + */ + ActivityDescriptor(const std::string& uri, + const SessionDescriptorPtr& session, + const ActivityId& uniqueId) + : mURI(uri), + mSession(session), + mActivityId(uniqueId) + {} + ~ActivityDescriptor() = default; + + /** + * Constructs a new immutable activity descriptor with a generated unique ID. + * + * @param uri The URI of the extension as requested by the activity. + * @param session The session associated with this activity. + * @return The new activity descriptor + */ + static std::shared_ptr create(const std::string& uri, + const SessionDescriptorPtr& session) { + return std::make_shared(uri, session); + } + + /** + * Constructs a new immutable activity descriptor with specified ID. This ID should be globally + * unique. + * + * @param uri The URI of the extension as requested by the activity. + * @param session The session associated with this activity. + * @param uniqueId A globally unique activity ID + * @return The new activity descriptor + */ + static std::shared_ptr create(const std::string& uri, + const SessionDescriptorPtr& session, + const ActivityId& uniqueId) { + return std::make_shared(uri, session, uniqueId); + } + + /** + * @return The URI of the extension as requested by the activity. + */ + const std::string& getURI() const { return mURI; } + + /** + * @return The session for this activity. + */ + SessionDescriptorPtr getSession() const { return mSession; } + + /** + * @return The identifier for this activity. + */ + const ActivityId& getId() const { return mActivityId; } + + bool operator==(const ActivityDescriptor& other) const { + return mURI == other.mURI && mActivityId == other.mActivityId + && (mSession ? other.mSession && *mSession == *other.mSession + : other.mSession == nullptr); + } + + bool operator!=(const ActivityDescriptor& rhs) const { return !(rhs == *this); } + + /** + * Provides hashing for activity descriptors so they can easily be used as unordered map keys. + */ + class Hash final { + public: + std::size_t operator()(const ActivityDescriptor& descriptor) const { + static SessionDescriptor::Hash sessionHash; + static std::hash stringHash; + // Variant of Apache Commons HashCodeBuilder hashing algorithm + std::size_t hash = 17; + hash = hash * 37 + stringHash(descriptor.getURI()); + if (auto session = descriptor.getSession()) { + hash = hash * 37 + sessionHash(*session); + } + hash = hash * 37 + stringHash(descriptor.getId()); + return hash; + } + }; + + /** + * Provides comparison for activity descriptors so they can easily be used as ordered map keys. + */ + class Compare final { + public: + bool operator()(const ActivityDescriptor& first, const ActivityDescriptor& second) const { + static SessionDescriptor::Compare sessionCompare; + + if (first.getURI() < second.getURI()) return true; + if (first.getURI() > second.getURI()) return false; + if (first.getId() < second.getId()) return true; + if (first.getId() > second.getId()) return false; + + // URI and IDs are both the same, compare sessions + if (!first.getSession() && !second.getSession()) return false; // nullptr == nullptr + if (!first.getSession() && second.getSession()) return true; // nullptr < first + if (first.getSession() && !second.getSession()) return false; // first > nullptr + return sessionCompare(*first.getSession(), *second.getSession()); + } + }; + +private: + std::string mURI; + SessionDescriptorPtr mSession; + ActivityId mActivityId; +}; + +using ActivityDescriptorPtr = std::shared_ptr; + +} // namespace alexaext + +#endif // _ALEXAEXT_ACTIVITY_DESCRIPTOR_H diff --git a/extensions/alexaext/include/alexaext/alexaext.h b/extensions/alexaext/include/alexaext/alexaext.h index cd7e7b0..ad49033 100644 --- a/extensions/alexaext/include/alexaext/alexaext.h +++ b/extensions/alexaext/include/alexaext/alexaext.h @@ -31,6 +31,7 @@ * alexaext:myfeature:10 */ +#include "activitydescriptor.h" #include "extension.h" #include "extensionbase.h" #include "extensionexception.h" @@ -43,6 +44,11 @@ #include "extensionschema.h" #include "extensionregistrar.h" #include "localextensionproxy.h" +#include "sessiondescriptor.h" +#include "types.h" #include "APLAudioPlayerExtension/AplAudioPlayerExtension.h" +#include "APLE2EEncryptionExtension/AplE2eEncryptionExtension.h" +#include "APLWebflowExtension/AplWebflowExtension.h" +#include "APLMusicAlarmExtension/AplMusicAlarmExtension.h" #endif //_ALEXAEXT_H diff --git a/extensions/alexaext/include/alexaext/extension.h b/extensions/alexaext/include/alexaext/extension.h index 8034e2f..eb0a1df 100644 --- a/extensions/alexaext/include/alexaext/extension.h +++ b/extensions/alexaext/include/alexaext/extension.h @@ -23,18 +23,65 @@ #include +#include "activitydescriptor.h" #include "extensionmessage.h" #include "extensionresourceholder.h" +#include "sessiondescriptor.h" +#include "types.h" namespace alexaext { /** - * The Extension interface defines the contract exposed from the Extension to the - * document. It supports invoking an extension command from the document, and sending - * events from the extension to the document. + * The Extension interface defines the contract exposed from the extension to a activity (e.g. a + * typical activity for an APL extension is a rendering task for an APL document). Extensions are + * typically lazily instantiated by an execution environment (e.g. APL or Alexa Web for Games) in + * response to the extension being requested by an activity. + * + * The extension contract also defines the lifecycle of an extension. The lifecycle of an extension + * starts with an activity requesting it. Each activity belongs to exactly one session for the + * entire duration of the activity. + * + * During a activity interaction, an extension will receive a well-defined sequence of calls. For + * example, consider a common extension use case: a single, standalone APL document requests an + * extension to render its contents, and then gets finished (i.e. taken off screen) after a short + * interaction. In this example, the activity corresponds to the rendering task for the APL document, + * the session corresponds to the skill session. + * + * The extension would, for this example, receive the following sequence of calls as + * follows: + * + * - @c onSessionStarted is called with the document's session descriptor + * - @c createRegistration is called for the document + * - @c onActivityRegistered is called when the registration succeeds + * - @c onForeground is called to indicate that the activity is being rendered in the foreground + * - the activity can then send commands and receive events with the extension + * - the document is finished by the APL execution environment after user interactions are done + * - @c onActivityUnregistered is called to indicate that the document is no longer active + * - @c onSessionEnded is called (could be delayed) + * + * Consider the more complex case of an extension being requested by a set of related APL documents + * interacting with each other via the APL backstack. For example, this could be a menu flow + * implemented as a series of distinct documents. For a multi-document session, a typical flow + * would instead be as follows: + * - @c onSessionStarted is called with the first document's session descriptor + * - @c createRegistration is called for the first document + * - @c onActivityRegistered is called when the registration succeeds + * - @c onForeground is called to indicate that the activity is being rendered in the foreground + * - the activity can then send commands and receive events with the extension + * - a new document is rendered in the same session, and the current one is pushed to the backstack + * - @c createRegistration is called for the second document + * - @c onActivityRegistered is called when the registration succeeds + * - @c onHidden is called for the first activity to indicate it is now hidden + * - @c onForeground is called to indicate that the activity is in the foreground + * - the second document can now interact with the extension + * - the second document restores the first document from the backstack + * - @c onActivityUnregistered is called to indicate that the second document is no longer active + * - @c onForeground is called to indicate that the first document is now again in the foreground + * - the first document is finished + * - @c onActivityUnregistered is called to indicate that the first document is no longer active + * - @c onSessionEnded is called (could be delayed) */ class Extension { - public: virtual ~Extension() = default; @@ -48,28 +95,60 @@ class Extension { /** * Create a registration for the extension. The registration is returned in a * "RegistrationSuccess" or "RegistrationFailure" message. The extension is defined by a unique - * token per registration, an environment of static properties, and the extension schema. + * token per registration, an environment of static properties, and the extension schema. This + * method is called by the extension framework when the extension is requested by an + * activity. * * The schema defines the extension api, including commands, events and live data. The * "RegistrationRequest" parameter contains a schema version, which matches the schema versions - * supported by the runtime, and extension settings defined by the requesting document. + * supported by the execution environment, and extension settings defined by the requesting + * activity. * * std::exception or ExtensionException thrown from this method are converted to * "RegistrationFailure" messages and returned to the caller. * - * This method is called by the extension framework when the extension is requested by a - * document. - * + * @deprecated Use the @c ActivityDescriptor variant * @param uri The extension URI. * @param registrationRequest A "RegistrationRequest" message, includes extension settings. * @return A extension "RegistrationSuccess" or "RegistrationFailure" message. */ virtual rapidjson::Document createRegistration(const std::string& uri, - const rapidjson::Value& registrationRequest) = 0; + const rapidjson::Value& registrationRequest) { + return RegistrationFailure::forException(uri, "Not implemented"); + }; + + /** + * Create a registration for the extension. The registration is returned in a + * "RegistrationSuccess" or "RegistrationFailure" message. The extension is defined by a unique + * token per registration, an environment of static properties, and the extension schema. This + * method is called by the extension framework when the extension is requested by an + * activity. + * + * The schema defines the extension api, including commands, events and live data. The + * "RegistrationRequest" parameter contains a schema version, which matches the schema versions + * supported by the execution environment, and extension settings defined by the requesting + * activity. + * + * std::exception or ExtensionException thrown from this method are converted to + * "RegistrationFailure" messages and returned to the caller. + * + * The activity descriptor has a pre-populated activity identifier. If an extension chooses to + * use this identifier, it can simply return a response that uses "" as the activity + * token. If an extension chooses to provide a new token instead, it will be used as the + * activity identifier for all subsequent calls. + * + * @param activity The activity using this extension. + * @param registrationRequest A "RegistrationRequest" message, includes extension settings. + * @return A extension "RegistrationSuccess" or "RegistrationFailure" message. + */ + virtual rapidjson::Document createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) { + return createRegistration(activity.getURI(), registrationRequest); + } /** * Callback definition for extension "Event" messages. The extension will call back to - * invoke an extension event handler in the document. + * invoke an extension event handler in the activity. * * @param uri The URI of the extension. * @param event A extension "Event" message. @@ -78,17 +157,39 @@ class Extension { std::function; /** - * Callback registration for extension "Event" messages. Guaranteed to be called before the - * document is mounted. The callback forwards events to the document event handlers. + * Callback definition for extension "Event" messages. The extension will call back to + * invoke an extension event handler in the activity. * + * @param activity The activity using this extension. + * @param event A extension "Event" message. + */ + using EventActivityCallback = + std::function; + + /** + * Callback registration for extension "Event" messages. When the activity corresponds to an APL + * document rendering task, this method is guaranteed to be called before the document is + * mounted. The callback forwards events to the activity event handlers. + * + * @deprecated Use the @c ActivityDescriptor variant * @param callback The callback for events generating from the extension. */ - virtual void registerEventCallback(EventCallback callback) = 0; + virtual void registerEventCallback(EventCallback callback) { } + + /** + * Callback registration for extension "Event" messages. When the activity corresponds to an APL + * document rendering task, this method is guaranteed to be called before the document is + * mounted. The callback forwards events to the document event handlers. + * + * @param callback The callback for events generating from the extension. + */ + virtual void registerEventCallback(EventActivityCallback&& callback) { } /** * Callback definition for extension "LiveDataUpdate" messages. The extension will call back to - * update the data binding or invoke a lived data handler in the document. + * update the data binding or invoke a lived data handler in the activity. * + * @deprecated Use the ActivityDescriptor variant * @param uri The URI of the extension. * @param liveDataUpdate The "LiveDataUpdate" message. */ @@ -96,53 +197,128 @@ class Extension { std::function; /** - * Callback for extension "LiveDataUpdate" messages. Guaranteed to be called before the document - * is mounted. The callback forwards live data changes to the document data binding and live - * data handlers. + * Callback definition for extension "LiveDataUpdate" messages. The extension will call back to + * update the data binding or invoke a lived data handler in the activity. + * + * @param activity The activity using this extension. + * @param liveDataUpdate The "LiveDataUpdate" message. + */ + using LiveDataUpdateActivityCallback = + std::function; + + /** + * Callback for extension "LiveDataUpdate" messages. When the activity corresponds to an APL + * document rendering task, this method is guaranteed to be called before the document is + * mounted. The callback forwards live data changes to the activity and live data handlers. + * In the case of APL activities, this will update the document data binding. + * + * @param callback The callback for live data updates generating from the extension. + */ + virtual void registerLiveDataUpdateCallback(LiveDataUpdateCallback callback) { } + + /** + * Callback for extension "LiveDataUpdate" messages. When the activity corresponds to an APL + * document rendering task, this method is guaranteed to be called before the document is + * mounted. The callback forwards live data changes to the activity and live data handlers. + * In the case of APL activities, this will update the document data binding. * * @param callback The callback for live data updates generating from the extension. */ - virtual void registerLiveDataUpdateCallback(LiveDataUpdateCallback callback) = 0; + virtual void registerLiveDataUpdateCallback(LiveDataUpdateActivityCallback&& callback) { } /** - * Execute a Command that was initiated by the document. + * Execute a Command that was initiated by the activity. * * std::exception or ExtensionException thrown from this method are converted to * "CommandFailure" messages and returned to the caller. * + * @deprecated Use the @c ActivityDescriptor variant * @param uri The extension URI. * @param command The requested Command message. * @return true if the command succeeded. */ - virtual bool invokeCommand(const std::string& uri, const rapidjson::Value& command) = 0; + virtual bool invokeCommand(const std::string& uri, const rapidjson::Value& command) { + return false; + } + + /** + * Execute a Command that was initiated by the activity. + * + * std::exception or ExtensionException thrown from this method are converted to + * "CommandFailure" messages and returned to the caller. + * + * @param activity The activity using this extension. + * @param command The requested Command message. + * @return true if the command succeeded. + */ + virtual bool invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value& command) { + return invokeCommand(activity.getURI(), command); + } /** * Invoked after registration has been completed successfully. This is useful for - * stateful extensions that require initializing session data upfront. + * stateful extensions that require initializing activity data upfront. * + * @deprecated Use the @c ActivityDescriptor variant * @param uri The extension URI used during registration. - * @param token The client token issued during registration. + * @param token The activity token issued during registration. */ virtual void onRegistered(const std::string& uri, const std::string& token) {} /** - * Invoked after extension unregistered. This is useful for stateful extensions that require clearing up - * session data. + * Invoked after registration has been completed successfully. This is useful for + * stateful extensions that require initializing activity data upfront. * + * @param activity The activity using this extension. + */ + virtual void onActivityRegistered(const ActivityDescriptor& activity) { + onRegistered(activity.getURI(), activity.getId()); + } + + /** + * Invoked after extension unregistered. This is useful for stateful extensions that require + * cleaning up activity data. + * + * @deprecated Use the @c ActivityDescriptor variant * @param uri The extension URI used during registration. - * @param token The client token issued during registration. + * @param token The activity token issued during registration. */ virtual void onUnregistered(const std::string& uri, const std::string& token) {} + /** + * Invoked after extension unregistered. This is useful for stateful extensions that require + * cleaning up activity data. + * + * @param activity The activity using this extension. + */ + virtual void onActivityUnregistered(const ActivityDescriptor& activity) { + onUnregistered(activity.getURI(), activity.getId()); + } + /** * Update an Extension Component. A "Component" message is received when the extension * component changes state, or has a property updated. * + * @deprecated Use the @c ActivityDescriptor variant * @param uri The extension URI. * @param command The Component message. * @return true if the update succeeded. */ - virtual bool updateComponent(const std::string& uri, const rapidjson::Value& command) = 0; + virtual bool updateComponent(const std::string& uri, const rapidjson::Value& command) { + return false; + } + + /** + * Update an Extension Component. A "Component" message is received when the extension + * component changes state, or has a property updated. + * + * @param activity The activity using this extension. + * @param command The Component message. + * @return true if the update succeeded. + */ + virtual bool updateComponent(const ActivityDescriptor& activity, const rapidjson::Value& command) { + return updateComponent(activity.getURI(), command); + } /** * Invoked when a system resource, such as display surface, is ready for use. This method @@ -150,10 +326,91 @@ class Extension { * Messages supporting shared resources: "Component" * Not all execution environments support shared resources. * + * @deprecated Use the @c ActivityDescriptor variant * @param uri The extension URI. * @param resourceHolder Access to the resource. */ virtual void onResourceReady(const std::string& uri, const ResourceHolderPtr& resourceHolder) {} + + /** + * Invoked when a system resource, such as display surface, is ready for use. This method + * will be called after the extension receives a message indicating the resource is "Ready". + * Messages supporting shared resources: "Component" + * Not all execution environments support shared resources. + * + * @param activity The activity using this extension. + * @param resourceHolder Access to the resource. + */ + virtual void onResourceReady(const ActivityDescriptor& activity, const ResourceHolderPtr& resourceHolder) { + onResourceReady(activity.getURI(), resourceHolder); + } + + /** + * Called whenever a new session that requires this extension is started. This is guaranteed + * to be called before @c onActivityRegistered for any activity that belongs to the specified + * session. + * + * No guarantees are made regarding the time at which this is invoked, only that if + * @c onActivityRegistered is invoked, this call will have happened prior to it. For example, a + * typical implementation will withhold the call until extension registration is triggered in + * order to avoid spurious notifications about contexts being created / destroyed that do not + * require this extension. + * + * This call is guaranteed to be made only once for a given session and extension pair. + * + * @param session The session being started. + */ + virtual void onSessionStarted(const SessionDescriptor& session) {} + + /** + * Invoked when a previously started session has ended. This is only called when + * @c onSessionStarted was previously called for the same session. + * + * This call is guaranteed to be made only once for a given session and extension pair. + * + * @param session The session that ended. + */ + virtual void onSessionEnded(const SessionDescriptor& session) {} + + /** + * Invoked when a visual activity becomes in the foreground. If an activity does not + * have any associated visual presentation, this method is never called for it. If a + * visual activity starts in the foreground, this method will be called right after + * a successful registration. + * + * @param activity The activity using this extension. + */ + virtual void onForeground(const ActivityDescriptor& activity) {} + + /** + * Invoked when a visual activity becomes in the background, i.e. it is still completely or + * partially visible, but is no longer the active visual presentation. If an activity does not + * have any associated visual presentation, this method is never called for it. If a + * visual activity starts in the background, this method will be called right after + * a successful registration. + * + * Extensions are encouraged to avoid publishing updates to backgrounded activities as + * they may not be able to process them. + * + * @param activity The activity using this extension. + */ + virtual void onBackground(const ActivityDescriptor& activity) {} + + /** + * Invoked when a visual activity becomes in hidden, i.e. it is no longer visible (e.g. it was + * pushed to the backstack, or was temporarily replaced by another presentation activity). If an + * activity does not have any associated visual presentation, this method is never called for + * it. If a visual activity starts in the background, this method will be called right after + * a successful registration. + * + * This method is not called when an activity leaves the screen because it ended. + * + * Extensions are encouraged to avoid publishing updates to hidden activities as + * they are typically not able to process them. + * + * @param activity The activity using this extension. + */ + virtual void onHidden(const ActivityDescriptor& activity) {} }; using ExtensionPtr = std::shared_ptr; diff --git a/extensions/alexaext/include/alexaext/extensionbase.h b/extensions/alexaext/include/alexaext/extensionbase.h index 37e407b..7c7368c 100644 --- a/extensions/alexaext/include/alexaext/extensionbase.h +++ b/extensions/alexaext/include/alexaext/extensionbase.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _ALEXAEXT_BASEEXTENSION_H -#define _ALEXAEXT_BASEEXTENSION_H +#ifndef _ALEXAEXT_EXTENSION_BASE_H +#define _ALEXAEXT_EXTENSION_BASE_H #include @@ -44,24 +44,46 @@ class ExtensionBase : public Extension { * to the document. This callback is registered by the runtime and called by the extension * via invokeExtensionEventHandler(...). * + * @deprecated use the activity descriptor variant * @param callback The extension event callback. */ void registerEventCallback(EventCallback callback) override { mEventCallback = callback; } + /** + * Register a callback for extension generated "Event" messages that are sent from the extension + * to the document. This callback is registered by the runtime and called by the extension + * via invokeExtensionEventHandler(...). + * + * @param callback The extension event callback. + */ + void registerEventCallback(EventActivityCallback&& callback) override { mEventActivityCallback = callback; } + /** * Register a callback for extension "LiveDataUpdate" messages that are sent from the extension * to the document. This callback is registered by the runtime and called by the extension * via invokeLiveDataUpdate(...). * + * @deprecated use the activity descriptor variant * @param callback The extension event callback. */ void registerLiveDataUpdateCallback(LiveDataUpdateCallback callback) override { mLiveDataCallback = callback; } + /** + * Register a callback for extension "LiveDataUpdate" messages that are sent from the extension + * to the document. This callback is registered by the runtime and called by the extension + * via invokeLiveDataUpdate(...). + * + * @deprecated use the activity descriptor variant + * @param callback The extension event callback. + */ + void registerLiveDataUpdateCallback(LiveDataUpdateActivityCallback&& callback) override { mLiveDataActivityCallback = callback; } + protected: /** * Invoke an extension event handler in the document. * + * @deprecated Use the activity descriptor variant * @param uri The extension URI. * @param event The extension generated event. * @return true if the event is delivered, false if there is no callback registered. @@ -74,9 +96,31 @@ class ExtensionBase : public Extension { return false; } + /** + * Invoke an extension event handler in the document. + * + * @param activity The activity using this extension's functionality. + * @param event The extension generated event. + * @return true if the event is delivered, false if there is no callback registered. + */ + bool invokeExtensionEventHandler(const ActivityDescriptor& activity, const rapidjson::Value& event) { + if (mEventActivityCallback) { + mEventActivityCallback(activity, event); + return true; + } + + // For backwards compatibility + if (mEventCallback) { + mEventCallback(activity.getURI(), event); + return true; + } + return false; + } + /** * Invoke an live data binding change, or data update handler in the document. * + * @deprecated Use the activity descriptor variant * @param uri The extension URI. * @param event The extension generated event. * @return true if the event is delivered, false if there is no callback registered. @@ -89,6 +133,27 @@ class ExtensionBase : public Extension { return false; } + /** + * Invoke an live data binding change, or data update handler in the document. + * + * @param activity The activity using this extension's functionality. + * @param event The extension generated event. + * @return true if the event is delivered, false if there is no callback registered. + */ + bool invokeLiveDataUpdate(const ActivityDescriptor& activity, const rapidjson::Value& liveDataUpdate) { + if (mLiveDataActivityCallback) { + mLiveDataActivityCallback(activity, liveDataUpdate); + return true; + } + // For backwards compatibility + if (mLiveDataCallback) { + mLiveDataCallback(activity.getURI(), liveDataUpdate); + return true; + } + + return false; + } + /** * Component update ignored by default. * @@ -100,12 +165,25 @@ class ExtensionBase : public Extension { return false; }; + /** + * Component update ignored by default. + * + * @param activity The activity using this extension's functionality. + * @param command The Component message. + * @return true if the update succeeded. + */ + bool updateComponent(const ActivityDescriptor& activity, const rapidjson::Value &command) override { + return updateComponent(activity.getURI(), command); + }; + private: - EventCallback mEventCallback; - LiveDataUpdateCallback mLiveDataCallback; + EventCallback mEventCallback; // deprecated + EventActivityCallback mEventActivityCallback; + LiveDataUpdateCallback mLiveDataCallback; // deprecated + LiveDataUpdateActivityCallback mLiveDataActivityCallback; std::set mURIs; }; } // namespace alexaext -#endif //_ALEXAEXT_BASEEXTENSION_H +#endif //_ALEXAEXT_EXTENSION_BASE_H diff --git a/extensions/alexaext/include/alexaext/extensionexception.h b/extensions/alexaext/include/alexaext/extensionexception.h index 0c3fe8c..5107511 100644 --- a/extensions/alexaext/include/alexaext/extensionexception.h +++ b/extensions/alexaext/include/alexaext/extensionexception.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _ALEXAEXT_EXTENSIONEXCEPTION_H -#define _ALEXAEXT_EXTENSIONEXCEPTION_H +#ifndef _ALEXAEXT_EXTENSION_EXCEPTION_H +#define _ALEXAEXT_EXTENSION_EXCEPTION_H #include #include @@ -89,4 +89,4 @@ class ExtensionException : public std::exception { } // namespace alexaext -#endif //_ALEXAEXT_EXTENSIONEXCEPTION_H +#endif //_ALEXAEXT_EXTENSION_EXCEPTION_H diff --git a/extensions/alexaext/include/alexaext/extensionprovider.h b/extensions/alexaext/include/alexaext/extensionprovider.h index 85a3d52..09e119b 100644 --- a/extensions/alexaext/include/alexaext/extensionprovider.h +++ b/extensions/alexaext/include/alexaext/extensionprovider.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _ALEXAEXT_EXTENSIONPROVIDER_H -#define _ALEXAEXT_EXTENSIONPROVIDER_H +#ifndef _ALEXAEXT_EXTENSION_PROVIDER_H +#define _ALEXAEXT_EXTENSION_PROVIDER_H #include #include @@ -57,4 +57,4 @@ using ExtensionProviderPtr = std::shared_ptr; } // namespace alexaext -#endif // _ALEXAEXT_EXTENSIONPROVIDER_H +#endif // _ALEXAEXT_EXTENSION_PROVIDER_H diff --git a/extensions/alexaext/include/alexaext/extensionproxy.h b/extensions/alexaext/include/alexaext/extensionproxy.h index 354eef9..d2773e3 100644 --- a/extensions/alexaext/include/alexaext/extensionproxy.h +++ b/extensions/alexaext/include/alexaext/extensionproxy.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _ALEXAEXT_EXTENSIONPROXY_H -#define _ALEXAEXT_EXTENSIONPROXY_H +#ifndef _ALEXAEXT_EXTENSION_PROXY_H +#define _ALEXAEXT_EXTENSION_PROXY_H #include @@ -22,18 +22,16 @@ #include #include - #include "extension.h" namespace alexaext { /** * Extension proxy provides access to a single extension. It is responsible for - * providing the runtime access to the ExtensionDescriptor before the extension + * providing the execution environment access to the ExtensionDescriptor before the extension * is in use, and instantiating the extension when requested. */ class ExtensionProxy { - public: virtual ~ExtensionProxy() = default; @@ -55,6 +53,7 @@ class ExtensionProxy { /** * Check if extension was initialized. + * * @param uri The extension URI. * @return true if the extension initialized, false otherwise. */ @@ -99,14 +98,75 @@ class ExtensionProxy { * * Implementors of the callback may enforce a timeout. * + * @deprecated Use the activity descriptor variant * @param uri The URI used to identify the extension. * @param registrationRequest A "RegistrationRequest" message, includes extension settings. * @param success The callback for success, provides the extension schema. * @param error The callback for failure, identifies the error code and message. * @return true, if the request for a schema can be processed. */ - virtual bool getRegistration(const std::string &uri, const rapidjson::Value ®istrationRequest, - RegistrationSuccessCallback success, RegistrationFailureCallback error) = 0; + virtual bool getRegistration(const std::string &uri, + const rapidjson::Value ®istrationRequest, + RegistrationSuccessCallback success, + RegistrationFailureCallback error) { return false; } + + /** + * Callback supplied by the runtime for successful execution of getRegistration(...). This + * callback supports asynchronous response. + * + * @param activity The extension activity. + * @param registrationSuccess A "RegistrationSuccess" message, containing the extension schema. + */ + using RegistrationSuccessActivityCallback = + std::function; + + /** + * Callback supplied by the runtime for failed execution of getRegistration(...). This + * callback supports asynchronous response. + * + * @param activity The extension activity. + * @param registrationFailure A "RegistrationFailure" message containing the error codee and message. + */ + using RegistrationFailureActivityCallback = + std::function; + + /** + * Called by the runtime to get the extension schema for the URI. This call may be + * responded to asynchronously via callback. The method should return true if success + * is expected, and false if the request cannot be handled. An invalid URI is + * an example of an immediate return of false. + * + * Successful execution of the request will call the SuccessCallback with a "RegistrationSuccess" message + * and return true. + * + * The extension may process the registration request and respond with "RegistrationFailure". This method + * will return true because the message was processed. An example of "RegistrationFailure" would be the document + * missing a required extension setting. + * + * Failure during execution of the request will call the FailureCallback with "RegistrationFailure" and + * return false. Reasons for execution failure may include unavailable system resources that prevent communication + * with the extension, or exceptions thrown by the extension. + * + * Implementors of the callback may enforce a timeout. + * + * @param activity The activity requesting this extension's functionality. + * @param registrationRequest A "RegistrationRequest" message, includes extension settings. + * @param success The callback for success, provides the extension schema. + * @param error The callback for failure, identifies the error code and message. + * @return true, if the request for a schema can be processed. + */ + virtual bool getRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest, + RegistrationSuccessActivityCallback&& success, + RegistrationFailureActivityCallback&& error) { + return getRegistration(activity.getURI(), registrationRequest, + [activity, success](const std::string &uri, const rapidjson::Value ®istrationSuccess) { + success(activity, registrationSuccess); + }, + [activity, error](const std::string &uri, const rapidjson::Value ®istrationFailure) { + error(activity, registrationFailure); + }); + } /** * Callback for successful execution of invokeCommand(...). @@ -131,23 +191,90 @@ class ExtensionProxy { * * Implementors of the callback may enforce a timeout. * + * @deprecated Use the activity descriptor variant * @param uri The extension URI. * @param command A "Command" message. * @param success The callback for success, provides the command results. * @param error The callback for failure, identifies the command error. * @return true, if the request can be processed. */ - virtual bool invokeCommand(const std::string &uri, const rapidjson::Value &command, - CommandSuccessCallback success, CommandFailureCallback error) = 0; + virtual bool invokeCommand(const std::string &uri, + const rapidjson::Value &command, + CommandSuccessCallback success, + CommandFailureCallback error) { return false; } + + /** + * Callback for successful execution of invokeCommand(...). + * + * @param activity The extension activity. + * @param successResponse The "CommandSuccess" message containing the result. + */ + using CommandSuccessActivityCallback = + std::function; + + /** + * Callback for failed execution of invokeCommand(...). + * + * @param activity The extension activity. + * @param commandResponse The the "CommandFailure" message containing failure the reason. + */ + using CommandFailureActivityCallback = + std::function; + + /** + * Forwards a command invocation to the extension. The command is initiated by the document. + * + * Implementors of the callback may enforce a timeout. + * + * @param activity The activity requesting this extension's functionality. + * @param command A "Command" message. + * @param success The callback for success, provides the command results. + * @param error The callback for failure, identifies the command error. + * @return true, if the request can be processed. + */ + virtual bool invokeCommand(const ActivityDescriptor& activity, + const rapidjson::Value& command, + CommandSuccessActivityCallback&& success, + CommandFailureActivityCallback&& error) { + return invokeCommand(activity.getURI(), command, + [activity, success](const std::string &uri, const rapidjson::Value &commandSuccess) { + success(activity, commandSuccess); + }, + [activity, error](const std::string &uri, const rapidjson::Value &commandFailure) { + error(activity, commandFailure); + }); + } /** - * Forward a message to the extension. May be initiated by the document or core. + * Forward a component message to the extension. May be initiated by the document or core. * + * @deprecated Use @c sendComponentMessage * @param uri The extension URI. * @param message Extension message. * @return true, if the request can be processed. */ - virtual bool sendMessage(const std::string &uri, const rapidjson::Value &message) = 0; + virtual bool sendMessage(const std::string &uri, const rapidjson::Value &message) { return false; } + + /** + * Forward a component message to the extension. May be initiated by the document or core. + * + * @deprecated Use the activity descriptor variant + * @param uri The extension URI. + * @param message Extension message. + * @return true, if the request can be processed. + */ + virtual bool sendComponentMessage(const std::string &uri, const rapidjson::Value &message) { return sendMessage(uri, message); } + + /** + * Forward a component message to the extension. May be initiated by the document or core. + * + * @param activity The activity requesting this extension's functionality. + * @param message Extension message. + * @return true, if the request can be processed. + */ + virtual bool sendComponentMessage(const ActivityDescriptor& activity, const rapidjson::Value& message) { + return sendComponentMessage(activity.getURI(), message); + } /** * Register a callback for extension generated "Event" messages that are sent from the extension @@ -156,9 +283,23 @@ class ExtensionProxy { * * This method can be called multiple times to register multiple callbacks. * + * @deprecated Use the activity descriptor variant * @param callback The extension event callback. */ - virtual void registerEventCallback(Extension::EventCallback callback) = 0; + virtual void registerEventCallback(Extension::EventCallback callback) {} + + /** + * Register a callback for extension generated "Event" messages that are sent from the extension + * to the document. This callback is registered by the execution environment and called by ExtensionBase + * via invokeExtensionEventHandler(...). + * + * This method can be called multiple times to register multiple callbacks. + * + * @param activity The extension activity for the specified callback + * @param callback The extension event callback. + */ + virtual void registerEventCallback(const alexaext::ActivityDescriptor& activity, + Extension::EventActivityCallback&& callback) {} /** * Register a callback for extension generated "LiveDataUpdate" messages that are sent from @@ -167,41 +308,111 @@ class ExtensionProxy { * * This method can be called multiple times to register multiple callbacks. * + * @deprecated Use the activity descriptor variant * @param callback The live data update callback. */ - virtual void registerLiveDataUpdateCallback(Extension::LiveDataUpdateCallback callback) = 0; + virtual void registerLiveDataUpdateCallback(Extension::LiveDataUpdateCallback callback) {} - /** - * Invoked when an extension behind this proxy is successfully registered. - * - * @param uri The extension URI - * @param token The client token issued during registration - */ - virtual void onRegistered(const std::string &uri, const std::string &token) = 0; + /** + * Register a callback for extension generated "LiveDataUpdate" messages that are sent from + * the extension to the document. This callback is registered by the runtime and called by + * ExtensionBase via invokeLiveDataUpdate(...). + * + * This method can be called multiple times to register multiple callbacks. + * + * @param activity The extension activity for the specified callback + * @param callback The live data update callback. + */ + virtual void registerLiveDataUpdateCallback(const alexaext::ActivityDescriptor& activity, + Extension::LiveDataUpdateActivityCallback&& callback) {} - /** - * Invoked when an extension is unregistered. Session represented by the provided token is no longer valid. - * - * @param uri The extension URI - * @param token The client token issued during registration - */ - virtual void onUnregistered(const std::string &uri, const std::string &token) {} + /** + * Invoked when an extension behind this proxy is successfully registered. + * + * @deprecated Use the activity descriptor variant + * @param uri The extension URI + * @param token The activity token issued during registration + */ + virtual void onRegistered(const std::string &uri, const std::string &token) {} + + /** + * Invoked when an extension behind this proxy is successfully registered. + * + * @param activity The activity requesting this extension's functionality. + */ + virtual void onRegistered(const ActivityDescriptor& activity) { + onRegistered(activity.getURI(), activity.getId()); + } + + /** + * Invoked when an extension is unregistered. Session represented by the provided token is no longer valid. + * + * @deprecated Use the activity descriptor variant + * @param uri The extension URI + * @param token The activity token issued during registration + */ + virtual void onUnregistered(const std::string &uri, const std::string &token) {} + + /** + * Invoked when an extension is unregistered. Session represented by the provided token is no longer valid. + * + * @param activity The activity requesting this extension's functionality. + */ + virtual void onUnregistered(const ActivityDescriptor& activity) { + onUnregistered(activity.getURI(), activity.getId()); + } /** - * Invoked when a system rendering resource, such as display surface, is ready for use. This method - * will be called after the viewhost receives a "Component" message with a resource state of - * "Ready". Not all execution environments support shared rendering resources. + * Invoked when a system rendering resource, such as display surface, is ready for use. This + * method will be called after the execution environment receives a "Component" message with a + * resource state of "Ready". Not all execution environments support shared rendering resources. * + * @deprecated Use the activity descriptor variant * @param uri The extension URI. * @param resourceHolder Access to the rendering resource. */ - virtual void onResourceReady( const std::string& uri, - const ResourceHolderPtr& resourceHolder) = 0; + virtual void onResourceReady(const std::string &uri, const ResourceHolderPtr& resourceHolder) {} + + /** + * Invoked when a system rendering resource, such as display surface, is ready for use. This + * method will be called after the execution environment receives a "Component" message with a + * resource state of "Ready". Not all execution environments support shared rendering resources. + * + * @param activity The activity requesting this extension's functionality. + * @param resourceHolder Access to the rendering resource. + */ + virtual void onResourceReady(const ActivityDescriptor& activity, const ResourceHolderPtr& resourceHolder) { + onResourceReady(activity.getURI(), resourceHolder); + } + + /** + * @see Extension::onSessionStarted + */ + virtual void onSessionStarted(const SessionDescriptor& session) {} + + /** + * @see Extension::onSessionEnded + */ + virtual void onSessionEnded(const SessionDescriptor& session) {} + /** + * @see Extension::onForeground + */ + virtual void onForeground(const ActivityDescriptor& activity) {} + + /** + * @see Extension::onBackground + */ + virtual void onBackground(const ActivityDescriptor& activity) {} + + /** + * @see Extension::onHidden + */ + virtual void onHidden(const ActivityDescriptor& activity) {} }; using ExtensionProxyPtr = std::shared_ptr; } // namespace alexaext -#endif //_ALEXAEXT_EXTENSIONPROXY_H +#endif //_ALEXAEXT_EXTENSION_PROXY_H diff --git a/extensions/alexaext/include/alexaext/extensionregistrar.h b/extensions/alexaext/include/alexaext/extensionregistrar.h index 484dd91..725cfb6 100644 --- a/extensions/alexaext/include/alexaext/extensionregistrar.h +++ b/extensions/alexaext/include/alexaext/extensionregistrar.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _ALEXAEXT_EXTENSIONREGISTRAR_H -#define _ALEXAEXT_EXTENSIONREGISTRAR_H +#ifndef _ALEXAEXT_EXTENSION_REGISTRAR_H +#define _ALEXAEXT_EXTENSION_REGISTRAR_H #include #include @@ -27,11 +27,20 @@ namespace alexaext { /** * Default implementation of ExtensionProvider, maintained by the runtime. - * Provides a registry of extension URI to extension proxy. + * Provides a registry of directly registered extension URI to extension proxy plus provider + * delegation. */ class ExtensionRegistrar : public ExtensionProvider { public: + /** + * Add a specific ExtensionProvider. + * + * @param provider Provider implementation. + * @return This object for chaining. + */ + ExtensionRegistrar& addProvider(const ExtensionProviderPtr& provider); + /** * Register an extension. Called by the runtime to register a known extension. * @@ -43,7 +52,7 @@ class ExtensionRegistrar : public ExtensionProvider { /** * Identifies the presence of an extension. Called when a document has * requested an extension. This method returns true if an extension matching - * the given uri has been registered. + * the given uri has been registered or is available through any of known providers. * * @param uri The requsted extension URI. * @return true if the extension is registered. @@ -51,16 +60,19 @@ class ExtensionRegistrar : public ExtensionProvider { bool hasExtension(const std::string& uri) override; /** - * Get a proxy to the extension. Called when a document has requested - * an extension. + * Get a proxy to the extension. Called when a document has requested an extension. + * If an extension that supports the specified URI has been directly registered with this + * registrar, it will be returned. If not, the providers added to this registrar prior to + * this call will be queried inthe hash order (undefined). The first provider to have an + * extension with the specified URI will be used. Any remaining providers will not be queried. * * @param uri The extension URI. - * @return An extension proxy of a registered extension, nullptr if the extension - * was not registered. + * @return An extension proxy of a registered or provider-held extension. */ ExtensionProxyPtr getExtension(const std::string& uri) override; private: + std::set mProviders; std::map mExtensions; }; @@ -68,4 +80,4 @@ using ExtensionRegistrarPtr = std::shared_ptr; } // namespace alexaext -#endif // _ALEXAEXT_EXTENSIONREGISTRAR_H +#endif // _ALEXAEXT_EXTENSION_REGISTRAR_H diff --git a/extensions/alexaext/include/alexaext/extensionresourceprovider.h b/extensions/alexaext/include/alexaext/extensionresourceprovider.h index 9292045..42cef3b 100644 --- a/extensions/alexaext/include/alexaext/extensionresourceprovider.h +++ b/extensions/alexaext/include/alexaext/extensionresourceprovider.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef APL_EXTENSIONRESOURCEPROVIDER_H -#define APL_EXTENSIONRESOURCEPROVIDER_H +#ifndef APL_EXTENSION_RESOURCE_PROVIDER_H +#define APL_EXTENSION_RESOURCE_PROVIDER_H #include "extensionresourceholder.h" @@ -70,4 +70,4 @@ class ExtensionResourceProvider { using ExtensionResourceProviderPtr = std::shared_ptr; } // namespace alexaext -#endif // APL_EXTENSIONRESOURCEPROVIDER_H +#endif // APL_EXTENSION_RESOURCE_PROVIDER_H diff --git a/extensions/alexaext/include/alexaext/extensionuuid.h b/extensions/alexaext/include/alexaext/extensionuuid.h new file mode 100644 index 0000000..896a730 --- /dev/null +++ b/extensions/alexaext/include/alexaext/extensionuuid.h @@ -0,0 +1,76 @@ +/* + * 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 EXTENSIONS_EXTENSIONUUID_H +#define EXTENSIONS_EXTENSIONUUID_H + +#include +#include +#include +#include +#include + +namespace alexaext { +namespace uuid { + +inline std::string generateUUIDV4() { + const int dataSize = 16; + static const std::array hexValues = {'0', '1', '2', '3', '4', '5', '6', '7', + '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'}; + static std::random_device rd; + static std::mt19937_64 gen(rd()); + static std::uniform_int_distribution<> dis(0, 15); + std::array randomData {}; + + for(int i = 0; i < dataSize; ++i) + randomData[i] = static_cast(dis(rd)); + + // Not fully required, based on the function format_uuid_v3or5 from the RFC 4122 to help + // format + randomData[6] = 0x40 | (randomData[6] & 0xf); + randomData[8] = 0x80 | (randomData[8] & 0x3f); + // Transform to string, we rely on small string optimization so += doesn't take + // too much time + std::string returnValue; + auto hexValueToUuidValue = [&](uint8_t value) { + return std::make_tuple(hexValues[(value & 0xF0) >> 4], hexValues[value & 0xF]); + }; + auto transformHex = [&hexValueToUuidValue](int start, + int bytesToTransform, + const std::array &randomData, + std::string &returnValue) { + for (int i = start; i < start + bytesToTransform; i++) { + auto hexPair = hexValueToUuidValue(randomData[i]); + returnValue += std::get<0>(hexPair); + returnValue += std::get<1>(hexPair); + } + }; + transformHex(0, 4, randomData, returnValue); + returnValue += "-"; + transformHex(4, 2, randomData, returnValue); + returnValue += "-"; + transformHex(6, 2, randomData, returnValue); + returnValue += "-"; + transformHex(8, 2, randomData, returnValue); + returnValue += "-"; + transformHex(10, 6, randomData, returnValue); + return returnValue; +} + +using UUIDFunction = std::function; + +} // namespace uuid +} // namespace alexaext + +#endif // EXTENSIONS_EXTENSIONUUID_H \ No newline at end of file diff --git a/extensions/alexaext/include/alexaext/localextensionproxy.h b/extensions/alexaext/include/alexaext/localextensionproxy.h index 24665ce..5d1da83 100644 --- a/extensions/alexaext/include/alexaext/localextensionproxy.h +++ b/extensions/alexaext/include/alexaext/localextensionproxy.h @@ -13,8 +13,8 @@ * permissions and limitations under the License. */ -#ifndef _ALEXAEXT_LOCALEXTENSIONPROXY_H -#define _ALEXAEXT_LOCALEXTENSIONPROXY_H +#ifndef _ALEXAEXT_LOCAL_EXTENSION_PROXY_H +#define _ALEXAEXT_LOCAL_EXTENSION_PROXY_H #include @@ -70,40 +70,75 @@ class LocalExtensionProxy final : public ExtensionProxy, explicit LocalExtensionProxy(std::set uri, ExtensionFactory factory); std::set getURIs() const override; - bool getRegistration(const std::string& uri, const rapidjson::Value& registrationRequest, + bool getRegistration(const std::string& uri, + const rapidjson::Value& registrationRequest, RegistrationSuccessCallback success, RegistrationFailureCallback error) override; + bool getRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest, + RegistrationSuccessActivityCallback&& success, + RegistrationFailureActivityCallback&& error) override; bool initializeExtension(const std::string& uri) override; bool isInitialized(const std::string& uri) const override; bool invokeCommand(const std::string& uri, const rapidjson::Value& command, CommandSuccessCallback success, CommandFailureCallback error) override; + bool invokeCommand(const ActivityDescriptor& activity, + const rapidjson::Value& command, + CommandSuccessActivityCallback&& success, + CommandFailureActivityCallback&& error) override; void registerEventCallback(Extension::EventCallback callback) override; void registerLiveDataUpdateCallback(Extension::LiveDataUpdateCallback callback) override; - void onRegistered(const std::string &uri, const std::string &token) override; - void onUnregistered(const std::string &uri, const std::string &token) override; + void registerEventCallback(const ActivityDescriptor& activity, Extension::EventActivityCallback&& callback) override; + void registerLiveDataUpdateCallback(const ActivityDescriptor& activity, Extension::LiveDataUpdateActivityCallback&& callback) override; + void onRegistered(const std::string& uri, const std::string& token) override; + void onRegistered(const ActivityDescriptor& activity) override; + void onUnregistered(const std::string& uri, const std::string& token) override; + void onUnregistered(const ActivityDescriptor& activity) override; - bool sendMessage(const std::string &uri, const rapidjson::Value &message) override { - if (mExtension) - return mExtension->updateComponent(uri, message); - return false; - } + bool sendComponentMessage(const std::string &uri, const rapidjson::Value &message) override; + bool sendComponentMessage(const ActivityDescriptor &activity, const rapidjson::Value &message) override; - void onResourceReady(const std::string &uri, const ResourceHolderPtr& resourceHolder) override { - if (mExtension) - mExtension->onResourceReady(uri, resourceHolder); - } + void onResourceReady(const std::string& uri, const ResourceHolderPtr& resourceHolder) override; + void onResourceReady(const ActivityDescriptor& activity, const ResourceHolderPtr& resourceHolder) override; + + void onSessionStarted(const SessionDescriptor& session) override; + void onSessionEnded(const SessionDescriptor& session) override; + void onForeground(const ActivityDescriptor& activity) override; + void onBackground(const ActivityDescriptor& activity) override; + void onHidden(const ActivityDescriptor& activity) override; + +private: + using ProcessRegistrationCallback = std::function; + using ProcessCommandCallback = std::function; + + bool getRegistrationInternal(const std::string& uri, + const rapidjson::Value& registrationRequest, + RegistrationSuccessCallback&& success, + RegistrationFailureCallback&& error, + ProcessRegistrationCallback&& processRegistration); + + bool invokeCommandInternal(const std::string& uri, + const rapidjson::Value& command, + CommandSuccessCallback&& success, + CommandFailureCallback&& error, + ProcessCommandCallback&& processCommand); private: + using EventCallbacks = std::shared_ptr>; + using LiveDataCallbacks = std::shared_ptr>; + ExtensionPtr mExtension; ExtensionFactory mFactory; std::set mURIs; std::set mInitialized; - std::vector mEventCallbacks; - std::vector mLiveDataCallbacks; + std::vector mEventCallbacks; // For backwards compatibility + std::map mEventActivityCallbacks; + std::vector mLiveDataCallbacks; // For backwards compatibility + std::map mLiveDataActivityCallbacks; }; using LocalExtensionProxyPtr = std::shared_ptr; } // namespace alexaext -#endif //_ALEXAEXT_LOCALEXTENSIONPROXY_H +#endif //_ALEXAEXT_LOCAL_EXTENSION_PROXY_H diff --git a/extensions/alexaext/include/alexaext/random.h b/extensions/alexaext/include/alexaext/random.h new file mode 100644 index 0000000..e01337f --- /dev/null +++ b/extensions/alexaext/include/alexaext/random.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 _ALEXAEXT_RANDOM_H +#define _ALEXAEXT_RANDOM_H + +#include + +namespace alexaext { + +/** + * Generates a base-36 token with the specified length. + * + * @param prefix The optional prefix to prepend to the random token + * @param len The length of the generated token, ignoring the prefix. + * @return The randomly generated token + */ +std::string generateBase36Token(const std::string& prefix = "", unsigned int len = 8); + +}; + +#endif // _ALEXAEXT_RANDOM_H diff --git a/extensions/alexaext/include/alexaext/sessiondescriptor.h b/extensions/alexaext/include/alexaext/sessiondescriptor.h new file mode 100644 index 0000000..0d4daf8 --- /dev/null +++ b/extensions/alexaext/include/alexaext/sessiondescriptor.h @@ -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. + */ + +#ifndef _ALEXAEXT_SESSION_DESCRIPTOR_H +#define _ALEXAEXT_SESSION_DESCRIPTOR_H + +#include + +#include "types.h" + +namespace alexaext { + +/** + * Represents an extension session, i.e. a group of related activities. + * + * Session descriptors are immutable and hashable, so they are + * suitable to use as keys in unordered maps or other hashing data structures. + * + * @see SessionId + */ +class SessionDescriptor final { +public: + /** + * Use create + */ + SessionDescriptor(); + + /** + * Use create + */ + explicit SessionDescriptor(const SessionId& sessionId); + ~SessionDescriptor() = default; + + /** + * Creates a session descriptor with a randomly generated ID. + * + * @return The session descriptor + */ + static std::shared_ptr create() { return std::make_shared(); } + + /** + * Creates a session descriptor with the specified ID. This is only intended to be used for + * situations where a session needs to be serialized/deserialized. Prefer using the no-arg + * variant to create a new original session descriptor. + * + * @return The session descriptor + */ + static std::shared_ptr create(const SessionId& sessionId) { return std::make_shared(sessionId); } + + /** + * Returns the globally unique identifier for the current session. + * + * @return The globally unique identifier for this session. + */ + const SessionId& getId() const { return mSessionId; } + + bool operator==(const SessionDescriptor& other) const { + return mSessionId == other.mSessionId; + } + + bool operator!=(const SessionDescriptor& rhs) const { return !(rhs == *this); } + + /** + * Provides hashing for session descriptors so they can easily be used as unordered map keys. + */ + class Hash final { + public: + std::size_t operator()(const SessionDescriptor& descriptor) const { + static std::hash stringHash; + // Variant of Apache Commons HashCodeBuilder hashing algorithm + std::size_t hash = 17; + hash = hash * 37 + stringHash(descriptor.getId()); + return hash; + } + }; + + /** + * Provides comparison for session descriptors so they can easily be used as ordered map keys. + */ + class Compare final { + public: + bool operator()(const SessionDescriptor& first, const SessionDescriptor& second) const { + return first.getId() < second.getId(); + } + }; + +private: + SessionId mSessionId; +}; + +using SessionDescriptorPtr = std::shared_ptr; + +} // namespace alexaext + +#endif // _ALEXAEXT_SESSION_DESCRIPTOR_H diff --git a/extensions/alexaext/include/alexaext/types.h b/extensions/alexaext/include/alexaext/types.h new file mode 100644 index 0000000..ecc46ec --- /dev/null +++ b/extensions/alexaext/include/alexaext/types.h @@ -0,0 +1,53 @@ +/** + * 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 _ALEXAEXT_TYPES_H +#define _ALEXAEXT_TYPES_H + +#include +#include +#include + +namespace alexaext { + +/** + * Uniquely identifies an extension session. A session is a group of related activities for which + * sharing of state is allowed (proper care must be taken to protect sensitive data when defining + * session boundaries). The lifetime of each activity is bound by the lifetime of its + * enclosing session. For example, a typical session for APL rendering activities is an Alexa Skills + * Kit (ASK) skill session. Nesting sessions is not allowed. + * + * Session identifiers are intended to be opaque values. They are typically generated by + * the execution environment. Extensions should not make assumptions about their format or attempt + * to parse them to extract information. For example, there is no guarantee that a session + * identifier would relate to any existing Alexa identifier. + */ +using SessionId = std::string; + +/** + * Uniquely identifies an activity that requested the extension. For example, a common activity + * is an APL rendering task for a given document (the same document can be used by more than one + * activity). + * + * Activity identifiers are intended to be opaque values. Extensions should not make assumptions + * about their format or attempt to parse them to extract information. For example, there is + * no guarantee that an activity identifier would relate to any existing Alexa identifier or + * APL token (for APL rendering activities). + */ +using ActivityId = std::string; + +} // namespace alexaext + +#endif // _ALEXAEXT_TYPES_H diff --git a/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp b/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp new file mode 100644 index 0000000..fac3898 --- /dev/null +++ b/extensions/alexaext/src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp @@ -0,0 +1,227 @@ +/* + * 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/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h" + +using namespace E2EEncryption; +using namespace alexaext; + +// version of the extension definition Schema +static const std::string SCHEMA_VERSION = "1.0"; + +// Available algorithms +static const char* E2EENCRYPTION_AES = "AES/GCM/NoPadding"; +static const char* E2EENCRYPTION_RSA = "RSA/ECB/OAEPWithSHA1AndMGF1Padding"; + +// Available commands +static const char* BASE64_ENCRYPT_VALUE = "Base64EncryptValue"; +static const char* BASE64_ENCODE_VALUE = "Base64EncodeValue"; + +// Command Response Types +static const char* ENCRYPTION_PAYLOAD = "EncryptionPayload"; +static const char* ENCODING_PAYLOAD = "EncodingPayload"; + +// Types Properties and Constants +static const char* TOKEN_PROPERTY = "token"; +static const char* VALUE_PROPERTY = "value"; +static const char* ALGORITHM_PROPERTY = "algorithm"; +static const char* KEY_PROPERTY = "key"; +static const char* AAD_PROPERTY = "aad"; +static const char* BASE64_ENCODED_PROPERTY = "base64Encoded"; +static const char* BASE64_ENCODED_DATA_PROPERTY = "base64EncodedData"; +static const char* ERROR_REASON_PROPERTY = "errorReason"; +static const char* BASE64_ENCRYPTED_DATA_PROPERTY = "base64EncryptedData"; +static const char* BASE64_ENCODED_IV_PROPERTY = "base64EncodedIV"; +static const char* BASE64_ENCODED_KEY_PROPERTY = "base64EncodedKey"; + +static const char* STRING_TYPE = "string"; +static const char* BOOLEAN_TYPE = "boolean"; + +// Events +static const char* ON_ENCRYPT_SUCCESS = "OnEncryptSuccess"; +static const char* ON_ENCRYPT_FAILURE = "OnEncryptFailure"; +static const char* ON_BASE64_ENCODE_SUCCESS = "OnBase64EncodeSuccess"; + +AplE2eEncryptionExtension::AplE2eEncryptionExtension( + AplE2eEncryptionExtensionObserverInterfacePtr observer, + alexaext::ExecutorPtr executor, + alexaext::uuid::UUIDFunction uuidGenerator) + : alexaext::ExtensionBase(URI), + mObserver(std::move(observer)), + mExecutor(executor), + mUuidGenerator(std::move(uuidGenerator)) {} + +rapidjson::Document +AplE2eEncryptionExtension::createRegistration(const std::string& uri, + const rapidjson::Value& registrationRequest) { + if (uri != URI) + return RegistrationFailure::forUnknownURI(uri); + + return RegistrationSuccess(SCHEMA_VERSION) + .uri(URI) + .token(mUuidGenerator()) + .environment([](Environment& environment) { + environment.version(ENVIRONMENT_VERSION); + environment.property(E2EENCRYPTION_AES, true); + environment.property(E2EENCRYPTION_RSA, true); + }) + .schema(SCHEMA_VERSION, [&](ExtensionSchema& schema) { + schema.uri(URI) + .command(BASE64_ENCRYPT_VALUE, + [](CommandSchema& commandSchema) { + commandSchema.dataType(ENCRYPTION_PAYLOAD); + }) + .command(BASE64_ENCODE_VALUE, + [](CommandSchema& commandSchema) { + commandSchema.dataType(ENCODING_PAYLOAD); + }) + .event(ON_ENCRYPT_SUCCESS) + .event(ON_ENCRYPT_FAILURE) + .event(ON_BASE64_ENCODE_SUCCESS) + .dataType(ENCRYPTION_PAYLOAD, + [](TypeSchema& typeSchema) { + typeSchema + .property(TOKEN_PROPERTY, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE); + typePropertySchema.required(true); + }) + .property(VALUE_PROPERTY, STRING_TYPE) + .property(ALGORITHM_PROPERTY, STRING_TYPE) + .property(KEY_PROPERTY, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE); + typePropertySchema.required(false); + }) + .property(AAD_PROPERTY, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE); + typePropertySchema.required(false); + }) + .property(BASE64_ENCODED_PROPERTY, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(BOOLEAN_TYPE); + typePropertySchema.required(false); + }); + }) + .dataType(ENCODING_PAYLOAD, [](TypeSchema& typeSchema) { + typeSchema + .property(TOKEN_PROPERTY, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE); + typePropertySchema.required(true); + }) + .property(VALUE_PROPERTY, STRING_TYPE); + }); + }); +} + +bool +AplE2eEncryptionExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) { + if (uri != URI) + return false; + + if (!mObserver) + return false; + + auto executor = mExecutor.lock(); + if (!executor) + return false; + + const std::string& name = GetWithDefault(Command::NAME(), command, ""); + if (BASE64_ENCRYPT_VALUE == name) { + const rapidjson::Value* params = Command::PAYLOAD().Get(command); + if (!params || !params->HasMember(VALUE_PROPERTY) || !params->HasMember(ALGORITHM_PROPERTY)) + return false; + auto token = GetWithDefault(TOKEN_PROPERTY, params, ""); + auto value = GetWithDefault(VALUE_PROPERTY, params, ""); + auto key = GetWithDefault(KEY_PROPERTY, params, ""); + auto algorithm = GetWithDefault(ALGORITHM_PROPERTY, params, ""); + auto aad = GetWithDefault(AAD_PROPERTY, params, ""); + auto base64Encoded = GetWithDefault(BASE64_ENCODED_PROPERTY, params, false); + auto thisWeakPointer = std::weak_ptr(shared_from_this()); + auto successCallback = [thisWeakPointer](const std::string& token, + const std::string& base64EncryptedData, + const std::string& base64EncodedIV, + const std::string& base64EncodedKey) { + auto ptr = thisWeakPointer.lock(); + if (!ptr) + return; + auto event = Event(SCHEMA_VERSION) + .uri(URI) + .target(URI) + .name(ON_ENCRYPT_SUCCESS) + .property(TOKEN_PROPERTY, token); + if (!base64EncryptedData.empty()) + event.property(BASE64_ENCRYPTED_DATA_PROPERTY, base64EncryptedData); + if (!base64EncodedIV.empty()) + event.property(BASE64_ENCODED_IV_PROPERTY, base64EncodedIV); + if (!base64EncodedKey.empty()) + event.property(BASE64_ENCODED_KEY_PROPERTY, base64EncodedKey); + + ptr->invokeExtensionEventHandler(URI, event); + }; + auto errorCallback = [thisWeakPointer](const std::string& token, + const std::string& reason) { + auto ptr = thisWeakPointer.lock(); + if (!ptr) + return; + auto event = Event(SCHEMA_VERSION) + .uri(URI) + .target(URI) + .name(ON_ENCRYPT_FAILURE) + .property(TOKEN_PROPERTY, token) + .property(ERROR_REASON_PROPERTY, reason); + ptr->invokeExtensionEventHandler(URI, event); + }; + executor->enqueueTask([&](){ + mObserver->onBase64EncryptValue(token, key, algorithm, aad, value, base64Encoded, + successCallback, errorCallback); + }); + return true; + } + + if (BASE64_ENCODE_VALUE == name) { + const rapidjson::Value* params = Command::PAYLOAD().Get(command); + if (!params || !params->HasMember(VALUE_PROPERTY)) + return false; + auto token = GetWithDefault(TOKEN_PROPERTY, params, ""); + auto value = GetWithDefault(VALUE_PROPERTY, params, ""); + auto thisWeakPointer = std::weak_ptr(shared_from_this()); + auto successCallback = [thisWeakPointer](const std::string& token, + const std::string& base64EncodedData) { + auto ptr = thisWeakPointer.lock(); + if (!ptr) + return; + auto event = Event(SCHEMA_VERSION) + .uri(URI) + .target(URI) + .name(ON_BASE64_ENCODE_SUCCESS) + .property(TOKEN_PROPERTY, token) + .property(BASE64_ENCODED_DATA_PROPERTY, base64EncodedData); + ptr->invokeExtensionEventHandler(URI, event); + }; + executor->enqueueTask([&](){ + return mObserver->onBase64EncodeValue(token, value, successCallback); + }); + return true; + } + + return false; +} \ No newline at end of file diff --git a/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp b/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp new file mode 100644 index 0000000..d820f85 --- /dev/null +++ b/extensions/alexaext/src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp @@ -0,0 +1,76 @@ +/* + * 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/APLMusicAlarmExtension/AplMusicAlarmExtension.h" + +using namespace MusicAlarm; +using namespace alexaext; + +// Commands +static const std::string COMMAND_DISMISS_NAME = "DismissAlarm"; +static const std::string COMMAND_SNOOZE_NAME = "SnoozeAlarm"; + +// version of the extension definition Schema +static const std::string SCHEMA_VERSION = "1.0"; + +AplMusicAlarmExtension::AplMusicAlarmExtension(AplMusicAlarmExtensionObserverInterfacePtr observer, + alexaext::ExecutorPtr executor, + alexaext::uuid::UUIDFunction uuidGenerator) + : alexaext::ExtensionBase(URI), + mObserver(std::move(observer)), + mExecutor(executor), + mUuidGenerator(std::move(uuidGenerator)) {} + +rapidjson::Document +AplMusicAlarmExtension::createRegistration(const std::string& uri, + const rapidjson::Value& registrationRequest) { + if (uri != URI) + return RegistrationFailure::forUnknownURI(uri); + + return RegistrationSuccess(SCHEMA_VERSION) + .uri(URI) + .token(mUuidGenerator()) + .schema(SCHEMA_VERSION, [&](ExtensionSchema& schema) { + schema + .uri(URI) + .command(COMMAND_DISMISS_NAME, + [](CommandSchema& commandSchema) { commandSchema.allowFastMode(true); }) + .command(COMMAND_SNOOZE_NAME, + [](CommandSchema& commandSchema) { commandSchema.allowFastMode(true); }); + }); +} + +bool +AplMusicAlarmExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) { + if (!mObserver) + return false; + + auto executor = mExecutor.lock(); + if (!executor) + return false; + + const std::string& name = GetWithDefault(Command::NAME(), command, ""); + if (COMMAND_DISMISS_NAME == name) { + executor->enqueueTask([&]() { mObserver->dismissAlarm(); }); + return true; + } + if (COMMAND_SNOOZE_NAME == name) { + executor->enqueueTask([&]() { mObserver->snoozeAlarm(); }); + return true; + } + return false; +} \ No newline at end of file diff --git a/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp b/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp new file mode 100644 index 0000000..e933fee --- /dev/null +++ b/extensions/alexaext/src/APLWebflowExtension/AplWebflowBase.cpp @@ -0,0 +1,38 @@ +/* + * 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/APLWebflowExtension/AplWebflowBase.h" + +using namespace Webflow; + +AplWebflowBase::AplWebflowBase(std::string token, std::string uri, std::string flowId) + : mToken(std::move(token)), + mUri(std::move(uri)), + mFlowId(std::move(flowId)) {} + +const std::string& +AplWebflowBase::getUri() const { + return mUri; +} + +const std::string& +AplWebflowBase::getFlowId() const { + return mFlowId; +} + +const std::string& +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 new file mode 100644 index 0000000..3396bbc --- /dev/null +++ b/extensions/alexaext/src/APLWebflowExtension/AplWebflowExtension.cpp @@ -0,0 +1,137 @@ +/* + * 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 +#include + +#include + +#include "alexaext/APLWebflowExtension/AplWebflowExtension.h" + +using namespace Webflow; +using namespace alexaext; + +// Data types +static const char* PAYLOAD_START_FLOW = "StartFlowPayload"; + +// Commands +static const char* COMMAND_START_FLOW = "StartFlow"; + +// Events +static const char* EVENT_ON_FLOW_END = "OnFlowEnd"; + +// Properties +static const char* PROPERTY_FLOW_ID = "flowId"; +static const char* PROPERTY_URL = "url"; +static const char* PROPERTY_TOKEN = "token"; + +// Constants +static const char* SCHEMA_VERSION = "1.0"; + +AplWebflowExtension::AplWebflowExtension( + std::function tokenGenerator, + std::shared_ptr observer, + const std::shared_ptr& executor) + : alexaext::ExtensionBase(URI), + mObserver(std::move(observer)), + mTokenGenerator(std::move(tokenGenerator)), + mExecutor(executor) {} + +rapidjson::Document +AplWebflowExtension::createRegistration(const std::string& uri, + const rapidjson::Value& registrationRequest) { + if (uri != URI) { + return RegistrationFailure::forUnknownURI(uri); + } + + // return success with the schema and environment + return RegistrationSuccess(SCHEMA_VERSION) + .uri(URI) + .token(mTokenGenerator()) + .schema(SCHEMA_VERSION, [&](ExtensionSchema& schema) { + schema + .uri(URI) + .dataType(PAYLOAD_START_FLOW, + [](TypeSchema& typeSchema) { + typeSchema + .property(PROPERTY_FLOW_ID, + [](TypePropertySchema& propertySchema) { + propertySchema.type("string").required(false); + }) + .property(PROPERTY_URL, [](TypePropertySchema& propertySchema) { + propertySchema.type("string").required(true); + }); + }) + .command(COMMAND_START_FLOW, + [](CommandSchema& commandSchema) { + commandSchema.allowFastMode(true).dataType(PAYLOAD_START_FLOW); + }) + .event(EVENT_ON_FLOW_END); + }); +} + +bool +AplWebflowExtension::invokeCommand(const std::string& uri, const rapidjson::Value& command) { + // unknown URI + if (uri != URI) + return false; + + if (!mObserver) + return false; + + auto executor = mExecutor.lock(); + if (!executor) + return false; + + const std::string &name = GetWithDefault(Command::NAME(), command, ""); + + if (COMMAND_START_FLOW == name) { + const rapidjson::Value *params = Command::PAYLOAD().Get(command); + if (!params || !params->HasMember(PROPERTY_URL)) + return false; + + std::string url = GetWithDefault(PROPERTY_URL, *params, ""); + if (url.empty()) + return false; + + std::string token = GetWithDefault(PROPERTY_TOKEN, *params, ""); + std::string flowId = GetWithDefault(PROPERTY_FLOW_ID, *params, ""); + + if (flowId.empty()) { + executor->enqueueTask([&]() { mObserver->onStartFlow(token, url, flowId); }); + } + else { + std::weak_ptr thisWeak = shared_from_this(); + auto onFlowEnd = [thisWeak](const std::string& token, const std::string& flowId){ + auto lockPtr = thisWeak.lock(); + if (lockPtr) { + auto event = Event(SCHEMA_VERSION) + .uri(URI) + .target(URI) + .name(EVENT_ON_FLOW_END) + .property(PROPERTY_TOKEN, token) + .property(PROPERTY_FLOW_ID, flowId); + lockPtr->invokeExtensionEventHandler(URI, event); + } + }; + executor->enqueueTask([&]() { mObserver->onStartFlow(token, url, flowId, onFlowEnd); }); + } + return true; + } + + return false; +} \ No newline at end of file diff --git a/extensions/alexaext/src/extensionregistrar.cpp b/extensions/alexaext/src/extensionregistrar.cpp index 2a3d52f..ad449cc 100644 --- a/extensions/alexaext/src/extensionregistrar.cpp +++ b/extensions/alexaext/src/extensionregistrar.cpp @@ -15,10 +15,22 @@ #include "alexaext/extensionregistrar.h" +#include + namespace alexaext { ExtensionRegistrar& -ExtensionRegistrar::registerExtension(const ExtensionProxyPtr& proxy) { +ExtensionRegistrar::addProvider(const ExtensionProviderPtr& provider) +{ + if (provider) { + mProviders.emplace(provider); + } + return *this; +} + +ExtensionRegistrar& +ExtensionRegistrar::registerExtension(const ExtensionProxyPtr& proxy) +{ if (proxy) { for (const auto& uri : proxy->getURIs()) { mExtensions.emplace(uri, proxy); @@ -30,19 +42,39 @@ ExtensionRegistrar::registerExtension(const ExtensionProxyPtr& proxy) { bool ExtensionRegistrar::hasExtension(const std::string& uri) { - return (mExtensions.find(uri) != mExtensions.end()); + if (mExtensions.find(uri) != mExtensions.end()) return true; + return std::any_of( + mProviders.begin(), + mProviders.end(), + [uri](const ExtensionProviderPtr& provider) { + return provider->hasExtension(uri); + }); } ExtensionProxyPtr ExtensionRegistrar::getExtension(const std::string& uri) { - auto proxy = mExtensions.find(uri); - if (proxy == mExtensions.end()) - return nullptr; - if (!proxy->second->isInitialized(uri)) { - if (!proxy->second->initializeExtension(uri)) return nullptr; + ExtensionProxyPtr proxy; + auto it = mExtensions.find(uri); + + if (it == mExtensions.end()) { + for (auto& provider : mProviders) { + proxy = provider->getExtension(uri); + if (proxy) { + mExtensions.emplace(uri, proxy); + break; + } + } + } else { + proxy = it->second; + } + + if (!proxy) return nullptr; + + if (!proxy->isInitialized(uri)) { + if (!proxy->initializeExtension(uri)) return nullptr; } - return proxy->second; + return proxy; } } // namespace alexaext diff --git a/extensions/alexaext/src/localextensionproxy.cpp b/extensions/alexaext/src/localextensionproxy.cpp index 76e43a1..065dd50 100644 --- a/extensions/alexaext/src/localextensionproxy.cpp +++ b/extensions/alexaext/src/localextensionproxy.cpp @@ -15,6 +15,8 @@ #include "alexaext/localextensionproxy.h" +#include + namespace alexaext { LocalExtensionProxy::LocalExtensionProxy(const ExtensionPtr& extension) @@ -43,10 +45,11 @@ std::set LocalExtensionProxy::getURIs() const } bool -LocalExtensionProxy::getRegistration(const std::string& uri, - const rapidjson::Value& registrationRequest, - RegistrationSuccessCallback success, - RegistrationFailureCallback error) +LocalExtensionProxy::getRegistrationInternal(const std::string& uri, + const rapidjson::Value& registrationRequest, + RegistrationSuccessCallback&& success, + RegistrationFailureCallback&& error, + ProcessRegistrationCallback&& processRegistration) { int errorCode = kErrorNone; std::string errorMsg; @@ -66,7 +69,7 @@ LocalExtensionProxy::getRegistration(const std::string& uri, // request the schema from the extension rapidjson::Document registration; try { - registration = mExtension->createRegistration(uri, registrationRequest); + registration = processRegistration(registrationRequest); } catch (const std::exception& e) { errorCode = kErrorExtensionException; errorMsg = e.what(); @@ -108,6 +111,39 @@ LocalExtensionProxy::getRegistration(const std::string& uri, return true; } +bool +LocalExtensionProxy::getRegistration(const std::string& uri, + const rapidjson::Value& registrationRequest, + ExtensionProxy::RegistrationSuccessCallback success, + ExtensionProxy::RegistrationFailureCallback error) { + return getRegistrationInternal(uri, + registrationRequest, + std::move(success), + std::move(error), + [&](const rapidjson::Value ®istrationRequest) { + return mExtension->createRegistration(uri, registrationRequest); + }); +} + +bool +LocalExtensionProxy::getRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest, + RegistrationSuccessActivityCallback&& success, + RegistrationFailureActivityCallback&& error) +{ + return getRegistrationInternal(activity.getURI(), + registrationRequest, + [activity, success](const std::string& uri, const rapidjson::Value ®istrationSuccess) { + success(activity, registrationSuccess); + }, + [activity, error](const std::string& uri, const rapidjson::Value ®istrationFailure) { + error(activity, registrationFailure); + }, + [&](const rapidjson::Value ®istrationRequest) { + return mExtension->createRegistration(activity, registrationRequest); + }); +} + bool LocalExtensionProxy::initializeExtension(const std::string& uri) { @@ -119,22 +155,58 @@ LocalExtensionProxy::initializeExtension(const std::string& uri) if (!mExtension || mInitialized.count(uri)) return false; std::weak_ptr weakSelf = shared_from_this(); - mExtension->registerEventCallback([weakSelf](const std::string& uri, const rapidjson::Value &event) { - if (auto self = weakSelf.lock()) { - for (const auto &callback : self->mEventCallbacks) { - callback(uri, event); - } - } - }); + mExtension->registerEventCallback( + [weakSelf](const alexaext::ActivityDescriptor& activity, const rapidjson::Value &event) { + if (auto self = weakSelf.lock()) { + auto it = self->mEventActivityCallbacks.find(activity); + if (it != self->mEventActivityCallbacks.end()) { + for (const auto& callback : *it->second) { + callback(activity, event); + } + } else { + // Fall back to legacy callbacks, but only if we don't have activity + // callbacks. Otherwise, we could end up double reporting events. + for (const auto& callback : self->mEventCallbacks) { + callback(activity.getURI(), event); + } + } + } + }); + // For backwards compatibility + mExtension->registerEventCallback( + [weakSelf](const std::string& uri, const rapidjson::Value &event) { + if (auto self = weakSelf.lock()) { + for (const auto &callback : self->mEventCallbacks) { + callback(uri, event); + } + } + }); mExtension->registerLiveDataUpdateCallback( - [weakSelf](const std::string& uri, const rapidjson::Value &liveDataUpdate) { - if (auto self = weakSelf.lock()) { - for (const auto &callback : self->mLiveDataCallbacks) { - callback(uri, liveDataUpdate); - } - } - }); + [weakSelf](const alexaext::ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + if (auto self = weakSelf.lock()) { + auto it = self->mLiveDataActivityCallbacks.find(activity); + if (it != self->mLiveDataActivityCallbacks.end()) { + for (const auto& callback : *it->second) { + callback(activity, liveDataUpdate); + } + } else { + // Fall back to legacy callbacks, but only if we don't have activity + // callbacks. Otherwise, we could end up double reporting events. + for (const auto& callback : self->mLiveDataCallbacks) { + callback(activity.getURI(), liveDataUpdate); + } + } + } + }); + mExtension->registerLiveDataUpdateCallback( + [weakSelf](const std::string& uri, const rapidjson::Value &liveDataUpdate) { + if (auto self = weakSelf.lock()) { + for (const auto &callback : self->mLiveDataCallbacks) { + callback(uri, liveDataUpdate); + } + } + }); mInitialized.emplace(uri); @@ -148,10 +220,11 @@ LocalExtensionProxy::isInitialized(const std::string& uri) const } bool -LocalExtensionProxy::invokeCommand(const std::string& uri, - const rapidjson::Value& command, - CommandSuccessCallback success, - CommandFailureCallback error) +LocalExtensionProxy::invokeCommandInternal(const std::string& uri, + const rapidjson::Value& command, + CommandSuccessCallback&& success, + CommandFailureCallback&& error, + ProcessCommandCallback&& processCommand) { // verify the command has an ID const rapidjson::Value* commandValue = Command::ID().Get(command); @@ -185,7 +258,7 @@ LocalExtensionProxy::invokeCommand(const std::string& uri, std::string errorMsg; bool result = false; try { - result = mExtension->invokeCommand(uri, command); + result = processCommand(command); } catch (const std::exception& e) { errorCode = kErrorExtensionException; errorMsg = e.what(); @@ -222,6 +295,38 @@ LocalExtensionProxy::invokeCommand(const std::string& uri, return true; } +bool +LocalExtensionProxy::invokeCommand(const std::string& uri, const rapidjson::Value& command, + ExtensionProxy::CommandSuccessCallback success, + ExtensionProxy::CommandFailureCallback error) { + return invokeCommandInternal(uri, + command, + std::move(success), + std::move(error), + [&](const rapidjson::Value& command) { + return mExtension->invokeCommand(uri, command); + }); +} + +bool +LocalExtensionProxy::invokeCommand(const ActivityDescriptor& activity, + const rapidjson::Value& command, + CommandSuccessActivityCallback&& success, + CommandFailureActivityCallback&& error) +{ + return invokeCommandInternal(activity.getURI(), + command, + [activity, success](const std::string& uri, const rapidjson::Value &commandSuccess) { + success(activity, commandSuccess); + }, + [activity, error](const std::string& uri, const rapidjson::Value &commandFailure) { + error(activity, commandFailure); + }, + [&](const rapidjson::Value& command) { + return mExtension->invokeCommand(activity, command); + }); +} + void LocalExtensionProxy::registerEventCallback(Extension::EventCallback callback) { @@ -235,15 +340,129 @@ LocalExtensionProxy::registerLiveDataUpdateCallback(Extension::LiveDataUpdateCal } void -LocalExtensionProxy::onRegistered(const std::string &uri, const std::string &token) +LocalExtensionProxy::registerEventCallback(const ActivityDescriptor& activity, Extension::EventActivityCallback&& callback) +{ + if (!callback) return; + + auto it = mEventActivityCallbacks.find(activity); + if (it != mEventActivityCallbacks.end()) { + it->second->emplace_back(callback); + } else { + auto callbacks = std::make_shared>(); + callbacks->emplace_back(callback); + mEventActivityCallbacks.emplace(activity, callbacks); + } +} + +void +LocalExtensionProxy::registerLiveDataUpdateCallback(const ActivityDescriptor& activity, Extension::LiveDataUpdateActivityCallback&& callback) +{ + if (!callback) return; + + auto it = mLiveDataActivityCallbacks.find(activity); + if (it != mLiveDataActivityCallbacks.end()) { + it->second->emplace_back(callback); + } else { + auto callbacks = std::make_shared>(); + callbacks->emplace_back(callback); + mLiveDataActivityCallbacks.emplace(activity, callbacks); + } +} + +void +LocalExtensionProxy::onRegistered(const std::string& uri, const std::string& token) { if (mExtension) mExtension->onRegistered(uri, token); } void -LocalExtensionProxy::onUnregistered(const std::string &uri, const std::string &token) +LocalExtensionProxy::onRegistered(const ActivityDescriptor& activity) +{ + if (mExtension) mExtension->onActivityRegistered(activity); +} + +void +LocalExtensionProxy::onUnregistered(const std::string& uri, const std::string& token) { if (mExtension) mExtension->onUnregistered(uri, token); } +void +LocalExtensionProxy::onUnregistered(const ActivityDescriptor& activity) +{ + if (mExtension) { + mExtension->onActivityUnregistered(activity); + } + + mEventActivityCallbacks.erase(activity); + mLiveDataActivityCallbacks.erase(activity); +} + +void +LocalExtensionProxy::onSessionStarted(const SessionDescriptor& session) { + if (mExtension) mExtension->onSessionStarted(session); +} + +void +LocalExtensionProxy::onSessionEnded(const SessionDescriptor& session) { + if (mExtension) mExtension->onSessionEnded(session); +} + +bool +LocalExtensionProxy::sendComponentMessage(const std::string& uri, const rapidjson::Value& message) { + if (!mExtension) return false; + + const auto* method = GetWithDefault("method", message, ""); + if (std::strcmp(method, "Component") == 0) { + return mExtension->updateComponent(uri, message); + } + + return false; +} + +bool +LocalExtensionProxy::sendComponentMessage(const ActivityDescriptor& activity, + const rapidjson::Value& message) { + if (!mExtension) return false; + + const auto* method = GetWithDefault("method", message, ""); + if (std::strcmp(method, "Component") == 0) { + return mExtension->updateComponent(activity, message); + } + + return false; +} + +void +LocalExtensionProxy::onResourceReady(const std::string& uri, + const ResourceHolderPtr& resourceHolder) { + if (mExtension) + mExtension->onResourceReady(uri, resourceHolder); +} + +void +LocalExtensionProxy::onResourceReady(const ActivityDescriptor& activity, + const ResourceHolderPtr& resourceHolder) { + if (mExtension) + mExtension->onResourceReady(activity, resourceHolder); +} + +void +LocalExtensionProxy::onForeground(const ActivityDescriptor& activity) { + if (mExtension) + mExtension->onForeground(activity); +} + +void +LocalExtensionProxy::onBackground(const ActivityDescriptor& activity) { + if (mExtension) + mExtension->onBackground(activity); +} + +void +LocalExtensionProxy::onHidden(const ActivityDescriptor& activity) { + if (mExtension) + mExtension->onHidden(activity); +} + } // namespace alexaext diff --git a/extensions/alexaext/src/random.cpp b/extensions/alexaext/src/random.cpp new file mode 100644 index 0000000..a77493b --- /dev/null +++ b/extensions/alexaext/src/random.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 "alexaext/random.h" + +#include +#include + +namespace alexaext { + +/** + * @return 64-bit Mersenne Twister random number generator. + */ +std::mt19937_64 +mt64Generator() +{ + static std::random_device random_device; + static std::mt19937_64 generator(random_device()); + return generator; +} + +static const char *base36chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"; + +std::string +generateBase36Token(const std::string& prefix, unsigned int len) { + static auto gen = mt64Generator(); + static std::uniform_int_distribution<> distrib(0, 35); + + auto prefixLen = prefix.length(); + auto totalLen = prefixLen + len + 1; + char token[totalLen]; + std::strcpy(token, prefix.c_str()); + + for (int i = 0; i < len; i++) { + token[prefixLen + i] = base36chars[distrib(gen)]; + } + + token[totalLen - 1] = '\0'; + + return std::string(token); +} + +} // namespace alexaext \ No newline at end of file diff --git a/extensions/alexaext/src/sessiondescriptor.cpp b/extensions/alexaext/src/sessiondescriptor.cpp new file mode 100644 index 0000000..2c7d0e1 --- /dev/null +++ b/extensions/alexaext/src/sessiondescriptor.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 "alexaext/sessiondescriptor.h" + +#include "alexaext/random.h" + +namespace alexaext { + +SessionDescriptor::SessionDescriptor() : mSessionId(generateBase36Token("session-")) {} + +SessionDescriptor::SessionDescriptor(const std::string& sessionId) : mSessionId(sessionId) {} + +} // namespace alexaext \ No newline at end of file diff --git a/extensions/unit/CMakeLists.txt b/extensions/unit/CMakeLists.txt index 4ef3b7f..7e0094b 100644 --- a/extensions/unit/CMakeLists.txt +++ b/extensions/unit/CMakeLists.txt @@ -23,11 +23,19 @@ include_directories(${RAPIDJSON_INCLUDE}) add_executable(alexaext-unittest unittest_apl_audio_player.cpp + unittest_apl_e2e_encryption.cpp + unittest_apl_webflow.cpp + unittest_apl_music_alarm.cpp + unittest_activity_descriptor.cpp + unittest_extension_lifecycle.cpp unittest_extension_message.cpp unittest_extension_provider.cpp + unittest_extension_registrar.cpp unittest_extension_schema.cpp unittest_local_extensions.cpp + unittest_random.cpp unittest_resource_provider.cpp + unittest_session_descriptor.cpp ) target_link_libraries(alexaext-unittest diff --git a/extensions/unit/unittest_activity_descriptor.cpp b/extensions/unit/unittest_activity_descriptor.cpp new file mode 100644 index 0000000..abcad61 --- /dev/null +++ b/extensions/unit/unittest_activity_descriptor.cpp @@ -0,0 +1,173 @@ +/** + * 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 + +namespace { + +static const char* URI = "aplext:test1:10"; +static const char* OTHER_URI = "aplext:test2:10"; + +} + +TEST(ActivityDescriptorTest, GeneratesUniqueIds) { + auto session = alexaext::SessionDescriptor::create(); + auto activity1 = alexaext::ActivityDescriptor::create(URI, session); + auto activity2 = alexaext::ActivityDescriptor::create(URI, session); + + ASSERT_NE(activity1->getId(), activity2->getId()); + + ASSERT_EQ(URI, activity1->getURI()); + ASSERT_EQ(session, activity1->getSession()); + + ASSERT_EQ(URI, activity2->getURI()); + ASSERT_EQ(session, activity2->getSession()); + + ASSERT_TRUE(*activity1 != *activity2); + ASSERT_FALSE(*activity1 == *activity2); +} + +TEST(ActivityDescriptorTest, AcceptsExternalId) { + auto session = alexaext::SessionDescriptor::create(); + auto externalId = "unittest-id"; + auto activity = alexaext::ActivityDescriptor::create(URI, session, externalId); + + ASSERT_EQ(URI, activity->getURI()); + ASSERT_EQ(session, activity->getSession()); + ASSERT_EQ(externalId, activity->getId()); + + auto identical = alexaext::ActivityDescriptor::create(URI, session, externalId); + ASSERT_TRUE(*identical == *activity); + ASSERT_FALSE(*identical != *activity); +} + +TEST(ActivityDescriptorTest, IsCopyable) { + auto session = alexaext::SessionDescriptor::create(); + auto activity = alexaext::ActivityDescriptor::create(URI, session); + alexaext::ActivityDescriptor copy = *activity; + + ASSERT_TRUE(copy == *activity); +} + +TEST(ActivityDescriptorTest, IsMovable) { + auto session = alexaext::SessionDescriptor::create(); + auto externalId = "unittest-id"; + alexaext::ActivityDescriptor moved = alexaext::ActivityDescriptor(URI, session, externalId); + + ASSERT_EQ(externalId, moved.getId()); + ASSERT_EQ(URI, moved.getURI()); + ASSERT_EQ(session, moved.getSession()); +} + +TEST(ActivityDescriptorTest, ComparesSessionsById) { + auto session = alexaext::SessionDescriptor::create(); + auto sessionCopy = alexaext::SessionDescriptor::create(session->getId()); + + auto activity1 = alexaext::ActivityDescriptor::create(URI, session, "unittest-id"); + auto activity2 = alexaext::ActivityDescriptor::create(URI, sessionCopy, "unittest-id"); + + ASSERT_TRUE(*activity1 == *activity2); + ASSERT_FALSE(*activity1 != *activity2); +} + +TEST(ActivityDescriptorTest, IsHashable) { + auto session1 = alexaext::SessionDescriptor::create(); + auto session2 = alexaext::SessionDescriptor::create(); + auto externalId = "unittest-id"; + auto activity1 = alexaext::ActivityDescriptor::create(URI, session1, externalId); + auto activity2 = alexaext::ActivityDescriptor::create(URI, session1, externalId); + + alexaext::ActivityDescriptor::Hash hash; + ASSERT_EQ(hash(*activity1), hash(*activity2)); + + // Different session should produce a different hash + activity2 = alexaext::ActivityDescriptor::create(URI, session2, externalId); + ASSERT_NE(hash(*activity1), hash(*activity2)); + + // Different URI should produce a different hash + activity2 = alexaext::ActivityDescriptor::create(OTHER_URI, session1, externalId); + ASSERT_NE(hash(*activity1), hash(*activity2)); + + // Different ID should produce a different hash + activity2 = alexaext::ActivityDescriptor::create(URI, session1, "other-id"); + ASSERT_NE(hash(*activity1), hash(*activity2)); + + // Handles null session + activity2 = alexaext::ActivityDescriptor::create(URI, nullptr, externalId); + ASSERT_NE(hash(*activity1), hash(*activity2)); +} + +TEST(ActivityDescriptorTest, IsComparable) { + auto session1 = alexaext::SessionDescriptor::create("abc"); + auto session2 = alexaext::SessionDescriptor::create("def"); + auto externalId = "test-id-1"; + auto activity1 = alexaext::ActivityDescriptor::create(URI, session1, externalId); + auto activity2 = alexaext::ActivityDescriptor::create(URI, session1, externalId); + + alexaext::ActivityDescriptor::Compare compare; + // By contract, identical objects should compare false + ASSERT_FALSE(compare(*activity1, *activity2)); + + // Give activity2 a session that is greater than activity1's + activity2 = alexaext::ActivityDescriptor::create(URI, session2, externalId); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + // Give activity2 a URI that is greater than activity1's + activity2 = alexaext::ActivityDescriptor::create(OTHER_URI, session1, externalId); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + activity2 = alexaext::ActivityDescriptor::create(OTHER_URI, session1, "test-id-2"); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + activity2 = alexaext::ActivityDescriptor::create(OTHER_URI, session2, "test-id-2"); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + // Give activity2 an ID that is greater than activity1's + activity2 = alexaext::ActivityDescriptor::create(URI, session1, "test-id-2"); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + activity2 = alexaext::ActivityDescriptor::create(URI, session2, "test-id-2"); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + // Handles null session + activity2 = alexaext::ActivityDescriptor::create(URI, nullptr, externalId); + ASSERT_FALSE(compare(*activity1, *activity2)); + ASSERT_TRUE(compare(*activity2, *activity1)); + + // Both activities are identical with a null session + activity1 = alexaext::ActivityDescriptor::create(URI, nullptr, externalId); + ASSERT_FALSE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + activity1 = alexaext::ActivityDescriptor::create(URI, session2, "test-id-2"); + activity2 = alexaext::ActivityDescriptor::create(OTHER_URI, session1, "test-id-1"); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); + + + activity1 = alexaext::ActivityDescriptor::create(URI, session2, "test-id-1"); + activity2 = alexaext::ActivityDescriptor::create(URI, session1, "test-id-2"); + ASSERT_TRUE(compare(*activity1, *activity2)); + ASSERT_FALSE(compare(*activity2, *activity1)); +} diff --git a/extensions/unit/unittest_apl_e2e_encryption.cpp b/extensions/unit/unittest_apl_e2e_encryption.cpp new file mode 100644 index 0000000..b80ad50 --- /dev/null +++ b/extensions/unit/unittest_apl_e2e_encryption.cpp @@ -0,0 +1,304 @@ +/* + * 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 "alexaext/APLE2EEncryptionExtension/AplE2eEncryptionExtension.h" +#include "alexaext/extensionmessage.h" + +using namespace alexaext; +using namespace E2EEncryption; +using namespace rapidjson; + +class TestE2EEncryptionObserver : public AplE2eEncryptionExtensionObserverInterface { +public: + void onBase64EncryptValue(const std::string& token, + const std::string& key, + const std::string& algorithm, + const std::string& aad, + const std::string& value, + bool base64Encoded, + EncryptionCallbackSuccess successCallback, + EncryptionCallbackError errorCallback) override { + mCommand = "Base64EncryptValue"; + if (value == "forcesuccess") { + mEvent = "OnEncryptSuccess"; + successCallback(token, "onEncryptSuccessData", "onEncryptSuccessIVData", "onEncryptSuccessKey"); + } else { + mEvent = "OnEncryptFailure"; + errorCallback(token, "error"); + } + } + + void onBase64EncodeValue(const std::string& token, const std::string& value, + EncodeCallbackSuccess successCallback) override { + mCommand = "Base64EncodeValue"; + if (value == "forcesuccess") { + mEvent = "OnBase64EncodeSuccess"; + successCallback(token, "XXXYY"); + } + } + + std::string mCommand; + std::string mEvent; +}; + +// Inject the UUID generator so we can reproduce tests +static int uuidValue = 0; +std::string testUuid() { + return "AplE2EEncryptionUuid-" + std::to_string(uuidValue); +} + +class AplE2EEncryptionExtensionTest : public ::testing::Test { +public: + void SetUp() override { + mObserver = std::make_shared(); + mExtension = std::make_shared(mObserver, Executor::getSynchronousExecutor(), testUuid); + // Register the event handler + mExtension->registerEventCallback([this](const std::string& uri, const rapidjson::Value& event){ + if (uri != "aplext:e2eencryption:10") + return; + std::string eventName = GetWithDefault("name", event, ""); + if (eventName == "OnEncryptSuccess") { + GetWithDefault("payload/base64EncryptedData", event, ""); + mEncryptedData = GetWithDefault("payload/base64EncryptedData", event, ""); + mEncodedIVData = GetWithDefault("payload/base64EncodedIV", event, ""); + mEncodedKey = GetWithDefault("payload/base64EncodedKey", event, ""); + } + if (eventName == "OnEncryptFailure") { + mErrorReason = GetWithDefault("payload/errorReason", event, ""); + } + if (eventName == "OnBase64EncodeSuccess") { + mEncodedData = GetWithDefault("payload/base64EncodedData", event, ""); + } + }); + } + + /** + * Simple registration for testing event/command/data. + */ + ::testing::AssertionResult registerExtension() + { + Document settings(kObjectType); + Document regReq = RegistrationRequest("1.0").uri("aplext:e2eencryption:10") + .settings(settings); + auto registration = mExtension->createRegistration("aplext:e2eencryption:10", 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(); + } + + std::shared_ptr mObserver; + std::shared_ptr mExtension; + std::string mClientToken; + std::string mEncodedData; + std::string mEncryptedData; + std::string mEncodedIVData; + std::string mEncodedKey; + std::string mErrorReason; +}; + +/** + * Simple create test for sanity. + */ +TEST_F(AplE2EEncryptionExtensionTest, CreateExtension) +{ + ASSERT_TRUE(mObserver); + ASSERT_TRUE(mExtension); + auto supported = mExtension->getURIs(); + ASSERT_EQ(1, supported.size()); + ASSERT_NE(supported.end(), supported.find("aplext:e2eencryption:10")); +} + +/** + * Registration request with bad URI. + */ +TEST_F(AplE2EEncryptionExtensionTest, RegistrationURIBad) +{ + Document regReq = RegistrationRequest("aplext:e2eencryption:BAD"); + auto registration = mExtension->createRegistration("aplext:e2eencryption:BAD", regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +/** + * Registration Success has required fields + */ +TEST_F(AplE2EEncryptionExtensionTest, RegistrationSuccess) +{ + uuidValue = 1; + Document regReq = RegistrationRequest("1.0").uri("aplext:e2eencryption:10"); + auto registration = mExtension->createRegistration("aplext:e2eencryption:10", regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + ASSERT_STREQ("aplext:e2eencryption:10", + GetWithDefault(RegistrationSuccess::URI(), registration, "")); + auto schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + ASSERT_STREQ("aplext:e2eencryption:10", GetWithDefault("uri", *schema, "")); + std::string token = GetWithDefault(RegistrationSuccess::TOKEN(), registration, ""); + ASSERT_EQ(token, "AplE2EEncryptionUuid-1"); +} + +/** + * Commands are defined at registration. + */ +TEST_F(AplE2EEncryptionExtensionTest, RegistrationCommands) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:e2eencryption:10"); + + auto registration = mExtension->createRegistration("aplext:e2eencryption:10", regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value *commands = ExtensionSchema::COMMANDS().Get(*schema); + ASSERT_TRUE(commands); + + auto expectedCommandSet = std::set(); + expectedCommandSet.insert("Base64EncryptValue"); + expectedCommandSet.insert("Base64EncodeValue"); + 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()); +} + +/** + * Events are defined + */ +TEST_F(AplE2EEncryptionExtensionTest, RegistrationEvents) +{ + Document regReq = RegistrationRequest("1.0").uri("aplext:e2eencryption:10"); + auto registration = mExtension->createRegistration("aplext:e2eencryption:10", 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); + + // FullSet event handler for audio player + auto expectedHandlerSet = std::set(); + expectedHandlerSet.insert("OnEncryptSuccess"); + expectedHandlerSet.insert("OnEncryptFailure"); + expectedHandlerSet.insert("OnBase64EncodeSuccess"); + 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, "MissingName"); + ASSERT_TRUE(expectedHandlerSet.count(name) == 1); + expectedHandlerSet.erase(name); + } + ASSERT_TRUE(expectedHandlerSet.empty()); +} + + +/** + * Command Base64EncodeValue calls observer. + */ +TEST_F(AplE2EEncryptionExtensionTest, InvokeBase64EncodeValue) +{ + auto testText = "forcesuccess"; + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0").target(mClientToken) + .uri("aplext:e2eencryption:10") + .name("Base64EncodeValue") + .property("token", mClientToken) + .property("value", testText); + auto invoke = mExtension->invokeCommand("aplext:e2eencryption:10", command); + ASSERT_TRUE(invoke); + + ASSERT_EQ("Base64EncodeValue", mObserver->mCommand); + ASSERT_EQ("OnBase64EncodeSuccess", mObserver->mEvent); + ASSERT_EQ("XXXYY", mEncodedData); +} + +/** + * Command Base64EncodeValue calls observer. + */ +TEST_F(AplE2EEncryptionExtensionTest, InvokeEncrypSuccess) +{ + auto testText = "forcesuccess"; + auto testKey = "key"; + auto testAlgorithm = ""; + auto testAad = "testAad"; + auto testBase64Encoded = true; + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0").target(mClientToken) + .uri("aplext:e2eencryption:10") + .name("Base64EncryptValue") + .property("token", mClientToken) + .property("value", testText) + .property("key", testKey) + .property("algorithm", testAlgorithm) + .property("aad", testAad) + .property("base64Encoded", testBase64Encoded); + auto invoke = mExtension->invokeCommand("aplext:e2eencryption:10", command); + ASSERT_TRUE(invoke); + + ASSERT_EQ("Base64EncryptValue", mObserver->mCommand); + ASSERT_EQ("OnEncryptSuccess", mObserver->mEvent); + ASSERT_EQ("onEncryptSuccessData", mEncryptedData); + ASSERT_EQ("onEncryptSuccessIVData", mEncodedIVData); + ASSERT_EQ("onEncryptSuccessKey", mEncodedKey); +} + +/** + * Command Base64EncodeValue calls observer. + */ +TEST_F(AplE2EEncryptionExtensionTest, InvokeEncrypError) +{ + auto testText = "forceerror"; + auto testKey = "key"; + auto testAlgorithm = ""; + auto testAad = "testAad"; + auto testBase64Encoded = true; + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0").target(mClientToken) + .uri("aplext:e2eencryption:10") + .name("Base64EncryptValue") + .property("token", mClientToken) + .property("value", testText) + .property("key", testKey) + .property("algorithm", testAlgorithm) + .property("aad", testAad) + .property("base64Encoded", testBase64Encoded); + auto invoke = mExtension->invokeCommand("aplext:e2eencryption:10", command); + ASSERT_TRUE(invoke); + + ASSERT_EQ("Base64EncryptValue", mObserver->mCommand); + ASSERT_EQ("OnEncryptFailure", mObserver->mEvent); + ASSERT_EQ("error", mErrorReason); +} \ No newline at end of file diff --git a/extensions/unit/unittest_apl_music_alarm.cpp b/extensions/unit/unittest_apl_music_alarm.cpp new file mode 100644 index 0000000..ebc3eb6 --- /dev/null +++ b/extensions/unit/unittest_apl_music_alarm.cpp @@ -0,0 +1,194 @@ +/* + * 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 "alexaext/APLMusicAlarmExtension/AplMusicAlarmExtension.h" +#include "alexaext/extensionmessage.h" + +using namespace alexaext; +using namespace MusicAlarm; +using namespace rapidjson; + + +static const std::string DISMISSCOMMAND = "DISMISS"; +static const std::string SNOOZECOMMAND = "SNOOZE"; + + +class TestMusicAlarmObserver : public AplMusicAlarmExtensionObserverInterface { +public: + + TestMusicAlarmObserver() = default; + + /** + * The DismissAlarm command is used to dismiss the current ringing alarm. + */ + void dismissAlarm() { + mCommand = DISMISSCOMMAND; + } + + /** + * The SnoozeAlarm command is used to snooze the current ringing alarm. + */ + void snoozeAlarm() { + mCommand = SNOOZECOMMAND; + } + std::string mCommand; +}; + +// Inject the UUID generator so we can reproduce tests +static int uuidValue = 0; +std::string testMusicUuid() { + return "AplMusicAlarmUuid-" + std::to_string(uuidValue); +} + + +class AplMusicAlarmExtensionTest : public ::testing::Test { +public: + void SetUp() override { + mObserver = std::make_shared(); + mExtension = std::make_shared( + mObserver, Executor::getSynchronousExecutor(), testMusicUuid); + } + + /** + * Simple registration for testing event/command/data. + */ + ::testing::AssertionResult registerExtension() + { + Document settings(kObjectType); + Document regReq = RegistrationRequest("1.0").uri(MusicAlarm::URI) + .settings(settings); + auto registration = mExtension->createRegistration(MusicAlarm::URI, 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(); + } + + std::shared_ptr mObserver; + std::shared_ptr mExtension; + std::string mClientToken; +}; + +/** + * Simple create test for sanity. + */ +TEST_F(AplMusicAlarmExtensionTest, CreateExtension) +{ + ASSERT_TRUE(mObserver); + ASSERT_TRUE(mExtension); + auto supported = mExtension->getURIs(); + ASSERT_EQ(1, supported.size()); + ASSERT_NE(supported.end(), supported.find(MusicAlarm::URI)); +} + +/** + * Registration request with bad URI. + */ +TEST_F(AplMusicAlarmExtensionTest, RegistrationURIBad) +{ + Document regReq = RegistrationRequest("aplext:music:BAD"); + auto registration = mExtension->createRegistration("aplext:music:BAD", regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +/** + * Registration Success has required fields + */ +TEST_F(AplMusicAlarmExtensionTest, RegistrationSuccess) +{ + uuidValue = 1; + Document regReq = RegistrationRequest("1.0").uri(MusicAlarm::URI); + auto registration = mExtension->createRegistration(MusicAlarm::URI, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + ASSERT_STREQ(MusicAlarm::URI.c_str(), + GetWithDefault(RegistrationSuccess::URI(), registration, "")); + auto schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + ASSERT_STREQ(MusicAlarm::URI.c_str(), GetWithDefault("uri", *schema, "")); + std::string token = GetWithDefault(RegistrationSuccess::TOKEN(), registration, ""); + ASSERT_EQ(token, "AplMusicAlarmUuid-1"); +} + +/** + * Commands are defined at registration. + */ +TEST_F(AplMusicAlarmExtensionTest, RegistrationCommands) +{ + Document regReq = RegistrationRequest("1.0").uri(MusicAlarm::URI); + + auto registration = mExtension->createRegistration(MusicAlarm::URI, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value *commands = ExtensionSchema::COMMANDS().Get(*schema); + ASSERT_TRUE(commands); + + auto expectedCommandSet = std::set(); + expectedCommandSet.insert("DismissAlarm"); + expectedCommandSet.insert("SnoozeAlarm"); + 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()); +} + + +/** + * Command Base64EncodeValue calls observer. + */ +TEST_F(AplMusicAlarmExtensionTest, InvokeDismiss) +{ + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0").target(mClientToken) + .uri(MusicAlarm::URI) + .name("DismissAlarm"); + auto invoke = mExtension->invokeCommand(MusicAlarm::URI, command); + ASSERT_TRUE(invoke); + + ASSERT_EQ(DISMISSCOMMAND, mObserver->mCommand); +} + +/** + * Command Base64EncodeValue calls observer. + */ +TEST_F(AplMusicAlarmExtensionTest, InvokeSnooze) +{ + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0").target(mClientToken) + .uri(MusicAlarm::URI) + .name("SnoozeAlarm"); + auto invoke = mExtension->invokeCommand(MusicAlarm::URI, command); + ASSERT_TRUE(invoke); + + ASSERT_EQ(SNOOZECOMMAND, mObserver->mCommand); +} \ No newline at end of file diff --git a/extensions/unit/unittest_apl_webflow.cpp b/extensions/unit/unittest_apl_webflow.cpp new file mode 100644 index 0000000..d6857b6 --- /dev/null +++ b/extensions/unit/unittest_apl_webflow.cpp @@ -0,0 +1,261 @@ +/* + * 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 +#include +#include +#include + +#include "gtest/gtest.h" + +#include "alexaext/APLWebflowExtension/AplWebflowExtension.h" +#include "alexaext/extensionmessage.h" + +using namespace alexaext; +using namespace Webflow; +using namespace rapidjson; + +static int uuidValue = 1; + +std::string +testGenUuid() { + return std::string("TestWebflowUUID-") + std::to_string(uuidValue); +} + +class SimpleTestWebflowObserver : public AplWebflowExtensionObserverInterface { +public: + SimpleTestWebflowObserver() : AplWebflowExtensionObserverInterface() {} + + ~SimpleTestWebflowObserver() override = default; + + void onStartFlow(const std::string& token, const std::string& url, const std::string& flowId, + std::function onFlowEndEvent) override { + mCommand = "START_FLOW"; + mUrl = url; + mFlowId = flowId; + mToken = testGenUuid(); + onFlowEndEvent(mToken, flowId); + } + + void resetTestData() { + mCommand.clear(); + mUrl.clear(); + mFlowId.clear(); + mToken.clear(); + } + + std::string mCommand; + std::string mUrl; + std::string mFlowId; + std::string mToken; +}; + +class SimpleTestWebflowExtension : public AplWebflowExtension { +public: + explicit SimpleTestWebflowExtension( + std::function uuidGenerator, + std::shared_ptr observer) + : AplWebflowExtension( + std::move(uuidGenerator), + std::move(observer), + alexaext::Executor::getSynchronousExecutor()) {} +}; + +class SimpleAplWebflowExtensionTest : public ::testing::Test { +public: + void SetUp() override { + mObserver = std::make_shared(); + mExtension = std::make_shared(testGenUuid, mObserver); + mExtension->registerEventCallback( + [&](const std::string& uri, const rapidjson::Value& event) { + if (uri == "aplext:webflow:10") { + auto eventFlow = GetWithDefault("payload/flowId", event, ""); + if (!eventFlow.empty()) { + mEventFlow = eventFlow; + } + auto token = GetWithDefault("payload/token", event, ""); + if (!token.empty()) { + mToken = token; + } + } + }); + } + + /** + * Simple registration for testing event/command/data. + */ + ::testing::AssertionResult registerExtension() { + Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); + auto registration = mExtension->createRegistration("aplext:webflow:10", 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(); + } + + void resetTestData() { + mClientToken.clear(); + mEventFlow.clear(); + mToken.clear(); + } + + std::shared_ptr mObserver; + AplWebflowExtensionPtr mExtension; + std::string mClientToken; + std::string mEventFlow; + std::string mToken; +}; + +/** + * Simple create test for sanity. + */ +TEST_F(SimpleAplWebflowExtensionTest, CreateExtension) { + ASSERT_TRUE(mObserver); + ASSERT_TRUE(mExtension); + auto supported = mExtension->getURIs(); + ASSERT_EQ(1, supported.size()); + ASSERT_NE(supported.end(), supported.find("aplext:webflow:10")); +} + +/** + * Registration request with bad URI. + */ +TEST_F(SimpleAplWebflowExtensionTest, RegistrationURIBad) { + Document regReq = RegistrationRequest("aplext:webflow:BAD"); + auto registration = mExtension->createRegistration("aplext:webflow:BAD", regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); +} + +/** + * Registration Success has required fields + */ +TEST_F(SimpleAplWebflowExtensionTest, RegistrationSuccess) { + Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); + auto registration = mExtension->createRegistration("aplext:webflow:10", regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + ASSERT_STREQ("aplext:webflow:10", + GetWithDefault(RegistrationSuccess::URI(), registration, "")); + auto schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + ASSERT_STREQ("aplext:webflow:10", GetWithDefault("uri", *schema, "")); + std::string token = GetWithDefault(RegistrationSuccess::TOKEN(), registration, ""); + ASSERT_TRUE(token.rfind("TestWebflowUUID") == 0); +} + +/** + * Commands are defined at registration. + */ +TEST_F(SimpleAplWebflowExtensionTest, RegistrationCommands) { + Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); + + auto registration = mExtension->createRegistration("aplext:webflow:10", regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value* schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value* commands = ExtensionSchema::COMMANDS().Get(*schema); + ASSERT_TRUE(commands); + + auto expectedCommandSet = std::set(); + expectedCommandSet.insert("StartFlow"); + 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()); +} + +/** + * Events are defined + */ +TEST_F(SimpleAplWebflowExtensionTest, RegistrationEvents) { + Document regReq = RegistrationRequest("1.0").uri("aplext:webflow:10"); + auto registration = mExtension->createRegistration("aplext:webflow:10", 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); + + // FullSet event handler for audio player + auto expectedHandlerSet = std::set(); + expectedHandlerSet.insert("OnFlowEnd"); + 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, "MissingName"); + ASSERT_TRUE(expectedHandlerSet.count(name) == 1); + expectedHandlerSet.erase(name); + } + ASSERT_TRUE(expectedHandlerSet.empty()); +} + +/** + * Command StartFlow calls observer. + */ +TEST_F(SimpleAplWebflowExtensionTest, InvokeCommandStartFlowSuccess) { + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0") + .target(mClientToken) + .uri("aplext:webflow:10") + .name("StartFlow") + .property("url", "test_url"); + auto invoke = mExtension->invokeCommand("aplext:webflow:10", command); + ASSERT_TRUE(invoke); + ASSERT_EQ("START_FLOW", mObserver->mCommand); + ASSERT_EQ("test_url", mObserver->mUrl); + ASSERT_TRUE(mObserver->mFlowId.empty()); +} + +/** + * Command StartFlow forward ClientId observer. + */ +TEST_F(SimpleAplWebflowExtensionTest, InvokeCommandStartFlowWithFlowIdSuccess) { + ASSERT_TRUE(registerExtension()); + + auto command = Command("1.0") + .target(mClientToken) + .uri("aplext:webflow:10") + .name("StartFlow") + .property("url", "test_url") + .property("flowId", "test_flow"); + auto invoke = mExtension->invokeCommand("aplext:webflow:10", 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 diff --git a/extensions/unit/unittest_extension_lifecycle.cpp b/extensions/unit/unittest_extension_lifecycle.cpp new file mode 100644 index 0000000..94975cd --- /dev/null +++ b/extensions/unit/unittest_extension_lifecycle.cpp @@ -0,0 +1,741 @@ +/** + * 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 "gtest/gtest.h" + +using namespace alexaext; + +namespace { + +static const char *URI = "test:lifecycle:1.0"; + +static const char *COMMAND_MESSAGE = R"( + { + "version": "1.0", + "method": "Command", + "payload": {}, + "uri": "test:lifecycle:1.0", + "target": "test:lifecycle:1.0", + "id": 42, + "name": "TestCommand" + } + )"; + +static const char *EVENT_MESSAGE = R"( + { + "version": "1.0", + "method": "Event", + "payload": {}, + "uri": "test:lifecycle:1.0", + "target": "test:lifecycle:1.0", + "name": "TestEvent" + } + )"; + +static const char * LIVE_DATA_MESSAGE = R"( + { + "version": "1.0", + "method": "LiveDataUpdate", + "operations": [ + { + "type": "Insert", + "index": 1, + "item": 1 + } + ], + "uri": "test:lifecycle:1.0", + "target": "test:lifecycle:1.0", + "name": "MyLiveArray" + } +)"; + +static const char *UPDATE_COMPONENT_MESSAGE = R"( + { + "version": "1.0", + "method": "Component", + "uri": "test:lifecycle:1.0", + "target": "test:lifecycle:1.0", + "resourceId": "SURFACE42", + "state": "Ready" + } +)"; + +class LegacyExtension : public alexaext::ExtensionBase { +public: + explicit LegacyExtension() : ExtensionBase(URI) {} + + rapidjson::Document createRegistration(const std::string& uri, + const rapidjson::Value& registerRequest) override { + return RegistrationSuccess("1.0") + .uri(uri) + .token("") + .schema("1.0", [uri](ExtensionSchema schema) { schema.uri(uri); }); + } + + void publishLiveData() { + rapidjson::Document update; + update.Parse(LIVE_DATA_MESSAGE); + invokeLiveDataUpdate(URI, update); + } + + bool invokeCommand(const std::string& uri, const rapidjson::Value& command) override { + processedCommand = true; + rapidjson::Document event; + event.Parse(EVENT_MESSAGE); + invokeExtensionEventHandler(uri, event); + return true; + } + + void onResourceReady(const std::string& uri, const ResourceHolderPtr& resourceHolder) override { + resourceReady = true; + } + + bool updateComponent(const std::string& uri, const rapidjson::Value& command) override { + processedComponentUpdate = true; + return true; + } + + void onRegistered(const std::string& uri, const std::string& token) override { + registered = true; + } + + void onUnregistered(const std::string& uri, const std::string& token) override { + registered = false; + } + + bool registered = false; + bool resourceReady = false; + bool processedCommand = false; + bool processedComponentUpdate = false; +}; + +class LifecycleExtension : public alexaext::ExtensionBase { +public: + explicit LifecycleExtension() : ExtensionBase(URI), + lastActivity(URI, nullptr) + {} + + void publishLiveData() { + publishLiveData(lastActivity); + } + + void publishLiveData(const ActivityDescriptor& activity) { + rapidjson::Document update; + update.Parse(LIVE_DATA_MESSAGE); + invokeLiveDataUpdate(activity, update); + } + + void publishEvent(const ActivityDescriptor& activity) { + rapidjson::Document event; + event.Parse(EVENT_MESSAGE); + invokeExtensionEventHandler(activity, event); + } + + rapidjson::Document createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) override { + lastActivity = activity; + const auto& uri = activity.getURI(); + + return RegistrationSuccess("1.0") + .uri(uri) + .token("") + .schema("1.0", [uri](ExtensionSchema schema) { schema.uri(uri); }); + } + + void onSessionStarted(const SessionDescriptor& session) override { + sessionActive = true; + } + + void onSessionEnded(const SessionDescriptor& session) override { + sessionActive = false; + } + + void onActivityRegistered(const ActivityDescriptor& activity) override { + registered = true; + } + + void onActivityUnregistered(const ActivityDescriptor& activity) override { + registered = false; + } + + void onForeground(const ActivityDescriptor& activity) override { + displayState = "foreground"; + } + + void onBackground(const ActivityDescriptor& activity) override { + displayState = "background"; + } + + void onHidden(const ActivityDescriptor& activity) override { + displayState = "hidden"; + } + + bool invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value& command) override { + processedCommand = true; + rapidjson::Document event; + event.Parse(EVENT_MESSAGE); + invokeExtensionEventHandler(activity, event); + return true; + } + + bool updateComponent(const ActivityDescriptor& activity, const rapidjson::Value& command) override { + processedComponentUpdate = true; + return true; + } + + void onResourceReady(const ActivityDescriptor& activity, + const ResourceHolderPtr& resourceHolder) override { + resourceReady = true; + } + + bool registered = false; + bool resourceReady = false; + bool sessionActive = false; + bool processedCommand = false; + bool processedComponentUpdate = false; + std::string displayState = "none"; + alexaext::ActivityDescriptor lastActivity; +}; + +class LegacyProxy : public alexaext::ExtensionProxy { +public: + LegacyProxy(const ExtensionPtr& extension) : mExtension(extension) {} + + std::set getURIs() const override { return mExtension ? mExtension->getURIs() : std::set(); } + + bool initializeExtension(const std::string& uri) override { return isInitialized(uri); } + bool isInitialized(const std::string& uri) const override { return mExtension && getURIs().count(uri) > 0; } + + bool getRegistration(const std::string& uri, const rapidjson::Value& registrationRequest, + RegistrationSuccessCallback success, + RegistrationFailureCallback error) override { + if (!isInitialized(uri)) return false; + + auto response = mExtension->createRegistration(uri, registrationRequest); + auto method = RegistrationSuccess::METHOD().Get(response); + if (method && *method != "RegisterSuccess") { + error(uri, response); + } else { + success(uri, response); + } + return true; + } + + bool invokeCommand(const std::string& uri, const rapidjson::Value& command, + CommandSuccessCallback success, CommandFailureCallback error) override { + if (!isInitialized(uri)) return false; + + auto commandId = (int) Command::ID().Get(command)->GetDouble(); + if (mExtension->invokeCommand(uri, command)) { + auto response = CommandSuccess("1.0") + .uri(uri) + .id(commandId); + success(uri, response); + } else { + auto response = CommandFailure("1.0") + .uri(uri) + .id(commandId) + .errorCode(kErrorFailedCommand) + .errorMessage("Extension failed"); + error(uri, response); + } + return true; + } + + bool sendMessage(const std::string& uri, const rapidjson::Value& message) override { + if (!isInitialized(uri)) return false; + + return mExtension->updateComponent(uri, message); + } + + void onResourceReady(const std::string& uri, const ResourceHolderPtr& resourceHolder) override { + mExtension->onResourceReady(uri, resourceHolder); + } + + void registerEventCallback(Extension::EventCallback callback) override { + mExtension->registerEventCallback(std::move(callback)); + } + void registerLiveDataUpdateCallback(Extension::LiveDataUpdateCallback callback) override { + mExtension->registerLiveDataUpdateCallback(std::move(callback)); + } + +private: + ExtensionPtr mExtension; +}; + +class ExtensionLifecycleTest : public ::testing::Test { +public: + void SetUp() override { + legacyExtension = std::make_shared(); + legacyProxy = std::make_shared(legacyExtension); + + extension = std::make_shared(); + proxy = std::make_shared(extension); + } + + std::shared_ptr legacyExtension; + ExtensionProxyPtr legacyProxy; + + std::shared_ptr extension; + ExtensionProxyPtr proxy; +}; + +} + +TEST_F(ExtensionLifecycleTest, LegacyExtension) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + bool receivedEvent = false; + + ASSERT_TRUE(legacyProxy->initializeExtension(URI)); + + legacyProxy->registerEventCallback([&](const std::string& uri, const rapidjson::Value& event) { + receivedEvent = true; + }); + + // No side effect expected for the legacy case + legacyProxy->onSessionStarted(*session); + + auto req = RegistrationRequest("1.0") + .uri(URI); + + bool successCallbackWasCalled = false; + bool registered = legacyProxy->getRegistration(*activity, req, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + successCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + legacyProxy->onRegistered(*activity); + ASSERT_TRUE(registered); + ASSERT_TRUE(successCallbackWasCalled); + ASSERT_TRUE(legacyExtension->registered); + + // No side effect expected for the legacy case + legacyProxy->onForeground(*activity); + + rapidjson::Document command; + command.Parse(COMMAND_MESSAGE); + + ASSERT_FALSE(receivedEvent); // The extension will publish an event in response to the command + bool commandSuccessCallbackWasCalled; + bool commandAccepted = legacyProxy->invokeCommand(*activity, command, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + commandSuccessCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + ASSERT_TRUE(commandAccepted); + ASSERT_TRUE(commandSuccessCallbackWasCalled); + ASSERT_TRUE(legacyExtension->processedCommand); + ASSERT_TRUE(receivedEvent); + + bool liveDataUpdateReceived = false; + legacyProxy->registerLiveDataUpdateCallback([&](const std::string& uri, const rapidjson::Value& liveDataUpdate) { + liveDataUpdateReceived = true; + }); + legacyExtension->publishLiveData(); + ASSERT_TRUE(liveDataUpdateReceived); + + // No side effect expected for the legacy case + legacyProxy->onBackground(*activity); + legacyProxy->onHidden(*activity); + + legacyProxy->onUnregistered(*activity); + ASSERT_FALSE(legacyExtension->registered); + + // No side effect expected for the legacy case + legacyProxy->onSessionEnded(*session); +} + +TEST_F(ExtensionLifecycleTest, Lifecycle) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + bool receivedEvent = false; + + ASSERT_TRUE(proxy->initializeExtension(URI)); + + proxy->registerEventCallback(*activity, [&](const ActivityDescriptor& activity, const rapidjson::Value& event) { + receivedEvent = true; + }); + + ASSERT_FALSE(extension->sessionActive); + + proxy->onSessionStarted(*session); + + ASSERT_TRUE(extension->sessionActive); + + auto req = RegistrationRequest("1.0") + .uri(URI); + + bool successCallbackWasCalled = false; + bool registered = proxy->getRegistration(*activity, req, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + successCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + proxy->onRegistered(*activity); + ASSERT_TRUE(registered); + ASSERT_TRUE(successCallbackWasCalled); + ASSERT_TRUE(extension->registered); + + proxy->onForeground(*activity); + ASSERT_EQ("foreground", extension->displayState); + + rapidjson::Document command; + command.Parse(COMMAND_MESSAGE); + + ASSERT_FALSE(receivedEvent); // The extension will publish an event in response to the command + bool commandSuccessCallbackWasCalled; + bool commandAccepted = proxy->invokeCommand(*activity, command, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + commandSuccessCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + ASSERT_TRUE(commandAccepted); + ASSERT_TRUE(commandSuccessCallbackWasCalled); + ASSERT_TRUE(extension->processedCommand); + ASSERT_TRUE(receivedEvent); + + bool liveDataUpdateReceived = false; + proxy->registerLiveDataUpdateCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& liveDataUpdate) { + ASSERT_EQ(*activity, callbackActivity); + liveDataUpdateReceived = true; + }); + extension->publishLiveData(); + ASSERT_TRUE(liveDataUpdateReceived); + + proxy->onBackground(*activity); + ASSERT_EQ("background", extension->displayState); + + proxy->onHidden(*activity); + ASSERT_EQ("hidden", extension->displayState); + + proxy->onUnregistered(*activity); + ASSERT_FALSE(extension->registered); + + proxy->onSessionEnded(*session); + ASSERT_FALSE(extension->sessionActive); +} + +TEST_F(ExtensionLifecycleTest, MultipleCallbacksForSameActivity) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + auto otherActivity = ActivityDescriptor::create(URI, session); + + ASSERT_TRUE(proxy->initializeExtension(URI)); + + bool receivedFirstEvent = false; + bool receivedSecondEvent = false; + proxy->registerEventCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& event) { + ASSERT_EQ(*activity, callbackActivity); + receivedFirstEvent = true; + }); + proxy->registerEventCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& event) { + ASSERT_EQ(*activity, callbackActivity); + receivedSecondEvent = true; + }); + + bool receivedFirstLiveDataUpdate = false; + bool receivedSecondLiveDataUpdate = false; + proxy->registerLiveDataUpdateCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& liveDataUpdate) { + ASSERT_EQ(*activity, callbackActivity); + receivedFirstLiveDataUpdate = true; + }); + proxy->registerLiveDataUpdateCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& liveDataUpdate) { + ASSERT_EQ(*activity, callbackActivity); + receivedSecondLiveDataUpdate = true; + }); + + ASSERT_FALSE(extension->sessionActive); + + proxy->onSessionStarted(*session); + + ASSERT_TRUE(extension->sessionActive); + + auto req = RegistrationRequest("1.0") + .uri(URI); + + bool successCallbackWasCalled = false; + bool registered = proxy->getRegistration(*activity, req, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + successCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + proxy->onRegistered(*activity); + ASSERT_TRUE(registered); + ASSERT_TRUE(successCallbackWasCalled); + ASSERT_TRUE(extension->registered); + + extension->publishEvent(*activity); + ASSERT_TRUE(receivedFirstEvent); + ASSERT_TRUE(receivedSecondEvent); + + extension->publishLiveData(*activity); + ASSERT_TRUE(receivedFirstLiveDataUpdate); + ASSERT_TRUE(receivedSecondLiveDataUpdate); + + proxy->onSessionEnded(*session); + ASSERT_FALSE(extension->sessionActive); +} + +TEST_F(ExtensionLifecycleTest, UnregisterCleansUpCallbacks) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + auto otherActivity = ActivityDescriptor::create(URI, session); + + ASSERT_TRUE(proxy->initializeExtension(URI)); + + bool receivedEvent = false; + proxy->registerEventCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& event) { + ASSERT_EQ(*activity, callbackActivity); + receivedEvent = true; + }); + + bool liveDataUpdateReceived = false; + proxy->registerLiveDataUpdateCallback(*activity, [&](const ActivityDescriptor& callbackActivity, const rapidjson::Value& liveDataUpdate) { + ASSERT_EQ(*activity, callbackActivity); + liveDataUpdateReceived = true; + }); + + ASSERT_FALSE(extension->sessionActive); + + proxy->onSessionStarted(*session); + + ASSERT_TRUE(extension->sessionActive); + + auto req = RegistrationRequest("1.0") + .uri(URI); + + bool successCallbackWasCalled = false; + bool registered = proxy->getRegistration(*activity, req, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + successCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + proxy->onRegistered(*activity); + ASSERT_TRUE(registered); + ASSERT_TRUE(successCallbackWasCalled); + ASSERT_TRUE(extension->registered); + + + extension->publishEvent(*activity); + ASSERT_TRUE(receivedEvent); + + extension->publishLiveData(*activity); + ASSERT_TRUE(liveDataUpdateReceived); + + // Reset the state + receivedEvent = false; + liveDataUpdateReceived = false; + + extension->publishEvent(*otherActivity); + ASSERT_FALSE(receivedEvent); + + extension->publishLiveData(*otherActivity); + ASSERT_FALSE(liveDataUpdateReceived); + + // Unregister activity, this will clear event and live data callbacks + proxy->onUnregistered(*activity); + + extension->publishEvent(*activity); + ASSERT_FALSE(receivedEvent); + extension->publishLiveData(*activity); + ASSERT_FALSE(liveDataUpdateReceived); + + proxy->onSessionEnded(*session); + ASSERT_FALSE(extension->sessionActive); +} + +TEST_F(ExtensionLifecycleTest, BaseProxyEnsuresBackwardsCompatibility) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + bool receivedEvent = false; + + legacyProxy = std::make_shared(legacyExtension); + + ASSERT_TRUE(legacyProxy->initializeExtension(URI)); + + legacyProxy->registerEventCallback([&](const std::string& uri, const rapidjson::Value& event) { + receivedEvent = true; + }); + + // No side effect expected for the legacy case + legacyProxy->onSessionStarted(*session); + + auto req = RegistrationRequest("1.0") + .uri(URI); + + bool successCallbackWasCalled = false; + bool registered = legacyProxy->getRegistration(*activity, req, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + successCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + ASSERT_TRUE(registered); + ASSERT_TRUE(successCallbackWasCalled); + + // No side effect expected for the legacy case + legacyProxy->onForeground(*activity); + + rapidjson::Document command; + command.Parse(COMMAND_MESSAGE); + + ASSERT_FALSE(receivedEvent); // The extension will publish an event in response to the command + bool commandSuccessCallbackWasCalled; + bool commandAccepted = legacyProxy->invokeCommand(*activity, command, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + commandSuccessCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + ASSERT_TRUE(commandAccepted); + ASSERT_TRUE(commandSuccessCallbackWasCalled); + ASSERT_TRUE(legacyExtension->processedCommand); + ASSERT_TRUE(receivedEvent); + + bool liveDataUpdateReceived = false; + legacyProxy->registerLiveDataUpdateCallback([&](const std::string& uri, const rapidjson::Value& liveDataUpdate) { + liveDataUpdateReceived = true; + }); + legacyExtension->publishLiveData(); + ASSERT_TRUE(liveDataUpdateReceived); + + // No side effect expected for the legacy case + legacyProxy->onBackground(*activity); + legacyProxy->onHidden(*activity); + + legacyProxy->onUnregistered(*activity); + ASSERT_FALSE(legacyExtension->registered); + + // No side effect expected for the legacy case + legacyProxy->onSessionEnded(*session); +} + +TEST_F(ExtensionLifecycleTest, ResourceReady) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + + ASSERT_TRUE(proxy->initializeExtension(URI)); + + auto resource = std::make_shared("SURFACE42"); + proxy->onResourceReady(*activity, resource); + + ASSERT_TRUE(extension->resourceReady); +} + +TEST_F(ExtensionLifecycleTest, ResourceReadyLegacy) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + + ASSERT_TRUE(legacyProxy->initializeExtension(URI)); + + auto resource = std::make_shared("SURFACE42"); + legacyProxy->onResourceReady(*activity, resource); + + ASSERT_TRUE(legacyExtension->resourceReady); +} + +TEST_F(ExtensionLifecycleTest, UpdateComponent) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + + ASSERT_TRUE(proxy->initializeExtension(URI)); + + rapidjson::Document message; + message.Parse(UPDATE_COMPONENT_MESSAGE); + proxy->sendComponentMessage(*activity, message); + + ASSERT_TRUE(extension->processedComponentUpdate); +} + +TEST_F(ExtensionLifecycleTest, UpdateComponentLegacy) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + + ASSERT_TRUE(legacyProxy->initializeExtension(URI)); + + rapidjson::Document message; + message.Parse(UPDATE_COMPONENT_MESSAGE); + legacyProxy->sendComponentMessage(*activity, message); + + ASSERT_TRUE(legacyExtension->processedComponentUpdate); +} + +TEST_F(ExtensionLifecycleTest, LegacyEventCallback) { + auto session = SessionDescriptor::create(); + auto activity = ActivityDescriptor::create(URI, session); + bool receivedEvent = false; + bool receivedLiveData = false; + + ASSERT_TRUE(proxy->initializeExtension(URI)); + + // Register legacy callbacks while the extension uses lifecycle APIs + proxy->registerEventCallback([&](const std::string& uri, + const rapidjson::Value& event) { + receivedEvent = true; + }); + proxy->registerLiveDataUpdateCallback([&](const std::string& uri, const rapidjson::Value& update) { + receivedLiveData = true; + }); + + auto req = RegistrationRequest("1.0") + .uri(URI); + bool successCallbackWasCalled; + proxy->getRegistration(*activity, req, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + successCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + ASSERT_TRUE(successCallbackWasCalled); + + rapidjson::Document command; + command.Parse(COMMAND_MESSAGE); + + ASSERT_FALSE(receivedEvent); // The extension will publish an event in response to the command + bool commandSuccessCallbackWasCalled; + bool commandAccepted = proxy->invokeCommand(*activity, command, + [&](const ActivityDescriptor& activity, const rapidjson::Value &response) { + commandSuccessCallbackWasCalled = true; + }, + [](const ActivityDescriptor& activity, const rapidjson::Value &error) { + FAIL(); + }); + ASSERT_TRUE(commandAccepted); + ASSERT_TRUE(commandSuccessCallbackWasCalled); + ASSERT_TRUE(receivedEvent); + + ASSERT_FALSE(receivedLiveData); + extension->publishLiveData(); + ASSERT_TRUE(receivedLiveData); +} \ No newline at end of file diff --git a/extensions/unit/unittest_extension_registrar.cpp b/extensions/unit/unittest_extension_registrar.cpp new file mode 100644 index 0000000..0154827 --- /dev/null +++ b/extensions/unit/unittest_extension_registrar.cpp @@ -0,0 +1,162 @@ +/** + * 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 "gtest/gtest.h" + +using namespace alexaext; +using namespace rapidjson; + +/** + * Test class; + */ +class ExtensionRegistrarTest : public ::testing::Test { +public: + void SetUp() override { + extRegistrar = std::make_shared(); + } + + void TearDown() override { + extRegistrar = nullptr; + ::testing::Test::TearDown(); + } + + ExtensionRegistrarPtr extRegistrar; +}; + + +class TextExtensionProxy : public ExtensionProxy { +public: + TextExtensionProxy(const std::string& uri) { mURIs.emplace(uri); } + + std::set getURIs() const override { return mURIs; } + + bool initializeExtension(const std::string &uri) override { return true; } + bool isInitialized(const std::string &uri) const override { return true; } + bool getRegistration(const std::string &uri, const rapidjson::Value ®istrationRequest, + RegistrationSuccessCallback success, RegistrationFailureCallback error) override { return false; } + bool invokeCommand(const std::string &uri, const rapidjson::Value &command, + CommandSuccessCallback success, CommandFailureCallback error) override { return false; } + bool sendMessage(const std::string &uri, const rapidjson::Value &message) override { return false; } + void registerEventCallback(Extension::EventCallback callback) override {} + void registerLiveDataUpdateCallback(Extension::LiveDataUpdateCallback callback) override {} + void onRegistered(const std::string &uri, const std::string &token) override {} + void onUnregistered(const std::string &uri, const std::string &token) override {} + void onResourceReady( const std::string& uri, const ResourceHolderPtr& resourceHolder) override {} + +private: + std::set mURIs; +}; + +/** + * Nothing will happen really, for coverage only. + */ +TEST_F(ExtensionRegistrarTest, EmptyAdds) { + extRegistrar->registerExtension(nullptr); + extRegistrar->addProvider(nullptr); +} + +TEST_F(ExtensionRegistrarTest, BasicLocallyRegisteredProxy) { + auto test1 = std::make_shared("test1"); + auto test2 = std::make_shared("test2"); + + extRegistrar->registerExtension(test1); + extRegistrar->registerExtension(test2); + + ASSERT_TRUE(extRegistrar->hasExtension("test1")); + ASSERT_TRUE(extRegistrar->hasExtension("test2")); + ASSERT_FALSE(extRegistrar->hasExtension("test3")); + + ASSERT_EQ(test1, extRegistrar->getExtension("test1")); + ASSERT_EQ(test2, extRegistrar->getExtension("test2")); + ASSERT_EQ(nullptr, extRegistrar->getExtension("test3")); +} + +class TestProvider : public ExtensionProvider { +public: + TestProvider(const std::string& prefix) { + mExtensions.emplace(prefix + "::test1"); + mExtensions.emplace(prefix + "::test2"); + } + + bool hasExtension(const std::string& uri) override { + return mExtensions.count(uri); + } + + ExtensionProxyPtr getExtension(const std::string& uri) override { + if (mExtensions.count(uri) > 0) { + return std::make_shared(uri); + } + return nullptr; + } + +private: + std::set mExtensions; +}; + +TEST_F(ExtensionRegistrarTest, MultipleProviders) { + auto test1 = std::make_shared("test1"); + auto test2 = std::make_shared("test2"); + + auto tp1 = std::make_shared("provider1"); + auto tp2 = std::make_shared("provider2"); + + extRegistrar->addProvider(tp1); + extRegistrar->addProvider(tp2); + extRegistrar->registerExtension(test1); + extRegistrar->registerExtension(test2); + + ASSERT_TRUE(extRegistrar->hasExtension("test1")); + ASSERT_TRUE(extRegistrar->hasExtension("test2")); + ASSERT_FALSE(extRegistrar->hasExtension("test3")); + + ASSERT_TRUE(extRegistrar->hasExtension("provider1::test1")); + ASSERT_TRUE(extRegistrar->hasExtension("provider1::test2")); + ASSERT_FALSE(extRegistrar->hasExtension("provider1::test3")); + + ASSERT_TRUE(extRegistrar->hasExtension("provider2::test1")); + ASSERT_TRUE(extRegistrar->hasExtension("provider2::test2")); + ASSERT_FALSE(extRegistrar->hasExtension("provider2::test3")); + + ASSERT_EQ(test1, extRegistrar->getExtension("test1")); + ASSERT_EQ(test2, extRegistrar->getExtension("test2")); + ASSERT_EQ(nullptr, extRegistrar->getExtension("test3")); + + ASSERT_NE(nullptr, extRegistrar->getExtension("provider1::test1")); + ASSERT_NE(nullptr, extRegistrar->getExtension("provider1::test2")); + ASSERT_EQ(nullptr, extRegistrar->getExtension("provider1::test3")); + + ASSERT_NE(nullptr, extRegistrar->getExtension("provider2::test1")); + ASSERT_NE(nullptr, extRegistrar->getExtension("provider2::test2")); + ASSERT_EQ(nullptr, extRegistrar->getExtension("provider2::test3")); +} + +TEST_F(ExtensionRegistrarTest, ReturnsSame) { + auto test1 = std::make_shared("test1"); + + auto tp1 = std::make_shared("provider1"); + + extRegistrar->addProvider(tp1); + extRegistrar->registerExtension(test1); + + ASSERT_TRUE(extRegistrar->hasExtension("test1")); + ASSERT_TRUE(extRegistrar->hasExtension("provider1::test1")); + ASSERT_EQ(test1, extRegistrar->getExtension("test1")); + + ASSERT_EQ(extRegistrar->getExtension("test1"), extRegistrar->getExtension("test1")); + ASSERT_EQ(extRegistrar->getExtension("provider1::test1"), extRegistrar->getExtension("provider1::test1")); +} \ No newline at end of file diff --git a/extensions/unit/unittest_random.cpp b/extensions/unit/unittest_random.cpp new file mode 100644 index 0000000..f1fa1ef --- /dev/null +++ b/extensions/unit/unittest_random.cpp @@ -0,0 +1,38 @@ +/** + * 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 + +TEST(Random, DefaultParameters) { + auto value = alexaext::generateBase36Token(); + + ASSERT_EQ(8, value.length()); + ASSERT_NE(value, alexaext::generateBase36Token()); +} + +TEST(Random, Size) { + auto value = alexaext::generateBase36Token("", 10); + + ASSERT_EQ(10, value.length()); +} + +TEST(Random, Prefix) { + auto value = alexaext::generateBase36Token("unit-"); + + ASSERT_EQ("unit-", value.substr(0, 5)); +} \ No newline at end of file diff --git a/extensions/unit/unittest_session_descriptor.cpp b/extensions/unit/unittest_session_descriptor.cpp new file mode 100644 index 0000000..d428f62 --- /dev/null +++ b/extensions/unit/unittest_session_descriptor.cpp @@ -0,0 +1,70 @@ +/** + * 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 + +TEST(SessionDescriptorTest, HasUniqueId) { + auto session1 = alexaext::SessionDescriptor::create(); + auto session2 = alexaext::SessionDescriptor::create(); + + ASSERT_NE(session1->getId(), session2->getId()); + ASSERT_TRUE(*session1 != *session2); + ASSERT_FALSE(*session1 == *session2); +} + +TEST(SessionDescriptorTest, CanBeDeserialized) { + auto session1 = alexaext::SessionDescriptor::create(); + auto session2 = alexaext::SessionDescriptor::create(session1->getId()); + + ASSERT_EQ(session1->getId(), session2->getId()); + ASSERT_TRUE(*session1 == *session2); + ASSERT_FALSE(*session1 != *session2); +} + +TEST(SessionDescriptorTest, IsCopyable) { + auto session = alexaext::SessionDescriptor::create(); + alexaext::SessionDescriptor copy = *session; + + ASSERT_TRUE(copy == *session); +} + +TEST(SessionDescriptorTest, IsMovable) { + alexaext::SessionDescriptor moved = alexaext::SessionDescriptor("unittest-id"); + + ASSERT_EQ("unittest-id", moved.getId()); +} + +TEST(SessionDescriptorTest, IsHashable) { + auto session1 = alexaext::SessionDescriptor::create(); + auto session2 = alexaext::SessionDescriptor::create(); + + alexaext::SessionDescriptor::Hash hash; + ASSERT_NE(hash(*session1), hash(*session2)); +} + +TEST(SessionDescriptorTest, IsComparable) { + auto session1 = alexaext::SessionDescriptor::create("abc"); + auto session2 = alexaext::SessionDescriptor::create("def"); + + alexaext::SessionDescriptor::Compare compare; + ASSERT_TRUE(compare(*session1, *session2)); + ASSERT_FALSE(compare(*session2, *session1)); + + // By contract, identical objects should compare as false + ASSERT_FALSE(compare(*session1, *session1)); +} diff --git a/options.cmake b/options.cmake index 0c0b4c5..bc1af65 100644 --- a/options.cmake +++ b/options.cmake @@ -17,12 +17,17 @@ option(TRACING "Enable tracing." OFF) option(COVERAGE "Coverage instrumentation" OFF) option(WERROR "Build with -Werror enabled." OFF) option(VALIDATE_HEADERS "Validate that only external headers are (transitively) included from apl.h" ON) +option(VALIDATE_FORBIDDEN_FUNCTIONS "Validate that there are no calls to forbidden functions" ON) option(USER_DATA_RELEASE_CALLBACKS "Enable release callbacks in UserData" ON) option(BUILD_SHARED "Build as shared library." OFF) option(ENABLE_PIC "Build position independent code (i.e. -fPIC)" OFF) option(USE_SYSTEM_RAPIDJSON "Use the system-provided RapidJSON instead of the bundled one." OFF) + option(USE_SYSTEM_YOGA "Use the system-provided Yoga library instead of the bundled one." OFF) +option(USE_PROVIDED_YOGA_INLINE "Use the provided yoga and build it directly into the library." OFF) +# Not listed: YOGA_EXTERNAL_INSTALL_DIR used for an externally provided Yoga library + option(ENABLE_ALEXAEXTENSIONS "Use the Alexa Extensions library." OFF) option(BUILD_ALEXAEXTENSIONS "Build Alexa Extensions library as part of the project." OFF) @@ -46,6 +51,28 @@ if(ENABLE_ALEXAEXTENSIONS) set(ALEXAEXTENSIONS ON) endif(ENABLE_ALEXAEXTENSIONS) +# 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") +endif() + +# Clean up Yoga based on settings. Throw a fatal error if more than one Yoga option is set +# The default before is to use the provided Yoga. Start with it off; turn it on if no other option is set +set(USE_PROVIDED_YOGA_AS_LIB OFF) + +if(YOGA_EXTERNAL_INSTALL_DIR) + if (USE_SYSTEM_YOGA OR USE_PROVIDED_YOGA_INLINE) + message(FATAL_ERROR "An external yoga directory is incompatible with specifying the system or provided yoga") + endif() +elseif(USE_SYSTEM_YOGA) + if (USE_PROVIDED_YOGA_INLINE) + message(FATAL_ERROR "Using the system yoga is incompatible with using the provided yoga") + endif() +elseif(NOT USE_PROVIDED_YOGA_INLINE) + set(USE_PROVIDED_YOGA_AS_LIB ON) +endif() + + # 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) include_directories(${CMAKE_CURRENT_BINARY_DIR}/aplcore/include) diff --git a/patches/yoga.patch b/patches/yoga.patch index fc30d1e..3ce01eb 100644 --- a/patches/yoga.patch +++ b/patches/yoga.patch @@ -1,18 +1,14 @@ -diff --git b/.DS_Store b/.DS_Store -new file mode 100644 -index 0000000..3ecbe97 -Binary files /dev/null and b/.DS_Store differ diff --git a/CMakeLists.txt b/CMakeLists.txt -index 595faef..102ce08 100644 +index 018c269f..5a1a73c8 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt -@@ -15,3 +15,4 @@ add_library(yogacore STATIC ${yogacore_SRC}) +@@ -22,3 +22,4 @@ add_library(yogacore STATIC ${yogacore_SRC}) target_include_directories(yogacore PUBLIC .) target_link_libraries(yogacore android log) set_target_properties(yogacore PROPERTIES CXX_STANDARD 11) +install(TARGETS yogacore ARCHIVE DESTINATION ${CMAKE_INSTALL_PREFIX}) diff --git a/lib/fb/src/main/cpp/include/lyra/lyra.h b/lib/fb/src/main/cpp/include/lyra/lyra.h -index 02e6078..2d53a87 100644 +index 02e6078d..2d53a87a 100644 --- a/lib/fb/src/main/cpp/include/lyra/lyra.h +++ b/lib/fb/src/main/cpp/include/lyra/lyra.h @@ -172,16 +172,16 @@ inline std::vector getStackTraceSymbols( @@ -43,7 +39,7 @@ index 02e6078..2d53a87 100644 /** * Log stack trace diff --git a/lib/fb/src/main/cpp/lyra/lyra.cpp b/lib/fb/src/main/cpp/lyra/lyra.cpp -index 599a360..c4c514c 100644 +index 599a360f..c4c514c0 100644 --- a/lib/fb/src/main/cpp/lyra/lyra.cpp +++ b/lib/fb/src/main/cpp/lyra/lyra.cpp @@ -8,7 +8,6 @@ @@ -120,7 +116,7 @@ index 599a360..c4c514c 100644 void logStackTrace(const vector& trace) { auto i = 0; diff --git a/lib/fb/src/main/cpp/lyra/lyra_exceptions.cpp b/lib/fb/src/main/cpp/lyra/lyra_exceptions.cpp -index c07e6fd..0fbcb0f 100644 +index c07e6fdb..0fbcb0f1 100644 --- a/lib/fb/src/main/cpp/lyra/lyra_exceptions.cpp +++ b/lib/fb/src/main/cpp/lyra/lyra_exceptions.cpp @@ -8,7 +8,6 @@ @@ -143,11 +139,11 @@ index c07e6fd..0fbcb0f 100644 return "Unknown exception"; } diff --git a/tests/YGNodeCallbackTest.cpp b/tests/YGNodeCallbackTest.cpp -index be019d1..c57836b 100644 +index 5e765a0d..cc824155 100644 --- a/tests/YGNodeCallbackTest.cpp +++ b/tests/YGNodeCallbackTest.cpp -@@ -6,7 +6,6 @@ - */ +@@ -7,7 +7,6 @@ + #include #include -#include @@ -155,11 +151,11 @@ index be019d1..c57836b 100644 inline bool operator==(const YGSize& lhs, const YGSize& rhs) { return lhs.width == rhs.width && lhs.height == rhs.height; diff --git a/tests/YGStyleTest.cpp b/tests/YGStyleTest.cpp -index 530d8de..0ef14e9 100644 +index 56aa299d..79f38e2c 100644 --- a/tests/YGStyleTest.cpp +++ b/tests/YGStyleTest.cpp -@@ -6,7 +6,6 @@ - */ +@@ -7,7 +7,6 @@ + #include #include -#include @@ -167,11 +163,11 @@ index 530d8de..0ef14e9 100644 TEST(YogaTest, copy_style_same) { const YGNodeRef node0 = YGNodeNew(); diff --git a/yoga/YGNode.cpp b/yoga/YGNode.cpp -index bb240df..85fc976 100644 +index f4c14bf3..aaa5cf7a 100644 --- a/yoga/YGNode.cpp +++ b/yoga/YGNode.cpp -@@ -6,7 +6,6 @@ - */ +@@ -7,7 +7,6 @@ + #include "YGNode.h" #include -#include @@ -179,36 +175,36 @@ index bb240df..85fc976 100644 #include "Utils.h" diff --git a/yoga/YGNodePrint.cpp b/yoga/YGNodePrint.cpp -index f91d037..b00c6fe 100644 +index 72d147db..57be97d9 100644 --- a/yoga/YGNodePrint.cpp +++ b/yoga/YGNodePrint.cpp -@@ -4,7 +4,7 @@ - * This source code is licensed under the MIT license found in the LICENSE - * file in the root directory of this source tree. +@@ -5,7 +5,7 @@ + * LICENSE file in the root directory of this source tree. */ + -#ifdef DEBUG +#ifndef NDEBUG #include "YGNodePrint.h" #include #include "YGEnums.h" diff --git a/yoga/YGNodePrint.h b/yoga/YGNodePrint.h -index 8df30e2..cafe23f 100644 +index 3db504b4..31ee0e4c 100644 --- a/yoga/YGNodePrint.h +++ b/yoga/YGNodePrint.h -@@ -4,7 +4,7 @@ - * This source code is licensed under the MIT license found in the LICENSE - * file in the root directory of this source tree. +@@ -5,7 +5,7 @@ + * LICENSE file in the root directory of this source tree. */ + -#ifdef DEBUG +#ifndef NDEBUG #pragma once #include diff --git a/yoga/Yoga.cpp b/yoga/Yoga.cpp -index 1a374ab..6d4a0e4 100644 +index 2c68674a..22d3cba0 100644 --- a/yoga/Yoga.cpp +++ b/yoga/Yoga.cpp -@@ -945,7 +945,7 @@ bool YGLayoutNodeInternal( +@@ -996,7 +996,7 @@ bool YGLayoutNodeInternal( const uint32_t depth, const uint32_t generationCount); @@ -217,7 +213,7 @@ index 1a374ab..6d4a0e4 100644 static void YGNodePrintInternal( const YGNodeRef node, const YGPrintOptions options) { -@@ -4140,7 +4140,7 @@ void YGNodeCalculateLayoutWithContext( +@@ -4190,7 +4190,7 @@ YOGA_EXPORT void YGNodeCalculateLayoutWithContext( node->getLayout().direction(), ownerWidth, ownerHeight, ownerWidth); YGRoundToPixelGrid(node, node->getConfig()->pointScaleFactor, 0.0f, 0.0f); @@ -226,7 +222,7 @@ index 1a374ab..6d4a0e4 100644 if (node->getConfig()->printTree) { YGNodePrint( node, -@@ -4202,7 +4202,7 @@ void YGNodeCalculateLayoutWithContext( +@@ -4250,7 +4250,7 @@ YOGA_EXPORT void YGNodeCalculateLayoutWithContext( !nodeWithoutLegacyFlag->isLayoutTreeEqualToNode(*node); node->setLayoutDoesLegacyFlagAffectsLayout(neededLegacyStretchBehaviour); @@ -235,6 +231,3 @@ index 1a374ab..6d4a0e4 100644 if (nodeWithoutLegacyFlag->getConfig()->printTree) { YGNodePrint( nodeWithoutLegacyFlag, -diff --git b/yoga/yoga.patch b/yoga/yoga.patch -new file mode 100644 -index 0000000..e69de29 diff --git a/test/utils.h b/test/utils.h index 3ae5b60..55dd054 100644 --- a/test/utils.h +++ b/test/utils.h @@ -281,7 +281,11 @@ class ViewportSettings { auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()); - auto rootConfig = apl::RootConfig().agent("APL", "1.3").utcTime(now.count()); + auto rootConfig = apl::RootConfig() + .set({ + {apl::RootProperty::kAgentName, "APL"}, + {apl::RootProperty::kAgentVersion, "1.3"}}) + .set(apl::RootProperty::kUTCTime, now.count()); auto context = apl::Context::createTestContext(metrics(), rootConfig); for (const auto& m : mVariables) context->putUserWriteable(m.first, m.second); diff --git a/thirdparty/thirdparty.cmake b/thirdparty/thirdparty.cmake index 570dce4..73ae803 100644 --- a/thirdparty/thirdparty.cmake +++ b/thirdparty/thirdparty.cmake @@ -28,6 +28,10 @@ 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}) +# Yoga can be built from local source or can be included as a system or external library +set(YOGA_SOURCE_URL "${APL_PROJECT_DIR}/thirdparty/yoga-1.19.0.tar.gz") +set(YOGA_SOURCE_MD5 "284d6752a3fea3937a1abd49e826b109") + if (YOGA_EXTERNAL_INSTALL_DIR) # Use an externally provided Yoga library find_path(YOGA_INCLUDE @@ -50,11 +54,21 @@ elseif (USE_SYSTEM_YOGA) PATHS ${CMAKE_SYSROOT}/usr/lib REQUIRED) set(YOGA_EXTERNAL_LIB ${YOGA_LIB}) # used by aplcoreConfig.cmake.in +elseif (USE_PROVIDED_YOGA_INLINE) + # Unpack the bundled Yoga library + FetchContent_Declare(yogasource + URL ${YOGA_SOURCE_URL} + URL_MD5 ${YOGA_SOURCE_MD5} + PATCH_COMMAND patch ${PATCH_FLAGS} -p1 < ${APL_PATCH_DIR}/yoga.patch + ) + FetchContent_MakeAvailable(yogasource) + set(YOGA_INCLUDE ${yogasource_SOURCE_DIR}) + file(GLOB_RECURSE YOGA_SRC ${yogasource_SOURCE_DIR}/yoga/*.cpp) else() # Build the bundled Yoga library ExternalProject_Add(yoga - URL ${APL_PROJECT_DIR}/thirdparty/yoga-1.16.0.tar.gz - URL_MD5 c9e88076ec371513fb23a0a5370ec2fd + URL ${YOGA_SOURCE_URL} + URL_MD5 ${YOGA_SOURCE_MD5} EXCLUDE_FROM_ALL TRUE INSTALL_DIR ${CMAKE_BINARY_DIR}/lib PATCH_COMMAND patch ${PATCH_FLAGS} -p1 < ${APL_PATCH_DIR}/yoga.patch @@ -68,14 +82,17 @@ else() set(YOGA_LIB ${install_dir}/${CMAKE_STATIC_LIBRARY_PREFIX}yogacore${CMAKE_STATIC_LIBRARY_SUFFIX}) endif() +if(NOT USE_PROVIDED_YOGA_INLINE) add_library(libyoga STATIC IMPORTED) set_target_properties(libyoga PROPERTIES IMPORTED_LOCATION "${YOGA_LIB}" ) +set(YOGA_PC_LIBS "-lyogacore") message(VERBOSE Using yoga include directory = ${YOGA_INCLUDE}) message(VERBOSE Using yoga lib = ${YOGA_LIB}) +endif() ExternalProject_Add(pegtl URL ${APL_PROJECT_DIR}/thirdparty/pegtl-2.8.3.tar.gz diff --git a/thirdparty/yoga-1.16.0.tar.gz b/thirdparty/yoga-1.16.0.tar.gz deleted file mode 100644 index 38f3cc0..0000000 Binary files a/thirdparty/yoga-1.16.0.tar.gz and /dev/null differ diff --git a/thirdparty/yoga-1.19.0.tar.gz b/thirdparty/yoga-1.19.0.tar.gz new file mode 100644 index 0000000..bfd0e7f Binary files /dev/null and b/thirdparty/yoga-1.19.0.tar.gz differ diff --git a/unit/animation/unittest_easing.cpp b/unit/animation/unittest_easing.cpp index 9bc6eba..3656b6e 100644 --- a/unit/animation/unittest_easing.cpp +++ b/unit/animation/unittest_easing.cpp @@ -15,6 +15,8 @@ #include "../testeventloop.h" +#include + #include "apl/animation/coreeasing.h" #include "apl/animation/easinggrammar.h" @@ -617,3 +619,43 @@ TEST_F(EasingTest, MultiSegmentPositionCurve) {1.00, 0}})); } +TEST_F(EasingTest, EastingParsingIgnoresCLocale) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + auto curve = Easing::parse(session, " path(0.25, 1, 0.75, 0)"); + + EXPECT_NEAR(0, curve->calc(0), 0.0001); + EXPECT_NEAR(0.5, curve->calc(0.125), 0.0001); + EXPECT_NEAR(1, curve->calc(0.25), 0.0001); + EXPECT_NEAR(0.5, curve->calc(0.5), 0.0001); + EXPECT_NEAR(0, curve->calc(0.75), 0.0001); + EXPECT_NEAR(0.5, curve->calc(0.875), 0.0001); + EXPECT_NEAR(1, curve->calc(1), 0.0001); + + const std::string TEST = "scurve(0,0,0,10,0,0,-10,0.1,0.1,0.5,0.5) send(1,10,10)"; + + // X-coordinate + ASSERT_TRUE(CheckCurve( + session, + "spatial(2,0) " + TEST, + {{0, 0}, + {0.25, 0.450455 * 10}, + {0.50, 0.875000 * 10}, + {0.75, 0.994079 * 10}, + {1.00, 10}})); + + // Y-coordinate + ASSERT_TRUE(CheckCurve( + session, + "spatial(2,1) " + TEST, + {{0, 0}, + {0.25, 0.005922 * 10}, + {0.50, 0.125000 * 10}, + {0.75, 0.549546 * 10}, + {1.00, 10}})); + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); +} + diff --git a/unit/command/unittest_command_animateitem.cpp b/unit/command/unittest_command_animateitem.cpp index b2cf7f1..9e480c8 100644 --- a/unit/command/unittest_command_animateitem.cpp +++ b/unit/command/unittest_command_animateitem.cpp @@ -84,7 +84,7 @@ TEST_F(AnimateItemTest, Basic) TEST_F(AnimateItemTest, AnimateNone) { - config->animationQuality(RootConfig::AnimationQuality::kAnimationQualityNone); + config->set(RootProperty::kAnimationQuality, RootConfig::AnimationQuality::kAnimationQualityNone); loadDocument(ANIMATE); auto frame = root->context().findComponentById("box"); auto goButton = root->context().findComponentById("go"); diff --git a/unit/command/unittest_command_event_binding.cpp b/unit/command/unittest_command_event_binding.cpp index c9a857d..3f93506 100644 --- a/unit/command/unittest_command_event_binding.cpp +++ b/unit/command/unittest_command_event_binding.cpp @@ -271,6 +271,7 @@ TEST_F(CommandEventBinding, VideoComponentEventInterpolation) 0, 12000, false, + false, false)); // Pause the video @@ -279,6 +280,7 @@ TEST_F(CommandEventBinding, VideoComponentEventInterpolation) 230, 12000, true, + false, false)); root->clearPending(); ASSERT_TRUE(CheckDirty(component, kPropertySource, kPropertyVisualHash)); diff --git a/unit/command/unittest_command_media.cpp b/unit/command/unittest_command_media.cpp index 79372eb..2c81309 100644 --- a/unit/command/unittest_command_media.cpp +++ b/unit/command/unittest_command_media.cpp @@ -47,32 +47,31 @@ class CommandMediaTest : public CommandTest { rapidjson::Document doc; }; -static const char *VIDEO = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Video\"," - " \"id\": \"myVideo\"," - " \"width\": 100," - " \"height\": 100," - " \"source\": [\"URL1\", \"URL2\"]" - " }," - " {" - " \"type\": \"Video\"," - " \"id\": \"myVideo3\"," - " \"width\": 100," - " \"height\": 100," - " \"source\": \"URL1\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *VIDEO = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "Video", + "id": "myVideo", + "width": 100, + "height": 100, + "source": ["URL1", "URL2"] + }, + { + "type": "Video", + "id": "myVideo3", + "width": 100, + "height": 100, + "source": "URL1" + } + ] + } + } +})"; TEST_F(CommandMediaTest, Control) { @@ -85,7 +84,7 @@ TEST_F(CommandMediaTest, Control) ASSERT_EQ(kEventTypeControlMedia, event.getType()); ASSERT_EQ(kEventControlMediaPlay, event.getValue(kEventPropertyCommand).asInt()); ASSERT_EQ(component->getCoreChildAt(0), event.getComponent()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); ASSERT_FALSE(root->hasEvent()); // Play in fast mode ignored @@ -215,7 +214,7 @@ TEST_F(CommandMediaTest, Play) ASSERT_EQ(kEventTypePlayMedia, event.getType()); ASSERT_EQ(kEventAudioTrackForeground, event.getValue(kEventPropertyAudioTrack).getInteger()); ASSERT_EQ(component->getCoreChildAt(0), event.getComponent()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); event.getActionRef().resolve(); // Play background audio @@ -225,7 +224,7 @@ TEST_F(CommandMediaTest, Play) ASSERT_EQ(kEventTypePlayMedia, event.getType()); ASSERT_EQ(kEventAudioTrackBackground, event.getValue(kEventPropertyAudioTrack).getInteger()); ASSERT_EQ(component->getCoreChildAt(0), event.getComponent()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); // Play without audio executePlayMedia("myVideo", "none", Object::EMPTY_ARRAY(), false); @@ -234,7 +233,7 @@ TEST_F(CommandMediaTest, Play) ASSERT_EQ(kEventTypePlayMedia, event.getType()); ASSERT_EQ(kEventAudioTrackNone, event.getValue(kEventPropertyAudioTrack).getInteger()); ASSERT_EQ(component->getCoreChildAt(0), event.getComponent()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); // Test the "mute" alias executePlayMedia("myVideo", "mute", Object::EMPTY_ARRAY(), false); @@ -243,7 +242,7 @@ TEST_F(CommandMediaTest, Play) ASSERT_EQ(kEventTypePlayMedia, event.getType()); ASSERT_EQ(kEventAudioTrackNone, event.getValue(kEventPropertyAudioTrack).getInteger()); ASSERT_EQ(component->getCoreChildAt(0), event.getComponent()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); // Play in fast mode ASSERT_FALSE(ConsoleMessage()); @@ -275,24 +274,23 @@ TEST_F(CommandMediaTest, PlayMalformed) ASSERT_TRUE(ConsoleMessage()); } -const static char *COMMAND_SERIES = - "[" - " {" - " \"type\": \"PlayMedia\"," - " \"componentId\": \"myVideo\"," - " \"source\": \"URLX\"" - " }," - " {" - " \"type\": \"ControlMedia\"," - " \"componentId\": \"myVideo\"," - " \"command\": \"next\"" - " }," - " {" - " \"type\": \"ControlMedia\"," - " \"componentId\": \"myVideo\"," - " \"command\": \"previous\"" - " }" - "]"; +const static char *COMMAND_SERIES = R"([ + { + "type": "PlayMedia", + "componentId": "myVideo", + "source": "URLX" + }, + { + "type": "ControlMedia", + "componentId": "myVideo", + "command": "next" + }, + { + "type": "ControlMedia", + "componentId": "myVideo", + "command": "previous" + } +])"; @@ -314,11 +312,11 @@ TEST_F(CommandMediaTest, ControlSeries) // The first event should be a play with an action reference ASSERT_EQ(kEventTypePlayMedia, event.getType()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); ASSERT_EQ(video, event.getComponent()); // Update the video state - MediaState state(0, 3, 0, 1000, false, false); + MediaState state(0, 3, 0, 1000, false, false, false); video->updateMediaState(state, true); CheckMediaState(state, video->getCalculated()); event.getActionRef().resolve(); @@ -332,11 +330,11 @@ TEST_F(CommandMediaTest, ControlSeries) // The second event should be a control media with an action reference ASSERT_EQ(kEventTypeControlMedia, event.getType()); ASSERT_EQ(kEventControlMediaNext, event.getValue(kEventPropertyCommand).asInt()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); ASSERT_EQ(video, event.getComponent()); // Update the video state. It's paused because the control media commands pause it - state = MediaState(1, 3, 0, 1000, true, false); + state = MediaState(1, 3, 0, 1000, true, false, false); video->updateMediaState(state, true); CheckMediaState(state, video->getCalculated()); event.getActionRef().resolve(); @@ -350,11 +348,11 @@ TEST_F(CommandMediaTest, ControlSeries) // The third event should be a control media with an action reference ASSERT_EQ(kEventTypeControlMedia, event.getType()); ASSERT_EQ(kEventControlMediaPrevious, event.getValue(kEventPropertyCommand).asInt()); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); ASSERT_EQ(video, event.getComponent()); // Update the video state. It's paused because the control media commands pause it - state = MediaState(0, 3, 0, 1000, true, false); + state = MediaState(0, 3, 0, 1000, true, false, false); video->updateMediaState(state, true); CheckMediaState(state, video->getCalculated()); event.getActionRef().resolve(); diff --git a/unit/command/unittest_command_openurl.cpp b/unit/command/unittest_command_openurl.cpp index 2c1a4e4..393ba96 100644 --- a/unit/command/unittest_command_openurl.cpp +++ b/unit/command/unittest_command_openurl.cpp @@ -26,7 +26,7 @@ class CommandOpenURLTest : public CommandTest { protected: void SetUp() override { - config->allowOpenUrl(true); + config->set(RootProperty::kAllowOpenUrl, true); } }; @@ -88,7 +88,7 @@ TEST_F(CommandOpenURLTest, OpenURLFail) TEST_F(CommandOpenURLTest, OpenURLNotAllowed) { - config->allowOpenUrl(false); + config->set(RootProperty::kAllowOpenUrl, false); loadDocument(OPEN_URL); performClick(1, 1); diff --git a/unit/command/unittest_command_page.cpp b/unit/command/unittest_command_page.cpp index eaa76ac..32ed2f9 100644 --- a/unit/command/unittest_command_page.cpp +++ b/unit/command/unittest_command_page.cpp @@ -618,7 +618,7 @@ TEST_F(CommandPageTest, SpeakItemCombination) ASSERT_TRUE(root->hasEvent()); auto event = root->popEvent(); ASSERT_EQ(kEventTypePreroll, event.getType()); - if (!event.getActionRef().isEmpty() && event.getActionRef().isPending()) event.getActionRef().resolve(); + if (!event.getActionRef().empty() && event.getActionRef().isPending()) event.getActionRef().resolve(); // And page should have switched - command is in parallel ASSERT_EQ(1, component->pagePosition()); @@ -626,7 +626,7 @@ TEST_F(CommandPageTest, SpeakItemCombination) ASSERT_TRUE(root->hasEvent()); event = root->popEvent(); ASSERT_EQ(kEventTypeSpeak, event.getType()); - if (!event.getActionRef().isEmpty() && event.getActionRef().isPending()) event.getActionRef().resolve(); + if (!event.getActionRef().empty() && event.getActionRef().isPending()) event.getActionRef().resolve(); root->clearPending(); @@ -634,12 +634,12 @@ TEST_F(CommandPageTest, SpeakItemCombination) ASSERT_TRUE(root->hasEvent()); event = root->popEvent(); ASSERT_EQ(kEventTypePreroll, event.getType()); - if (!event.getActionRef().isEmpty() && event.getActionRef().isPending()) event.getActionRef().resolve(); + if (!event.getActionRef().empty() && event.getActionRef().isPending()) event.getActionRef().resolve(); ASSERT_TRUE(root->hasEvent()); event = root->popEvent(); ASSERT_EQ(kEventTypeSpeak, event.getType()); - if (!event.getActionRef().isEmpty() && event.getActionRef().isPending()) event.getActionRef().resolve(); + if (!event.getActionRef().empty() && event.getActionRef().isPending()) event.getActionRef().resolve(); } static const char *AUTO_PAGER_ON_MOUNT_WITH_DELAY = R"apl( diff --git a/unit/command/unittest_commands.cpp b/unit/command/unittest_commands.cpp index 7be4b2e..2a9e52f 100644 --- a/unit/command/unittest_commands.cpp +++ b/unit/command/unittest_commands.cpp @@ -23,27 +23,27 @@ using namespace apl; -static const char *DATA = "{" - "\"title\": \"Pecan Pie V\"" - "}"; - - -const char *TOUCH_WRAPPER_EMPTY = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }" - "}"; +static const char *DATA = R"({ + "title": "Pecan Pie V" +})"; + + +const char *TOUCH_WRAPPER_EMPTY = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "items": { + "type": "Text", + "text": "${payload.title}" + } + } + } +})"; TEST_F(CommandTest, OnEmptyPress) { @@ -61,29 +61,29 @@ TEST_F(CommandTest, OnEmptyPress) } -const char *TOUCH_WRAPPER_OTHER = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"SetValue\"," - " \"property\": \"opacity\"," - " \"value\": 0.5," - " \"componentId\": \"foo\"" - " }," - " \"items\": {" - " \"type\": \"Text\"," - " \"id\": \"foo\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }" - "}"; +const char *TOUCH_WRAPPER_OTHER = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "SetValue", + "property": "opacity", + "value": 0.5, + "componentId": "foo" + }, + "items": { + "type": "Text", + "id": "foo", + "text": "${payload.title}" + } + } + } +})"; TEST_F(CommandTest, OnSetValueOther) { @@ -113,27 +113,27 @@ TEST_F(CommandTest, OnSetValueOther) } -const char *TOUCH_WRAPPER_SELF = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"SetValue\"," - " \"property\": \"opacity\"," - " \"value\": 0.5" - " }," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }" - "}"; +const char *TOUCH_WRAPPER_SELF = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "SetValue", + "property": "opacity", + "value": 0.5 + }, + "items": { + "type": "Text", + "text": "${payload.title}" + } + } + } +})"; TEST_F(CommandTest, OnSetValueSelf) { @@ -160,26 +160,26 @@ TEST_F(CommandTest, OnSetValueSelf) } -const char *TOUCH_WRAPPER_DISABLED = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"SendEvent\"," - " \"when\": false" - " }," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }" - "}"; +const char *TOUCH_WRAPPER_DISABLED = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "SendEvent", + "when": false + }, + "items": { + "type": "Text", + "text": "${payload.title}" + } + } + } +})"; TEST_F(CommandTest, OnPressDisabled) { @@ -199,27 +199,27 @@ TEST_F(CommandTest, OnPressDisabled) } -const char *TOUCH_WRAPPER_DELAYED = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"SendEvent\"," - " \"when\": true," - " \"delay\": 100" - " }," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }" - "}"; +const char *TOUCH_WRAPPER_DELAYED = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "SendEvent", + "when": true, + "delay": 100 + }, + "items": { + "type": "Text", + "text": "${payload.title}" + } + } + } +})"; TEST_F(CommandTest, OnPressDelayed) { @@ -245,40 +245,40 @@ TEST_F(CommandTest, OnPressDelayed) } -const char *TOUCH_WRAPPER_ARRAY = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": [" - " {" - " \"type\": \"SendEvent\"," - " \"when\": true," - " \"delay\": 100," - " \"arguments\": [1,2,\"3\"]" - " }," - " {" - " \"type\": \"Idle\"," - " \"when\": false," - " \"delay\": 50" - " }," - " {" - " \"type\": \"Idle\"," - " \"when\": true," - " \"delay\": 100" - " }" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"${payload.title}\"" - " }" - " }" - " }" - "}"; +const char *TOUCH_WRAPPER_ARRAY = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": [ + { + "type": "SendEvent", + "when": true, + "delay": 100, + "arguments": [1,2,"3"] + }, + { + "type": "Idle", + "when": false, + "delay": 50 + }, + { + "type": "Idle", + "when": true, + "delay": 100 + } + ], + "items": { + "type": "Text", + "text": "${payload.title}" + } + } + } +})"; TEST_F(CommandTest, OnPressCommandArray) { @@ -362,26 +362,26 @@ TEST_F(CommandTest, OnPressCommandArrayTerminate) ASSERT_EQ(0, loop->size()); } -const char * SEQ_TEST = "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"Sequential\"," - " \"delay\": 100," - " \"repeatCount\": 1," - " \"commands\": {" - " \"type\": \"SendEvent\"" - " }" - " }" - " }" - " }" - "}"; +const char * SEQ_TEST = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Sequential", + "delay": 100, + "repeatCount": 1, + "commands": { + "type": "SendEvent" + } + } + } + } +})"; TEST_F(CommandTest, SequentialTest) { @@ -410,73 +410,72 @@ TEST_F(CommandTest, SequentialTest) root->popEvent(); } -static const char *TRY_CATCH_FINALLY = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"Sequential\"," - " \"repeatCount\": 2," - " \"commands\": {" - " \"type\": \"Custom\"," - " \"delay\": 1000," - " \"arguments\": [" - " \"try\"" - " ]" - " }," - " \"catch\": [" - " {" - " \"type\": \"Custom\"," - " \"arguments\": [" - " \"catch1\"" - " ]," - " \"delay\": 1000" - " }," - " {" - " \"type\": \"Custom\"," - " \"arguments\": [" - " \"catch2\"" - " ]," - " \"delay\": 1000" - " }," - " {" - " \"type\": \"Custom\"," - " \"arguments\": [" - " \"catch3\"" - " ]," - " \"delay\": 1000" - " }" - " ]," - " \"finally\": [" - " {" - " \"type\": \"Custom\"," - " \"arguments\": [" - " \"finally1\"" - " ]," - " \"delay\": 1000" - " }," - " {" - " \"type\": \"Custom\"," - " \"arguments\": [" - " \"finally2\"" - " ]," - " \"delay\": 1000" - " }," - " {" - " \"type\": \"Custom\"," - " \"arguments\": [" - " \"finally3\"" - " ]," - " \"delay\": 1000" - " }" - " ]" - " }" - " }" - " }" - "}"; +static const char *TRY_CATCH_FINALLY = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Sequential", + "repeatCount": 2, + "commands": { + "type": "Custom", + "delay": 1000, + "arguments": [ + "try" + ] + }, + "catch": [ + { + "type": "Custom", + "arguments": [ + "catch1" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "catch2" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "catch3" + ], + "delay": 1000 + } + ], + "finally": [ + { + "type": "Custom", + "arguments": [ + "finally1" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "finally2" + ], + "delay": 1000 + }, + { + "type": "Custom", + "arguments": [ + "finally3" + ], + "delay": 1000 + } + ] + } + } + } +})"; // Let the entire command run normally through the "try" and "finally" parts @@ -604,44 +603,44 @@ TEST_F(CommandTest, TryCatchFinallyAbortAfterTry) -const char *PARALLEL_TEST = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onPress\": {" - " \"type\": \"Parallel\"," - " \"commands\": [" - " {" - " \"type\": \"Idle\"" - " }," - " {" - " \"type\": \"Idle\"," - " \"when\": false" - " }," - " {" - " \"type\": \"Idle\"," - " \"delay\": 100" - " }," - " {" - " \"type\": \"Idle\"," - " \"delay\": 150," - " \"when\": false" - " }," - " {" - " \"type\": \"Idle\"," - " \"delay\": 200," - " \"when\": true" - " }" - " ]" - " }" - " }" - " }" - "}"; +const char *PARALLEL_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "Parallel", + "commands": [ + { + "type": "Idle" + }, + { + "type": "Idle", + "when": false + }, + { + "type": "Idle", + "delay": 100 + }, + { + "type": "Idle", + "delay": 150, + "when": false + }, + { + "type": "Idle", + "delay": 200, + "when": true + } + ] + } + } + } +})"; TEST_F(CommandTest, ParallelTest) { @@ -675,103 +674,103 @@ TEST_F(CommandTest, ParallelTestTerminated) ASSERT_EQ(100, loop->currentTime()); } -const char *LARGE_TEST = "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"direction\": \"row\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"myTouchWrapper\"," - " \"onPress\": [" - " {" - " \"type\": \"Sequential\"," - " \"commands\": [" - " {" - " \"type\": \"Parallel\"," - " \"commands\": [" - " {" - " \"type\": \"SetValue\"," - " \"property\": \"text\"," - " \"value\": \"Hello 1\"," - " \"componentId\": \"text1\"" - " }," - " {" - " \"type\": \"SetValue\"," - " \"property\": \"text\"," - " \"value\": \"Hello 2\"," - " \"componentId\": \"text2\"" - " }" - " ]" - " }," - " {" - " \"type\": \"Idle\"," - " \"delay\": 1000" - " }," - " {" - " \"type\": \"SetValue\"," - " \"property\": \"backgroundColor\"," - " \"value\": \"red\"," - " \"componentId\": \"frame1\"" - " }," - " {" - " \"type\": \"Idle\"," - " \"delay\": 1000" - " }," - " {" - " \"type\": \"SetValue\"," - " \"property\": \"backgroundColor\"," - " \"value\": \"yellow\"," - " \"componentId\": \"frame2\"" - " }" - " ]" - " }" - " ]," - " \"width\": 100," - " \"height\": 100," - " \"item\": {" - " \"type\": \"Frame\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"backgroundColor\": \"green\"" - " }" - " }," - " {" - " \"type\": \"Container\"," - " \"direction\": \"column\"," - " \"items\": [" - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame1\"," - " \"backgroundColor\": \"yellow\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"Item 1\"," - " \"id\": \"text1\"" - " }" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame2\"," - " \"backgroundColor\": \"red\"," - " \"item\": {" - " \"type\": \"Text\"," - " \"text\": \"Item 2\"," - " \"id\": \"text2\"" - " }" - " }" - " ]" - " }" - " ]" - " }" - " }" - "}"; +const char *LARGE_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Container", + "direction": "row", + "items": [ + { + "type": "TouchWrapper", + "id": "myTouchWrapper", + "onPress": [ + { + "type": "Sequential", + "commands": [ + { + "type": "Parallel", + "commands": [ + { + "type": "SetValue", + "property": "text", + "value": "Hello 1", + "componentId": "text1" + }, + { + "type": "SetValue", + "property": "text", + "value": "Hello 2", + "componentId": "text2" + } + ] + }, + { + "type": "Idle", + "delay": 1000 + }, + { + "type": "SetValue", + "property": "backgroundColor", + "value": "red", + "componentId": "frame1" + }, + { + "type": "Idle", + "delay": 1000 + }, + { + "type": "SetValue", + "property": "backgroundColor", + "value": "yellow", + "componentId": "frame2" + } + ] + } + ], + "width": 100, + "height": 100, + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "green" + } + }, + { + "type": "Container", + "direction": "column", + "items": [ + { + "type": "Frame", + "id": "frame1", + "backgroundColor": "yellow", + "item": { + "type": "Text", + "text": "Item 1", + "id": "text1" + } + }, + { + "type": "Frame", + "id": "frame2", + "backgroundColor": "red", + "item": { + "type": "Text", + "text": "Item 2", + "id": "text2" + } + } + ] + } + ] + } + } +})"; TEST_F(CommandTest, ParallelSequentialMix) @@ -814,35 +813,33 @@ TEST_F(CommandTest, ParallelSequentialMix) } -static const char *REPEATED_SET_VALUE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": 100," - " \"height\": 100," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"Woof\"," - " \"id\": \"dogText\"" - " }," - " \"onPress\": {" - " \"type\": \"Sequential\"," - " \"repeatCount\": 6," - " \"commands\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"dogText\"," - " \"property\": \"opacity\"," - " \"value\": \"${event.target.opacity - 0.2}\"," - " \"delay\": 100" - " }" - " }" - " }" - " }" - "}"; +static const char *REPEATED_SET_VALUE = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": 100, + "height": 100, + "items": { + "type": "Text", + "text": "Woof", + "id": "dogText" + }, + "onPress": { + "type": "Sequential", + "repeatCount": 6, + "commands": { + "type": "SetValue", + "componentId": "dogText", + "property": "opacity", + "value": "${event.target.opacity - 0.2}", + "delay": 100 + } + } + } + } +})"; TEST_F(CommandTest, RepeatedSetValue) { @@ -860,32 +857,30 @@ TEST_F(CommandTest, RepeatedSetValue) } } -static const char *SET_STATE_DISABLED = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"SendEvent\"," - " \"arguments\": [" - " \"Sending\"" - " ]" - " }," - " {" - " \"type\": \"SetState\"," - " \"state\": \"disabled\"," - " \"value\": true" - " }" - " ]" - " }" - " }" - "}"; +static const char *SET_STATE_DISABLED = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "SendEvent", + "arguments": [ + "Sending" + ] + }, + { + "type": "SetState", + "state": "disabled", + "value": true + } + ] + } + } +})"; TEST_F(CommandTest, SetStateDisabled) { @@ -903,24 +898,22 @@ TEST_F(CommandTest, SetStateDisabled) ASSERT_FALSE(root->hasEvent()); } -static const char *SET_STATE_CHECKED = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": {" - " \"type\": \"SetState\"," - " \"state\": \"checked\"," - " \"value\": \"${!event.source.value}\"" - " }" - " }" - " }" - "}"; +static const char *SET_STATE_CHECKED = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": { + "type": "SetState", + "state": "checked", + "value": "${!event.source.value}" + } + } + } +})"; TEST_F(CommandTest, SetStateChecked) { @@ -942,45 +935,43 @@ TEST_F(CommandTest, SetStateChecked) ASSERT_TRUE(component->getState().get(kStateChecked)); } -static const char *SET_STATE_FOCUSED = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"Container\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"thing1\"," - " \"width\": 20," - " \"height\": 20," - " \"onPress\": {" - " \"type\": \"SetState\"," - " \"state\": \"focused\"," - " \"value\": true," - " \"componentId\": \"thing2\"" - " }" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"thing2\"," - " \"width\": 20," - " \"height\": 20," - " \"onPress\": {" - " \"type\": \"SetState\"," - " \"state\": \"focused\"," - " \"value\": true," - " \"componentId\": \"thing1\"" - " }" - " }" - " ]" - " }" - " }" - "}"; +static const char *SET_STATE_FOCUSED = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "TouchWrapper", + "id": "thing1", + "width": 20, + "height": 20, + "onPress": { + "type": "SetState", + "state": "focused", + "value": true, + "componentId": "thing2" + } + }, + { + "type": "TouchWrapper", + "id": "thing2", + "width": 20, + "height": 20, + "onPress": { + "type": "SetState", + "state": "focused", + "value": true, + "componentId": "thing1" + } + } + ] + } + } +})"; TEST_F(CommandTest, SetStateFocused) { @@ -1018,36 +1009,35 @@ TEST_F(CommandTest, SetStateFocused) ASSERT_EQ(thing2, event.getComponent()); } -static const char *SET_FOCUS_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touch1\"," - " \"height\": 10," - " \"onPress\": {" - " \"type\": \"SetFocus\"," - " \"componentId\": \"touch2\"" - " }" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touch2\"," - " \"height\": 10," - " \"onPress\": {" - " \"type\": \"SetFocus\"," - " \"componentId\": \"touch1\"" - " }" - " }" - " ]" - " }" - " }" - "}"; +static const char *SET_FOCUS_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "TouchWrapper", + "id": "touch1", + "height": 10, + "onPress": { + "type": "SetFocus", + "componentId": "touch2" + } + }, + { + "type": "TouchWrapper", + "id": "touch2", + "height": 10, + "onPress": { + "type": "SetFocus", + "componentId": "touch1" + } + } + ] + } + } +})"; TEST_F(CommandTest, SetFocus) { @@ -1078,35 +1068,34 @@ TEST_F(CommandTest, SetFocus) ASSERT_FALSE(root->hasEvent()); } -static const char *CLEAR_FOCUS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touch1\"," - " \"height\": 10," - " \"onPress\": {" - " \"type\": \"SetFocus\"," - " \"componentId\": \"touch2\"" - " }" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touch2\"," - " \"height\": 10," - " \"onPress\": {" - " \"type\": \"ClearFocus\"" - " }" - " }" - " ]" - " }" - " }" - "}"; +static const char *CLEAR_FOCUS = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "TouchWrapper", + "id": "touch1", + "height": 10, + "onPress": { + "type": "SetFocus", + "componentId": "touch2" + } + }, + { + "type": "TouchWrapper", + "id": "touch2", + "height": 10, + "onPress": { + "type": "ClearFocus" + } + } + ] + } + } +})"; TEST_F(CommandTest, ClearFocus) { @@ -1131,7 +1120,7 @@ TEST_F(CommandTest, ClearFocus) ASSERT_EQ(kEventTypeFocus, event.getType()); ASSERT_FALSE(event.getComponent().get()); ASSERT_FALSE(root->hasEvent()); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); root->clearPending(); // Hit it again @@ -1139,26 +1128,25 @@ TEST_F(CommandTest, ClearFocus) ASSERT_FALSE(root->hasEvent()); } -static const char *EXECUTE_FOCUS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touch1\"" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"touch2\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *EXECUTE_FOCUS = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "TouchWrapper", + "id": "touch1" + }, + { + "type": "TouchWrapper", + "id": "touch2" + } + ] + } + } +})"; TEST_F(CommandTest, ExecuteFocus) { @@ -1202,7 +1190,7 @@ TEST_F(CommandTest, ExecuteFocus) event = root->popEvent(); ASSERT_EQ(kEventTypeFocus, event.getType()); ASSERT_EQ(nullptr, event.getComponent().get()); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); root->clearPending(); ASSERT_FALSE(std::static_pointer_cast(touch1)->getState().get(kStateFocused)); ASSERT_FALSE(std::static_pointer_cast(touch2)->getState().get(kStateFocused)); @@ -1247,25 +1235,23 @@ TEST_F(CommandTest, ExecuteFocusDisabled) ASSERT_FALSE(std::static_pointer_cast(touch1)->getState().get(kStateFocused)); } -static const char *FINISH_BACK = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"Finish\"," - " \"reason\": \"back\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *FINISH_BACK = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "Finish", + "reason": "back" + } + ] + } + } +})"; TEST_F(CommandTest, FinishBack) { @@ -1278,25 +1264,23 @@ TEST_F(CommandTest, FinishBack) ASSERT_EQ(kEventReasonBack, event.getValue(kEventPropertyReason).asInt()); } -static const char *FINISH_EXIT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"Finish\"," - " \"reason\": \"exit\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *FINISH_EXIT = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "Finish", + "reason": "exit" + } + ] + } + } +})"; TEST_F(CommandTest, FinishExit) { @@ -1309,24 +1293,22 @@ TEST_F(CommandTest, FinishExit) ASSERT_EQ(kEventReasonExit, event.getValue(kEventPropertyReason).asInt()); } -static const char *FINISH_DEFAULT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"Finish\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *FINISH_DEFAULT = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "Finish" + } + ] + } + } +})"; TEST_F(CommandTest, FinishDefault) { @@ -1339,31 +1321,29 @@ TEST_F(CommandTest, FinishDefault) ASSERT_EQ(kEventReasonExit, event.getValue(kEventPropertyReason).asInt()); } -static const char *FINISH_COMMAND_LAST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"SendEvent\"," - " \"arguments\": [" - " \"Sending\"" - " ]" - " }," - " {" - " \"type\": \"Finish\"," - " \"reason\": \"back\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *FINISH_COMMAND_LAST = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "SendEvent", + "arguments": [ + "Sending" + ] + }, + { + "type": "Finish", + "reason": "back" + } + ] + } + } +})"; TEST_F(CommandTest, FinishCommandLast) { @@ -1379,31 +1359,29 @@ TEST_F(CommandTest, FinishCommandLast) ASSERT_EQ(kEventReasonBack, event.getValue(kEventPropertyReason).asInt()); } -static const char *FINISH_COMMAND_FIRST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"onPress\": [" - " {" - " \"type\": \"Finish\"," - " \"reason\": \"back\"" - " }," - " {" - " \"type\": \"SendEvent\"," - " \"arguments\": [" - " \"Sending\"" - " ]" - " }" - " ]" - " }" - " }" - "}"; +static const char *FINISH_COMMAND_FIRST = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "width": "100%", + "height": "100%", + "onPress": [ + { + "type": "Finish", + "reason": "back" + }, + { + "type": "SendEvent", + "arguments": [ + "Sending" + ] + } + ] + } + } +})"; TEST_F(CommandTest, FinishCommandFirst) { @@ -1419,20 +1397,18 @@ TEST_F(CommandTest, FinishCommandFirst) ASSERT_FALSE(root->hasEvent()); } -static const char *EXECUTE_FINISH = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"Frame\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"backgroundColor\": \"green\"" - " }" - " }" - "}"; +static const char *EXECUTE_FINISH = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "green" + } + } +})"; TEST_F(CommandTest, ExecuteFinishBack) { @@ -1487,37 +1463,37 @@ TEST_F(CommandTest, ExecuteFinishFastMode) } static const char *EXTERNAL_BINDING_UPDATE_TRANSFORM_DOCUMENT = R"({ - "type": "APL", - "version": "1.3", - "mainTemplate": { + "type": "APL", + "version": "1.3", + "mainTemplate": { + "items": [ + { + "type": "Container", + "id": "myContainer", + "width": "100%", + "height": "100%", + "bind": [ + { + "name": "len", + "value": 64, + "type": "dimension" + } + ], "items": [ { - "type": "Container", - "id": "myContainer", - "width": "100%", - "height": "100%", - "bind": [ + "type": "Text", + "text": "Some text.", + "transform": [ { - "name": "len", - "value": 64, - "type": "dimension" - } - ], - "items": [ - { - "type": "Text", - "text": "Some text.", - "transform": [ - { - "translateX": "${len}" - } - ] + "translateX": "${len}" } ] } ] } - })"; + ] + } +})"; TEST_F(CommandTest, BindingUpdateTransform){ loadDocument(EXTERNAL_BINDING_UPDATE_TRANSFORM_DOCUMENT); diff --git a/unit/command/unittest_serialize_event.cpp b/unit/command/unittest_serialize_event.cpp index 3d34058..da130d0 100644 --- a/unit/command/unittest_serialize_event.cpp +++ b/unit/command/unittest_serialize_event.cpp @@ -13,6 +13,9 @@ * permissions and limitations under the License. */ +#include +#include + #include "rapidjson/stringbuffer.h" #include "rapidjson/prettywriter.h" #include "apl/component/componenteventsourcewrapper.h" @@ -24,10 +27,11 @@ using namespace apl; -inline void dump(const Object& object) +template +inline void dump(const T& serializable) { rapidjson::Document doc; - auto json = object.serialize(doc.GetAllocator()); + auto json = serializable.serialize(doc.GetAllocator()); rapidjson::StringBuffer buffer; rapidjson::Writer writer(buffer); @@ -1122,6 +1126,7 @@ static const char* GRIDSEQ_MULTI_CHILD_DOC = R"({ TEST_F(SerializeEventTest, GridSequenceMultiChildEvent) { loadDocument(GRIDSEQ_MULTI_CHILD_DOC); + advanceTime(10); ASSERT_TRUE(component); ASSERT_EQ(kComponentTypeGridSequence, component->getType()); @@ -1129,6 +1134,7 @@ TEST_F(SerializeEventTest, GridSequenceMultiChildEvent) { // scroll to 10 component->update(kUpdateScrollPosition, 10); + advanceTime(10); ASSERT_TRUE(CheckValidate("gridScroll", R"( { "source":{ @@ -1285,7 +1291,7 @@ TEST_F(SerializeEventTest, VideoDocument) { ASSERT_FALSE(root->hasEvent()); // Start playing - auto state = MediaState(0, 3, 100, 1000, false, false) + auto state = MediaState(0, 3, 100, 1000, false, false, false) .withTrackState(kTrackReady); // Track 0 of 3, @100 ms of 1000 ms, not paused/ended, ready component->updateMediaState(state); @@ -1299,6 +1305,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -1328,6 +1335,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 100.0, "duration": 1000.0, "ended": false, + "muted": false, "paused": false, "source": { "bind": {}, @@ -1336,6 +1344,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -1366,6 +1375,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 100.0, "duration": 1000.0, "ended": false, + "muted": false, "paused": false, "source": { "bind": {}, @@ -1374,6 +1384,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "layoutDirection": "LTR", @@ -1401,7 +1412,7 @@ TEST_F(SerializeEventTest, VideoDocument) { ASSERT_FALSE(root->hasEvent()); // Move forward 100 milliseconds - state = MediaState(0, 3, 200, 1000, false, false) + state = MediaState(0, 3, 200, 1000, false, false, false) .withTrackState(kTrackReady); // Track 0 of 3, @200 ms of 1000 ms, not paused/ended and ready component->updateMediaState(state); @@ -1410,6 +1421,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 200.0, "duration": 1000.0, "ended": false, + "muted": false, "paused": false, "source": { "bind": {}, @@ -1418,6 +1430,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -1442,8 +1455,13 @@ TEST_F(SerializeEventTest, VideoDocument) { } )")); + // Mute the audio + state = MediaState(0, 3, 200, 1000, false, false, true) + .withTrackState(kTrackReady); // Track 0 of 3, @200 ms of 1000 ms, not paused/ended and ready + component->updateMediaState(state); + // Jump to the next track - state = MediaState(1, 3, 0, 1000, false, false) + state = MediaState(1, 3, 0, 1000, false, false, true) .withTrackState(kTrackNotReady); // Track 1 of 3, @0 ms of 1000 ms, not paused/ended, not ready component->updateMediaState(state); @@ -1453,6 +1471,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 0.0, "duration": 1000.0, "ended": false, + "muted": true, "paused": false, "source": { "bind": {}, @@ -1461,6 +1480,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": true, "focused": false, "height": 480.0, "id": "", @@ -1491,6 +1511,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 0.0, "duration": 1000.0, "ended": false, + "muted": true, "paused": false, "source": { "bind": {}, @@ -1499,6 +1520,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": true, "focused": false, "height": 480.0, "id": "", @@ -1524,7 +1546,7 @@ TEST_F(SerializeEventTest, VideoDocument) { )")); // Pause the video playback - state = MediaState(1, 3, 0, 1000, true, false); // Track 1 of 3, @0 ms of 1000 ms, paused/not ended, not ready + state = MediaState(1, 3, 0, 1000, true, false, false); // Track 1 of 3, @0 ms of 1000 ms, paused/not ended, not ready component->updateMediaState(state); ASSERT_TRUE(CheckValidate("pauseit", R"( @@ -1532,6 +1554,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 0.0, "duration": 1000.0, "ended": false, + "muted": false, "paused": true, "source": { "bind": {}, @@ -1540,6 +1563,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -1565,7 +1589,7 @@ TEST_F(SerializeEventTest, VideoDocument) { )")); // Track gets ready at paused state - state = MediaState(1, 3, 0, 1000, true, false) + state = MediaState(1, 3, 0, 1000, true, false, false) .withTrackState(kTrackReady); // Track 1 of 3, @0 ms of 1000 ms, paused/not ended, ready component->updateMediaState(state); @@ -1578,6 +1602,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -1602,7 +1627,7 @@ TEST_F(SerializeEventTest, VideoDocument) { )")); // Error occurred while playing track - state = MediaState(1, 3, 500, 1000, false, false) + state = MediaState(1, 3, 500, 1000, false, false, false) .withTrackState(kTrackFailed) .withErrorCode(99); // Track 1 of 3, @500 ms of 1000 ms, not paused/not ended and not ready component->updateMediaState(state); @@ -1618,6 +1643,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": false, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -1642,7 +1668,7 @@ TEST_F(SerializeEventTest, VideoDocument) { )")); // End the video playback - state = MediaState(1,3,500,1000,false,true); + state = MediaState(1,3,500,1000,false,true,false); component->updateMediaState(state); ASSERT_TRUE(CheckValidate("endit", R"( @@ -1650,6 +1676,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "currentTime": 500.0, "duration": 1000.0, "ended": true, + "muted": false, "paused": false, "source": { "bind": {}, @@ -1658,6 +1685,7 @@ TEST_F(SerializeEventTest, VideoDocument) { "disabled": false, "duration": 1000.0, "ended": true, + "muted": false, "focused": false, "height": 480.0, "id": "", @@ -2936,6 +2964,7 @@ TEST_F(SerializeEventTest, TargetVideo) { "disabled": false, "duration": 0.0, "ended": false, + "muted": false, "focused": false, "height": 100.0, "id": "MyTarget", @@ -3114,7 +3143,7 @@ static const char * OPEN_URL_EVENT = R"( * still need to test it. */ TEST_F(SerializeEventTest, OpenURL) { - config->allowOpenUrl(true); + config->set(RootProperty::kAllowOpenUrl, true); loadDocument(OPEN_URL_EVENT); ASSERT_TRUE(component); @@ -3185,7 +3214,7 @@ static const char * BIND_REFERENCES = R"( )"; TEST_F(SerializeEventTest, BindReferences) { - config->allowOpenUrl(true); + config->set(RootProperty::kAllowOpenUrl, true); loadDocument(BIND_REFERENCES); ASSERT_TRUE(component); @@ -3414,4 +3443,100 @@ TEST_F(SerializeEventTest, EditTextRTL) { } } )")); +} + +static const char * IMAGES = R"( + { + "type": "APL", + "version": "2022.1", + "mainTemplate": { + "items": { + "type": "Container", + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Image", + "width": "25%", + "height": "25%", + "source": "source0" + }, + { + "type": "Image", + "width": "25%", + "height": "25%", + "source": { + "url": "source1", + "headers": ["a: header1"] + } + }, + { + "type": "Image", + "width": "25%", + "height": "25%", + "sources": [ + { + "url": "source2", + "headers": ["a: header2"] + }, + { + "url": "source3", + "headers": ["a: header3.1", "a: header3.2"] + } + ] + } + ] + } + } + } +)"; + +TEST_F(SerializeEventTest, MediaRequest) { + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureManageMediaRequests); + + loadDocument(IMAGES); + ASSERT_TRUE(component); + + ASSERT_TRUE(root->hasEvent()); + auto event = root->popEvent(); + + rapidjson::Document doc; + auto serialized = event.serialize(doc.GetAllocator()); + dump(event); + ASSERT_TRUE(serialized.HasMember("type")); + ASSERT_TRUE(serialized.HasMember("headers")); + ASSERT_TRUE(serialized.HasMember("source")); + + ASSERT_EQ((int) apl::kEventTypeMediaRequest, serialized["type"].GetInt()); + + // Check that the source array contains ["source0", "source1", "source3"] (in any order) + const auto& source = serialized["source"]; + ASSERT_TRUE(source.IsArray()); + ASSERT_EQ(3, source.GetArray().Size()); + + std::set expectedSources = {"source0", "source1", "source3"}; + std::vector expectedHeaders; + for (const auto& url : source.GetArray()) { + ASSERT_TRUE(url.IsString()); + const auto* urlString = url.GetString(); + if (expectedSources.count(urlString) > 0) { + expectedSources.erase(urlString); + if (std::strcmp(urlString, "source0") == 0) { + expectedHeaders.emplace_back("[]"); + } else if (std::strcmp(urlString, "source1") == 0) { + expectedHeaders.emplace_back(R"(["a: header1"])"); + } else { + expectedHeaders.emplace_back(R"(["a: header3.1", "a: header3.2"])"); + } + } + } + ASSERT_TRUE(expectedSources.empty()); + + // Check that the headers match the source array + const auto& headers = serialized["headers"]; + ASSERT_EQ(3, headers.GetArray().Size()); + int headerIndex = 0; + for (const auto& header : headers.GetArray()) { + ASSERT_TRUE(CompareValue(header, expectedHeaders[headerIndex++].c_str())); + } } \ No newline at end of file diff --git a/unit/component/unittest_bounds.cpp b/unit/component/unittest_bounds.cpp index 0aa3092..0c94963 100644 --- a/unit/component/unittest_bounds.cpp +++ b/unit/component/unittest_bounds.cpp @@ -361,8 +361,8 @@ TEST_F(BoundsTest, ChildInParent) // Sanity test some binding logic auto context = Context::createTestContext(Metrics(), makeDefaultSession()); - ASSERT_EQ(StyledText::create(*context, "3"), sequence->getChildAt(2)->getChildAt(0)->getCalculated(kPropertyText)); - ASSERT_EQ(StyledText::create(*context, "Turtle"), sequence->getChildAt(2)->getChildAt(1)->getCalculated(kPropertyText)); + ASSERT_TRUE(IsEqual(StyledText::create(*context, "3"), sequence->getChildAt(2)->getChildAt(0)->getCalculated(kPropertyText))); + ASSERT_TRUE(IsEqual(StyledText::create(*context, "Turtle"), sequence->getChildAt(2)->getChildAt(1)->getCalculated(kPropertyText))); } static const char *NESTED_CHILD = R"({ diff --git a/unit/component/unittest_component_events.cpp b/unit/component/unittest_component_events.cpp index 83d33d6..7f0e3d4 100644 --- a/unit/component/unittest_component_events.cpp +++ b/unit/component/unittest_component_events.cpp @@ -385,7 +385,7 @@ TEST_F(ComponentEventsTest, MediaStateChanges) ASSERT_EQ("One", text->getCalculated(kPropertyText).asString()); // Simulate playback start - state = MediaState(0, 2, 7, 10, false, false); + state = MediaState(0, 2, 7, 10, false, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -393,7 +393,7 @@ TEST_F(ComponentEventsTest, MediaStateChanges) ASSERT_EQ("PLAY", text->getCalculated(kPropertyText).asString()); // Simulate playback pause - state = MediaState(0, 2, 7, 10, true, false); + state = MediaState(0, 2, 7, 10, true, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -401,7 +401,7 @@ TEST_F(ComponentEventsTest, MediaStateChanges) ASSERT_EQ("PAUSE", text->getCalculated(kPropertyText).asString()); // Simulate track change - state = MediaState(1, 2, 7, 10, true, false); + state = MediaState(1, 2, 7, 10, true, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -409,7 +409,7 @@ TEST_F(ComponentEventsTest, MediaStateChanges) ASSERT_EQ("TRACK_UPDATE", text->getCalculated(kPropertyText).asString()); // Simulate playback end - state = MediaState(1, 2, 7, 10, true, true); + state = MediaState(1, 2, 7, 10, true, true, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -433,7 +433,7 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) MediaState state; // Simulate playback error while trying to start first track - state = MediaState(0, 3, 0, 0, true, false) + state = MediaState(0, 3, 0, 0, true, false, false) .withTrackState(kTrackFailed) .withErrorCode(99); video->updateMediaState(state); @@ -444,13 +444,13 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) /* * Simulate playback error while playing */ - state = MediaState(0, 3, 7, 10, false, false); + state = MediaState(0, 3, 7, 10, false, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ("PLAY", text->getCalculated(kPropertyText).asString()); // Track is now playing // Update state as error - state = MediaState(0, 3, 7, 10, false, false) + state = MediaState(0, 3, 7, 10, false, false, false) .withTrackState(kTrackFailed) .withErrorCode(99); video->updateMediaState(state); @@ -458,13 +458,13 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) loop->advanceToEnd(); ASSERT_EQ(std::to_string(state.getErrorCode()), text->getCalculated(kPropertyText).asString()); // Advance to next track from error state - state = MediaState(1, 3, 0, 10, false, false); + state = MediaState(1, 3, 0, 10, false, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ("TRACK_UPDATE", text->getCalculated(kPropertyText).asString()); // Update state as error when playing second track - state = MediaState(1, 3, 5, 10, false, false) + state = MediaState(1, 3, 5, 10, false, false, false) .withTrackState(kTrackFailed) .withErrorCode(100); video->updateMediaState(state); @@ -472,7 +472,7 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) loop->advanceToEnd(); ASSERT_EQ(std::to_string(state.getErrorCode()), text->getCalculated(kPropertyText).asString()); // Update state as error when moving to third track - state = MediaState(2, 3, 0, 0, false, false) + state = MediaState(2, 3, 0, 0, false, false, false) .withTrackState(kTrackFailed) .withErrorCode(101); video->updateMediaState(state); @@ -895,7 +895,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) ASSERT_FALSE(root->hasEvent()); // Simulate track ready - state = MediaState(0, 2, 7, 10, true, false).withTrackState(kTrackReady); + state = MediaState(0, 2, 7, 10, true, false, false).withTrackState(kTrackReady); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -903,7 +903,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) validateMediaEvent(root, "TrackReady", "0", "true", "false", "URL1", "ready", "", Object::NULL_OBJECT()); // Simulate playback start - state = MediaState(0, 2, 7, 10, false, false).withTrackState(kTrackReady); + state = MediaState(0, 2, 7, 10, false, false, false).withTrackState(kTrackReady); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -911,7 +911,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) validateMediaEvent(root, "Play", "0", "false", "false", "URL1", "ready", "", Object::NULL_OBJECT()); // Simulate playback pause - state = MediaState(0, 2, 7, 10, true, false).withTrackState(kTrackReady); + state = MediaState(0, 2, 7, 10, true, false, false).withTrackState(kTrackReady); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -919,7 +919,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) validateMediaEvent(root, "Pause", "0", "true", "false", "URL1", "ready", "", Object::NULL_OBJECT()); // Simulate track change - state = MediaState(1, 2, 7, 10, true, false).withTrackState(kTrackNotReady); + state = MediaState(1, 2, 7, 10, true, false, false).withTrackState(kTrackNotReady); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd();// @@ -927,7 +927,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) validateMediaEvent(root, "TrackUpdate", "1", "true", "false", "URL2", "notReady", "", Object(1)); // Simulate next track ready - state = MediaState(1, 2, 7, 10, true, false).withTrackState(kTrackReady); + state = MediaState(1, 2, 7, 10, true, false, false).withTrackState(kTrackReady); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd();// @@ -935,7 +935,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) validateMediaEvent(root, "TrackReady", "1", "true", "false", "URL2", "ready", "", Object::NULL_OBJECT()); // Simulate playback end - state = MediaState(1, 2, 7, 10, true, true).withTrackState(kTrackReady); + state = MediaState(1, 2, 7, 10, true, true, false).withTrackState(kTrackReady); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); @@ -943,7 +943,7 @@ TEST_F(ComponentEventsTest, MediaSendEvent) validateMediaEvent(root, "End", "1", "true", "true", "URL2", "ready", "", Object::NULL_OBJECT()); // Simulate playback error - state = MediaState(1, 2, 7, 10, true, true) + state = MediaState(1, 2, 7, 10, true, true, false) .withTrackState(kTrackFailed) .withErrorCode(99); video->updateMediaState(state); @@ -1015,7 +1015,7 @@ TEST_F(ComponentEventsTest, MediaOnTimeUpdate) ASSERT_FALSE(root->hasEvent()); // Simulate time update - state = MediaState(0, 1, 5, 10, false, false); + state = MediaState(0, 1, 5, 10, false, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); @@ -1113,7 +1113,7 @@ TEST_F(ComponentEventsTest, MediaFastNormal) // Simulate time update ASSERT_FALSE(ConsoleMessage()); - MediaState state(0, 1, 5, 10, false, false); + MediaState state(0, 1, 5, 10, false, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); @@ -1126,7 +1126,7 @@ TEST_F(ComponentEventsTest, MediaFastNormal) ASSERT_EQ("Two", text->getCalculated(kPropertyText).asString()); // Simulate pause from event - state = MediaState(0, 1, 5, 10, true, false); + state = MediaState(0, 1, 5, 10, true, false, false); video->updateMediaState(state, true); CheckMediaState(state, video->getCalculated()); @@ -1138,7 +1138,7 @@ TEST_F(ComponentEventsTest, MediaFastNormal) ASSERT_EQ("Three", text->getCalculated(kPropertyText).asString()); // Switch back to play state - state = MediaState(0, 1, 5, 10, false, false); + state = MediaState(0, 1, 5, 10, false, false, false); video->updateMediaState(state, true); CheckMediaState(state, video->getCalculated()); @@ -1150,7 +1150,7 @@ TEST_F(ComponentEventsTest, MediaFastNormal) ASSERT_EQ("One", text->getCalculated(kPropertyText).asString()); // Pause by user event - state = MediaState(0, 1, 5, 10, true, false); + state = MediaState(0, 1, 5, 10, true, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); diff --git a/unit/component/unittest_dynamic_properties.cpp b/unit/component/unittest_dynamic_properties.cpp index 2339cd0..b08de84 100644 --- a/unit/component/unittest_dynamic_properties.cpp +++ b/unit/component/unittest_dynamic_properties.cpp @@ -811,7 +811,8 @@ TEST_F(DynamicPropertiesTest, BorderWidth) { frame1->setProperty(kPropertyBorderWidth, 10); ASSERT_EQ(1, root->getDirty().size()); - ASSERT_TRUE(CheckDirty(frame1, kPropertyBorderWidth, kPropertyInnerBounds, kPropertyNotifyChildrenChanged, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(frame1, kPropertyBorderWidth, kPropertyInnerBounds, kPropertyNotifyChildrenChanged, + kPropertyVisualHash, kPropertyDrawnBorderWidth)); ASSERT_TRUE(CheckDirty(root, frame1)); root->clearDirty(); @@ -974,8 +975,8 @@ TEST_F(DynamicPropertiesTest, ImageProperties) { auto grad1 = img1->getCalculated(kPropertyOverlayGradient); ASSERT_TRUE(grad1.isGradient()); - ASSERT_EQ(Object(Color(Color::BLUE)), grad1.getGradient().getColorRange().at(0)); - ASSERT_EQ(Object(Color(Color::RED)), grad1.getGradient().getColorRange().at(1)); + ASSERT_EQ(Object(Color(Color::BLUE)), grad1.getGradient().getProperty(kGradientPropertyColorRange).at(0)); + ASSERT_EQ(Object(Color(Color::RED)), grad1.getGradient().getProperty(kGradientPropertyColorRange).at(1)); // Set aline property of img img1->setProperty(kPropertyAlign, "left"); @@ -1020,8 +1021,8 @@ TEST_F(DynamicPropertiesTest, ImageProperties) { grad1 = img1->getCalculated(kPropertyOverlayGradient); ASSERT_TRUE(grad1.isGradient()); - ASSERT_EQ(Object(Color(Color::GREEN)), grad1.getGradient().getColorRange().at(0)); - ASSERT_EQ(Object(Color(Color::GRAY)), grad1.getGradient().getColorRange().at(1)); + ASSERT_EQ(Object(Color(Color::GREEN)), grad1.getGradient().getProperty(kGradientPropertyColorRange).at(0)); + ASSERT_EQ(Object(Color(Color::GRAY)), grad1.getGradient().getProperty(kGradientPropertyColorRange).at(1)); } static const char *VECTOR_GRAPHIC_SETVALUE = R"apl( @@ -1279,7 +1280,8 @@ TEST_F(DynamicPropertiesTest, EditTextProperties) { txt->setProperty(kPropertyBorderWidth, 5); ASSERT_EQ(1, root->getDirty().size()); - ASSERT_TRUE(CheckDirty(txt, kPropertyBorderWidth, kPropertyInnerBounds, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(txt, kPropertyBorderWidth, kPropertyInnerBounds, kPropertyVisualHash, + kPropertyDrawnBorderWidth)); ASSERT_TRUE(CheckDirty(root, txt)); root->clearDirty(); ASSERT_TRUE(CheckProperties(txt, { diff --git a/unit/component/unittest_edit_text_component.cpp b/unit/component/unittest_edit_text_component.cpp index b07a0ff..0321a0b 100644 --- a/unit/component/unittest_edit_text_component.cpp +++ b/unit/component/unittest_edit_text_component.cpp @@ -44,11 +44,11 @@ TEST_F(EditTextComponentTest, ComponentDefaults) { ASSERT_EQ(kComponentTypeEditText, et->getType()); ASSERT_TRUE(IsEqual(Color(Color::TRANSPARENT), et->getCalculated(kPropertyBorderColor))); - // when not set kPropertyBorderStrokeWidth is initialized from kPropertyBorderWidth - ASSERT_TRUE(IsEqual(et->getCalculated(kPropertyBorderWidth), et->getCalculated(kPropertyBorderStrokeWidth))); + + ASSERT_TRUE(et->getCalculated(kPropertyBorderStrokeWidth).isNull()); ASSERT_TRUE(IsEqual(Dimension(0), et->getCalculated(kPropertyBorderWidth))); - // kPropertyDrawnBorderWidth is calculated from kPropertyBorderStrokeWidth (inputOnly) and (kPropertyBorderWidth) ASSERT_TRUE(IsEqual(Dimension(0), et->getCalculated(kPropertyDrawnBorderWidth))); + ASSERT_TRUE(IsEqual(Color(0xfafafaff), et->getCalculated(kPropertyColor))); ASSERT_TRUE(IsEqual("sans-serif", et->getCalculated(kPropertyFontFamily))); ASSERT_TRUE(IsEqual(Dimension(40), et->getCalculated(kPropertyFontSize))); @@ -369,6 +369,8 @@ static const char* INVALID_CHARACTER_RANGES_DOC = R"({ } })"; +// The range "Q--" is U+0051 through U+002D +// Since the range is reversed, we accept the Q, but ignore everything else TEST_F(EditTextComponentTest, InvalidCharacterRanges) { loadDocument(INVALID_CHARACTER_RANGES_DOC); @@ -377,10 +379,11 @@ TEST_F(EditTextComponentTest, InvalidCharacterRanges) { ASSERT_EQ(kComponentTypeEditText, pEditText->getType()); // everything should be valid - ASSERT_TRUE(pEditText->isCharacterValid(L'\u2192')); - ASSERT_TRUE(pEditText->isCharacterValid(L'-')); - ASSERT_TRUE(pEditText->isCharacterValid(L'A')); - ASSERT_TRUE(pEditText->isCharacterValid(L'0')); + ASSERT_FALSE(pEditText->isCharacterValid(L'\u2192')); + ASSERT_FALSE(pEditText->isCharacterValid(L'-')); + ASSERT_TRUE(pEditText->isCharacterValid(L'Q')); + ASSERT_FALSE(pEditText->isCharacterValid(L'A')); + ASSERT_FALSE(pEditText->isCharacterValid(L'0')); session->clear(); } @@ -396,6 +399,7 @@ static const char* INVALID_DASH_CHARACTER_RANGES_DOC = R"({ } })"; +// The range is 0-9, a-y, A-Y, '-' to '@' (that's U+002D through U+0040) TEST_F(EditTextComponentTest, InvalidDashCharacterRanges) { loadDocument(INVALID_DASH_CHARACTER_RANGES_DOC); @@ -404,11 +408,14 @@ TEST_F(EditTextComponentTest, InvalidDashCharacterRanges) { ASSERT_EQ(kComponentTypeEditText, pEditText->getType()); // everything should be valid - ASSERT_TRUE(pEditText->isCharacterValid(L'\u2192')); + ASSERT_FALSE(pEditText->isCharacterValid(L'\u2192')); ASSERT_TRUE(pEditText->isCharacterValid(L'-')); ASSERT_TRUE(pEditText->isCharacterValid(L'A')); + ASSERT_FALSE(pEditText->isCharacterValid(L'Z')); ASSERT_TRUE(pEditText->isCharacterValid(L'0')); - ASSERT_TRUE(pEditText->isCharacterValid(L'}')); + ASSERT_TRUE(pEditText->isCharacterValid(L'?')); // U+003F + ASSERT_TRUE(pEditText->isCharacterValid(L'@')); // U+0040 + ASSERT_FALSE(pEditText->isCharacterValid(L'}')); // U+007D session->clear(); } @@ -525,15 +532,6 @@ static const char* BORDER_STROKE_CLAMP_DOC = R"({ } })"; -static const char * SET_VALUE_STROKEWIDTH_COMMAND = R"([ - { - "type": "SetValue", - "componentId": "myEditText", - "property": "borderStrokeWidth", - "value": "17" - } -])"; - /** * Test the setting of all properties to non default values. */ @@ -553,11 +551,15 @@ TEST_F(EditTextComponentTest, ClampDrawnBorder) { // execute command to set kPropertyBorderStrokeWidth within border bounds, // the drawn border should update - auto doc = rapidjson::Document(); - doc.Parse(SET_VALUE_STROKEWIDTH_COMMAND); - auto action = root->executeCommands(apl::Object(std::move(doc)), false); + executeCommand("SetValue", {{"componentId", "myEditText"}, {"property", "borderStrokeWidth"}, {"value", 17}}, false); ASSERT_TRUE(IsEqual(Dimension(17), et->getCalculated(kPropertyBorderStrokeWidth))); ASSERT_TRUE(IsEqual(Dimension(17), et->getCalculated(kPropertyDrawnBorderWidth))); + + // execute command to set kPropertyBorderWidth to something smaller. Drawn border width should update + executeCommand("SetValue", {{"componentId", "myEditText"}, {"property", "borderWidth"}, {"value", 5}}, false); + ASSERT_TRUE(IsEqual(Dimension(5), et->getCalculated(kPropertyBorderWidth))); + ASSERT_TRUE(IsEqual(Dimension(17), et->getCalculated(kPropertyBorderStrokeWidth))); + ASSERT_TRUE(IsEqual(Dimension(5), et->getCalculated(kPropertyDrawnBorderWidth))); } static const char* HANDLERS_DOC = R"({ @@ -869,6 +871,66 @@ TEST_F(EditTextComponentTest, OpenKeyboardEventOnFocus) { ASSERT_EQ(edittext, event.getComponent()); } +static const char *VALID_CHARACTERS = R"apl( +{ + "type": "APL", + "version": "1.8", + "styles": { + "base": { + "values": [ + { + "validCharacters": " 0-9a-z" + }, + { + "when": "${state.checked}", + "validCharacters": " 0-9a-nA-Z" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "EditText", + "style": "base", + "id": "TEST", + "text": "ab43,5%" + } + } +} +)apl"; + +TEST_F(EditTextComponentTest, ValidCharacters) { + loadDocument(VALID_CHARACTERS); + + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual("ab435", component->getCalculated(kPropertyText))); + + executeCommand("SetValue", {{"componentId", "TEST"}, {"property", "text"}, {"value", "##4"}}, + false); + ASSERT_TRUE(IsEqual("4", component->getCalculated(kPropertyText))); + + executeCommand( + "SetValue", + {{"componentId", "TEST"}, {"property", "text"}, {"value", "Now it is 12:00 O'Clock"}}, + false); + ASSERT_TRUE(IsEqual("ow it is 1200 lock", component->getCalculated(kPropertyText))); + + // Change the state - that will switch the valid characters + component->setState(apl::kStateChecked, true); + ASSERT_TRUE(IsEqual(" i i 1200 lck", component->getCalculated(kPropertyText))); + + // Set the same string again; a new set results + executeCommand( + "SetValue", + {{"componentId", "TEST"}, {"property", "text"}, {"value", "Now it is 12:00 O'Clock"}}, + false); + ASSERT_TRUE(IsEqual("N i i 1200 OClck", component->getCalculated(kPropertyText))); + + // Unset the state + component->setState(kStateChecked, false); + ASSERT_TRUE(IsEqual(" i i 1200 lck", component->getCalculated(kPropertyText))); +} + TEST_F(EditTextComponentTest, NoOpWhenAlreadyInFocus) { config->enableExperimentalFeature(apl::RootConfig::kExperimentalFeatureFocusEditTextOnTap); @@ -887,4 +949,4 @@ TEST_F(EditTextComponentTest, NoOpWhenAlreadyInFocus) { ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(0, 0), false)); loop->advanceToEnd(); ASSERT_FALSE(root->hasEvent()); -} \ No newline at end of file +} diff --git a/unit/component/unittest_find_component_at_position.cpp b/unit/component/unittest_find_component_at_position.cpp index 8df023e..2c7df76 100644 --- a/unit/component/unittest_find_component_at_position.cpp +++ b/unit/component/unittest_find_component_at_position.cpp @@ -164,7 +164,7 @@ static const char *SEQUENCE_WITH_PADDING = R"({ TEST_F(FindComponentAtPosition, SequenceWithPadding) { // Force loading of all items we are looking at to simplify testing. - config->sequenceChildCache(5); + config->set(RootProperty::kSequenceChildCache, 5); loadDocument(SEQUENCE_WITH_PADDING); ASSERT_TRUE(component); @@ -186,6 +186,7 @@ TEST_F(FindComponentAtPosition, SequenceWithPadding) // Scroll up component->update(kUpdateScrollPosition, 20); + advanceTime(10); ASSERT_EQ(component->getChildAt(1), component->findComponentAtPosition(Point(50, 0))); ASSERT_EQ(component->getChildAt(2), component->findComponentAtPosition(Point(50, 10))); ASSERT_EQ(component->getChildAt(3), component->findComponentAtPosition(Point(50, 20))); @@ -195,6 +196,7 @@ TEST_F(FindComponentAtPosition, SequenceWithPadding) // Maximum scroll (there are 6 children for a total child height of 60, plus 20 units // of padding in a container of height 40). component->update(kUpdateScrollPosition, 40); + advanceTime(10); ASSERT_EQ(component->getChildAt(3), component->findComponentAtPosition(Point(50, 0))); ASSERT_EQ(component->getChildAt(4), component->findComponentAtPosition(Point(50, 10))); ASSERT_EQ(component->getChildAt(5), component->findComponentAtPosition(Point(50, 20))); @@ -239,7 +241,7 @@ static const char *GRID_SEQUENCE_WITH_PADDING = R"({ TEST_F(FindComponentAtPosition, GridSequenceWithPadding) { // Force loading of all items we are looking at to simplify testing. - config->sequenceChildCache(10); + config->set(RootProperty::kSequenceChildCache, 10); loadDocument(GRID_SEQUENCE_WITH_PADDING); ASSERT_TRUE(component); @@ -260,6 +262,7 @@ TEST_F(FindComponentAtPosition, GridSequenceWithPadding) // Scroll down component->update(kUpdateScrollPosition, 20); + advanceTime(10); ASSERT_EQ(component->getChildAt(2), component->findComponentAtPosition(Point(15, 15))); ASSERT_EQ(component->getChildAt(3), component->findComponentAtPosition(Point(40, 15))); ASSERT_EQ(component->getChildAt(4), component->findComponentAtPosition(Point(15, 40))); @@ -267,7 +270,7 @@ TEST_F(FindComponentAtPosition, GridSequenceWithPadding) // Scroll down component->update(kUpdateScrollPosition, 40); - + advanceTime(10); ASSERT_EQ(component->getChildAt(4), component->findComponentAtPosition(Point(15, 15))); ASSERT_EQ(component->getChildAt(5), component->findComponentAtPosition(Point(40, 15))); ASSERT_EQ(component->getChildAt(6), component->findComponentAtPosition(Point(15, 40))); diff --git a/unit/component/unittest_flexbox.cpp b/unit/component/unittest_flexbox.cpp index 2f5514a..17acc19 100644 --- a/unit/component/unittest_flexbox.cpp +++ b/unit/component/unittest_flexbox.cpp @@ -1163,7 +1163,7 @@ const static char *SPACED_SEQUENCE = TEST_F(FlexboxTest, SequenceWithSpacingTest) { - config->sequenceChildCache(2); + config->set(RootProperty::kSequenceChildCache, 2); loadDocument(SPACED_SEQUENCE); advanceTime(10); ASSERT_EQ(Rect(0,0,1024,800), component->getCalculated(kPropertyBounds).getRect()); @@ -1181,7 +1181,7 @@ TEST_F(FlexboxTest, SequenceWithSpacingTest) TEST_F(FlexboxTest, SequenceWithSpacingTestEnsureJump) { - config->sequenceChildCache(2); + config->set(RootProperty::kSequenceChildCache, 2); loadDocument(SPACED_SEQUENCE); advanceTime(10); ASSERT_EQ(Rect(0,0,1024,800), component->getCalculated(kPropertyBounds).getRect()); diff --git a/unit/component/unittest_frame_component.cpp b/unit/component/unittest_frame_component.cpp index 16d7bd7..27d1ea9 100644 --- a/unit/component/unittest_frame_component.cpp +++ b/unit/component/unittest_frame_component.cpp @@ -54,10 +54,8 @@ TEST_F(FrameComponentTest, ComponentDefaults) { // kpropertyBorderRadii is calculated from all kpropertyBorderXXXRadius values ASSERT_TRUE(IsEqual(Object::EMPTY_RADII(), frame->getCalculated(kPropertyBorderRadii))); - // when not set kPropertyBorderStrokeWidth is initialized from kPropertyBorderWidth ASSERT_TRUE(IsEqual(Dimension(0), frame->getCalculated(kPropertyBorderWidth))); - ASSERT_TRUE(IsEqual(frame->getCalculated(kPropertyBorderWidth), frame->getCalculated(kPropertyBorderStrokeWidth))); - // kPropertyDrawnBorderWidth is calculated from kPropertyBorderStrokeWidth (inputOnly) and (kPropertyBorderWidth) + ASSERT_TRUE(frame->getCalculated(kPropertyBorderStrokeWidth).isNull()); ASSERT_TRUE(IsEqual(Dimension(0), frame->getCalculated(kPropertyDrawnBorderWidth))); // Should not have scrollable moves @@ -159,15 +157,6 @@ static const char* BORDER_STROKE_CLAMP_DOC = R"({ } })"; -static const char* SET_VALUE_STROKEWIDTH_COMMAND = R"([ - { - "type": "SetValue", - "componentId": "myFrame", - "property": "borderStrokeWidth", - "value": "17" - } -])"; - /** * Test the drawn border is clamped to the min of borderWidth and borderStrokeWidth. */ @@ -187,11 +176,15 @@ TEST_F(FrameComponentTest, ClampDrawnBorder) { // execute command to set kPropertyBorderStrokeWidth within border bounds, // the drawn border should update - auto doc = rapidjson::Document(); - doc.Parse(SET_VALUE_STROKEWIDTH_COMMAND); - auto action = root->executeCommands(apl::Object(std::move(doc)), false); + executeCommand("SetValue", {{"componentId", "myFrame"}, {"property", "borderStrokeWidth"}, {"value", 17}}, false); ASSERT_TRUE(IsEqual(Dimension(17), frame->getCalculated(kPropertyBorderStrokeWidth))); ASSERT_TRUE(IsEqual(Dimension(17), frame->getCalculated(kPropertyDrawnBorderWidth))); + + // execute command to set kPropertyBorderWidth to something smaller. Drawn border width should update + executeCommand("SetValue", {{"componentId", "myFrame"}, {"property", "borderWidth"}, {"value", 5}}, false); + ASSERT_TRUE(IsEqual(Dimension(5), frame->getCalculated(kPropertyBorderWidth))); + ASSERT_TRUE(IsEqual(Dimension(17), frame->getCalculated(kPropertyBorderStrokeWidth))); + ASSERT_TRUE(IsEqual(Dimension(5), frame->getCalculated(kPropertyDrawnBorderWidth))); } diff --git a/unit/component/unittest_grid_sequence_component.cpp b/unit/component/unittest_grid_sequence_component.cpp index 37a738c..1c2431f 100644 --- a/unit/component/unittest_grid_sequence_component.cpp +++ b/unit/component/unittest_grid_sequence_component.cpp @@ -993,6 +993,7 @@ TEST_F(GridSequenceComponentTest, GridSequenceScrollingContext) ASSERT_TRUE(CheckChildrenLaidOut(component, {17, 19}, false)); completeScroll(component, 3); + advanceTime(10); scrollPosition = component->getCalculated(kPropertyScrollPosition).asNumber(); ASSERT_EQ(600, scrollPosition); @@ -1187,6 +1188,7 @@ TEST_F(GridSequenceComponentTest, GridSequenceScrollingContextRTL) ASSERT_TRUE(CheckChildrenLaidOut(component, {17, 19}, false)); completeScroll(component, 3); + advanceTime(10); scrollPosition = component->getCalculated(kPropertyScrollPosition).asNumber(); ASSERT_EQ(-600, scrollPosition); diff --git a/unit/component/unittest_layout_direction.cpp b/unit/component/unittest_layout_direction.cpp index f75e0fb..4b03129 100644 --- a/unit/component/unittest_layout_direction.cpp +++ b/unit/component/unittest_layout_direction.cpp @@ -1008,4 +1008,66 @@ TEST_F(LayoutDirectionText, LayoutDirectionEnvironmentValue) auto ld2 = static_cast(component->getChildAt(0)->getCalculated(kPropertyLayoutDirection).asInt()); ASSERT_EQ(ld2, kLayoutDirectionLTR); -} \ No newline at end of file +} + +static const char *CHILD_RTL_IN_PARENT_LTR = R"( +{ + "version": "1.8", + "type": "APL", + "mainTemplate": { + "item": { + "type": "GridSequence", + "scrollDirection": "horizontal", + "childWidth": 120, + "childHeight": 120, + "height": "100%", + "width": "100%", + "items": [ + { + "type": "Frame", + "borderColor": "red", + "borderWidth": 2, + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "item": { + "type": "Sequence", + "layoutDirection": "RTL", + "height": "100%", + "width": 120, + "items": [ + { + "type": "Text", + "text": "text" + } + ] + } + } + } + ] + } + } +} +)"; + +TEST_F(LayoutDirectionText, ChildLayoutDirectionShouldNotAffectParentLayoutDirection) +{ + loadDocument(CHILD_RTL_IN_PARENT_LTR); + ASSERT_EQ(Object(kLayoutDirectionLTR), component->getCalculated(kPropertyLayoutDirection)); + ASSERT_EQ(1, component->getChildCount()); + + auto frame = component->getChildAt(0); + ASSERT_EQ(Object(kLayoutDirectionLTR), frame->getCalculated(kPropertyLayoutDirection)); + ASSERT_EQ(1, frame->getChildCount()); + + auto container = frame->getChildAt(0); + ASSERT_EQ(Object(kLayoutDirectionLTR), container->getCalculated(kPropertyLayoutDirection)); + ASSERT_EQ(1, container->getChildCount()); + + auto sequence = container->getChildAt(0); + ASSERT_EQ(Object(kLayoutDirectionRTL), sequence->getCalculated(kPropertyLayoutDirection)); + ASSERT_EQ(1, sequence->getChildCount()); + + ASSERT_TRUE(IsEqual(Rect(0, 0, 120, 120), frame->getCalculated(kPropertyBounds))); +} diff --git a/unit/component/unittest_pager.cpp b/unit/component/unittest_pager.cpp index d80fd78..8d83e64 100644 --- a/unit/component/unittest_pager.cpp +++ b/unit/component/unittest_pager.cpp @@ -105,7 +105,7 @@ static const char *PAGE_CACHE_BY_NAVIGATION = R"apl( TEST_F(PagerTest, PageCacheByNavigation) { - config->pagerChildCache(2); // Two pages around starting place + config->set(RootProperty::kPagerChildCache, 2); // Two pages around starting place loadDocument(PAGE_CACHE_BY_NAVIGATION); ASSERT_TRUE(component); ASSERT_EQ(4, component->getChildCount()); diff --git a/unit/component/unittest_serialize.cpp b/unit/component/unittest_serialize.cpp index f32fc90..44f5901 100644 --- a/unit/component/unittest_serialize.cpp +++ b/unit/component/unittest_serialize.cpp @@ -214,9 +214,9 @@ TEST_F(SerializeTest, Components) ASSERT_EQ(image->getCalculated(kPropertyOverlayColor).getColor(), Color(session, imageJson["overlayColor"].GetString())); auto gradient = image->getCalculated(kPropertyOverlayGradient).getGradient(); ASSERT_EQ(gradient.getType(), imageJson["overlayGradient"]["type"].GetDouble()); - ASSERT_EQ(gradient.getAngle(), imageJson["overlayGradient"]["angle"].GetDouble()); - ASSERT_EQ(gradient.getColorRange().size(), imageJson["overlayGradient"]["colorRange"].Size()); - ASSERT_EQ(gradient.getInputRange().size(), imageJson["overlayGradient"]["inputRange"].Size()); + ASSERT_EQ(gradient.getProperty(kGradientPropertyAngle), imageJson["overlayGradient"]["angle"].GetDouble()); + ASSERT_EQ(gradient.getProperty(kGradientPropertyColorRange).size(), imageJson["overlayGradient"]["colorRange"].Size()); + ASSERT_EQ(gradient.getProperty(kGradientPropertyInputRange).size(), imageJson["overlayGradient"]["inputRange"].Size()); ASSERT_EQ(image->getCalculated(kPropertyScale), imageJson["scale"].GetDouble()); ASSERT_EQ(image->getCalculated(kPropertySource), imageJson["source"].GetString()); @@ -235,8 +235,10 @@ TEST_F(SerializeTest, Components) ASSERT_EQ(text->getCalculated(kPropertyMaxLines), textJson["maxLines"].GetDouble()); auto styledText = text->getCalculated(kPropertyText).getStyledText(); ASSERT_EQ(styledText.getText(), textJson["text"]["text"].GetString()); - ASSERT_EQ(styledText.getSpans().size(), textJson["text"]["spans"].Size()); - ASSERT_EQ(styledText.getSpans()[0].attributes.size(), textJson["text"]["spans"][0][3].Size()); + auto styledTextIt = StyledText::Iterator(styledText); + styledTextIt.next(); + ASSERT_EQ(styledTextIt.spanCount(), textJson["text"]["spans"].Size()); + ASSERT_EQ(styledTextIt.getSpanAttributes().size(), textJson["text"]["spans"][0][3].Size()); ASSERT_EQ(text->getCalculated(kPropertyTextAlignAssigned), textJson["_textAlign"].GetDouble()); ASSERT_EQ(text->getCalculated(kPropertyTextAlignVertical), textJson["textAlignVertical"].GetDouble()); @@ -299,6 +301,7 @@ TEST_F(SerializeTest, Components) checkCommonProperties(video, videoJson); ASSERT_EQ(video->getCalculated(kPropertyAudioTrack), videoJson["audioTrack"].GetDouble()); ASSERT_EQ(video->getCalculated(kPropertyAutoplay), videoJson["autoplay"].GetBool()); + ASSERT_EQ(video->getCalculated(kPropertyMuted), videoJson["muted"].GetBool()); ASSERT_EQ(video->getCalculated(kPropertyScale), videoJson["scale"].GetDouble()); auto videoSource = video->getCalculated(kPropertySource).getArray(); ASSERT_EQ(3, videoSource.size()); diff --git a/unit/component/unittest_tick.cpp b/unit/component/unittest_tick.cpp index f5e1b3c..f139e5e 100644 --- a/unit/component/unittest_tick.cpp +++ b/unit/component/unittest_tick.cpp @@ -460,7 +460,7 @@ TEST_F(TickTest, FpsLimitedByDefault) { } TEST_F(TickTest, AdjustedFpsLimit) { - config->tickHandlerUpdateLimit(10); + config->set(RootProperty::kTickHandlerUpdateLimit, 10); loadDocument(UNLIMITED_UPDATES); advanceTime(10); @@ -477,7 +477,7 @@ TEST_F(TickTest, AdjustedFpsLimit) { } TEST_F(TickTest, CantGo0) { - config->tickHandlerUpdateLimit(0); + config->set(RootProperty::kTickHandlerUpdateLimit, 0); ASSERT_EQ(1.0, config->getTickHandlerUpdateLimit()); loadDocument(UNLIMITED_UPDATES); diff --git a/unit/component/unittest_visual_context.cpp b/unit/component/unittest_visual_context.cpp index 7253a87..1545d14 100644 --- a/unit/component/unittest_visual_context.cpp +++ b/unit/component/unittest_visual_context.cpp @@ -575,6 +575,7 @@ TEST_F(VisualContextTest, HorizontalSequence) { TEST_F(VisualContextTest, RevertedSequence) { loadDocument(SEQUENCE, DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); // Check parent @@ -641,10 +642,12 @@ TEST_F(VisualContextTest, RevertedSequence) { ASSERT_EQ(2, c3t["listItem"]["index"]); component->update(kUpdateScrollPosition, 100); + advanceTime(10); root->clearPending(); // Roll back. component->update(kUpdateScrollPosition, 0); + advanceTime(10); root->clearPending(); ASSERT_TRUE(CheckDirtyVisualContext(root, component)); @@ -1316,7 +1319,7 @@ TEST_F(VisualContextTest, Media) { auto video = component->getChildAt(0); ASSERT_EQ(kComponentTypeVideo, video->getType()); - video->updateMediaState(MediaState(1, 2, 1000, 38000, true, false)); + video->updateMediaState(MediaState(1, 2, 1000, 38000, true, false, false)); ASSERT_TRUE(CheckDirtyVisualContext(root, video)); serializeVisualContext(); ASSERT_FALSE(CheckDirtyVisualContext(root, video)); diff --git a/unit/content/unittest_apl.cpp b/unit/content/unittest_apl.cpp index d5838ae..bf39c71 100644 --- a/unit/content/unittest_apl.cpp +++ b/unit/content/unittest_apl.cpp @@ -153,7 +153,8 @@ TEST(APLTest, Basic) ASSERT_EQ(Object(Color()), frame->getCalculated(kPropertyBorderColor)); auto text = frame->getChildAt(0); ASSERT_EQ(Rect(2, 2, 120, 60), text->getCalculated(kPropertyBounds).getRect()); // Frame has a 2 dp border - ASSERT_EQ(StyledText::create(root->context(), "Your text inserted here"), text->getCalculated(kPropertyText)); + ASSERT_EQ(StyledText::create(root->context(), "Your text inserted here"), + text->getCalculated(kPropertyText).getStyledText()); ASSERT_EQ(Object(Color(root->getSession(), "#ff1020")), text->getCalculated(kPropertyColor)); // Simulate a user touching on the screen @@ -178,5 +179,5 @@ TEST(APLTest, Basic) auto args = event.getValue(kEventPropertyArguments); ASSERT_EQ(1, args.size()); ASSERT_EQ(Object("test"), args.at(0)); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); } diff --git a/unit/content/unittest_document.cpp b/unit/content/unittest_document.cpp index 6846924..d022d2c 100644 --- a/unit/content/unittest_document.cpp +++ b/unit/content/unittest_document.cpp @@ -16,6 +16,7 @@ #include "rapidjson/stringbuffer.h" #include "rapidjson/writer.h" #include "gtest/gtest.h" +#include "apl/buildTimeConstants.h" #include "apl/content/content.h" #include "apl/content/metrics.h" #include "apl/content/rootconfig.h" @@ -25,19 +26,18 @@ using namespace apl; -const char *BASIC_DOC = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *BASIC_DOC = R"apl({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, Load) { @@ -53,23 +53,21 @@ TEST(DocumentTest, Load) ASSERT_TRUE(content->isReady()); auto m = Metrics().size(1024,800).theme("dark"); - auto config = RootConfig().defaultIdleTimeout(15000); + auto config = RootConfig().set(RootProperty::kDefaultIdleTimeout, 15000); auto doc = RootContext::create(m, content, config); ASSERT_TRUE(doc); ASSERT_EQ(15000, content->getDocumentSettings()->idleTimeout(config)); - ASSERT_EQ(15000, doc->settings().idleTimeout()); } -const char *BASIC_DOC_NO_TYPE_FIELD = - "{" - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *BASIC_DOC_NO_TYPE_FIELD = R"apl({ + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, NoTypeField) { @@ -77,16 +75,15 @@ TEST(DocumentTest, NoTypeField) ASSERT_FALSE(content); } -const char *BASIC_DOC_BAD_TYPE_FIELD = - "{" - " \"type\": \"APMLTemplate\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *BASIC_DOC_BAD_TYPE_FIELD = R"apl({ + "type": "APMLTemplate", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, DontEnforceBadTypeField) { @@ -103,24 +100,23 @@ TEST(DocumentTest, EnforceBadTypeField) auto content = Content::create(BASIC_DOC_BAD_TYPE_FIELD, makeDefaultSession()); ASSERT_TRUE(content->isReady()); auto m = Metrics().size(1024,800).theme("dark"); - auto config = RootConfig().enforceTypeField(true); + auto config = RootConfig().set(RootProperty::kEnforceTypeField, true); auto doc = RootContext::create(m, content, config); ASSERT_FALSE(doc); } -const char *BASIC_DOC_WITH_SETTINGS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"settings\": {" - " \"idleTimeout\": 10000" - " }," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *BASIC_DOC_WITH_SETTINGS = R"apl({ + "type": "APL", + "version": "1.0", + "settings": { + "idleTimeout": 10000 + }, + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, Settings) { @@ -133,23 +129,21 @@ TEST(DocumentTest, Settings) ASSERT_TRUE(doc); ASSERT_EQ(10000, content->getDocumentSettings()->idleTimeout(doc->rootConfig())); - ASSERT_EQ(10000, doc->settings().idleTimeout()); } -// NOTE: Backward compatibility for some APL 1.0 users where a runtime allowed "features" instead of "settings" -static const char *BASIC_DOC_WITH_FEATURES = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"features\": {" - " \"idleTimeout\": 10002" - " }," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +// NOTE: Backward compatibility for some APL 1.0 users where a runtime allowed "features" instead of "settings +static const char *BASIC_DOC_WITH_FEATURES = R"apl({ + "type": "APL", + "version": "1.0", + "features": { + "idleTimeout": 10002 + }, + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, Features) { @@ -161,28 +155,26 @@ TEST(DocumentTest, Features) auto doc = RootContext::create(m, content); ASSERT_TRUE(doc); - ASSERT_EQ(10002, doc->settings().idleTimeout()); ASSERT_EQ(10002, content->getDocumentSettings()->idleTimeout(doc->rootConfig())); } -// NOTE: Ensure that "settings" overrides "features" -static const char *BASIC_DOC_WITH_FEATURES_AND_SETTINGS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"features\": {" - " \"idleTimeout\": 10002" - " }," - " \"settings\": {" - " \"idleTimeout\": 80000" - " }," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +// NOTE: Ensure that "settings" overrides "features +static const char *BASIC_DOC_WITH_FEATURES_AND_SETTINGS = R"apl({ + "type": "APL", + "version": "1.0", + "features": { + "idleTimeout": 10002 + }, + "settings": { + "idleTimeout": 80000 + }, + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, SettingsAndFeatures) { @@ -194,36 +186,34 @@ TEST(DocumentTest, SettingsAndFeatures) auto doc = RootContext::create(m, content); ASSERT_TRUE(doc); - ASSERT_EQ(80000, doc->settings().idleTimeout()); ASSERT_EQ(80000, content->getDocumentSettings()->idleTimeout(doc->rootConfig())); } -const char *BASIC_DOC_WITH_USER_DEFINED_SETTINGS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"settings\": {" - " \"idleTimeout\": 20000," - " \"userSettingString\": \"MyValue\"," - " \"userSettingNumber\": 500," - " \"userSettingBool\": true," - " \"userSettingDimension\": \"100dp\"," - " \"userSettingArray\": [" - " \"valueA\"," - " \"valueB\"," - " \"valueC\"" - " ]," - " \"userSettingMap\": {" - " \"keyA\": \"valueA\"," - " \"keyB\": \"valueB\"" - " }" - " }," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *BASIC_DOC_WITH_USER_DEFINED_SETTINGS = R"apl({ + "type": "APL", + "version": "1.0", + "settings": { + "idleTimeout": 20000, + "userSettingString": "MyValue", + "userSettingNumber": 500, + "userSettingBool": true, + "userSettingDimension": "100dp", + "userSettingArray": [ + "valueA", + "valueB", + "valueC" + ], + "userSettingMap": { + "keyA": "valueA", + "keyB": "valueB" + } + }, + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, UserDefinedSettings) { @@ -239,7 +229,7 @@ TEST(DocumentTest, UserDefinedSettings) ASSERT_TRUE(settings); ASSERT_EQ(Object::NULL_OBJECT(), settings->getValue("settingAbsent")); - ASSERT_EQ(20000, settings->idleTimeout()); + ASSERT_EQ(20000, settings->idleTimeout(doc->rootConfig())); ASSERT_STREQ("MyValue",settings->getValue("userSettingString").getString().c_str()); ASSERT_EQ(500, settings->getValue("userSettingNumber").getInteger()); ASSERT_TRUE(settings->getValue("userSettingBool").getBoolean()); @@ -249,16 +239,15 @@ TEST(DocumentTest, UserDefinedSettings) } -const char *BASIC_DOC_WITHOUT_SETTINGS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *BASIC_DOC_WITHOUT_SETTINGS = R"apl({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, WithoutSettings) { @@ -270,9 +259,8 @@ TEST(DocumentTest, WithoutSettings) auto doc = RootContext::create(m, content); ASSERT_TRUE(doc); - ASSERT_EQ(30000, doc->settings().idleTimeout()); ASSERT_EQ(30000, content->getDocumentSettings()->idleTimeout(doc->rootConfig())); - ASSERT_EQ(Object::NULL_OBJECT(), doc->settings().getValue("userSetting")); + ASSERT_EQ(Object::NULL_OBJECT(), content->getDocumentSettings()->getValue("userSetting")); } @@ -281,51 +269,48 @@ TEST(DocumentTest, LoadError) { ASSERT_FALSE(content); } -const char *ONE_DEPENDENCY = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"basic\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; - -const char *BASIC_PACKAGE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"" - "}"; - -const char *ONE_DEPENDENCY_VERSION = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.0\"," - " \"import\": [" - " {" - " \"name\": \"basic\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *ONE_DEPENDENCY = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "basic", + "version": "1.2" + } + ], + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Text" + } + } +})apl"; + +const char *BASIC_PACKAGE = R"apl({ + "type": "APL", + "version": "1.1" +})apl"; + +const char *ONE_DEPENDENCY_VERSION = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "basic", + "version": "1.2" + } + ], + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, LoadOneDependency) { @@ -352,16 +337,15 @@ TEST(DocumentTest, LoadOneDependency) ASSERT_EQ("1.1", content->getAPLVersion()); } -const char *INCOMPATIBLE_MAIN = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.very_custom_version\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +const char *INCOMPATIBLE_MAIN = R"apl({ + "type": "APL", + "version": "1.very_custom_version", + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; TEST(DocumentTest, IncompatibleMainVersion) { @@ -379,11 +363,10 @@ TEST(DocumentTest, IncompatibleMainVersion) ASSERT_FALSE(doc); } -const char *BASIC_INCOMPATIBLE_PACKAGE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.very_custom_version\"" - "}"; +const char *BASIC_INCOMPATIBLE_PACKAGE = R"apl({ + "type": "APL", + "version": "1.very_custom_version" +})apl"; TEST(DocumentTest, IncompatibleImportVersion) { @@ -496,43 +479,41 @@ TEST(DocumentTest, EnforceSpecVersionCheckMultipleVersions) ASSERT_TRUE(doc); } -const char *SINGLE_WITH_RESOURCE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"basic\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"resources\": [" - " {" - " \"strings\": {" - " \"test\": \"A\"" - " }" - " }" - " ]," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; - -const char *BASIC_SINGLE_PKG = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"resources\": [" - " {" - " \"string\": {" - " \"item\": \"Here\"," - " \"test\": \"B\"" - " }" - " }" - " ]" - "}"; +const char *SINGLE_WITH_RESOURCE = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "basic", + "version": "1.2" + } + ], + "resources": [ + { + "strings": { + "test": "A" + } + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +const char *BASIC_SINGLE_PKG = R"apl({ + "type": "APL", + "version": "1.1", + "resources": [ + { + "string": { + "item": "Here", + "test": "B" + } + } + ] +})apl"; TEST(DocumentTest, DependencyCheck) { @@ -559,94 +540,90 @@ TEST(DocumentTest, DependencyCheck) ASSERT_EQ(Object("A"), root->context().opt("@test")); // test gets overridden } -const char *DIAMOND = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"A\"," - " \"version\": \"2.2\"" - " }," - " {" - " \"name\": \"B\"," - " \"version\": \"1.0\"" - " }" - " ]," - " \"resources\": [" - " {" - " \"strings\": {" - " \"test\": \"Hello\"" - " }" - " }" - " ]," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; - -const char *DIAMOND_A = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"C\"," - " \"version\": \"1.5\"" - " }" - " ]," - " \"resources\": [" - " {" - " \"strings\": {" - " \"test\": \"My A\"," - " \"A\": \"This is A\"," - " \"overwrite_A\": \"Original_A\"," - " \"overwrite_C\": \"A\"" - " }" - " }" - " ]" - "}"; - -const char *DIAMOND_B = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"C\"," - " \"version\": \"1.5\"" - " }" - " ]," - " \"resources\": [" - " {" - " \"strings\": {" - " \"test\": \"My B\"," - " \"B\": \"This is B\"," - " \"overwrite_B\": \"Original_B\"," - " \"overwrite_C\": \"B\"" - " }" - " }" - " ]" - "}"; - -const char *DIAMOND_C = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"resources\": [" - " {" - " \"strings\": {" - " \"C\": \"This is C\"," - " \"test\": \"My C\"," - " \"overwrite_A\": \"C's version of A\"," - " \"overwrite_B\": \"C's version of B\"," - " \"overwrite_C\": \"C's version of C\"" - " }" - " }" - " ]" - "}"; +const char *DIAMOND = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "A", + "version": "2.2" + }, + { + "name": "B", + "version": "1.0" + } + ], + "resources": [ + { + "strings": { + "test": "Hello" + } + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +const char *DIAMOND_A = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "C", + "version": "1.5" + } + ], + "resources": [ + { + "strings": { + "test": "My A", + "A": "This is A", + "overwrite_A": "Original_A", + "overwrite_C": "A" + } + } + ] +})apl"; + +const char *DIAMOND_B = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "C", + "version": "1.5" + } + ], + "resources": [ + { + "strings": { + "test": "My B", + "B": "This is B", + "overwrite_B": "Original_B", + "overwrite_C": "B" + } + } + ] +})apl"; + +const char *DIAMOND_C = R"apl({ + "type": "APL", + "version": "1.1", + "resources": [ + { + "strings": { + "C": "This is C", + "test": "My C", + "overwrite_A": "C's version of A", + "overwrite_B": "C's version of B", + "overwrite_C": "C's version of C" + } + } + ] +})apl"; TEST(DocumentTest, MultipleDependencies) { @@ -694,59 +671,56 @@ TEST(DocumentTest, MultipleDependencies) ASSERT_EQ(Object("B"), context->opt("@overwrite_C")); } -static const char *DUPLICATE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"A\"," - " \"version\": \"2.2\"" - " }," - " {" - " \"name\": \"A\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; - -static const char *DUPLICATE_A_2_2 = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"A\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"resources\": [" - " {" - " \"strings\": {" - " \"A\": \"Not A\"" - " }" - " }" - " ]" - "}"; - -static const char *DUPLICATE_A_1_2 = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"resources\": [" - " {" - " \"strings\": {" - " \"A\": \"A\"," - " \"B\": \"B\"" - " }" - " }" - " ]" - "}"; +static const char *DUPLICATE = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "A", + "version": "2.2" + }, + { + "name": "A", + "version": "1.2" + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +static const char *DUPLICATE_A_2_2 = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "A", + "version": "1.2" + } + ], + "resources": [ + { + "strings": { + "A": "Not A" + } + } + ] +})apl"; + +static const char *DUPLICATE_A_1_2 = R"apl({ + "type": "APL", + "version": "1.1", + "resources": [ + { + "strings": { + "A": "A", + "B": "B" + } + } + ] +})apl"; TEST(DocumentTest, Duplicate) { @@ -783,12 +757,11 @@ TEST(DocumentTest, Duplicate) ASSERT_EQ(Object("B"), context->opt("@B")); } -const char *FAKE_MAIN_TEMPLATE = - "{" - " \"item\": {" - " \"type\": \"Text\"" - " }" - "}"; +const char *FAKE_MAIN_TEMPLATE = R"apl({ + "item": { + "type": "Text" + } +})apl"; static std::string makeTestPackage(std::vector dependencies, std::map stringMap) @@ -1030,27 +1003,26 @@ TEST(DocumentTest, DeepLoop) ASSERT_TRUE(content->isError()); } -static const char * PAYLOAD_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"onMount\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"TestId\"," - " \"property\": \"text\"," - " \"value\": \"${payload.value}\"" - " }," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"text\": \"Not set\"," - " \"id\": \"TestId\"" - " }" - " }" - "}"; +static const char * PAYLOAD_TEST = R"apl({ + "type": "APL", + "version": "1.3", + "onMount": { + "type": "SetValue", + "componentId": "TestId", + "property": "text", + "value": "${payload.value}" + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "items": { + "type": "Text", + "text": "Not set", + "id": "TestId" + } + } +})apl"; /** * Verify that the onMount command has access to the document payload @@ -1076,31 +1048,29 @@ TEST(DocumentTest, PayloadTest) } -static const char * EXTERNAL_COMMAND_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"parameters\": [" - " \"payload\"" - " ]," - " \"items\": {" - " \"type\": \"Text\"," - " \"id\": \"TextId\"," - " \"text\": \"${payload.start}\"" - " }" - " }" - "}"; - -static const char * EXTERNAL_COMMAND_TEST_COMMAND = - "[" - " {" - " \"type\": \"SetValue\"," - " \"componentId\": \"TextId\"," - " \"property\": \"text\"," - " \"value\": \"${payload.end}\"" - " }" - "]"; +static const char * EXTERNAL_COMMAND_TEST = R"apl({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "parameters": [ + "payload" + ], + "items": { + "type": "Text", + "id": "TextId", + "text": "${payload.start}" + } + } +})apl"; + +static const char * EXTERNAL_COMMAND_TEST_COMMAND = R"apl([ + { + "type": "SetValue", + "componentId": "TextId", + "property": "text", + "value": "${payload.end}" + } +])apl"; /** * Verify that an external command has access to the document payload @@ -1132,18 +1102,16 @@ TEST(DocumentTest, ExternalCommandTest) } -static const char *ENVIRONMENT_TEST = R"apl( - { - "type": "APL", - "version": "1.8", - "environment": { - "parameters": [ "a", "b" ] - }, - "mainTemplate": { - "parameters": [ "b", "c" ] - } - } -)apl"; +static const char *ENVIRONMENT_TEST = R"apl({ + "type": "APL", + "version": "1.8", + "environment": { + "parameters": [ "a", "b" ] + }, + "mainTemplate": { + "parameters": [ "b", "c" ] + } +})apl"; /** * Check parameter handling from the environment and mainTemplate @@ -1171,18 +1139,16 @@ TEST(DocumentTest, EnvironmentTest) } -static const char *REDUNDANT_ENVIRONMENT_TEST = R"apl( - { - "type": "APL", - "version": "1.8", - "environment": { - "parameters": [ "a", "b", "a", "b" ] - }, - "mainTemplate": { - "parameters": [ "b", "c", "b", "c" ] - } - } -)apl"; +static const char *REDUNDANT_ENVIRONMENT_TEST = R"apl({ + "type": "APL", + "version": "1.8", + "environment": { + "parameters": [ "a", "b", "a", "b" ] + }, + "mainTemplate": { + "parameters": [ "b", "c", "b", "c" ] + } +})apl"; /** * Check parameter handling from the environment and mainTemplate @@ -1209,3 +1175,129 @@ TEST(DocumentTest, RedundantEnvironmentTest) ASSERT_TRUE(content->isReady()); } +class MemoizingLogBridge : public LogBridge { +public: + void transport(LogLevel level, const std::string& log) override { + mLog = log; + } + + void reset() { + mLog = ""; + } + + std::string mLog; +}; + +const char *NO_DIAGNOSTIC_TAG = R"apl({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST(DocumentTest, LogId) +{ + auto logBridge = std::make_shared(); + LoggerFactory::instance().initialize(logBridge); + + auto content = Content::create(NO_DIAGNOSTIC_TAG, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + + auto m = Metrics().size(1024,800).theme("dark"); + auto config = RootConfig(); + auto doc = RootContext::create(m, content, config); + + ASSERT_TRUE(doc); + + ASSERT_EQ(doc->getSession()->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); + + LoggerFactory::instance().reset(); +} + +const char *LOG_ID_WITH_PREFIX = R"apl({ + "type": "APL", + "version": "1.0", + "settings": { + "-diagnosticLabel": "FOOBAR" + }, + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST(DocumentTest, ShortLogId) +{ + auto logBridge = std::make_shared(); + LoggerFactory::instance().initialize(logBridge); + + auto content = Content::create(LOG_ID_WITH_PREFIX, makeDefaultSession()); + ASSERT_TRUE(content->isReady()); + + auto m = Metrics().size(1024,800).theme("dark"); + auto config = RootConfig(); + auto doc = RootContext::create(m, content, config); + + 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); + + 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); + + LoggerFactory::instance().reset(); +} + +TEST(DocumentTest, TwoDocuments) +{ + auto logBridge = std::make_shared(); + LoggerFactory::instance().initialize(logBridge); + + auto content1 = Content::create(LOG_ID_WITH_PREFIX, makeDefaultSession()); + 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()); + ASSERT_TRUE(content2->isReady()); + ASSERT_TRUE(content2->getSession()->getLogId().rfind("FOOBAR-", 0) == 0); + ASSERT_EQ(content2->getSession()->getLogId() + ":content.cpp:Content : Initializing experience using " + + std::string(sCoreRepositoryVersion), logBridge->mLog); + + + auto m = Metrics().size(1024,800).theme("dark"); + auto config1 = RootConfig(); + auto config2 = RootConfig(); + + auto doc1 = RootContext::create(m, content1, config1); + ASSERT_TRUE(doc1); + 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(doc2->getSession()) << "TEST"; + ASSERT_EQ(17, doc2->getSession()->getLogId().size()); + ASSERT_EQ(doc2->getSession()->getLogId() + ":unittest_document.cpp:TestBody : TEST", logBridge->mLog); + + ASSERT_NE(doc1->getSession()->getLogId(), doc2->getSession()->getLogId()); + + LoggerFactory::instance().reset(); +} \ No newline at end of file diff --git a/unit/content/unittest_document_background.cpp b/unit/content/unittest_document_background.cpp index f6b05c3..49bad5e 100644 --- a/unit/content/unittest_document_background.cpp +++ b/unit/content/unittest_document_background.cpp @@ -22,7 +22,7 @@ class DocumentBackgroundTest : public ::testing::Test { DocumentBackgroundTest() : Test() { metrics.theme("black").size(1000, 1000).dpi(160).mode(kViewportModeHub); - config.agent("backgroundTest", "0.1"); + config.set({{RootProperty::kAgentName, "backgroundTest"}, {RootProperty::kAgentVersion, "0.1"}}); } Object load(const char *document) { @@ -40,16 +40,15 @@ class DocumentBackgroundTest : public ::testing::Test { * a color or a gradient. If it is poorly defined, it will be returned as the TRANSPARENT color. */ -static const char *NO_BACKGROUND = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *NO_BACKGROUND = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, NoBackground) { @@ -59,17 +58,16 @@ TEST_F(DocumentBackgroundTest, NoBackground) ASSERT_TRUE(IsEqual(Color(Color::TRANSPARENT), background)); } -static const char *COLOR_BACKGROUND = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"background\": \"blue\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *COLOR_BACKGROUND = R"({ + "type": "APL", + "version": "1.1", + "background": "blue", + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, ColorBackground) { @@ -79,28 +77,27 @@ TEST_F(DocumentBackgroundTest, ColorBackground) ASSERT_TRUE(IsEqual(Color(Color::BLUE), background)); } -static const char *GRADIENT_BACKGROUND = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"background\": {" - " \"type\": \"linear\"," - " \"colorRange\": [" - " \"darkgreen\"," - " \"white\"" - " ]," - " \"inputRange\": [" - " 0," - " 0.25" - " ]," - " \"angle\": 90" - " }," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *GRADIENT_BACKGROUND = R"({ + "type": "APL", + "version": "1.1", + "background": { + "type": "linear", + "colorRange": [ + "darkgreen", + "white" + ], + "inputRange": [ + 0, + 0.25 + ], + "angle": 90 + }, + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, GradientBackground) { @@ -110,24 +107,23 @@ TEST_F(DocumentBackgroundTest, GradientBackground) auto gradient = background.getGradient(); ASSERT_EQ(Gradient::GradientType::LINEAR, gradient.getType()); - ASSERT_EQ(90, gradient.getAngle()); - ASSERT_EQ(std::vector({0x006400ff, 0xffffffff}), gradient.getColorRange()); - ASSERT_EQ(std::vector({0, 0.25}), gradient.getInputRange()); + ASSERT_EQ(90, gradient.getProperty(kGradientPropertyAngle).getInteger()); + ASSERT_EQ(std::vector({Color(0x006400ff), Color(0xffffffff)}), gradient.getProperty(kGradientPropertyColorRange).getArray()); + ASSERT_EQ(std::vector({0.0, 0.25}), gradient.getProperty(kGradientPropertyInputRange).getArray()); } -static const char *BAD_BACKGROUND_MAP = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"background\": {" - " \"type\": \"Foo\"" - " }," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *BAD_BACKGROUND_MAP = R"({ + "type": "APL", + "version": "1.1", + "background": { + "type": "Foo" + }, + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, BadBackgroundMap) { @@ -137,17 +133,16 @@ TEST_F(DocumentBackgroundTest, BadBackgroundMap) ASSERT_TRUE(IsEqual(Color(Color::TRANSPARENT), background)); } -static const char *BAD_BACKGROUND_COLOR = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"background\": \"bluish\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *BAD_BACKGROUND_COLOR = R"({ + "type": "APL", + "version": "1.1", + "background": "bluish", + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, BadBackgroundColor) { @@ -157,17 +152,16 @@ TEST_F(DocumentBackgroundTest, BadBackgroundColor) ASSERT_TRUE(IsEqual(Color(Color::TRANSPARENT), background)); } -static const char *DATA_BINDING_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"background\": \"${viewport.width > 500 ? 'blue' : 'red'}\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *DATA_BINDING_TEST = R"({ + "type": "APL", + "version": "1.1", + "background": "${viewport.width > 500 ? 'blue' : 'red'}", + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, DataBindingTest) { @@ -187,17 +181,16 @@ TEST_F(DocumentBackgroundTest, DataBindingTest) /* * Check to see that a data-binding expression can use the system theme */ -static const char *DATA_BOUND_THEME = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"background\": \"${viewport.theme == 'dark' ? 'rgb(16,32,64)' : 'rgb(224, 224, 192)'}\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *DATA_BOUND_THEME = R"({ + "type": "APL", + "version": "1.1", + "background": "${viewport.theme == 'dark' ? 'rgb(16,32,64)' : 'rgb(224, 224, 192)'}", + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, DataBoundTheme) { @@ -215,18 +208,17 @@ TEST_F(DocumentBackgroundTest, DataBoundTheme) /* * Check that a data-binding expression using a theme can be overridden by the document-supplied theme */ -static const char *DATA_BOUND_THEME_OVERRIDE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"theme\": \"light\"," - " \"background\": \"${viewport.theme == 'dark' ? 'rgb(16,32,64)' : 'rgb(224, 224, 192)'}\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Text\"" - " }" - " }" - "}"; +static const char *DATA_BOUND_THEME_OVERRIDE = R"({ + "type": "APL", + "version": "1.1", + "theme": "light", + "background": "${viewport.theme == 'dark' ? 'rgb(16,32,64)' : 'rgb(224, 224, 192)'}", + "mainTemplate": { + "items": { + "type": "Text" + } + } +})"; TEST_F(DocumentBackgroundTest, DataBoundThemeOverride) { diff --git a/unit/datagrammar/unittest_grammar.cpp b/unit/datagrammar/unittest_grammar.cpp index 0db6b94..c912437 100644 --- a/unit/datagrammar/unittest_grammar.cpp +++ b/unit/datagrammar/unittest_grammar.cpp @@ -13,6 +13,7 @@ * permissions and limitations under the License. */ +#include #include #include @@ -372,6 +373,18 @@ TEST_F(GrammarTest, Basic) EXPECT_EQ(o(false), eval("${10==11}")); } +TEST_F(GrammarTest, NumbersIgnoreCLocale) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + EXPECT_EQ(42, eval( "${42}" ).asNumber()); + EXPECT_EQ(-10, eval( "${-10.0}" ).asNumber()); + EXPECT_EQ(-11.4, eval( "${-11.4}" ).asNumber()); + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); +} + TEST_F(GrammarTest, Functions) { ASSERT_NEAR(std::acos(0.5), eval("${Math.acos(0.5)}").asNumber(), 0.0000001); diff --git a/unit/datagrammar/unittest_parse.cpp b/unit/datagrammar/unittest_parse.cpp index 9df63d1..a18b1e4 100644 --- a/unit/datagrammar/unittest_parse.cpp +++ b/unit/datagrammar/unittest_parse.cpp @@ -15,6 +15,8 @@ #include "../testeventloop.h" +#include + #include "apl/primitives/symbolreferencemap.h" using namespace apl; @@ -67,6 +69,20 @@ TEST_F(ParseTest, Simple) ASSERT_EQ(82, foo.asNumber()); } +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}"); + EXPECT_EQ(12.4, binding.asNumber()); + + binding = parseDataBinding(*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/"}}, diff --git a/unit/datasource/unittest_datasource.cpp b/unit/datasource/unittest_datasource.cpp index ea44538..a426bb3 100644 --- a/unit/datasource/unittest_datasource.cpp +++ b/unit/datasource/unittest_datasource.cpp @@ -422,7 +422,7 @@ static const char *DATA_OFFSET = TEST_F(DynamicSourceTest, Offset) { - config->sequenceChildCache(5); + config->set(RootProperty::kSequenceChildCache, 5); auto ds = std::make_shared(ITEMS); config->dataSourceProvider("GenericList", ds); diff --git a/unit/datasource/unittest_dynamicindexlist.cpp b/unit/datasource/unittest_dynamicindexlist.cpp index 0703701..69da57a 100644 --- a/unit/datasource/unittest_dynamicindexlist.cpp +++ b/unit/datasource/unittest_dynamicindexlist.cpp @@ -699,6 +699,7 @@ R"({ TEST_F(DynamicIndexListTest, WithFirstAndLast) { loadDocument(FIRST_AND_LAST, FIRST_AND_LAST_DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -727,6 +728,7 @@ TEST_F(DynamicIndexListTest, WithFirstAndLast) ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); component->update(kUpdateScrollPosition, 600); + advanceTime(10); root->clearPending(); @@ -794,6 +796,7 @@ static const char *FIRST_AND_LAST_HORIZONTAL_RTL = R"({ TEST_F(DynamicIndexListTest, WithFirstAndLastHorizontalRTL) { loadDocument(FIRST_AND_LAST_HORIZONTAL_RTL, FIRST_AND_LAST_DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -822,6 +825,7 @@ TEST_F(DynamicIndexListTest, WithFirstAndLastHorizontalRTL) ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); component->update(kUpdateScrollPosition, -600); + advanceTime(10); root->clearPending(); @@ -880,6 +884,7 @@ static const char *FIRST = R"({ TEST_F(DynamicIndexListTest, WithFirst) { loadDocument(FIRST, FIRST_AND_LAST_DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -908,6 +913,7 @@ TEST_F(DynamicIndexListTest, WithFirst) ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); component->update(kUpdateScrollPosition, 600); + advanceTime(10); root->clearPending(); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 0, 5)); @@ -990,6 +996,7 @@ TEST_F(DynamicIndexListTest, WithLast) ASSERT_EQ(400, component->getCalculated(kPropertyScrollPosition).asNumber()); component->update(kUpdateScrollPosition, 600); + advanceTime(10); root->clearPending(); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 16, 4)); @@ -1023,6 +1030,7 @@ static const char *LAST_DATA = R"({ TEST_F(DynamicIndexListTest, WithLastOneWay) { loadDocument(LAST, LAST_DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1061,6 +1069,7 @@ TEST_F(DynamicIndexListTest, WithLastOneWay) ASSERT_EQ(0, component->getCalculated(kPropertyScrollPosition).asNumber()); component->update(kUpdateScrollPosition, 600); + advanceTime(10); root->clearPending(); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 11, 5)); @@ -1116,6 +1125,7 @@ static const char *EMPTY_DATA = R"({ TEST_F(DynamicIndexListTest, EmptySequence) { loadDocument(BASIC, EMPTY_DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -1199,6 +1209,7 @@ static const char *MULTI_DATA = R"({ TEST_F(DynamicIndexListTest, Multi) { loadDocument(MULTI, MULTI_DATA); + advanceTime(10); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok1", "101", 15, 5)); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok2", "102", 5, 5)); @@ -1284,6 +1295,58 @@ TEST_F(DynamicIndexListTest,MultiCloneData) { ASSERT_EQ(component->getChildCount(), 2); } +TEST_F(DynamicIndexListTest, DuplicateListVersionErrorForRemovedComponent) +{ + loadDocument(BASIC, DATA); + advanceTime(10); + + ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); + + component = nullptr; + root = nullptr; + ASSERT_FALSE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); +} + +TEST_F(DynamicIndexListTest, MissingListVersionErrorForRemovedComponent) +{ + loadDocument(BASIC, DATA); + advanceTime(10); + + ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); + + component = nullptr; + root = nullptr; + ASSERT_FALSE(ds->processUpdate(createLazyLoad(-1, 101, 15, "15, 16, 17, 18, 19"))); +} + +TEST_F(DynamicIndexListTest, ConnectionInFailedStateForRemovedComponent) +{ + loadDocument(BASIC, DATA); + advanceTime(10); + + ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); + // put connection into failed state with invalid update + ASSERT_FALSE(ds->processUpdate(createLazyLoad(-1, 101, 15, "15, 16, 17, 18, 19"))); + + component = nullptr; + root = nullptr; + ASSERT_FALSE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); + ASSERT_FALSE(ds->getPendingErrors().empty()); +} + +TEST_F(DynamicIndexListTest, InvalidUpdatePayloadForRemovedComponent) +{ + loadDocument(BASIC, DATA); + advanceTime(10); + + ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); + + component = nullptr; + root = nullptr; + auto invalidPayload = "{\"presentationToken\": \"presentationToken\", \"listId\": \"vQdpOESlok\"}"; + ASSERT_FALSE(ds->processUpdate(invalidPayload)); +} + static const char *BASIC_CONTAINER = R"({ "type": "APL", "version": "1.3", @@ -3525,6 +3588,8 @@ static const char *BASIC_CONFIG_CHANGE = R"({ })"; TEST_F(DynamicIndexListTest, Reinflate) { + config->set(RootProperty::kSequenceChildCache, 0); + loadDocument(BASIC_CONFIG_CHANGE, DATA); ASSERT_EQ(kComponentTypeSequence, component->getType()); ASSERT_EQ(5, component->getChildCount()); @@ -3532,26 +3597,28 @@ TEST_F(DynamicIndexListTest, Reinflate) { ASSERT_TRUE(CheckChildren({10, 11, 12, 13, 14})); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "101", 15, 5)); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "102", 5, 5)); - ASSERT_TRUE(ds->processUpdate(createLazyLoad(-1, 101, 15, "15, 16, 17, 18, 19"))); - ASSERT_TRUE(ds->processUpdate(createLazyLoad(-1, 102, 5, "5, 6, 7, 8, 9"))); + ASSERT_TRUE(ds->processUpdate(createLazyLoad(1, 101, 15, "15, 16, 17, 18, 19"))); + ASSERT_TRUE(ds->processUpdate(createLazyLoad(2, 102, 5, "5, 6, 7, 8, 9"))); root->clearPending(); ASSERT_EQ(15, component->getChildCount()); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", 0, 5)); - ASSERT_TRUE(ds->processUpdate(createLazyLoad(-1, 103, 0, "0, 1, 2, 3, 4"))); + ASSERT_TRUE(ds->processUpdate(createLazyLoad(3, 103, 0, "0, 1, 2, 3, 4"))); root->clearPending(); ASSERT_EQ(20, component->getChildCount()); ASSERT_FALSE(root->hasEvent()); // re-inflate should get same result. - auto oldComponent = component; + auto oldComponentId = component->getId(); configChangeReinflate(ConfigurationChange(100, 100)); ASSERT_EQ(kComponentTypeSequence, component->getType()); ASSERT_TRUE(component); - ASSERT_EQ(component->getId(), oldComponent->getId()); + ASSERT_EQ(component->getId(), oldComponentId); ASSERT_EQ(20, component->getChildCount()); ASSERT_TRUE(CheckBounds(0, 20)); ASSERT_FALSE(root->hasEvent()); + + ASSERT_TRUE(ds->processUpdate(createReplace(4, 10, 110))); } static const char *TYPED_DATA = R"({ diff --git a/unit/datasource/unittest_dynamictokenlist.cpp b/unit/datasource/unittest_dynamictokenlist.cpp index 44350be..404aae5 100644 --- a/unit/datasource/unittest_dynamictokenlist.cpp +++ b/unit/datasource/unittest_dynamictokenlist.cpp @@ -816,6 +816,7 @@ static const char* LAST_DATA = R"({ TEST_F(DynamicTokenListTest, WithLastOneWay) { loadDocument(LAST, LAST_DATA); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); @@ -854,6 +855,7 @@ TEST_F(DynamicTokenListTest, WithLastOneWay) { ASSERT_EQ(0, component->getCalculated(kPropertyScrollPosition).asNumber()); component->update(kUpdateScrollPosition, 600); + advanceTime(10); root->clearPending(); ASSERT_TRUE(CheckFetchRequest("vQdpOESlok", "103", "forwardPageToken2")); diff --git a/unit/debugtools.cpp b/unit/debugtools.cpp index 9feb0a3..c43f3aa 100644 --- a/unit/debugtools.cpp +++ b/unit/debugtools.cpp @@ -198,7 +198,8 @@ dumpLayoutInternal(const CoreComponentPtr& component, int indent, int childIndex << " laidOut=" << component->getCalculated(kPropertyLaidOut) << " isAttached=" << component->isAttached() << " bounds=" << component->getCalculated(kPropertyBounds) - << " innerBounds=" << component->getCalculated(kPropertyInnerBounds); + << " innerBounds=" << component->getCalculated(kPropertyInnerBounds) + << " transform=" << component->getCalculated(kPropertyTransform); if (YGNodeGetDirtiedFunc(component->getNode())) s << " LAYOUT_SIZE=" << component->getLayoutSize(); diff --git a/unit/engine/unittest_builder.cpp b/unit/engine/unittest_builder.cpp index cf058ae..8982cf4 100644 --- a/unit/engine/unittest_builder.cpp +++ b/unit/engine/unittest_builder.cpp @@ -278,7 +278,7 @@ TEST_F(BuilderTest, FullImage) auto grad = component->getCalculated(kPropertyOverlayGradient); ASSERT_TRUE(grad.isGradient()); ASSERT_EQ(Gradient::LINEAR, grad.getGradient().getType()); - ASSERT_EQ(Object(Color(0x0000ffff)), grad.getGradient().getColorRange().at(0)); + ASSERT_EQ(Object(Color(0x0000ffff)), grad.getGradient().getProperty(kGradientPropertyColorRange).at(0)); auto filters = component->getCalculated(kPropertyFilters); ASSERT_EQ(1, filters.size()); @@ -324,7 +324,7 @@ TEST_F(BuilderTest, GradientInResource) auto grad = component->getCalculated(kPropertyOverlayGradient); ASSERT_TRUE(grad.isGradient()); ASSERT_EQ(Gradient::LINEAR, grad.getGradient().getType()); - ASSERT_EQ(Object(Color(0x0000ffff)), grad.getGradient().getColorRange().at(0)); + ASSERT_EQ(Object(Color(0x0000ffff)), grad.getGradient().getProperty(kGradientPropertyColorRange).at(0)); } static const char *SIMPLE_TEXT = R"( @@ -1400,6 +1400,7 @@ TEST_F(BuilderTest, SimpleVideo) ASSERT_EQ(Object::EMPTY_ARRAY(), component->getCalculated(kPropertyOnPlay)); ASSERT_EQ(Object::EMPTY_ARRAY(), component->getCalculated(kPropertyOnTrackUpdate)); ASSERT_EQ(false, component->getCalculated(kPropertyAutoplay).getBoolean()); + ASSERT_EQ(false, component->getCalculated(kPropertyMuted).getBoolean()); } static const char *OLD_AUTO_PLAY_VIDEO = R"( @@ -1462,6 +1463,7 @@ static const char *FULL_VIDEO = R"( "type": "Video", "audioTrack": "background", "autoplay": "true", + "muted": "true", "scale": "best-fill", "source": [ "URL1", @@ -1596,6 +1598,7 @@ TEST_F(BuilderTest, FullVideo) ASSERT_EQ(5, map.get(kPropertyOnTrackFail).size()); ASSERT_EQ(6, map.get(kPropertyOnTrackReady).size()); ASSERT_EQ(true, map.get(kPropertyAutoplay).getBoolean()); + ASSERT_EQ(true, map.get(kPropertyMuted).getBoolean()); ASSERT_EQ(3, map.get(kPropertySource).size()); auto source1 = map.get(kPropertySource).at(0).getMediaSource(); @@ -2432,7 +2435,7 @@ static const char *CONFIG_TEXT_DEFAULT_THEME = R"( // Verify that we can configure the default text color and font family TEST_F(BuilderTest, ConfigTextDarkTheme) { - config->defaultFontFamily("Helvetica"); + config->set(RootProperty::kDefaultFontFamily, "Helvetica"); // The default theme is "dark", which has a color of 0xFAFAFAFF loadDocument(CONFIG_TEXT_DEFAULT_THEME); @@ -2441,7 +2444,7 @@ TEST_F(BuilderTest, ConfigTextDarkTheme) ASSERT_TRUE(IsEqual("Helvetica", component->getCalculated(kPropertyFontFamily))); // Override the generic theme color. The document defaults to dark theme, so this is ignored - config->defaultFontColor(0x11223344); + config->set(RootProperty::kDefaultFontColor, 0x11223344); loadDocument(CONFIG_TEXT_DEFAULT_THEME); ASSERT_TRUE(IsEqual(Color(0xFAFAFAFF), component->getCalculated(kPropertyColor))); @@ -2473,7 +2476,7 @@ TEST_F(BuilderTest, ConfigTextLightTheme) ASSERT_TRUE(IsEqual(Color(0x1E2222FF), component->getCalculated(kPropertyColor))); // Override the generic theme color. The document has a theme, so this is ignored - config->defaultFontColor(0x11223344); + config->set(RootProperty::kDefaultFontColor, 0x11223344); loadDocument(CONFIG_TEXT_LIGHT_THEME); ASSERT_TRUE(IsEqual(Color(0x1E2222FF), component->getCalculated(kPropertyColor))); @@ -2505,7 +2508,7 @@ TEST_F(BuilderTest, ConfigTextFuzzyTheme) ASSERT_TRUE(IsEqual(Color(0xfafafaff), component->getCalculated(kPropertyColor))); // Override the generic theme color. Because 'fuzzy' isn't light or dark, this should happen - config->defaultFontColor(0x11223344); + config->set(RootProperty::kDefaultFontColor, 0x11223344); loadDocument(CONFIG_TEXT_FUZZY_THEME); ASSERT_TRUE(IsEqual(Color(0x11223344), component->getCalculated(kPropertyColor))); diff --git a/unit/engine/unittest_builder_config_change.cpp b/unit/engine/unittest_builder_config_change.cpp index 8d4d499..af6ce08 100644 --- a/unit/engine/unittest_builder_config_change.cpp +++ b/unit/engine/unittest_builder_config_change.cpp @@ -79,7 +79,10 @@ TEST_F(BuilderConfigChange, CheckEnvironment) { // Note: explicitly set these properties although most of them are the default values metrics.size(100,200).theme("dark").mode(kViewportModeHub); - config->disallowVideo(false).fontScale(1.0).screenMode(RootConfig::kScreenModeNormal).screenReader(false); + config->set(RootProperty::kDisallowVideo, false) + .set(RootProperty::kFontScale, 1.0) + .set(RootProperty::kScreenMode, RootConfig::kScreenModeNormal) + .set(RootProperty::kScreenReader, false); loadDocument(CHECK_ENVIRONMENT); ASSERT_TRUE(component); @@ -262,7 +265,10 @@ static const char *ALL_SETTINGS = R"apl( */ TEST_F(BuilderConfigChange, AllSettings) { metrics.size(400, 600).theme("light").mode(kViewportModeAuto); - config->disallowVideo(false).fontScale(2.0).screenMode(RootConfig::kScreenModeNormal).screenReader(true); + config->set(RootProperty::kDisallowVideo, false) + .set(RootProperty::kFontScale, 2.0) + .set(RootProperty::kScreenMode, RootConfig::kScreenModeNormal) + .set(RootProperty::kScreenReader, true); loadDocument(ALL_SETTINGS); ASSERT_TRUE(component); diff --git a/unit/engine/unittest_builder_preserve.cpp b/unit/engine/unittest_builder_preserve.cpp index 05b62ba..dd51aa4 100644 --- a/unit/engine/unittest_builder_preserve.cpp +++ b/unit/engine/unittest_builder_preserve.cpp @@ -606,7 +606,7 @@ static const char *PAGER_PRESERVE_ID = R"apl( */ TEST_F(BuilderPreserveTest, PagerPreserveId) { - config->pagerChildCache(10); // Cache all pages (simplifies dirty) + config->set(RootProperty::kPagerChildCache, 10); // Cache all pages (simplifies dirty) metrics.size(1000,500); loadDocument(PAGER_PRESERVE_ID); ASSERT_TRUE(component); @@ -682,7 +682,7 @@ static const char *PAGER_SET_VALUE = R"apl( */ TEST_F(BuilderPreserveTest, PagerChangePages) { - config->pagerChildCache(10); // Set the cache so that all pages will be laid out immediately + config->set(RootProperty::kPagerChildCache, 10); // Set the cache so that all pages will be laid out immediately metrics.size(1000,500); loadDocument(PAGER_SET_VALUE); ASSERT_TRUE(component); @@ -898,6 +898,7 @@ TEST_F(BuilderPreserveTest, VideoComponentPlayState) 3003, // Duration false, // Paused false, // Ended + false // Muted }; component->updateMediaState(ms, false); ASSERT_TRUE(IsEqual(1, component->getCalculated(kPropertyTrackIndex))); @@ -906,6 +907,7 @@ TEST_F(BuilderPreserveTest, VideoComponentPlayState) ASSERT_TRUE(IsEqual(3003, component->getCalculated(kPropertyTrackDuration))); ASSERT_TRUE(IsEqual(false, component->getCalculated(kPropertyTrackPaused))); ASSERT_TRUE(IsEqual(false, component->getCalculated(kPropertyTrackEnded))); + ASSERT_TRUE(IsEqual(false, component->getCalculated(kPropertyMuted))); configChangeReinflate(ConfigurationChange(200,200)); // Verify that the component changed on the reinflation @@ -918,6 +920,7 @@ TEST_F(BuilderPreserveTest, VideoComponentPlayState) ASSERT_TRUE(IsEqual(3003, component->getCalculated(kPropertyTrackDuration))); ASSERT_TRUE(IsEqual(false, component->getCalculated(kPropertyTrackPaused))); ASSERT_TRUE(IsEqual(false, component->getCalculated(kPropertyTrackEnded))); + ASSERT_TRUE(IsEqual(false, component->getCalculated(kPropertyMuted))); } diff --git a/unit/engine/unittest_builder_sequence.cpp b/unit/engine/unittest_builder_sequence.cpp index b354bc6..4411c14 100644 --- a/unit/engine/unittest_builder_sequence.cpp +++ b/unit/engine/unittest_builder_sequence.cpp @@ -358,13 +358,16 @@ TEST_F(BuilderTestSequence, LayoutCacheHorizontal) ASSERT_TRUE(CheckChildrenLaidOut(component, Range(5, 5), false)); component->update(kUpdateScrollPosition, 100); + advanceTime(10); ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 5), true)); } TEST_F(BuilderTestSequence, LayoutCacheHorizontalRTL) { loadDocument(LAYOUT_CACHE_TEST_HORIZONTAL); + advanceTime(10); component->update(kUpdateScrollPosition, 50); + advanceTime(10); ASSERT_EQ(Point(50, 0), component->scrollPosition()); ASSERT_EQ(6, component->getChildCount()); ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 4), true)); @@ -384,6 +387,7 @@ TEST_F(BuilderTestSequence, LayoutCacheHorizontalRTL) ASSERT_TRUE(CheckChildrenLaidOut(component, Range(5, 5), false)); component->update(kUpdateScrollPosition, -100); + advanceTime(10); ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 5), true)); for (auto i = 0 ; i < component->getChildCount() ; i++) { auto child = component->getChildAt(i); @@ -650,4 +654,171 @@ TEST_F(BuilderTestSequence, AutoSequenceSizing) auto frame = component->getCoreChildAt(0); ASSERT_EQ(Object(Rect(0, 0, 1024, 104)), frame->getCalculated(kPropertyBounds)); +} + +const char *RTL_SEQUENCE_VERTICAL_LOAD_TEST = R"( +{ + "type": "APL", + "version": "1.9", + "mainTemplate": { + "parameters": [ + "layoutDir", + "scrollDir" + ], + "items": { + "type": "Sequence", + "scrollDirection": "${scrollDir}", + "layoutDirection": "${layoutDir}", + "items": { + "type": "Text", + "id": "${data}", + "text": "${data}" + }, + "data": "${TestArray}" + } + } +} +)"; + +// Test that the correct number of children are inflated as a sequence scrolls regardless of layoutDirection +TEST_F(BuilderTestSequence, SequenceInflationTestVerticalRTL) +{ + std::vector v; + for( int i = 0; i < 50; i++ ) + v.push_back( i ); + + auto myArray = LiveArray::create(std::move(v)); + config->liveData("TestArray", myArray); + + config->set(RootProperty::kSequenceChildCache, 5); + + loadDocument(RTL_SEQUENCE_VERTICAL_LOAD_TEST, "{\"layoutDir\": \"RTL\", \"scrollDir\": \"vertical\"}"); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 10), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(11, 49), false)); + + component->update(kUpdateScrollPosition, 50); + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 14), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(15, 49), false)); + + for (int i = 0; i < 50; i++) + myArray->insert(0, -i); + + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 3), false)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(4, 99), true)); +} + +// Test that the correct number of children are inflated as a sequence scrolls regardless of layoutDirection +TEST_F(BuilderTestSequence, SequenceInflationTestVerticalLTR) +{ + std::vector v; + for( int i = 0; i < 50; i++ ) + v.push_back( i ); + + auto myArray = LiveArray::create(std::move(v)); + config->liveData("TestArray", myArray); + config->set(RootProperty::kSequenceChildCache, 5); + + loadDocument(RTL_SEQUENCE_VERTICAL_LOAD_TEST, "{\"layoutDir\": \"LTR\", \"scrollDir\": \"vertical\"}"); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 10), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(11, 49), false)); + + component->update(kUpdateScrollPosition, 50); + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 14), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(15, 49), false)); + + for (int i = 0; i < 50; i++) + myArray->insert(0, -i); + + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 3), false)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(4, 99), true)); +} + +// Test that the correct number of children are inflated as a sequence scrolls regardless of layoutDirection +TEST_F(BuilderTestSequence, SequenceInflationTestHorizontalRTL) +{ + std::vector v; + for( int i = 0; i < 50; i++ ) + v.push_back( i ); + + auto myArray = LiveArray::create(std::move(v)); + config->liveData("TestArray", myArray); + + config->set(RootProperty::kSequenceChildCache, 5); + + loadDocument(RTL_SEQUENCE_VERTICAL_LOAD_TEST, "{\"layoutDir\": \"RTL\", \"scrollDir\": \"horizontal\"}"); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 10), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(11, 49), false)); + + component->update(kUpdateScrollPosition, -100); + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 14), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(15, 49), false)); + + for (int i = 0; i < 50; i++) + myArray->insert(0, -i); + + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 31), false)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(32, 90), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(91, 99), false)); +} + +// Test that the correct number of children are inflated as a sequence scrolls regardless of layoutDirection +TEST_F(BuilderTestSequence, SequenceInflationTestHorizontalLTR) +{ + std::vector v; + for( int i = 0; i < 50; i++ ) + v.push_back( i ); + + auto myArray = LiveArray::create(std::move(v)); + config->liveData("TestArray", myArray); + + config->set(RootProperty::kSequenceChildCache, 5); + + loadDocument(RTL_SEQUENCE_VERTICAL_LOAD_TEST, "{\"layoutDir\": \"LTR\", \"scrollDir\": \"horizontal\"}"); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 10), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(11, 49), false)); + + component->update(kUpdateScrollPosition, 100); + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 14), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(15, 49), false)); + + for (int i = 0; i < 50; i++) + myArray->insert(0, -i); + + root->clearPending(); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(0, 31), false)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(32, 90), true)); + + ASSERT_TRUE(CheckChildrenLaidOut(component, Range(91, 99), false)); } \ No newline at end of file diff --git a/unit/engine/unittest_context.cpp b/unit/engine/unittest_context.cpp index 75ed31e..bf5788b 100644 --- a/unit/engine/unittest_context.cpp +++ b/unit/engine/unittest_context.cpp @@ -26,7 +26,7 @@ class ContextTest : public MemoryWrapper { .theme("green") .shape(apl::ROUND) .mode(apl::kViewportModeTV); - auto r = RootConfig().agent("UnitTests", "1.0"); + auto r = RootConfig().set(RootProperty::kAgentName, "UnitTests"); r.setEnvironmentValue("testEnvironment", "23.2"); c = Context::createTestContext(m,r); } @@ -47,7 +47,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("1.9", env.get("aplVersion").asString()); + EXPECT_EQ("2022.1", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); @@ -91,24 +91,25 @@ TEST_F(ContextTest, Basic) TEST_F(ContextTest, AlternativeConfig) { - auto root = RootConfig().agent("MyTest", "0.2") + auto root = RootConfig() + .set({{RootProperty::kAgentName, "MyTest"}, {RootProperty::kAgentVersion, "0.2"}}) .set(RootProperty::kDisallowDialog, true) .set(RootProperty::kDisallowEditText, true) - .disallowVideo(true) - .reportedAPLVersion("1.2") - .allowOpenUrl(true) - .animationQuality(RootConfig::kAnimationQualitySlow) + .set(RootProperty::kDisallowVideo, true) + .set(RootProperty::kReportedVersion, "1.2") + .set(RootProperty::kAllowOpenUrl, true) + .set(RootProperty::kAnimationQuality, RootConfig::kAnimationQualitySlow) .setEnvironmentValue("testEnvironment", 122) - .fontScale(2.0) - .screenMode(RootConfig::kScreenModeHighContrast) - .screenReader(true) - .doublePressTimeout(2000) + .set(RootProperty::kFontScale, 2.0) + .set(RootProperty::kScreenMode, RootConfig::kScreenModeHighContrast) + .set(RootProperty::kScreenReader, true) + .set(RootProperty::kDoublePressTimeout, 2000) .set(RootProperty::kLang, "en-US") .set(RootProperty::kLayoutDirection, "RTL") - .longPressTimeout(2100) - .minimumFlingVelocity(565) - .pressedDuration(999) - .tapOrScrollTimeout(777); + .set(RootProperty::kLongPressTimeout, 2100) + .set(RootProperty::kMinimumFlingVelocity, 565) + .set(RootProperty::kPressedDuration, 999) + .set(RootProperty::kTapOrScrollTimeout, 777); c = Context::createTestContext(Metrics().size(400,400), root); @@ -201,7 +202,7 @@ TEST_F(ContextTest, Time) const unsigned long long utcTime = 1567697957924; const long long deltaTime = 3600 * 1000; - auto rootConfig = RootConfig().utcTime(utcTime).localTimeAdjustment(deltaTime); + auto rootConfig = RootConfig().set(RootProperty::kUTCTime, utcTime).set(RootProperty::kLocalTimeAdjustment, deltaTime); ASSERT_EQ(utcTime, rootConfig.getUTCTime()); ASSERT_EQ(deltaTime, rootConfig.getLocalTimeAdjustment()); @@ -228,7 +229,7 @@ TEST_F(ContextTest, Time) // Demonstrate how to set the root config to reflect the current time in local time. auto now = std::chrono::duration_cast( std::chrono::system_clock::now().time_since_epoch()); - rootConfig = RootConfig().utcTime(now.count()); + rootConfig = RootConfig().set(RootProperty::kUTCTime, now.count()); ASSERT_EQ(std::chrono::milliseconds{static_cast(rootConfig.getUTCTime())}, now); diff --git a/unit/engine/unittest_current_time.cpp b/unit/engine/unittest_current_time.cpp index f6b22d9..3d46a8a 100644 --- a/unit/engine/unittest_current_time.cpp +++ b/unit/engine/unittest_current_time.cpp @@ -37,7 +37,7 @@ TEST_F(CurrentTimeTest, Basic) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME); ASSERT_TRUE(component); @@ -75,7 +75,7 @@ TEST_F(CurrentTimeTest, Year) const apl_time_t START_TIME = 1567685739476; // Start in 1989 - config->utcTime(START_TIME - 30.0 * 1000.0 * 3600.0 * 24.0 * 365.0); + config->set(RootProperty::kUTCTime, START_TIME - 30.0 * 1000.0 * 3600.0 * 24.0 * 365.0); loadDocument(TIME_YEAR); ASSERT_TRUE(component); @@ -118,7 +118,7 @@ TEST_F(CurrentTimeTest, Month) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_MONTH); ASSERT_TRUE(component); @@ -144,7 +144,7 @@ TEST_F(CurrentTimeTest, Date) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476.0; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_DATE); ASSERT_TRUE(component); @@ -172,7 +172,7 @@ TEST_F(CurrentTimeTest, UTCDate) { // Thu Sep 05 2019 15:39:17 (UTCTime) const apl_time_t START_TIME = 1567697957924; - config->utcTime(START_TIME).localTimeAdjustment(-16.0 * 3600.0 * 1000.0); // -16 hours from UTC + config->set(RootProperty::kUTCTime, START_TIME).set(RootProperty::kLocalTimeAdjustment, -16.0 * 3600.0 * 1000.0); // -16 hours from UTC loadDocument(TIME_UTC_DATE); ASSERT_TRUE(component); @@ -201,7 +201,7 @@ TEST_F(CurrentTimeTest, WeekDay) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_WEEK_DAY); ASSERT_TRUE(component); @@ -228,7 +228,7 @@ TEST_F(CurrentTimeTest, UTCWeekDay) { // Thu Sep 05 2019 15:39:17 (UTCTime) const apl_time_t START_TIME = 1567697957924; - config->utcTime(START_TIME).localTimeAdjustment(-16 * 3600 * 1000); // -16 hours from UTC + config->set(RootProperty::kUTCTime, START_TIME).set(RootProperty::kLocalTimeAdjustment, -16 * 3600 * 1000); // -16 hours from UTC loadDocument(TIME_UTC_WEEK_DAY); ASSERT_TRUE(component); @@ -257,7 +257,7 @@ TEST_F(CurrentTimeTest, Hours) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_HOURS); ASSERT_TRUE(component); ASSERT_TRUE(IsEqual("12", component->getCalculated(kPropertyText).asString())); @@ -286,7 +286,7 @@ TEST_F(CurrentTimeTest, UTCHours) { // Thu Sep 05 2019 15:39:17 (UTCTime) const apl_time_t START_TIME = 1567697957924; - config->utcTime(START_TIME).localTimeAdjustment(+9 * 3600 * 1000); // +9 hours from UTC + config->set(RootProperty::kUTCTime, START_TIME).set(RootProperty::kLocalTimeAdjustment, 9 * 3600 * 1000); // +9 hours from UTC loadDocument(TIME_UTC_HOURS); ASSERT_TRUE(component); @@ -323,7 +323,7 @@ TEST_F(CurrentTimeTest, Minutes) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_MINUTES); ASSERT_TRUE(component); @@ -354,7 +354,7 @@ TEST_F(CurrentTimeTest, UTCMinutes) { // Thu Sep 05 2019 15:39:17 (UTCTime) const apl_time_t START_TIME = 1567697957924; - config->utcTime(START_TIME).localTimeAdjustment(+6.5 * 3600 * 1000); // +6.5 hours from UTC + config->set(RootProperty::kUTCTime, START_TIME).set(RootProperty::kLocalTimeAdjustment, 6.5 * 3600 * 1000); // +6.5 hours from UTC loadDocument(TIME_UTC_MINUTES); ASSERT_TRUE(component); @@ -391,7 +391,7 @@ TEST_F(CurrentTimeTest, Seconds) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_SECONDS); ASSERT_TRUE(component); @@ -422,7 +422,7 @@ TEST_F(CurrentTimeTest, UTCSeconds) { // Thu Sep 05 2019 15:39:17 (UTCTime) const apl_time_t START_TIME = 1567697957924; - config->utcTime(START_TIME).localTimeAdjustment(6.5 * 3600.0 * 1000.0); // +6.5 hours from UTC + config->set(RootProperty::kUTCTime, START_TIME).set(RootProperty::kLocalTimeAdjustment, 6.5 * 3600.0 * 1000.0); // +6.5 hours from UTC loadDocument(TIME_UTC_SECONDS); ASSERT_TRUE(component); @@ -458,7 +458,7 @@ TEST_F(CurrentTimeTest, Milliseconds) { // Thu Sep 05 2019 12:15:39 (UTCTime) const apl_time_t START_TIME = 1567685739476; - config->utcTime(START_TIME); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_MILLISECONDS); ASSERT_TRUE(component); @@ -489,7 +489,7 @@ TEST_F(CurrentTimeTest, UTCMilliseconds) { // Thu Sep 05 2019 15:39:17 (UTCTime) const apl_time_t START_TIME = 1567697957924; - config->utcTime(START_TIME).localTimeAdjustment(+6.5 * 3600 * 1000); // +6.5 hours from UTC + config->set(RootProperty::kUTCTime, START_TIME).set(RootProperty::kLocalTimeAdjustment, 6.5 * 3600 * 1000); // +6.5 hours from UTC loadDocument(TIME_UTC_MILLISECONDS); ASSERT_TRUE(component); @@ -545,8 +545,8 @@ TEST_F(CurrentTimeTest, Format) // Thu Sep 05 2019 21:39:07 (LocalTime) const apl_time_t START_TIME = 1567696147924; - config->localTimeAdjustment(+6.5 * 3600.0 * 1000.0); - config->utcTime(START_TIME); + config->set(RootProperty::kLocalTimeAdjustment, 6.5 * 3600.0 * 1000.0); + config->set(RootProperty::kUTCTime, START_TIME); loadDocument(TIME_FORMAT); ASSERT_TRUE(component); diff --git a/unit/engine/unittest_dependant.cpp b/unit/engine/unittest_dependant.cpp index 45698d2..02912f1 100644 --- a/unit/engine/unittest_dependant.cpp +++ b/unit/engine/unittest_dependant.cpp @@ -1089,4 +1089,290 @@ TEST_F(DependantTest, LayoutLiveArray) advanceTime(10); ASSERT_EQ("5", calculatedStuff->getCalculated(kPropertyText).asString()); +} + +static const char *LAYOUT_LIVE_ARRAY_SWAP = R"({ + "type": "APL", + "version": "1.10", + "theme": "dark", + "layouts": { + "MyLayout": { + "parameters": [ + "things" + ], + "item": { + "type": "Sequence", + "id": "cont", + "height": "100%", + "width": "100%", + "direction": "column", + "data": "${things}", + "items": [ + { + "type": "Container", + "bind": [ + { + "name": "Item", + "type": "number", + "value": "${data}" + } + ], + "items": [ + { + "type": "Text", + "display": "${Item > 0 ? 'normal' : 'none'}", + "text": "${Item}" + }, + { + "type": "Text", + "display": "${Item <= 0 ? 'normal' : 'none'}", + "text": "NAN" + } + ] + } + ] + } + } + }, + "mainTemplate": { + "items": { + "type": "MyLayout", + "things": "${ExampleArray}" + } + } +})"; + +TEST_F(DependantTest, LayoutLiveArrayFromEmpty) +{ + auto la = LiveArray::create(); + config->liveData("ExampleArray", la); + loadDocument(LAYOUT_LIVE_ARRAY_SWAP); + ASSERT_TRUE(component); + + advanceTime(10); + ASSERT_TRUE(CheckDirty(root)); + ASSERT_EQ(0, component->getChildCount()); + + la->push_back(0); + la->push_back(1); + advanceTime(10); + + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(0))); + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(1), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(0), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(1))); + + auto notifyInternal0 = component->getChildAt(0)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notifyInternal0.size()); + ASSERT_EQ(Object("insert"), notifyInternal0.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notifyInternal0.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getChildAt(0)->getUniqueId()), notifyInternal0.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notifyInternal0.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notifyInternal0.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getChildAt(1)->getUniqueId()), notifyInternal0.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 0)); + + auto notifyInternal1 = component->getChildAt(1)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notifyInternal1.size()); + ASSERT_EQ(Object("insert"), notifyInternal1.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notifyInternal1.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getChildAt(0)->getUniqueId()), notifyInternal1.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notifyInternal1.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notifyInternal1.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getChildAt(1)->getUniqueId()), notifyInternal1.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 1)); + + auto notify = component->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notify.size()); + ASSERT_EQ(Object("insert"), notify.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notify.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getUniqueId()), notify.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notify.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notify.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getUniqueId()), notify.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + root->clearDirty(); + + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_EQ(2, component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(0, component->getChildAt(0)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("NAN", component->getChildAt(0)->getChildAt(1)->getCalculated(kPropertyText).asString()); + + ASSERT_EQ(0, component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(2, component->getChildAt(1)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("1", component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyText).asString()); + + la->update(0, 2); + la->update(1, 0); + advanceTime(10); + + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(0), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash, + kPropertyDisplay, kPropertyText)); + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(1), + kPropertyBounds, kPropertyInnerBounds, kPropertyVisualHash, kPropertyDisplay)); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(0), + kPropertyBounds, kPropertyInnerBounds, kPropertyVisualHash, kPropertyDisplay, + kPropertyText)); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(1), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash, + kPropertyDisplay)); + + notifyInternal0 = component->getChildAt(0)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(0, notifyInternal0.size()); + ASSERT_TRUE(CheckDirty(component->getChildAt(0), kPropertyNotifyChildrenChanged)); + + notifyInternal1 = component->getChildAt(1)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(0, notifyInternal1.size()); + ASSERT_TRUE(CheckDirty(component->getChildAt(1), kPropertyNotifyChildrenChanged)); + + notify = component->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(0, notify.size()); + ASSERT_TRUE(CheckDirty(component)); + + root->clearDirty(); + + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_EQ(0, component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(2, component->getChildAt(0)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("2", component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyText).asString()); + + ASSERT_EQ(2, component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(0, component->getChildAt(1)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("NAN", component->getChildAt(1)->getChildAt(1)->getCalculated(kPropertyText).asString()); +} + +TEST_F(DependantTest, LayoutLiveArrayFromEmptyReplace) +{ + auto la = LiveArray::create(); + config->liveData("ExampleArray", la); + loadDocument(LAYOUT_LIVE_ARRAY_SWAP); + ASSERT_TRUE(component); + + advanceTime(10); + ASSERT_TRUE(CheckDirty(root)); + + ASSERT_EQ(0, component->getChildCount()); + + la->push_back(0); + la->push_back(1); + advanceTime(10); + + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(0))); + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(1), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(0), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(1))); + + auto notifyInternal0 = component->getChildAt(0)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notifyInternal0.size()); + ASSERT_EQ(Object("insert"), notifyInternal0.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notifyInternal0.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getChildAt(0)->getUniqueId()), notifyInternal0.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notifyInternal0.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notifyInternal0.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getChildAt(1)->getUniqueId()), notifyInternal0.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 0)); + + auto notifyInternal1 = component->getChildAt(1)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notifyInternal1.size()); + ASSERT_EQ(Object("insert"), notifyInternal1.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notifyInternal1.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getChildAt(0)->getUniqueId()), notifyInternal1.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notifyInternal1.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notifyInternal1.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getChildAt(1)->getUniqueId()), notifyInternal1.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 1)); + + auto cachedUid0 = component->getChildAt(0)->getUniqueId(); + auto cachedUid1 = component->getChildAt(1)->getUniqueId(); + auto notify = component->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notify.size()); + ASSERT_EQ(Object("insert"), notify.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notify.at(0).getMap().at("index")); + ASSERT_EQ(Object(cachedUid0), notify.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notify.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notify.at(1).getMap().at("index")); + ASSERT_EQ(Object(cachedUid1), notify.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + root->clearDirty(); + + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_EQ(2, component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(0, component->getChildAt(0)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("NAN", component->getChildAt(0)->getChildAt(1)->getCalculated(kPropertyText).asString()); + + ASSERT_EQ(0, component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(2, component->getChildAt(1)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("1", component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyText).asString()); + + la->clear(); + la->push_back(2); + la->push_back(0); + advanceTime(10); + + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(0), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash)); + ASSERT_TRUE(CheckDirty(component->getChildAt(0)->getChildAt(1))); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(0))); + ASSERT_TRUE(CheckDirty(component->getChildAt(1)->getChildAt(1), + kPropertyBounds, kPropertyInnerBounds, kPropertyLaidOut, kPropertyVisualHash)); + + notifyInternal0 = component->getChildAt(0)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notifyInternal0.size()); + ASSERT_EQ(Object("insert"), notifyInternal0.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notifyInternal0.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getChildAt(0)->getUniqueId()), notifyInternal0.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notifyInternal0.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notifyInternal0.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getChildAt(1)->getUniqueId()), notifyInternal0.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 0)); + + notifyInternal1 = component->getChildAt(1)->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(2, notifyInternal1.size()); + ASSERT_EQ(Object("insert"), notifyInternal1.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notifyInternal1.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getChildAt(0)->getUniqueId()), notifyInternal1.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notifyInternal1.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notifyInternal1.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getChildAt(1)->getUniqueId()), notifyInternal1.at(1).getMap().at("uid")); + ASSERT_TRUE(CheckChildLaidOutDirtyFlagsWithNotify(component, 1)); + + auto sp = component->scrollPosition(); + + notify = component->getCalculated(kPropertyNotifyChildrenChanged).getArray(); + ASSERT_EQ(4, notify.size()); + ASSERT_EQ(Object("insert"), notify.at(0).getMap().at("action")); + ASSERT_EQ(Object(0), notify.at(0).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(0)->getUniqueId()), notify.at(0).getMap().at("uid")); + ASSERT_EQ(Object("insert"), notify.at(1).getMap().at("action")); + ASSERT_EQ(Object(1), notify.at(1).getMap().at("index")); + ASSERT_EQ(Object(component->getChildAt(1)->getUniqueId()), notify.at(1).getMap().at("uid")); + ASSERT_EQ(Object("remove"), notify.at(2).getMap().at("action")); + ASSERT_EQ(Object(2), notify.at(2).getMap().at("index")); + ASSERT_EQ(Object(cachedUid0), notify.at(2).getMap().at("uid")); + ASSERT_EQ(Object("remove"), notify.at(3).getMap().at("action")); + ASSERT_EQ(Object(2), notify.at(3).getMap().at("index")); + ASSERT_EQ(Object(cachedUid1), notify.at(3).getMap().at("uid")); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged, kPropertyScrollPosition)); + + ASSERT_EQ(sp, component->scrollPosition()); + + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_EQ(0, component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(2, component->getChildAt(0)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("2", component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyText).asString()); + + ASSERT_EQ(2, component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ(0, component->getChildAt(1)->getChildAt(1)->getCalculated(kPropertyDisplay).asNumber()); + ASSERT_EQ("NAN", component->getChildAt(1)->getChildAt(1)->getCalculated(kPropertyText).asString()); } \ No newline at end of file diff --git a/unit/engine/unittest_hover.cpp b/unit/engine/unittest_hover.cpp index 5f481b9..fa7cbb0 100644 --- a/unit/engine/unittest_hover.cpp +++ b/unit/engine/unittest_hover.cpp @@ -212,7 +212,7 @@ class HoverTest : public DocumentWrapper { if (str != TEXT_TEXT) { ASSERT_EQ(1, text->getDirty().count(kPropertyText)); } - ASSERT_EQ(StyledText::create(*context, str), text->getCalculated(kPropertyText)); + ASSERT_TRUE(IsEqual(StyledText::create(*context, str), text->getCalculated(kPropertyText))); } void resetTextString() { diff --git a/unit/engine/unittest_memory.cpp b/unit/engine/unittest_memory.cpp index 6ab2158..8a70dd3 100644 --- a/unit/engine/unittest_memory.cpp +++ b/unit/engine/unittest_memory.cpp @@ -46,7 +46,7 @@ TEST_F(MemoryTest, Basic) ASSERT_FALSE(content->isError()); auto m = Metrics().size(1024,800).theme("dark"); - auto config = RootConfig().defaultIdleTimeout(15000); + auto config = RootConfig().set(RootProperty::kDefaultIdleTimeout, 15000); root = RootContext::create(m, content, config); ASSERT_TRUE(root); @@ -91,7 +91,9 @@ TEST_F(MemoryTest, Text) ASSERT_FALSE(content->isError()); auto m = Metrics().size(1024,800).theme("dark"); - auto config = RootConfig().defaultIdleTimeout(15000).measure(std::make_shared()); + auto config = RootConfig() + .set(RootProperty::kDefaultIdleTimeout, 15000) + .measure(std::make_shared()); root = RootContext::create(m, content, config); ASSERT_TRUE(root); diff --git a/unit/engine/unittest_resources.cpp b/unit/engine/unittest_resources.cpp index 5048322..686a56c 100644 --- a/unit/engine/unittest_resources.cpp +++ b/unit/engine/unittest_resources.cpp @@ -208,7 +208,7 @@ TEST_F(ResourceTest, BasicInfo) TEST_F(ResourceTest, DisabledProvenance) { metrics.size(1024,800); - config->trackProvenance(false); + config->set(RootProperty::kTrackProvenance, false); loadDocument(BASIC_TEST); ASSERT_STREQ("", context->provenance("@one").c_str()); @@ -360,19 +360,19 @@ TEST_F(ResourceTest, LinearGradient) auto object = context->opt("@myLinear"); ASSERT_TRUE(object.isGradient()); - auto grad = object.getGradient(); + auto& grad = object.getGradient(); ASSERT_EQ(Gradient::LINEAR, grad.getType()); - ASSERT_EQ(0, grad.getAngle()); + ASSERT_EQ(Object(0), grad.getProperty(kGradientPropertyAngle)); - auto colorRange = grad.getColorRange(); + auto colorRange = grad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0)); - ASSERT_EQ(Color(Color::RED), colorRange.at(1)); + ASSERT_EQ(Object(Color(Color::BLUE)), colorRange.at(0)); + ASSERT_EQ(Object(Color(Color::RED)), colorRange.at(1)); - auto inputRange = grad.getInputRange(); + auto inputRange = grad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); - ASSERT_EQ(0, inputRange.at(0)); - ASSERT_EQ(1, inputRange.at(1)); + ASSERT_EQ(Object(0), inputRange.at(0)); + ASSERT_EQ(Object(1), inputRange.at(1)); } static const char *RADIAL_GRADIENT = R"({ @@ -412,18 +412,18 @@ TEST_F(ResourceTest, RadialGradient) auto object = context->opt("@myRadial"); ASSERT_TRUE(object.isGradient()); - auto grad = object.getGradient(); + auto& grad = object.getGradient(); ASSERT_EQ(Gradient::RADIAL, grad.getType()); - auto colorRange = grad.getColorRange(); + auto colorRange = grad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0)); - ASSERT_EQ(Color(Color::RED), colorRange.at(1)); + ASSERT_EQ(Object(Color(Color::BLUE)), colorRange.at(0)); + ASSERT_EQ(Object(Color(Color::RED)), colorRange.at(1)); - auto inputRange = grad.getInputRange(); + auto inputRange = grad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); - ASSERT_EQ(0.2, inputRange.at(0)); - ASSERT_EQ(0.5, inputRange.at(1)); + ASSERT_EQ(Object(0.2), inputRange.at(0)); + ASSERT_EQ(Object(0.5), inputRange.at(1)); } static const char *RICH_LINEAR = R"({ @@ -474,21 +474,21 @@ TEST_F(ResourceTest, RichLinearGradient) auto object = context->opt("@myLinear"); ASSERT_TRUE(object.isGradient()); - auto grad = object.getGradient(); + auto& grad = object.getGradient(); ASSERT_EQ(Gradient::LINEAR, grad.getType()); - ASSERT_EQ(45, grad.getAngle()); + ASSERT_EQ(Object(45), grad.getProperty(kGradientPropertyAngle)); - auto colorRange = grad.getColorRange(); + auto colorRange = grad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(3, colorRange.size()); - ASSERT_EQ(Color(0xff00007f), colorRange.at(0)); - ASSERT_EQ(Color(0x0000ffff), colorRange.at(1)); - ASSERT_EQ(Color(0x0000ff7f), colorRange.at(2)); + ASSERT_EQ(Object(Color(0xff00007f)), colorRange.at(0)); + ASSERT_EQ(Object(Color(0x0000ffff)), colorRange.at(1)); + ASSERT_EQ(Object(Color(0x0000ff7f)), colorRange.at(2)); - auto inputRange = grad.getInputRange(); + auto inputRange = grad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(3, inputRange.size()); - ASSERT_EQ(0.25, inputRange.at(0)); - ASSERT_EQ(0.8, inputRange.at(1)); - ASSERT_EQ(1, inputRange.at(2)); + ASSERT_EQ(Object(0.25), inputRange.at(0)); + ASSERT_EQ(Object(0.8), inputRange.at(1)); + ASSERT_EQ(Object(1), inputRange.at(2)); } static const char *GRADIENT_ANGLE = R"({ @@ -616,8 +616,8 @@ TEST_F(ResourceTest, GradientAngle) auto colorRange = grad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::RED), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).asColor()); + ASSERT_EQ(Color(Color::RED), colorRange.at(0).getColor()); + ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).getColor()); auto inputRange = grad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -670,8 +670,8 @@ TEST_F(ResourceTest, GradientRadialFull) auto colorRange = grad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::RED), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).asColor()); + ASSERT_EQ(Color(Color::RED), colorRange.at(0).getColor()); + ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).getColor()); auto inputRange = grad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -683,25 +683,23 @@ TEST_F(ResourceTest, GradientRadialFull) ASSERT_EQ(0.7071, grad.getProperty(kGradientPropertyRadius).asNumber()); } -static const char *EASING = R"( +static const char *EASING = R"({ + "type": "APL", + "version": "1.4", + "mainTemplate": { + "items": { + "type": "Text", + "text": "${@jagged(0.25)}" + } + }, + "resources": [ { - "type": "APL", - "version": "1.4", - "mainTemplate": { - "items": { - "type": "Text", - "text": "${@jagged(0.25)}" - } - }, - "resources": [ - { - "easings": { - "jagged": "line(0.25,0.75) end(1,1) " - } - } - ] + "easings": { + "jagged": "line(0.25,0.75) end(1,1) " + } } -)"; + ] +})"; TEST_F(ResourceTest, Easing) { diff --git a/unit/extension/CMakeLists.txt b/unit/extension/CMakeLists.txt index 96e1c9e..d8835dd 100644 --- a/unit/extension/CMakeLists.txt +++ b/unit/extension/CMakeLists.txt @@ -19,4 +19,5 @@ target_sources_local(unittest unittest_requested_extension.cpp unittest_extension_handler.cpp unittest_extension_mediator.cpp + unittest_extension_session.cpp ) \ No newline at end of file diff --git a/unit/extension/unittest_extension_client.cpp b/unit/extension/unittest_extension_client.cpp index 5ed9042..bcbedfb 100644 --- a/unit/extension/unittest_extension_client.cpp +++ b/unit/extension/unittest_extension_client.cpp @@ -29,7 +29,7 @@ class ExtensionClientTest : public DocumentWrapper { void createConfig(JsonData&& document) { configPtr = std::make_shared(); - configPtr->agent("Unit tests", "1.0").timeManager(loop).session(session); + configPtr->set(RootProperty::kAgentName, "Unit tests").timeManager(loop).session(session); content = Content::create(std::move(document), session); ASSERT_TRUE(content->isReady()); } @@ -2735,7 +2735,7 @@ TEST_F(ExtensionClientTest, ExtensionComponentCommandAndEvent) { ASSERT_TRUE(client->processMessage(root, extensionEvent)); ASSERT_TRUE(CheckDirty(touchwrapper, kPropertyShadowColor, kPropertyVisualHash)); ASSERT_TRUE(CheckDirty(root, touchwrapper)); - ASSERT_EQ(touchwrapper->getCalculated(kPropertyShadowColor).asColor().get(), Color::ColorConstants::BLUE); + ASSERT_EQ(touchwrapper->getCalculated(kPropertyShadowColor).getColor(), Color::ColorConstants::BLUE); } TEST_F(ExtensionClientTest, ExtensionComponentProperty) { @@ -2912,7 +2912,7 @@ TEST_F(ExtensionClientTest, ExtensionClientDisconnection) { auto alexaButton = component->findComponentById("AlexaButton"); ASSERT_EQ(alexaButton->getType(), kComponentTypeTouchWrapper); // Verifies that onFatalError was called. - ASSERT_EQ(alexaButton->getCalculated(kPropertyShadowColor).asColor().get(), Color::ColorConstants::BLACK); + ASSERT_EQ(alexaButton->getCalculated(kPropertyShadowColor).getColor(), Color::ColorConstants::BLACK); } static const char* EXT_DOC_EXTCOMP_INVALID_COMPONENT_ID = R"({ diff --git a/unit/extension/unittest_extension_command.cpp b/unit/extension/unittest_extension_command.cpp index c26a86e..59d664f 100644 --- a/unit/extension/unittest_extension_command.cpp +++ b/unit/extension/unittest_extension_command.cpp @@ -146,7 +146,7 @@ TEST_F(ExtensionCommandTest, BasicCommand) ASSERT_TRUE(ext.isMap()); ASSERT_TRUE(IsEqual(7, ext.get("value"))); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); // The SetValue command should also have run by now ASSERT_TRUE(IsEqual(Color(Color::BLACK), frame->getCalculated(kPropertyBackgroundColor))); @@ -186,7 +186,7 @@ TEST_F(ExtensionCommandTest, BasicCommandWithActionRef) ASSERT_TRUE(ext.isMap()); ASSERT_TRUE(IsEqual(7, ext.get("value"))); - ASSERT_FALSE(event.getActionRef().isEmpty()); + ASSERT_FALSE(event.getActionRef().empty()); // The SetValue command should NOT have run ASSERT_TRUE(IsEqual(Color(Color::WHITE), frame->getCalculated(kPropertyBackgroundColor))); @@ -450,7 +450,7 @@ TEST_F(ExtensionCommandTest, FastModeAllowed) ASSERT_TRUE(IsEqual(0.5, ext.get("value"))); // Scroll position of 50% ASSERT_TRUE(IsEqual("MyScrollView", ext.get("id"))); - ASSERT_TRUE(event.getActionRef().isEmpty()); // No action ref is generated in fast mode + ASSERT_TRUE(event.getActionRef().empty()); // No action ref is generated in fast mode // The SetValue command should have run ASSERT_TRUE(IsEqual(Color(Color::RED), frame->getCalculated(kPropertyBackgroundColor))); @@ -556,7 +556,7 @@ TEST_F(ExtensionCommandTest, OptionalProperties) ASSERT_TRUE(IsEqual(-1001, ext.get("value"))); // Expect the default value ASSERT_TRUE(IsEqual("NO_ID", ext.get("id"))); // Expect the default value - ASSERT_TRUE(event.getActionRef().isEmpty()); // No action ref is generated in fast mode + ASSERT_TRUE(event.getActionRef().empty()); // No action ref is generated in fast mode // The SetValue command should have run ASSERT_TRUE(IsEqual(Color(Color::RED), frame->getCalculated(kPropertyBackgroundColor))); diff --git a/unit/extension/unittest_extension_mediator.cpp b/unit/extension/unittest_extension_mediator.cpp index 9f6a1bf..bb71599 100644 --- a/unit/extension/unittest_extension_mediator.cpp +++ b/unit/extension/unittest_extension_mediator.cpp @@ -18,6 +18,10 @@ #include "../testeventloop.h" #include "apl/extension/extensioncomponent.h" +#include +#include +#include + using namespace apl; using namespace alexaext; @@ -206,7 +210,6 @@ static bool sForceFail = false; * Sample Extension for testing. */ class TestExtension final : public alexaext::ExtensionBase { - public: explicit TestExtension(const std::set& uris) : ExtensionBase(uris) {}; @@ -287,6 +290,286 @@ class TestExtension final : public alexaext::ExtensionBase { ResourceHolderPtr mResource; }; +enum class InteractionKind { + kSessionStarted, + kSessionEnded, + kActivityRegistered, + kActivityUnregistered, + kDisplayStateChanged, + kCommandReceived, + kResourceReady, + kUpdateComponentReceived, +}; + +/** + * Defines utilities to record extension interactions for verification purposes. Can be used + * as a mixin or standalone. + */ +class LifecycleInteractionRecorder { +public: + virtual ~LifecycleInteractionRecorder() = default; + + struct Interaction { + Interaction(InteractionKind kind) + : kind(kind) + {} + Interaction(InteractionKind kind, const Object& value) + : kind(kind), + value(value) + {} + Interaction(InteractionKind kind, const alexaext::ActivityDescriptor& activity) + : kind(kind), + activity(activity) + {} + Interaction(InteractionKind kind, const alexaext::ActivityDescriptor& activity, const Object& value) + : kind(kind), + activity(activity), + value(value) + {} + + bool operator==(const Interaction& other) const { + return kind == other.kind + && activity == other.activity + && value == other.value; + } + + bool operator!=(const Interaction& rhs) const { return !(rhs == *this); } + + InteractionKind kind; + alexaext::ActivityDescriptor activity = ActivityDescriptor("", nullptr, ""); + Object value = Object::NULL_OBJECT(); + }; + + ::testing::AssertionResult verifyNextInteraction(const Interaction& interaction) { + if (mRecordedInteractions.empty()) { + return ::testing::AssertionFailure() << "Expected an interaction but none was found"; + } + + auto nextInteraction = mRecordedInteractions.front(); + if (interaction != nextInteraction) { + return ::testing::AssertionFailure() << "Found mismatched interactions"; + } + + // Consume the interaction since it was a match + mRecordedInteractions.pop_front(); + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult verifyUnordered(std::vector interactions) { + while (!interactions.empty()) { + if (mRecordedInteractions.empty()) { + return ::testing::AssertionFailure() << "Expected an interaction but none was found"; + } + + const auto& targetInteraction = interactions.back(); + bool found = false; + for (auto it = mRecordedInteractions.begin(); it != mRecordedInteractions.end(); it++) { + if (*it == targetInteraction) { + interactions.pop_back(); + mRecordedInteractions.erase(it); + found = true; + break; + } + } + + if (!found) return ::testing::AssertionFailure() << "Interaction not found"; + } + + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult verifyNoMoreInteractions() { + if (!mRecordedInteractions.empty()) { + return ::testing::AssertionFailure() << "Expected no more interactions, but some were found"; + } + return ::testing::AssertionSuccess(); + } + + virtual void recordInteraction(const Interaction& interaction) { + mRecordedInteractions.emplace_back(interaction); + } + +private: + std::deque mRecordedInteractions; +}; + +/** + * Extension that uses activity-based APIs. + */ +class LifecycleTestExtension : public alexaext::ExtensionBase, + public LifecycleInteractionRecorder { +public: + static const char *URI; + static const char *TOKEN; + + explicit LifecycleTestExtension(const std::string& uri = URI) + : ExtensionBase(uri), + LifecycleInteractionRecorder(), + lastActivity(uri, nullptr, "") + {} + ~LifecycleTestExtension() override = default; + + rapidjson::Document createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) override { + const auto& uri = activity.getURI(); + lastActivity = activity; + + if (failRegistration) { + return alexaext::RegistrationFailure::forException(uri, "Failure for unit tests"); + } + + const auto *settings = RegistrationRequest::SETTINGS().Get(registrationRequest); + std::string prefix = ""; + if (settings) { + prefix = GetWithDefault("prefix", settings, ""); + prefixByActivity.emplace(activity, prefix); + } + + rapidjson::Document response = RegistrationSuccess("1.0") + .uri(uri) + .token(useAutoToken ? "" : TOKEN) + .schema("1.0", [uri, prefix](ExtensionSchema schema) { + schema + .uri(uri) + .dataType("liveMapSchema", [](TypeSchema &dataTypeSchema) { + dataTypeSchema + .property("state", "string"); + }) + .dataType("liveArraySchema") + .command("PublishState") + .event(prefix + "ExtensionReady") + .liveDataMap(prefix + "liveMap", [](LiveDataSchema &liveDataSchema) { + liveDataSchema.dataType("liveMapSchema"); + }) + .liveDataArray(prefix + "liveArray", [](LiveDataSchema &liveDataSchema) { + liveDataSchema.dataType("liveArraySchema"); + }); + }); + + // The schema API doesn't support component definitions yet, so we amend the response + // directly here instead + rapidjson::Value component; + component.SetObject(); + component.AddMember("name", "Component", response.GetAllocator()); + rapidjson::Value components; + components.SetArray(); + components.PushBack(component, response.GetAllocator()); + response["schema"].AddMember("components", components, response.GetAllocator()); + return response; + } + + void onSessionStarted(const SessionDescriptor& session) override { + recordInteraction({InteractionKind::kSessionStarted, session.getId()}); + } + + void onSessionEnded(const SessionDescriptor& session) override { + recordInteraction({InteractionKind::kSessionEnded, session.getId()}); + } + + void onActivityRegistered(const ActivityDescriptor& activity) override { + recordInteraction({InteractionKind::kActivityRegistered, activity}); + } + + void onActivityUnregistered(const ActivityDescriptor& activity) override { + recordInteraction({InteractionKind::kActivityUnregistered, activity}); + } + + void onForeground(const ActivityDescriptor& activity) override { + recordInteraction({InteractionKind::kDisplayStateChanged, activity, DisplayState::kDisplayStateForeground}); + } + + void onBackground(const ActivityDescriptor& activity) override { + recordInteraction({InteractionKind::kDisplayStateChanged, activity, DisplayState::kDisplayStateBackground}); + } + + void onHidden(const ActivityDescriptor& activity) override { + recordInteraction({InteractionKind::kDisplayStateChanged, activity, DisplayState::kDisplayStateHidden}); + } + + bool invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value& command) override { + const std::string &name = GetWithDefault(alexaext::Command::NAME(), command, ""); + if (command.HasMember("token")) { + lastToken = command["token"].GetString(); + } + recordInteraction({InteractionKind::kCommandReceived, activity, name}); + + std::string prefix = ""; + auto it = prefixByActivity.find(activity); + if (it != prefixByActivity.end()) { + prefix = it->second; + } + + if (name == "PublishState") { + const auto& uri = activity.getURI(); + auto event = alexaext::Event("1.0") + .uri(uri) + .target(uri) + .name(prefix + "ExtensionReady"); + invokeExtensionEventHandler(activity, event); + + auto liveMapUpdate = LiveDataUpdate("1.0") + .uri(uri) + .objectName(prefix + "liveMap") + .target(uri) + .liveDataMapUpdate([&](LiveDataMapOperation &operation) { + operation + .type("Set") + .key("status") + .item("Ready"); + }); + invokeLiveDataUpdate(activity, liveMapUpdate); + + auto liveArrayUpdate = LiveDataUpdate("1.0") + .uri(uri) + .objectName(prefix + "liveArray") + .target(uri) + .liveDataArrayUpdate([&](LiveDataArrayOperation &operation) { + operation + .type("Insert") + .index(0) + .item("Ready"); + }); + invokeLiveDataUpdate(activity, liveArrayUpdate); + + return true; + } + + return false; + } + + bool updateComponent(const ActivityDescriptor& activity, const rapidjson::Value& command) override { + recordInteraction({InteractionKind::kUpdateComponentReceived, activity}); + return true; + } + + void onResourceReady(const ActivityDescriptor& activity, + const ResourceHolderPtr& resourceHolder) override { + recordInteraction({InteractionKind::kResourceReady, activity}); + } + + void setInteractionRecorder(const std::shared_ptr& recorder) { + interactionRecorder = recorder; + } + + void recordInteraction(const Interaction& interaction) override { + LifecycleInteractionRecorder::recordInteraction(interaction); + if (interactionRecorder) interactionRecorder->recordInteraction(interaction); + } + +public: + ActivityDescriptor lastActivity; + std::string lastToken = ""; + bool useAutoToken = true; + bool failRegistration = false; + +private: + std::unordered_map prefixByActivity; + std::shared_ptr interactionRecorder = nullptr; +}; + +const char* LifecycleTestExtension::URI = "test:lifecycle:1.0"; +const char* LifecycleTestExtension::TOKEN = "lifecycle-extension-token"; + class TestResourceProvider final: public ExtensionResourceProvider { public: @@ -1102,7 +1385,7 @@ static const char* AUDIO_PLAYER = R"( } )"; -class AudioPlayerObserverStub : public AudioPlayer::AplAudioPlayerExtensionObserverInterface { +class AudioPlayerObserverStub : public ::AudioPlayer::AplAudioPlayerExtensionObserverInterface { public: void onAudioPlayerPlay() override {} void onAudioPlayerPause() override {} @@ -1121,7 +1404,7 @@ TEST_F(ExtensionMediatorTest, AudioPlayerIntegration) { createProvider(); auto stub = std::make_shared(); - auto extension = std::make_shared(stub); + auto extension = std::make_shared<::AudioPlayer::AplAudioPlayerExtension>(stub); extensionProvider->registerExtension(std::make_shared(extension)); loadExtensions(AUDIO_PLAYER); @@ -1254,7 +1537,7 @@ class ExtensionCommunicationTestAdapter : public ExtensionProxy { bool invokeCommand(const std::string &uri, const rapidjson::Value &command, CommandSuccessCallback success, CommandFailureCallback error) override { return false; } - bool sendMessage(const std::string &uri, const rapidjson::Value &message) override { return false; } + bool sendComponentMessage(const std::string &uri, const rapidjson::Value &message) override { return false; } void registerEventCallback(Extension::EventCallback callback) override {} @@ -2417,15 +2700,25 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentResourceProviderError) { class TestExtensionProvider : public alexaext::ExtensionRegistrar { public : - bool returnNullExtensionProxy = false; + std::function returnNullProxyPredicate = nullptr; ExtensionProxyPtr getExtension(const std::string& uri) { - if(returnNullExtensionProxy) + if (returnNullProxyPredicate && returnNullProxyPredicate(uri)) return nullptr; else return ExtensionRegistrar::getExtension(uri); } + void returnNullProxy(bool returnNull) { + if (returnNull) + returnNullProxyPredicate = [](const std::string& uri) { return true; }; + else + returnNullProxyPredicate = [](const std::string& uri) { return false; }; + } + + void returnNullProxyForURI(const std::string& uri) { + returnNullProxyPredicate = [uri](const std::string& candidateURI) { return candidateURI == uri; }; + } }; TEST_F(ExtensionMediatorTest, ExtensionProviderFaultTest) { @@ -2444,11 +2737,946 @@ TEST_F(ExtensionMediatorTest, ExtensionProviderFaultTest) { mediator->initializeExtensions(config, content); // To mock a faulty provider that returns null proxy for an initialized extension - std::static_pointer_cast(extensionProvider)->returnNullExtensionProxy = true; + std::static_pointer_cast(extensionProvider)->returnNullProxy(true); mediator->loadExtensions(config, content, [](){}); inflate(); ASSERT_TRUE(ConsoleMessage()); } +static const char* LIFECYCLE_DOC = R"({ + "type": "APL", + "version": "1.9", + "theme": "dark", + "extensions": [ + { + "uri": "test:lifecycle:1.0", + "name": "Lifecycle" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "TouchWrapper", + "id": "tw1", + "width": 100, + "height": 100, + "onPress": { + "type": "Lifecycle:PublishState" + } + } + } + }, + "Lifecycle:ExtensionReady": { + "type": "SendEvent", + "sequencer": "ExtensionEvent", + "arguments": [ "ExtensionReadyReceived" ] + } +})"; + +TEST_F(ExtensionMediatorTest, BasicExtensionLifecycle) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + ASSERT_TRUE(root); + + root->updateTime(100); + performClick(50, 50); + root->clearPending(); + + root->updateTime(200); + root->updateDisplayState(DisplayState::kDisplayStateBackground); + + root->updateTime(300); + root->updateDisplayState(DisplayState::kDisplayStateHidden); + + root->cancelExecution(); + mediator->finish(); + session->end(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kCommandReceived, extension->lastActivity, "PublishState"})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateBackground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateHidden})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + + ASSERT_TRUE(CheckSendEvent(root, "ExtensionReadyReceived")); +} + +TEST_F(ExtensionMediatorTest, SessionUsedAcrossDocuments) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + auto extension = std::make_shared(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + // Render a first document + + createContent(LIFECYCLE_DOC, nullptr); + ASSERT_TRUE(content->isReady()); + + // Experimental feature required + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + auto firstDocumentActivity = extension->lastActivity; + + inflate(); + ASSERT_TRUE(root); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + // Render a second document within the same session + + createContent(LIFECYCLE_DOC, nullptr); + ASSERT_TRUE(content->isReady()); + + // Experimental feature required + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE(firstDocumentActivity, extension->lastActivity); + + inflate(); + ASSERT_TRUE(root); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + session->end(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); +} + +TEST_F(ExtensionMediatorTest, SessionEndedBeforeDocumentFinished) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); +} + +TEST_F(ExtensionMediatorTest, SessionEndedBeforeDocumentRendered) { + auto session = ExtensionSession::create(); + session->end(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + inflate(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNoMoreInteractions()); +} + +TEST_F(ExtensionMediatorTest, SessionEndedBeforeExtensionsLoaded) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + session->end(); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + inflate(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNoMoreInteractions()); +} + +static const char* LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC = R"({ + "type": "APL", + "version": "1.9", + "theme": "dark", + "extensions": [ + { + "uri": "test:lifecycle:1.0", + "name": "Lifecycle" + }, + { + "uri": "test:lifecycleOther:2.0", + "name": "LifecycleOther" + } + ], + "settings": { + "LifecycleOther": { + "prefix": "other_" + } + }, + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "TouchWrapper", + "id": "tw1", + "width": 100, + "height": 100, + "onPress": { + "type": "Lifecycle:PublishState" + } + } + } + }, + "Lifecycle:ExtensionReady": { + "type": "SendEvent", + "sequencer": "ExtensionEvent", + "arguments": [ "ExtensionReadyReceived" ] + }, + "Lifecycle:other_ExtensionReady": { + "type": "SendEvent", + "sequencer": "ExtensionEvent", + "arguments": [ "OtherExtensionReadyReceived" ] + } +})"; + +TEST_F(ExtensionMediatorTest, SessionEndsAfterAllActivitiesHaveFinished) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + auto otherExtension = std::make_shared("test:lifecycleOther:2.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(std::make_shared(otherExtension)); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kActivityRegistered, otherExtension->lastActivity})); + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, otherExtension->lastActivity, DisplayState::kDisplayStateForeground})); + + // Start collecting interactions for both extensions in a combined timeline so we can assert + // the order across extensions + auto combinedTimeline = std::make_shared(); + extension->setInteractionRecorder(combinedTimeline); + otherExtension->setInteractionRecorder(combinedTimeline); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kActivityUnregistered, otherExtension->lastActivity})); + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + ASSERT_TRUE(otherExtension->verifyNoMoreInteractions()); + + // Verify that both activities were finished before the session was ended + ASSERT_TRUE(combinedTimeline->verifyUnordered({ + {InteractionKind::kActivityUnregistered, extension->lastActivity}, + {InteractionKind::kActivityUnregistered, otherExtension->lastActivity} + })); + ASSERT_TRUE(combinedTimeline->verifyUnordered({ + {InteractionKind::kSessionEnded, session->getId()}, + {InteractionKind::kSessionEnded, session->getId()} + })); + + ASSERT_TRUE(combinedTimeline->verifyNoMoreInteractions()); +} + +TEST_F(ExtensionMediatorTest, RejectedExtensionsDoNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + auto otherExtension = std::make_shared("test:lifecycleOther:2.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(std::make_shared(otherExtension)); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content, &grantedExtensions); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + // Check that there were no interactions with the denied extension + ASSERT_TRUE(otherExtension->verifyNoMoreInteractions()); +} + +TEST_F(ExtensionMediatorTest, FailureDuringRegistrationDoesNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + auto otherExtension = std::make_shared("test:lifecycleOther:2.0"); + otherExtension->failRegistration = true; + extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(std::make_shared(otherExtension)); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + // Check that there were no interactions with the denied extension + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(otherExtension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(otherExtension->verifyNoMoreInteractions()); +} + +TEST_F(ExtensionMediatorTest, RejectedRegistrationDoesNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + auto failingProxy = std::make_shared("test:lifecycleOther:2.0", true, false); + extensionProvider->registerExtension(failingProxy); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + ASSERT_TRUE(ConsoleMessage()); // Consume the failed registration console message +} + +TEST_F(ExtensionMediatorTest, MissingProxyDoesNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + auto extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + auto otherExtension = std::make_shared("test:lifecycleOther:2.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(std::make_shared(otherExtension)); + + extensionProvider->returnNullProxyForURI("test:lifecycleOther:2.0"); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(ExtensionMediatorTest, UnknownExtensionDoesNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); +} + +TEST_F(ExtensionMediatorTest, BrokenProviderDoesNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + auto extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + auto otherExtension = std::make_shared("test:lifecycleOther:2.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(std::make_shared(otherExtension)); + + // Broken provider will return a valid proxy once but then nullptr subsequently for the same URI + int proxyRequestCount = 0; + extensionProvider->returnNullProxyPredicate = [&](const std::string& uri) { + if (uri != "test:lifecycleOther:2.0") return false; + // Return false on the first getProxy call, nullptr thereafter + proxyRequestCount += 1; + return proxyRequestCount > 1; + }; + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(ExtensionMediatorTest, FailureToInitializeDoesNotPreventEndingSessions) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared("test:lifecycle:1.0"); + extensionProvider->registerExtension(std::make_shared(extension)); + auto failingProxy = std::make_shared("test:lifecycleOther:2.0", false, true); + extensionProvider->registerExtension(failingProxy); + + createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + + std::set grantedExtensions = {"test:lifecycle:1.0"}; + + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + ASSERT_TRUE(extension->verifyNoMoreInteractions()); + + ASSERT_TRUE(ConsoleMessage()); // Consume the failed initialization console message +} + +static const char* LIFECYCLE_COMPONENT_DOC = R"({ + "type": "APL", + "version": "1.9", + "theme": "dark", + "extensions": [ + { + "uri": "test:lifecycle:1.0", + "name": "Lifecycle" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "item": { + "type": "Lifecycle:Component", + "id": "extensionComponent", + "width": 100, + "height": 100 + } + } + } +})"; + +TEST_F(ExtensionMediatorTest, LifecycleWithComponent) { + 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(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_COMPONENT_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + + auto component = root->findComponentById("extensionComponent"); + ASSERT_TRUE(component); + + ASSERT_TRUE(IsEqual(kResourcePending, component->getCalculated(kPropertyResourceState))); + component->updateResourceState(kResourceReady); + ASSERT_TRUE(IsEqual(kResourceReady, component->getCalculated(kPropertyResourceState))); + + session->end(); + + root->cancelExecution(); + mediator->finish(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kUpdateComponentReceived, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kUpdateComponentReceived, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kResourceReady, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); +} + +static const char* LIFECYCLE_LIVE_DATA_DOC = R"({ + "type": "APL", + "version": "1.9", + "theme": "dark", + "extensions": [ + { + "uri": "test:lifecycle:1.0", + "name": "Lifecycle" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "TouchWrapper", + "id": "tw1", + "width": "100px", + "height": "100px", + "onPress": { + "type": "Lifecycle:PublishState" + } + }, + { + "type": "Text", + "id": "mapStatus", + "text": "${liveMap.status}", + "width": "100px", + "height": "100px" + }, + { + "type": "Text", + "id": "arrayLength", + "text": "${liveArray.length}", + "width": "100px", + "height": "100px" + } + ] + } + }, + "Lifecycle:ExtensionReady": { + "type": "SendEvent", + "sequencer": "ExtensionEvent", + "arguments": [ "ExtensionReadyReceived" ] + } +})"; + +TEST_F(ExtensionMediatorTest, LifecycleWithLiveData) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(); + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_LIVE_DATA_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + ASSERT_NE("", extension->lastActivity.getId()); + + inflate(); + ASSERT_TRUE(root); + + root->updateTime(100); + performClick(50, 50); + root->clearPending(); + + root->updateTime(200); + root->clearPending(); + + auto mapComponent = root->findComponentById("mapStatus"); + ASSERT_TRUE(mapComponent); + ASSERT_EQ("Ready", mapComponent->getCalculated(kPropertyText).asString()); + + auto arrayComponent = root->findComponentById("arrayLength"); + ASSERT_TRUE(arrayComponent); + ASSERT_EQ("1", arrayComponent->getCalculated(kPropertyText).asString()); + + root->cancelExecution(); + mediator->finish(); + session->end(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kCommandReceived, extension->lastActivity, "PublishState"})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + + ASSERT_TRUE(CheckSendEvent(root, "ExtensionReadyReceived")); +} + +TEST_F(ExtensionMediatorTest, LifecycleAPIsRespectExtensionToken) { + auto session = ExtensionSession::create(); + + extensionProvider = std::make_shared(); + mediator = ExtensionMediator::create(extensionProvider, + nullptr, + alexaext::Executor::getSynchronousExecutor(), + session); + + auto extension = std::make_shared(); + extension->useAutoToken = false; // make sure the extension specifies its own token + auto proxy = std::make_shared(extension); + extensionProvider->registerExtension(proxy); + + createContent(LIFECYCLE_DOC, nullptr); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + ASSERT_TRUE(content->isReady()); + mediator->initializeExtensions(config, content); + mediator->loadExtensions(config, content); + + inflate(); + ASSERT_TRUE(root); + + root->updateTime(100); + performClick(50, 50); + root->clearPending(); + + // The extension's token from the registration response should be used + ASSERT_EQ(LifecycleTestExtension::TOKEN, extension->lastToken); + + root->cancelExecution(); + mediator->finish(); + session->end(); + + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionStarted, session->getId()})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityRegistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kDisplayStateChanged, extension->lastActivity, DisplayState::kDisplayStateForeground})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kCommandReceived, extension->lastActivity, "PublishState"})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kActivityUnregistered, extension->lastActivity})); + ASSERT_TRUE(extension->verifyNextInteraction({InteractionKind::kSessionEnded, session->getId()})); + + ASSERT_TRUE(CheckSendEvent(root, "ExtensionReadyReceived")); +} + #endif \ No newline at end of file diff --git a/unit/extension/unittest_extension_session.cpp b/unit/extension/unittest_extension_session.cpp new file mode 100644 index 0000000..b01b900 --- /dev/null +++ b/unit/extension/unittest_extension_session.cpp @@ -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. + */ + +#ifdef ALEXAEXTENSIONS + +#include + +#include + +TEST(ExtensionSessionTest, CreatesSessionsWithUniqueDescriptors) { + auto session1 = apl::ExtensionSession::create(); + auto session2 = apl::ExtensionSession::create(); + + ASSERT_NE(session1->getSessionDescriptor(), session2->getSessionDescriptor()); +} + +TEST(ExtensionSessionTest, CreatesSessionsWithSpecifiedDescriptors) { + auto descriptor = alexaext::SessionDescriptor::create(); + auto session = apl::ExtensionSession::create(descriptor); + + ASSERT_EQ(descriptor, session->getSessionDescriptor()); +} + +#endif //ALEXAEXTENSIONS \ No newline at end of file diff --git a/unit/focus/unittest_focus_manager.cpp b/unit/focus/unittest_focus_manager.cpp index 09b9cb8..1c73838 100644 --- a/unit/focus/unittest_focus_manager.cpp +++ b/unit/focus/unittest_focus_manager.cpp @@ -21,34 +21,32 @@ using namespace apl; class FocusManagerTest : public DocumentWrapper {}; -static const char *FOCUS_TEST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"parameters\": []," - " \"item\": {" - " \"type\": \"Container\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"thing1\"," - " \"width\": 20," - " \"height\": 20" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"thing2\"," - " \"width\": 20," - " \"height\": 20" - " }" - " ]" - " }" - " }" - "}"; - +static const char *FOCUS_TEST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [], + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "TouchWrapper", + "id": "thing1", + "width": 20, + "height": 20 + }, + { + "type": "TouchWrapper", + "id": "thing2", + "width": 20, + "height": 20 + } + ] + } + } +})"; TEST_F(FocusManagerTest, ManualControl) { @@ -178,48 +176,45 @@ TEST_F(FocusManagerTest, ClearCheck) ASSERT_TRUE(CheckState(thing2)); } -static const char *BLUR_FOCUS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Container\"," - " \"width\": \"100%\"," - " \"height\": \"100%\"," - " \"data\": [" - " 1," - " 2" - " ]," - " \"items\": [" - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"thing${data}\"," - " \"onFocus\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"frame${data}\"," - " \"property\": \"borderColor\"," - " \"value\": \"red\"" - " }," - " \"onBlur\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"frame${data}\"," - " \"property\": \"borderColor\"," - " \"value\": \"black\"" - " }," - " \"item\": {" - " \"type\": \"Frame\"," - " \"id\": \"frame${data}\"," - " \"borderColor\": \"black\"," - " \"borderWidth\": 1" - " }" - " }" - " ]" - " }" - " }" - "}" -; - +static const char *BLUR_FOCUS = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "data": [ + 1, + 2 + ], + "items": [ + { + "type": "TouchWrapper", + "id": "thing${data}", + "onFocus": { + "type": "SetValue", + "componentId": "frame${data}", + "property": "borderColor", + "value": "red" + }, + "onBlur": { + "type": "SetValue", + "componentId": "frame${data}", + "property": "borderColor", + "value": "black" + }, + "item": { + "type": "Frame", + "id": "frame${data}", + "borderColor": "black", + "borderWidth": 1 + } + } + ] + } + } +})"; TEST_F(FocusManagerTest, BlurFocus) { @@ -285,32 +280,31 @@ TEST_F(FocusManagerTest, BlurFocus) } -static const char *FOCUS_EVENT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"TouchWrapper\"," - " \"onFocus\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"frame\"," - " \"property\": \"text\"," - " \"value\": \"${event.source.handler}:${event.source.focused}\"" - " }," - " \"onBlur\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"frame\"," - " \"property\": \"text\"," - " \"value\": \"${event.source.handler}:${event.source.focused}\"" - " }," - " \"item\": {" - " \"type\": \"Text\"," - " \"id\": \"frame\"" - " }" - " }" - " }" - "}"; +static const char *FOCUS_EVENT = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onFocus": { + "type": "SetValue", + "componentId": "frame", + "property": "text", + "value": "${event.source.handler}:${event.source.focused}" + }, + "onBlur": { + "type": "SetValue", + "componentId": "frame", + "property": "text", + "value": "${event.source.handler}:${event.source.focused}" + }, + "item": { + "type": "Text", + "id": "frame" + } + } + } +})"; /** * Check that the event.source.handler and event.source.focused properties are set @@ -349,108 +343,107 @@ TEST_F(FocusManagerTest, FocusEvent) ASSERT_TRUE(CheckDirty(root, text)); } -static const char *FOCUS_COMPONENT_TYPES = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"id\": \"ContainerID\"" - " }," - " {" - " \"type\": \"Image\"," - " \"id\": \"ImageID\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"TextID\"" - " }," - " {" - " \"type\": \"Sequence\"," - " \"id\": \"SequenceID\"" - " }," - " {" - " \"type\": \"Frame\"," - " \"id\": \"FrameID\"" - " }," - " {" - " \"type\": \"Pager\"," - " \"id\": \"PagerID\"" - " }," - " {" - " \"type\": \"ScrollView\"," - " \"id\": \"ScrollViewID\"" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"TouchWrapperID\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithNoHandlerID\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithFocusHandlerID\"," - " \"onFocus\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithBlurHandlerID\"," - " \"onBlur\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithPressHandlerID\"," - " \"onPress\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithKDownHandlerID\"," - " \"handleKeyDown\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithKUpHandlerID\"," - " \"handleKeyUp\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithUpHandlerID\"," - " \"onUp\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithDownHandlerID\"," - " \"onDown\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithGesturesID\"," - " \"gestures\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithCancelHandlerID\"," - " \"onCancel\": \"[]\"" - " }," - " {" - " \"type\": \"VectorGraphic\"," - " \"id\": \"VectorGraphicWithMoveHandlerID\"," - " \"onMove\": \"[]\"" - " }," - " {" - " \"type\": \"Video\"," - " \"id\": \"VideoID\"" - " }" - " ]" - " }" - " }" - "}"; +static const char *FOCUS_COMPONENT_TYPES = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "Container", + "id": "ContainerID" + }, + { + "type": "Image", + "id": "ImageID" + }, + { + "type": "Text", + "id": "TextID" + }, + { + "type": "Sequence", + "id": "SequenceID" + }, + { + "type": "Frame", + "id": "FrameID" + }, + { + "type": "Pager", + "id": "PagerID" + }, + { + "type": "ScrollView", + "id": "ScrollViewID" + }, + { + "type": "TouchWrapper", + "id": "TouchWrapperID" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithNoHandlerID" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithFocusHandlerID", + "onFocus": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithBlurHandlerID", + "onBlur": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithPressHandlerID", + "onPress": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithKDownHandlerID", + "handleKeyDown": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithKUpHandlerID", + "handleKeyUp": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithUpHandlerID", + "onUp": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithDownHandlerID", + "onDown": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithGesturesID", + "gestures": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithCancelHandlerID", + "onCancel": "[]" + }, + { + "type": "VectorGraphic", + "id": "VectorGraphicWithMoveHandlerID", + "onMove": "[]" + }, + { + "type": "Video", + "id": "VideoID" + } + ] + } + } +})"; static std::map sCanFocus = { {"ContainerID", false}, @@ -538,61 +531,60 @@ TEST_F(FocusManagerTest, FocusOnComponentType) } } -static const char * INHERIT_PARENT_STATE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"TouchWrapper\"," - " \"items\": {" - " \"type\": \"Container\"," - " \"inheritParentState\": true," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"MyText\"," - " \"text\": \"Nothing\"" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"TouchWrapperA\"," - " \"inheritParentState\": true," - " \"onFocus\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"MyText\"," - " \"property\": \"text\"," - " \"value\": \"A in focus\"" - " }," - " \"onBlur\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"MyText\"," - " \"property\": \"text\"," - " \"value\": \"A not in focus\"" - " }" - " }," - " {" - " \"type\": \"TouchWrapper\"," - " \"id\": \"TouchWrapperB\"," - " \"inheritParentState\": false," - " \"onFocus\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"MyText\"," - " \"property\": \"text\"," - " \"value\": \"B in focus\"" - " }," - " \"onBlur\": {" - " \"type\": \"SetValue\"," - " \"componentId\": \"MyText\"," - " \"property\": \"text\"," - " \"value\": \"B not in focus\"" - " }" - " }" - " ]" - " }" - " }" - " }" - "}"; +static const char * INHERIT_PARENT_STATE = R"({ + "type": "APL", + "version": "1.3", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "items": { + "type": "Container", + "inheritParentState": true, + "items": [ + { + "type": "Text", + "id": "MyText", + "text": "Nothing" + }, + { + "type": "TouchWrapper", + "id": "TouchWrapperA", + "inheritParentState": true, + "onFocus": { + "type": "SetValue", + "componentId": "MyText", + "property": "text", + "value": "A in focus" + }, + "onBlur": { + "type": "SetValue", + "componentId": "MyText", + "property": "text", + "value": "A not in focus" + } + }, + { + "type": "TouchWrapper", + "id": "TouchWrapperB", + "inheritParentState": false, + "onFocus": { + "type": "SetValue", + "componentId": "MyText", + "property": "text", + "value": "B in focus" + }, + "onBlur": { + "type": "SetValue", + "componentId": "MyText", + "property": "text", + "value": "B not in focus" + } + } + ] + } + } + } +})"; /** * Verify that a component with "inheritParentState=true" does not respond to a SetFocus command @@ -659,7 +651,7 @@ TEST_F(FocusManagerTest, FocusWithInheritParentState) event = root->popEvent(); ASSERT_EQ(kEventTypeFocus, event.getType()); ASSERT_FALSE(event.getComponent()); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); root->clearPending(); ASSERT_FALSE(component->getState().get(kStateFocused)); ASSERT_FALSE(a->getState().get(kStateFocused)); diff --git a/unit/focus/unittest_native_focus.cpp b/unit/focus/unittest_native_focus.cpp index fef0227..1c1d6c6 100644 --- a/unit/focus/unittest_native_focus.cpp +++ b/unit/focus/unittest_native_focus.cpp @@ -107,7 +107,7 @@ class NativeFocusTest : public DocumentWrapper { if (boundsResult != ::testing::AssertionSuccess()) { return boundsResult; } - if (!event.getActionRef().isEmpty() && event.getActionRef().isPending()) { + if (!event.getActionRef().empty() && event.getActionRef().isPending()) { event.getActionRef().resolve(true); } root->clearPending(); @@ -285,7 +285,7 @@ TEST_F(NativeFocusTest, SimpleGridClear) auto event = root->popEvent(); ASSERT_EQ(kEventTypeFocus, event.getType()); ASSERT_EQ(nullptr, event.getComponent().get()); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); ASSERT_EQ(nullptr, fm.getFocus()); } @@ -5962,7 +5962,7 @@ TEST_F(NativeFocusTest, RemoveWhileFocused) auto event = root->popEvent(); ASSERT_EQ(kEventTypeFocus, event.getType()); ASSERT_EQ(event.getComponent(), nullptr); - ASSERT_TRUE(event.getActionRef().isEmpty()); + ASSERT_TRUE(event.getActionRef().empty()); // Releases as component dissapeared. It's up to a viewhost to figure what to do in that case ASSERT_EQ(nullptr, fm.getFocus()); diff --git a/unit/graphic/unittest_dependant_graphic.cpp b/unit/graphic/unittest_dependant_graphic.cpp index 1e28ca1..aeb9474 100644 --- a/unit/graphic/unittest_dependant_graphic.cpp +++ b/unit/graphic/unittest_dependant_graphic.cpp @@ -334,7 +334,7 @@ TEST_F(DependantGraphicTest, Transformed) ASSERT_EQ(kGraphicElementTypePath, path->getType()); auto fill = path->getValue(kGraphicPropertyFill); - ASSERT_EQ(Color(Color::GREEN), fill.asColor()); + ASSERT_EQ(Color::GREEN, fill.getColor()); auto fillTransform = path->getValue(kGraphicPropertyFillTransform).getTransform2D(); ASSERT_EQ(Transform2D::skewX(40), fillTransform); @@ -422,14 +422,14 @@ TEST_F(DependantGraphicTest, ChangingGradient) auto pathGrad = path->getValue(kGraphicPropertyFill); ASSERT_TRUE(pathGrad.isGradient()); - ASSERT_EQ(Color(Color::RED), pathGrad.getGradient().getColorRange().at(0)); + ASSERT_EQ(Object(Color(Color::RED)), pathGrad.getGradient().getProperty(kGradientPropertyColorRange).at(0)); auto text = group->getChildAt(1); ASSERT_EQ(kGraphicElementTypeText, text->getType()); auto textGrad = text->getValue(kGraphicPropertyStroke); ASSERT_TRUE(textGrad.isGradient()); - ASSERT_EQ(Color(Color::RED), textGrad.getGradient().getColorRange().at(0)); + ASSERT_EQ(Object(Color(Color::RED)), textGrad.getGradient().getProperty(kGradientPropertyColorRange).at(0)); executeCommand("SetValue", {{"componentId", "gc"}, {"property", "gradientColor"}, diff --git a/unit/graphic/unittest_graphic.cpp b/unit/graphic/unittest_graphic.cpp index 3e0c788..5ed9d63 100644 --- a/unit/graphic/unittest_graphic.cpp +++ b/unit/graphic/unittest_graphic.cpp @@ -423,7 +423,7 @@ TEST_F(GraphicTest, GraphicResourceComponentContextScoping) auto object = context->opt("@myColor"); ASSERT_TRUE(object.isColor()); - ASSERT_EQ(Color(Color::RED), object.asColor()); + ASSERT_EQ(Color::RED, object.getColor()); auto graphic = component->getCalculated(kPropertyGraphic).getGraphic(); ASSERT_TRUE(graphic); @@ -434,7 +434,7 @@ TEST_F(GraphicTest, GraphicResourceComponentContextScoping) ASSERT_EQ(1, container->getChildCount()); auto text = container->getChildAt(0); ASSERT_EQ(kGraphicElementTypeText, text->getType()); - ASSERT_EQ(Color(Color::RED), text->getValue(kGraphicPropertyFill).asColor()); + ASSERT_EQ(Color::RED, text->getValue(kGraphicPropertyFill).getColor()); } @@ -538,6 +538,8 @@ TEST_F(GraphicTest, GroupProperties) static const char *MINIMAL_TEXT = R"({ "type": "AVG", + "layoutDirection": "RTL", + "lang": "ar-IR", "version": "1.0", "height": 100, "width": 200, @@ -553,13 +555,15 @@ TEST_F(GraphicTest, MinimalText) auto container = graphic->getRoot(); ASSERT_TRUE(container); - ASSERT_EQ(Object(""), container->getValue(kGraphicPropertyLang)); + ASSERT_EQ(Object("ar-IR"), container->getValue(kGraphicPropertyLang)); ASSERT_EQ(kGraphicLayoutDirectionLTR, container->getValue(kGraphicPropertyLang).asInt()); ASSERT_EQ(1, container->getChildCount()); auto text = container->getChildAt(0); ASSERT_EQ(kGraphicElementTypeText, text->getType()); + ASSERT_EQ(kGraphicLayoutDirectionRTL, text->getLayoutDirection()); + ASSERT_EQ("ar-IR", text->getLang()); ASSERT_EQ(Object(Color(Color::BLACK)), text->getValue(kGraphicPropertyFill)); ASSERT_EQ(Object(1), text->getValue(kGraphicPropertyFillOpacity)); ASSERT_EQ(Object("sans-serif"), text->getValue(kGraphicPropertyFontFamily)); @@ -819,17 +823,6 @@ TEST_F(GraphicTest, BadContent) } } - -TEST_F(GraphicTest, BadContentNoSession) -{ - for (auto& s : BAD_CONTENT) { - auto gc = GraphicContent::create(s); - ASSERT_FALSE(gc); - ASSERT_FALSE(ConsoleMessage()); - ASSERT_TRUE(LogMessage()); - } -} - static std::vector BAD_CONTAINER_PROPERTIES = { R"({"type": "AVG", "version": "1.0", "height": 0, "width": 200})", // Zero height R"({"type": "AVG", "version": "1.0", "height": 100, "width": 0})", // Zero width @@ -962,8 +955,9 @@ TEST_F(GraphicTest, InvalidUpdateWithInvalidJson) { ASSERT_EQ(Object::NULL_OBJECT(), stretch->getCalculated(kPropertyGraphic)); auto json = JsonData(R"(abcd)"); - auto graphicContent = GraphicContent::create(std::move(json)); + auto graphicContent = GraphicContent::create(session, std::move(json)); ASSERT_EQ(nullptr, graphicContent); + ASSERT_TRUE(session->checkAndClear()); none = component->findComponentById("none"); ASSERT_EQ(Object::NULL_OBJECT(), none->getCalculated(kPropertyGraphic)); @@ -987,7 +981,7 @@ TEST_F(GraphicTest, InvalidUpdateWithValidJson) { ASSERT_EQ(Object::NULL_OBJECT(), stretch->getCalculated(kPropertyGraphic)); auto json = JsonData(PILL_AVG); - auto graphicContent = GraphicContent::create(std::move(json)); + auto graphicContent = GraphicContent::create(session, std::move(json)); stretch->updateGraphic(graphicContent); none = component->findComponentById("none"); @@ -2063,8 +2057,8 @@ TEST_F(GraphicTest, Transformed) ASSERT_EQ(Gradient::LINEAR, fillGrad.getProperty(kGradientPropertyType).getInteger()); auto colorRange = fillGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::RED), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::RED, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); auto inputRange = fillGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -2097,8 +2091,8 @@ TEST_F(GraphicTest, Transformed) ASSERT_EQ(Gradient::RADIAL, strokeGrad.getProperty(kGradientPropertyType).getInteger()); colorRange = strokeGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::GREEN, colorRange.at(1).getColor()); inputRange = strokeGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -2209,8 +2203,8 @@ TEST_F(GraphicTest, AVGResourceTypes) auto fill = path->getValue(kGraphicPropertyFill).getGradient(); ASSERT_EQ(Gradient::LINEAR, fill.getType()); ASSERT_EQ(Gradient::kGradientUnitsUserSpace, fill.getProperty(kGradientPropertyUnits).getInteger()); - ASSERT_EQ(std::vector({Color::RED, Color::TRANSPARENT}), fill.getColorRange()); - ASSERT_EQ(std::vector({0, 0.4}), fill.getInputRange()); + ASSERT_EQ(std::vector({Color(Color::RED), Color(Color::TRANSPARENT)}), fill.getProperty(kGradientPropertyColorRange).getArray()); + ASSERT_EQ(std::vector({0.0, 0.4}), fill.getProperty(kGradientPropertyInputRange).getArray()); ASSERT_EQ(25, fill.getProperty(kGradientPropertyX1).getDouble()); ASSERT_EQ(75, fill.getProperty(kGradientPropertyX2).getDouble()); ASSERT_EQ(15, fill.getProperty(kGradientPropertyY1).getDouble()); @@ -2535,8 +2529,8 @@ TEST_F(GraphicTest, LocalResourcedGradient) ASSERT_EQ(Gradient::LINEAR, fillGrad.getProperty(kGradientPropertyType).getInteger()); auto colorRange = fillGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::RED), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::RED, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); auto inputRange = fillGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -2564,8 +2558,8 @@ TEST_F(GraphicTest, LocalResourcedGradient) ASSERT_EQ(Gradient::RADIAL, strokeGrad.getProperty(kGradientPropertyType).getInteger()); colorRange = strokeGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::GREEN, colorRange.at(1).getColor()); inputRange = strokeGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -2656,8 +2650,8 @@ TEST_F(GraphicTest, ExternalResourcedGradient) ASSERT_EQ(Gradient::LINEAR, fillGrad.getProperty(kGradientPropertyType).getInteger()); auto colorRange = fillGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); auto inputRange = fillGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -2685,8 +2679,8 @@ TEST_F(GraphicTest, ExternalResourcedGradient) ASSERT_EQ(Gradient::RADIAL, strokeGrad.getProperty(kGradientPropertyType).getInteger()); colorRange = strokeGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::RED), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).asColor()); + ASSERT_EQ(Color::RED, colorRange.at(0).getColor()); + ASSERT_EQ(Color::GREEN, colorRange.at(1).getColor()); inputRange = strokeGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -2750,8 +2744,8 @@ TEST_F(GraphicTest, GradientInline) ASSERT_EQ(Gradient::LINEAR, fillGrad.getProperty(kGradientPropertyType).getInteger()); auto colorRange = fillGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); } static const char* MIXED_RESOURCES = R"({ @@ -2858,8 +2852,8 @@ TEST_F(GraphicTest, MixedResources) ASSERT_EQ(Gradient::RADIAL, strokeGrad.getProperty(kGradientPropertyType).getInteger()); auto colorRange = strokeGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::RED), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(1).asColor()); + ASSERT_EQ(Color::RED, colorRange.at(0).getColor()); + ASSERT_EQ(Color::GREEN, colorRange.at(1).getColor()); auto inputRange = strokeGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -3052,8 +3046,8 @@ TEST_F(GraphicTest, GradientChecks) ASSERT_EQ(Gradient::LINEAR, fillLinearGrad.getProperty(kGradientPropertyType).getInteger()); auto colorRange = fillLinearGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); auto inputRange = fillLinearGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); ASSERT_EQ(0, inputRange.at(0).getDouble()); @@ -3074,8 +3068,8 @@ TEST_F(GraphicTest, GradientChecks) ASSERT_EQ(Gradient::RADIAL, fillRadialGrad.getProperty(kGradientPropertyType).getInteger()); colorRange = fillRadialGrad.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); inputRange = fillRadialGrad.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); ASSERT_EQ(0, inputRange.at(0).getDouble()); diff --git a/unit/graphic/unittest_graphic_bind.cpp b/unit/graphic/unittest_graphic_bind.cpp index b9238dc..dd4d584 100644 --- a/unit/graphic/unittest_graphic_bind.cpp +++ b/unit/graphic/unittest_graphic_bind.cpp @@ -188,7 +188,7 @@ TEST_F(GraphicBindTest, Nested) ASSERT_EQ(i+1, row->getChildCount()); // Rows alternate blue and red auto first = row->getChildAt(0); - ASSERT_TRUE(IsEqual(Color(i % 2 == 0 ? Color::BLUE : Color::RED), - first->getValue(kGraphicPropertyFill).asColor())); + ASSERT_TRUE(IsEqual(i % 2 == 0 ? Color::BLUE : Color::RED, + first->getValue(kGraphicPropertyFill).getColor())); } } diff --git a/unit/graphic/unittest_graphic_component.cpp b/unit/graphic/unittest_graphic_component.cpp index b01e1fe..b6c4207 100644 --- a/unit/graphic/unittest_graphic_component.cpp +++ b/unit/graphic/unittest_graphic_component.cpp @@ -854,7 +854,7 @@ TEST_F(GraphicComponentTest, RelayoutTest) ASSERT_EQ(Object(Dimension(100)), component->getCalculated(kPropertyBorderWidth)); ASSERT_EQ(Rect(100, 100, 824, 600), component->getCalculated(kPropertyInnerBounds).getRect()); ASSERT_TRUE(CheckDirty(component, kPropertyInnerBounds, kPropertyBorderWidth, kPropertyNotifyChildrenChanged, - kPropertyVisualHash)); + kPropertyVisualHash, kPropertyDrawnBorderWidth)); // The graphic itself should have a new viewport height and width ASSERT_EQ(100, graphic->getViewportWidth()); @@ -1108,8 +1108,8 @@ TEST_F(GraphicComponentTest, GraphicFocusAndHover) { auto path = graphic->getRoot()->getChildAt(0); auto pathData = path->getValue(kGraphicPropertyPathData); ASSERT_EQ("M25,50 a25,25 0 1 1 50,0 a25,25 0 1 1 -50,0", pathData.asString()); - auto stroke = path->getValue(kGraphicPropertyStroke).asColor(); - ASSERT_EQ(Color(0xffffffff), stroke); + auto stroke = path->getValue(kGraphicPropertyStroke).getColor(); + ASSERT_EQ(0xffffffff, stroke); // Hover on root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(75, 75))); @@ -1117,8 +1117,8 @@ TEST_F(GraphicComponentTest, GraphicFocusAndHover) { ASSERT_TRUE(CheckDirty(path, kGraphicPropertyStroke)); ASSERT_TRUE(CheckDirty(gc, kPropertyGraphic, kPropertyVisualHash)); ASSERT_TRUE(CheckDirty(root, gc)); - stroke = path->getValue(kGraphicPropertyStroke).asColor(); - ASSERT_EQ(Color(0xff0000ff), stroke); + stroke = path->getValue(kGraphicPropertyStroke).getColor(); + ASSERT_EQ(0xff0000ff, stroke); // Hover off root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(200, 200))); @@ -1126,8 +1126,8 @@ TEST_F(GraphicComponentTest, GraphicFocusAndHover) { ASSERT_TRUE(CheckDirty(path, kGraphicPropertyStroke)); ASSERT_TRUE(CheckDirty(gc, kPropertyGraphic, kPropertyVisualHash)); ASSERT_TRUE(CheckDirty(root, gc)); - stroke = path->getValue(kGraphicPropertyStroke).asColor(); - ASSERT_EQ(Color(0xffffffff), stroke); + stroke = path->getValue(kGraphicPropertyStroke).getColor(); + ASSERT_EQ(0xffffffff, stroke); } static const char * SLIDER = @@ -1762,7 +1762,7 @@ TEST_F(GraphicComponentTest, StyleEverything) auto fillPattern = fill.getGraphicPattern(); auto fillPatternPath = fillPattern->getItems().at(0); ASSERT_EQ(kGraphicElementTypePath, fillPatternPath->getType()); - ASSERT_EQ(Color(Color::RED), fillPatternPath->getValue(kGraphicPropertyFill).asColor()); + ASSERT_EQ(Color::RED, fillPatternPath->getValue(kGraphicPropertyFill).getColor()); ASSERT_EQ(0.9, path->getValue(kGraphicPropertyFillOpacity).asNumber()); ASSERT_EQ("M 50 0 L 100 50 L 50 100 L 0 50 z", path->getValue(kGraphicPropertyPathData).asString()); @@ -1773,8 +1773,8 @@ TEST_F(GraphicComponentTest, StyleEverything) ASSERT_EQ(Gradient::LINEAR, stroke.getProperty(kGradientPropertyType).asInt()); auto colorRange = stroke.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::BLUE), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::BLUE, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); auto inputRange = stroke.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -1808,7 +1808,7 @@ TEST_F(GraphicComponentTest, StyleEverything) fillPattern = fill.getGraphicPattern(); fillPatternPath = fillPattern->getItems().at(0); ASSERT_EQ(kGraphicElementTypePath, fillPatternPath->getType()); - ASSERT_EQ(Color(Color::RED), fillPatternPath->getValue(kGraphicPropertyFill).asColor()); + ASSERT_EQ(Color::RED, fillPatternPath->getValue(kGraphicPropertyFill).getColor()); ASSERT_EQ(0.9, text->getValue(kGraphicPropertyFillOpacity).asNumber()); ASSERT_EQ("sans-serif", text->getValue(kGraphicPropertyFontFamily).asString()); @@ -1855,7 +1855,7 @@ TEST_F(GraphicComponentTest, StyleEverything) fillPattern = fill.getGraphicPattern(); fillPatternPath = fillPattern->getItems().at(0); ASSERT_EQ(kGraphicElementTypePath, fillPatternPath->getType()); - ASSERT_EQ(Color(Color::BLUE), fillPatternPath->getValue(kGraphicPropertyFill).asColor()); + ASSERT_EQ(Color::BLUE, fillPatternPath->getValue(kGraphicPropertyFill).getColor()); ASSERT_EQ(0.8, path->getValue(kGraphicPropertyFillOpacity).asNumber()); ASSERT_EQ("M 25 0 L 50 25 L 25 50 L 0 25 z", path->getValue(kGraphicPropertyPathData).asString()); @@ -1866,8 +1866,8 @@ TEST_F(GraphicComponentTest, StyleEverything) ASSERT_EQ(Gradient::LINEAR, stroke.getProperty(kGradientPropertyType).asInt()); colorRange = stroke.getProperty(kGradientPropertyColorRange); ASSERT_EQ(2, colorRange.size()); - ASSERT_EQ(Color(Color::GREEN), colorRange.at(0).asColor()); - ASSERT_EQ(Color(Color::WHITE), colorRange.at(1).asColor()); + ASSERT_EQ(Color::GREEN, colorRange.at(0).getColor()); + ASSERT_EQ(Color::WHITE, colorRange.at(1).getColor()); inputRange = stroke.getProperty(kGradientPropertyInputRange); ASSERT_EQ(2, inputRange.size()); @@ -1900,7 +1900,7 @@ TEST_F(GraphicComponentTest, StyleEverything) fillPattern = fill.getGraphicPattern(); fillPatternPath = fillPattern->getItems().at(0); ASSERT_EQ(kGraphicElementTypePath, fillPatternPath->getType()); - ASSERT_EQ(Color(Color::BLUE), fillPatternPath->getValue(kGraphicPropertyFill).asColor()); + ASSERT_EQ(Color::BLUE, fillPatternPath->getValue(kGraphicPropertyFill).getColor()); ASSERT_EQ(0.8, text->getValue(kGraphicPropertyFillOpacity).asNumber()); ASSERT_EQ("funky", text->getValue(kGraphicPropertyFontFamily).asString()); diff --git a/unit/graphic/unittest_graphic_filters.cpp b/unit/graphic/unittest_graphic_filters.cpp index 7b05412..6d8a02e 100644 --- a/unit/graphic/unittest_graphic_filters.cpp +++ b/unit/graphic/unittest_graphic_filters.cpp @@ -94,7 +94,7 @@ TEST(GraphicFilterTest, DropShadowGraphicFilter) ASSERT_TRUE(filterObject.isGraphicFilter()) << m.json; const auto& filter = filterObject.getGraphicFilter(); ASSERT_EQ(kGraphicFilterTypeDropShadow, filter.getType()) << m.json; - ASSERT_TRUE(IsEqual(m.color, filter.getValue(kGraphicPropertyFilterColor).asColor())) << m.json; + ASSERT_TRUE(IsEqual(m.color, filter.getValue(kGraphicPropertyFilterColor).asColor(*context))) << m.json; ASSERT_TRUE(IsEqual(m.horizontalOffset, filter.getValue(kGraphicPropertyFilterHorizontalOffset))) << m.json; ASSERT_TRUE(IsEqual(m.radius, filter.getValue(kGraphicPropertyFilterRadius))) << m.json; ASSERT_TRUE(IsEqual(m.verticalOffset, filter.getValue(kGraphicPropertyFilterVerticalOffset))) << m.json; diff --git a/unit/livedata/unittest_livearray_rebuild.cpp b/unit/livedata/unittest_livearray_rebuild.cpp index e919f48..b916662 100644 --- a/unit/livedata/unittest_livearray_rebuild.cpp +++ b/unit/livedata/unittest_livearray_rebuild.cpp @@ -135,6 +135,7 @@ class LiveArrayRebuildTest : public DocumentWrapper { ASSERT_FALSE(root->hasEvent()); executeScroll(component, distance); advanceTime(1000); + advanceTime(10); } private: @@ -896,6 +897,7 @@ TEST_F(LiveArrayRebuildTest, SequencePositionContext) config->liveData("TestArray", myArray); loadDocument(LIVE_SEQUENCE); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); ASSERT_EQ(0, component->getChildCount()); @@ -960,6 +962,7 @@ TEST_F(LiveArrayRebuildTest, SequencePositionContext) // Move position and check it's still right component->update(kUpdateScrollPosition, 100); + advanceTime(10); root->clearPending(); scrollPosition = component->getCalculated(kPropertyScrollPosition).asNumber(); @@ -1094,6 +1097,7 @@ TEST_F(LiveArrayRebuildTest, SequenceScrollingContext) config->liveData("TestArray", myArray); loadDocument(LIVE_SEQUENCE); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); ASSERT_EQ(0, component->getChildCount()); @@ -1214,6 +1218,7 @@ TEST_F(LiveArrayRebuildTest, SequenceUpdateContext) config->liveData("TestArray", myArray); loadDocument(LIVE_SEQUENCE); + advanceTime(10); ASSERT_EQ(kComponentTypeSequence, component->getType()); ASSERT_EQ(5, component->getChildCount()); @@ -1224,6 +1229,7 @@ TEST_F(LiveArrayRebuildTest, SequenceUpdateContext) // Move position component->update(kUpdateScrollPosition, 100); + advanceTime(10); root->clearPending(); // Update first item size and see if position moved on. @@ -1674,7 +1680,7 @@ CheckSpacing(const CoreComponentPtr& comp, float spacing) { TEST_F(LiveArrayRebuildTest, SpacedSequence) { auto myArray = LiveArray::create(ObjectArray{0, 1}); config->liveData("TestArray", myArray); - config->sequenceChildCache(2); + config->set(RootProperty::kSequenceChildCache, 2); loadDocument(SPACED_SEQUENCE); ASSERT_TRUE(component); @@ -1753,7 +1759,7 @@ static const char *SPACED_CONTAINER = R"({ TEST_F(LiveArrayRebuildTest, SpacedContainer) { auto myArray = LiveArray::create(ObjectArray{0, 1}); config->liveData("TestArray", myArray); - config->sequenceChildCache(2); + config->set(RootProperty::kSequenceChildCache, 2); loadDocument(SPACED_CONTAINER); ASSERT_TRUE(component); diff --git a/unit/media/testmediaplayer.cpp b/unit/media/testmediaplayer.cpp index d6415a4..df9b608 100644 --- a/unit/media/testmediaplayer.cpp +++ b/unit/media/testmediaplayer.cpp @@ -364,7 +364,8 @@ TestMediaPlayer::doCallback(MediaPlayerEventType eventType) mPlayer->getPosition(), // Current time mPlayer->getDuration(), // Current track duration !mPlayer->isPlaying(), // paused - atEnd) // ended + atEnd, // ended + false) // muted .withTrackState(mPlayer->getTrackState())); } diff --git a/unit/media/unittest_media_manager.cpp b/unit/media/unittest_media_manager.cpp index 6f901c4..5c01fa5 100644 --- a/unit/media/unittest_media_manager.cpp +++ b/unit/media/unittest_media_manager.cpp @@ -55,8 +55,30 @@ class MediaManagerTest : public DocumentWrapper for (const auto& m : sources.getArray()) actualSources.emplace(m.getString()); + if (expectedSources.size() != actualSources.size()) + return ::testing::AssertionFailure() + << "Source list size mismatch. Expected: " << expectedSources.size() + << ", actual: " << actualSources.size(); + + std::string es; + std::string as; + + auto first = true; + for (auto const& s : expectedSources) { + if (!first) es += ','; + first = false; + es += s; + } + + first = true; + for (auto const& s : actualSources) { + if (!first) as += ','; + first = false; + as += s; + } + if (expectedSources != actualSources) - return ::testing::AssertionFailure() << "Source mismatch"; + return ::testing::AssertionFailure() << "Source mismatch. Expected: [" << es << "], actual: [" << as << "]"; return ::testing::AssertionSuccess(); } @@ -278,6 +300,7 @@ TEST_F(MediaManagerTest, SimpleSequence) { ASSERT_FALSE(root->hasEvent()); component->update(kUpdateScrollPosition, 100); + advanceTime(10); root->clearPending(); ASSERT_TRUE(MediaRequested(kEventMediaTypeImage, "universe5")); @@ -2389,6 +2412,92 @@ TEST_F(MediaManagerTest, VideoWithMultipleHeaders) { ASSERT_EQ(headers.at(4), "E: F"); } +static const char* CHANGING_IMAGES = R"({ + "type": "APL", + "version": "1.8", + "mainTemplate": { + "item": { + "type": "Image", + "id": "IMAGE", + "source": "duck.png" + } + } +})"; + +TEST_F(MediaManagerTest, ChangingImages) { + loadDocument(CHANGING_IMAGES); + ASSERT_FALSE(root->isDirty()); + + // Event should be fired that requests media to be loaded. + ASSERT_TRUE(MediaRequested(kEventMediaTypeImage, "duck.png")); + ASSERT_EQ(kMediaStatePending, component->getCalculated(kPropertyMediaState).getInteger()); + ASSERT_TRUE(CheckLoadedMedia(component, "duck.png")); + + executeCommand("SetValue", + {{"componentId", "IMAGE"}, {"property", "source"}, {"value", "duck2.png"}}, + true); + ASSERT_TRUE(CheckDirty(component, kPropertySource, kPropertyMediaState, kPropertyVisualHash)); + + // Event should be fired that requests media to be loaded. + ASSERT_TRUE(MediaRequested(kEventMediaTypeImage, "duck2.png")); + ASSERT_EQ(kMediaStatePending, component->getCalculated(kPropertyMediaState).getInteger()); + ASSERT_TRUE(CheckLoadedMedia(component, "duck2.png")); +} + + +static const char* FIRST_LAST_SEQUENCE = R"apl({ + "type": "APL", + "version": "1.10", + "theme": "dark", + "mainTemplate": { + "items": [ + { + "type": "Sequence", + "width": 500, + "height": 500, + "data": [0,1,2,3,4,5,6,7,8,9], + "firstItem": { + "type": "Image", + "width": 200, + "height": 100, + "source": "universe_first" + }, + "items": [ + { + "type": "Image", + "width": 200, + "height": 200, + "source": "universe_${data}" + } + ], + "lastItem": { + "type": "Image", + "width": 200, + "height": 100, + "source": "universe_last" + } + } + ] + } +})apl"; + +TEST_F(MediaManagerTest, FirstLastSequence) { + config->set(RootProperty::kSequenceChildCache, 1); + loadDocument(FIRST_LAST_SEQUENCE); + + ASSERT_FALSE(root->isDirty()); + + ASSERT_TRUE(MediaRequested(kEventMediaTypeImage, "universe_first", "universe_0", "universe_1", "universe_2")); + + // Two more will be requested to cover cache position here. + advanceTime(10); + + ASSERT_TRUE(MediaRequested(kEventMediaTypeImage, "universe_3", "universe_4")); + + ASSERT_FALSE(root->hasEvent()); +} + + static const char* DEEP_EVALUATE_SOURCE = R"apl({ "type": "APL", "version": "1.5", diff --git a/unit/primitives/CMakeLists.txt b/unit/primitives/CMakeLists.txt index 573aef5..2a0756d 100644 --- a/unit/primitives/CMakeLists.txt +++ b/unit/primitives/CMakeLists.txt @@ -18,7 +18,10 @@ target_sources_local(unittest unittest_filters.cpp unittest_keyboard.cpp unittest_object.cpp + unittest_radii.cpp + unittest_range.cpp unittest_rect.cpp + unittest_roundedrect.cpp unittest_styledtext.cpp unittest_symbols.cpp unittest_time_grammar.cpp diff --git a/unit/primitives/unittest_color.cpp b/unit/primitives/unittest_color.cpp index d1db460..d86a61d 100644 --- a/unit/primitives/unittest_color.cpp +++ b/unit/primitives/unittest_color.cpp @@ -15,6 +15,8 @@ #include "../testeventloop.h" +#include + #include "apl/common.h" using namespace apl; @@ -113,3 +115,36 @@ TEST_F(ColorTest, BadEnumConversionTest) // it should be able to convert back to a color ASSERT_EQ(Color::RED, Color(d_color)); } + +TEST_F(ColorTest, LocaleIndependentLiterals) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + EXPECT_EQ( 0xff0000ff, Color(session, "red") ); + EXPECT_EQ( 0x008000ff, Color(session, "green") ); + EXPECT_EQ( 0xeeddbbff, Color(session, "#edb")); + EXPECT_EQ( 0x11223344, Color(session, "#1234")); + EXPECT_EQ( 0x123456ff, Color(session, "#123456")); + EXPECT_EQ( 0xfedcba98, Color(session, "#fedcba98")); + + EXPECT_EQ( 0x0000ff7f, Color(session, "rgba(blue, 50%)")); + EXPECT_EQ( 0x0080003f, Color(session, "rgb(rgba(green, 50%), 50%)")); + + EXPECT_EQ( 0x8040c0ff, Color(session, "rgb(128, 64, 192)")); + EXPECT_EQ( 0xff072040, Color(session, "rgba(255, 7, 32, 25%)")); + EXPECT_EQ( 0xff072040, Color(session, "rgba(255, 7, 32, 0.25)")); + EXPECT_EQ( 0xff072040, Color(session, "rgba(255, 7, 32, .25)")); + + EXPECT_EQ( 0xb8860bff, Color(session, "darkgoldenrod")); + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); +} + +TEST_F(ColorTest, Transparent) +{ + ASSERT_TRUE(Color(Color::TRANSPARENT).transparent()); + ASSERT_FALSE(Color(Color::RED).transparent()); + ASSERT_FALSE(Color(session, "rgb(0,0,0)").transparent()); + ASSERT_TRUE(Color(session, "rgb(0,0,0,0)").transparent()); +} \ No newline at end of file diff --git a/unit/primitives/unittest_dimension.cpp b/unit/primitives/unittest_dimension.cpp index 2ea9a55..5a7e527 100644 --- a/unit/primitives/unittest_dimension.cpp +++ b/unit/primitives/unittest_dimension.cpp @@ -15,6 +15,8 @@ #include "../testeventloop.h" +#include + using namespace apl; class DimensionTest : public MemoryWrapper { @@ -100,6 +102,28 @@ TEST_F(DimensionTest, Basic) EXPECT_TRUE(IsRelative(-124, Dimension(*c, " -124% "))); } +TEST_F(DimensionTest, DimensionParsingIgnoresCLocale) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + EXPECT_TRUE(IsAbsolute(1024, Dimension(*c, " 100 vw "))); + EXPECT_TRUE(IsAbsolute(250, Dimension(*c, "50vh"))); + EXPECT_TRUE(IsAbsolute(125, Dimension(*c, "125 dp"))); + EXPECT_TRUE(IsAbsolute(150, Dimension(*c, "150"))); + EXPECT_TRUE(IsAbsolute(175, Dimension(*c, "175.0"))); + EXPECT_TRUE(IsAbsolute(150, Dimension(*c, " 300px "))); + + EXPECT_TRUE(IsRelative(30, Dimension(*c, "30%"))); + EXPECT_TRUE(IsRelative(31.5, Dimension(*c, "31.5%"))); + + EXPECT_TRUE(IsRelative(-30, Dimension(*c, "-30%"))); + EXPECT_TRUE(IsRelative(-31.5, Dimension(*c, "-31.5%"))); + EXPECT_TRUE(IsRelative(-124, Dimension(*c, " -124% "))); + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); +} + TEST_F(DimensionTest, PreferRelative) { EXPECT_TRUE(Dimension(*c, "auto", true).isAuto()); diff --git a/unit/primitives/unittest_filters.cpp b/unit/primitives/unittest_filters.cpp index 887d477..e790163 100644 --- a/unit/primitives/unittest_filters.cpp +++ b/unit/primitives/unittest_filters.cpp @@ -160,16 +160,16 @@ namespace { struct GradientFilterTest { std::string json; Gradient::GradientType type; - std::vector colorRange; - std::vector inputRange; + std::vector colorRange; + std::vector inputRange; }; std::vector GRADIENT_TESTS = { { // Minimal gradient R"({"type":"Gradient", "gradient": {"type": "linear", "colorRange":["blue", "red"]}})", Gradient::GradientType::LINEAR, - std::vector{Color(Color::BLUE), Color(Color::RED)}, - std::vector{0, 1}, + std::vector{Color(Color::BLUE), Color(Color::RED)}, + std::vector{0, 1}, }, { // Bad gradient - need to specify an actual gradient R"({"type": "Gradient"})", @@ -180,8 +180,8 @@ std::vector GRADIENT_TESTS = { { R"({"type":"Gradient", "gradient": {"type": "radial", "colorRange":["green", "red"]}})", Gradient::GradientType::RADIAL, - std::vector{Color(Color::GREEN), Color(Color::RED)}, - std::vector{0, 1}, + std::vector{Color(Color::GREEN), Color(Color::RED)}, + std::vector{0, 1}, }, { // Invalid gradient - one that does not have a type R"({"type":"Gradient", "gradient": {"type": "odd", "colorRange":["green", "red"]}})", @@ -208,8 +208,8 @@ TEST(FilterTest, GradientFilter) { } else { const auto& g = gradient.getGradient(); ASSERT_EQ(m.type, g.getType()) << m.json; - ASSERT_EQ(m.colorRange, g.getColorRange()) << m.json; - ASSERT_EQ(m.inputRange, g.getInputRange()) << m.json; + ASSERT_EQ(m.colorRange, g.getProperty(kGradientPropertyColorRange).getArray()) << m.json; + ASSERT_EQ(m.inputRange, g.getProperty(kGradientPropertyInputRange).getArray()) << m.json; } } } diff --git a/unit/primitives/unittest_object.cpp b/unit/primitives/unittest_object.cpp index 4ce16cc..c343e46 100644 --- a/unit/primitives/unittest_object.cpp +++ b/unit/primitives/unittest_object.cpp @@ -197,32 +197,33 @@ TEST(ObjectTest, RapidJson) TEST(ObjectTest, Color) { + class TestSession : public Session { + public: + void write(const char *filename, const char *func, const char *value) override { + count++; + } + + int count = 0; + }; + + auto session = std::make_shared(); + Object o = Object(Color(Color::RED)); ASSERT_TRUE(o.isColor()); - ASSERT_EQ(Color::RED, o.asColor()); + ASSERT_EQ(Color::RED, o.asColor(session)); o = Object(Color::RED); ASSERT_TRUE(o.isNumber()); - ASSERT_EQ(Color::RED, o.asColor()); + ASSERT_EQ(Color::RED, o.asColor(session)); o = Object("red"); ASSERT_TRUE(o.isString()); - ASSERT_EQ(Color::RED, o.asColor()); + ASSERT_EQ(Color::RED, o.asColor(session)); o = Object::NULL_OBJECT(); ASSERT_TRUE(o.isNull()); - ASSERT_EQ(Color::TRANSPARENT, o.asColor()); - - class TestSession : public Session { - public: - void write(const char *filename, const char *func, const char *value) override { - count++; - } - - int count = 0; - }; + ASSERT_EQ(Color::TRANSPARENT, o.asColor(session)); - auto session = std::make_shared(); o = Object("blue"); ASSERT_EQ(Color::BLUE, o.asColor(session)); ASSERT_EQ(0, session->count); @@ -252,18 +253,18 @@ TEST(ObjectTest, Gradient) ASSERT_TRUE(a.isGradient()); ASSERT_EQ(Gradient::RADIAL, a.getGradient().getType()); - ASSERT_EQ(0xff0000ff, a.getGradient().getColorRange().at(0).get()); + ASSERT_EQ(0xff0000ff, a.getGradient().getProperty(kGradientPropertyColorRange).at(0).getColor()); Object b(a); ASSERT_TRUE(b.isGradient()); ASSERT_EQ(Gradient::RADIAL, b.getGradient().getType()); - ASSERT_EQ(0xff0000ff, b.getGradient().getColorRange().at(0).get()); + ASSERT_EQ(0xff0000ff, b.getGradient().getProperty(kGradientPropertyColorRange).at(0).getColor()); Object c; c = a; ASSERT_TRUE(c.isGradient()); ASSERT_EQ(Gradient::RADIAL, c.getGradient().getType()); - ASSERT_EQ(0xff0000ff, c.getGradient().getColorRange().at(0).get()); + ASSERT_EQ(0xff0000ff, c.getGradient().getProperty(kGradientPropertyColorRange).at(0).getColor()); { rapidjson::Document doc2; @@ -282,17 +283,17 @@ TEST(ObjectTest, Gradient) ASSERT_TRUE(c.isGradient()); ASSERT_EQ(Gradient::LINEAR, c.getGradient().getType()); - ASSERT_EQ(0x0000ffff, c.getGradient().getColorRange().at(0).get()); + ASSERT_EQ(0x0000ffff, c.getGradient().getProperty(kGradientPropertyColorRange).at(0).getColor()); b = c; ASSERT_TRUE(b.isGradient()); ASSERT_EQ(Gradient::LINEAR, b.getGradient().getType()); - ASSERT_EQ(0x0000ffff, b.getGradient().getColorRange().at(0).get()); + ASSERT_EQ(0x0000ffff, b.getGradient().getProperty(kGradientPropertyColorRange).at(0).getColor()); // Make sure a has not changed ASSERT_TRUE(a.isGradient()); ASSERT_EQ(Gradient::RADIAL, a.getGradient().getType()); - ASSERT_EQ(0xff0000ff, a.getGradient().getColorRange().at(0).get()); + ASSERT_EQ(0xff0000ff, a.getGradient().getProperty(kGradientPropertyColorRange).at(0).getColor()); } const char *BAD_CASES = @@ -430,7 +431,7 @@ TEST(ObjectTest, Radii) { Object a = Object(Radii()); ASSERT_EQ(Object::EMPTY_RADII(), a); - ASSERT_TRUE(a.getRadii().isEmpty()); + ASSERT_TRUE(a.getRadii().empty()); Object b = Object(Radii(4)); ASSERT_TRUE(b.isRadii()); @@ -438,7 +439,7 @@ TEST(ObjectTest, Radii) ASSERT_EQ(4, b.getRadii().bottomRight()); ASSERT_EQ(4, b.getRadii().topLeft()); ASSERT_EQ(4, b.getRadii().topRight()); - ASSERT_FALSE(b.getRadii().isEmpty()); + ASSERT_FALSE(b.getRadii().empty()); Object c = Object(Radii(1,2,3,4)); ASSERT_TRUE(c.isRadii()); @@ -452,7 +453,7 @@ TEST(ObjectTest, Radii) ASSERT_EQ(4, c.getRadii().radius(Radii::kBottomRight)); ASSERT_EQ(Radii(1,2,3,4), c.getRadii()); ASSERT_NE(Radii(1,2,3,5), c.getRadii()); - ASSERT_FALSE(c.getRadii().isEmpty()); + ASSERT_FALSE(c.getRadii().empty()); auto foo = std::array{1,2,3,4}; ASSERT_EQ(foo, c.getRadii().get()); @@ -496,6 +497,27 @@ TEST(ObjectTest, DoubleConversion) #endif } +TEST(ObjectTest, DoubleConversionIgnoresCLocale) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + feclearexcept (FE_ALL_EXCEPT); + for (const auto& m : DOUBLE_TEST) { + Object object(m.first); + std::string result = object.asString(); + EXPECT_EQ(m.second, result) << m.first << " : " << m.second; + } + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); + + // Note: Not all architectures support FE_INVALID (e.g., wasm) +#if defined(FE_INVALID) + int fe = fetestexcept (FE_INVALID); + ASSERT_EQ(0, fe); +#endif +} + static const std::vector> STRING_TO_DOUBLE{ {"0", 0}, {"1", 1}, @@ -529,6 +551,22 @@ TEST(ObjectTest, StringToDouble) } } +TEST(ObjectTest, StringToDoubleIgnoresCLocale) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + for (const auto& m : STRING_TO_DOUBLE) { + Object object(m.first); + double result = object.asNumber(); + if (std::isnan(result) && std::isnan(m.second)) // NaN do not compare as equal, but they are valid + continue; + EXPECT_EQ(m.second, result) << "'" << m.first << "' : " << m.second; + } + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); +} + TEST(ObjectTest, AbsoluteDimensionConversion) { auto dimension = Object(Dimension(DimensionType::Absolute, 42)); @@ -659,27 +697,27 @@ TEST(ObjectTest, WhenDimensionIsNotFiniteSerializeReturnsZero) class DocumentObjectTest : public CommandTest {}; static const char * SEND_EVENT_DIMENSION_NAN = R"apl({ - "type": "APL", - "version": "1.1", - "resources": [ - { - "dimension": { - "absDimen": "${100/0}" - } - } - ], - "mainTemplate": { - "item": { - "type": "TouchWrapper", - "onPress": { - "type": "SendEvent", - "arguments": [ - "@absDimen" - ] - } - } - } - })apl"; + "type": "APL", + "version": "1.1", + "resources": [ + { + "dimension": { + "absDimen": "${100/0}" + } + } + ], + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "SendEvent", + "arguments": [ + "@absDimen" + ] + } + } + } +})apl"; TEST_F(DocumentObjectTest, WithNewArguments) { @@ -700,27 +738,27 @@ TEST_F(DocumentObjectTest, WithNewArguments) static const char * SEND_EVENT_NUMBER_NAN = R"apl({ - "type": "APL", - "version": "1.1", - "resources": [ - { - "number": { - "value": "${100/0}" - } - } - ], - "mainTemplate": { - "item": { - "type": "TouchWrapper", - "onPress": { - "type": "SendEvent", - "arguments": [ - "@value" - ] - } - } - } - })apl"; + "type": "APL", + "version": "1.1", + "resources": [ + { + "number": { + "value": "${100/0}" + } + } + ], + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "onPress": { + "type": "SendEvent", + "arguments": [ + "@value" + ] + } + } + } +})apl"; TEST_F(DocumentObjectTest, WhenNumberIsNotFiniteSerializeReturnsNull) { diff --git a/unit/primitives/unittest_radii.cpp b/unit/primitives/unittest_radii.cpp new file mode 100644 index 0000000..bb756d4 --- /dev/null +++ b/unit/primitives/unittest_radii.cpp @@ -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. + */ + +#include "gtest/gtest.h" + +#include "apl/primitives/radii.h" + +using namespace apl; + +class RadiiTest : public ::testing::Test {}; + +TEST_F(RadiiTest, Empty) +{ + Radii radii; + ASSERT_TRUE(radii.empty()); + ASSERT_TRUE(radii.isRegular()); + ASSERT_FALSE(radii.truthy()); + + ASSERT_EQ(0, radii.topLeft()); + ASSERT_EQ(0, radii.topRight()); + ASSERT_EQ(0, radii.bottomLeft()); + ASSERT_EQ(0, radii.bottomRight()); + ASSERT_EQ(0, radii.radius(apl::Radii::kTopLeft)); + ASSERT_EQ(0, radii.radius(apl::Radii::kTopRight)); + ASSERT_EQ(0, radii.radius(apl::Radii::kBottomLeft)); + ASSERT_EQ(0, radii.radius(apl::Radii::kBottomRight)); +} + +TEST_F(RadiiTest, Simple) +{ + auto radii = Radii(20); + + ASSERT_FALSE(radii.empty()); + ASSERT_TRUE(radii.isRegular()); + ASSERT_TRUE(radii.truthy()); + + ASSERT_EQ(20, radii.topLeft()); + ASSERT_EQ(20, radii.topRight()); + ASSERT_EQ(20, radii.bottomLeft()); + ASSERT_EQ(20, radii.bottomRight()); + ASSERT_EQ(20, radii.radius(apl::Radii::kTopLeft)); + ASSERT_EQ(20, radii.radius(apl::Radii::kTopRight)); + ASSERT_EQ(20, radii.radius(apl::Radii::kBottomLeft)); + ASSERT_EQ(20, radii.radius(apl::Radii::kBottomRight)); +} + +TEST_F(RadiiTest, Complex) +{ + auto radii = Radii(1,2,3,4); + + ASSERT_FALSE(radii.empty()); + ASSERT_FALSE(radii.isRegular()); + ASSERT_TRUE(radii.truthy()); + + ASSERT_EQ(1, radii.topLeft()); + ASSERT_EQ(2, radii.topRight()); + ASSERT_EQ(3, radii.bottomLeft()); + ASSERT_EQ(4, radii.bottomRight()); + ASSERT_EQ(1, radii.radius(apl::Radii::kTopLeft)); + ASSERT_EQ(2, radii.radius(apl::Radii::kTopRight)); + ASSERT_EQ(3, radii.radius(apl::Radii::kBottomLeft)); + ASSERT_EQ(4, radii.radius(apl::Radii::kBottomRight)); +} + +TEST_F(RadiiTest, Equality) +{ + ASSERT_EQ(Radii(), Radii(0) ); + ASSERT_EQ(Radii(10), Radii(10,10,10,10)); + ASSERT_NE(Radii(10), Radii(10,10,10,2)); +} + +TEST_F(RadiiTest, Sanitize) +{ + ASSERT_EQ(Radii(), Radii(-10)); + ASSERT_EQ(Radii(), Radii(-1, -2, -3, -4)); +} + +TEST_F(RadiiTest, Subtract) +{ + auto radii = Radii(10,15,20,25); + ASSERT_EQ(radii.subtract(5), Radii(5,10,15,20)); + ASSERT_EQ(radii.subtract(20), Radii(0,0,0,5)); + ASSERT_EQ(radii.subtract(30), Radii()); +} \ No newline at end of file diff --git a/unit/utils/unittest_range.cpp b/unit/primitives/unittest_range.cpp similarity index 64% rename from unit/utils/unittest_range.cpp rename to unit/primitives/unittest_range.cpp index ef31822..3de0650 100644 --- a/unit/utils/unittest_range.cpp +++ b/unit/primitives/unittest_range.cpp @@ -15,7 +15,7 @@ #include "gtest/gtest.h" -#include "apl/utils/range.h" +#include "apl/primitives/range.h" using namespace apl; @@ -134,4 +134,66 @@ TEST_F(RangeTest, ExtendTowards) ASSERT_EQ(0, r1.lowerBound()); ASSERT_EQ(5, r1.upperBound()); +} + +TEST_F(RangeTest, Iterator) +{ + Range r1{2,4}; + + auto it = r1.begin(); + ASSERT_EQ(2, *it); + ASSERT_EQ(2, *it++); + ASSERT_EQ(3, *it++); + ASSERT_EQ(4, *it++); + ASSERT_EQ(r1.end(), it); + + Range r2; + ASSERT_EQ(r2.begin(), r2.end()); +} + +TEST_F(RangeTest, Serialize) +{ + rapidjson::Document doc; + Range r1{1,10}; + + auto result = r1.serialize(doc.GetAllocator()); + ASSERT_TRUE(result.IsObject()); + ASSERT_EQ(1, result["lowerBound"].GetInt()); + ASSERT_EQ(10, result["upperBound"].GetInt()); +} + +TEST_F(RangeTest, Intersect) +{ + ASSERT_EQ( Range(5,6), Range(2, 10).intersectWith(Range(5, 6))); + ASSERT_EQ( Range(5,10), Range(2, 10).intersectWith(Range(5, 15))); + ASSERT_EQ( Range(2,5), Range(2, 10).intersectWith(Range(0, 5))); + ASSERT_EQ( Range(2,10), Range(2, 10).intersectWith(Range(0, 15))); + + ASSERT_EQ( Range(), Range().intersectWith(Range())); + ASSERT_EQ( Range(), Range(2, 10).intersectWith(Range())); + ASSERT_EQ( Range(), Range().intersectWith(Range(-10, 10))); + ASSERT_EQ( Range(), Range(2, 10).intersectWith(Range(11, 20))); + ASSERT_EQ( Range(), Range(2, 10).intersectWith(Range(-10, 1))); +} + +TEST_F(RangeTest, SubsetBelow) +{ + ASSERT_EQ( Range(2,4), Range(2,10).subsetBelow(5)); + ASSERT_EQ( Range(2,9), Range(2,10).subsetBelow(10)); + ASSERT_EQ( Range(2,10), Range(2,10).subsetBelow(20)); + ASSERT_EQ( Range(2,2), Range(2,10).subsetBelow(3)); + ASSERT_EQ( Range(), Range(2,10).subsetBelow(2)); + + ASSERT_EQ( Range(), Range().subsetBelow(2)); +} + +TEST_F(RangeTest, SubsetAbove) +{ + ASSERT_EQ( Range(6,10), Range(2,10).subsetAbove(5)); + ASSERT_EQ( Range(), Range(2,10).subsetAbove(10)); + ASSERT_EQ( Range(2,10), Range(2,10).subsetAbove(1)); + ASSERT_EQ( Range(10,10), Range(2,10).subsetAbove(9)); + ASSERT_EQ( Range(3,10), Range(2,10).subsetAbove(2)); + + ASSERT_EQ( Range(), Range().subsetAbove(2)); } \ No newline at end of file diff --git a/unit/primitives/unittest_rect.cpp b/unit/primitives/unittest_rect.cpp index f41181c..b7fe597 100644 --- a/unit/primitives/unittest_rect.cpp +++ b/unit/primitives/unittest_rect.cpp @@ -140,11 +140,11 @@ TEST_F(RectTest, EqualityNaN) { TEST_F(RectTest, Empty) { Rect rect(0, 0, 0, 0); - ASSERT_TRUE(rect.isEmpty()); + ASSERT_TRUE(rect.empty()); rect = Rect(0, 0, NAN, 100); - ASSERT_TRUE(rect.isEmpty()); + ASSERT_TRUE(rect.empty()); rect = Rect(0, 0, 100, NAN); - ASSERT_TRUE(rect.isEmpty()); + ASSERT_TRUE(rect.empty()); } TEST_F(RectTest, Contains) { @@ -175,7 +175,7 @@ TEST_F(RectTest, DistanceTo) { TEST_F(RectTest, Serialize) { Rect rect(10, 20, 30, 40); - ASSERT_FALSE(rect.isEmpty()); + ASSERT_FALSE(rect.empty()); rapidjson::Document doc; @@ -195,4 +195,28 @@ TEST_F(RectTest, Serialize) { ASSERT_EQ(0, serialized[1]); ASSERT_EQ(0, serialized[2]); ASSERT_EQ(0, serialized[3]); +} + +TEST_F(RectTest, Extend) { + // Extending with an empty rect doesn't do anything + ASSERT_EQ(Rect(), Rect().extend(Rect())); + ASSERT_EQ(Rect(1,2,3,4), Rect().extend(Rect(1,2,3,4))); + ASSERT_EQ(Rect(1,2,3,4), Rect(1,2,3,4).extend(Rect())); + + ASSERT_EQ(Rect(0,0,30,40), Rect(0,0,10,10).extend(Rect(20,30,10,10))); + ASSERT_EQ(Rect(10,15,15,15), Rect(10,15,10,10).extend(Rect(15,20,10,10))); + ASSERT_EQ(Rect(-10,-20,230,300), Rect(-10,-20,100,120).extend(Rect(20,30,200,250))); + ASSERT_EQ(Rect(-25,-25,200,200), Rect(25,-25,100,200).extend(Rect(-25,25,200,100))); +} + +TEST_F(RectTest, Inset) { + ASSERT_EQ(Rect(3,4,3,4), Rect(2,3,5,6).inset(1)); + ASSERT_EQ(Rect(0,1,5,6), Rect(1,2,3,4).inset(-1)); // Grow outwards + + ASSERT_EQ(Rect(16,10,8,20), Rect(10,10,20,20).inset(6, 0)); + ASSERT_EQ(Rect(10,15,20,10), Rect(10,10,20,20).inset(0,5)); + + // 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)); } \ No newline at end of file diff --git a/unit/primitives/unittest_roundedrect.cpp b/unit/primitives/unittest_roundedrect.cpp new file mode 100644 index 0000000..50238d8 --- /dev/null +++ b/unit/primitives/unittest_roundedrect.cpp @@ -0,0 +1,117 @@ +/** + * 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 "rapidjson/document.h" + +#include "apl/primitives/roundedrect.h" +#include + +using namespace apl; + +class RoundedRectTest : public ::testing::Test {}; + +TEST_F(RoundedRectTest, Empty) { + ASSERT_TRUE(RoundedRect().empty()); + ASSERT_TRUE(RoundedRect().isRect()); + ASSERT_TRUE(RoundedRect().isRegular()); + + ASSERT_EQ(Rect(), RoundedRect().rect()); + ASSERT_EQ(Radii(), RoundedRect().radii()); + ASSERT_EQ(Size(), RoundedRect().getSize()); +} + +TEST_F(RoundedRectTest, Basic) { + auto rrect = RoundedRect(Rect(10,20,100,200), 25); + + ASSERT_FALSE(rrect.empty()); + ASSERT_FALSE(rrect.isRect()); + ASSERT_TRUE(rrect.isRegular()); + + ASSERT_EQ(Rect(10,20,100,200), rrect.rect()); + ASSERT_EQ(Radii(25), rrect.radii()); + ASSERT_EQ(Point(10,20), rrect.getTopLeft()); + ASSERT_EQ(Size(100,200), rrect.getSize()); +} + +TEST_F(RoundedRectTest, Complex) { + auto rrect = RoundedRect(Rect(10,20,100,200), Radii(5,10,15,20)); + + ASSERT_FALSE(rrect.empty()); + ASSERT_FALSE(rrect.isRect()); + ASSERT_FALSE(rrect.isRegular()); + + ASSERT_EQ(Rect(10,20,100,200), rrect.rect()); + ASSERT_EQ(Radii(5,10,15,20), rrect.radii()); + ASSERT_EQ(Point(10,20), rrect.getTopLeft()); + ASSERT_EQ(Size(100,200), rrect.getSize()); +} + +TEST_F(RoundedRectTest, Trimmed) { + // A zero width -> all the radii are trimmed to zero + auto rr = RoundedRect(Rect(10,20,0,10), Radii(20)); + ASSERT_EQ(Radii(0,0,0,0), rr.radii()); + + // A square with too much of a radius is trimmed to a circle + rr = RoundedRect(Rect(10,20,10,10), Radii(100)); + ASSERT_EQ(Radii(5), rr.radii()); + + // A rectangle with too much of a radius is trimmed to a pill shape + rr = RoundedRect(Rect(0,0,100,20), Radii(100)); + ASSERT_EQ(Radii(10), rr.radii()); + + rr = RoundedRect(Rect(0,0,20,100), Radii(100)); + ASSERT_EQ(Radii(10), rr.radii()); + + // A rectangle can have uneven radii if they all fit. They are clipped to a side length + rr = RoundedRect(Rect(0,0,20,100), Radii(20,0,50,0)); + ASSERT_EQ(Radii(20,0,20,0), rr.radii()); + + // If two radii conflict, they are scaled proportionally + auto radii = RoundedRect(Rect(0,0,100,100), Radii(60,80,0,0)).radii(); + ASSERT_FLOAT_EQ(radii.topLeft(), 100 * 6.0 / 14.0); + ASSERT_FLOAT_EQ(radii.topRight(), 100 * 8.0 / 14.0); +} + +TEST_F(RoundedRectTest, Equality) { + ASSERT_EQ(RoundedRect(), RoundedRect()); + ASSERT_EQ(RoundedRect(), RoundedRect(Rect(), Radii(5))); // Radius gets trimmed + + auto rect1 = Rect(0,10,20,30); + auto rect2 = Rect(-10,15,30,20); + auto radii1 = Radii(0,2,3,4); + auto radii2 = Radii(2,0,5,3); + + ASSERT_EQ(RoundedRect(rect1, radii1), RoundedRect(rect1, radii1)); + ASSERT_NE(RoundedRect(rect1, radii1), RoundedRect(rect2, radii1)); + ASSERT_NE(RoundedRect(rect1, radii1), RoundedRect(rect1, radii2)); + ASSERT_NE(RoundedRect(rect1, radii1), RoundedRect(rect2, radii2)); +} + +TEST_F(RoundedRectTest, Offset) { + auto rr = RoundedRect(Rect(10,0,100,100), Radii(5,5,5,5)); + auto rr2 = rr; + rr2.offset(Point(10,100)); + ASSERT_EQ(RoundedRect(Rect(20,100,100,100), Radii(5)), rr2); +} + +TEST_F(RoundedRectTest, Inset) { + auto rr = RoundedRect( Rect(-10, -20, 100, 200), Radii(10,20,30,40)); + ASSERT_EQ(RoundedRect(Rect(0,-10,80,180), Radii(0,10,20,30)), rr.inset(10)); + ASSERT_EQ(RoundedRect(Rect(-20,-30,120,220), Radii(20,30,40,50)), rr.inset(-10)); + + ASSERT_EQ(rr, rr.inset(-100).inset(100)); // Expand outwards and then back in => same + ASSERT_EQ(RoundedRect(rr.rect(), Radii(45)), rr.inset(45).inset(-45)); // Expand in and out => different +} \ No newline at end of file diff --git a/unit/primitives/unittest_styledtext.cpp b/unit/primitives/unittest_styledtext.cpp index 299ea1f..2038c80 100644 --- a/unit/primitives/unittest_styledtext.cpp +++ b/unit/primitives/unittest_styledtext.cpp @@ -29,80 +29,118 @@ class StyledTextTest : public ::testing::Test { void createAndVerifyStyledText(const std::string& rawText, const std::string& expectedText, size_t spansCount) { styledText = StyledText::create(*context, rawText); ASSERT_EQ(Object::kStyledTextType, styledText.getType()); - spans = std::make_shared>(styledText.getStyledText().getSpans()); + auto iterator = StyledText::Iterator(styledText.getStyledText()); ASSERT_EQ(expectedText, styledText.getStyledText().getText()); - ASSERT_EQ(spansCount, spans->size()); + ASSERT_EQ(spansCount, iterator.spanCount()); } - void verifySpan(size_t spanIndex, StyledText::SpanType type, size_t start, size_t end) { - verifySpan(spanIndex, type, start, end, 0); + ::testing::AssertionResult + verifyText(StyledText::Iterator& it, const std::string& text) { + auto token = it.next(); + if (token != StyledText::Iterator::kString) { + return ::testing::AssertionFailure() << "Mismatching token=" << token << ", expected=" << StyledText::Iterator::kString; + } + + if (text != it.getString()) { + return ::testing::AssertionFailure() << "Mismatching text=" << it.getString() << ", expected=" << text; + } + + return ::testing::AssertionSuccess(); + } + + ::testing::AssertionResult + verifySpanStart(StyledText::Iterator& it, StyledText::SpanType type) { + auto token = it.next(); + if (token != StyledText::Iterator::kStartSpan) { + return ::testing::AssertionFailure() << "Mismatching token=" << token << ", expected=" << StyledText::Iterator::kStartSpan; + } + + if (type != it.getSpanType()) { + return ::testing::AssertionFailure() << "Mismatching type=" << it.getSpanType() << ", expected=" << type; + } + + return ::testing::AssertionSuccess(); } - void verifySpan(size_t spanIndex, StyledText::SpanType type, size_t start, size_t end, size_t attributesCount) { - auto span = spans->at(spanIndex); - ASSERT_EQ(type, span.type); - ASSERT_EQ(start, span.start); - ASSERT_EQ(end, span.end); - ASSERT_EQ(span.attributes.size(), attributesCount); + ::testing::AssertionResult + verifySpanEnd(StyledText::Iterator& it, StyledText::SpanType type) { + auto token = it.next(); + if (token != StyledText::Iterator::kEndSpan) { + return ::testing::AssertionFailure() << "Mismatching token=" << token << ", expected=" << StyledText::Iterator::kEndSpan; + } + + if (type != it.getSpanType()) { + return ::testing::AssertionFailure() << "Mismatching type=" << it.getSpanType() << ", expected=" << type; + } + + return ::testing::AssertionSuccess(); } - void verifyColorAttribute( - size_t spanIndex, - size_t attributeIndex, - const std::string& attributeValue) { - auto span = spans->at(spanIndex); - auto attribute = span.attributes.at(attributeIndex); - EXPECT_EQ(attribute.name, 0); - EXPECT_TRUE(attribute.value.isColor()); - EXPECT_EQ(attribute.value.asString(), attributeValue); + ::testing::AssertionResult + verifyColorAttribute(StyledText::Iterator& it, size_t attributeIndex, const std::string& attributeValue) { + auto attribute = it.getSpanAttributes().at(attributeIndex); + if (attribute.name != 0) return ::testing::AssertionFailure() << "Wrong attribute name."; + if (!attribute.value.isColor()) return ::testing::AssertionFailure() << "Not a color."; + if (attribute.value.asString() != attributeValue) return ::testing::AssertionFailure() << "Wron value."; + + return ::testing::AssertionSuccess(); } - void verifyFontSizeAttribute( - size_t spanIndex, - size_t attributeIndex, - const std::string& attributeValue) { - auto span = spans->at(spanIndex); - auto attribute = span.attributes.at(attributeIndex); - EXPECT_EQ(attribute.name, 1); - EXPECT_TRUE(attribute.value.isAbsoluteDimension()); - EXPECT_EQ(attribute.value.asString(), attributeValue); + ::testing::AssertionResult + verifyFontSizeAttribute(StyledText::Iterator& it, size_t attributeIndex, const std::string& attributeValue) { + auto attribute = it.getSpanAttributes().at(attributeIndex); + if (attribute.name != 1) return ::testing::AssertionFailure() << "Wrong attribute name."; + if (!attribute.value.isAbsoluteDimension()) return ::testing::AssertionFailure() << "Not a dimension."; + if (attribute.value.asString() != attributeValue) return ::testing::AssertionFailure() << "Wron value."; + + return ::testing::AssertionSuccess(); } ContextPtr context; Object styledText; - -private: - std::shared_ptr> spans; }; TEST_F(StyledTextTest, Casting) { ASSERT_TRUE(IsEqual("FOO", StyledText::create(*context, "FOO").asString())); - ASSERT_TRUE(IsEqual(4.5, StyledText::create(*context, "4.5").asNumber())); - ASSERT_TRUE(IsEqual(4, StyledText::create(*context, "4.3").asInt())); - ASSERT_TRUE(IsEqual(Color(Color::RED), StyledText::create(*context, "#ff0000").asColor())); - - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 10), StyledText::create(*context, "10dp").asDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(), StyledText::create(*context, "auto").asDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 10), StyledText::create(*context, "10%").asDimension(*context))); - - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 5), StyledText::create(*context, "5dp").asAbsoluteDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 0), StyledText::create(*context, "auto").asAbsoluteDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 0), StyledText::create(*context, "10%").asAbsoluteDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 5), StyledText::create(*context, "5dp").asNonAutoDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 0), StyledText::create(*context, "auto").asNonAutoDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 10), StyledText::create(*context, "10%").asNonAutoDimension(*context))); - - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 5), StyledText::create(*context, "5dp").asNonAutoRelativeDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 0), StyledText::create(*context, "auto").asNonAutoRelativeDimension(*context))); - ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 10), StyledText::create(*context, "10%").asNonAutoRelativeDimension(*context))); + ASSERT_TRUE(IsEqual(4.5, Object(StyledText::create(*context, "4.5")).asNumber())); + ASSERT_TRUE(IsEqual(4, Object(StyledText::create(*context, "4.3")).asInt())); + ASSERT_TRUE(IsEqual(Color(Color::RED), Object(StyledText::create(*context, "#ff0000")).asColor(*context))); + + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 10), + Object(StyledText::create(*context, "10dp")).asDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(), + Object(StyledText::create(*context, "auto")).asDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 10), + Object(StyledText::create(*context, "10%")).asDimension(*context))); + + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 5), + Object(StyledText::create(*context, "5dp")).asAbsoluteDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 0), + Object(StyledText::create(*context, "auto")).asAbsoluteDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 0), + Object(StyledText::create(*context, "10%")).asAbsoluteDimension(*context))); + + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 5), + Object(StyledText::create(*context, "5dp")).asNonAutoDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 0), + Object(StyledText::create(*context, "auto")).asNonAutoDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 10), + Object(StyledText::create(*context, "10%")).asNonAutoDimension(*context))); + + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Absolute, 5), + Object(StyledText::create(*context, "5dp")).asNonAutoRelativeDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 0), + Object(StyledText::create(*context, "auto")).asNonAutoRelativeDimension(*context))); + ASSERT_TRUE(IsEqual(Dimension(DimensionType::Relative, 10), + Object(StyledText::create(*context, "10%")).asNonAutoRelativeDimension(*context))); ASSERT_TRUE(StyledText::create(*context, "").empty()); ASSERT_FALSE(StyledText::create(*context, "

").empty()); - ASSERT_EQ(0, StyledText::create(*context, "").size()); - ASSERT_EQ(9, StyledText::create(*context, "

").size()); + ASSERT_EQ(0, Object(StyledText::create(*context, "")).size()); + ASSERT_EQ(9, Object(StyledText::create(*context, "

")).size()); } TEST_F(StyledTextTest, NotStyled) @@ -113,21 +151,40 @@ TEST_F(StyledTextTest, NotStyled) TEST_F(StyledTextTest, Simple) { createAndVerifyStyledText(u8"Simple styled text.", u8"Simple styled text.", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 7, 13); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Simple ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "styled")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " text.")); } TEST_F(StyledTextTest, Multiple) { createAndVerifyStyledText(u8"Simple somewhat styled text.", u8"Simple somewhat styled text.", 2); - verifySpan(0, StyledText::kSpanTypeItalic, 7, 15); - verifySpan(1, StyledText::kSpanTypeUnderline, 16, 22); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Simple ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "somewhat")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, "styled")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, " text.")); } TEST_F(StyledTextTest, LineBreak) { createAndVerifyStyledText(u8"Line
break
text.", u8"Linebreaktext.", 2); - verifySpan(0, StyledText::kSpanTypeLineBreak, 4, 4); - verifySpan(1, StyledText::kSpanTypeLineBreak, 9, 9); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Line")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator, "break")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator, "text.")); } TEST_F(StyledTextTest, EscapeCharacters) @@ -139,29 +196,52 @@ TEST_F(StyledTextTest, Wchar) { createAndVerifyStyledText(u8"\u524D\u9031\n\u672B\u6BD434\u518680\u92AD", u8"\u524D\u9031 \u672B\u6BD434\u518680\u92AD", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 4, 8); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, u8"\u524D\u9031 \u672B")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, u8"\u6BD434\u5186")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, u8"80\u92AD")); } TEST_F(StyledTextTest, Cyrillics) { // String just means "Russian language" createAndVerifyStyledText(u8"\u0440\0443\u0441\u043a\u0438\u0439 \u044F\u0437\u044B\u043a", u8"\u0440\0443\u0441\u043a\u0438\u0439 \u044F\u0437\u044B\u043a", 1); - verifySpan(0, StyledText::kSpanTypeStrong, 8, 12); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, u8"\u0440\0443\u0441\u043a\u0438\u0439 ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, u8"\u044F\u0437\u044B\u043a")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); } TEST_F(StyledTextTest, UnclosedTag) { createAndVerifyStyledText(u8"Unclosed tag.", u8"Unclosed tag.", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 8, 13); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Unclosed")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " tag.")); } TEST_F(StyledTextTest, UnclosedTagIntersect) { createAndVerifyStyledText(u8"This is bold text, this is bold-italic and plain.", u8"This is bold text, this is bold-italic and plain.", 3); - verifySpan(0, StyledText::kSpanTypeStrong, 7, 38); - verifySpan(1, StyledText::kSpanTypeItalic, 18, 38); - verifySpan(2, StyledText::kSpanTypeItalic, 38, 43); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "This is")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, " bold text,")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " this is bold-italic")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " and ")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "plain.")); } TEST_F(StyledTextTest, UnopenedTag) @@ -173,34 +253,67 @@ TEST_F(StyledTextTest, UnopenedTagComplex) { createAndVerifyStyledText(u8"Hello, I'm a turtle who likes lettuce.", u8"Hello, I'm a turtle who likes lettuce.", 2); - verifySpan(0, StyledText::kSpanTypeStrong, 0, 38); - verifySpan(1, StyledText::kSpanTypeItalic, 7, 38); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "Hello, ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "I'm a turtle who likes lettuce.")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); } TEST_F(StyledTextTest, UnopenedTagNested) { createAndVerifyStyledText(u8"Unopened tag.", u8"Unopened tag.", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 0, 8); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "Unopened")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " tag.")); } TEST_F(StyledTextTest, UnopenedTagDeepNested) { createAndVerifyStyledText(u8"Unopened tag.", u8"Unopened tag.", 2); - verifySpan(0, StyledText::kSpanTypeItalic, 0, 8); - verifySpan(1, StyledText::kSpanTypeItalic, 0, 8); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "Unopened")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " tag.")); } TEST_F(StyledTextTest, UnclosedTagComplex) { createAndVerifyStyledText(u8"Multiple very unclosed tags. And few unclosed at the end.", u8"Multiple very unclosed tags. And few unclosed at the end.", 7); - verifySpan(0, StyledText::kSpanTypeStrong, 9, 27); - verifySpan(1, StyledText::kSpanTypeUnderline, 14, 27); - verifySpan(2, StyledText::kSpanTypeItalic, 22, 27); - verifySpan(3, StyledText::kSpanTypeItalic, 27, 57); - verifySpan(4, StyledText::kSpanTypeUnderline, 27, 57); - verifySpan(5, StyledText::kSpanTypeMonospace, 37, 57); - verifySpan(6, StyledText::kSpanTypeStrike, 46, 57); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Multiple ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "very ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, "unclosed")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " tags")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, ". And few ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifyText(iterator, "unclosed ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrike)); + ASSERT_TRUE(verifyText(iterator, "at the end.")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrike)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); } @@ -208,24 +321,50 @@ TEST_F(StyledTextTest, UnclosedSameTypeTagNested) { createAndVerifyStyledText(u8"Multiple nested very unclosed tags.", u8"Multiple nested very unclosed tags.", 4); - verifySpan(0, StyledText::kSpanTypeStrong, 16, 35); - verifySpan(1, StyledText::kSpanTypeStrong, 16, 35); - verifySpan(2, StyledText::kSpanTypeStrong, 16, 20); - verifySpan(3, StyledText::kSpanTypeStrong, 16, 20); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Multiple nested ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "very")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, " unclosed tags.")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); } TEST_F(StyledTextTest, UnclosedSameTypeTagNestedComplex) { createAndVerifyStyledText(u8"Multiple very unclosed tags. And few unclosed at the end.", u8"Multiple very unclosed tags. And few unclosed at the end.", 8); - verifySpan(0, StyledText::kSpanTypeStrong, 9, 57); - verifySpan(1, StyledText::kSpanTypeStrong, 9, 27); - verifySpan(2, StyledText::kSpanTypeUnderline, 14, 27); - verifySpan(3, StyledText::kSpanTypeItalic, 22, 27); - verifySpan(4, StyledText::kSpanTypeItalic, 27, 57); - verifySpan(5, StyledText::kSpanTypeUnderline, 27, 57); - verifySpan(6, StyledText::kSpanTypeMonospace, 37, 57); - verifySpan(7, StyledText::kSpanTypeStrike, 46, 57); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Multiple ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "very ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, "unclosed")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " tags")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, ". And few ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifyText(iterator, "unclosed ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrike)); + ASSERT_TRUE(verifyText(iterator, "at the end.")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrike)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); } TEST_F(StyledTextTest, UnsupportedTag) @@ -237,17 +376,37 @@ TEST_F(StyledTextTest, UnsupportedTag) TEST_F(StyledTextTest, SingleChildStyle) { createAndVerifyStyledText(u8"Text with one child.", u8"Text with one child.", 2); - verifySpan(0, StyledText::kSpanTypeItalic, 5, 19); - verifySpan(1, StyledText::kSpanTypeStrong, 10, 13); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Text ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "with ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "one")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, " child")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, ".")); } TEST_F(StyledTextTest, SeveralChildStyles) { createAndVerifyStyledText(u8"Text with child and another child.", u8"Text with child and another child.", 3); - verifySpan(0, StyledText::kSpanTypeItalic, 5, 33); - verifySpan(1, StyledText::kSpanTypeStrong, 10, 15); - verifySpan(2, StyledText::kSpanTypeUnderline, 28, 33); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Text ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "with ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "child")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, " and another ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, "child")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, ".")); } TEST_F(StyledTextTest, CollapseSpaces) @@ -255,9 +414,7 @@ TEST_F(StyledTextTest, CollapseSpaces) createAndVerifyStyledText(u8"Text value.", u8"Text value.", 0); createAndVerifyStyledText(u8" foo ", u8"foo", 0); createAndVerifyStyledText(u8" and
this ", u8"andthis", 1); - verifySpan(0, StyledText::kSpanTypeLineBreak, 3, 3); createAndVerifyStyledText(u8" this is a
test of whitespace ", u8"this is atest of whitespace", 1); - verifySpan(0, StyledText::kSpanTypeLineBreak, 9, 9); } TEST_F(StyledTextTest, Complex) @@ -267,18 +424,51 @@ TEST_F(StyledTextTest, Complex) "your
\r office\n a holiday; \u524D\u9031\u672B\u6BD434\u518680\u92ad look. ", u8"Since you are not going on a? holiday this year! Boss,I thought I should give your office " "a holiday; \u524D\u9031\u672B\u6BD434\u518680\u92ad look.", 12); - verifySpan(0, StyledText::kSpanTypeItalic, 6, 9); - verifySpan(1, StyledText::kSpanTypeUnderline, 24, 54); - verifySpan(2, StyledText::kSpanTypeStrong, 30, 42); - verifySpan(3, StyledText::kSpanTypeItalic, 38, 42); - verifySpan(4, StyledText::kSpanTypeStrong, 54, 63); - verifySpan(5, StyledText::kSpanTypeLineBreak, 54, 54); - verifySpan(6, StyledText::kSpanTypeStrike, 66, 82); - verifySpan(7, StyledText::kSpanTypeMonospace, 66, 72); - verifySpan(8, StyledText::kSpanTypeSuperscript, 73, 77); - verifySpan(9, StyledText::kSpanTypeSubscript, 83, 89); - verifySpan(10, StyledText::kSpanTypeMonospace, 92, 100); - verifySpan(11, StyledText::kSpanTypeItalic, 104, 117); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Since ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "you")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " are not going ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifyText(iterator, "on a? ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "holiday ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "this")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, " year! Boss,")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeUnderline)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator, "I thought")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, " I ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrike)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifyText(iterator, "should")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifyText(iterator, " ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeSuperscript)); + ASSERT_TRUE(verifyText(iterator, "give")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeSuperscript)); + ASSERT_TRUE(verifyText(iterator, " your")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrike)); + ASSERT_TRUE(verifyText(iterator, " ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeSubscript)); + ASSERT_TRUE(verifyText(iterator, "office")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeSubscript)); + ASSERT_TRUE(verifyText(iterator, " a ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifyText(iterator, "holiday;")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeMonospace)); + ASSERT_TRUE(verifyText(iterator, " 前週末")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "比34円80銭 look.")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); } TEST_F(StyledTextTest, WithMarkdownCharacters) @@ -299,13 +489,24 @@ TEST_F(StyledTextTest, IncompleteEntities) { TEST_F(StyledTextTest, LongSpecialEntity) { createAndVerifyStyledText(u8"go → right", u8"go \u2192 right", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 5, 10); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, u8"go \u2192 ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "right")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); } TEST_F(StyledTextTest, UppercaseTags) { createAndVerifyStyledText(u8"Simple styled text.", u8"Simple styled text.", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 7, 13); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Simple ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "styled")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " text.")); } TEST_F(StyledTextTest, UnneededSpansSimple) @@ -313,20 +514,33 @@ TEST_F(StyledTextTest, UnneededSpansSimple) createAndVerifyStyledText(u8"", u8"", 0); createAndVerifyStyledText(u8"", u8"", 0); createAndVerifyStyledText(u8"
", u8"", 1); - verifySpan(0, StyledText::kSpanTypeLineBreak, 0, 0); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeLineBreak)); } TEST_F(StyledTextTest, UnneededSpansCollapse) { createAndVerifyStyledText(u8"spancalypse", u8"spancalypse", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 0, 11); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "spancalypse")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); } TEST_F(StyledTextTest, UnneededSpansCollapseComplex) { createAndVerifyStyledText(u8"spancalypse", u8"spancalypse", 3); - verifySpan(0, StyledText::kSpanTypeStrong, 0, 6); - verifySpan(1, StyledText::kSpanTypeItalic, 0, 6); - verifySpan(2, StyledText::kSpanTypeItalic, 6, 11); + + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "spanca")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, "lypse")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); } TEST_F(StyledTextTest, TagAttribute) @@ -337,28 +551,64 @@ TEST_F(StyledTextTest, TagAttribute) // single attr createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 6, 10); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeItalic)); + ASSERT_EQ(0, iterator.getSpanAttributes().size()); + ASSERT_TRUE(verifyText(iterator, "this")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator, " is an attr")); // multiple attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 6, 10); + auto iterator2 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator2, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator2, StyledText::kSpanTypeItalic)); + ASSERT_EQ(0, iterator2.getSpanAttributes().size()); + ASSERT_TRUE(verifyText(iterator2, "this")); + ASSERT_TRUE(verifySpanEnd(iterator2, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator2, " is an attr")); // special allowed characters for attribute name and value createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 6, 10); + auto iterator3 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator3, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator3, StyledText::kSpanTypeItalic)); + ASSERT_EQ(0, iterator.getSpanAttributes().size()); + ASSERT_TRUE(verifyText(iterator3, "this")); + ASSERT_TRUE(verifySpanEnd(iterator3, StyledText::kSpanTypeItalic)); + ASSERT_TRUE(verifyText(iterator3, " is an attr")); // special allowed characters for break tag's attribute name and value createAndVerifyStyledText(u8"Hello
this
is an attr", u8"Hellothis is an attr", 1); - verifySpan(0, StyledText::kSpanTypeLineBreak, 5, 5); + auto iterator4 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator4, "Hello")); + ASSERT_TRUE(verifySpanStart(iterator4, StyledText::kSpanTypeLineBreak)); + ASSERT_EQ(0, iterator4.getSpanAttributes().size()); + ASSERT_TRUE(verifySpanEnd(iterator4, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator4, "this is an attr")); // using special start character and all three types of entity references createAndVerifyStyledText(u8"Hello
this is an attr", u8"Hellothis is an attr", 2); - verifySpan(0, StyledText::kSpanTypeLineBreak, 5, 5); - verifySpan(1, StyledText::kSpanTypeItalic, 16, 20); + auto iterator5 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator5, "Hello")); + ASSERT_TRUE(verifySpanStart(iterator5, StyledText::kSpanTypeLineBreak)); + ASSERT_EQ(0, iterator5.getSpanAttributes().size()); + ASSERT_TRUE(verifySpanEnd(iterator5, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator5, "this is an ")); + ASSERT_TRUE(verifySpanStart(iterator5, StyledText::kSpanTypeItalic)); + ASSERT_EQ(0, iterator5.getSpanAttributes().size()); + ASSERT_TRUE(verifyText(iterator5, "attr")); + ASSERT_TRUE(verifySpanEnd(iterator5, StyledText::kSpanTypeItalic)); // Checking for dec entity collisions createAndVerifyStyledText(u8"go → right", u8"go \u2192 right", 1); - verifySpan(0, StyledText::kSpanTypeItalic, 5, 10); + auto iterator6 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator6, u8"go \u2192 ")); + ASSERT_TRUE(verifySpanStart(iterator6, StyledText::kSpanTypeItalic)); + ASSERT_EQ(0, iterator6.getSpanAttributes().size()); + ASSERT_TRUE(verifyText(iterator6, "right")); + ASSERT_TRUE(verifySpanEnd(iterator6, StyledText::kSpanTypeItalic)); createAndVerifyStyledText(u8"hello world", u8"helloworld", 1); @@ -370,72 +620,145 @@ TEST_F(StyledTextTest, TagAttribute) // cat literally walks across the keyboard createAndVerifyStyledText(u8"hello
world", u8"helloworld", 1); + createAndVerifyStyledText(u8"helloworld", u8"helloworld", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 5, 10, 1); - verifyColorAttribute(0, 0, "#ff0000ff"); + auto iterator7 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator7, "hello")); + ASSERT_TRUE(verifySpanStart(iterator7, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator7, "world")); + ASSERT_TRUE(verifySpanEnd(iterator7, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator7, 0, "#ff0000ff")); // span tag with attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyColorAttribute(0, 0, "#ff0000ff"); + auto iterator8 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator8, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator8, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator8, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator8, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator8, 0, "#ff0000ff")); + createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyFontSizeAttribute(0, 0, "48dp"); + auto iterator9 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator9, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator9, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator9, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator9, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyFontSizeAttribute(iterator9, 0, "48dp")); // span tag with attribute name with resource binding createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyFontSizeAttribute(0, 0, "10dp"); + auto iterator10 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator10, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator10, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator10, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator10, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyFontSizeAttribute(iterator10, 0, "10dp")); // span tag with multiple attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 2); - verifyColorAttribute(0, 0, "#ff0000ff"); - verifyFontSizeAttribute(0, 1, "48dp"); + auto iterator11 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator11, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator11, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator11, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator11, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator11, 0, "#ff0000ff")); + ASSERT_TRUE(verifyFontSizeAttribute(iterator11, 1, "48dp")); // span tag with different kinds of color attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyColorAttribute(0, 0, "#eeddbbff"); + auto iterator12 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator12, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator12, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator12, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator12, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator12, 0, "#eeddbbff")); + createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyColorAttribute(0, 0, "#0000ff7f"); + auto iterator13 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator13, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator13, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator13, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator13, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator13, 0, "#0000ff7f")); + createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyColorAttribute(0, 0, "#0080003f"); + auto iterator14 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator14, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator14, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator14, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator14, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator14, 0, "#0080003f")); + createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyColorAttribute(0, 0, "#ff0000ff"); + auto iterator15 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator15, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator15, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator15, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator15, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator15, 0, "#ff0000ff")); + createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 1); - verifyColorAttribute(0, 0, "#80808040"); + auto iterator16 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator16, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator16, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator16, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator16, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator16, 0, "#80808040")); // span tag with inherit attribute value createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21); + auto iterator17 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator17, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator17, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator17, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator17, StyledText::kSpanTypeSpan)); // span tag with same attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21, 2); - verifyColorAttribute(0, 0, "#0000ffff"); - verifyFontSizeAttribute(0, 1, "50dp"); + auto iterator18 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator18, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator18, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator18, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator18, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyColorAttribute(iterator18, 0, "#0000ffff")); + ASSERT_TRUE(verifyFontSizeAttribute(iterator18, 1, "50dp")); // span tag without attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21); + auto iterator19 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator19, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator19, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator19, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator19, StyledText::kSpanTypeSpan)); // span tag with non-supported attributes createAndVerifyStyledText(u8"Hello this is an attr", u8"Hello this is an attr", 1); - verifySpan(0, StyledText::kSpanTypeSpan, 6, 21); + auto iterator20 = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator20, "Hello ")); + ASSERT_TRUE(verifySpanStart(iterator20, StyledText::kSpanTypeSpan)); + ASSERT_TRUE(verifyText(iterator20, "this is an attr")); + ASSERT_TRUE(verifySpanEnd(iterator20, StyledText::kSpanTypeSpan)); } TEST_F(StyledTextTest, NobrSimple) { createAndVerifyStyledText(u8"He screamed \"Run fasterthetiger isrightbehindyou!!!\"", u8"He screamed \"Run fasterthetiger isrightbehindyou!!!\"", 3); - verifySpan(0, StyledText::kSpanTypeNoBreak, 17, 23); - verifySpan(1, StyledText::kSpanTypeNoBreak, 26, 34); - verifySpan(2, StyledText::kSpanTypeNoBreak, 45, 51); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, u8"He screamed \"Run ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "faster")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "the")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "tiger is")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "rightbehind")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "you!!!")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "\"")); } TEST_F(StyledTextTest, NobrMerge) @@ -443,8 +766,14 @@ TEST_F(StyledTextTest, NobrMerge) // Only some tags can be merged. For example "text" can become "text" createAndVerifyStyledText(u8"This should not merge into one big tag", u8"This should not merge into one big tag", 2); - verifySpan(0, StyledText::kSpanTypeNoBreak, 0, 15); - verifySpan(1, StyledText::kSpanTypeNoBreak, 15, 21); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "This should not")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, " merge")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, " into one big tag")); } TEST_F(StyledTextTest, NobrNested) @@ -452,13 +781,31 @@ TEST_F(StyledTextTest, NobrNested) createAndVerifyStyledText(u8"He screamed \"Run fasterthetiger is" "rightbehindyou!!!\"", u8"He screamed \"Run fasterthetiger isrightbehindyou!!!\"", 7); - verifySpan(0, StyledText::kSpanTypeNoBreak, 17, 23); - verifySpan(1, StyledText::kSpanTypeNoBreak, 17, 23); - verifySpan(2, StyledText::kSpanTypeNoBreak, 17, 23); - verifySpan(3, StyledText::kSpanTypeNoBreak, 26, 34); - verifySpan(4, StyledText::kSpanTypeNoBreak, 29, 32); - verifySpan(5, StyledText::kSpanTypeNoBreak, 45, 51); - verifySpan(6, StyledText::kSpanTypeNoBreak, 45, 49); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, "He screamed \"Run ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "faster")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "the")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "tig")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "er ")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "is")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "rightbehind")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "you!")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "!!")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "\"")); } TEST_F(StyledTextTest, NobrComplex) @@ -466,16 +813,39 @@ TEST_F(StyledTextTest, NobrComplex) createAndVerifyStyledText(u8"He screamed \"Run
fas
ter
thetiger is" "rightbehindyou!!!\"", u8"He screamed \"Run fasterthetiger isrightbehindyou!!!\"", 10); - verifySpan(0, StyledText::kSpanTypeNoBreak, 17, 23); - verifySpan(1, StyledText::kSpanTypeNoBreak, 17, 23); - verifySpan(2, StyledText::kSpanTypeLineBreak, 17, 17); - verifySpan(3, StyledText::kSpanTypeLineBreak, 20, 20); - verifySpan(4, StyledText::kSpanTypeStrong, 23, 33); - verifySpan(5, StyledText::kSpanTypeNoBreak, 26, 33); - verifySpan(6, StyledText::kSpanTypeNoBreak, 29, 32); - verifySpan(7, StyledText::kSpanTypeNoBreak, 33, 34); - verifySpan(8, StyledText::kSpanTypeNoBreak, 45, 51); - verifySpan(9, StyledText::kSpanTypeNoBreak, 45, 49); + auto iterator = StyledText::Iterator(styledText.getStyledText()); + ASSERT_TRUE(verifyText(iterator, u8"He screamed \"Run ")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator, "fas")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeLineBreak)); + ASSERT_TRUE(verifyText(iterator, "ter")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifyText(iterator, "the")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "tig")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "er ")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "i")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeStrong)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "s")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "rightbehind")); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifySpanStart(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "you!")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "!!")); + ASSERT_TRUE(verifySpanEnd(iterator, StyledText::kSpanTypeNoBreak)); + ASSERT_TRUE(verifyText(iterator, "\"")); } TEST_F(StyledTextTest, StyledTextIteratorBasic) @@ -547,50 +917,95 @@ TEST_F(StyledTextTest, StyledTextIteratorEmpty) TEST_F(StyledTextTest, StyledTextSpanEquality) { - auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\"").getStyledText(); - auto st2 = StyledText::create(*context, u8"He screamed \"Runslowerthepuppywantstolickyou\"").getStyledText(); - auto st1Spans = st1.getSpans(); - auto st2Spans = st2.getSpans(); - - EXPECT_EQ(st1Spans.size(), st2Spans.size()); - EXPECT_FALSE(st1Spans.empty()); - EXPECT_TRUE(st1Spans[0] == st2Spans[0]); + auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\""); + auto st2 = StyledText::create(*context, u8"He screamed \"Runslowerthepuppywantstolickyou\""); + auto st1Spans = StyledText::Iterator(st1); + auto st2Spans = StyledText::Iterator(st2); + + EXPECT_EQ(st1Spans.spanCount(), st2Spans.spanCount()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); + st1Spans.next(); + st2Spans.next(); + EXPECT_NE(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); } TEST_F(StyledTextTest, StyledTextSpanWithAttributesEquality) { - auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\"").getStyledText(); - auto st2 = StyledText::create(*context, u8"He screamed \"Runslowerthepuppywantstolickyou\"").getStyledText(); - auto st1Spans = st1.getSpans(); - auto st2Spans = st2.getSpans(); + auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\""); + auto st2 = StyledText::create(*context, u8"He screamed \"Runslowerthepuppywantstolickyou\""); + auto st1Spans = StyledText::Iterator(st1); + auto st2Spans = StyledText::Iterator(st2); + + EXPECT_EQ(st1Spans.spanCount(), st2Spans.spanCount()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); + EXPECT_EQ(st1Spans.getSpanAttributes(), st2Spans.getSpanAttributes()); + st1Spans.next(); + st2Spans.next(); + EXPECT_NE(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); - EXPECT_EQ(st1Spans.size(), st2Spans.size()); - EXPECT_FALSE(st1Spans.empty()); - EXPECT_TRUE(st1Spans[0] == st2Spans[0]); } TEST_F(StyledTextTest, StyledTextSpanInequality) { - auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\"").getStyledText(); - auto st2 = StyledText::create(*context, u8"He screamed \"Runslowertheturtleneedstolickyou\"").getStyledText(); - auto st1Spans = st1.getSpans(); - auto st2Spans = st2.getSpans(); + auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\""); + auto st2 = StyledText::create(*context, u8"He screamed \"Runslowertheturtleneedstolickyou\""); + auto st1Spans = StyledText::Iterator(st1); + auto st2Spans = StyledText::Iterator(st2); + + EXPECT_EQ(st1Spans.spanCount(), st2Spans.spanCount()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); + st1Spans.next(); + st2Spans.next(); + EXPECT_NE(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); - EXPECT_EQ(st1Spans.size(), st2Spans.size()); - EXPECT_FALSE(st1Spans.empty()); - EXPECT_TRUE(st1Spans[0] != st2Spans[0]); } TEST_F(StyledTextTest, StyledTextSpanWithAttributesInequality) { - auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\"").getStyledText(); - auto st2 = StyledText::create(*context, u8"He screamed \"Runslowerthepuppywantstolickyou\"").getStyledText(); - auto st1Spans = st1.getSpans(); - auto st2Spans = st2.getSpans(); + auto st1 = StyledText::create(*context, u8"He screamed \"Runfasterthetigerisbehindyou!!!\""); + auto st2 = StyledText::create(*context, u8"He screamed \"Runslowerthepuppywantstolickyou\""); + auto st1Spans = StyledText::Iterator(st1); + auto st2Spans = StyledText::Iterator(st2); + + EXPECT_EQ(st1Spans.spanCount(), st2Spans.spanCount()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); + EXPECT_NE(st1Spans.getSpanAttributes(), st2Spans.getSpanAttributes()); + st1Spans.next(); + st2Spans.next(); + EXPECT_NE(st1Spans.getString(), st2Spans.getString()); + st1Spans.next(); + st2Spans.next(); + EXPECT_EQ(st1Spans.getSpanType(), st2Spans.getSpanType()); - EXPECT_EQ(st1Spans.size(), st2Spans.size()); - EXPECT_FALSE(st1Spans.empty()); - EXPECT_TRUE(st1Spans[0] != st2Spans[0]); } TEST_F(StyledTextTest, StyledTextTruthy) diff --git a/unit/primitives/unittest_transform.cpp b/unit/primitives/unittest_transform.cpp index 5210606..20143d0 100644 --- a/unit/primitives/unittest_transform.cpp +++ b/unit/primitives/unittest_transform.cpp @@ -17,6 +17,7 @@ #include "../testeventloop.h" +#include #include using namespace apl; @@ -440,6 +441,38 @@ TEST_F(TransformTest, NumberParsing) } } +TEST_F(TransformTest, NumberParsingIgnoresCLocale) +{ + std::string previousLocale = std::setlocale(LC_NUMERIC, nullptr); + std::setlocale(LC_NUMERIC, "fr_FR.UTF-8"); + + // Move these tests inside the function because they call Transform2D::scale when initialized + std::map const NUMBER_PARSE = { + {"scale(2)", Transform2D::scale(2)}, + {"scale(2.5)", Transform2D::scale(2.5)}, + {"scale(00002)", Transform2D::scale(2)}, + {"scale(.5)", Transform2D::scale(0.5)}, + {"scale(.500000000)", Transform2D::scale(0.5)}, + {"scale(+2)", Transform2D::scale(2)}, + {"scale(-2)", Transform2D::scale(-2)}, + {"scale(2e1)", Transform2D::scale(20)}, + {"scale(2E1)", Transform2D::scale(20)}, + {"scale(2e+2)", Transform2D::scale(200)}, + {"scale(10e-1)", Transform2D::scale(1)}, + {"scale(5e0)", Transform2D::scale(5)}, + {"scale(1+2)", Transform2D::scale(1,2)}, // Note that "1+2" is valid + {"scale(1-2)", Transform2D::scale(1,-2)}, + }; + + for (const auto& test : NUMBER_PARSE) { + auto transform = Transform2D::parse(session, test.first); + EXPECT_EQ(test.second, transform) << "Test case: " << test.first; + EXPECT_FALSE(session->checkAndClear()); + } + + std::setlocale(LC_NUMERIC, previousLocale.c_str()); +} + static const std::vector EMPTY_TRANSFORMS = { "", " ", diff --git a/unit/primitives/unittest_unicode.cpp b/unit/primitives/unittest_unicode.cpp index 61b8ce6..c52ab5e 100644 --- a/unit/primitives/unittest_unicode.cpp +++ b/unit/primitives/unittest_unicode.cpp @@ -98,3 +98,80 @@ TEST_F(UnicodeTest, StringSlice) { for (const auto& m : STRING_SLICE_TESTS) ASSERT_EQ(m.expected, utf8StringSlice(m.original, m.start, m.end)); } + +struct StripTest { + std::string original; + std::string valid; + std::string expected; +}; + +static auto STRING_STRIP_TESTS = std::vector { + { u8"", u8"abcd", u8"" }, + { u8"abcd", u8"", u8"abcd"}, // Empty valid set returns everything + { u8"abcd", u8"bd", u8"bd"}, + { u8"abcd", u8"abdefghij", u8"abd"}, + { u8"\u27a3€17,23\u261ac", u8"$€0123456789,.", u8"€17,23"}, // 3-byte characters + { u8"123,631", u8"0-9", u8"123631"}, // Simple range + { u8"+--+", u8"-", u8"--"}, // Just hyphens + { u8"+*-*", u8"-+", u8"+-"}, + { u8"+*-*", u8"+-", u8"+"}, // Malformed hyphen range +}; + +TEST_F(UnicodeTest, StringStripInvalid) +{ + for (const auto& m : STRING_STRIP_TESTS) + ASSERT_EQ(m.expected, utf8StripInvalid(m.original, m.valid)); +} + +struct ValidCharacters { + std::string original; + std::string valid; + bool expected; +}; + +static auto VALID_CHARACTER_TESTS = std::vector { + { u8"This is a test with an empty string", u8"", true}, + { u8"", u8"a-z", true}, // Empty strings are generally fine + { u8"abc", u8"a-z", true}, + { u8"ABc", u8"a-z", false}, + { u8"☜", u8"a-zA-Z0-9", false}, // Out of normal range + { u8"⇐", u8"\u21d0", true}, // The actual character + { u8"⇐", u8"\u2100-\uffff", true}, // Large range + { u8"\U0001f603", u8"\u0020-\uffff", false}, // Emoji are outside of the BMP + { u8"\U0001f603", u8"\U0001f600-\U0001f64f", true}, // Emoticon ranges are fine +}; + +TEST_F(UnicodeTest, StringValidCharacters) +{ + for (const auto& m : VALID_CHARACTER_TESTS) + ASSERT_EQ(m.expected, utf8ValidCharacters(m.original, m.valid)) << m.original; +} + + +struct TrimTest { + std::string original; + std::string expected; + int trim; +}; + +static auto TRIM_TEST = std::vector { + { u8"1234567890", u8"123", 3}, + { u8"1234567890", u8"1234567890", 0}, // No trimming + { u8"", u8"", 10}, // Nothing to trim + { u8"", u8"", -1}, // Nothing to trim + { u8"1234567890", u8"1234567890", 10}, // Fits within the trim window + { u8"1234567890", u8"1234567890", 20}, // Fits within the trim window + { u8"Stühle", u8"Stü", 3}, // Two-byte character + { u8"\u27a3€17,23\u261ac", u8"\u27a3€17", 4}, // Three-byte characters + { u8"\U0001f601\U0001f602\U0001f603", u8"\U0001f601\U0001f602", 2 }, // Four-byte characters +}; + +TEST_F(UnicodeTest, TrimTest) +{ + for (const auto& m : TRIM_TEST) { + auto s = m.original; + utf8StringTrim(s, m.trim); + ASSERT_EQ(m.expected, s) << m.original << ":" << m.expected << ":" << m.trim; + } + +} \ No newline at end of file diff --git a/unit/testeventloop.h b/unit/testeventloop.h index e953fc9..b7df26b 100644 --- a/unit/testeventloop.h +++ b/unit/testeventloop.h @@ -31,10 +31,9 @@ #include "apl/apl.h" #include "apl/command/commandfactory.h" #include "apl/command/corecommand.h" +#include "apl/primitives/range.h" #include "apl/time/coretimemanager.h" - #include "apl/utils/make_unique.h" -#include "apl/utils/range.h" #include "apl/utils/searchvisitor.h" namespace apl { @@ -200,17 +199,6 @@ class MemoryWrapper : public ::testing::Test { logBridge->clear(); } - void TearDown() override - { - Test::TearDown(); - -#ifdef DEBUG_MEMORY_USE - for (const auto& m : getMemoryCounterMap()) - EXPECT_TRUE(MemoryMatch(mMemoryCounters.at(m.first), m.second())) << "for class " << m.first; -#endif - - ASSERT_FALSE(session->getCount()) << "Extra console message left behind: " << session->getLast(); - } bool CheckNoActions() { @@ -231,6 +219,19 @@ class MemoryWrapper : public ::testing::Test { return logBridge->checkAndClear(); } +protected: + void TearDown() override + { + Test::TearDown(); + +#ifdef DEBUG_MEMORY_USE + for (const auto& m : getMemoryCounterMap()) + EXPECT_TRUE(MemoryMatch(mMemoryCounters.at(m.first), m.second())) << "for class " << m.first; +#endif + + ASSERT_FALSE(session->getCount()) << "Extra console message left behind: " << session->getLast(); + } + private: #ifdef DEBUG_MEMORY_USE std::map mMemoryCounters; @@ -305,8 +306,10 @@ class DocumentWrapper : public ActionWrapper { { config = RootConfig::create(); metrics.size(1024,800).dpi(160).theme("dark"); - config->agent("Unit tests", "1.0").timeManager(loop).session(session); - config->measure(std::make_shared()); + config->set(RootProperty::kAgentName, "Unit tests") + .timeManager(loop) + .session(session) + .measure(std::make_shared()); } void loadDocument(const char *docName, const char *dataName = nullptr) { @@ -1271,8 +1274,8 @@ compareTransformApprox(const Transform2D& left, const Transform2D& right, float auto diff = std::abs(leftComponents.at(i) - rightComponents.at(i)); if (diff > delta) { return ::testing::AssertionFailure() - << "transorms is not equal: " - << " Expected: " << left.toDebugString() + << "transforms are not equal: " + << " expected: " << left.toDebugString() << ", actual: " << right.toDebugString(); } } diff --git a/unit/time/unittest_sequencer.cpp b/unit/time/unittest_sequencer.cpp index 6ae6fae..c4fc512 100644 --- a/unit/time/unittest_sequencer.cpp +++ b/unit/time/unittest_sequencer.cpp @@ -484,7 +484,7 @@ TEST_F(SequencerTest, SelectOnDifferentSequencer) TEST_F(SequencerTest, SelectOnDifferentSequencerTerminate) { - config->agent("Unit tests", "1.1"); + config->set({{RootProperty::kAgentName, "Unit tests"}, {RootProperty::kAgentVersion, "1.1"}}); loadDocument(BASIC); // Should schedule send event @@ -529,7 +529,7 @@ static const char *SELECT_OTHERWISE = R"([ TEST_F(SequencerTest, SelectOtherwise) { - config->agent("Unit tests", "1.2"); + config->set({{RootProperty::kAgentName, "Unit tests"}, {RootProperty::kAgentVersion, "1.2"}}); loadDocument(BASIC); // Should schedule send event diff --git a/unit/touch/unittest_native_gestures_scrollable.cpp b/unit/touch/unittest_native_gestures_scrollable.cpp index 9b3978f..dfa8282 100644 --- a/unit/touch/unittest_native_gestures_scrollable.cpp +++ b/unit/touch/unittest_native_gestures_scrollable.cpp @@ -461,7 +461,7 @@ static const char *LIVE_SCROLL_TEST = R"({ TEST_F(NativeGesturesScrollableTest, LiveScroll) { - config->pointerInactivityTimeout(100); + config->set(RootProperty::kPointerInactivityTimeout, 100); auto myArray = LiveArray::create(ObjectArray{"red", "green", "yellow", "blue", "purple"}); config->liveData("TestArray", myArray); loadDocument(LIVE_SCROLL_TEST); @@ -492,7 +492,7 @@ TEST_F(NativeGesturesScrollableTest, LiveScroll) TEST_F(NativeGesturesScrollableTest, LiveScrollBackwards) { - config->pointerInactivityTimeout(100); + config->set(RootProperty::kPointerInactivityTimeout, 100); auto myArray = LiveArray::create(ObjectArray{"red", "green", "yellow", "blue", "purple"}); config->liveData("TestArray", myArray); loadDocument(LIVE_SCROLL_TEST); @@ -572,6 +572,7 @@ TEST_F(NativeGesturesScrollableTest, LiveFlingBackwards) auto myArray = LiveArray::create(ObjectArray{"red", "green", "yellow", "blue", "purple"}); config->liveData("TestArray", myArray); loadDocument(LIVE_SCROLL_TEST); + advanceTime(10); ASSERT_TRUE(CheckChildrenLaidOut(component, {0, 4}, true)); // Give ability to scroll backwards @@ -613,6 +614,7 @@ TEST_F(NativeGesturesScrollableTest, LiveFlingBackwards) ASSERT_TRUE(CheckChildrenLaidOut(component, {2, 19}, true)); ASSERT_TRUE(CheckChildrenLaidOut(component, {20, 24}, false)); advanceTime(2400); + advanceTime(10); ASSERT_EQ(Point(0, 275), component->scrollPosition()); } @@ -647,7 +649,7 @@ static const char *LIVE_SCROLL_SPACED_TEST = R"({ TEST_F(NativeGesturesScrollableTest, LiveScrollBackwardsSpaced) { - config->pointerInactivityTimeout(100); + config->set(RootProperty::kPointerInactivityTimeout, 100); auto myArray = LiveArray::create(ObjectArray{"red", "green", "yellow", "blue", "purple"}); config->liveData("TestArray", myArray); loadDocument(LIVE_SCROLL_SPACED_TEST); @@ -674,8 +676,9 @@ TEST_F(NativeGesturesScrollableTest, LiveScrollBackwardsSpaced) advanceTime(100); ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,300), true)); - ASSERT_EQ(Point(0, 710), component->scrollPosition()); + ASSERT_EQ(Point(0, 590), component->scrollPosition()); + // After scrolling finished more data will be cached. advanceTime(100); ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(0,300), true)); @@ -716,6 +719,7 @@ TEST_F(NativeGesturesScrollableTest, LiveFlingBackwardsSpaced) advanceTime(100); advanceTime(100); advanceTime(2400); + advanceTime(10); ASSERT_EQ(Point(0, 475), component->scrollPosition()); } @@ -2079,7 +2083,7 @@ static const char *SCROLL_SNAP_SPACED_CENTER_TEST = R"({ TEST_F(NativeGesturesScrollableTest, ScrollSnapSpacedCenter) { - config->pointerInactivityTimeout(600); + config->set(RootProperty::kPointerInactivityTimeout, 600); loadDocument(SCROLL_SNAP_SPACED_CENTER_TEST); ASSERT_EQ(Point(), component->scrollPosition()); diff --git a/unit/touch/unittest_velocity_tracking.cpp b/unit/touch/unittest_velocity_tracking.cpp index 30b6e18..0639c58 100644 --- a/unit/touch/unittest_velocity_tracking.cpp +++ b/unit/touch/unittest_velocity_tracking.cpp @@ -105,7 +105,7 @@ TEST_F(VelocityTrackingTest, ContinuousDirectionChange) { TEST_F(VelocityTrackingTest, InteractionTimeout) { auto config = RootConfig(); - config.pointerInactivityTimeout(50); + config.set(RootProperty::kPointerInactivityTimeout, 50); auto vt = std::make_shared(config); vt->addPointerEvent(PointerEvent(kPointerDown, Point(0, 0)), 0); diff --git a/unit/utils/CMakeLists.txt b/unit/utils/CMakeLists.txt index 53aabd0..107dcb4 100644 --- a/unit/utils/CMakeLists.txt +++ b/unit/utils/CMakeLists.txt @@ -18,9 +18,9 @@ target_sources_local(unittest unittest_log.cpp unittest_lrucache.cpp unittest_path.cpp - unittest_range.cpp unittest_ringbuffer.cpp unittest_session.cpp + unittest_stringfunctions.cpp unittest_url.cpp unittest_userdata.cpp unittest_weakcache.cpp diff --git a/unit/utils/unittest_log.cpp b/unit/utils/unittest_log.cpp index 1943ceb..d3dd01a 100644 --- a/unit/utils/unittest_log.cpp +++ b/unit/utils/unittest_log.cpp @@ -16,6 +16,7 @@ #include "gtest/gtest.h" #include "apl/utils/log.h" +#include "apl/utils/session.h" using namespace apl; @@ -97,3 +98,16 @@ TEST_F(LogTest, Conditional) ASSERT_STREQ("", log().c_str()); ASSERT_EQ(0, calls()); } + +TEST_F(LogTest, WithId) +{ + SessionPtr session = makeDefaultSession(); + LOG(LogLevel::kInfo).session(session) << "Log"; + ASSERT_EQ(LogLevel::kInfo, level()); + ASSERT_EQ(std::string(session->getLogId() + ":unittest_log.cpp:TestBody : Log"), log()); + + session->setLogIdPrefix("PREFIX"); + LOG(LogLevel::kInfo).session(session) << "Log"; + ASSERT_EQ(std::string(session->getLogId() + ":unittest_log.cpp:TestBody : Log"), log()); + ASSERT_TRUE(log().rfind("PREFIX-", 0) == 0); +} \ No newline at end of file diff --git a/unit/utils/unittest_path.cpp b/unit/utils/unittest_path.cpp index a421cae..d7c30d7 100644 --- a/unit/utils/unittest_path.cpp +++ b/unit/utils/unittest_path.cpp @@ -32,32 +32,31 @@ class PathTest : public DocumentWrapper { ASSERT_FALSE(component) << kv.first; } else { ASSERT_TRUE(component) << kv.first; - ASSERT_EQ(kv.second, component->getPath()) << kv.first; + ASSERT_EQ(kv.second, component->provenance()) << kv.first; } } } }; -static const char * BASIC_USING_ITEMS = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }" - " ]" - " }" - " }" - "}"; +static const char * BASIC_USING_ITEMS = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "Text", + "id": "text1" + }, + { + "type": "Text", + "id": "text2" + } + ] + } + } +})"; TEST_F(PathTest, BasicUsingItems) { @@ -69,34 +68,33 @@ TEST_F(PathTest, BasicUsingItems) auto text2 = context->findComponentById("text2"); ASSERT_TRUE(text2); - ASSERT_STREQ("_main/mainTemplate/items", component->getPath().c_str()); - ASSERT_STREQ("_main/mainTemplate/items/items/0", text1->getPath().c_str()); - ASSERT_STREQ("_main/mainTemplate/items/items/1", text2->getPath().c_str()); + ASSERT_STREQ("_main/mainTemplate/items", component->provenance().c_str()); + ASSERT_STREQ("_main/mainTemplate/items/items/0", text1->provenance().c_str()); + ASSERT_STREQ("_main/mainTemplate/items/items/1", text2->provenance().c_str()); // Sanity check that path actually matches rapidjson Pointer implementation - ASSERT_STREQ(followPath(text1->getPath())->GetObject()["id"].GetString(), "text1"); + ASSERT_STREQ(followPath(text1->provenance())->GetObject()["id"].GetString(), "text1"); } -static const char * BASIC_USING_ITEM = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"item\": {" - " \"type\": \"Container\"," - " \"item\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }" - " ]" - " }" - " }" - "}"; +static const char * BASIC_USING_ITEM = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "item": { + "type": "Container", + "item": [ + { + "type": "Text", + "id": "text1" + }, + { + "type": "Text", + "id": "text2" + } + ] + } + } +})"; TEST_F(PathTest, BasicUsingItem) { @@ -108,36 +106,35 @@ TEST_F(PathTest, BasicUsingItem) auto text2 = context->findComponentById("text2"); ASSERT_TRUE(text2); - ASSERT_STREQ("_main/mainTemplate/item", component->getPath().c_str()); - ASSERT_STREQ("_main/mainTemplate/item/item/0", text1->getPath().c_str()); - ASSERT_STREQ("_main/mainTemplate/item/item/1", text2->getPath().c_str()); + ASSERT_STREQ("_main/mainTemplate/item", component->provenance().c_str()); + ASSERT_STREQ("_main/mainTemplate/item/item/0", text1->provenance().c_str()); + ASSERT_STREQ("_main/mainTemplate/item/item/1", text2->provenance().c_str()); } -static const char * CONDITIONAL_LIST = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"" - " }," - " {" - " \"when\": false," - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text3\"" - " }" - " ]" - " }" - " }" - "}"; +static const char * CONDITIONAL_LIST = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": { + "type": "Container", + "items": [ + { + "type": "Text", + "id": "text1" + }, + { + "when": false, + "type": "Text", + "id": "text2" + }, + { + "type": "Text", + "id": "text3" + } + ] + } + } +})"; TEST_F(PathTest, ConditionalList) { @@ -152,55 +149,54 @@ TEST_F(PathTest, ConditionalList) auto text3 = context->findComponentById("text3"); ASSERT_TRUE(text3); - ASSERT_STREQ("_main/mainTemplate/items", component->getPath().c_str()); - ASSERT_STREQ("_main/mainTemplate/items/items/0", text1->getPath().c_str()); - ASSERT_STREQ("_main/mainTemplate/items/items/2", text3->getPath().c_str()); + ASSERT_STREQ("_main/mainTemplate/items", component->provenance().c_str()); + ASSERT_STREQ("_main/mainTemplate/items/items/0", text1->provenance().c_str()); + ASSERT_STREQ("_main/mainTemplate/items/items/2", text3->provenance().c_str()); } -static const char *NESTING = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"id\": \"container1\"," - " \"when\": false" - " }," - " {" - " \"type\": \"Container\"," - " \"id\": \"container2\"," - " \"items\": [" - " {" - " \"type\": \"Frame\"," - " \"id\": \"frame1\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"when\": false" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }" - " ]" - " }," - " {" - " \"when\": false," - " \"type\": \"Text\"," - " \"id\": \"text3\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text4\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *NESTING = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": [ + { + "type": "Container", + "id": "container1", + "when": false + }, + { + "type": "Container", + "id": "container2", + "items": [ + { + "type": "Frame", + "id": "frame1", + "items": [ + { + "type": "Text", + "id": "text1", + "when": false + }, + { + "type": "Text", + "id": "text2" + } + ] + }, + { + "when": false, + "type": "Text", + "id": "text3" + }, + { + "type": "Text", + "id": "text4" + } + ] + } + ] + } +})"; TEST_F(PathTest, Nesting) { @@ -215,36 +211,35 @@ TEST_F(PathTest, Nesting) }); } -static const char *FIRST_LAST_ITEM = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"firstItem\": {" - " \"type\": \"Text\"," - " \"id\": \"text1\"" - " }," - " \"lastItem\": {" - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text3\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text4\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *FIRST_LAST_ITEM = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": [ + { + "type": "Container", + "firstItem": { + "type": "Text", + "id": "text1" + }, + "lastItem": { + "type": "Text", + "id": "text2" + }, + "items": [ + { + "type": "Text", + "id": "text3" + }, + { + "type": "Text", + "id": "text4" + } + ] + } + ] + } +})"; TEST_F(PathTest, FirstLast) { @@ -256,45 +251,37 @@ TEST_F(PathTest, FirstLast) }); } -static const char *DATA_SEQUENCE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"firstItem\": {" - " \"type\": \"Text\"," - " \"id\": \"text1\"" - " }," - " \"lastItem\": {" - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text3_${data}\"," - " \"when\": \"${data%2}\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text4_${data}\"" - " }" - " ]," - " \"data\": [" - " 1," - " 2," - " 3," - " 4," - " 5," - " 6" - " ]" - " }" - " ]" - " }" - "}"; +static const char *DATA_SEQUENCE = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": [ + { + "type": "Container", + "firstItem": { + "type": "Text", + "id": "text1" + }, + "lastItem": { + "type": "Text", + "id": "text2" + }, + "items": [ + { + "type": "Text", + "id": "text3_${data}", + "when": "${data%2}" + }, + { + "type": "Text", + "id": "text4_${data}" + } + ], + "data": [1,2,3,4,5,6] + } + ] + } +})"; TEST_F(PathTest, DataSequence) { @@ -316,29 +303,28 @@ TEST_F(PathTest, DataSequence) }); } -static const char *CONDITIONAL_FRAME = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Frame\"," - " \"item\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"text1\"," - " \"when\": false" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"text2\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *CONDITIONAL_FRAME = R"({ + "type": "APL", + "version": "1.1", + "mainTemplate": { + "items": [ + { + "type": "Frame", + "item": [ + { + "type": "Text", + "id": "text1", + "when": false + }, + { + "type": "Text", + "id": "text2" + } + ] + } + ] + } +})"; TEST_F(PathTest, ConditionalFrame) { @@ -348,55 +334,54 @@ TEST_F(PathTest, ConditionalFrame) }); } -static const char *LAYOUT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"layouts\": {" - " \"header\": {" - " \"description\": \"Fake header\"," - " \"parameters\": [" - " \"title\"," - " \"subtitle\"" - " ]," - " \"items\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"title\"," - " \"text\": \"${title}\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"subtitle\"," - " \"text\": \"${subtitle}\"" - " }" - " ]" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"id\": \"container1\"," - " \"items\": [" - " {" - " \"type\": \"header\"," - " \"id\": \"headerId\"," - " \"title\": \"Dogs\"," - " \"subtitle\": \"Our canine friends\"" - " }," - " {" - " \"type\": \"Image\"," - " \"id\": \"dogPicture\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *LAYOUT = R"({ + "type": "APL", + "version": "1.1", + "layouts": { + "header": { + "description": "Fake header", + "parameters": [ + "title", + "subtitle" + ], + "items": { + "type": "Container", + "items": [ + { + "type": "Text", + "id": "title", + "text": "${title}" + }, + { + "type": "Text", + "id": "subtitle", + "text": "${subtitle}" + } + ] + } + } + }, + "mainTemplate": { + "items": [ + { + "type": "Container", + "id": "container1", + "items": [ + { + "type": "header", + "id": "headerId", + "title": "Dogs", + "subtitle": "Our canine friends" + }, + { + "type": "Image", + "id": "dogPicture" + } + ] + } + ] + } +})"; TEST_F(PathTest, Layout) { @@ -409,54 +394,53 @@ TEST_F(PathTest, Layout) }); } -static const char *LAYOUT_WITH_DATA = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.3\"," - " \"layouts\": {" - " \"ListItem\": {" - " \"parameters\": [" - " \"title\"," - " \"subtitle\"" - " ]," - " \"items\": {" - " \"type\": \"Container\"," - " \"id\": \"Container${index}\"," - " \"bind\": {" - " \"name\": \"cindex\"," - " \"value\": \"${index}\"" - " }," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"text\": \"${title}\"," - " \"id\": \"Title${cindex}\"" - " }," - " {" - " \"type\": \"Text\"," - " \"test\": \"${subtitle}\"," - " \"id\": \"Subtitle${cindex}\"" - " }" - " ]" - " }" - " }" - " }," - " \"mainTemplate\": {" - " \"items\": {" - " \"type\": \"Sequence\"," - " \"id\": \"Sequence1\"," - " \"items\": {" - " \"type\": \"ListItem\"," - " \"title\": \"Title for ${data}\"," - " \"subtitle\": \"Subtitle for ${data}\"" - " }," - " \"data\": [" - " \"alpha\"," - " \"bravo\"" - " ]" - " }" - " }" - "}"; +static const char *LAYOUT_WITH_DATA = R"({ + "type": "APL", + "version": "1.3", + "layouts": { + "ListItem": { + "parameters": [ + "title", + "subtitle" + ], + "items": { + "type": "Container", + "id": "Container${index}", + "bind": { + "name": "cindex", + "value": "${index}" + }, + "items": [ + { + "type": "Text", + "text": "${title}", + "id": "Title${cindex}" + }, + { + "type": "Text", + "test": "${subtitle}", + "id": "Subtitle${cindex}" + } + ] + } + } + }, + "mainTemplate": { + "items": { + "type": "Sequence", + "id": "Sequence1", + "items": { + "type": "ListItem", + "title": "Title for ${data}", + "subtitle": "Subtitle for ${data}" + }, + "data": [ + "alpha", + "bravo" + ] + } + } +})"; TEST_F(PathTest, LayoutWithData) @@ -472,93 +456,92 @@ TEST_F(PathTest, LayoutWithData) }); } -static const char *LAYOUT_WITH_DATA_2 = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"layouts\": {" - " \"HorizontalListItem\": {" - " \"item\": [" - " {" - " \"type\": \"Container\"," - " \"id\": \"ItemContainer${index}\"," - " \"bind\": {" - " \"name\": \"cindex\"," - " \"value\": \"${index}\"" - " }," - " \"items\": [" - " {" - " \"type\": \"Image\"," - " \"id\": \"ItemImage${cindex}\"," - " \"source\": \"${data.image}\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"ItemPrimaryText${cindex}\"," - " \"text\": \"${ordinal}. ${data.primaryText}\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"ItemSecondaryText${cindex}\"," - " \"text\": \"${data.secondaryText}\"" - " }" - " ]" - " }" - " ]" - " }," - " \"ListTemplate2\": {" - " \"parameters\": [" - " \"backgroundImage\"," - " \"listData\"" - " ]," - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"id\": \"TopContainer\"," - " \"items\": [" - " {" - " \"type\": \"Image\"," - " \"id\": \"BackgroundImage\"," - " \"source\": \"${backgroundImage}\"" - " }," - " {" - " \"type\": \"Sequence\"," - " \"id\": \"MasterSequence\"," - " \"scrollDirection\": \"horizontal\"," - " \"data\": \"${listData}\"," - " \"numbered\": true," - " \"item\": [" - " {" - " \"type\": \"HorizontalListItem\"" - " }" - " ]" - " }" - " ]" - " }" - " ]" - " }" - " }," - " \"mainTemplate\": {" - " \"item\": [" - " {" - " \"type\": \"ListTemplate2\"," - " \"backgroundImage\": \"foo\"," - " \"listData\": [" - " {" - " \"image\": \"IMAGE1\"," - " \"primaryText\": \"PRIMARY1\"," - " \"secondaryText\": \"SECONDARY1\"" - " }," - " {" - " \"image\": \"IMAGE1\"," - " \"primaryText\": \"PRIMARY1\"," - " \"secondaryText\": \"SECONDARY1\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; +static const char *LAYOUT_WITH_DATA_2 = R"({ + "type": "APL", + "version": "1.1", + "layouts": { + "HorizontalListItem": { + "item": [ + { + "type": "Container", + "id": "ItemContainer${index}", + "bind": { + "name": "cindex", + "value": "${index}" + }, + "items": [ + { + "type": "Image", + "id": "ItemImage${cindex}", + "source": "${data.image}" + }, + { + "type": "Text", + "id": "ItemPrimaryText${cindex}", + "text": "${ordinal}. ${data.primaryText}" + }, + { + "type": "Text", + "id": "ItemSecondaryText${cindex}", + "text": "${data.secondaryText}" + } + ] + } + ] + }, + "ListTemplate2": { + "parameters": [ + "backgroundImage", + "listData" + ], + "items": [ + { + "type": "Container", + "id": "TopContainer", + "items": [ + { + "type": "Image", + "id": "BackgroundImage", + "source": "${backgroundImage}" + }, + { + "type": "Sequence", + "id": "MasterSequence", + "scrollDirection": "horizontal", + "data": "${listData}", + "numbered": true, + "item": [ + { + "type": "HorizontalListItem" + } + ] + } + ] + } + ] + } + }, + "mainTemplate": { + "item": [ + { + "type": "ListTemplate2", + "backgroundImage": "foo", + "listData": [ + { + "image": "IMAGE1", + "primaryText": "PRIMARY1", + "secondaryText": "SECONDARY1" + }, + { + "image": "IMAGE1", + "primaryText": "PRIMARY1", + "secondaryText": "SECONDARY1" + } + ] + } + ] + } +})"; TEST_F(PathTest, LayoutWithData2) @@ -578,80 +561,78 @@ TEST_F(PathTest, LayoutWithData2) }); } -static const char *DOCUMENT_WITH_IMPORT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"import\": [" - " {" - " \"name\": \"base\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"resources\": [" - " {" - " \"strings\": {" - " \"firstname\": \"Pebbles\"" - " }" - " }" - " ]," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Header\"," - " \"id\": \"headerId\"," - " \"title\": \"Dogs\"," - " \"subtitle\": \"Our canine friends\"" - " }," - " {" - " \"type\": \"Image\"," - " \"id\": \"dogPicture\"" - " }" - " ]" - " }" - " ]" - " }" - "}"; - -static const char *BASE_PACKAGE = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"resources\": [" - " {" - " \"strings\": {" - " \"firstname\": \"Fred\"," - " \"lastname\": \"Flintstone\"" - " }" - " }" - " ]," - " \"layouts\": {" - " \"Header\": {" - " \"parameters\": [" - " \"title\"," - " \"subtitle\"" - " ]," - " \"item\": {" - " \"type\": \"Container\"," - " \"items\": [" - " {" - " \"type\": \"Text\"," - " \"id\": \"title\"," - " \"text\": \"${title}\"" - " }," - " {" - " \"type\": \"Text\"," - " \"id\": \"subtitle\"," - " \"text\": \"${subtitle}\"" - " }" - " ]" - " }" - " }" - " }" - "}"; +static const char *DOCUMENT_WITH_IMPORT = R"({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "base", + "version": "1.2" + } + ], + "resources": [ + { + "strings": { + "firstname": "Pebbles" + } + } + ], + "mainTemplate": { + "items": [ + { + "type": "Container", + "items": [ + { + "type": "Header", + "id": "headerId", + "title": "Dogs", + "subtitle": "Our canine friends" + }, + { + "type": "Image", + "id": "dogPicture" + } + ] + } + ] + } +})"; + +static const char *BASE_PACKAGE = R"({ + "type": "APL", + "version": "1.1", + "resources": [ + { + "strings": { + "firstname": "Fred", + "lastname": "Flintstone" + } + } + ], + "layouts": { + "Header": { + "parameters": [ + "title", + "subtitle" + ], + "item": { + "type": "Container", + "items": [ + { + "type": "Text", + "id": "title", + "text": "${title}" + }, + { + "type": "Text", + "id": "subtitle", + "text": "${subtitle}" + } + ] + } + } + } +})"; TEST_F(PathTest, DocumentWithImport) { @@ -674,32 +655,31 @@ TEST_F(PathTest, DocumentWithImport) ASSERT_STREQ("base:1.2/resources/0/strings/lastname", context->provenance("@lastname").c_str()); } -static const char *HIDDEN_COMPONENT = - "{" - " \"type\": \"APL\"," - " \"version\": \"1.1\"," - " \"imports\": [" - " {" - " \"name\": \"base\"," - " \"version\": \"1.2\"" - " }" - " ]," - " \"mainTemplate\": {" - " \"items\": [" - " {" - " \"type\": \"Frame\"," - " \"bind\": {" - " \"name\": \"foo\"," - " \"value\": {" - " \"type\": \"Text\"," - " \"id\": \"hiddenText\"" - " }" - " }," - " \"items\": \"${foo}\"" - " }" - " ]" - " }" - "}"; +static const char *HIDDEN_COMPONENT = R"({ + "type": "APL", + "version": "1.1", + "imports": [ + { + "name": "base", + "version": "1.2" + } + ], + "mainTemplate": { + "items": [ + { + "type": "Frame", + "bind": { + "name": "foo", + "value": { + "type": "Text", + "id": "hiddenText" + } + }, + "items": "${foo}" + } + ] + } +})"; TEST_F(PathTest, HiddenComponent) { @@ -711,8 +691,8 @@ TEST_F(PathTest, HiddenComponent) ASSERT_EQ(kComponentTypeText, child->getType()); ASSERT_EQ(child, context->findComponentById("hiddenText")); - ASSERT_EQ(std::string("_main/mainTemplate/items/0"), component->getPath()); + ASSERT_EQ(std::string("_main/mainTemplate/items/0"), component->provenance()); // TODO: This is not a real path because of the data-bound component definition. Fix this. - ASSERT_EQ(std::string("_main/mainTemplate/items/0/items"), child->getPath()); + ASSERT_EQ(std::string("_main/mainTemplate/items/0/items"), child->provenance()); } diff --git a/unit/utils/unittest_session.cpp b/unit/utils/unittest_session.cpp index 49a1800..355f0c7 100644 --- a/unit/utils/unittest_session.cpp +++ b/unit/utils/unittest_session.cpp @@ -67,13 +67,13 @@ class ConsoleTest : public ::testing::Test { TEST_F(ConsoleTest, Stream) { - CONSOLE_S(session) << "Test1"; + CONSOLE(session) << "Test1"; ASSERT_STREQ("Test1", console().c_str()); } TEST_F(ConsoleTest, Formatted) { - CONSOLE_S(session).log("%s: %d", "Test1", 26); + CONSOLE(session).log("%s: %d", "Test1", 26); ASSERT_STREQ("Test1: 26", console().c_str()); } @@ -98,10 +98,10 @@ TEST(DefaultConsole, VerifyLog) auto session = makeDefaultSession(); - CONSOLE_S(session) << "TestVerifyLog"; + CONSOLE(session) << "TestVerifyLog"; ASSERT_EQ(1, bridge->mCount); ASSERT_EQ(LogLevel::kWarn, bridge->mLevel); - ASSERT_STREQ("unittest_session.cpp:TestBody : TestVerifyLog", bridge->mLog.c_str()); + ASSERT_STREQ((session->getLogId() + ":unittest_session.cpp:TestBody : TestVerifyLog").c_str(), bridge->mLog.c_str()); } /** @@ -115,8 +115,50 @@ TEST(DefaultConsole, UserDataInjection) auto session = makeDefaultSession(); // if this entry expands, it will crash - CONSOLE_S(session) << "cce %s"; + CONSOLE(session) << "cce %s"; ASSERT_EQ(1, bridge->mCount); ASSERT_EQ(LogLevel::kWarn, bridge->mLevel); - ASSERT_STREQ("unittest_session.cpp:TestBody : cce %s", bridge->mLog.c_str()); + ASSERT_STREQ((session->getLogId() + ":unittest_session.cpp:TestBody : cce %s").c_str(), bridge->mLog.c_str()); +} + +TEST(DefaultConsole, SameSessionId) +{ + auto session = makeDefaultSession(); + session->setLogIdPrefix("ABCDEF"); + auto idWithPrefix1 = session->getLogId(); + session->setLogIdPrefix("ABCDEF"); + auto idWithPrefix2 = session->getLogId(); + + ASSERT_EQ(idWithPrefix1, idWithPrefix2); + + ASSERT_TRUE(idWithPrefix1.rfind("ABCDEF-", 0) == 0); +} + +TEST(DefaultConsole, ShortSessionId) +{ + auto session = makeDefaultSession(); + session->setLogIdPrefix("ABC"); + ASSERT_TRUE(session->getLogId().rfind("ABC___-", 0) == 0); +} + +TEST(DefaultConsole, LongSessionId) +{ + auto session = makeDefaultSession(); + session->setLogIdPrefix("ABCDEFGH"); + ASSERT_TRUE(session->getLogId().rfind("ABCDEF-", 0) == 0); +} + +TEST(DefaultConsole, InvalidCharsSessionId) +{ + auto session = makeDefaultSession(); + session->setLogIdPrefix("A- +1k"); + ASSERT_TRUE(session->getLogId().rfind("A_____-", 0) == 0); +} + +TEST(DefaultConsole, InvalidSessionId) +{ + auto session = makeDefaultSession(); + auto currentId = session->getLogId(); + session->setLogIdPrefix("1- +1k"); + ASSERT_EQ(currentId, session->getLogId()); } \ No newline at end of file diff --git a/unit/utils/unittest_stringfunctions.cpp b/unit/utils/unittest_stringfunctions.cpp new file mode 100644 index 0000000..07d41d7 --- /dev/null +++ b/unit/utils/unittest_stringfunctions.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. + */ + +#include "../testeventloop.h" + +#include + +#include + +using namespace apl; + +namespace { + +struct ParseDoubleTestCase { + std::string input; + double expectedValue; + std::size_t expectedPosition; +}; + +} + +static const std::vector PARSE_FP_TEST_CASES = { + // Finite decimal values + {"4", 4.0, 1}, + {"-4", -4.0, 2}, + {"4.0", 4.0, 3}, + {"4.", 4.0, 2}, + {"14.5", 14.5, 4}, + {"14.5", 14.5, 4}, + {".5", 0.5, 2}, + {".5000", 0.5, 5}, + {".5000X", 0.5, 5}, + {".5F", 0.5, 2}, + {"012.45", 12.45, 6}, + {"14.5E2", 1450.0, 6}, + {"14.5E2X", 1450.0, 6}, + {"14.E2", 1400.0, 5}, + {"14.5E+2", 1450.0, 7}, + {"14.5e+2", 1450.0, 7}, + {"14.625e+10", 1.4625e+11, 10}, + {"14.56E-2", 0.1456, 8}, + {"14.56e-2", 0.1456, 8}, + + // Finite hex values + {"0XFF", 255.0, 4}, + {"0X12.", 18.0, 5}, + {" 0X12.", 18.0, 7}, + {"0X12.F", 18.9375, 6}, + {"0X12.50", 18.3125, 7}, + {"0X12.AX", 18.625, 6}, + {"0X12.AP2", 74.5, 8}, + {"0X12.Ap2", 74.5, 8}, + {"0X12.AP2X", 74.5, 8}, + {"0X12.AP+2", 74.5, 9}, + {"0X12.AP+2X", 74.5, 9}, + {"0X12.AP-2", 4.65625, 9}, + {"0X12.AP-2X", 4.65625, 9}, + {"0X1.BC70A3D70A3D7P+6", 111.11, 20}, + + // Infinite cases + {"INF", std::numeric_limits::infinity(), 3}, + {"inf", std::numeric_limits::infinity(), 3}, + {"+inf", std::numeric_limits::infinity(), 4}, + {"-INF", -std::numeric_limits::infinity(), 4}, + {"-inf", -std::numeric_limits::infinity(), 4}, + {"INFINITY", std::numeric_limits::infinity(), 8}, + {"infinity", std::numeric_limits::infinity(), 8}, + {"+INFINITY", std::numeric_limits::infinity(), 9}, + {"-INFINITY", -std::numeric_limits::infinity(), 9}, + {"-infinity", -std::numeric_limits::infinity(), 9}, + + // NaN cases + {"NAN", std::numeric_limits::quiet_NaN(), 3}, + {"NaN", std::numeric_limits::quiet_NaN(), 3}, + {"nan", std::numeric_limits::quiet_NaN(), 3}, + {"-NAN", std::numeric_limits::quiet_NaN(), 4}, + + // Whitespace + {" 4", 4.0, 3}, + {" -4", -4.0, 3}, + {" 4.5", 4.5, 6}, + {" NAN ", std::numeric_limits::quiet_NaN(), 5}, + {" +INF ", std::numeric_limits::infinity(), 6}, + {" -INF ", -std::numeric_limits::infinity(), 7}, + + // Suffixes + {" 4%", 4.0, 3}, + {" -4%", -4.0, 3}, + {" -4.5%", -4.5, 5}, + {" NANX ", std::numeric_limits::quiet_NaN(), 5}, + {" +INFX ", std::numeric_limits::infinity(), 6}, + + // Edge cases + {"", std::numeric_limits::quiet_NaN(), 0}, + {"\t", std::numeric_limits::quiet_NaN(), 1}, + {" ", std::numeric_limits::quiet_NaN(), 2}, + + // Invalid numbers + {"e2", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"e+2", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"e-2", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"p2", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"p+2", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"p-2", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"X34", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {" X34", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"+X", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"-X", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"14.56e", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"14.56e+", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"14.56e-", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"14.56eX", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"14.56e+X", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"14.56e-X", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12P", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12P+", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12P-", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12P+X", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12P-X", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12PX", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12.PX", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, + {"0X12.APX", std::numeric_limits::quiet_NaN(), std::numeric_limits::max()}, +}; + +TEST(StringFunctionsTest, ParseFloatLiteral) +{ + double max_delta = 1e-6; + for (const auto &testCase : PARSE_FP_TEST_CASES) { + std::size_t pos = std::numeric_limits::max(); + float parsed = sutil::stof(testCase.input, &pos); + + if (std::isnan(testCase.expectedValue)) { + ASSERT_TRUE(std::isnan(parsed)) << "Input: '" << testCase.input << "'"; + } else if (std::isinf(testCase.expectedValue)) { + ASSERT_EQ(testCase.expectedValue, parsed); + } else { + ASSERT_NEAR((float) testCase.expectedValue, parsed, max_delta) << "Input: '" << testCase.input << "'"; + } + + ASSERT_EQ(testCase.expectedPosition, pos) << "Input: '" << testCase.input << "'"; + } +} + +TEST(StringFunctionsTest, ParseDoubleLiteral) +{ + double max_delta = 1e-6; + for (const auto &testCase : PARSE_FP_TEST_CASES) { + std::size_t pos = std::numeric_limits::max(); + double parsed = sutil::stod(testCase.input, &pos); + + if (std::isnan(testCase.expectedValue)) { + ASSERT_TRUE(std::isnan(parsed)) << "Input: '" << testCase.input << "'"; + } else if (std::isinf(testCase.expectedValue)) { + ASSERT_EQ(testCase.expectedValue, parsed); + } else { + ASSERT_NEAR(testCase.expectedValue, parsed, max_delta) << "Input: '" << testCase.input << "'"; + } + + ASSERT_EQ(testCase.expectedPosition, pos) << "Input: '" << testCase.input << "'"; + } +} + +TEST(StringFunctionsTest, ParseLongDoubleLiteral) +{ + double max_delta = 1e-6; + for (const auto &testCase : PARSE_FP_TEST_CASES) { + std::size_t pos = std::numeric_limits::max(); + long double parsed = sutil::stold(testCase.input, &pos); + + if (std::isnan(testCase.expectedValue)) { + ASSERT_TRUE(std::isnan(parsed)) << "Input: '" << testCase.input << "'"; + } else if (std::isinf(testCase.expectedValue)) { + ASSERT_EQ(testCase.expectedValue, parsed); + } else { + ASSERT_NEAR(testCase.expectedValue, parsed, max_delta) << "Input: '" << testCase.input << "'"; + } + + ASSERT_EQ(testCase.expectedPosition, pos) << "Input: '" << testCase.input << "'"; + } +} + +TEST(StringFunctionsTest, FormatFloat) +{ + ASSERT_EQ("4.000000", sutil::to_string(4.0f)); + ASSERT_EQ("-4.000000", sutil::to_string(-4.0f)); + ASSERT_EQ("4.500000", sutil::to_string(4.5f)); + ASSERT_EQ("-4.500000", sutil::to_string(-4.5f)); + ASSERT_EQ("1004.500000", sutil::to_string(1004.5f)); + ASSERT_EQ("0.666667", sutil::to_string((float) 2.0/3)); + ASSERT_EQ("-0.500000", sutil::to_string(-0.5f)); + ASSERT_EQ("0.005000", sutil::to_string(0.005f)); + ASSERT_EQ("-0.005000", sutil::to_string(-0.005f)); + ASSERT_EQ("0.000001", sutil::to_string(1e-6f)); + ASSERT_EQ("0.000000", sutil::to_string(1e-7f)); + ASSERT_EQ("0.000000", sutil::to_string(0.0f)); + ASSERT_EQ("1.000000", sutil::to_string(0.9999997f)); + ASSERT_EQ("-1.000000", sutil::to_string(-0.9999997f)); + ASSERT_EQ("10.000000", sutil::to_string(9.9999997f)); + ASSERT_EQ("9.999999", sutil::to_string(9.9999993f)); + ASSERT_EQ("-10.000000", sutil::to_string(-9.9999997f)); + ASSERT_EQ("-9.999999", sutil::to_string(-9.9999993f)); + ASSERT_EQ("inf", sutil::to_string(std::numeric_limits::infinity())); + ASSERT_EQ("-inf", sutil::to_string(-std::numeric_limits::infinity())); + ASSERT_EQ("nan", sutil::to_string(std::numeric_limits::quiet_NaN())); +} + +TEST(StringFunctionsTest, FormatDouble) +{ + ASSERT_EQ("4.000000", sutil::to_string(4.0)); + ASSERT_EQ("-4.000000", sutil::to_string(-4.0)); + ASSERT_EQ("4.500000", sutil::to_string(4.5)); + ASSERT_EQ("-4.500000", sutil::to_string(-4.5)); + ASSERT_EQ("1004.500000", sutil::to_string(1004.5)); + ASSERT_EQ("0.666667", sutil::to_string(2.0/3)); + ASSERT_EQ("-0.500000", sutil::to_string(-0.5)); + ASSERT_EQ("0.005000", sutil::to_string(0.005)); + ASSERT_EQ("-0.005000", sutil::to_string(-0.005)); + ASSERT_EQ("0.000001", sutil::to_string(1e-6)); + ASSERT_EQ("0.000000", sutil::to_string(1e-7)); + ASSERT_EQ("0.000000", sutil::to_string(0.0)); + ASSERT_EQ("1.000000", sutil::to_string(0.9999997)); + ASSERT_EQ("-1.000000", sutil::to_string(-0.9999997)); + ASSERT_EQ("10.000000", sutil::to_string(9.9999997)); + ASSERT_EQ("9.999999", sutil::to_string(9.9999993)); + ASSERT_EQ("-10.000000", sutil::to_string(-9.9999997)); + ASSERT_EQ("-9.999999", sutil::to_string(-9.9999993)); + ASSERT_EQ("inf", sutil::to_string(std::numeric_limits::infinity())); + ASSERT_EQ("-inf", sutil::to_string(-std::numeric_limits::infinity())); + ASSERT_EQ("nan", sutil::to_string(std::numeric_limits::quiet_NaN())); +} + +TEST(StringFunctionsTest, FormatLongDouble) +{ + ASSERT_EQ("4.000000", sutil::to_string((long double) 4.0)); + ASSERT_EQ("-4.000000", sutil::to_string((long double) -4.0)); + ASSERT_EQ("4.500000", sutil::to_string((long double) 4.5)); + ASSERT_EQ("-4.500000", sutil::to_string((long double) -4.5)); + ASSERT_EQ("1004.500000", sutil::to_string((long double) 1004.5)); + ASSERT_EQ("0.666667", sutil::to_string((long double) 2.0/3)); + ASSERT_EQ("-0.500000", sutil::to_string((long double) -0.5)); + ASSERT_EQ("0.005000", sutil::to_string((long double) 0.005)); + ASSERT_EQ("-0.005000", sutil::to_string((long double) -0.005)); + ASSERT_EQ("0.000001", sutil::to_string((long double) 1e-6)); + ASSERT_EQ("0.000000", sutil::to_string((long double) 1e-7)); + ASSERT_EQ("0.000000", sutil::to_string((long double) 0.0)); + ASSERT_EQ("1.000000", sutil::to_string((long double) 0.9999997)); + ASSERT_EQ("-1.000000", sutil::to_string((long double) -0.9999997)); + ASSERT_EQ("10.000000", sutil::to_string((long double) 9.9999997)); + ASSERT_EQ("9.999999", sutil::to_string((long double) 9.9999993)); + ASSERT_EQ("-10.000000", sutil::to_string((long double) -9.9999997)); + ASSERT_EQ("-9.999999", sutil::to_string((long double) -9.9999993)); + ASSERT_EQ("inf", sutil::to_string(std::numeric_limits::infinity())); + ASSERT_EQ("-inf", sutil::to_string(-std::numeric_limits::infinity())); + ASSERT_EQ("nan", sutil::to_string(std::numeric_limits::quiet_NaN())); +} + +TEST(StringFunctionsTest, CharacterChecks) +{ + ASSERT_EQ('.', sutil::DECIMAL_POINT); + + ASSERT_TRUE(sutil::isspace(' ')); + ASSERT_TRUE(sutil::isspace('\t')); + ASSERT_TRUE(sutil::isspace('\r')); + ASSERT_TRUE(sutil::isspace('\n')); + ASSERT_TRUE(sutil::isspace('\v')); + ASSERT_TRUE(sutil::isspace('\f')); + ASSERT_FALSE(sutil::isspace('0')); + ASSERT_FALSE(sutil::isspace('\0')); + ASSERT_FALSE(sutil::isspace('A')); + + ASSERT_TRUE(sutil::isspace((unsigned char) ' ')); + ASSERT_TRUE(sutil::isspace((unsigned char) '\t')); + ASSERT_TRUE(sutil::isspace((unsigned char) '\r')); + ASSERT_TRUE(sutil::isspace((unsigned char) '\n')); + ASSERT_TRUE(sutil::isspace((unsigned char) '\v')); + ASSERT_TRUE(sutil::isspace((unsigned char) '\f')); + ASSERT_FALSE(sutil::isspace((unsigned char) '0')); + ASSERT_FALSE(sutil::isspace((unsigned char) '\0')); + ASSERT_FALSE(sutil::isspace((unsigned char) 'A')); + + ASSERT_FALSE(sutil::isalnum(' ')); + ASSERT_TRUE(sutil::isalnum('0')); + ASSERT_TRUE(sutil::isalnum('A')); + ASSERT_TRUE(sutil::isalnum('x')); + ASSERT_FALSE(sutil::isalnum('-')); + + ASSERT_FALSE(sutil::isalnum((unsigned char) ' ')); + ASSERT_TRUE(sutil::isalnum((unsigned char) '0')); + ASSERT_TRUE(sutil::isalnum((unsigned char) 'A')); + ASSERT_TRUE(sutil::isalnum((unsigned char) 'x')); + ASSERT_FALSE(sutil::isalnum((unsigned char) '-')); + + ASSERT_FALSE(sutil::isupper(' ')); + ASSERT_FALSE(sutil::isupper(',')); + ASSERT_FALSE(sutil::isupper('0')); + ASSERT_FALSE(sutil::isupper('a')); + ASSERT_TRUE(sutil::isupper('A')); + + ASSERT_FALSE(sutil::isupper((unsigned char) ' ')); + ASSERT_FALSE(sutil::isupper((unsigned char) ',')); + ASSERT_FALSE(sutil::isupper((unsigned char) '0')); + ASSERT_FALSE(sutil::isupper((unsigned char) 'a')); + ASSERT_TRUE(sutil::isupper((unsigned char) 'A')); + + ASSERT_FALSE(sutil::islower(' ')); + ASSERT_FALSE(sutil::islower(',')); + ASSERT_FALSE(sutil::islower('0')); + ASSERT_TRUE(sutil::islower('a')); + ASSERT_FALSE(sutil::islower('A')); + + ASSERT_FALSE(sutil::islower((unsigned char) ' ')); + ASSERT_FALSE(sutil::islower((unsigned char) ',')); + ASSERT_FALSE(sutil::islower((unsigned char) '0')); + ASSERT_TRUE(sutil::islower((unsigned char) 'a')); + ASSERT_FALSE(sutil::islower((unsigned char) 'A')); +} + +TEST(StringFunctionsTest, CaseConversions) +{ + ASSERT_EQ('a', sutil::tolower('a')); + ASSERT_EQ('a', sutil::tolower('A')); + ASSERT_EQ('0', sutil::tolower('0')); + ASSERT_EQ('-', sutil::tolower('-')); + + ASSERT_EQ('a', sutil::tolower((unsigned char) 'a')); + ASSERT_EQ('a', sutil::tolower((unsigned char) 'A')); + ASSERT_EQ('0', sutil::tolower((unsigned char) '0')); + ASSERT_EQ('-', sutil::tolower((unsigned char) '-')); + + ASSERT_EQ('A', sutil::toupper('a')); + ASSERT_EQ('A', sutil::toupper('A')); + ASSERT_EQ('0', sutil::toupper('0')); + ASSERT_EQ('-', sutil::toupper('-')); + + ASSERT_EQ('A', sutil::toupper((unsigned char) 'a')); + ASSERT_EQ('A', sutil::toupper((unsigned char) 'A')); + ASSERT_EQ('0', sutil::toupper((unsigned char) '0')); + ASSERT_EQ('-', sutil::toupper((unsigned char) '-')); +} + +