diff --git a/CHANGELOG.md b/CHANGELOG.md index b629372..719f9b3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,25 @@ # Changelog +## [2024.1] + +This release adds support for version 2024.1 of the APL specification. + +### Added + +- Added an “onChange” handler bound variables and moved all “bind” documentation to the Bounds Variables section +- Added Visibility Change section discussing visibility change handlers. Added handleVisibilityChange component handler +- Add the Map.keys() function to Map functions +- Add pseudoLocalization to the document to enable localization-related testing of textual components +- Add maxHeight, maxWidth, minHeight, minWidth to the onConfigChange handler +- Add -experimentalHardwareAccelerationForAndroid as an experimental flag for enabling hardware acceleration for AVG rendering in Android devices as a document setting +- Add accessibilityAdjustableRange and accessibilityAdjustableValue as new properties. Add the “increment” and “decrement” standard actions +- Add 'AudioItemId' property to audioplayer extension + +### Changed + +- Bug fixes +- Performance improvements + ## [2023.3] This release adds support for version 2023.3 of the APL specification. diff --git a/apl-dev-env.sh b/apl-dev-env.sh index c782583..8c72d3c 100644 --- a/apl-dev-env.sh +++ b/apl-dev-env.sh @@ -128,7 +128,13 @@ function apl-check-core { # Run make for the core build with -Werror function apl-test-core { # Run unit tests in the core build ( apl-switch-to-build-directory build $@ && \ - $CMAKE -DBUILD_TESTS=ON -DCOVERAGE=OFF .. && \ + $CMAKE -DBUILD_TESTS=ON \ + -DDEBUG_MEMORY_USE=ON \ + -DCOVERAGE=OFF \ + -DWERROR=ON \ + -DDISABLE_RTTI=ON \ + -DBUILD_ALEXAEXTENSIONS=ON \ + -DENABLE_SCENEGRAPH=ON .. && \ make -j$APL_BUILD_PROCS && \ aplcore/unit/unittest && \ tools/unit/tools-unittest && \ diff --git a/aplcore/include/apl/animation/easing.h b/aplcore/include/apl/animation/easing.h index 241e96d..02800f4 100644 --- a/aplcore/include/apl/animation/easing.h +++ b/aplcore/include/apl/animation/easing.h @@ -68,7 +68,7 @@ class Easing : public ObjectData { virtual bool operator==(const CoreEasing& other) const = 0; virtual ~Easing() noexcept; - class ObjectType final : public PointerHolderObjectType { + class ObjectType final : public SimplePointerHolderObjectType { public: bool isCallable() const override { return true; } diff --git a/aplcore/include/apl/common.h b/aplcore/include/apl/common.h index a7c4eb3..443ea13 100644 --- a/aplcore/include/apl/common.h +++ b/aplcore/include/apl/common.h @@ -17,6 +17,7 @@ #define _APL_COMMON_H #include +#include #include #include @@ -45,9 +46,17 @@ using apl_time_t = double; */ using apl_duration_t = double; -// Common definitions of shared pointer data structures. We define the XXXPtr variations -// here so they can be conveniently used from any source file. +/** + * Objects are used in a lot of places and are often passed by reference. + */ +class Object; +/** + * Common object types which are often used in shared pointers. Each of + * these objects is also declared as a shared pointer version of the form: + * + * using MyClassPtr = std::shared_ptr + */ class AccessibilityAction; class Action; class AudioPlayer; @@ -147,9 +156,16 @@ using StyleInstancePtr = std::shared_ptr; using TextMeasurementPtr = std::shared_ptr; using TimersPtr = std::shared_ptr; -// Convenience templates for creating sets of weak and strong pointers +/** + * Convenience templates for creating sets of weak and strong pointers + */ template using SharedPtrSet = std::set, std::owner_less>>; template using WeakPtrSet = std::set, std::owner_less>>; +template using WeakPtrMap = std::map, + T, + std::owner_less>, + std::allocator, T>> + >; } // namespace apl diff --git a/aplcore/include/apl/component/componenteventsourcewrapper.h b/aplcore/include/apl/component/componenteventsourcewrapper.h index cca6edb..d4728d8 100644 --- a/aplcore/include/apl/component/componenteventsourcewrapper.h +++ b/aplcore/include/apl/component/componenteventsourcewrapper.h @@ -41,6 +41,7 @@ class ComponentEventSourceWrapper : public ComponentEventWrapper { Object get(const std::string& key) const override; Object opt(const std::string& key, const Object& def) const override; bool has(const std::string& key) const override; + std::pair keyAt(std::size_t offset) const override; std::uint64_t size() const override; rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const override; @@ -55,6 +56,7 @@ class ComponentEventSourceWrapper : public ComponentEventWrapper { private: std::string mHandler; + std::string mSource; Object mValue; }; diff --git a/aplcore/include/apl/component/componenteventwrapper.h b/aplcore/include/apl/component/componenteventwrapper.h index b81482e..37aac74 100644 --- a/aplcore/include/apl/component/componenteventwrapper.h +++ b/aplcore/include/apl/component/componenteventwrapper.h @@ -38,10 +38,9 @@ class ComponentEventWrapper : public ObjectData { Object get(const std::string& key) const override; Object opt(const std::string& key, const Object& def) const override; bool has(const std::string& key) const override; + std::pair keyAt(std::size_t offset) const override; std::uint64_t size() const override; - const ObjectMap& getMap() const override; - ConstCoreComponentPtr getComponent() const { return mComponent.lock(); } virtual bool operator==(const ComponentEventWrapper& rhs) const = 0; diff --git a/aplcore/include/apl/component/componentproperties.h b/aplcore/include/apl/component/componentproperties.h index 49ba992..788f962 100644 --- a/aplcore/include/apl/component/componentproperties.h +++ b/aplcore/include/apl/component/componentproperties.h @@ -365,6 +365,10 @@ enum PropertyKey { kPropertyAccessibilityActions, /// An array of assigned accessibility actions kPropertyAccessibilityActionsAssigned, + /// Range configuration for a TouchableComponent with an adjustable role + kPropertyAccessibilityAdjustableRange, + /// Current value for a TouchableComponent with an adjustable role + kPropertyAccessibilityAdjustableValue, /// Component accessibility label kPropertyAccessibilityLabel, /// ImageComponent and VectorGraphicComponent alignment (see #ImageAlign, #VectorGraphicAlign) @@ -463,6 +467,8 @@ enum PropertyKey { kPropertyFontWeight, /// Component handler for tick kPropertyHandleTick, + /// Component handler for visibility changes + kPropertyHandleVisibilityChange, /// EditTextComponent highlight color behind selected text. kPropertyHighlightColor, /// EditTextComponent hint text,displayed when no text has been entered @@ -641,6 +647,8 @@ enum PropertyKey { kPropertyRole, /// ImageComponent, VideoComponent, and VectorGraphicComponent scale property (see #ImageScale, #VectorGraphicScale, #VideoScale) kPropertyScale, + /// VideoComponent screen lock + kPropertyScreenLock, /// SequenceComponent scroll animation setting kPropertyScrollAnimation, /// Scrollable preserve position by absolute scroll position diff --git a/aplcore/include/apl/component/corecomponent.h b/aplcore/include/apl/component/corecomponent.h index 66bc9ea..bc3fc57 100644 --- a/aplcore/include/apl/component/corecomponent.h +++ b/aplcore/include/apl/component/corecomponent.h @@ -370,6 +370,14 @@ class CoreComponent : public Component, */ size_t getEventPropertySize() const; + /** + * Return the event property at a given offset into the map. This method is used when iterating + * over the set of event properties. + * @param index The offset of the property in the map. + * @return A pair where the first value is the key and the second property is the event property value. + */ + std::pair getEventPropertyAt(size_t index) const; + /** * The component hierarchy signature is a unique text string that represents the type * of this component and all of the components below it in the hierarchy. This signature @@ -425,6 +433,11 @@ class CoreComponent : public Component, */ void setVisualContextDirty(); + /** + * Mark component visibility state as dirty. + */ + void setVisibilityDirty(); + /** * Convert this component into a JSON object * @param allocator RapidJSON memory allocator @@ -982,6 +995,18 @@ class CoreComponent : public Component, */ void markAccessibilityDirty(); + /** + * Register this component for visibility calculation and tracking. No-op if component has no + * VisibilityChange handler. + */ + void registerForVisibilityTrackingIfRequired(); + + /** + * Deregister this component from visibility calculation. No-op if it's not registered for such + * in a first place. + */ + void deregisterFromVisibilityTracking(); + #ifdef SCENEGRAPH /** * @return The current scene graph node. @@ -1228,6 +1253,9 @@ class CoreComponent : public Component, void processChildrenChanges(); + void addDownstreamVisibilityTarget(const CoreComponentPtr& child); + void removeDownstreamVisibilityTarget(const CoreComponentPtr& child); + static std::string toStringAction(ChildChangeAction action); protected: @@ -1258,16 +1286,17 @@ class CoreComponent : public Component, size_t index; }; - std::vector mChildrenChanges; - - Transform2D mGlobalToLocal; - bool mGlobalToLocalIsStale; - Point mStickyOffset; - bool mTextMeasurementHashStale; - bool mVisualHashStale; - std::string mTextMeasurementHash; - timeout_id mTickHandlerId = 0; - bool mAccessibilityDirty = false; + std::vector mChildrenChanges; + + Transform2D mGlobalToLocal; + bool mGlobalToLocalIsStale; + Point mStickyOffset; + bool mTextMeasurementHashStale; + bool mVisualHashStale; + std::string mTextMeasurementHash; + timeout_id mTickHandlerId = 0; + bool mAccessibilityDirty = false; + std::unique_ptr> mAffectedByVisibilityChange; }; } // namespace apl diff --git a/aplcore/include/apl/component/multichildscrollablecomponent.h b/aplcore/include/apl/component/multichildscrollablecomponent.h index 17d36c1..945cdb5 100644 --- a/aplcore/include/apl/component/multichildscrollablecomponent.h +++ b/aplcore/include/apl/component/multichildscrollablecomponent.h @@ -114,6 +114,7 @@ class MultiChildScrollableComponent : public ScrollableComponent { std::map getChildrenVisibility(float realOpacity, const Rect &visibleRect) const override; bool insertChild(const CoreComponentPtr& child, size_t index, bool useDirtyFlag) override; void removeChildAfterMarkedRemoved(const CoreComponentPtr& child, size_t index, bool useDirtyFlag) override; + void releaseSelf() override; bool getTags(rapidjson::Value& outMap, rapidjson::Document::AllocatorType& allocator) override; virtual void layoutChildIfRequired(const CoreComponentPtr& child, size_t childIdx, bool useDirtyFlag, bool first); void relayoutInPlace(bool useDirtyFlag, bool first); @@ -180,6 +181,7 @@ class MultiChildScrollableComponent : public ScrollableComponent { Point getPaddedScrollPosition(LayoutDirection layoutDirection) const; void processLayoutChangesInternal(bool useDirtyFlag, bool first, bool delayed, bool needsFullReProcess); void scheduleDelayedLayout(); + double clampScrollPositionToValidValue(double scrollPosition, LayoutDirection layoutDirection, bool isHorizontal); private: Range mIndexesSeen; diff --git a/aplcore/include/apl/component/videocomponent.h b/aplcore/include/apl/component/videocomponent.h index 138e973..8f60d21 100644 --- a/aplcore/include/apl/component/videocomponent.h +++ b/aplcore/include/apl/component/videocomponent.h @@ -19,6 +19,7 @@ #include "apl/component/mediacomponenttrait.h" #include "apl/media/mediaplayerfactory.h" #include "apl/primitives/mediastate.h" +#include "apl/utils/screenlockholder.h" namespace apl { @@ -26,7 +27,7 @@ class VideoComponent : public CoreComponent { public: static CoreComponentPtr create(const ContextPtr& context, Properties&& properties, const Path& path); VideoComponent(const ContextPtr& context, Properties&& properties, const Path& path); - virtual ~VideoComponent() noexcept; + ~VideoComponent() noexcept override; ComponentType getType() const override { return kComponentTypeVideo; } @@ -77,9 +78,11 @@ class VideoComponent : public CoreComponent { std::shared_ptr createErrorEventProperties(int errorCode); std::shared_ptr createReadyEventProperties(); void playerCallback(MediaPlayerEventType eventType, const MediaState& mediaState); + void updateScreenLock(); MediaPlayerPtr mMediaPlayer; const std::string mMediaSequencer; // Internal sequencer used for onEnd/onPause/onPlay + ScreenLockHolder mScreenLock; }; diff --git a/aplcore/include/apl/content/aplversion.h b/aplcore/include/apl/content/aplversion.h index 79470c5..a2b6c03 100644 --- a/aplcore/include/apl/content/aplversion.h +++ b/aplcore/include/apl/content/aplversion.h @@ -39,6 +39,7 @@ class APLVersion { kAPLVersion20231 = 0x1U << 12, /// Support version 2023.1 kAPLVersion20232 = 0x1U << 13, /// Support version 2023.2 kAPLVersion20233 = 0x1U << 14, /// Support version 2023.3 + kAPLVersion20241 = 0x1U << 15, /// Support version 2024.1 kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, @@ -53,9 +54,10 @@ class APLVersion { kAPLVersion20222to20231 = kAPLVersion20221to20222 | kAPLVersion20231, kAPLVersion20231to20232 = kAPLVersion20222to20231 | kAPLVersion20232, kAPLVersion20232to20233 = kAPLVersion20231to20232 | kAPLVersion20233, - kAPLVersionLatest = kAPLVersion20232to20233, /// Support the most recent engine version - kAPLVersionDefault = kAPLVersion20232to20233, /// Default value - kAPLVersionReported = kAPLVersion20233, /// Default reported version + kAPLVersion20233to20241 = kAPLVersion20232to20233 | kAPLVersion20241, + kAPLVersionLatest = kAPLVersion20233to20241, /// Support the most recent engine version + kAPLVersionDefault = kAPLVersion20233to20241, /// Default value + kAPLVersionReported = kAPLVersion20241, /// Default reported version kAPLVersionAny = 0xffffffff, /// Support any versions in the list }; diff --git a/aplcore/include/apl/content/rootconfig.h b/aplcore/include/apl/content/rootconfig.h index 4bf468d..13e5a46 100644 --- a/aplcore/include/apl/content/rootconfig.h +++ b/aplcore/include/apl/content/rootconfig.h @@ -482,9 +482,6 @@ class RootConfig { * @deprecated Extensions should be managed via ExtensionMediator */ RootConfig& registerExtensionFlags(const std::string& uri, const Object& flags) { - if (mSupportedExtensions.find(uri) == mSupportedExtensions.end()) { - registerExtension(uri); - } mExtensionFlags[uri] = flags; return *this; } diff --git a/aplcore/include/apl/datagrammar/bytecode.h b/aplcore/include/apl/datagrammar/bytecode.h index 3bfc07a..023f901 100644 --- a/aplcore/include/apl/datagrammar/bytecode.h +++ b/aplcore/include/apl/datagrammar/bytecode.h @@ -283,8 +283,14 @@ class ByteCode : public ObjectData, public std::enable_shared_from_this { + class ObjectType final : public SimplePointerHolderObjectType { public: + bool isEvaluable() const final { return true; } + + Object eval(const Object::DataHolder& dataHolder) const final { + return dataHolder.data->eval(); + } + rapidjson::Value serialize(const Object::DataHolder&, rapidjson::Document::AllocatorType& allocator) const override { return {"COMPILED BYTE CODE", allocator}; diff --git a/aplcore/include/apl/document/coredocumentcontext.h b/aplcore/include/apl/document/coredocumentcontext.h index 3ccfe20..e685557 100644 --- a/aplcore/include/apl/document/coredocumentcontext.h +++ b/aplcore/include/apl/document/coredocumentcontext.h @@ -182,7 +182,7 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr /** * @return The content */ - const ContentPtr& content() const { return mContent; } + const ContentPtr& content() const override { return mContent; } /** * Create a suitable document-level data-binding context for evaluating a document-level diff --git a/aplcore/include/apl/document/documentcontext.h b/aplcore/include/apl/document/documentcontext.h index f5afb56..9a08d17 100644 --- a/aplcore/include/apl/document/documentcontext.h +++ b/aplcore/include/apl/document/documentcontext.h @@ -60,6 +60,11 @@ class DocumentContext : public NonCopyable, public Counter { */ virtual void clearDataSourceContextDirty() = 0; + /** + * Retrieve the document's content. + */ + virtual const ContentPtr& content() const = 0; + /** * Retrieve datasource context as a JSON array object. This method also clears the * datasource context dirty flag diff --git a/aplcore/include/apl/document/documentcontextdata.h b/aplcore/include/apl/document/documentcontextdata.h index 3989835..9962028 100644 --- a/aplcore/include/apl/document/documentcontextdata.h +++ b/aplcore/include/apl/document/documentcontextdata.h @@ -38,6 +38,7 @@ class LiveDataManager; class Sequencer; class Styles; class UIDManager; +class VisibilityManager; class DataSourceConnection; using DataSourceConnectionPtr = std::shared_ptr; @@ -101,6 +102,7 @@ class DocumentContextData : public ContextData, public std::enable_shared_from_t MediaPlayerFactory& mediaPlayerFactory() const; UIDManager& uniqueIdManager() const { return *mUniqueIdManager; } DependantManager& dependantManager() const; + VisibilityManager& visibilityManager() const; const YGConfigRef& ygconfig() const; const SessionPtr& session() const override { return mSession; } diff --git a/aplcore/include/apl/engine/bindingchange.h b/aplcore/include/apl/engine/bindingchange.h new file mode 100644 index 0000000..0c516bb --- /dev/null +++ b/aplcore/include/apl/engine/bindingchange.h @@ -0,0 +1,84 @@ +/** + * 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_BINDING_CHANGE_H +#define APL_BINDING_CHANGE_H + +#include "apl/common.h" +#include "apl/primitives/object.h" + +namespace apl { + +/** + * An abstract base class for the function to be executed when a data-binding has an "onChange" + * handler. This base class takes care of ensuring that the function is not re-entrant. Subclasses + * should override the execute() method. + */ +class BindingChange { +public: + explicit BindingChange(Object&& commands) : mCommands(std::move(commands)) { + assert(!mCommands.empty()); + } + + virtual ~BindingChange() = default; + + /** + * Override this in your subclass + * @param value The new value assigned to the data-binding. + * @param previous The old value assigned to the data-binding. + */ + virtual void execute(const Object& value, const Object& previous) = 0; + + /** + * Run this when the data-binding value changes. + * @param value The new value assigned to the data binding. + * @param previous The old value assigned to the data binding. + */ + void run(const Object& value, const Object& previous) { + if (!mInExecute) { + mInExecute = true; + execute(value, previous); + mInExecute = false; + } + } + + /** + * @return The commands associated with this binding change. + */ + const Object& commands() { return mCommands; } + +private: + Object mCommands; + bool mInExecute = false; +}; + +using BindingChangePtr = std::shared_ptr; + +/** + * Process the "bind" variable in a component or graphic element and add all bound values + * to the data-binding context. + * + * @param context The data-binding context in which to evaluate the item + * @param item The item that contains a "bind" property + * @param func A function that takes an array of "onChange" commands and returns a BindingChangePtr. + * May be nullptr. + * @return An array of BindingChangePtr. + */ +extern std::vector +attachBindings(const ContextPtr& context, const Object& item, std::function func); + +} // namespace apl + +#endif // APL_BINDING_CHANGE_H diff --git a/aplcore/include/apl/engine/builder.h b/aplcore/include/apl/engine/builder.h index 7904e18..e1167ab 100644 --- a/aplcore/include/apl/engine/builder.h +++ b/aplcore/include/apl/engine/builder.h @@ -106,7 +106,6 @@ class Builder { bool fullBuild, bool useDirtyFlag); - static void attachBindings(const ContextPtr& context, const Object& item); private: MakeComponentFunc findComponentBuilderFunc(const ContextPtr& context, const std::string &type); diff --git a/aplcore/include/apl/engine/context.h b/aplcore/include/apl/engine/context.h index 449b0ad..e0b950e 100644 --- a/aplcore/include/apl/engine/context.h +++ b/aplcore/include/apl/engine/context.h @@ -54,6 +54,7 @@ class KeyboardManager; class LayoutManager; class LiveDataManager; class UIDManager; +class VisibilityManager; using DataSourceConnectionPtr = std::shared_ptr; @@ -322,10 +323,11 @@ class Context : public RecalculateTarget, * * @param key The string key name * @param value The value to store. + * @param onChange A BindingChange object to execute when the object changes. May be null. */ - void putUserWriteable(const std::string& key, const Object& value) + void putUserWriteable(const std::string& key, const Object& value, const BindingChangePtr& onChange = nullptr) { - mMap.emplace(key, ContextObject(value).userWriteable()); + mMap.emplace(key, ContextObject(value).userWriteable().onChange(onChange)); } /** @@ -640,6 +642,7 @@ class Context : public RecalculateTarget, MediaPlayerFactory& mediaPlayerFactory() const; UIDManager& uniqueIdManager() const; DependantManager& dependantManager() const; + VisibilityManager& visibilityManager() const; std::shared_ptr styles() const; diff --git a/aplcore/include/apl/engine/contextobject.h b/aplcore/include/apl/engine/contextobject.h index 44b9ea2..438a924 100644 --- a/aplcore/include/apl/engine/contextobject.h +++ b/aplcore/include/apl/engine/contextobject.h @@ -18,6 +18,7 @@ #include +#include "apl/engine/bindingchange.h" #include "apl/primitives/object.h" #include "apl/utils/path.h" @@ -53,6 +54,13 @@ class ContextObject { */ ContextObject& userWriteable() { mUserWriteable = true; mMutable = true; return *this; } + /** + * Attach a change watcher event handler to this object + * @param command The commands to execute when the context object changes + * @return This object, for chaining. + */ + ContextObject& onChange(const BindingChangePtr& onChange) { mOnChange = onChange; return *this; } + /** * @return The value of the object. */ @@ -78,12 +86,7 @@ class ContextObject { * @param value The new value to store. * @return True if the value was changed. */ - bool set(const Object& value) { - bool result = (mMutable && mValue != value); - if (result) - mValue = value; - return result; - } + bool set(const Object& value); std::string toDebugString() const; @@ -91,6 +94,7 @@ class ContextObject { private: Object mValue; + BindingChangePtr mOnChange; Path mProvenance; bool mMutable = false; bool mUserWriteable = false; diff --git a/aplcore/include/apl/engine/contextwrapper.h b/aplcore/include/apl/engine/contextwrapper.h index c765aa7..3acd309 100644 --- a/aplcore/include/apl/engine/contextwrapper.h +++ b/aplcore/include/apl/engine/contextwrapper.h @@ -72,9 +72,6 @@ class ContextWrapper : public ObjectData { // Context wrappers intentionally return a size of zero std::uint64_t size() const override { return 0; } - // Context wrappers intentionally return an empty map - const ObjectMap& getMap() const override; - bool truthy() const override { return !mContext.expired(); } bool empty() const override { return mContext.expired(); } diff --git a/aplcore/include/apl/engine/propdef.h b/aplcore/include/apl/engine/propdef.h index 8a9a1a4..18c9f6a 100644 --- a/aplcore/include/apl/engine/propdef.h +++ b/aplcore/include/apl/engine/propdef.h @@ -104,6 +104,14 @@ inline Object asNonAutoRelativeDimension(const Context& context, const Object& o return object.asNonAutoRelativeDimension(context); } +inline Object asAdjustableRange(const Context &context, const Object &object) { + if (!object.isMap()) + return Object::NULL_OBJECT(); + if (!object.has("minValue") || !object.has("maxValue") || !object.has("currentValue")) + return Object::NULL_OBJECT(); + return object; +} + extern Object asStyledText(const Context& context, const Object& object); extern Object asColor(const Context& context, const Object& object); @@ -231,6 +239,8 @@ enum PropertyDefFlags : uint32_t { kPropVisualHash = 0x8000, /// Property affects accessibility state of the component kPropAccessibility = 0x10000, + /// This property may influence the visual state of the component. + kPropVisibility = 0x20000, }; /** diff --git a/aplcore/include/apl/engine/sharedcontextdata.h b/aplcore/include/apl/engine/sharedcontextdata.h index 5663f6c..89535da 100644 --- a/aplcore/include/apl/engine/sharedcontextdata.h +++ b/aplcore/include/apl/engine/sharedcontextdata.h @@ -45,6 +45,7 @@ class KeyboardManager; class LayoutManager; class PointerManager; class TickScheduler; +class VisibilityManager; class UIDGenerator; class DataSourceConnection; using DataSourceConnectionPtr = std::shared_ptr; @@ -128,6 +129,7 @@ class SharedContextData : public NonCopyable, public Counter, UIDGenerator& uidGenerator() const { return deref(mUniqueIdGenerator); } EventManager& eventManager() const { return deref(mEventManager); } DependantManager& dependantManager() const { return deref(mDependantManager); } + VisibilityManager& visibilityManager() const { return deref(mVisibilityManager); } const YGConfigRef& ygconfig() const { return mYGConfigRef; } @@ -181,6 +183,7 @@ class SharedContextData : public NonCopyable, public Counter, std::unique_ptr mUniqueIdGenerator; std::unique_ptr mEventManager; std::unique_ptr mDependantManager; + std::unique_ptr mVisibilityManager; const DocumentManagerPtr mDocumentManager; std::shared_ptr mTimeManager; diff --git a/aplcore/include/apl/engine/visibilitymanager.h b/aplcore/include/apl/engine/visibilitymanager.h new file mode 100644 index 0000000..25a8f8f --- /dev/null +++ b/aplcore/include/apl/engine/visibilitymanager.h @@ -0,0 +1,71 @@ +/* + * 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_VISIBILITY_MANAGER_H +#define _APL_VISIBILITY_MANAGER_H + +#include +#include + +#include "apl/common.h" + +namespace apl { + +/** + * Simple manager class to take care of any tracked visibility change propagation and processing. + */ +class VisibilityManager { +public: + VisibilityManager() = default; + + /** + * Register component for visibility updates. + * @param component target component. + */ + void registerForUpdates(const CoreComponentPtr& component); + + /** + * De-register component from visibility updates. + * @param component target component. + */ + void deregister(const CoreComponentPtr& component); + + /** + * Mark component's visibility as dirty. Only directly registered components or their ancestors + * will be marked. + * @param component target component. + */ + void markDirty(const CoreComponentPtr& component); + + /** + * Process list of dirty components and report visibility changes if required. + * Happens once per frame. + */ + void processVisibilityChanges(); + +private: + struct VisibilityState { + double visibleRegionPercentage; + double cumulativeOpacity; + }; + + WeakPtrMap mTrackedComponentVisibility; + WeakPtrSet mDirtyVisibility; +}; + +} // namespace apl + +#endif // _APL_VISIBILITY_MANAGER_H \ No newline at end of file diff --git a/aplcore/include/apl/graphic/graphic.h b/aplcore/include/apl/graphic/graphic.h index 32b1999..fdfa28d 100644 --- a/aplcore/include/apl/graphic/graphic.h +++ b/aplcore/include/apl/graphic/graphic.h @@ -182,7 +182,7 @@ class Graphic : public UIDObject, void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph); #endif // SCENEGRAPH - class ObjectType final : public PointerHolderObjectType {}; + class ObjectType final : public SimplePointerHolderObjectType {}; private: /** diff --git a/aplcore/include/apl/graphic/graphicpattern.h b/aplcore/include/apl/graphic/graphicpattern.h index d8e8220..3ec80b3 100644 --- a/aplcore/include/apl/graphic/graphicpattern.h +++ b/aplcore/include/apl/graphic/graphicpattern.h @@ -75,7 +75,7 @@ class GraphicPattern : public UIDObject, bool truthy() const override { return true; } std::uint64_t size() const override { return mItems.size(); } - class ObjectType final : public PointerHolderObjectType {}; + class ObjectType final : public SimplePointerHolderObjectType {}; private: std::string mDescription; diff --git a/aplcore/include/apl/livedata/livemapobject.h b/aplcore/include/apl/livedata/livemapobject.h index a7d48a3..bd1ee3a 100644 --- a/aplcore/include/apl/livedata/livemapobject.h +++ b/aplcore/include/apl/livedata/livemapobject.h @@ -64,7 +64,10 @@ class LiveMapObject : public LiveDataObject, Counter { */ Object get(const std::string& key) const override; bool has(const std::string& key) const override; + std::pair keyAt(std::size_t offset) const override; const ObjectMap& getMap() const override; + std::uint64_t size() const override; + void accept(Visitor& visitor) const override; std::string toDebugString() const override { return "LiveMapObject"; } diff --git a/aplcore/include/apl/primitives/accessibilityaction.h b/aplcore/include/apl/primitives/accessibilityaction.h index 4beec12..ec3d29d 100644 --- a/aplcore/include/apl/primitives/accessibilityaction.h +++ b/aplcore/include/apl/primitives/accessibilityaction.h @@ -128,7 +128,7 @@ class AccessibilityAction : public ObjectData, AccessibilityAction(const CoreComponentPtr& component, std::string name, std::string label) : mComponent(component), mName(std::move(name)), mLabel(std::move(label)), mEnabled(true) {} - class ObjectType final : public PointerHolderObjectType {}; + class ObjectType final : public SimplePointerHolderObjectType {}; protected: void initialize(const ContextPtr& context, const Object& object); diff --git a/aplcore/include/apl/primitives/boundsymbol.h b/aplcore/include/apl/primitives/boundsymbol.h index d0eda51..6111196 100644 --- a/aplcore/include/apl/primitives/boundsymbol.h +++ b/aplcore/include/apl/primitives/boundsymbol.h @@ -47,7 +47,18 @@ class BoundSymbol friend streamer& operator<<(streamer&, const BoundSymbol&); - class ObjectType final : public EvaluableReferenceObjectType {}; + class ObjectType final : public ReferenceHolderObjectType { + public: + bool isEvaluable() const final { return true; } + + Object eval(const Object::DataHolder& dataHolder) const final { + return dataHolder.data->eval(); + } + + static std::shared_ptr createDirectObjectData(BoundSymbol&& content) { + return EvaluableDirectObjectData::create(std::move(content)); + } + }; private: std::weak_ptr mContext; diff --git a/aplcore/include/apl/primitives/color.h b/aplcore/include/apl/primitives/color.h index 9133a11..3acd980 100644 --- a/aplcore/include/apl/primitives/color.h +++ b/aplcore/include/apl/primitives/color.h @@ -159,8 +159,10 @@ class Color { return { true, it->second }; } - class ObjectType final : public TrueObjectType { + class ObjectType final : public SimpleObjectType { public: + bool truthy(const Object::DataHolder&) const override { return true; } + std::string asString(const Object::DataHolder& dataHolder) const override { return Color(dataHolder.value).asString(); } diff --git a/aplcore/include/apl/primitives/dimension.h b/aplcore/include/apl/primitives/dimension.h index 8daddfc..3199baf 100644 --- a/aplcore/include/apl/primitives/dimension.h +++ b/aplcore/include/apl/primitives/dimension.h @@ -107,7 +107,7 @@ class Dimension friend streamer& operator<<(streamer&, const Dimension&); - class DimensionObjectType : public BaseObjectType { + class DimensionObjectType : public SimpleObjectType { public: bool isDimension() const final { return true; } @@ -116,13 +116,16 @@ class Dimension } }; - class AutoDimensionObjectType final : public TrueObjectType { + class AutoDimensionObjectType final : public SimpleObjectType { public: + static ObjectTypeRef instance() { static Dimension::AutoDimensionObjectType sType; return &sType; } + bool truthy(const Object::DataHolder&) const override { return true; } + bool isAutoDimension() const override { return true; } std::string asString(const Object::DataHolder& dataHolder) const override { return "auto"; } diff --git a/aplcore/include/apl/primitives/functions.h b/aplcore/include/apl/primitives/functions.h index 9ca8177..e4c7269 100644 --- a/aplcore/include/apl/primitives/functions.h +++ b/aplcore/include/apl/primitives/functions.h @@ -59,7 +59,7 @@ class Function : public ObjectData { return "function<" + mName + ">"; } - class ObjectType final : public PointerHolderObjectType { + class ObjectType final : public SimplePointerHolderObjectType { public: bool isCallable() const override { return true; } diff --git a/aplcore/include/apl/primitives/object.h b/aplcore/include/apl/primitives/object.h index 8c12c53..bd6fcb0 100644 --- a/aplcore/include/apl/primitives/object.h +++ b/aplcore/include/apl/primitives/object.h @@ -284,6 +284,7 @@ class Object Object get(const std::string& key) const; bool has(const std::string& key) const; Object opt(const std::string& key, const Object& def) const; + std::pair keyAt(std::size_t offset) const; // ARRAY objects Object at(std::uint64_t index) const; diff --git a/aplcore/include/apl/primitives/objectdata.h b/aplcore/include/apl/primitives/objectdata.h index ca36631..28c56f1 100644 --- a/aplcore/include/apl/primitives/objectdata.h +++ b/aplcore/include/apl/primitives/objectdata.h @@ -136,6 +136,10 @@ class ObjectData : public NonCopyable { aplThrow("Illegal mutable map"); } + virtual std::pair keyAt(std::size_t offset) const { + aplThrow("Illegal map"); + } + virtual std::string toDebugString() const { return "Unknown type"; } @@ -347,6 +351,15 @@ class MapData : public ObjectData { return *mMap; } + std::pair keyAt(std::size_t offset) const override { + if (offset >= mMap->size()) + aplThrow("Attempted to return key beyond the end of a map"); + + auto it = mMap->cbegin(); + std::advance(it, offset); + return *it; + } + void accept(Visitor& visitor) const override { @@ -432,6 +445,15 @@ class JSONData : public JSONBaseData { return mValue->FindMember(key.c_str()) != mValue->MemberEnd(); } + std::pair keyAt(std::size_t offset) const override { + if (mValue->IsObject() && mValue->MemberCount() > offset) { + auto itr = mValue->MemberBegin(); + itr += offset; + return { itr->name.GetString(), itr->value }; + } + return { "", Object::NULL_OBJECT() }; + } + Object at(std::uint64_t index) const override { if (!mValue->IsArray() || index >= mValue->Size()) @@ -530,6 +552,15 @@ class JSONDocumentData : public JSONBaseData { return mDoc.FindMember(key.c_str()) != mDoc.MemberEnd(); } + std::pair keyAt(std::size_t offset) const override { + if (mDoc.IsObject() && mDoc.MemberCount() > offset) { + auto itr = mDoc.MemberBegin(); + itr += offset; + return { itr->name.GetString(), itr->value }; + } + return { "", Object::NULL_OBJECT() }; + } + Object at(std::uint64_t index) const override { if (!mDoc.IsArray() || index >= mDoc.Size()) diff --git a/aplcore/include/apl/primitives/objecttype.h b/aplcore/include/apl/primitives/objecttype.h index 62340a0..aedef02 100644 --- a/aplcore/include/apl/primitives/objecttype.h +++ b/aplcore/include/apl/primitives/objecttype.h @@ -27,7 +27,31 @@ static const char *NOT_SUPPORTED_ERROR = "Operation not supported on this type." /** * Object type class. Should be extended by specific type/class implementations, which may be stored - * in the Object. + * in the Object. A number of subclasses of ObjectType are provided to simplify the implementation + * of new Object types. The hierarchy of abstract ObjectTypes is: + * + * - ObjectType + * - BaseObjectType Provides the instance() method + * - SimpleObjectType Disallows Map and Array methods + * - ReferenceHolderObjectType For objects of storage type kStorageTypeReference + * - PointerHolderObjectType For objects of storage type kStorageTypePointer + * - SimplePointerHolderObjectType Disallows map and array methods + * - ContainerObjectType Support visitor method on containers + * - AbstractMapObjectType Disallows array methods + * - MapLikeObjectType For "almost-map" objects which support map methods but you can't directly read the map + * - MapObjectType For true maps where you can read the map directly + * - ArrayObjectType Disallows maps. Supports basic array operations. + * + * The concrete types are defined inline to a class and inherit from one of the abstract types. + * + * SimpleObjectType: Boolean, Color, Null, Number, String, Dimension, AutoDimension + * ReferenceHolderObjectType: MediaSource, Radii, Range, Rect, StyledText, Transform2D, URLRequest, + * BoundSymbol, Filter, Gradient, GraphicFilter + * SimplePointerHolderObjectType: GraphicPattern, Transform, AccessibilityAction, ByteCode, Easing, + * Function, Graphic + * MapLikeObjectType: ComponentEventWrapper, ContextWrapper + * MapObjectType: LiveMapObject, Map + * ArrayObjectType: LiveArrayObject, Array */ class ObjectType : public NonCopyable { public: @@ -43,9 +67,6 @@ class ObjectType : public NonCopyable { template bool is() const { return (T::ObjectType::instance() == this); } /// Complex type checks. - virtual bool isArray() const { return false; } - virtual bool isMap() const { return false; } - virtual bool isTrueMap() const { return false; } virtual bool isCallable() const { return false; } virtual bool isEvaluable() const { return false; } virtual bool isAbsoluteDimension() const { return false; } @@ -83,16 +104,12 @@ class ObjectType : public NonCopyable { /// ObjectData held types. template - const T& get(const Object::DataHolder& dataHolder) const { + const T& getReferenced(const Object::DataHolder& dataHolder) const { assert(is()); assert(T::ObjectType::scStorageType == Object::StorageType::kStorageTypeReference); return *static_cast(dataHolder.data->inner()); } - virtual const ObjectMap& getMap(const Object::DataHolder&) const { aplThrow(NOT_SUPPORTED_ERROR); } - virtual ObjectMap& getMutableMap(const Object::DataHolder&) const { aplThrow(NOT_SUPPORTED_ERROR); } - virtual const ObjectArray& getArray(const Object::DataHolder&) const { aplThrow(NOT_SUPPORTED_ERROR); } - virtual ObjectArray& getMutableArray(const Object::DataHolder&) const { aplThrow(NOT_SUPPORTED_ERROR); } virtual std::shared_ptr getLiveDataObject(const Object::DataHolder&) const { aplThrow(NOT_SUPPORTED_ERROR); @@ -100,15 +117,21 @@ class ObjectType : public NonCopyable { virtual bool truthy(const Object::DataHolder&) const { return false; } - // MAP objects - virtual Object get(const Object::DataHolder&, const std::string&) const { aplThrow(NOT_SUPPORTED_ERROR); } - virtual bool has(const Object::DataHolder&, const std::string&) const { aplThrow(NOT_SUPPORTED_ERROR); } - virtual Object opt(const Object::DataHolder&, const std::string&, const Object&) const { - aplThrow(NOT_SUPPORTED_ERROR); - } - - // ARRAY objects - virtual Object at(const Object::DataHolder&, std::uint64_t) const { aplThrow(NOT_SUPPORTED_ERROR); } + // MAP objects. Subclasses must override these. Use the MapFreeMixin class if not a map + virtual bool isMap() const = 0; + virtual bool isTrueMap() const = 0; + virtual const ObjectMap& getMap(const Object::DataHolder&) const = 0; + virtual ObjectMap& getMutableMap(const Object::DataHolder&) const = 0; + virtual Object get(const Object::DataHolder&, const std::string&) const = 0; + virtual bool has(const Object::DataHolder&, const std::string&) const = 0; + virtual Object opt(const Object::DataHolder&, const std::string&, const Object&) const = 0; + virtual std::pair keyAt(const Object::DataHolder&, std::size_t offset) const = 0; + + // ARRAY objects. Subclasses must override these. Use the ArrayFreeMixin class if not an array + virtual bool isArray() const = 0; + virtual const ObjectArray& getArray(const Object::DataHolder&) const = 0; + virtual ObjectArray& getMutableArray(const Object::DataHolder&) const = 0; + virtual Object at(const Object::DataHolder&, std::uint64_t) const = 0; // MAP, ARRAY, and STRING objects virtual std::uint64_t size(const Object::DataHolder&) const { return 0; } @@ -163,7 +186,7 @@ using ObjectTypeRef = const ObjectType*; Object::StorageType storageType() const override { return Object::StorageType::T; } template -class BaseObjectType : public ObjectType { +class BaseObjectType : public virtual ObjectType { public: static ObjectTypeRef instance() { static typename T::ObjectType sObjectType; @@ -173,14 +196,62 @@ class BaseObjectType : public ObjectType { STORAGE_TYPE(kStorageTypeValue); }; -template -class TrueObjectType : public BaseObjectType { +/** + * Mixin this class to mark an object type as NOT supporting map functions. + */ +class MapFreeMixin : public virtual ObjectType { public: - bool truthy(const Object::DataHolder&) const override { return true; } + bool isMap() const final { return false; } + bool isTrueMap() const final { return false; } + const ObjectMap& getMap(const Object::DataHolder&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + ObjectMap& getMutableMap(const Object::DataHolder&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + Object get(const Object::DataHolder&, const std::string&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + bool has(const Object::DataHolder&, const std::string&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + Object opt(const Object::DataHolder&, const std::string&, const Object&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + std::pair keyAt(const Object::DataHolder&, + std::size_t offset) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } +}; + +/** + * Mixin this class to mark an object type as NOT supporting array functions + */ +class ArrayFreeMixin : public virtual ObjectType { +public: + bool isArray() const final { return false; } + const ObjectArray& getArray(const Object::DataHolder&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + ObjectArray& getMutableArray(const Object::DataHolder&) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } + Object at(const Object::DataHolder&, std::uint64_t) const final { + aplThrow(NOT_SUPPORTED_ERROR); + } }; +/** + * A simple extension of the base object that does not support maps or arrays + */ template -class ReferenceHolderObjectType : public BaseObjectType { +class SimpleObjectType : public virtual BaseObjectType, + public virtual MapFreeMixin, + public virtual ArrayFreeMixin { +}; + +template +class ReferenceHolderObjectType : public virtual SimpleObjectType { public: bool truthy(const Object::DataHolder& dataHolder) const final { return dataHolder.data->truthy(); @@ -212,8 +283,10 @@ class ReferenceHolderObjectType : public BaseObjectType { }; template -class PointerHolderObjectType : public TrueObjectType { +class PointerHolderObjectType : public BaseObjectType { public: + bool truthy(const Object::DataHolder&) const override { return true; } + rapidjson::Value serialize( const Object::DataHolder& dataHolder, rapidjson::Document::AllocatorType& allocator) const override { @@ -239,6 +312,12 @@ class PointerHolderObjectType : public TrueObjectType { STORAGE_TYPE(kStorageTypePointer); }; +template +class SimplePointerHolderObjectType : public virtual PointerHolderObjectType, + public virtual MapFreeMixin, + public virtual ArrayFreeMixin { +}; + template class ContainerObjectType : public PointerHolderObjectType { public: @@ -252,7 +331,8 @@ class ContainerObjectType : public PointerHolderObjectType { }; template -class MapLikeObjectType : public ContainerObjectType { +class AbstractMapObjectType : public virtual ContainerObjectType, + public virtual ArrayFreeMixin { public: bool isMap() const final { return true; } @@ -267,10 +347,23 @@ class MapLikeObjectType : public ContainerObjectType { Object opt(const Object::DataHolder& dataHolder, const std::string& key, const Object& def) const final { return dataHolder.data->opt(key, def); } + + std::pair keyAt(const Object::DataHolder& dataHolder, std::size_t offset) const final { + return dataHolder.data->keyAt(offset); + } +}; + +template +class MapLikeObjectType : public AbstractMapObjectType { +public: + // Map-like objects do not allow you to return the map + bool isTrueMap() const final { return false; } + const ObjectMap& getMap(const Object::DataHolder&) const final { aplThrow(NOT_SUPPORTED_ERROR); } + ObjectMap& getMutableMap(const Object::DataHolder&) const final { aplThrow(NOT_SUPPORTED_ERROR); } }; template -class MapObjectType : public MapLikeObjectType { +class MapObjectType : public AbstractMapObjectType { public: bool isTrueMap() const final { return true; } @@ -297,7 +390,8 @@ class MapObjectType : public MapLikeObjectType { }; template -class ArrayObjectType : public ContainerObjectType { +class ArrayObjectType : public virtual ContainerObjectType, + public virtual MapFreeMixin { public: bool isArray() const final { return true; } @@ -330,40 +424,11 @@ class ArrayObjectType : public ContainerObjectType { } }; -template -class EvaluableObjectType : public PointerHolderObjectType { -public: - bool isEvaluable() const final { return true; } - - Object eval(const Object::DataHolder& dataHolder) const final { - return dataHolder.data->eval(); - } -}; - -/*** - * Store a referenced class in Object which supports the "eval" method. - * @tparam T - */ -template -class EvaluableReferenceObjectType : public ReferenceHolderObjectType { -public: - bool isEvaluable() const final { return true; } - - Object eval(const Object::DataHolder& dataHolder) const final { - return dataHolder.data->eval(); - } - - static std::shared_ptr createDirectObjectData(T&& content) { - return EvaluableDirectObjectData::create(std::move(content)); - } -}; - - /// Primitive types class Null { public: - class ObjectType final : public BaseObjectType { + class ObjectType final : public SimpleObjectType { public: bool empty(const Object::DataHolder&) const override { return true; } @@ -377,7 +442,7 @@ class Null { class Boolean { public: - class ObjectType final : public BaseObjectType { + class ObjectType final : public SimpleObjectType { public: std::string asString(const Object::DataHolder& dataHolder) const override { return static_cast(dataHolder.value) ? "true": "false"; @@ -426,7 +491,7 @@ class Boolean { class Number { public: - class ObjectType final : public BaseObjectType { + class ObjectType final : public SimpleObjectType { public: std::string asString(const Object::DataHolder& dataHolder) const override { return doubleToAplFormattedString(dataHolder.value); @@ -498,7 +563,7 @@ class Number { class String { public: - class ObjectType final : public BaseObjectType { + class ObjectType final : public SimpleObjectType { public: std::string asString(const Object::DataHolder& dataHolder) const override { return dataHolder.string; diff --git a/aplcore/include/apl/primitives/pseudolocalizer.h b/aplcore/include/apl/primitives/pseudolocalizer.h new file mode 100644 index 0000000..a2e8e6a --- /dev/null +++ b/aplcore/include/apl/primitives/pseudolocalizer.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_PSEUDO_LOCALIZATION_H +#define _APL_PSEUDO_LOCALIZATION_H + +#include "apl/primitives/object.h" +#include "apl/primitives/texttransform.h" + +namespace apl { + +// Concrete implementation for pseudo-localization +class PseudoLocalizationTextTransformer : public TextTransformer { +public: + /** + * Transform the input string using pseudo-localization techniques. + * + * This method applies pseudo-localization transformations to the input string, + * such as expanding by duplicating vowels, adding padding, and replacing + * characters with accented equivalents. The specific transformations are + * configured using the provided Object object. + * + * @param input The input string to be transformed. + * @param config An Object representing configuration settings for pseudo-localization. + * @return The pseudo-localized string. + */ + std::string transform(const std::string& input, const Object& config) const override; + +private: + /** + * Expand the string by duplicating vowels. + * @param input The input string. + * @param maxVowelsToBeAdded Maximum no. of vowels that can be added. + * @return The expanded string. + */ + static std::string expandByDuplicatingVowels(const std::string& input, int maxVowelsToBeAdded); + /** + * Add padding to the input string. + * Padding is added at the start and end of the string. + * @param input The input string. + * @param paddingToBeAdded The desired padding. + * @return The string with added padding. + */ + static std::string addPaddingToMeetExpansion(const std::string& input, int paddingToBeAdded); + + /** + * Function to expand the input string by duplicating vowels to meet the specified expansion + * factor. + * + * @param input The input string to be expanded. + * @param expansionPercentage The desired expansion percentage. In case its less than + * 0 or greater than 100, default expansion percentage of 30 will be used. + * @return The expanded string. + */ + static std::string expandString(const std::string& input, double expansionPercentage); + /** + * Add start and end markers around the input string.This would help identify truncations in + * case of missing markers. + * @param input The input string to which markers will be added. + * @return The string with added markers. + */ + static std::string addStartAndEndMarkers(const std::string& input); + /** + * Replace characters with their Latin accented equivalents. + * If a character is a vowel, it's retained to expand the length. + * @param input The input string to be transformed. + * @return The transformed string. + */ + static std::string replaceCharsWithAccentedVersions(const std::string& input); +}; +} // namespace apl + +#endif // _APL_PSEUDO_LOCALIZATION_H diff --git a/aplcore/include/apl/primitives/texttransform.h b/aplcore/include/apl/primitives/texttransform.h new file mode 100644 index 0000000..26e97c1 --- /dev/null +++ b/aplcore/include/apl/primitives/texttransform.h @@ -0,0 +1,41 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_TEXT_TRANSFORM_H +#define _APL_TEXT_TRANSFORM_H + +namespace apl { + +// Class to handle various text transformations +class TextTransformer { +public: + /** + * Transform the input string based on the provided configuration. + * + * This pure virtual function needs to be implemented by concrete classes. + * + * @param input The input string to be transformed. + * @param config An Object representing configuration settings for the transformation. + * @return The transformed string. + */ + virtual std::string transform(const std::string& input, const Object& config) const = 0; + /** + * Virtual destructor for proper polymorphic destruction. + */ + virtual ~TextTransformer() = default; +}; +} // namespace apl + +#endif // _APL_TEXT_TRANSFORM_H diff --git a/aplcore/include/apl/primitives/transform.h b/aplcore/include/apl/primitives/transform.h index d81e4e2..01d5158 100644 --- a/aplcore/include/apl/primitives/transform.h +++ b/aplcore/include/apl/primitives/transform.h @@ -78,7 +78,7 @@ class Transformation : public ObjectData { return "Transform<>"; } - class ObjectType final : public PointerHolderObjectType { + class ObjectType final : public SimplePointerHolderObjectType { public: rapidjson::Value serialize( const Object::DataHolder&, diff --git a/aplcore/include/apl/scenegraph/accessibility.h b/aplcore/include/apl/scenegraph/accessibility.h index cc5960b..733b9d1 100644 --- a/aplcore/include/apl/scenegraph/accessibility.h +++ b/aplcore/include/apl/scenegraph/accessibility.h @@ -25,6 +25,9 @@ #include "apl/component/componentproperties.h" namespace apl { + +class Object; + namespace sg { /** @@ -56,6 +59,12 @@ class Accessibility { } }; + struct AdjustableRange { + double minValue; + double maxValue; + double currentValue; + }; + explicit Accessibility(ActionCallback callback) : mActionCallback(callback) {} @@ -73,6 +82,12 @@ class Accessibility { const std::vector& actions() const { return mActions; } + bool setAdjustableRange(const apl::Object& object); + AdjustableRange getAdjustableRange() const { return mAdjustableRange; } + + bool setAdjustableValue(const std::string& value); + std::string getAdjustableValue() const { return mAdjustableValue; } + bool empty() const { return mLabel.empty() && mRole == kRoleNone && mActions.empty();} friend bool operator==(const Accessibility& lhs, const Accessibility& rhs){ @@ -90,6 +105,8 @@ class Accessibility { std::string mLabel; Role mRole = kRoleNone; std::vector mActions; + std::string mAdjustableValue; + AdjustableRange mAdjustableRange; }; } // namespace sg diff --git a/aplcore/include/apl/utils/screenlockholder.h b/aplcore/include/apl/utils/screenlockholder.h new file mode 100644 index 0000000..a2b1227 --- /dev/null +++ b/aplcore/include/apl/utils/screenlockholder.h @@ -0,0 +1,86 @@ +/* + * 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_SCREENLOCKHOLDER_H +#define APL_SCREENLOCKHOLDER_H + +#include "apl/engine/context.h" + +namespace apl { + +/** + * This is a helper class that holds a screen lock for a component and ensures + * that the screen lock is released if the component is destroyed. + * It's designed to be used as a member variable inside a class. For example:: + * + * class FooComponent { + * FooComponent() : mScreenLock(mContext) {} + * ScreenLockHolder mScreenLock; + * } + */ +class ScreenLockHolder { +public: + explicit ScreenLockHolder(ContextPtr context) + : mContext(std::move(context)) { + assert(mContext); + } + + ~ScreenLockHolder() { + if (mHasScreenLock) + mContext->releaseScreenLock(); + } + + /** + * Acquire the screen lock if it is not already held. + * This method may be safely called multiple times. + */ + void take() { + if (!mHasScreenLock) { + mHasScreenLock = true; + mContext->takeScreenLock(); + } + } + + /** + * Release the screen lock if it is currently held. + * This method may be safely called multiple times. + */ + void release() { + if (mHasScreenLock) { + mHasScreenLock = false; + mContext->releaseScreenLock(); + } + } + + /** + * Ensure that the screen lock is held or released based on the argument. + * This method may be safely called multiple times with the same argument. + * @param takeScreenLock If true, take the screen lock. Otherwise release it. + */ + void ensure(bool takeScreenLock) { + if (takeScreenLock) + take(); + else + release(); + } + +private: + ContextPtr mContext; + bool mHasScreenLock = false; +}; + +} // namespace apl + +#endif // APL_SCREENLOCKHOLDER_H diff --git a/aplcore/src/component/componenteventsourcewrapper.cpp b/aplcore/src/component/componenteventsourcewrapper.cpp index 77a3cbc..5c1f57c 100644 --- a/aplcore/src/component/componenteventsourcewrapper.cpp +++ b/aplcore/src/component/componenteventsourcewrapper.cpp @@ -20,11 +20,14 @@ namespace apl { std::shared_ptr ComponentEventSourceWrapper::create(const ConstCoreComponentPtr &component, - std::string handler, - const Object &value) { + std::string handler, + const Object &value) { + assert(!handler.empty()); auto result = std::make_shared(component); result->mHandler = handler; result->mValue = value; + if (component) + result->mSource = sComponentTypeBimap.at(component->getType()); return result; } @@ -37,12 +40,8 @@ ComponentEventSourceWrapper::get(const std::string& key) const if (key == "value") return mValue; - if (key == "source") { - auto component = mComponent.lock(); - if (component) - return sComponentTypeBimap.at(component->getType()); - return ""; - } + if (key == "source") + return mSource; return ComponentEventWrapper::get(key); } @@ -56,12 +55,8 @@ ComponentEventSourceWrapper::opt(const std::string& key, const Object& def) cons if (key == "value") return mHandler.empty() ? def : mValue; - if (key == "source") { - auto component = mComponent.lock(); - if (component) - return sComponentTypeBimap.at(component->getType()); - return def; - } + if (key == "source") + return mSource.empty() ? def : mSource; return ComponentEventWrapper::opt(key, def); } @@ -69,33 +64,35 @@ ComponentEventSourceWrapper::opt(const std::string& key, const Object& def) cons bool ComponentEventSourceWrapper::has(const std::string& key) const { - if (key == "handler") - return !mHandler.empty(); - - if (key == "value") + if (key == "handler" || key == "value" || key == "source") return true; - if (key == "source") - return mComponent.lock() != nullptr; - return ComponentEventWrapper::has(key); } +std::pair +ComponentEventSourceWrapper::keyAt(std::size_t offset) const +{ + auto basicSize = ComponentEventWrapper::size(); + if (offset < basicSize) + return ComponentEventWrapper::keyAt(offset); + + offset -= basicSize; + + // Provide a consistent ordering of "value", "handler", and "source" properties. + switch (offset) { + case 0: return { "value", mValue }; + case 1: return { "handler", mHandler }; + case 2: return { "source", mSource }; + default: return { "", Object::NULL_OBJECT() }; + } +} + std::uint64_t ComponentEventSourceWrapper::size() const { - // The number of properties in the source wrapper will be size of the parent class, one - // for the "value" property, and one each for the "handler" and "source" properties if - // they are present. - std::uint64_t result = ComponentEventWrapper::size() + 1; // The "value" property is always present. - - if (!mHandler.empty()) // The "handler" property may be not be set - result += 1; - - if (mComponent.lock() != nullptr) // The "source" property may not be available. - result += 1; - - return result; + // We add three properties to the size of the component wrapper. + return ComponentEventWrapper::size() + 3; } rapidjson::Value @@ -106,12 +103,11 @@ ComponentEventSourceWrapper::serialize(rapidjson::Document::AllocatorType& alloc if (component) { component->serializeEvent(m, allocator); // Note that the source properties are assigned AFTER serialization. This ensures we overwrite. - m.AddMember("source", rapidjson::StringRef(sComponentTypeBimap.at(component->getType()).c_str()), allocator); } m.AddMember("value", mValue.serialize(allocator), allocator); - if (!mHandler.empty()) - m.AddMember("handler", rapidjson::Value(mHandler.c_str(), allocator), allocator); + m.AddMember("handler", rapidjson::Value(mHandler.c_str(), allocator), allocator); + m.AddMember("source", rapidjson::Value(mSource.c_str(), allocator), allocator); return m; } diff --git a/aplcore/src/component/componenteventwrapper.cpp b/aplcore/src/component/componenteventwrapper.cpp index 391c88c..f87b393 100644 --- a/aplcore/src/component/componenteventwrapper.cpp +++ b/aplcore/src/component/componenteventwrapper.cpp @@ -56,6 +56,15 @@ ComponentEventWrapper::has(const std::string& key) const return false; } +std::pair +ComponentEventWrapper::keyAt(std::size_t offset) const +{ + auto component = mComponent.lock(); + if (component) + return component->getEventPropertyAt(offset); + return { "", Object::NULL_OBJECT() }; +} + std::uint64_t ComponentEventWrapper::size() const { @@ -66,12 +75,4 @@ ComponentEventWrapper::size() const return 0; } -// This routine returns an empty map to avoid crashing the system in complicated ways -const ObjectMap& -ComponentEventWrapper::getMap() const -{ - static auto empty = ObjectMap(); - return empty; -} - } // apl diff --git a/aplcore/src/component/componentproperties.cpp b/aplcore/src/component/componentproperties.cpp index 4d85130..49126cc 100644 --- a/aplcore/src/component/componentproperties.cpp +++ b/aplcore/src/component/componentproperties.cpp @@ -290,6 +290,8 @@ Bimap sComponentPropertyBimap = { {kPropertyAccessibilityActions, "_actions"}, {kPropertyAccessibilityActionsAssigned, "action"}, {kPropertyAccessibilityActionsAssigned, "actions"}, + {kPropertyAccessibilityAdjustableRange, "accessibilityAdjustableRange"}, + {kPropertyAccessibilityAdjustableValue, "accessibilityAdjustableValue"}, {kPropertyAccessibilityLabel, "accessibilityLabel"}, {kPropertyAlign, "align"}, {kPropertyAlignItems, "alignItems"}, @@ -344,6 +346,7 @@ Bimap sComponentPropertyBimap = { {kPropertyGestures, "gestures"}, {kPropertyGestures, "gesture"}, {kPropertyHandleTick, "handleTick"}, + {kPropertyHandleVisibilityChange, "handleVisibilityChange"}, {kPropertyHighlightColor, "highlightColor"}, {kPropertyHint, "hint"}, {kPropertyHintColor, "hintColor"}, @@ -438,6 +441,7 @@ Bimap sComponentPropertyBimap = { {kPropertyRight, "right"}, {kPropertyRole, "role"}, {kPropertyScale, "scale"}, + {kPropertyScreenLock, "screenLock"}, {kPropertyScrollAnimation, "-scrollAnimation"}, {kPropertyScrollDirection, "scrollDirection"}, {kPropertyScrollOffset, "scrollOffset"}, diff --git a/aplcore/src/component/corecomponent.cpp b/aplcore/src/component/corecomponent.cpp index e27a03c..8e7a05f 100644 --- a/aplcore/src/component/corecomponent.cpp +++ b/aplcore/src/component/corecomponent.cpp @@ -30,6 +30,7 @@ #include "apl/engine/hovermanager.h" #include "apl/engine/layoutmanager.h" #include "apl/engine/typeddependant.h" +#include "apl/engine/visibilitymanager.h" #include "apl/focus/focusmanager.h" #include "apl/livedata/layoutrebuilder.h" #include "apl/livedata/livearray.h" @@ -41,6 +42,7 @@ #include "apl/time/timemanager.h" #include "apl/touch/pointerevent.h" #include "apl/utils/hash.h" +#include "apl/utils/make_unique.h" #include "apl/utils/searchvisitor.h" #include "apl/utils/session.h" #include "apl/utils/stickychildrentree.h" @@ -239,6 +241,7 @@ CoreComponent::releaseSelf() { // TODO: Must remove this component from any dirty lists mContext->layoutManager().remove(shared_from_corecomponent()); + deregisterFromVisibilityTracking(); RecalculateTarget::removeUpstreamDependencies(); mParent = nullptr; mChildren.clear(); @@ -562,6 +565,10 @@ CoreComponent::insertChild(const CoreComponentPtr& child, size_t index, bool use markDisplayedChildrenStale(useDirtyFlag); setVisualContextDirty(); + // Register component for visibility calculation considerations, if required + coreChild->registerForVisibilityTrackingIfRequired(); + setVisibilityDirty(); + // Update the position: sticky components tree auto p = stickyfunctions::getAncestorHorizontalAndVerticalScrollable(coreChild); auto horizontalScrollable = std::get<0>(p); @@ -617,7 +624,9 @@ CoreComponent::removeChildAfterMarkedRemoved(const CoreComponentPtr& child, size markDisplayedChildrenStale(useDirtyFlag); mDisplayedChildren.clear(); - if (useDirtyFlag) setVisualContextDirty(); + if (useDirtyFlag) { + setVisualContextDirty(); + } // Update the position: sticky components tree auto p = stickyfunctions::getHorizontalAndVerticalScrollable(shared_from_corecomponent()); @@ -1003,6 +1012,9 @@ CoreComponent::handlePropertyChange(const ComponentPropDef& def, const Object& v if ((def.flags & kPropVisualContext) != 0) setVisualContextDirty(); + if ((def.flags & kPropVisibility) != 0) + setVisibilityDirty(); + // Properties with the kPropTextHash flag the text measurement hash as dirty if ((def.flags & kPropTextHash) != 0) mTextMeasurementHashStale = true; @@ -1416,6 +1428,9 @@ CoreComponent::setDirty( PropertyKey key ) setVisualContextDirty(); } + if (def->second.flags & kPropVisibility) + setVisibilityDirty(); + // Set text measurement hash as stale if (!mTextMeasurementHashStale && (def->second.flags & kPropTextHash)) { mTextMeasurementHashStale = true; @@ -1626,6 +1641,7 @@ CoreComponent::processLayoutChanges(bool useDirtyFlag, bool first) if (mParent) mParent->markDisplayedChildrenStale(useDirtyFlag); setVisualContextDirty(); + setVisibilityDirty(); if (useDirtyFlag) setDirty(kPropertyBounds); } @@ -1763,6 +1779,8 @@ CoreComponent::createEventProperties(const std::string& handler, const Object& v ContextPtr CoreComponent::createEventContext(const std::string& handler, const ObjectMapPtr& optional, const Object& value) const { + assert(!handler.empty()); + ContextPtr ctx = Context::createFromParent(mContext); auto compValue = getValue(); if (!value.isNull()) { @@ -1824,6 +1842,17 @@ CoreComponent::getEventPropertySize() const return eventPropertyMap().size(); } +std::pair +CoreComponent::getEventPropertyAt(size_t index) const +{ + const auto& map = eventPropertyMap(); + auto it = map.cbegin(); + std::advance(it, index); + if (it != map.cend()) + return { it->first, it->second(this) }; + return { "", Object::NULL_OBJECT() }; +} + void CoreComponent::serializeEvent(rapidjson::Value& out, rapidjson::Document::AllocatorType& allocator) const { @@ -2165,18 +2194,77 @@ CoreComponent::getVisualContextType() const } void -CoreComponent::setVisualContextDirty() { +CoreComponent::setVisualContextDirty() +{ // set this component as dirty visual context mContext->setDirtyVisualContext(shared_from_this()); } +void +CoreComponent::setVisibilityDirty() +{ + mContext->visibilityManager().markDirty(shared_from_corecomponent()); + + if (!mAffectedByVisibilityChange) return; + + for (const auto& c : *mAffectedByVisibilityChange) { + mContext->visibilityManager().markDirty(c.lock()); + } +} + +void +CoreComponent::addDownstreamVisibilityTarget(const CoreComponentPtr& child) +{ + if (!mAffectedByVisibilityChange) { + mAffectedByVisibilityChange = std::make_unique>(); + } + + mAffectedByVisibilityChange->emplace(child); + + if (mParent) mParent->addDownstreamVisibilityTarget(child); +} + +void +CoreComponent::removeDownstreamVisibilityTarget(const CoreComponentPtr& child) +{ + if (!mAffectedByVisibilityChange) return; + + mAffectedByVisibilityChange->erase(child); + if (mAffectedByVisibilityChange->empty()) + mAffectedByVisibilityChange = nullptr; + + if (mParent) mParent->removeDownstreamVisibilityTarget(child); +} + +void +CoreComponent::registerForVisibilityTrackingIfRequired() +{ + auto handlers = getCalculated(kPropertyHandleVisibilityChange); + if (handlers.isArray() && !handlers.empty()) { + mContext->visibilityManager().registerForUpdates(shared_from_corecomponent()); + if (mParent) mParent->addDownstreamVisibilityTarget(shared_from_corecomponent()); + setVisibilityDirty(); + } +} + +void +CoreComponent::deregisterFromVisibilityTracking() +{ + mContext->visibilityManager().deregister(shared_from_corecomponent()); + if (mParent) { + mParent->removeDownstreamVisibilityTarget(shared_from_corecomponent()); + mParent->setVisibilityDirty(); + } +} + bool CoreComponent::isVisualContextDirty() { return mContext->isVisualContextDirty(shared_from_this()); } std::map -CoreComponent::getChildrenVisibility(float realOpacity, const Rect &visibleRect) const { +CoreComponent::getChildrenVisibility(float realOpacity, const Rect &visibleRect) const +{ std::map visibleIndexes; for(int index = 0; index < mChildren.size(); index++) { @@ -2602,6 +2690,7 @@ CoreComponent::propDefSet() const { {kPropertyAccessibilityActionsAssigned, Object::EMPTY_ARRAY(), asArray, kPropIn}, {kPropertyBounds, Rect(0,0,0,0), nullptr, kPropOut | kPropVisualContext | + kPropVisibility | kPropVisualHash}, {kPropertyChecked, false, asBoolean, kPropInOut | kPropDynamic | @@ -2611,7 +2700,8 @@ CoreComponent::propDefSet() const { {kPropertyDisplay, kDisplayNormal, sDisplayMap, kPropInOut | kPropStyled | kPropDynamic | - kPropVisualContext, yn::setDisplay}, + kPropVisualContext | + kPropVisibility, yn::setDisplay}, {kPropertyDisabled, false, asBoolean, kPropInOut | kPropDynamic | kPropMixedState | @@ -2623,11 +2713,13 @@ CoreComponent::propDefSet() const { kPropVisualContext}, {kPropertyFocusable, false, nullptr, kPropOut}, {kPropertyHandleTick, Object::EMPTY_ARRAY(), asArray, kPropIn}, + {kPropertyHandleVisibilityChange, Object::EMPTY_ARRAY(), asArray, kPropIn}, {kPropertyHeight, Dimension(), asDimension, kPropIn | kPropDynamic | kPropStyled, yn::setHeight, defaultHeight}, {kPropertyInnerBounds, Rect(0,0,0,0), nullptr, kPropOut | kPropVisualContext | + kPropVisibility | kPropVisualHash}, {kPropertyLayoutDirectionAssigned, kLayoutDirectionInherit, sLayoutDirectionMap, kPropIn | kPropDynamic | @@ -2654,6 +2746,7 @@ CoreComponent::propDefSet() const { kPropStyled | kPropDynamic | kPropVisualContext | + kPropVisibility | kPropVisualHash}, {kPropertyPadding, Object::EMPTY_ARRAY(), asPaddingArray, kPropIn | kPropDynamic | @@ -2710,7 +2803,8 @@ CoreComponent::propDefSet() const { {kPropertyOnCursorEnter, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyOnCursorExit, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyLaidOut, false, asBoolean, kPropOut | - kPropVisualContext}, + kPropVisualContext | + kPropVisibility}, {kPropertyVisualHash, "", asString, kPropOut | kPropRuntimeState}, }); diff --git a/aplcore/src/component/gridsequencecomponent.cpp b/aplcore/src/component/gridsequencecomponent.cpp index 4ac6c8f..a051328 100644 --- a/aplcore/src/component/gridsequencecomponent.cpp +++ b/aplcore/src/component/gridsequencecomponent.cpp @@ -104,7 +104,7 @@ GridSequenceComponent::propDefSet() const static ComponentPropDefSet sSequenceComponentProperties(MultiChildScrollableComponent::propDefSet(), { {kPropertyChildHeight, Object::EMPTY_ARRAY(), asArray, kPropIn | kPropRequired | kPropDynamic}, {kPropertyChildWidth, Object::EMPTY_ARRAY(), asArray, kPropIn | kPropRequired | kPropDynamic}, - {kPropertyScrollDirection, kScrollDirectionVertical, sScrollDirectionMap, kPropInOut|kPropVisualContext, + {kPropertyScrollDirection, kScrollDirectionVertical, sScrollDirectionMap, kPropInOut | kPropVisualContext | kPropVisibility, yn::setGridScrollDirection}, {kPropertyWrap, kFlexboxWrapWrap, asInteger, kPropOut, yn::setWrap}, diff --git a/aplcore/src/component/multichildscrollablecomponent.cpp b/aplcore/src/component/multichildscrollablecomponent.cpp index 1b82185..fc046b7 100644 --- a/aplcore/src/component/multichildscrollablecomponent.cpp +++ b/aplcore/src/component/multichildscrollablecomponent.cpp @@ -475,21 +475,43 @@ getSpacing(const CoreComponent& component) return 0; } +double +MultiChildScrollableComponent::clampScrollPositionToValidValue(double scrollPosition, LayoutDirection layoutDirection, bool isHorizontal) +{ + if (isHorizontal) { + // scrollDirection is horizontal, clamp scrollPosition according to layoutDirection + if ((layoutDirection == kLayoutDirectionLTR && scrollPosition < 0) || + (layoutDirection == kLayoutDirectionRTL && scrollPosition > 0)) { + scrollPosition = 0; + } + } else { + // scrollDirection is vertical, so clamp any negative scrollPosition + if (scrollPosition < 0) { + scrollPosition = 0; + } + } + return scrollPosition; +} + void MultiChildScrollableComponent::fixScrollPosition(const Rect& oldAnchorRect, const Rect& anchorRect) { if (anchorRect != oldAnchorRect) { auto layoutDirection = static_cast(getCalculated(kPropertyLayoutDirection).asInt()); auto currentPosition = getCalculated(kPropertyScrollPosition).asNumber(); - float offset; - if (isHorizontal()) { + double offset; + bool horizontal = isHorizontal(); + if (horizontal) { offset = layoutDirection == kLayoutDirectionLTR ? anchorRect.getLeft() - oldAnchorRect.getLeft() : anchorRect.getRight() - oldAnchorRect.getRight(); } else { offset = anchorRect.getTop() - oldAnchorRect.getTop(); } - currentPosition += offset; + + currentPosition = + clampScrollPositionToValidValue(currentPosition + offset, layoutDirection, horizontal); + mCalculated.set(kPropertyScrollPosition, Dimension(DimensionType::Absolute, currentPosition)); setDirty(kPropertyScrollPosition); } @@ -814,6 +836,15 @@ MultiChildScrollableComponent::removeChildAfterMarkedRemoved(const CoreComponent mChildrenVisibilityStale = true; } +void +MultiChildScrollableComponent::releaseSelf() +{ + ScrollableComponent::releaseSelf(); + + // Children are cleared during release, so clear any "ensured children" indices + mEnsuredChildren = Range(); +} + /** * Relatively simple heuristics: take laid-out anchor component and estimate how many components will be required to * cover child cache region. Precise calculation is still up to proper layout pass, but (especially for cases when @@ -1080,10 +1111,12 @@ MultiChildScrollableComponent::onScrollPositionUpdated() { ScrollableComponent::onScrollPositionUpdated(); - for (int i = mEnsuredChildren.lowerBound(); i <= mEnsuredChildren.upperBound(); i++) { - auto child = CoreComponent::cast(mChildren.at(i)); - if (child != nullptr && child->isAttached()) { - child->markGlobalToLocalTransformStale(); + if (!mEnsuredChildren.empty()) { + for (int i = mEnsuredChildren.lowerBound(); i <= mEnsuredChildren.upperBound(); i++) { + auto child = CoreComponent::cast(mChildren.at(i)); + if (child != nullptr && child->isAttached()) { + child->markGlobalToLocalTransformStale(); + } } } diff --git a/aplcore/src/component/pagercomponent.cpp b/aplcore/src/component/pagercomponent.cpp index 240c76d..e32468d 100644 --- a/aplcore/src/component/pagercomponent.cpp +++ b/aplcore/src/component/pagercomponent.cpp @@ -166,12 +166,12 @@ PagerComponent::propDefSet() const { static ComponentPropDefSet sPagerComponentProperties(ActionableComponent::propDefSet(), { {kPropertyHeight, Dimension(100), asNonAutoDimension, kPropIn, yn::setHeight, defaultHeight}, {kPropertyWidth, Dimension(100), asNonAutoDimension, kPropIn, yn::setWidth, defaultWidth}, - {kPropertyInitialPage, 0, asInteger, kPropIn}, - {kPropertyNavigation, kNavigationWrap, sNavigationMap, kPropInOut | kPropDynamic | kPropVisualContext}, - {kPropertyPageDirection, kScrollDirectionHorizontal, sScrollDirectionMap, kPropIn | kPropDynamic}, - {kPropertyHandlePageMove, Object::EMPTY_ARRAY(), asArray, kPropIn}, - {kPropertyOnPageChanged, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyCurrentPage, 0, asInteger, kPropRuntimeState | kPropVisualContext | kPropAccessibility}, + {kPropertyInitialPage, 0, asInteger, kPropIn }, + {kPropertyNavigation, kNavigationWrap, sNavigationMap, kPropInOut | kPropDynamic | kPropVisualContext }, + {kPropertyPageDirection, kScrollDirectionHorizontal, sScrollDirectionMap, kPropIn | kPropDynamic }, + {kPropertyHandlePageMove, Object::EMPTY_ARRAY(), asArray, kPropIn }, + {kPropertyOnPageChanged, Object::EMPTY_ARRAY(), asCommand, kPropIn }, + {kPropertyCurrentPage, 0, asInteger, kPropRuntimeState | kPropVisualContext | kPropVisibility | kPropAccessibility }, {kPropertyPageId, getPageId, setPageId, kPropDynamic }, {kPropertyPageIndex, getPageIndex, setPageIndex, kPropDynamic }, }); @@ -217,6 +217,7 @@ PagerComponent::setPage(int page) { mCalculated.set(kPropertyCurrentPage, page); setVisualContextDirty(); + setVisibilityDirty(); attachPageAndReportLoaded(page); setDirty(kPropertyCurrentPage); @@ -235,6 +236,7 @@ PagerComponent::setPageImmediate(int pageIndex) mContext->sequencer().releaseResource({kExecutionResourcePosition, shared_from_this()}); mCalculated.set(kPropertyCurrentPage, pageIndex); setVisualContextDirty(); + setVisibilityDirty(); attachPageAndReportLoaded(pageIndex); setDirty(kPropertyCurrentPage); diff --git a/aplcore/src/component/scrollablecomponent.cpp b/aplcore/src/component/scrollablecomponent.cpp index b209621..6395e6c 100644 --- a/aplcore/src/component/scrollablecomponent.cpp +++ b/aplcore/src/component/scrollablecomponent.cpp @@ -146,7 +146,7 @@ ScrollableComponent::propDefSet() const static ComponentPropDefSet sScrollableComponentProperties(ActionableComponent::propDefSet(), { {kPropertyScrollOffset, getScrollOffset, setScrollOffset, kPropDynamic | kPropSetAfterLayout }, {kPropertyScrollPercent, getScrollPercent, setScrollPercent, kPropDynamic | kPropSetAfterLayout }, - {kPropertyScrollPosition, Dimension(0), asAbsoluteDimension, kPropRuntimeState | kPropVisualContext | kPropAccessibility}, + {kPropertyScrollPosition, Dimension(0), asAbsoluteDimension, kPropRuntimeState | kPropVisualContext | kPropVisibility | kPropAccessibility }, }); return sScrollableComponentProperties; } @@ -238,6 +238,7 @@ void ScrollableComponent::onScrollPositionUpdated() { setVisualContextDirty(); + setVisibilityDirty(); markDisplayedChildrenStale(true); setDirty(kPropertyScrollPosition); diff --git a/aplcore/src/component/sequencecomponent.cpp b/aplcore/src/component/sequencecomponent.cpp index b4bf94d..c10419a 100644 --- a/aplcore/src/component/sequencecomponent.cpp +++ b/aplcore/src/component/sequencecomponent.cpp @@ -41,7 +41,7 @@ SequenceComponent::propDefSet() const { static ComponentPropDefSet sSequenceComponentProperties(MultiChildScrollableComponent::propDefSet(), { {kPropertyFastScrollScale, 1.0, asNonNegativeNumber, kPropInOut | kPropStyled}, {kPropertyScrollAnimation, kScrollAnimationDefault, sScrollAnimationMap, kPropInOut | kPropStyled}, - {kPropertyScrollDirection, kScrollDirectionVertical, sScrollDirectionMap, kPropInOut | kPropVisualContext, + {kPropertyScrollDirection, kScrollDirectionVertical, sScrollDirectionMap, kPropInOut | kPropVisualContext | kPropVisibility, yn::setScrollDirection} }); return sSequenceComponentProperties; diff --git a/aplcore/src/component/touchablecomponent.cpp b/aplcore/src/component/touchablecomponent.cpp index 6a6522e..bf9c71d 100644 --- a/aplcore/src/component/touchablecomponent.cpp +++ b/aplcore/src/component/touchablecomponent.cpp @@ -89,12 +89,14 @@ const ComponentPropDefSet & TouchableComponent::propDefSet() const { static ComponentPropDefSet sTouchableComponentProperties(ActionableComponent::propDefSet(), { - {kPropertyOnCancel, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnDown, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnMove, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnPress, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyOnUp, Object::EMPTY_ARRAY(), asCommand, kPropIn}, - {kPropertyGestures, Object::EMPTY_ARRAY(), asArray, kPropIn} + {kPropertyOnCancel, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnDown, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnMove, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnPress, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnUp, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyGestures, Object::EMPTY_ARRAY(), asArray, kPropIn}, + {kPropertyAccessibilityAdjustableRange, Object::NULL_OBJECT(), asAdjustableRange, kPropInOut | kPropDynamic | kPropEvaluated}, + {kPropertyAccessibilityAdjustableValue, "", asString, kPropInOut | kPropDynamic} }); return sTouchableComponentProperties; } diff --git a/aplcore/src/component/videocomponent.cpp b/aplcore/src/component/videocomponent.cpp index a9506c6..cb7cf6c 100644 --- a/aplcore/src/component/videocomponent.cpp +++ b/aplcore/src/component/videocomponent.cpp @@ -78,6 +78,7 @@ VideoComponent::playerCallback(MediaPlayerEventType eventType, const MediaState& auto& sequencer = mContext->sequencer(); saveMediaState(mediaState); + updateScreenLock(); // The event handlers are invoked using new sequencer logic. In this version, the // onEnd/onPause/onPlay handlers are always invoked on a sequencer unique to the video @@ -156,6 +157,7 @@ void VideoComponent::detachPlayer() { mMediaPlayer = nullptr; + mScreenLock.release(); } void @@ -172,7 +174,8 @@ VideoComponent::VideoComponent(const ContextPtr& context, Properties&& properties, const Path& path) : CoreComponent(context, std::move(properties), path), - mMediaSequencer("VIDEO"+getUniqueId()) + mMediaSequencer("VIDEO"+getUniqueId()), + mScreenLock(mContext) { mIsDisallowed = context->getRootConfig().getProperty(RootProperty::kDisallowVideo).asBoolean(); @@ -198,6 +201,7 @@ VideoComponent::remove() if (mMediaPlayer) mMediaPlayer->halt(); + mScreenLock.release(); return CoreComponent::remove(); } @@ -209,6 +213,7 @@ VideoComponent::releaseSelf() mMediaPlayer = nullptr; } + mScreenLock.release(); CoreComponent::releaseSelf(); } @@ -251,6 +256,7 @@ VideoComponent::propDefSet() const static auto resetMediaState = [](Component& component) { auto& comp = (VideoComponent&)component; + comp.mScreenLock.release(); auto mediaPlayer = comp.getMediaPlayer(); if (mediaPlayer) mediaPlayer->setTrackList(mediaSourcesToTracks(comp.getCalculated(kPropertySource))); @@ -263,6 +269,11 @@ VideoComponent::propDefSet() const mediaPlayer->setMute(self.getCalculated(kPropertyMuted).asBoolean()); }; + static auto setScreenLock = [](Component& component) -> void { + auto& comp = (VideoComponent&)component; + comp.updateScreenLock(); + }; + static ComponentPropDefSet sVideoComponentProperties = ComponentPropDefSet( CoreComponent::propDefSet(), MediaComponentTrait::propDefList()).add({ { kPropertyAudioTrack, kAudioTrackForeground, sAudioTrackMap, kPropInOut }, @@ -285,7 +296,8 @@ VideoComponent::propDefSet() const { kPropertyTrackPaused, true, asBoolean, kPropRuntimeState | kPropVisualContext }, { kPropertyTrackEnded, false, asBoolean, kPropRuntimeState | kPropVisualContext }, { kPropertyTrackState, kTrackNotReady, sTrackStateMap, kPropRuntimeState}, - { kPropertyPlayingState, getPlayingState, setPlayingState, kPropDynamic }, + { kPropertyPlayingState, getPlayingState, setPlayingState, kPropDynamic}, + { kPropertyScreenLock, true, asBoolean, kPropDynamic| kPropIn, setScreenLock }, }); return sVideoComponentProperties; @@ -333,6 +345,19 @@ VideoComponent::saveMediaState(const MediaState& state) mCalculated.set(kPropertyTrackState, state.getTrackState()); } + +void +VideoComponent::updateScreenLock() +{ + bool hasScreenLock = getProperty(kPropertyTrackCount).asInt() > 0 && + !getProperty(kPropertyTrackEnded).truthy() && + !getProperty(kPropertyTrackPaused).truthy() && + getProperty(kPropertyScreenLock).truthy(); + + mScreenLock.ensure(hasScreenLock); +} + + void VideoComponent::updateMediaState(const MediaState& state, bool fromEvent) { @@ -345,6 +370,7 @@ VideoComponent::updateMediaState(const MediaState& state, bool fromEvent) auto previousCurrentTime = getCalculated(kPropertyTrackCurrentTime).asInt(); auto previousTrackState = getCalculated(kPropertyTrackState).asInt(); saveMediaState(state); + updateScreenLock(); if (state.getTrackIndex() != previousTrackIndex) { auto& commands = getCalculated(kPropertyOnTrackUpdate); diff --git a/aplcore/src/content/aplversion.cpp b/aplcore/src/content/aplversion.cpp index b941023..b759ccc 100644 --- a/aplcore/src/content/aplversion.cpp +++ b/aplcore/src/content/aplversion.cpp @@ -36,6 +36,7 @@ static const Bimap sVersionMap = { { APLVersion::kAPLVersion20231, "2023.1" }, { APLVersion::kAPLVersion20232, "2023.2" }, { APLVersion::kAPLVersion20233, "2023.3" }, + { APLVersion::kAPLVersion20241, "2024.1" }, }; bool diff --git a/aplcore/src/content/configurationchange.cpp b/aplcore/src/content/configurationchange.cpp index f3d600c..f458186 100644 --- a/aplcore/src/content/configurationchange.cpp +++ b/aplcore/src/content/configurationchange.cpp @@ -159,6 +159,10 @@ ConfigurationChange::asEventProperties(const RootConfig& rootConfig, const Metri return { {"height", metrics.pxToDp(mPixelHeight)}, {"width", metrics.pxToDp(mPixelWidth)}, + {"maxHeight", metrics.pxToDp(mMaxPixelHeight)}, + {"maxWidth", metrics.pxToDp(mMaxPixelWidth)}, + {"minHeight", metrics.pxToDp(mMinPixelHeight)}, + {"minWidth", metrics.pxToDp(mMinPixelWidth)}, {"theme", mTheme}, {"viewportMode", sViewportModeBimap.at(mViewportMode)}, {"disallowVideo", mDisallowVideo}, diff --git a/aplcore/src/document/coredocumentcontext.cpp b/aplcore/src/document/coredocumentcontext.cpp index 8c77423..6108f5a 100644 --- a/aplcore/src/document/coredocumentcontext.cpp +++ b/aplcore/src/document/coredocumentcontext.cpp @@ -32,6 +32,7 @@ #include "apl/engine/sharedcontextdata.h" #include "apl/engine/styles.h" #include "apl/engine/uidmanager.h" +#include "apl/engine/visibilitymanager.h" #include "apl/extension/extensionclient.h" #include "apl/extension/extensionmanager.h" #include "apl/focus/focusmanager.h" @@ -216,8 +217,10 @@ CoreDocumentContext::finishReinflate(const LayoutCallbackFunc& layoutCallback, c } // If there was a previous top component, release it and its children to free memory - if (oldTop) + if (oldTop) { + oldTop->deregisterFromVisibilityTracking(); oldTop->release(); + } // Clear the old active configuration; it is reset on a reinflation mActiveConfigurationChanges.clear(); @@ -672,6 +675,7 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) if (!mCore->mTop) return false; + mCore->mTop->registerForVisibilityTrackingIfRequired(); mCore->mTop->markGlobalToLocalTransformStale(); #ifdef ALEXAEXTENSIONS diff --git a/aplcore/src/document/documentcontextdata.cpp b/aplcore/src/document/documentcontextdata.cpp index 4fbfab2..4780fc8 100644 --- a/aplcore/src/document/documentcontextdata.cpp +++ b/aplcore/src/document/documentcontextdata.cpp @@ -119,6 +119,7 @@ LayoutManager& DocumentContextData::layoutManager() const { return mSharedData-> MediaManager& DocumentContextData::mediaManager() const { return mSharedData->mediaManager(); } MediaPlayerFactory& DocumentContextData::mediaPlayerFactory() const { return mSharedData->mediaPlayerFactory(); } DependantManager& DocumentContextData::dependantManager() const { return mSharedData->dependantManager(); } +VisibilityManager& DocumentContextData::visibilityManager() const { return mSharedData->visibilityManager(); } const YGConfigRef& DocumentContextData::ygconfig() const { return mSharedData->ygconfig(); } const TextMeasurementPtr& DocumentContextData::measure() const { return mSharedData->measure(); } void DocumentContextData::takeScreenLock() { mSharedData->takeScreenLock(); } diff --git a/aplcore/src/engine/CMakeLists.txt b/aplcore/src/engine/CMakeLists.txt index c8d2aad..769f786 100644 --- a/aplcore/src/engine/CMakeLists.txt +++ b/aplcore/src/engine/CMakeLists.txt @@ -15,10 +15,10 @@ target_sources_local(apl PRIVATE arrayify.cpp binding.cpp + bindingchange.cpp builder.cpp context.cpp contextobject.cpp - contextwrapper.cpp corerootcontext.cpp dependant.cpp dependantmanager.cpp @@ -42,4 +42,5 @@ target_sources_local(apl uidgenerator.cpp uidmanager.cpp uidobject.cpp + visibilitymanager.cpp ) diff --git a/aplcore/src/engine/bindingchange.cpp b/aplcore/src/engine/bindingchange.cpp new file mode 100644 index 0000000..de58ed3 --- /dev/null +++ b/aplcore/src/engine/bindingchange.cpp @@ -0,0 +1,84 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/engine/bindingchange.h" + +#include "apl/engine/propdef.h" +#include "apl/engine/typeddependant.h" +#include "apl/utils/identifier.h" +#include "apl/utils/log.h" +#include "apl/utils/session.h" +#include "apl/utils/tracing.h" + + +namespace apl { + +std::vector +attachBindings(const ContextPtr& context, const Object& item, std::function makeBCP) +{ + APL_TRACE_BLOCK("Builder:attachBindings"); + auto bindings = arrayifyProperty(*context, item, "bind"); + std::vector bindList; + + for (const auto& binding : bindings) { + auto name = propertyAsString(*context, binding, "name"); + if (!isValidIdentifier(name)) { + CONSOLE(context) << "Invalid binding name '" << name << "'"; + continue; + } + + if (!binding.has("value")) { + CONSOLE(context) << "Binding '" << name << "' did not specify a value"; + continue; + } + + if (context->hasLocal(name)) { + CONSOLE(context) << "Attempted to bind to pre-existing property '" << name << "'"; + continue; + } + + // Extract the binding as an optional node tree. + auto result = parseAndEvaluate(*context, binding.get("value")); + auto bindingType = + propertyAsMapped(*context, binding, "type", kBindingTypeAny, sBindingMap); + auto bindingFunc = sBindingFunctions.at(bindingType); + + BindingChangePtr ptr; + if (makeBCP) { + // Construct a BindingChange object if there is a valid "onChange" property on the binding + auto onChangeCommands = binding.opt("onChange", Object::NULL_OBJECT()); + if (!onChangeCommands.empty()) { + auto commands = asCommand(*context, onChangeCommands); + if (!commands.empty()) { + ptr = makeBCP(std::move(commands)); + bindList.push_back(ptr); + } + } + } + + // Store the value in the new context. Binding values are mutable; they can be changed later. + context->putUserWriteable(name, bindingFunc(*context, result.value), ptr); + + if (!result.symbols.empty()) + ContextDependant::create(context, name, std::move(result.expression), context, + std::move(bindingFunc), std::move(result.symbols)); + } + + return bindList; +} + + + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/engine/builder.cpp b/aplcore/src/engine/builder.cpp index 33cf45f..5b8b251 100644 --- a/aplcore/src/engine/builder.cpp +++ b/aplcore/src/engine/builder.cpp @@ -44,6 +44,7 @@ #include "apl/livedata/livearray.h" #include "apl/livedata/livearrayobject.h" #include "apl/primitives/object.h" +#include "apl/time/sequencer.h" #include "apl/utils/identifier.h" #include "apl/utils/log.h" #include "apl/utils/path.h" @@ -70,6 +71,35 @@ static const std::map sComponentMap = { {"Host", HostComponent::create} }; + +class BindingChangeImpl : public BindingChange { +public: + static std::shared_ptr create(Object&& commands) + { + return std::make_shared(std::move(commands)); + } + + explicit BindingChangeImpl(Object&& commands) : BindingChange(std::move(commands)) {} + + void setComponent(const CoreComponentPtr& component) { mComponent = component; } + + void execute(const Object& value, const Object& previous) override { + auto component = mComponent.lock(); + if (component) { + auto context = component->createEventContext("Change", + std::make_shared(ObjectMap{{"current", value}, + {"previous", previous}})); + component->getContext()->sequencer().executeCommands(commands(), + context, + component->shared_from_corecomponent(), + true); + } + } + +private: + std::weak_ptr mComponent; +}; + void Builder::populateSingleChildLayout(const ContextPtr& context, const Object& item, @@ -254,7 +284,8 @@ Builder::expandSingleComponent(const ContextPtr& context, ContextPtr expanded = Context::createFromParent(context); expanded->putConstant("__source", "component"); expanded->putConstant("__name", type); - attachBindings(expanded, item); + + auto bindingChangeList = attachBindings(expanded, item, BindingChangeImpl::create); // Construct the component CoreComponentPtr component = CoreComponentPtr(method(expanded, std::move(properties), path)); @@ -274,6 +305,10 @@ Builder::expandSingleComponent(const ContextPtr& context, return nullptr; } + // Assign the component to any bindings that had a "onChange" method + for (const auto& m : bindingChangeList) + std::static_pointer_cast(m)->setComponent(component); + CoreComponentPtr oldComponent; if(mOld) { oldComponent = CoreComponent::cast( @@ -311,47 +346,6 @@ Builder::expandSingleComponent(const ContextPtr& context, return nullptr; } -/** - * Process data bindings - * @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 ContextPtr& context, const Object& item) -{ - APL_TRACE_BLOCK("Builder:attachBindings"); - auto bindings = arrayifyProperty(*context, item, "bind"); - for (const auto& binding : bindings) { - auto name = propertyAsString(*context, binding, "name"); - if (!isValidIdentifier(name)) { - CONSOLE(context) << "Invalid binding name '" << name << "'"; - continue; - } - - if (!binding.has("value")) { - CONSOLE(context) << "Binding '" << name << "' did not specify a value"; - continue; - } - - if (context->hasLocal(name)) { - CONSOLE(context) << "Attempted to bind to pre-existing property '" << name << "'"; - continue; - } - - // Extract the binding as an optional node tree. - auto result = parseAndEvaluate(*context, binding.get("value")); - auto bindingType = - propertyAsMapped(*context, binding, "type", kBindingTypeAny, sBindingMap); - auto bindingFunc = sBindingFunctions.at(bindingType); - - // Store the value in the new context. Binding values are mutable; they can be changed later. - context->putUserWriteable(name, bindingFunc(*context, result.value)); - if (!result.symbols.empty()) - ContextDependant::create(context, name, std::move(result.expression), context, - std::move(bindingFunc), std::move(result.symbols)); - } -} - /** * Expand a single component from a "when" list of possible components * @@ -430,7 +424,7 @@ Builder::expandLayout(const std::string& name, properties.addToContext(cptr, param, true); } - Builder::attachBindings(cptr, layout); + auto bindingChangeList = attachBindings(cptr, layout, BindingChangeImpl::create); if (DEBUG_BUILDER) { for (ConstContextPtr p = cptr; p; p = p->parent()) { @@ -438,13 +432,19 @@ Builder::expandLayout(const std::string& name, LOG(LogLevel::kDebug).session(context) << m.first << ": " << m.second; } } - return expandSingleComponentFromArray(cptr, - arrayifyProperty(*cptr, layout, "item", "items"), - std::move(properties), - parent, - path.addProperty(layout, "item", "items"), - fullBuild, - useDirtyFlag); + auto component = expandSingleComponentFromArray(cptr, + arrayifyProperty(*cptr, layout, "item", "items"), + std::move(properties), + parent, + path.addProperty(layout, "item", "items"), + fullBuild, + useDirtyFlag); + if (component) { + // Assign the component to any bindings that had a "onChange" method + for (const auto& m : bindingChangeList) + std::static_pointer_cast(m)->setComponent(component); + } + return component; } /** diff --git a/aplcore/src/engine/context.cpp b/aplcore/src/engine/context.cpp index 738f9b3..1a36cdc 100644 --- a/aplcore/src/engine/context.cpp +++ b/aplcore/src/engine/context.cpp @@ -484,6 +484,12 @@ Context::dependantManager() const return documentContextData(mCore)->dependantManager(); } +VisibilityManager& +Context::visibilityManager() const +{ + return documentContextData(mCore)->visibilityManager(); +} + const SessionPtr& Context::session() const { diff --git a/aplcore/src/engine/contextobject.cpp b/aplcore/src/engine/contextobject.cpp index 377767d..5ee0081 100644 --- a/aplcore/src/engine/contextobject.cpp +++ b/aplcore/src/engine/contextobject.cpp @@ -17,6 +17,22 @@ namespace apl { +bool +ContextObject::set(const Object& value) { + bool result = (mMutable && mValue != value); + if (result) { + if (mOnChange) { + auto previous = mValue; + mValue = value; + mOnChange->run(value, previous); + } + else { + mValue = value; + } + } + return result; +} + std::string ContextObject::toDebugString() const { diff --git a/aplcore/src/engine/contextwrapper.cpp b/aplcore/src/engine/contextwrapper.cpp deleted file mode 100644 index 29896af..0000000 --- a/aplcore/src/engine/contextwrapper.cpp +++ /dev/null @@ -1,27 +0,0 @@ -/** - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * - * Licensed under the Apache License, Version 2.0 (the "License"). - * You may not use this file except in compliance with the License. - * A copy of the License is located at - * - * http://aws.amazon.com/apache2.0/ - * - * or in the "license" file accompanying this file. This file is distributed - * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either - * express or implied. See the License for the specific language governing - * permissions and limitations under the License. - */ - -#include "apl/engine/contextwrapper.h" - -namespace apl { - -const ObjectMap& -ContextWrapper::getMap() const -{ - static auto empty = ObjectMap(); - return empty; -} - -} // namespace apl diff --git a/aplcore/src/engine/corerootcontext.cpp b/aplcore/src/engine/corerootcontext.cpp index 54efddc..9dda871 100644 --- a/aplcore/src/engine/corerootcontext.cpp +++ b/aplcore/src/engine/corerootcontext.cpp @@ -30,6 +30,7 @@ #include "apl/engine/sharedcontextdata.h" #include "apl/engine/tickscheduler.h" #include "apl/engine/uidmanager.h" +#include "apl/engine/visibilitymanager.h" #include "apl/extension/extensionmanager.h" #include "apl/focus/focusmanager.h" #include "apl/graphic/graphic.h" @@ -184,6 +185,8 @@ CoreRootContext::clearPendingInternal(bool first) const for (const auto& comp : mShared->dirtyComponents().getAll()) { CoreComponent::cast(comp)->postClearPending(); } + + mShared->visibilityManager().processVisibilityChanges(); } bool diff --git a/aplcore/src/engine/sharedcontextdata.cpp b/aplcore/src/engine/sharedcontextdata.cpp index 3f57189..3cfb9ae 100644 --- a/aplcore/src/engine/sharedcontextdata.cpp +++ b/aplcore/src/engine/sharedcontextdata.cpp @@ -23,6 +23,7 @@ #include "apl/engine/layoutmanager.h" #include "apl/engine/tickscheduler.h" #include "apl/engine/uidgenerator.h" +#include "apl/engine/visibilitymanager.h" #include "apl/focus/focusmanager.h" #include "apl/media/mediamanager.h" #include "apl/touch/pointermanager.h" @@ -82,6 +83,7 @@ SharedContextData::SharedContextData(const CoreRootContextPtr& root, mUniqueIdGenerator(std::make_unique()), mEventManager(std::make_unique()), mDependantManager(std::make_unique()), + mVisibilityManager(std::make_unique()), mDocumentManager(config.getDocumentManager()), mTimeManager(config.getTimeManager()), mMediaManager(config.getMediaManager()), diff --git a/aplcore/src/engine/visibilitymanager.cpp b/aplcore/src/engine/visibilitymanager.cpp new file mode 100644 index 0000000..9b2dd73 --- /dev/null +++ b/aplcore/src/engine/visibilitymanager.cpp @@ -0,0 +1,97 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/engine/visibilitymanager.h" + +#include "apl/component/corecomponent.h" +#include "apl/engine/arrayify.h" +#include "apl/time/sequencer.h" + +namespace apl { + +void +VisibilityManager::registerForUpdates(const CoreComponentPtr& component) +{ + // Stash tracked component itself + mTrackedComponentVisibility.emplace(component, VisibilityState{-1, -1}); +} + +void +VisibilityManager::deregister(const CoreComponentPtr& component) +{ + if (component && mTrackedComponentVisibility.count(component)) { + mTrackedComponentVisibility.erase(component); + } +} + +void +VisibilityManager::markDirty(const CoreComponentPtr& component) +{ + if (component && mTrackedComponentVisibility.count(component)) { + mDirtyVisibility.emplace(component); + } +} + +void +VisibilityManager::processVisibilityChanges() +{ + for (const auto& cc : mDirtyVisibility) { + auto it = mTrackedComponentVisibility.find(cc); + if (it == mTrackedComponentVisibility.end()) continue; + + auto component = it->first.lock(); + if (!component) continue; + + auto handlers = component->getCalculated(kPropertyHandleVisibilityChange); + + // Should not happen, components without handler defined should not even be registered + assert(!handlers.isNull()); + + auto& ctx = *component->getContext(); + auto commands = Object::NULL_OBJECT(); + for (auto& handler : handlers.getArray()) { + if (propertyAsBoolean(ctx, handler, "when", true)) { + commands = Object(arrayifyProperty(ctx, handler, "commands")); + } + } + + if (!commands.isArray()) continue; + + // displayed + auto cumulativeOpacity = component->calculateRealOpacity(); + auto visibleArea = component->calculateVisibleRect().area(); + auto ownArea = component->getProperty(kPropertyBounds).get().area(); + auto visibleRegionPercentage = (ownArea == 0 || visibleArea == 0) ? 0 : visibleArea / ownArea; + + // If same state - do not report + if (it->second.visibleRegionPercentage == visibleRegionPercentage && + it->second.cumulativeOpacity == cumulativeOpacity) { + continue; + } + + it->second = VisibilityState{visibleRegionPercentage, cumulativeOpacity}; + + auto visibilityOpt = std::make_shared>(); + visibilityOpt->emplace("visibleRegionPercentage", visibleRegionPercentage); + visibilityOpt->emplace("cumulativeOpacity", cumulativeOpacity); + + auto eventContext = component->createEventContext("VisibilityChange", visibilityOpt); + eventContext->sequencer().executeCommands(commands, eventContext, component, true); + } + + mDirtyVisibility.clear(); +} + +} // namespace apl diff --git a/aplcore/src/extension/extensionclient.cpp b/aplcore/src/extension/extensionclient.cpp index a2bbc21..8226a3f 100644 --- a/aplcore/src/extension/extensionclient.cpp +++ b/aplcore/src/extension/extensionclient.cpp @@ -1151,7 +1151,7 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const } rapidjson::Value -ExtensionClient::createComponentChange(rapidjson::MemoryPoolAllocator<>& allocator, ExtensionComponent& component) +ExtensionClient::createComponentChange(rapidjson::Document::AllocatorType& allocator, ExtensionComponent& component) { auto extensionURI = component.getUri(); if (extensionURI != mUri) { @@ -1246,21 +1246,21 @@ ExtensionClient::handleDisconnectionInternal(const CoreDocumentContextPtr& docum } rapidjson::Value -ExtensionClient::processComponentRequest(rapidjson::MemoryPoolAllocator<>& allocator, +ExtensionClient::processComponentRequest(rapidjson::Document::AllocatorType& allocator, ExtensionComponent& component) { return createComponentChange(allocator, component); } rapidjson::Value -ExtensionClient::processComponentRelease(rapidjson::MemoryPoolAllocator<>& allocator, +ExtensionClient::processComponentRelease(rapidjson::Document::AllocatorType& allocator, ExtensionComponent& component) { return createComponentChange(allocator, component); } rapidjson::Value -ExtensionClient::processComponentUpdate(rapidjson::MemoryPoolAllocator<>& allocator, +ExtensionClient::processComponentUpdate(rapidjson::Document::AllocatorType& allocator, ExtensionComponent& component) { return createComponentChange(allocator, component); diff --git a/aplcore/src/graphic/graphicbuilder.cpp b/aplcore/src/graphic/graphicbuilder.cpp index 2870fab..96d2b3d 100644 --- a/aplcore/src/graphic/graphicbuilder.cpp +++ b/aplcore/src/graphic/graphicbuilder.cpp @@ -132,7 +132,6 @@ GraphicBuilder::addChildren(GraphicElement& element, const Object& json) } } - GraphicElementPtr GraphicBuilder::createChild(const ContextPtr& context, const Object& json) { @@ -148,34 +147,13 @@ GraphicBuilder::createChild(const ContextPtr& context, const Object& json) // Create a new context and apply data binding ContextPtr expanded = Context::createFromParent(context); - auto bindings = arrayifyProperty(*context, json, "bind"); - for (const auto& binding : bindings) { - auto name = propertyAsString(*expanded, binding, "name"); - if (!isValidIdentifier(name)) { - CONSOLE(context) << "Invalid binding name '" << name << "'"; - continue; - } - - if (!binding.has("value")) { - CONSOLE(context) << "Binding '" << name << "' did not specify a value"; - continue; - } - - // Extract the binding as an optional node tree. - auto result = parseAndEvaluate(*context, binding.get("value")); - auto bindingType = - propertyAsMapped(*expanded, binding, "type", kBindingTypeAny, sBindingMap); - auto bindingFunc = sBindingFunctions.at(bindingType); - context->putUserWriteable(name, bindingFunc(*context, result.value)); - if (!result.symbols.empty()) - ContextDependant::create(context, name, std::move(result.expression), context, - std::move(bindingFunc), std::move(result.symbols)); - } + attachBindings(context, json, nullptr); // Attach bindings, but don't support the "onChange" handler // Inflate the child auto child = it->second(mGraphic, expanded, json); if (child && child->hasChildren()) addChildren(*child, json); + return child; } diff --git a/aplcore/src/livedata/livemapobject.cpp b/aplcore/src/livedata/livemapobject.cpp index 51d53be..7b20782 100644 --- a/aplcore/src/livedata/livemapobject.cpp +++ b/aplcore/src/livedata/livemapobject.cpp @@ -49,6 +49,24 @@ LiveMapObject::has(const std::string& key) const return mLiveMap->has(key); } +std::pair +LiveMapObject::keyAt(std::size_t offset) const +{ + const auto& map = mLiveMap->getMap(); + if (offset < map.size()) { + auto it = map.cbegin(); + std::advance(it, offset); + return *it; + } + return { "", Object::NULL_OBJECT() }; +} + +std::uint64_t +LiveMapObject::size() const +{ + return mLiveMap->getMap().size(); +} + const ObjectMap& LiveMapObject::getMap() const { diff --git a/aplcore/src/primitives/CMakeLists.txt b/aplcore/src/primitives/CMakeLists.txt index bc44044..13fa6c7 100644 --- a/aplcore/src/primitives/CMakeLists.txt +++ b/aplcore/src/primitives/CMakeLists.txt @@ -25,6 +25,7 @@ target_sources_local(apl mediasource.cpp object.cpp objecttype.cpp + pseudolocalizer.cpp radii.cpp range.cpp rect.cpp diff --git a/aplcore/src/primitives/functions.cpp b/aplcore/src/primitives/functions.cpp index 001bb51..3428788 100644 --- a/aplcore/src/primitives/functions.cpp +++ b/aplcore/src/primitives/functions.cpp @@ -232,6 +232,26 @@ arraySlice(const ObjectArray& args) return SliceGenerator::create(array, start, end); } +static Object +mapKeys(const ObjectArray& args) +{ + if (args.empty()) + return Object::EMPTY_ARRAY(); + + if (!args.at(0).isMap()) + return Object::EMPTY_ARRAY(); + + const auto length = args.at(0).size(); + if (length == 0) + return Object::EMPTY_ARRAY(); + + auto result = std::make_shared(); + for (auto i = 0 ; i < length ; i++) + result->emplace_back(args.at(0).keyAt(i).first); + + return result; +} + /** * This method assumes a UTF-8 encoded string. It returns the number * of code points in the string. @@ -521,11 +541,22 @@ createArrayMap() return map; } +static ObjectMapPtr +createMapMap() +{ + auto map = std::make_shared(); + + map->emplace("keys", Function::create("keys", mapKeys)); + + return map; +} + void createStandardFunctions(Context& context) { static auto sArrayFunctions = createArrayMap(); static auto sLogFunctions = createLogMap(); + static auto sMapFunctions = createMapMap(); static auto sMathFunctions = createMathMap(); // String functions are dependent on RootConfig locale methods auto sStringFunctions = createStringMap(context.getLocaleMethods()); @@ -533,6 +564,7 @@ createStandardFunctions(Context& context) context.putConstant("Array", sArrayFunctions); context.putConstant("Log", sLogFunctions); + context.putConstant("Map", sMapFunctions); context.putConstant("Math", sMathFunctions); context.putConstant("String", sStringFunctions); context.putConstant("Time", sTimeFunctions); diff --git a/aplcore/src/primitives/object.cpp b/aplcore/src/primitives/object.cpp index 228eea3..ae39790 100644 --- a/aplcore/src/primitives/object.cpp +++ b/aplcore/src/primitives/object.cpp @@ -479,6 +479,7 @@ bool Object::truthy() const { return mType->truthy(mU); } Object Object::get(const std::string& key) const { return mType->get(mU, key); } bool Object::has(const std::string& key) const { return mType->has(mU, key); } Object Object::opt(const std::string& key, const Object& def) const { return mType->opt(mU, key, def); } +std::pair Object::keyAt(std::size_t offset) const { return mType->keyAt(mU, offset); } // Methods for ARRAY objects Object Object::at(std::uint64_t index) const { return mType->at(mU, index); } diff --git a/aplcore/src/primitives/pseudolocalizer.cpp b/aplcore/src/primitives/pseudolocalizer.cpp new file mode 100644 index 0000000..644335d --- /dev/null +++ b/aplcore/src/primitives/pseudolocalizer.cpp @@ -0,0 +1,148 @@ +/** + * 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/pseudolocalizer.h" +#include "apl/utils/stringfunctions.h" + +namespace apl { + +const double DEFAULT_EXPANSION_PERCENTAGE = 30; + +// Map of characters to their French Accented versions +static const std::map latinEquivalentMap = { + {'A', "Ȧ"}, {'B', "Ɓ"}, {'C', "Ƈ"}, {'D', "Ḓ"}, {'E', "Ḗ"}, {'F', "Ƒ"}, {'G', "Ɠ"}, {'H', "Ħ"}, + {'I', "Ī"}, {'J', "Ĵ"}, {'K', "Ķ"}, {'L', "Ŀ"}, {'M', "Ḿ"}, {'N', "Ƞ"}, {'O', "Ǿ"}, {'P', "Ƥ"}, + {'Q', "Ɋ"}, {'R', "Ř"}, {'S', "Ş"}, {'T', "Ŧ"}, {'U', "Ŭ"}, {'V', "Ṽ"}, {'W', "Ẇ"}, {'X', "Ẋ"}, + {'Y', "Ẏ"}, {'Z', "Ẑ"}, {'a', "ȧ"}, {'b', "ƀ"}, {'c', "ƈ"}, {'d', "ḓ"}, {'e', "ḗ"}, {'f', "ƒ"}, + {'g', "ɠ"}, {'h', "ħ"}, {'i', "ī"}, {'j', "ĵ"}, {'k', "ķ"}, {'l', "ŀ"}, {'m', "ḿ"}, {'n', "ƞ"}, + {'o', "ǿ"}, {'p', "ƥ"}, {'q', "ɋ"}, {'r', "ř"}, {'s', "ş"}, {'t', "ŧ"}, {'u', "ŭ"}, {'v', "ṽ"}, + {'w', "ẇ"}, {'x', "ẋ"}, {'y', "ẏ"}, {'z', "ẑ"}}; + +std::string +PseudoLocalizationTextTransformer::expandByDuplicatingVowels(const std::string& input, + int maxVowelsToBeAdded) +{ + if (maxVowelsToBeAdded <= 0) { + return input; + } + else { + auto duplicatedVowels = std::string(""); + for (char c : input) { + char lower_c = sutil::tolower(c); + if ((lower_c == 'a' || lower_c == 'e' || lower_c == 'i' || lower_c == 'o' || + lower_c == 'u') && + maxVowelsToBeAdded > 0) { + duplicatedVowels += c; + maxVowelsToBeAdded--; + } + duplicatedVowels += c; + } + return duplicatedVowels; + } +} + +std::string +PseudoLocalizationTextTransformer::addPaddingToMeetExpansion(const std::string& input, + int paddingToBeAdded) +{ + if (paddingToBeAdded <= 0) { + return input; + } + auto desiredTotalLength = input.length() + paddingToBeAdded; + + auto paddedText = std::string(""); + paddedText.reserve(desiredTotalLength); + + // Add padding at the start + for (auto i = 0; i < paddingToBeAdded / 2; ++i) { + paddedText += '-'; + } + + paddedText += input; + + // Add padding at the end + for (auto i = 0; i < paddingToBeAdded - paddingToBeAdded / 2; ++i) { + paddedText += '-'; + } + + return paddedText; +} + +std::string +PseudoLocalizationTextTransformer::expandString(const std::string& input, + double expansionPercentage) +{ + if (input.empty()) { + return input; + } + double expansionFactor; + if (expansionPercentage >= 0 && expansionPercentage <= 100) { + expansionFactor = 1 + expansionPercentage / 100.0; + } + else { + expansionFactor = 1 + DEFAULT_EXPANSION_PERCENTAGE / 100.0; + } + + // Calculate total characters to be added + auto expandedLength = static_cast(input.length() * expansionFactor); + auto charactersToAdd = expandedLength - input.length(); + + // Add characters by duplicating vowels + auto duplicatedVowels = expandByDuplicatingVowels(input, charactersToAdd); + + // In case there is still scope for expansion, add padding + auto paddingToBeAdded = expandedLength - duplicatedVowels.length(); + auto expanded = addPaddingToMeetExpansion(duplicatedVowels, paddingToBeAdded); + + return expanded; +} + +std::string +PseudoLocalizationTextTransformer::addStartAndEndMarkers(const std::string& input) +{ + return "[" + input + "]"; +} + +std::string +PseudoLocalizationTextTransformer::replaceCharsWithAccentedVersions(const std::string& input) +{ + auto diacriticized = std::string(""); + for (char c : input) { + auto it = latinEquivalentMap.find(c); + if (it != latinEquivalentMap.end()) + diacriticized += it->second; + else + diacriticized += c; + } + return diacriticized; +} + +std::string +PseudoLocalizationTextTransformer::transform(const std::string& input, const Object& config) const +{ + if (config.isMap() && config.get("enabled").asBoolean()) { + // Horizontally expand the string. + auto expanded = expandString(input, config.get("expansionPercentage").asNumber()); + // Adding start and end markers. + auto bracketed = addStartAndEndMarkers(expanded); + + // Replace characters with accented latin versions + auto replaced = replaceCharsWithAccentedVersions(bracketed); + return replaced; + } + return input; +} + +} // namespace apl diff --git a/aplcore/src/primitives/styledtext.cpp b/aplcore/src/primitives/styledtext.cpp index 33ba07a..c6b0a6a 100644 --- a/aplcore/src/primitives/styledtext.cpp +++ b/aplcore/src/primitives/styledtext.cpp @@ -21,9 +21,12 @@ #include +#include "apl/content/content.h" +#include "apl/document/coredocumentcontext.h" #include "apl/primitives/color.h" #include "apl/primitives/dimension.h" #include "apl/primitives/objectdata.h" +#include "apl/primitives/pseudolocalizer.h" #include "apl/primitives/styledtextstate.h" #include "apl/primitives/unicode.h" #include "apl/utils/stringfunctions.h" @@ -244,6 +247,25 @@ StyledText::create(const Context& context, const Object& object) if (object.is()) return object.get(); + // Some primitive tests don't have document context added + if (context.documentContext() == nullptr) { + return {context, object.asString()}; + } + + // Get PseudoLocalization settings + auto pseudoLocalizationSettings = CoreDocumentContext::cast(context.documentContext()) + ->content() + ->getDocumentSettings() + ->getValue("pseudoLocalization"); + // Transform text into its Pseudo-localized version. The method returns the same string if the + // PseudoLocalization feature is disabled. + if (pseudoLocalizationSettings.isMap() && + pseudoLocalizationSettings.get("enabled").asBoolean()) { + PseudoLocalizationTextTransformer transformer; + auto transformedText = transformer.transform(object.asString(), pseudoLocalizationSettings); + return {context, transformedText}; + } + return {context, object.asString()}; } @@ -358,69 +380,69 @@ StyledText::Iterator::spanCount() { std::string StyledText::ObjectType::asString(const Object::DataHolder& dataHolder) const { - return get(dataHolder).asString(); + return getReferenced(dataHolder).asString(); } double StyledText::ObjectType::asNumber(const Object::DataHolder& dataHolder) const { - return aplFormattedStringToDouble(get(dataHolder).asString()); + return aplFormattedStringToDouble(getReferenced(dataHolder).asString()); } int StyledText::ObjectType::asInt(const Object::DataHolder& dataHolder, int base) const { - return sutil::stoi(get(dataHolder).asString(), nullptr, base); + return sutil::stoi(getReferenced(dataHolder).asString(), nullptr, base); } int64_t StyledText::ObjectType::asInt64(const Object::DataHolder& dataHolder, int base) const { - return sutil::stoll(get(dataHolder).asString(), nullptr, base); + return sutil::stoll(getReferenced(dataHolder).asString(), nullptr, base); } Color StyledText::ObjectType::asColor(const Object::DataHolder& dataHolder, const apl::SessionPtr& session) const { - return Color(session, get(dataHolder).asString()); + return Color(session, getReferenced(dataHolder).asString()); } Dimension StyledText::ObjectType::asDimension(const Object::DataHolder& dataHolder, const Context& context) const { - return Dimension(context, get(dataHolder).asString()); + return Dimension(context, getReferenced(dataHolder).asString()); } Dimension StyledText::ObjectType::asAbsoluteDimension(const Object::DataHolder& dataHolder, const Context& context) const { - auto d = Dimension(context, get(dataHolder).asString()); + auto d = Dimension(context, getReferenced(dataHolder).asString()); return (d.getType() == DimensionType::Absolute ? d : Dimension(DimensionType::Absolute, 0)); } Dimension StyledText::ObjectType::asNonAutoDimension(const Object::DataHolder& dataHolder, const Context& context) const { - auto d = Dimension(context, get(dataHolder).asString()); + auto d = Dimension(context, getReferenced(dataHolder).asString()); return (d.getType() == DimensionType::Auto ? Dimension(DimensionType::Absolute, 0) : d); } Dimension StyledText::ObjectType::asNonAutoRelativeDimension(const Object::DataHolder& dataHolder, const Context& context) const { - auto d = Dimension(context, get(dataHolder).asString(), true); + auto d = Dimension(context, getReferenced(dataHolder).asString(), true); return (d.getType() == DimensionType::Auto ? Dimension(DimensionType::Relative, 0) : d); } std::uint64_t StyledText::ObjectType::size(const Object::DataHolder& dataHolder) const { - return get(dataHolder).asString().size(); + return getReferenced(dataHolder).asString().size(); } size_t StyledText::ObjectType::hash(const Object::DataHolder& dataHolder) const { - return std::hash{}(get(dataHolder).getRawText()); + return std::hash{}(getReferenced(dataHolder).getRawText()); } } // namespace apl diff --git a/aplcore/src/scenegraph/accessibility.cpp b/aplcore/src/scenegraph/accessibility.cpp index c5b7cf3..2b5773a 100644 --- a/aplcore/src/scenegraph/accessibility.cpp +++ b/aplcore/src/scenegraph/accessibility.cpp @@ -15,6 +15,8 @@ #include "apl/scenegraph/accessibility.h" +#include "apl/primitives/object.h" + namespace apl { namespace sg { @@ -51,6 +53,23 @@ Accessibility::appendAction(const std::string& name, const std::string& label, b mActions.emplace_back(Action{name, label, enabled}); } +bool +Accessibility::setAdjustableValue(const std::string& adjustableValue) { + if (adjustableValue == mAdjustableValue) + return false; + + mAdjustableValue = adjustableValue; + return true; +} + +bool +Accessibility::setAdjustableRange(const apl::Object& object) { + if (object.isNull()) + return false; + mAdjustableRange = AdjustableRange{object.get("minValue").asNumber(), object.get("maxValue").asNumber(), object.get("currentValue").asNumber()}; + return true; +} + rapidjson::Value Accessibility::serialize(rapidjson::Document::AllocatorType& allocator) const { auto out = rapidjson::Value(rapidjson::kObjectType); @@ -66,6 +85,12 @@ Accessibility::serialize(rapidjson::Document::AllocatorType& allocator) const { actionArray.PushBack(actionItem, allocator); } out.AddMember("actions", actionArray, allocator); + auto range = rapidjson::Value(rapidjson::kObjectType); + range.AddMember("minValue", rapidjson::Value(mAdjustableRange.minValue), allocator); + range.AddMember("maxValue", rapidjson::Value(mAdjustableRange.maxValue), allocator); + range.AddMember("currentValue", rapidjson::Value(mAdjustableRange.currentValue), allocator); + out.AddMember("adjustableRange", range, allocator); + out.AddMember("adjustableValue", rapidjson::Value(mAdjustableValue.c_str(), allocator), allocator); return out; } diff --git a/aplcore/src/scenegraph/builder.cpp b/aplcore/src/scenegraph/builder.cpp index 4f6fc6d..e04a158 100644 --- a/aplcore/src/scenegraph/builder.cpp +++ b/aplcore/src/scenegraph/builder.cpp @@ -342,8 +342,10 @@ accessibility(CoreComponent& component) auto label = component.getCalculated(kPropertyAccessibilityLabel).getString(); auto role = component.getCalculated(kPropertyRole).asEnum(); const auto& actions = component.getCalculated(kPropertyAccessibilityActions).getArray(); + auto adjustableRange = component.getCalculated(kPropertyAccessibilityAdjustableRange); + auto adjustableValue = component.getCalculated(kPropertyAccessibilityAdjustableValue).asString(); - if (label.empty() && actions.empty() && role == kRoleNone) + if (label.empty() && actions.empty() && role == kRoleNone && adjustableRange.isNull() && adjustableValue.empty()) return nullptr; auto weak = std::weak_ptr(component.shared_from_corecomponent()); @@ -361,6 +363,9 @@ accessibility(CoreComponent& component) acc->appendAction(ptr->getName(), ptr->getLabel(), ptr->enabled()); } + acc->setAdjustableRange(adjustableRange); + acc->setAdjustableValue(adjustableValue); + return acc; } diff --git a/aplcore/src/touch/gestures/pagerflinggesture.cpp b/aplcore/src/touch/gestures/pagerflinggesture.cpp index 3a34f3b..e0e3771 100644 --- a/aplcore/src/touch/gestures/pagerflinggesture.cpp +++ b/aplcore/src/touch/gestures/pagerflinggesture.cpp @@ -282,7 +282,8 @@ PagerFlingGesture::onUp(const PointerEvent& event, apl_time_t timestamp) if (!FlingGesture::onUp(event, timestamp)) return false; - mAmount = getTranslationAmount(mActionable, getDistance(mActionable, mStartPosition, event.pointerEventPosition)); + auto localPoint = mActionable->toLocalPoint(event.pointerEventPosition); + mAmount = getTranslationAmount(mActionable, getDistance(mActionable, mStartPosition, localPoint)); finishUp(); return true; } diff --git a/aplcore/unit/component/CMakeLists.txt b/aplcore/unit/component/CMakeLists.txt index 93a4892..53fdc49 100644 --- a/aplcore/unit/component/CMakeLists.txt +++ b/aplcore/unit/component/CMakeLists.txt @@ -15,6 +15,7 @@ target_sources_local(unittest PRIVATE unittest_accessibility_actions.cpp unittest_accessibility_api.cpp + unittest_accessibility_properties.cpp unittest_bounds.cpp unittest_component_events.cpp unittest_default_component_size.cpp diff --git a/aplcore/unit/component/unittest_accessibility_properties.cpp b/aplcore/unit/component/unittest_accessibility_properties.cpp new file mode 100644 index 0000000..7c76866 --- /dev/null +++ b/aplcore/unit/component/unittest_accessibility_properties.cpp @@ -0,0 +1,161 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "../testeventloop.h" + +using namespace apl; + +class AccessibilityPropertiesTest : public DocumentWrapper {}; + +static const char *BASIC_TEST = R"apl( + { + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "id": "touch", + "width": 100, + "height": 100, + "role": "adjustable", + "accessibilityAdjustableValue": "50", + "accessibilityAdjustableRange": { + "minValue": 0, + "maxValue": 100, + "currentValue": 50 + } + } + } + } +)apl"; + +TEST_F(AccessibilityPropertiesTest, Basic) +{ + loadDocument(BASIC_TEST); + auto component = root->findComponentById("touch"); + auto jsonData = JsonData(R"({"minValue": 0, "maxValue": 100, "currentValue": 50})"); + ASSERT_EQ("50", component->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + ASSERT_EQ(Object(jsonData.get()), component->getCalculated(kPropertyAccessibilityAdjustableRange)); +} + +static const char *ACCESSIBILITY_ADJUSTABLE_RANGE_PROPERTY_MISSING = R"apl( + { + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "id": "touch", + "width": 100, + "height": 100, + "role": "adjustable", + "accessibilityAdjustableValue": "50" + } + } + } +)apl"; + +TEST_F(AccessibilityPropertiesTest, AccessibilityAdjustableRangePropertyMissing) +{ + loadDocument(ACCESSIBILITY_ADJUSTABLE_RANGE_PROPERTY_MISSING); + auto component = root->findComponentById("touch"); + ASSERT_EQ("50", component->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + ASSERT_EQ(Object::NULL_OBJECT(), component->getCalculated(kPropertyAccessibilityAdjustableRange)); +} + +static const char *ACCESSIBILITY_ADJUSTABLE_RANGE_PROPERTY_INCOMPLETE = R"apl( + { + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "id": "touch", + "width": 100, + "height": 100, + "role": "adjustable", + "accessibilityAdjustableValue": "50", + "accessibilityAdjustableRange": { + "minValue": 0, + "currentValue": 50 + } + } + } + } +)apl"; + +TEST_F(AccessibilityPropertiesTest, AccessibilityAdjustableRangePropertyIncomplete) +{ + loadDocument(ACCESSIBILITY_ADJUSTABLE_RANGE_PROPERTY_INCOMPLETE); + auto component = root->findComponentById("touch"); + ASSERT_EQ("50", component->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + ASSERT_EQ(Object::NULL_OBJECT(), component->getCalculated(kPropertyAccessibilityAdjustableRange)); +} + +static const char *ACCESSIBILITY_ADJUSTABLE_RANGE_PROPERTY_NOT_MAP = R"apl( + { + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "id": "touch", + "width": 100, + "height": 100, + "role": "adjustable", + "accessibilityAdjustableValue": "50", + "accessibilityAdjustableRange": [0, 100] + } + } + } +)apl"; + +TEST_F(AccessibilityPropertiesTest, AccessibilityAdjustableRangePropertyNotMap) +{ + loadDocument(ACCESSIBILITY_ADJUSTABLE_RANGE_PROPERTY_NOT_MAP); + auto component = root->findComponentById("touch"); + ASSERT_EQ("50", component->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + ASSERT_EQ(Object::NULL_OBJECT(), component->getCalculated(kPropertyAccessibilityAdjustableRange)); +} + +static const char *ACCESSIBILITY_ADJUSTABLE_VALUE_PROPERTY_MISSING = R"apl( + { + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "TouchWrapper", + "id": "touch", + "width": 100, + "height": 100, + "role": "adjustable", + "accessibilityAdjustableRange": { + "minValue": 0, + "maxValue": 100, + "currentValue": 50 + } + } + } + } +)apl"; + +TEST_F(AccessibilityPropertiesTest, AccessibilityAdjustableValuePropertyMissing) +{ + loadDocument(ACCESSIBILITY_ADJUSTABLE_VALUE_PROPERTY_MISSING); + auto component = root->findComponentById("touch"); + auto jsonData = JsonData(R"({"minValue": 0, "maxValue": 100, "currentValue": 50})"); + ASSERT_EQ("", component->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + ASSERT_EQ(Object(jsonData.get()), component->getCalculated(kPropertyAccessibilityAdjustableRange)); +} \ No newline at end of file diff --git a/aplcore/unit/component/unittest_component_events.cpp b/aplcore/unit/component/unittest_component_events.cpp index 5e55681..7f4e5c8 100644 --- a/aplcore/unit/component/unittest_component_events.cpp +++ b/aplcore/unit/component/unittest_component_events.cpp @@ -16,11 +16,14 @@ #include "gtest/gtest.h" #include "apl/component/component.h" +#include "apl/component/componenteventsourcewrapper.h" +#include "apl/component/componenteventtargetwrapper.h" #include "apl/engine/event.h" #include "apl/primitives/mediastate.h" #include "../testeventloop.h" + using namespace apl; class ComponentEventsTest : public CommandTest {}; @@ -440,6 +443,7 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ(std::to_string(state.getErrorCode()), text->getCalculated(kPropertyText).asString()); + ASSERT_FALSE(root->screenLock()); /* * Simulate playback error while playing @@ -449,6 +453,8 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ("PLAY", text->getCalculated(kPropertyText).asString()); // Track is now playing + ASSERT_TRUE(root->screenLock()); + // Update state as error state = MediaState(0, 3, 7, 10, false, false, false) .withTrackState(kTrackFailed) @@ -457,12 +463,16 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ(std::to_string(state.getErrorCode()), text->getCalculated(kPropertyText).asString()); + ASSERT_TRUE(root->screenLock()); + // Advance to next track from error state 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()); + ASSERT_TRUE(root->screenLock()); + // Update state as error when playing second track state = MediaState(1, 3, 5, 10, false, false, false) .withTrackState(kTrackFailed) @@ -471,6 +481,8 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ(std::to_string(state.getErrorCode()), text->getCalculated(kPropertyText).asString()); + ASSERT_TRUE(root->screenLock()); + // Update state as error when moving to third track state = MediaState(2, 3, 0, 0, false, false, false) .withTrackState(kTrackFailed) @@ -479,7 +491,17 @@ TEST_F(ComponentEventsTest, MediaErrorStateChanges) CheckMediaState(state, video->getCalculated()); loop->advanceToEnd(); ASSERT_EQ(std::to_string(state.getErrorCode()), text->getCalculated(kPropertyText).asString()); + ASSERT_TRUE(root->screenLock()); + // Mark the state as paused + state = MediaState(2, 3, 0, 0, true, false, false) + .withTrackState(kTrackFailed) + .withErrorCode(101); + video->updateMediaState(state); + CheckMediaState(state, video->getCalculated()); + loop->advanceToEnd(); + ASSERT_EQ("PAUSE", text->getCalculated(kPropertyText).asString()); + ASSERT_FALSE(root->screenLock()); } static const char *TOUCH_WRAPPER_SEND_EVENT = R"( @@ -1005,6 +1027,7 @@ TEST_F(ComponentEventsTest, MediaOnTimeUpdate) auto video = context->findComponentById("video"); ASSERT_TRUE(video); ASSERT_EQ(kComponentTypeVideo, video->getType()); + ASSERT_FALSE(root->screenLock()); // Screen currently unlocked // Simulate "no change" MediaState state; @@ -1014,7 +1037,7 @@ TEST_F(ComponentEventsTest, MediaOnTimeUpdate) ASSERT_FALSE(root->hasEvent()); - // Simulate time update + // Simulate time update. This triggers playing state = MediaState(0, 1, 5, 10, false, false, false); video->updateMediaState(state); CheckMediaState(state, video->getCalculated()); @@ -1036,6 +1059,13 @@ TEST_F(ComponentEventsTest, MediaOnTimeUpdate) ASSERT_EQ(Object(10), arguments.at(7)); // duration ASSERT_EQ(Object(false), arguments.at(8)); // paused ASSERT_EQ(Object(false), arguments.at(9)); // ended + + // We should have a screen lock. + ASSERT_TRUE(root->screenLock()); + // Stop the playback (by putting the media state into pause) + state = MediaState(0, 1, 5, 10, true, false, false); + video->updateMediaState(state); + CheckMediaState(state, video->getCalculated()); } static const char *MEDIA_FAST_NORMAL = R"( @@ -1246,3 +1276,126 @@ TEST_F(ComponentEventsTest, ChildrenChanged) ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 2, "insert")); ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "remove")); } + +/** + * These tests are more intrusive into the ComponentEventWrapperStructure + */ + +static const char *TEXT_COMPONENT = R"( + { + "type": "APL", + "version": "1.0", + "mainTemplate": { + "items": { + "type": "Text", + "text": "Hello" + } + } + } +)"; + +static const std::vector TARGET_KEYS = { + "bind", + "checked", + "color", + "disabled", + "focused", + "height", + "id", + "layoutDirection", + "opacity", + "pressed", + "text", + "type", + "uid", + "width", +}; + +static const std::vector SOURCE_KEYS = { + "bind", + "checked", + "color", + "disabled", + "focused", + "height", + "id", + "layoutDirection", + "opacity", + "pressed", + "text", + "type", + "uid", + "width", + "value", + "handler", + "source" +}; + + +TEST_F(ComponentEventsTest, InnerLogic) +{ + loadDocument(TEXT_COMPONENT); + ASSERT_TRUE(component); + + auto target_context = ComponentEventTargetWrapper::create(component); + // Check possession of keys + ASSERT_EQ(TARGET_KEYS.size(), target_context->size()); + for (auto i = 0 ; i < TARGET_KEYS.size() ; i++) { + ASSERT_TRUE(target_context->has(TARGET_KEYS[i])); + ASSERT_EQ(target_context->keyAt(i).first, TARGET_KEYS[i]); + } + + auto source_context = ComponentEventSourceWrapper::create(component, "Test", 243); + ASSERT_EQ(SOURCE_KEYS.size(), source_context->size()); + for (auto i = 0 ; i < SOURCE_KEYS.size() ; i++) { + ASSERT_TRUE(source_context->has(SOURCE_KEYS[i])); + ASSERT_EQ(source_context->keyAt(i).first, SOURCE_KEYS[i]); + } + ASSERT_TRUE(IsEqual("Test", source_context->opt("handler", 23))); + ASSERT_TRUE(IsEqual(243, source_context->opt("value", 20000))); + ASSERT_TRUE(IsEqual("Text", source_context->opt("source", "Error"))); + ASSERT_TRUE(IsEqual("Fuzzy", source_context->opt("MissingProperty", "Fuzzy"))); +} + +static const char *BINDING_CONTEXT = R"apl( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "bind": { + "name": "FOO", + "value": "BAR" + }, + "text": "Bind is ${BAR}" + } + } +} +)apl"; + +TEST_F(ComponentEventsTest, BindingContext) +{ + loadDocument(BINDING_CONTEXT); + ASSERT_TRUE(component); + advanceTime(123); // Move time forward + + auto target_context = ComponentEventTargetWrapper::create(component); + ASSERT_TRUE(target_context->has("bind")); + + auto bindings = target_context->get("bind"); + ASSERT_EQ(0, bindings.size()); // The context bindings are hidden from the component event + + // "elapsedTime" is a global that we can read from the context + // Because ContextWrapper is a "Map-Like" object, you can't read the size of the bindings + // or directly return a map or list of keys, but you can _get_ a value out of it. + auto et = component->getContext()->find("elapsedTime"); + ASSERT_FALSE(et.empty()); + ASSERT_TRUE(IsEqual(123, et.object().value())); + + ASSERT_TRUE(bindings.has("elapsedTime")); + ASSERT_TRUE(IsEqual(et.object().value(), bindings.get("elapsedTime"))); + + ASSERT_FALSE(bindings.has("MissingProperty")); + ASSERT_TRUE(IsEqual("FOO", bindings.opt("MissingProperty", "FOO"))); +} \ No newline at end of file diff --git a/aplcore/unit/component/unittest_dynamic_properties.cpp b/aplcore/unit/component/unittest_dynamic_properties.cpp index ee06162..094574f 100644 --- a/aplcore/unit/component/unittest_dynamic_properties.cpp +++ b/aplcore/unit/component/unittest_dynamic_properties.cpp @@ -1485,4 +1485,96 @@ TEST_F(DynamicPropertiesTest, SequenceDynamic) { ASSERT_EQ(Rect(0, 0, 100, 20), child0->getCalculated(kPropertyBounds).get()); ASSERT_EQ(Rect(0, 40, 100, 20), child1->getCalculated(kPropertyBounds).get()); ASSERT_EQ(Rect(0, 70, 100, 20), child2->getCalculated(kPropertyBounds).get()); +} + +static const char *ACCESSIBILITY_SETVALUE = R"apl( + { + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": [{ + "type": "TouchWrapper", + "role": "adjustable", + "id": "touch", + "accessibilityAdjustableRange": { + "minValue": "${minRange}", + "maxValue": "${maxRange}", + "currentValue": "${sliderValue}" + }, + "accessibilityAdjustableValue": "${sliderValue}", + "bind": [ + { + "name": "sliderValue", + "value": 4 + }, + { + "name": "minRange", + "value": 0 + }, + { + "name": "maxRange", + "value": 10 + } + ], + "actions": [ + { + "name": "increment", + "label": "Increment Value", + "command": { + "type": "SetValue", + "property": "sliderValue", + "value": "${Math.min(sliderValue + 1, maxRange)}" + } + }, + { + "name": "decrement", + "label": "Decrement Value", + "command": { + "type": "SetValue", + "property": "sliderValue", + "value": "${Math.max(sliderValue - 1, minRange)}" + } + } + ] + }] + } + } +)apl"; + +// Test for touchable component accessibility properties for dynamic +TEST_F(DynamicPropertiesTest, AccessibilityProperties) { + loadDocument(ACCESSIBILITY_SETVALUE); + ASSERT_TRUE(component); + + auto touchwrapper = CoreComponent::cast(context->findComponentById("touch")); + ASSERT_TRUE(touchwrapper); + ASSERT_EQ(kComponentTypeTouchWrapper, touchwrapper->getType()); + //Verify that the correct number of bindings are present + ASSERT_EQ(2, touchwrapper->getContext()->countDownstream("sliderValue")); + ASSERT_EQ(1, touchwrapper->getContext()->countDownstream("minRange")); + ASSERT_EQ(1, touchwrapper->getContext()->countDownstream("maxRange")); + + // Verify that the "increment" action works + component->update(kUpdateAccessibilityAction, "increment"); + root->clearPending(); + ASSERT_EQ(1, root->getDirty().size()); + ASSERT_TRUE(CheckDirty(touchwrapper, kPropertyAccessibilityAdjustableValue, kPropertyAccessibilityAdjustableRange)); + ASSERT_TRUE(CheckDirty(root, touchwrapper)); + root->clearDirty(); + + ASSERT_EQ("5", touchwrapper->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + auto incrementJsonData = JsonData(R"({"minValue": 0, "maxValue": 10, "currentValue": 5})"); + ASSERT_EQ(Object(incrementJsonData.get()), touchwrapper->getCalculated(kPropertyAccessibilityAdjustableRange)); + + // Verify that the "decrement" action works + component->update(kUpdateAccessibilityAction, "decrement"); + root->clearPending(); + ASSERT_EQ(1, root->getDirty().size()); + ASSERT_TRUE(CheckDirty(touchwrapper, kPropertyAccessibilityAdjustableValue, kPropertyAccessibilityAdjustableRange)); + ASSERT_TRUE(CheckDirty(root, touchwrapper)); + root->clearDirty(); + + ASSERT_EQ("4", touchwrapper->getCalculated(kPropertyAccessibilityAdjustableValue).asString()); + auto decrementJsonData = JsonData(R"({"minValue": 0, "maxValue": 10, "currentValue": 4})"); + ASSERT_EQ(Object(decrementJsonData.get()), touchwrapper->getCalculated(kPropertyAccessibilityAdjustableRange)); } \ No newline at end of file diff --git a/aplcore/unit/component/unittest_scroll.cpp b/aplcore/unit/component/unittest_scroll.cpp index c0e8fc2..519ae04 100644 --- a/aplcore/unit/component/unittest_scroll.cpp +++ b/aplcore/unit/component/unittest_scroll.cpp @@ -3768,4 +3768,97 @@ TEST_F(ScrollTest, ScrollToComponentInstant) scrollToComponent(map["frame4"], kCommandScrollAlignFirst, 0); ASSERT_EQ(Point(0,600), component->scrollPosition()); +} + +static const char *LIVE_SCROLL_RAINBOWS = R"({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Container", + "data": "${rainbows}", + "item": { + "type": "Sequence", + "id": "Rainbow${data}", + "width": 200, + "height": 200, + "data": ["red", "orange", "yellow", "green", "blue", "indigo", "violet"], + "items": [ + { + "type": "Frame", + "id": "${data}${index}", + "backgroundColor": "${data}", + "width": 200, + "height": 50 + } + ] + } + } + } +} +)"; + +TEST_F(ScrollTest, ClearLiveDataDuringChildScrollCommand) +{ + auto liveArray = LiveArray::create({"One", "Two"}); + config->liveData("rainbows", liveArray); + loadDocument(LIVE_SCROLL_RAINBOWS); + advanceTime(100); + + ASSERT_EQ(2, component->getChildCount()); + + auto rainbowOne = component->getCoreChildAt(0); + ASSERT_EQ("RainbowOne", rainbowOne->getId()); + ASSERT_EQ(Point(), rainbowOne->scrollPosition()); + + auto rainbowTwo = component->getCoreChildAt(1); + ASSERT_EQ("RainbowTwo", rainbowTwo->getId()); + ASSERT_EQ(Point(), rainbowTwo->scrollPosition()); + + // Initiate scroll down by 100 over a duration of 1 second + executeScroll("RainbowOne", 100, 1000); + + // Move forward by 0.5 seconds, and we'll be partway there + advanceTime(500); + ASSERT_NE(Point(), rainbowOne->scrollPosition()); + ASSERT_EQ(Point(), rainbowTwo->scrollPosition()); + + // Clear live data + liveArray->clear(); + advanceTime(10); + + // Rainbows are gone + ASSERT_EQ(0, component->getChildCount()); +} + +TEST_F(ScrollTest, ClearLiveDataDuringChildScrollGesture) +{ + auto liveArray = LiveArray::create({"One", "Two"}); + config->liveData("rainbows", liveArray); + loadDocument(LIVE_SCROLL_RAINBOWS); + advanceTime(100); + + ASSERT_EQ(2, component->getChildCount()); + + auto rainbowOne = component->getCoreChildAt(0); + ASSERT_EQ("RainbowOne", rainbowOne->getId()); + ASSERT_EQ(Point(), rainbowOne->scrollPosition()); + + auto rainbowTwo = component->getCoreChildAt(1); + ASSERT_EQ("RainbowTwo", rainbowTwo->getId()); + ASSERT_EQ(Point(), rainbowTwo->scrollPosition()); + + // Scroll down (rainbow #2) by 100, but don't release finger + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(100, 300), false)); + advanceTime(100); + ASSERT_EQ(Point(), rainbowOne->scrollPosition()); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(100, 200), true)); + ASSERT_EQ(Point(0, 100), rainbowTwo->scrollPosition()); + + // Clear live data + liveArray->clear(); + advanceTime(10); + + // Rainbows are gone + ASSERT_EQ(0, component->getChildCount()); } \ No newline at end of file diff --git a/aplcore/unit/component/unittest_serialize.cpp b/aplcore/unit/component/unittest_serialize.cpp index ac14a51..96948a0 100644 --- a/aplcore/unit/component/unittest_serialize.cpp +++ b/aplcore/unit/component/unittest_serialize.cpp @@ -438,6 +438,7 @@ const static char *SERIALIZE_ALL_RESULT = R"({ "fontStyle": "normal", "fontWeight": "normal", "handleTick": [], + "handleVisibilityChange": [], "height": "50%", "_innerBounds": [ 0, diff --git a/aplcore/unit/component/unittest_text_component.cpp b/aplcore/unit/component/unittest_text_component.cpp index 34f64b1..302ce03 100644 --- a/aplcore/unit/component/unittest_text_component.cpp +++ b/aplcore/unit/component/unittest_text_component.cpp @@ -420,3 +420,166 @@ TEST_F(TextComponentTest, ParametersChangeMeasurement) ASSERT_EQ(3, ctm->measures); } + +const char *TEXT_HORIZONTAL_GROWTH = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "items": [ + { + "type": "Container", + "width": "100%", + "height": "100%", + "items": [ + { + "type": "Sequence", + "scrollDirection": "horizontal", + "width": 500, + "height": 100, + "items": [ + { + "type": "Text", + "width": "auto", + "height": "auto", + "maxLines": 1, + "text": "Alexa can show you even more – With a 15.6” Full HD (1080p) smart display and 5 MP camera, family organization and entertainment will look brilliant. You can choose portrait or landscape orientation.", + "onMount": [ + { + "type": "SetValue", + "componentId": "result", + "property": "TextLength", + "value": "${event.source.width}" + } + ] + } + ] + }, + { + "type": "Text", + "id": "result", + "height": "100%", + "bind": [ + { + "name": "TextLength", + "value": 0 + } + ], + "text": "Text length: ${TextLength}" + } + ] + } + ] + } +})"; + +TEST_F(TextComponentTest, TextHorizontalGrowth) +{ + auto ctm = std::make_shared(); + config->measure(ctm); + loadDocument(TEXT_HORIZONTAL_GROWTH); + advanceTime(10); + + ASSERT_EQ("Text length: 2030", component->getCoreChildAt(1)->getCalculated(apl::kPropertyText).asString()); +} + +static const char* PSEUDO_LOCALIZATION_ENABLED_DOC = R"({ +"type": "APL", +"version": "2023.2", +"theme": "dark", +"settings": { + "pseudoLocalization": { + "enabled": true, + "expansionPercentage": 40 + } +}, +"mainTemplate": { + "items": { + "type": "Text", + "text": "Hello World" + } + } +} +})"; + +TEST_F(TextComponentTest, ValidPseudoLocalizationSettingsTest_Enabled) { + + loadDocument(PSEUDO_LOCALIZATION_ENABLED_DOC); + + ASSERT_TRUE(component); + ASSERT_EQ("[Ħḗḗŀŀǿǿ Ẇǿǿřŀḓ-]", component->getCalculated(apl::kPropertyText).asString()); +} + +static const char* PSEUDO_LOCALIZATION_ENABLED_DOC_ODD_STRING = R"({ +"type": "APL", +"version": "2023.2", +"theme": "dark", +"settings": { + "pseudoLocalization": { + "enabled": true, + "expansionPercentage": 40 + } +}, +"mainTemplate": { + "items": { + "type": "Text", + "text": "Marks & Spencer" + } + } +} +})"; + +TEST_F(TextComponentTest, ValidPseudoLocalizationSettingsOddStringTest_Enabled) { + + loadDocument(PSEUDO_LOCALIZATION_ENABLED_DOC_ODD_STRING); + + ASSERT_TRUE(component); + ASSERT_EQ("[-Ḿȧȧřķş &ȧȧḿƥ; Şƥḗḗƞƈḗḗř--]", + component->getCalculated(apl::kPropertyText).asString()); +} + +static const char* PSEUDO_LOCALIZATION_PL_SETTINGS_EMPTY_DOC = R"({ +"type": "APL", +"version": "2023.2", +"theme": "dark", +"settings": { + "pseudoLocalization": { + } +}, +"mainTemplate": { + "items": { + "type": "Text", + "text": "Hello World" + } + } +} +})"; + +TEST_F(TextComponentTest, EmptyEnabledPseudoLocalizationSettingTest_Disabled) { + + loadDocument(PSEUDO_LOCALIZATION_PL_SETTINGS_EMPTY_DOC); + + ASSERT_TRUE(component); + ASSERT_EQ("Hello World", component->getCalculated(apl::kPropertyText).asString()); +} + +static const char* PSEUDO_LOCALIZATION_EMPTY_SETTINGS_DOC = R"({ +"type": "APL", +"version": "2023.2", +"theme": "dark", +"mainTemplate": { + "items": { + "type": "Text", + "text": "Hello World" + } + } +} +})"; + +TEST_F(TextComponentTest, EmptySettings_Disabled) { + + loadDocument(PSEUDO_LOCALIZATION_EMPTY_SETTINGS_DOC); + + ASSERT_TRUE(component); + ASSERT_EQ("Hello World", component->getCalculated(apl::kPropertyText).asString()); +} \ No newline at end of file diff --git a/aplcore/unit/content/unittest_document.cpp b/aplcore/unit/content/unittest_document.cpp index 9e633f3..59b68b0 100644 --- a/aplcore/unit/content/unittest_document.cpp +++ b/aplcore/unit/content/unittest_document.cpp @@ -1478,4 +1478,17 @@ TEST(DocumentTest, RepeatedImportDifferentSources) auto expected = std::vector{ "B:1.0" }; ASSERT_EQ(expected, doc->getLoadedPackageNames()); +} + +TEST(DocumentTest, DocumentContent) +{ + auto content = Content::create(BASIC_DOC_WITHOUT_SETTINGS, makeDefaultSession()); + + ASSERT_TRUE(content->isReady()); + + auto m = Metrics().size(1024,800).theme("dark"); + auto rc = RootContext::create(m, content); + + ASSERT_TRUE(rc); + ASSERT_EQ(rc->topDocument()->content(), content); } \ No newline at end of file diff --git a/aplcore/unit/content/unittest_packages.cpp b/aplcore/unit/content/unittest_packages.cpp index 472429b..845b9d3 100644 --- a/aplcore/unit/content/unittest_packages.cpp +++ b/aplcore/unit/content/unittest_packages.cpp @@ -773,6 +773,171 @@ TEST_F(PackagesTest, RefreshUsesStashedPackages) ASSERT_EQ(0x0101ffff, component->getCalculated(apl::kPropertyBorderColor).getColor()); } +static const char *MAIN_RED_GREEN_BLUE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "red", + "version": "1.0" + }, + { + "name": "blue", + "version": "1.0", + "when": "${!environment.bluegreen}" + }, + { + "name": "bluegreen", + "version": "1.0", + "when": "${environment.bluegreen}" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "borderColor": "@MyBlue", + "backgroundColor": "@MyRed" + } + } +})apl"; + +static const char *BLUEGREEN = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "blue", + "version": "1.0" + } + ], + "resources": [ + { + "colors": { + "MyGreen": "#01ff01ff" + } + } + ] +})apl"; + +TEST_F(PackagesTest, RefreshUsesStashedPackagesForNewImportDependency) +{ + add("red:1.0", BASIC); + add("blue:1.0", BLUE); + + content = Content::create(MAIN_RED_GREEN_BLUE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + config->setEnvironmentValue("bluegreen", true); + content->refresh(metrics, *config); + + // Reset the package manager to ensure we rely on stashed "blue" and "red" + reset(); + // Reprocessing is needed to add the "bluegreen" import, which depends on "blue" + add("bluegreen:1.0", BLUEGREEN); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0xff0101ff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); + ASSERT_EQ(0x0101ffff, component->getCalculated(apl::kPropertyBorderColor).getColor()); +} + +static const char *MAIN_DEEP_BLUE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "red", + "version": "1.0" + }, + { + "name": "blue", + "version": "1.0" + } + ], + "mainTemplate": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyDeepBlue" + } + } +})apl"; + +static const char *CONDITITIONAL_BLUE = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "deepblue", + "version": "1.0", + "when": "${environment.foo}" + } + ], + "resources": [ + { + "colors": { + "MyBlue": "#0101ffff" + } + } + ] +})apl"; + +static const char *DEEPBLUE = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyDeepBlue": "#0000ffff" + } + } + ] +})apl"; + +TEST_F(PackagesTest, StashedPackageCanPullInNewDependency) +{ + add("red:1.0", BASIC); + add("blue:1.0", CONDITITIONAL_BLUE); + + content = Content::create(MAIN_DEEP_BLUE, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + config->setEnvironmentValue("foo", true); + content->refresh(metrics, *config); + + // Reset the package manager to ensure we rely on stashed "blue" and "red" + reset(); + // Existing stashed "blue" will suddenly need "deepblue" + add("deepblue:1.0", DEEPBLUE); + ASSERT_TRUE(content->isWaiting()); + ASSERT_TRUE(process(content)); + ASSERT_TRUE(content->isReady()); + + auto root = RootContext::create(metrics, content, *config); + ASSERT_TRUE(root); + + rootDocument = root->topDocument(); + component = CoreComponent::cast(root->topComponent()); + + ASSERT_EQ(0x0000ffff, component->getCalculated(apl::kPropertyBackgroundColor).getColor()); +} + TEST_F(PackagesTest, ChangeConfigAfterContentInitializationReused) { add("StyledFrame:1.0", STYLED_FRAME_OVERRIDE_DEPENDS); diff --git a/aplcore/unit/datagrammar/CMakeLists.txt b/aplcore/unit/datagrammar/CMakeLists.txt index a6c1f39..c619502 100644 --- a/aplcore/unit/datagrammar/CMakeLists.txt +++ b/aplcore/unit/datagrammar/CMakeLists.txt @@ -17,6 +17,7 @@ target_sources_local(unittest unittest_decompile.cpp unittest_grammar.cpp unittest_grammar_error.cpp + unittest_grammar_map.cpp unittest_optimize.cpp unittest_parse.cpp ) \ No newline at end of file diff --git a/aplcore/unit/datagrammar/unittest_grammar_map.cpp b/aplcore/unit/datagrammar/unittest_grammar_map.cpp new file mode 100644 index 0000000..f653160 --- /dev/null +++ b/aplcore/unit/datagrammar/unittest_grammar_map.cpp @@ -0,0 +1,277 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#include "../testeventloop.h" + +using namespace apl; + +class MapGrammarTest : public DocumentWrapper {}; + + +static auto MAP_TESTS = std::vector> { + {"Map.keys()", {}}, + {"Map.keys(TEST)", {"a", "b", "c"}}, +}; + +TEST_F(MapGrammarTest, MapFunctions) +{ + auto c = Context::createTestContext(Metrics(), RootConfig()); + auto map = std::make_shared(); + map->emplace("a", "adventure"); + map->emplace("b", "beauty"); + map->emplace("c", "culture"); + auto obj = Object(map); + + c->putConstant("TEST", obj); + + for (const auto& m : MAP_TESTS) { + auto len = evaluate(*c, "${" + m.first + ".length}"); + ASSERT_TRUE(IsEqual(m.second.size(), len)) << m.first << " LENGTH " << len.toDebugString(); + + for (int i = 0 ; i < m.second.size() ; i++ ) { + auto valueAt = evaluate(*c, "${" + m.first + "[" + std::to_string(i) + "]}"); + ASSERT_TRUE(IsEqual(m.second.at(i), valueAt)) << m.first << " INDEX=" << i << valueAt.toDebugString(); + } + } +} + +static const char *JSON_MAP = R"apl({ + "a": "adventure", + "b": "beauty", + "c": "culture" +})apl"; + +TEST_F(MapGrammarTest, MapFunctionsWithJSONDocument) +{ + auto c = Context::createTestContext(Metrics(), RootConfig()); + + rapidjson::Document doc; + doc.Parse(JSON_MAP); + c->putConstant("TEST", Object(std::move(doc))); // This adds a constant which is the rapidjson Document + + for (const auto& m : MAP_TESTS) { + auto len = evaluate(*c, "${" + m.first + ".length}"); + ASSERT_TRUE(IsEqual(m.second.size(), len)) << m.first << " LENGTH " << len.toDebugString(); + + for (int i = 0 ; i < m.second.size() ; i++ ) { + auto valueAt = evaluate(*c, "${" + m.first + "[" + std::to_string(i) + "]}"); + ASSERT_TRUE(IsEqual(m.second.at(i), valueAt)) << m.first << " INDEX=" << i << valueAt.toDebugString(); + } + } +} + + +static const char *DEEP_JSON_MAP = R"apl( +{ + "TEST": { + "a": "adventure", + "b": "beauty", + "c": "culture" + } +})apl"; + + +TEST_F(MapGrammarTest, MapFunctionsWithJSONValue) +{ + auto c = Context::createTestContext(Metrics(), RootConfig()); + + rapidjson::Document doc; + doc.Parse(DEEP_JSON_MAP); + ASSERT_TRUE(doc.IsObject()); + auto itr = doc.FindMember("TEST"); + ASSERT_TRUE(itr != doc.MemberEnd()); + c->putConstant("TEST", itr->value); // This adds a constant from the rapidjson::Value + + for (const auto& m : MAP_TESTS) { + auto len = evaluate(*c, "${" + m.first + ".length}"); + ASSERT_TRUE(IsEqual(m.second.size(), len)) << m.first << " LENGTH " << len.toDebugString(); + + for (int i = 0 ; i < m.second.size() ; i++ ) { + auto valueAt = evaluate(*c, "${" + m.first + "[" + std::to_string(i) + "]}"); + ASSERT_TRUE(IsEqual(m.second.at(i), valueAt)) << m.first << " INDEX=" << i << valueAt.toDebugString(); + } + } +} + + +static const char *COMPONENT_SOURCE_EVENT_KEYS = R"apl({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "onPress": { + "type": "SendEvent", + "arguments": "${Map.keys(event.source)}" + } + } + } +})apl"; + +// The ComponentEventWrapper is exposed to the APL document in event.source +TEST_F(MapGrammarTest, SourceEventKeys) +{ + loadDocument(COMPONENT_SOURCE_EVENT_KEYS); + ASSERT_TRUE(component); + + performTap(0,0); + ASSERT_TRUE(CheckSendEvent(root, "bind", + "checked", + "disabled", + "focused", + "height", + "id", + "layoutDirection", + "opacity", + "pressed", + "type", + "uid", + "width", + // These values are added by the ComponentSourceEventWrapper + "value", + "handler", + "source")); +} + + +static const char *COMPONENT_TARGET_EVENT_KEYS = R"apl({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "onPress": { + "type": "SendEvent", + "arguments": "${Map.keys(event.target)}" + } + } + } +})apl"; + +// The ComponentEventWrapper is exposed to the APL document in event.source +TEST_F(MapGrammarTest, TargetEventKeys) +{ + loadDocument(COMPONENT_TARGET_EVENT_KEYS); + ASSERT_TRUE(component); + + performTap(0,0); + ASSERT_TRUE(CheckSendEvent(root, "bind", + "checked", + "disabled", + "focused", + "height", + "id", + "layoutDirection", + "opacity", + "pressed", + "type", + "uid", + "width")); +} + + +static const char *COMPONENT_ON_SCROLL_SOURCE_EVENT_KEYS = R"apl({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "ScrollView", + "items": { + "type": "Frame", + "height": 10000 + }, + "onScroll": { + "type": "SendEvent", + "sequencer": "S", + "arguments": "${Map.keys(event.source)}" + } + } + } +})apl"; + +TEST_F(MapGrammarTest, OnScrollSourceEventKeys) +{ + loadDocument(COMPONENT_ON_SCROLL_SOURCE_EVENT_KEYS); + ASSERT_TRUE(component); + + executeCommand("Scroll", {{"componentId", ":root"}, {"distance", 1}}, false); + advanceTime(300); + + ASSERT_TRUE(CheckSendEvent(root, "bind", + "checked", + "disabled", + "focused", + "height", + "id", + "layoutDirection", + "opacity", + "position", // This property is added by the ScrollableComponent + "pressed", + "type", + "uid", + "width", + // Values after this point are added by the ComponentSourceEventWrapper + "value", + "handler", + "source")); +} + + +static const char *COMPONENT_ON_SCROLL_TARGET_EVENT_KEYS = R"apl({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "ScrollView", + "items": { + "type": "Frame", + "height": 10000 + }, + "onScroll": { + "type": "SendEvent", + "sequencer": "S", + "arguments": "${Map.keys(event.target)}" + } + } + } +})apl"; + +TEST_F(MapGrammarTest, OnScrollTargetEventKeys) +{ + loadDocument(COMPONENT_ON_SCROLL_TARGET_EVENT_KEYS); + ASSERT_TRUE(component); + + executeCommand("Scroll", {{"componentId", ":root"}, {"distance", 1}}, false); + advanceTime(300); + + ASSERT_TRUE(CheckSendEvent(root, "bind", + "checked", + "disabled", + "focused", + "height", + "id", + "layoutDirection", + "opacity", + "position", // This property is added by the ScrollableComponent + "pressed", + "type", + "uid", + "width")); +} \ No newline at end of file diff --git a/aplcore/unit/engine/CMakeLists.txt b/aplcore/unit/engine/CMakeLists.txt index 0384801..e2103bd 100644 --- a/aplcore/unit/engine/CMakeLists.txt +++ b/aplcore/unit/engine/CMakeLists.txt @@ -36,5 +36,6 @@ target_sources_local(unittest unittest_propdef.cpp unittest_resources.cpp unittest_styles.cpp + unittest_visibility.cpp unittest_viewhost.cpp ) diff --git a/aplcore/unit/engine/unittest_builder_bind.cpp b/aplcore/unit/engine/unittest_builder_bind.cpp index d4d7d8e..6494c05 100644 --- a/aplcore/unit/engine/unittest_builder_bind.cpp +++ b/aplcore/unit/engine/unittest_builder_bind.cpp @@ -333,4 +333,453 @@ TEST_F(BuilderBindTest, MissingValue) auto context = component->getContext(); ASSERT_FALSE(context->hasLocal("NAME")); ASSERT_TRUE(ConsoleMessage()); -} \ No newline at end of file +} + +static const char *ON_CHANGE = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "TARGET", + "bind": { + "name": "NAME", + "value": 1, + "onChange": { + "type": "SendEvent", + "arguments": [ + "${event.source.handler}", + "${event.current}", + "${event.previous}", + "${event.source.bind.NAME}" + ], + "sequencer": "FOO" + } + } + } + } +} +)apl"; + +/** + * Simple test for onChange. The handler name, current value, previous value, and event.source binding are checked. + */ +TEST_F(BuilderBindTest, OnChange) +{ + loadDocument(ON_CHANGE); + ASSERT_TRUE(component); + auto context = component->getContext(); + ASSERT_TRUE(context->hasLocal("NAME")); + ASSERT_FALSE(ConsoleMessage()); + + executeCommand("SetValue", {{"property", "NAME"}, {"componentId", "TARGET"}, {"value", 2}}, true); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, "Change", 2, 1, 2)); + + executeCommand("SetValue", {{"property", "NAME"}, {"componentId", "TARGET"}, {"value", 12}}, true); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, "Change", 12, 2, 12)); +} + +static const char *ON_CHANGE_ARRAY = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "TARGET", + "bind": { + "name": "NAME", + "value": [1,2,3], + "onChange": { + "type": "SendEvent", + "arguments": [ + "${event.current[0]}", + "${event.previous[0]}", + "${event.current[1]}", + "${event.previous[1]}", + "${event.current.length}" + ], + "sequencer": "FOO" + } + } + } + } +} +)apl"; + +/** + * Start with a bound array and assign new values to it. The "onChange" handler + * should be called unless the two arrays are equal. + */ +TEST_F(BuilderBindTest, OnChangeArray) +{ + loadDocument(ON_CHANGE_ARRAY); + ASSERT_TRUE(component); + auto context = component->getContext(); + ASSERT_TRUE(context->hasLocal("NAME")); + ASSERT_FALSE(ConsoleMessage()); + + // Assign a new array + executeCommand("SetValue", {{"property", "NAME"}, + {"componentId", "TARGET"}, + {"value", ObjectArray{10, 2, 3, 4}}}, true); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, 10, 1, 2, 2, 4)); + + // Assign an array with the same values + executeCommand("SetValue", {{"property", "NAME"}, + {"componentId", "TARGET"}, + {"value", ObjectArray{10, 2, 3, 4}}}, true); + root->clearPending(); + ASSERT_FALSE(root->hasEvent()); + + // Change to something that is not an array + executeCommand("SetValue", {{"property", "NAME"}, + {"componentId", "TARGET"}, + {"value", "fred"}}, true); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, Object::NULL_OBJECT(), 10, Object::NULL_OBJECT(), 2, Object::NULL_OBJECT())); +} + + +static const char *ON_CHANGE_OBJECT = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "TARGET", + "bind": { + "name": "NAME", + "value": {"A": 1, "B": 2}, + "onChange": { + "type": "SendEvent", + "arguments": [ + "${event.current['A']}", + "${event.previous['A']}", + "${event.current['B']}", + "${event.previous['B']}" + ], + "sequencer": "FOO" + } + } + } + } +} +)apl"; + +/** + * Start with a bound Map and assign new values to it. The "onChange" handler + * should be invoked if the two maps are not equal. + */ +TEST_F(BuilderBindTest, OnChangeObject) +{ + loadDocument(ON_CHANGE_OBJECT); + ASSERT_TRUE(component); + auto context = component->getContext(); + ASSERT_TRUE(context->hasLocal("NAME")); + ASSERT_FALSE(ConsoleMessage()); + + // Assign a new object with the same keys + executeCommand("SetValue", {{"property", "NAME"}, + {"componentId", "TARGET"}, + {"value", std::make_shared(ObjectMap{{"A", 10}, {"B", 20}})}}, true); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, 10, 1, 20, 2)); + + // Assign the object with the same values + executeCommand("SetValue", {{"property", "NAME"}, + {"componentId", "TARGET"}, + {"value", std::make_shared(ObjectMap{{"A", 10}, {"B", 20}})}}, true); + root->clearPending(); + ASSERT_FALSE(root->hasEvent()); + + // Change to something that is not an object + executeCommand("SetValue", {{"property", "NAME"}, + {"componentId", "TARGET"}, + {"value", 2}}, true); + root->clearPending(); + ASSERT_TRUE(CheckSendEvent(root, Object::NULL_OBJECT(), 10, Object::NULL_OBJECT(), 20)); +} + + +static const char *ON_CHANGE_RECURSIVE = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "TARGET", + "text": "${NAME}", + "bind": { + "name": "NAME", + "value": 10, + "onChange": [ + { + "type": "SetValue", + "property": "NAME", + "value": "${NAME + 1}" + } + ] + } + } + } +} +)apl"; + +/** + * Test a recursive call - that is, changing the value of a bound property causes it + * to change itself again. We avoid an infinite loop by preventing the "onChange" handler + * from being called recursively. + */ +TEST_F(BuilderBindTest, OnChangeRecursive) +{ + loadDocument(ON_CHANGE_RECURSIVE); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(component->getCalculated(kPropertyText).asString(), "10")); + + // Set the value of NAME. This should + // 1. Change the value of NAME from 10 to 1. + // 2. Call NAME's "onChange" handler + // 3. Set the value of NAME to NAME+1 (2). + // 4. Call NAME's "onChange" handler, which refuses to run because the call in step #2 hasn't returned yet. + executeCommand("SetValue", {{"property", "NAME"}, {"componentId", "TARGET"}, {"value", 1}}, true); + root->clearPending(); + ASSERT_TRUE(IsEqual(component->getCalculated(kPropertyText).asString(), "2")); +} + +static const char *ON_CHANGE_RECURSIVE_TWO = R"apl( +{ + "type": "APL", + "version": "2022.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "TARGET", + "bind": [ + { + "name": "A", + "value": 1, + "onChange": { + "type": "SetValue", + "property": "B", + "value": "${A + 1}" + } + }, + { + "name": "B", + "value": 2, + "onChange": { + "type": "SetValue", + "property": "A", + "value": "${B + 1}" + } + } + ], + "text": "A=${A} B=${B}" + } + } +} +)apl"; + +/** + * Test the recursion block with two variables. + */ +TEST_F(BuilderBindTest, OnChangeRecursiveTwo) +{ + loadDocument(ON_CHANGE_RECURSIVE_TWO); + ASSERT_TRUE(component); + ASSERT_TRUE(IsEqual(component->getCalculated(kPropertyText).asString(), "A=1 B=2")); + + // Set the value of A. This should: + // 1. Change the value of A to 10 + // 2. Call A's "onChange" handler + // 3. Set the value of B to 11 + // 4. Call B's "onChange" handler + // 5. Set the value of A to 12 + // 6. Call A's "onChange" handler which refuses to run because step #2 hasn't finished + executeCommand("SetValue", {{"property", "A"}, {"componentId", "TARGET"}, {"value", 10}}, true); + root->clearPending(); + ASSERT_TRUE(IsEqual(component->getCalculated(kPropertyText).asString(), "A=12 B=11")); + + // Set the value of B. This should: + // 1. Change the value of B to 20 + // 2. Call B's "onChange" handler + // 3. Set the value of A to 21 + // 4. Call A's "onChange" handler + // 5. Set the value of B to 22 + // 6. Call B's "onChange" handler which refuses to run because step #2 hasn't finished + executeCommand("SetValue", {{"property", "B"}, {"componentId", "TARGET"}, {"value", 20}}, true); + root->clearPending(); + ASSERT_TRUE(IsEqual(component->getCalculated(kPropertyText).asString(), "A=21 B=22")); +} + +static const char *ON_CHANGE_LIVE_ARRAY = R"apl( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Text", + "bind": [ + { + "name": "COUNTER", + "value": 0 + }, + { + "name": "DATA", + "value": "${data}", + "onChange": { + "type": "SetValue", + "property": "COUNTER", + "value": "${COUNTER + 1}" + } + } + ], + "text": "${data} ${COUNTER}" + }, + "data": "${TestArray}" + } + } +} +)apl"; + +/** + * Hook up a live array. + */ +TEST_F(BuilderBindTest, OnChangeLiveArray) +{ + auto myArray = LiveArray::create({"A", "B", "C"}); + config->liveData("TestArray", myArray); + + loadDocument(ON_CHANGE_LIVE_ARRAY); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + auto c1 = component->getChildAt(0); + auto c2 = component->getChildAt(1); + auto c3 = component->getChildAt(2); + + auto checker = [&](std::string s1, std::string s2, std::string s3) -> bool { + return IsEqual(c1->getCalculated(kPropertyText).asString(), s1) && + IsEqual(c2->getCalculated(kPropertyText).asString(), s2) && + IsEqual(c3->getCalculated(kPropertyText).asString(), s3); + }; + + ASSERT_TRUE(checker("A 0", "B 0", "C 0")); + + myArray->update(0, "D"); + root->clearPending(); + ASSERT_TRUE(checker("D 1", "B 0", "C 0")); + + myArray->update(0, "E"); + myArray->update(1, "F"); + myArray->update(2, "G"); + root->clearPending(); + ASSERT_TRUE(checker("E 2", "F 1", "G 1")); + + // Modify the array, but don't actually change the values + myArray->update(0, "E"); + myArray->update(1, "F"); + myArray->update(2, "G"); + root->clearPending(); + ASSERT_TRUE(checker("E 2", "F 1", "G 1")); +} + + +static const char *ON_CHANGE_LAYOUT = R"apl( +{ + "type": "APL", + "version": "2023.2", + "layouts": { + "Wrapper": { + "parameters": [ + "NAME", + "VALUE" + ], + "bind": [ + { + "name": "COUNTER", + "value": 0 + }, + { + "name": "WATCHER", + "value": "${VALUE}", + "onChange": { + "type": "SetValue", + "property": "COUNTER", + "value": "${COUNTER + 1}" + } + } + ], + "items": { + "type": "Text", + "text": "${NAME} ${VALUE} ${COUNTER}" + } + } + }, + "mainTemplate": { + "item": { + "type": "Container", + "items": { + "type": "Wrapper", + "NAME": "${data}", + "VALUE": "${MyObject[data]}" + }, + "data": "${Map.keys(MyObject)}" + } + } +} +)apl"; + +/** + * Bindings in a layout are eventually hooked up to the underlying component. + * + * We build a series of Text components based on a LiveMap and display the + * name, value, and number of times the object has changed. + */ +TEST_F(BuilderBindTest, OnChangeLayout) +{ + auto myMap = LiveMap::create(ObjectMap{{"A", "Hello"},{"B", "Goodbye"}}); + config->liveData("MyObject", myMap); + + loadDocument(ON_CHANGE_LAYOUT); + ASSERT_TRUE(component); + ASSERT_EQ(2, component->getChildCount()); + + auto c1 = component->getChildAt(0); + auto c2 = component->getChildAt(1); + + auto checker = [&](std::string s1, std::string s2) -> bool { + return IsEqual(c1->getCalculated(kPropertyText).asString(), s1) && + IsEqual(c2->getCalculated(kPropertyText).asString(), s2); + }; + + ASSERT_TRUE(checker("A Hello 0", "B Goodbye 0")); + + myMap->set("A", "Salut"); + root->clearPending(); + ASSERT_TRUE(checker("A Salut 1", "B Goodbye 0")); + + myMap->set("B", "Adios"); + root->clearPending(); + ASSERT_TRUE(checker("A Salut 1", "B Adios 1")); + + myMap->set("A", "Bonjour"); + myMap->set("B", "Au revoir"); + root->clearPending(); + ASSERT_TRUE(checker("A Bonjour 2", "B Au revoir 2")); + + // Update without changing anything + myMap->set("B", "Au revoir"); + root->clearPending(); + ASSERT_TRUE(checker("A Bonjour 2", "B Au revoir 2")); +} + diff --git a/aplcore/unit/engine/unittest_builder_config_change.cpp b/aplcore/unit/engine/unittest_builder_config_change.cpp index 51b51c3..ab88748 100644 --- a/aplcore/unit/engine/unittest_builder_config_change.cpp +++ b/aplcore/unit/engine/unittest_builder_config_change.cpp @@ -58,6 +58,10 @@ static const char *CHECK_ENVIRONMENT = R"apl( "${event.source.handler}", "${event.width}", "${event.height}", + "${event.minWidth}", + "${event.maxWidth}", + "${event.minHeight}", + "${event.maxHeight}", "${event.theme}", "${event.viewportMode}", "${event.disallowVideo}", @@ -93,38 +97,42 @@ TEST_F(BuilderConfigChange, CheckEnvironment) // Just theme, to existing one configChange(ConfigurationChange().theme("dark")); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 100, "dark", "hub", false, 1.0, "normal", false, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 100, 100, 100, 100, 100, "dark", "hub", false, 1.0, "normal", false, false, false)); // Rotate the screen configChange(ConfigurationChange(200,100)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 200, 100, "dark", "hub", false, 1.0, "normal", false, true, true)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 200, 100, 200, 200, 100, 100, "dark", "hub", false, 1.0, "normal", false, true, true)); // Resize the screen configChange(ConfigurationChange(400,400)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 400, 400, "dark", "hub", false, 1.0, "normal", false, true, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 400, 400, 400, 400, 400, 400, "dark", "hub", false, 1.0, "normal", false, true, false)); // Rotate back. Since we never re-inflated, the sizeChanged and rotated flags should be false now configChange(ConfigurationChange(100,200)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "dark", "hub", false, 1.0, "normal", false, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "dark", "hub", false, 1.0, "normal", false, false, false)); // Modify other properties configChange(ConfigurationChange().theme("purple").screenReader(true)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "hub", false, 1.0, "normal", true, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "purple", "hub", false, 1.0, "normal", true, false, false)); configChange(ConfigurationChange().mode(kViewportModeAuto).fontScale(3.0)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "auto", false, 3.0, "normal", true, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "purple", "auto", false, 3.0, "normal", true, false, false)); configChange(ConfigurationChange().screenMode(RootConfig::kScreenModeHighContrast)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "auto", false, 3.0, "high-contrast", true, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "purple", "auto", false, 3.0, "high-contrast", true, false, false)); configChange(ConfigurationChange().disallowVideo(true)); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "auto", true, 3.0, "high-contrast", true, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "purple", "auto", true, 3.0, "high-contrast", true, false, false)); configChange(ConfigurationChange().mode("tv")); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "tv", true, 3.0, "high-contrast", true, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "purple", "tv", true, 3.0, "high-contrast", true, false, false)); configChange(ConfigurationChange().screenMode("normal")); - ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, "purple", "tv", true, 3.0, "normal", true, false, false)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 200, 100, 100, 200, 200, "purple", "tv", true, 3.0, "normal", true, false, false)); + + // Resize to a variable size + configChange(ConfigurationChange().sizeRange(100, 50, 150, 300, 250, 350)); + ASSERT_TRUE(CheckSendEvent(root, "Document", "ConfigChange", 100, 300, 50, 150, 250, 350, "purple", "tv", true, 3.0, "normal", true, true, false)); } TEST_F(BuilderConfigChange, NoopConfigurationChangeDoesNotCreateEvent) diff --git a/aplcore/unit/engine/unittest_builder_sequence.cpp b/aplcore/unit/engine/unittest_builder_sequence.cpp index 0d9ae1e..db375ec 100644 --- a/aplcore/unit/engine/unittest_builder_sequence.cpp +++ b/aplcore/unit/engine/unittest_builder_sequence.cpp @@ -873,4 +873,96 @@ TEST_F(BuilderTestSequence, AutoSizeTextChild) ASSERT_EQ(Rect(0, 20, 50, 20), component->getChildAt(1)->getCalculated(apl::kPropertyBounds).get()); textBounds = component->getChildAt(1)->getChildAt(0)->getCalculated(apl::kPropertyBounds).get(); ASSERT_EQ(Rect(0, 0, 50, 80), textBounds); +} + +// Test that kPropertyScrollPosition is 0 after the first item is removed +TEST_F(BuilderTestSequence, SequenceRebuildLiveDataFirstChildRemovedVerticalLTR) +{ + 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_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 50); + + myArray->remove(0, 1); + root->clearPending(); + + ASSERT_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 49); +} + +// Test that kPropertyScrollPosition is 0 after the first item is removed +TEST_F(BuilderTestSequence, SequenceRebuildLiveDataFirstChildRemovedVerticalRTL) +{ + 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_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 50); + + myArray->remove(0, 1); + root->clearPending(); + + ASSERT_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 49); +} + +// Test that kPropertyScrollPosition is 0 after the first item is removed +TEST_F(BuilderTestSequence, SequenceRebuildLiveDataFirstChildRemovedHorizontalLTR) +{ + 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_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 50); + + myArray->remove(0, 1); + root->clearPending(); + + ASSERT_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 49); +} + +// Test that kPropertyScrollPosition is 0 after the first item is removed +TEST_F(BuilderTestSequence, SequenceRebuildLiveDataFirstChildRemovedHorizontalRTL) +{ + 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_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 50); + + myArray->remove(0, 1); + root->clearPending(); + + ASSERT_EQ(component->getCalculated(apl::kPropertyScrollPosition).asNumber(), 0); + ASSERT_EQ(component->getChildCount(), 49); } \ No newline at end of file diff --git a/aplcore/unit/engine/unittest_context.cpp b/aplcore/unit/engine/unittest_context.cpp index 26932fc..82fe996 100644 --- a/aplcore/unit/engine/unittest_context.cpp +++ b/aplcore/unit/engine/unittest_context.cpp @@ -51,7 +51,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("1.0", env.get("agentVersion").asString()); EXPECT_EQ("normal", env.get("animation").asString()); EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); - EXPECT_EQ("2023.3", env.get("aplVersion").asString()); + EXPECT_EQ("2024.1", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); @@ -61,7 +61,7 @@ TEST_F(ContextTest, Basic) EXPECT_EQ("", env.get("lang").asString()); EXPECT_EQ("LTR", env.get("layoutDirection").asString()); EXPECT_EQ(false, env.get("screenReader").asBoolean()); - EXPECT_EQ("2023.3", env.get("documentAPLVersion").asString()); + EXPECT_EQ("2024.1", env.get("documentAPLVersion").asString()); auto timing = env.get("timing"); EXPECT_EQ(500, timing.get("doublePressTimeout").asNumber()); @@ -113,7 +113,7 @@ TEST_F(ContextTest, Evaluation) EXPECT_EQ("1.0", env.get("agentVersion").asString()); EXPECT_EQ("normal", env.get("animation").asString()); EXPECT_FALSE(env.get("allowOpenURL").asBoolean()); - EXPECT_EQ("2023.3", env.get("aplVersion").asString()); + EXPECT_EQ("2024.1", env.get("aplVersion").asString()); EXPECT_FALSE(env.get("disallowDialog").asBoolean()); EXPECT_FALSE(env.get("disallowEditText").asBoolean()); EXPECT_FALSE(env.get("disallowVideo").asBoolean()); diff --git a/aplcore/unit/engine/unittest_context_apl_version.cpp b/aplcore/unit/engine/unittest_context_apl_version.cpp index b324a26..4cae49b 100644 --- a/aplcore/unit/engine/unittest_context_apl_version.cpp +++ b/aplcore/unit/engine/unittest_context_apl_version.cpp @@ -56,7 +56,7 @@ TEST_F(ContextAPLVersionTest, Basic) loadDocument(BASIC); auto context = component->getContext(); ASSERT_EQ("1.9", context->getRequestedAPLVersion()); - ASSERT_TRUE(IsEqual("2023.3", evaluate(*context, "${environment.aplVersion}"))); + ASSERT_TRUE(IsEqual("2024.1", evaluate(*context, "${environment.aplVersion}"))); ASSERT_TRUE(IsEqual("1.9", evaluate(*context, "${environment.documentAPLVersion}"))); // The document background is evaluated is a special data-binding context diff --git a/aplcore/unit/engine/unittest_visibility.cpp b/aplcore/unit/engine/unittest_visibility.cpp new file mode 100644 index 0000000..8fff1dd --- /dev/null +++ b/aplcore/unit/engine/unittest_visibility.cpp @@ -0,0 +1,680 @@ +/** + * 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 + +using namespace apl; + +class ViewabilityTest : public DocumentWrapper { +public: + void TearDown() override + { + changes.clear(); + DocumentWrapper::TearDown(); + } + + bool collectVisibilityChanges() { + auto hasChanges = false; + while (root->hasEvent()) { + auto event = root->popEvent(); + auto args = event.getValue(apl::kEventPropertyArguments).getArray(); + changes.emplace_back(args.at(0).asString()); + hasChanges = true; + } + return hasChanges; + } + + ::testing::AssertionResult CheckVisibilityChange(const std::string& change) { + if (changes.empty()) return ::testing::AssertionFailure() << "No changes available"; + + auto it = std::find(changes.begin(), changes.end(), "Visibility:" + change); + if (it == changes.end()) { + auto fail = ::testing::AssertionFailure() << "Have no expected VC: " << "Visibility:" + change; + for (const auto& c : changes) + fail << "\n" << c; + + return fail; + } + + changes.erase(it); + + return ::testing::AssertionSuccess(); + } + + std::vector changes; +}; + +static const char *BASIC_TEST = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "width": 1200, + "height": 800, + "direction": "row", + "wrap": "wrap", + "items": [ + { + "type": "Frame", + "id": "parent0", + "opacity": 0.75, + "width": 600, + "height": 450, + "borderColor": "green", + "borderWidth": 5, + "item": { + "type": "Frame", + "opacity": 0.75, + "width": "100%", + "height": "100%", + "borderColor": "red", + "borderWidth": 5, + "id": "fullViewTransparent", + "handleVisibilityChange": { + "commands": { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" ] + } + } + } + }, + { + "type": "Frame", + "id": "parent1", + "width": 600, + "height": 450, + "borderColor": "green", + "borderWidth": 5, + "item": { + "type": "Sequence", + "id": "parentSequence", + "width": "100%", + "height": "100%", + "data": [ + "red", + "yellow", + "blue" + ], + "items": { + "type": "Frame", + "width": "100%", + "height": 250, + "borderColor": "${data}", + "borderWidth": 5, + "id": "inSequence${data}", + "handleVisibilityChange": { + "commands": { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" ] + } + } + } + } + }, + { + "type": "Frame", + "id": "parent2", + "width": 600, + "height": 450, + "borderColor": "green", + "borderWidth": 5, + "item": { + "type": "Frame", + "opacity": 0.75, + "width": "100%", + "height": "100%", + "borderColor": "red", + "borderWidth": 5, + "id": "cutOutByGlobalViewport", + "handleVisibilityChange": { + "commands": { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" ] + } + } + } + }, + { + "type": "Frame", + "id": "parent3", + "width": 600, + "height": 450, + "borderColor": "green", + "borderWidth": 5, + "item": { + "type": "Frame", + "opacity": 0.75, + "width": "200%", + "height": "100%", + "borderColor": "red", + "borderWidth": 5, + "id": "cutOutByInception", + "handleVisibilityChange": { + "commands": { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" ] + } + }, + "item": { + "type": "Frame", + "opacity": 0.75, + "width": "200%", + "height": "100%", + "borderColor": "blue", + "borderWidth": 5, + "id": "cutOutByDeepInception", + "handleVisibilityChange": { + "commands": { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" ] + } + } + } + } + } + ] + } + } +})"; + +TEST_F(ViewabilityTest, Changes) +{ + // Set for changes to ensure ordering. + auto changesBag = std::set(); + + metrics.size(1200, 800); + + loadDocument(BASIC_TEST); + + ASSERT_TRUE(component); + + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("fullViewTransparent:1:0.5625")); + ASSERT_TRUE(CheckVisibilityChange("inSequencered:1:1")); + ASSERT_TRUE(CheckVisibilityChange("inSequenceyellow:0.76:1")); + ASSERT_TRUE(CheckVisibilityChange("inSequenceblue:0:1")); + ASSERT_TRUE(CheckVisibilityChange("cutOutByGlobalViewport:0.784091:0.75")); + ASSERT_TRUE(CheckVisibilityChange("cutOutByInception:0.395368:0.75")); + ASSERT_TRUE(CheckVisibilityChange("cutOutByDeepInception:0.199364:0.5625")); + + executeCommand("SetValue", {{"componentId", "fullViewTransparent"}, {"property", "opacity"}, {"value", 1.0}}, true); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("fullViewTransparent:1:0.75")); + + executeCommand("SetValue", {{"componentId", "parent0"}, {"property", "opacity"}, {"value", 1.0}}, true); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("fullViewTransparent:1:1")); + + executeCommand("Scroll", {{"componentId", "parentSequence"}, {"distance", 2}}, false); + advanceTime(5000); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("inSequencered:0:1")); + ASSERT_TRUE(CheckVisibilityChange("inSequenceblue:1:1")); + + executeCommand("SetValue", {{"componentId", "cutOutByInception"}, {"property", "width"}, {"value", "100%"}}, true); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("cutOutByInception:0.784091:0.75")); + ASSERT_TRUE(CheckVisibilityChange("cutOutByDeepInception:0.398757:0.5625")); +} + +static const char *VISIBLE_FOR_TIME = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Sequence", + "height": 250, + "width": 100, + "bind": [ + { "name": "VisibleStartTime", "value": 0, "type": "number" }, + { "name": "EndOfListVisible", "value": false, "type": "boolean" }, + { "name": "TimeReached", "value": false, "type": "boolean" } + ], + "handleTick": { + "when": "${!TimeReached && EndOfListVisible}", + "minimumDelay": 100, + "commands": { + "type": "Sequential", + "when": "${elapsedTime - VisibleStartTime >= 1000}", + "commands": [ + { + "type": "SetValue", + "property": "TimeReached", + "value": true + }, + { + "type": "SendEvent", + "sequencer": "NOTIFY_ME", + "arguments": [ "LastItem was visible for ${elapsedTime - VisibleStartTime} ms" ] + } + ] + } + }, + "data": [ "red", "yellow", "green", "blue" ], + "items": { + "type": "Frame", + "backgroundColor": "${data}", + "height": 100, + "width": 100 + }, + "lastItem": { + "type": "Frame", + "backgroundColor": "pink", + "height": 100, + "width": 100, + "handleVisibilityChange": { + "when": "${!TimeReached}", + "commands": [ + { + "type": "SetValue", + "property": "EndOfListVisible", + "value": "${event.visibleRegionPercentage > 0.5 && event.cumulativeOpacity > 0}" + }, + { + "when": "${EndOfListVisible && VisibleStartTime < 0}", + "type": "SetValue", + "property": "VisibleStartTime", + "value": "${elapsedTime}" + }, + { + "when": "${!EndOfListVisible && VisibleStartTime > 0}", + "type": "SetValue", + "property": "VisibleStartTime", + "value": -1 + } + ] + } + } + } + } +})"; + +TEST_F(ViewabilityTest, VisibleForTime) +{ + loadDocument(VISIBLE_FOR_TIME); + + ASSERT_TRUE(component); + advanceTime(16); + + executeCommand("Scroll", {{"componentId", ":root"}, {"distance", 2}}, false); + + // Advance 100 frames + for (auto i = 0; i < 100; i++) { + advanceTime(16); + } + + ASSERT_TRUE(root->hasEvent()); + ASSERT_TRUE(CheckSendEvent(root, "LastItem was visible for 1088 ms")); + + advanceTime(100); + + // No new events, only fired once + ASSERT_FALSE(root->hasEvent()); +} + +static const char *UPDATES_ON_CHANGES_ONLY = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Frame", + "id": "level1", + "opacity": 1, + "width": 500, + "height": 500, + "borderColor": "blue", + "borderWidth": 10, + "items": [ + { + "type": "Frame", + "id": "level2", + "opacity": 0.75, + "width": 480, + "height": 480, + "borderColor": "green", + "borderWidth": 10, + "item": { + "type": "Frame", + "opacity": 0.75, + "width": 460, + "height": 460, + "borderColor": "red", + "borderWidth": 10, + "id": "level3", + "handleVisibilityChange": { + "commands": { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ + "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" + ] + } + } + } + } + ] + } + } +})"; + +TEST_F(ViewabilityTest, UpdatesOnChangesOnly) +{ + loadDocument(UPDATES_ON_CHANGES_ONLY); + + ASSERT_TRUE(component); + + // Initial visibility state + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("level3:1:0.5625")); + + executeCommand("SetValue", {{"componentId", "level2"}, {"property", "opacity"}, {"value", 1.0}}, true); + + advanceTime(16); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("level3:1:0.75")); + + // Dirty property which results in same values make no difference + executeCommand("SetValue", {{"componentId", "level2"}, {"property", "opacity"}, {"value", 0.75}}, true); + executeCommand("SetValue", {{"componentId", "level2"}, {"property", "opacity"}, {"value", 1.0}}, true); + + advanceTime(16); + ASSERT_FALSE(collectVisibilityChanges()); + + // Change size + executeCommand("SetValue", {{"componentId", "level3"}, {"property", "width"}, {"value", 920}}, true); + advanceTime(16); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("level3:0.51087:0.75")); + + // Swap + executeCommand("SetValue", {{"componentId", "level3"}, {"property", "width"}, {"value", 460}}, true); + executeCommand("SetValue", {{"componentId", "level3"}, {"property", "height"}, {"value", 920}}, true); + + advanceTime(16); + ASSERT_FALSE(collectVisibilityChanges()); +} + +static const char *UPDATE_ONCE = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "width": 480, + "height": 480, + "items": { + "type": "Frame", + "bind": [ + { + "name": "VisibilityReported", + "type": "boolean", + "value": false + } + ], + "id": "level1", + "opacity": 0, + "width": 480, + "height": 480, + "borderColor": "green", + "borderWidth": 10, + "handleVisibilityChange": { + "when": "${!VisibilityReported}", + "commands": [ + { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ + "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" + ] + }, + { + "type": "SetValue", + "property": "VisibilityReported", + "value": "${event.visibleRegionPercentage > 0 && event.cumulativeOpacity > 0}" + } + ] + } + } + } + } +})"; + +TEST_F(ViewabilityTest, UpdatesOnce) +{ + loadDocument(UPDATE_ONCE); + + ASSERT_TRUE(component); + + // Initial visibility state + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("level1:1:0")); + + executeCommand("SetValue", {{"componentId", "level1"}, {"property", "opacity"}, {"value", 1.0}}, true); + + advanceTime(16); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("level1:1:1")); + + // Dirty property which results in same values make no difference + executeCommand("SetValue", {{"componentId", "level1"}, {"property", "opacity"}, {"value", 0.75}}, true); + + advanceTime(16); + ASSERT_FALSE(collectVisibilityChanges()); +} + +static const char *DEREGISTER = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "data": "${TestArray}", + "width": 240, + "height": 480, + "items": { + "type": "Frame", + "id": "box${data}", + "width": 240, + "height": 240, + "borderColor": "${data}", + "borderWidth": 10, + "handleVisibilityChange": { + "commands": [ + { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ + "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" + ] + } + ] + } + } + } + } +})"; + +TEST_F(ViewabilityTest, Deregister) +{ + auto myArray = LiveArray::create(ObjectArray{"red", "green"}); + config->liveData("TestArray", myArray); + + loadDocument(DEREGISTER); + + // Initial visibility state + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("boxred:1:1")); + ASSERT_TRUE(CheckVisibilityChange("boxgreen:1:1")); + + myArray->remove(0); + advanceTime(10); + ASSERT_FALSE(collectVisibilityChanges()); + + executeCommand("SetValue", {{"componentId", ":root"}, {"property", "opacity"}, {"value", 0.75}}, true); + + advanceTime(10); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_FALSE(CheckVisibilityChange("boxred:1:0.75")); + ASSERT_TRUE(CheckVisibilityChange("boxgreen:1:0.75")); +} + +static const char *ROOT_VISIBILITY_AND_REINFLATION = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "items": { + "type": "Frame", + "preserve": ["opacity"], + "id": "root", + "width": 500, + "height": 400, + "borderColor": "red", + "borderWidth": 10, + "handleVisibilityChange": { + "commands": [ + { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ + "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" + ] + } + ] + } + } + } +})"; + +TEST_F(ViewabilityTest, RootVisibility) +{ + metrics.size(400, 400); + + loadDocument(ROOT_VISIBILITY_AND_REINFLATION); + + // Initial visibility state + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("root:0.8:1")); + + executeCommand("SetValue", {{"componentId", ":root"}, {"property", "opacity"}, {"value", 0.75}}, true); + + advanceTime(10); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("root:0.8:0.75")); + + configChangeReinflate(ConfigurationChange(500, 500)); + + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("root:1:0.75")); +} + +static const char *CHILDREN_CHANGE_AND_MOUNT = R"({ + "type": "APL", + "version": "2024.1", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "data": "${TestArray}", + "width": 240, + "height": 480, + "onChildrenChanged": { + "type": "Sequential", + "sequencer": "CHILD_CHANGE", + "data": "${event.changes}", + "commands": { + "type": "SendEvent", + "arguments": [ + "childChange:${data.action}" + ] + } + }, + "items": { + "type": "Frame", + "id": "box${data}", + "width": 240, + "height": 240, + "borderColor": "${data}", + "borderWidth": 10, + "onMount": { + "type": "SendEvent", + "sequencer": "MOUNT", + "arguments": [ + "onMount:${event.source.id}" + ] + }, + "handleVisibilityChange": { + "commands": [ + { + "type": "SendEvent", + "sequencer": "VC", + "arguments": [ + "Visibility:${event.source.id}:${event.visibleRegionPercentage}:${event.cumulativeOpacity}" + ] + } + ] + } + } + } + } +})"; + +TEST_F(ViewabilityTest, EventOrdering) +{ + auto myArray = LiveArray::create(ObjectArray{"red", "green"}); + config->liveData("TestArray", myArray); + + loadDocument(CHILDREN_CHANGE_AND_MOUNT); + + // onMount happens first + ASSERT_TRUE(CheckSendEvent(root, "onMount:boxred")); + ASSERT_TRUE(CheckSendEvent(root, "onMount:boxgreen")); + + + // Initial visibility state + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_TRUE(CheckVisibilityChange("boxred:1:1")); + ASSERT_TRUE(CheckVisibilityChange("boxgreen:1:1")); + + myArray->remove(0); + advanceTime(10); + + ASSERT_TRUE(CheckSendEvent(root, "childChange:remove")); + + ASSERT_FALSE(collectVisibilityChanges()); + + executeCommand("SetValue", {{"componentId", ":root"}, {"property", "opacity"}, {"value", 0.75}}, true); + + advanceTime(10); + ASSERT_TRUE(collectVisibilityChanges()); + ASSERT_FALSE(CheckVisibilityChange("boxred:1:0.75")); + ASSERT_TRUE(CheckVisibilityChange("boxgreen:1:0.75")); +} diff --git a/aplcore/unit/extension/unittest_extension_client.cpp b/aplcore/unit/extension/unittest_extension_client.cpp index b115750..05fd3f4 100644 --- a/aplcore/unit/extension/unittest_extension_client.cpp +++ b/aplcore/unit/extension/unittest_extension_client.cpp @@ -2656,7 +2656,7 @@ TEST_F(ExtensionClientTest, TypeWithoutProperties) { ASSERT_TRUE(client->registered()); // Verify the live map is configured, without properties - const auto &liveDataMap = client->extensionSchema().liveData; + const auto& liveDataMap = client->extensionSchema().liveData; ASSERT_EQ(1, liveDataMap.size()); auto& map = liveDataMap.at("MyWeather"); ASSERT_EQ(LiveObject::ObjectType::kMapType, map->getType()); @@ -2674,7 +2674,9 @@ TEST_F(ExtensionClientTest, TypeWithoutProperties) { ASSERT_FALSE(ConsoleMessage()); // Verify the LiveData object exists in the document context with expected properties - ASSERT_TRUE(IsEqual(Object::EMPTY_MAP(), evaluate(*context, "${MyWeather}"))); + const auto EXPECTED = std::make_shared(ObjectMap{ + {"location", "Boston"}, {"temperature", "64"}, {"propNull", Object::NULL_OBJECT()}}); + ASSERT_TRUE(IsEqual(EXPECTED, evaluate(*context, "${MyWeather}"))); ASSERT_TRUE(IsEqual("Boston", evaluate(*context, "${MyWeather.location}"))); ASSERT_TRUE(IsEqual("64", evaluate(*context, "${MyWeather.temperature}"))); ASSERT_TRUE(IsEqual(Object::NULL_OBJECT(), evaluate(*context, "${MyWeather.propNull}"))); diff --git a/aplcore/unit/extension/unittest_extension_mediator.cpp b/aplcore/unit/extension/unittest_extension_mediator.cpp index 8dc745c..b15cb73 100644 --- a/aplcore/unit/extension/unittest_extension_mediator.cpp +++ b/aplcore/unit/extension/unittest_extension_mediator.cpp @@ -911,6 +911,67 @@ TEST_F(ExtensionMediatorTest, ParseSettings) { ASSERT_EQ("MAGIC", hello->mAuthorizationCode); } +static const char* EXT_DOC_SUPER_SIMPLE = R"({ + "type": "APL", + "version": "1.4", + "extension": [ + { + "uri": "aplext:notloaded:10", + "name": "NotLoaded" + } + ], + "mainTemplate": { + "item": { + "type": "Text", + "width": 500, + "height": 50, + "text": "Loaded: ${environment.extension.NotLoaded}" + } + } +})"; + +TEST_F(ExtensionMediatorTest, RegistrationFlagsDontMeanExtension) { + config->registerExtensionFlags("aplext:notloaded:10", "--hello"); + + createContent(EXT_DOC_SUPER_SIMPLE, nullptr); + + if (!extensionProvider) { + createProvider(); + } + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + mediator->loadExtensions(ObjectMap{}, content); + + inflate(); + + ASSERT_TRUE(root); + ASSERT_TRUE(component); + ASSERT_EQ("Loaded: false", component->getCalculated(kPropertyText).asString()); +} + +TEST_F(ExtensionMediatorTest, RegistrationFlagsDontMeanExtensionNoConfig) { + createContent(EXT_DOC_SUPER_SIMPLE, nullptr); + + if (!extensionProvider) { + createProvider(); + } + + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + mediator->loadExtensions(ObjectMap{{"aplext:hello:10", "--hello"}}, content); + + inflate(); + + ASSERT_TRUE(root); + ASSERT_TRUE(component); + ASSERT_EQ("Loaded: false", component->getCalculated(kPropertyText).asString()); +} + TEST_F(ExtensionMediatorTest, ExtensionParseCommands) { loadExtensions(EXT_DOC); @@ -1496,7 +1557,7 @@ TEST_F(ExtensionMediatorTest, AudioPlayerIntegration) { auto commands = context->extensionManager().getCommandDefinitions(); ASSERT_EQ(11, commands.size()); auto handlers = context->extensionManager().getEventHandlerDefinitions(); - ASSERT_EQ(1, handlers.size()); + ASSERT_EQ(2, handlers.size()); // Validate Live Data auto trackers = context->dataManager().trackers(); @@ -2868,7 +2929,7 @@ TEST_F(ExtensionMediatorTest, BasicExtensionLifecycle) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_DOC, nullptr); @@ -2920,7 +2981,7 @@ TEST_F(ExtensionMediatorTest, SessionUsedAcrossDocuments) { extensionProvider = std::make_shared(); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); // Render a first document @@ -3001,7 +3062,7 @@ TEST_F(ExtensionMediatorTest, SessionEndedBeforeDocumentFinished) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_DOC, nullptr); @@ -3041,7 +3102,7 @@ TEST_F(ExtensionMediatorTest, SessionEndedBeforeDocumentRendered) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_DOC, nullptr); @@ -3072,7 +3133,7 @@ TEST_F(ExtensionMediatorTest, SessionEndedBeforeExtensionsLoaded) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_DOC, nullptr); @@ -3153,8 +3214,8 @@ TEST_F(ExtensionMediatorTest, SessionEndsAfterAllActivitiesHaveFinished) { 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->registerExtension(ThreadSafeExtensionProxy::create(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(otherExtension)); createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); @@ -3224,8 +3285,8 @@ TEST_F(ExtensionMediatorTest, RejectedExtensionsDoNotPreventEndingSessions) { 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->registerExtension(ThreadSafeExtensionProxy::create(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(otherExtension)); createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); @@ -3274,8 +3335,8 @@ TEST_F(ExtensionMediatorTest, FailureDuringRegistrationDoesNotPreventEndingSessi 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)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(otherExtension)); createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); @@ -3323,7 +3384,7 @@ TEST_F(ExtensionMediatorTest, RejectedRegistrationDoesNotPreventEndingSessions) session); auto extension = std::make_shared("test:lifecycle:1.0"); - extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(extension)); auto failingProxy = std::make_shared("test:lifecycleOther:2.0", true, false); extensionProvider->registerExtension(failingProxy); @@ -3372,8 +3433,8 @@ TEST_F(ExtensionMediatorTest, MissingProxyDoesNotPreventEndingSessions) { 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->registerExtension(ThreadSafeExtensionProxy::create(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(otherExtension)); extensionProvider->returnNullProxyForURI("test:lifecycleOther:2.0"); @@ -3421,7 +3482,7 @@ TEST_F(ExtensionMediatorTest, UnknownExtensionDoesNotPreventEndingSessions) { session); auto extension = std::make_shared("test:lifecycle:1.0"); - extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(extension)); createContent(LIFECYCLE_WITH_MULTIPLE_EXTENSIONS_DOC, nullptr); @@ -3466,8 +3527,8 @@ TEST_F(ExtensionMediatorTest, BrokenProviderDoesNotPreventEndingSessions) { 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->registerExtension(ThreadSafeExtensionProxy::create(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(otherExtension)); // Broken provider will return a valid proxy once but then nullptr subsequently for the same URI int proxyRequestCount = 0; @@ -3522,7 +3583,7 @@ TEST_F(ExtensionMediatorTest, FailureToInitializeDoesNotPreventEndingSessions) { session); auto extension = std::make_shared("test:lifecycle:1.0"); - extensionProvider->registerExtension(std::make_shared(extension)); + extensionProvider->registerExtension(ThreadSafeExtensionProxy::create(extension)); auto failingProxy = std::make_shared("test:lifecycleOther:2.0", false, true); extensionProvider->registerExtension(failingProxy); @@ -3596,7 +3657,7 @@ TEST_F(ExtensionMediatorTest, LifecycleWithComponent) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_COMPONENT_DOC, nullptr); @@ -3694,7 +3755,7 @@ TEST_F(ExtensionMediatorTest, LifecycleWithLiveData) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_LIVE_DATA_DOC, nullptr); @@ -3752,7 +3813,7 @@ TEST_F(ExtensionMediatorTest, LifecycleAPIsRespectExtensionToken) { auto extension = std::make_shared(); extension->useAutoToken = false; // make sure the extension specifies its own token - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(LIFECYCLE_DOC, nullptr); @@ -3944,7 +4005,7 @@ TEST_F(ExtensionMediatorTest, ExtensionComponentSchema) { session); auto extension = std::make_shared(); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(COMPONENT_DOC, nullptr); @@ -4083,7 +4144,7 @@ TEST_F(ExtensionMediatorTest, RequiredExtension) { alexaext::Executor::getSynchronousExecutor(), session); - auto proxy = std::make_shared(std::make_shared(false)); + auto proxy = ThreadSafeExtensionProxy::create(std::make_shared(false)); extensionProvider->registerExtension(proxy); createContent(REQUIRED_EXTENSION, nullptr); @@ -4115,7 +4176,7 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionWithFlags) { session); auto extension = std::make_shared(false); - auto proxy = std::make_shared(extension); + auto proxy = ThreadSafeExtensionProxy::create(extension); extensionProvider->registerExtension(proxy); createContent(REQUIRED_EXTENSION, nullptr); @@ -4149,7 +4210,7 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionRegistrationFail) { alexaext::Executor::getSynchronousExecutor(), session); - auto proxy = std::make_shared(std::make_shared(true)); + auto proxy = ThreadSafeExtensionProxy::create(std::make_shared(true)); extensionProvider->registerExtension(proxy); createContent(REQUIRED_EXTENSION, nullptr); @@ -4306,7 +4367,7 @@ TEST_F(ExtensionMediatorTest, RequiredExtensionDenied) { alexaext::Executor::getSynchronousExecutor(), session); - auto proxy = std::make_shared(std::make_shared(false)); + auto proxy = ThreadSafeExtensionProxy::create(std::make_shared(false)); extensionProvider->registerExtension(proxy); createContent(REQUIRED_EXTENSION, nullptr); diff --git a/aplcore/unit/livedata/unittest_livemap_change.cpp b/aplcore/unit/livedata/unittest_livemap_change.cpp index a12f8c9..74fd98d 100644 --- a/aplcore/unit/livedata/unittest_livemap_change.cpp +++ b/aplcore/unit/livedata/unittest_livemap_change.cpp @@ -280,4 +280,42 @@ TEST_F(LiveMapChangeTest, PopulateLayoutMap) root->clearPending(); ASSERT_TRUE(IsEqual("think so", component->getCalculated(kPropertyText).asString())); +} + +static const char *MAP_FUNCTION = R"apl( +{ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "items": { + "type": "Container", + "items": { + "type": "Text", + "text": "${data} = ${MyMap[data]}" + }, + "data": "${Map.keys(MyMap)}" + } + } +} +)apl"; + +TEST_F(LiveMapChangeTest, MapFunction) +{ + auto myMap = LiveMap::create(ObjectMap{{"a", "alpha"}, {"b", "bravo"}, {"c", "charlie"}}); + config->liveData("MyMap", myMap); + + loadDocument(MAP_FUNCTION); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + auto a = component->getChildAt(0); + auto b = component->getChildAt(1); + auto c = component->getChildAt(2); + ASSERT_TRUE(IsEqual("a = alpha", a->getCalculated(apl::kPropertyText).asString())); + ASSERT_TRUE(IsEqual("b = bravo", b->getCalculated(apl::kPropertyText).asString())); + ASSERT_TRUE(IsEqual("c = charlie", c->getCalculated(apl::kPropertyText).asString())); + + myMap->set("a", "another"); + root->clearPending(); + ASSERT_TRUE(IsEqual("a = another", a->getCalculated(apl::kPropertyText).asString())); + // Note: We can't add or remove from the map without re-inflating (yet). } \ No newline at end of file diff --git a/aplcore/unit/logstream.h b/aplcore/unit/logstream.h new file mode 100644 index 0000000..dd0b5a4 --- /dev/null +++ b/aplcore/unit/logstream.h @@ -0,0 +1,62 @@ +/* + * 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_LOGSTREAM_H +#define APL_LOGSTREAM_H + +#include "apl/apl.h" +#include "rapidjson/prettywriter.h" + +namespace apl { + +/** + * A convenience class to write a rapidjson object to the debug log. + * You can use this with the rapidjson pretty printer class. + */ +class LogOSStream { +public: + typedef char Ch; + + Ch Peek() const { assert(false); return '\0'; } + Ch Take() { assert(false); return '\0'; } + size_t Tell() const { return 0; } + + Ch* PutBegin() { assert(false); return 0; } + void Put(Ch c) { buf.push_back(c); } + void Flush() { + LOG(LogLevel::kDebug) << buf; + buf.clear(); + } + size_t PutEnd(Ch*) { assert(false); return 0; } + +private: + std::string buf; +}; + +/** + * Write a rapidjson Value to the debug log + * @param value The value to write. + */ +void dumpRapidJSONValue( rapidjson::Value& value ) +{ + LogOSStream log; + rapidjson::PrettyWriter writer(log); + value.Accept(writer); +} + +} // namespace apl + +#endif // APL_LOGSTREAM_H diff --git a/aplcore/unit/media/unittest_media_player.cpp b/aplcore/unit/media/unittest_media_player.cpp index b1c0e6f..fef2edb 100644 --- a/aplcore/unit/media/unittest_media_player.cpp +++ b/aplcore/unit/media/unittest_media_player.cpp @@ -761,10 +761,12 @@ TEST_F(MediaPlayerTest, MultiTrackPlayback) loadDocument(MULTI_TRACK_PLAYBACK); ASSERT_TRUE(component); + ASSERT_FALSE(root->screenLock()); // Nothing is playing // After 100 milliseconds the "onTrackReady" handler executes mediaPlayerFactory->advanceTime(100); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track1 0/P")); + ASSERT_FALSE(root->screenLock()); // Nothing is playing ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -775,66 +777,83 @@ TEST_F(MediaPlayerTest, MultiTrackPlayback) // Start playing. We'll let the player go through track1 onto track2. Track 2 fails after 1200 ms. executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); ASSERT_TRUE(CheckSendEvent(root, "Play track1 0/")); + ASSERT_TRUE(root->screenLock()); // Playing causes a screen lock mediaPlayerFactory->advanceTime(500); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track1 500/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(500); ASSERT_TRUE(CheckSendEvent(root, "TrackUpdate track2 0/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(500); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track2 0/")); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track2 400/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(500); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track2 900/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(500); ASSERT_TRUE(CheckSendEvent(root, "TrackFail track2 1200/P")); + ASSERT_FALSE(root->screenLock()); // The player pauses automatically on a fail mediaPlayerFactory->advanceTime(100); + ASSERT_FALSE(root->screenLock()); ASSERT_FALSE(root->hasEvent()); // Skip to the next track executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "next"}}, false); ASSERT_TRUE(CheckSendEvent(root, "TrackUpdate track3 0/P")); + ASSERT_FALSE(root->screenLock()); // Start playback again executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); ASSERT_TRUE(CheckSendEvent(root, "Play track3 0/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(250); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track3 0/")); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 250/")); + ASSERT_TRUE(root->screenLock()); // Note that the third track repeats once mediaPlayerFactory->advanceTime(250); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 0/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(250); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 250/")); + ASSERT_TRUE(root->screenLock()); mediaPlayerFactory->advanceTime(250); ASSERT_TRUE(CheckSendEvent(root, "End track3 500/EP")); + ASSERT_FALSE(root->screenLock()); // Jump back to the first track executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "setTrack"}, {"value", 0}}, false); ASSERT_TRUE(CheckSendEvent(root, "TrackUpdate track1 0/P")); + ASSERT_FALSE(root->screenLock()); // Jump back to the first track AGAIN. This should not generate an event (there's no new information) executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "setTrack"}, {"value", 0}}, false); ASSERT_FALSE(root->hasEvent()); + ASSERT_FALSE(root->screenLock()); // Even if we don't start playing, it buffers up to get ready mediaPlayerFactory->advanceTime(500); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track1 0/P")); + ASSERT_FALSE(root->screenLock()); // Advance to the third track executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "next"}}, false); ASSERT_TRUE(CheckSendEvent(root, "TrackUpdate track2 0/P")); executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "next"}}, false); ASSERT_TRUE(CheckSendEvent(root, "TrackUpdate track3 0/P")); + ASSERT_FALSE(root->screenLock()); // Play through the entire track. There is a repeat, so we need to run twice as long executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); @@ -844,6 +863,7 @@ TEST_F(MediaPlayerTest, MultiTrackPlayback) ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 250/")); // One repeat has occurred, so we've wrapped mediaPlayerFactory->advanceTime(1000); ASSERT_TRUE(CheckSendEvent(root, "End track3 500/EP")); // One repeat has occurred, so we've wrapped + ASSERT_FALSE(root->screenLock()); // Calling setTrack on this track will reset the repeat counter and take it out of the End state executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "setTrack"}, {"value", 2}}, false); @@ -854,6 +874,12 @@ TEST_F(MediaPlayerTest, MultiTrackPlayback) ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 300/")); mediaPlayerFactory->advanceTime(300); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 100/")); // We've wrapped + ASSERT_TRUE(root->screenLock()); + + // Finally, stop the playback + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "pause"}}, false); + ASSERT_TRUE(CheckSendEvent(root, "Pause track3 100/P")); + ASSERT_FALSE(root->screenLock()); } static const char *PLAY_MEDIA = R"apl( @@ -902,6 +928,7 @@ TEST_F(MediaPlayerTest, PlayMedia) loadDocument(PLAY_MEDIA); ASSERT_TRUE(component); + ASSERT_FALSE(root->screenLock()); ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -912,10 +939,12 @@ TEST_F(MediaPlayerTest, PlayMedia) // After 100 milliseconds nothing happens mediaPlayerFactory->advanceTime(100); ASSERT_FALSE(root->hasEvent()); + ASSERT_FALSE(root->screenLock()); // Play an existing track executeCommand("PlayMedia", {{"componentId", "MyVideo"}, {"source", "track3" }}, false); ASSERT_TRUE(CheckSendEvent(root, "Play track3 0/")); + ASSERT_TRUE(root->screenLock()); ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -927,12 +956,14 @@ TEST_F(MediaPlayerTest, PlayMedia) mediaPlayerFactory->advanceTime(250); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track3 0/")); ASSERT_TRUE(CheckSendEvent(root, "TimeUpdate track3 250/")); + ASSERT_TRUE(root->screenLock()); // Play a non-existent track. This will fail immediately executeCommand("PlayMedia", {{"componentId", "MyVideo"}, {"source", "track9" }}, false); // A track fail terminates action which pauses the previously playing track ASSERT_TRUE(CheckSendEvent(root, "Pause track3 250/P")); ASSERT_TRUE(CheckSendEvent(root, "Play track9 0/")); + ASSERT_TRUE(root->screenLock()); // We briefly think we have screen lock until told otherwise. ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -943,10 +974,12 @@ TEST_F(MediaPlayerTest, PlayMedia) mediaPlayerFactory->advanceTime(100); ASSERT_TRUE(CheckSendEvent(root, "TrackFail track9 0/EP")); + ASSERT_FALSE(root->screenLock()); // Use "SetValue" to change the tracks. This doesn't report a "PLAY" event because it wasn't a play command executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "source"}, {"value", "track1" }}, false); ASSERT_FALSE(root->hasEvent()); + ASSERT_FALSE(root->screenLock()); // However, the track does start to buffer, so it sends a Ready mediaPlayerFactory->advanceTime(100); @@ -960,6 +993,7 @@ TEST_F(MediaPlayerTest, PlayMedia) // Start playing, then use another SetValue to stop the existing playback executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "play"}}, false); ASSERT_TRUE(CheckSendEvent(root, "Play track1 0/")); + ASSERT_TRUE(root->screenLock()); ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventPlay, 1}, })); @@ -971,6 +1005,7 @@ TEST_F(MediaPlayerTest, PlayMedia) // This should stop the playback, but it doesn't emit an event (should it?) executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "source"}, {"value", "track3" }}, false); ASSERT_FALSE(root->hasEvent()); + ASSERT_FALSE(root->screenLock()); ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -1048,6 +1083,11 @@ TEST_F(MediaPlayerTest, PlayMediaTerminationByTap) performTap(1, 100); // Player is not paused if audioTrack is anything other than foreground ASSERT_FALSE(CheckSendEvent(root, "Pause track3 250/P")); + ASSERT_TRUE(root->screenLock()); // Screen lock is still held + + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "pause"}}, false); + ASSERT_TRUE(CheckSendEvent(root, "Pause track3 250/P")); + ASSERT_FALSE(root->screenLock()); // Screen lock has been released } static const char *PLAY_MEDIA_IN_SEQUENCE = R"apl( @@ -1198,12 +1238,18 @@ TEST_F(MediaPlayerTest, ControlMediaInSequence) loadDocument(CONTROL_MEDIA_IN_SEQUENCE); ASSERT_TRUE(component); + ASSERT_FALSE(root->screenLock()); // Play the track in foreground executeCommand("PLAY_AND_SEND", {}, false); ASSERT_TRUE(CheckSendEvent(root, "Play track1 0/")); // After the command we should receive a send event immediately ASSERT_TRUE(CheckSendEvent(root, "STARTED")); + ASSERT_TRUE(root->screenLock()); + + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "pause"}}, false); + ASSERT_TRUE(CheckSendEvent(root, "Pause track1 0/P")); + ASSERT_FALSE(root->screenLock()); } @@ -1250,6 +1296,7 @@ TEST_F(MediaPlayerTest, AutoPlay) ASSERT_TRUE(component); ASSERT_TRUE(CheckSendEvent(root, "Play track1 0/")); + ASSERT_TRUE(root->screenLock()); ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -1261,6 +1308,7 @@ TEST_F(MediaPlayerTest, AutoPlay) mediaPlayerFactory->advanceTime(2000); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track1 0/")); ASSERT_TRUE(CheckSendEvent(root, "End track1 1000/EP")); + ASSERT_FALSE(root->screenLock()); } static const char *AUTO_PLAY_NESTED = R"apl( @@ -1309,6 +1357,7 @@ TEST_F(MediaPlayerTest, AutoPlayNested) ASSERT_TRUE(component); ASSERT_TRUE(CheckSendEvent(root, "Play track1 0/")); + ASSERT_TRUE(root->screenLock()); ASSERT_TRUE(CheckPlayerEvents(eventCounts, { {TestMediaPlayer::EventType::kPlayerEventSetTrackList, 1}, @@ -1320,6 +1369,7 @@ TEST_F(MediaPlayerTest, AutoPlayNested) mediaPlayerFactory->advanceTime(2000); ASSERT_TRUE(CheckSendEvent(root, "TrackReady track1 0/")); ASSERT_TRUE(CheckSendEvent(root, "End track1 1000/EP")); + ASSERT_FALSE(root->screenLock()); } @@ -1572,11 +1622,13 @@ TEST_F(MediaPlayerTest, DestroyMediaPlayer) ASSERT_TRUE(component); ASSERT_EQ(1, component->getChildCount()); + ASSERT_FALSE(root->screenLock()); executeCommand("PlayMedia", {{"componentId", "MyVideo"}, {"source", "track1"}}, false); stepForward(500); ASSERT_FALSE(root->hasEvent()); root->clearPending(); + ASSERT_TRUE(root->screenLock()); auto child = component->getChildAt(0); ASSERT_TRUE(child->getType() == kComponentTypeVideo); @@ -1586,6 +1638,7 @@ TEST_F(MediaPlayerTest, DestroyMediaPlayer) ASSERT_TRUE(child->remove()); child = nullptr; // This should release the media player ASSERT_EQ(0, component->getChildCount()); + ASSERT_FALSE(root->screenLock()); // We need this to clear out the old OnPlay handler that is holding onto the video resource root->clearPending(); @@ -1670,3 +1723,207 @@ TEST_F(MediaPlayerTest, AutoplayDoesntPlayVideoWhenDisallowVideoTrue) { // onPlay was not triggered ASSERT_FALSE(root->hasEvent()); } + + +static const char *SCREEN_LOCK_PROPERTY = R"apl( + { + "type": "APL", + "version": "1.7", + "mainTemplate": { + "item": { + "type": "Container", + "items": { + "type": "Video", + "id": "MyVideo", + "screenLock": false + } + } + } + } +)apl"; + +TEST_F(MediaPlayerTest, ScreenLockProperty) +{ + mediaPlayerFactory->addFakeContent({ + {"track1", 1000, 0, -1}, + }); + + loadDocument(SCREEN_LOCK_PROPERTY); + root->clearPending(); + + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getChildCount()); + ASSERT_FALSE(root->screenLock()); + + // Playing media with screenLock=FALSE doesn't do anything + executeCommand("PlayMedia", {{"componentId", "MyVideo"}, {"source", "track1"}}, false); + stepForward(500); + ASSERT_FALSE(root->hasEvent()); + root->clearPending(); + ASSERT_FALSE(root->screenLock()); + + // Changing screenLock=TRUE should toggle the screen lock + executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "screenLock"}, {"value", true}}, true); + ASSERT_TRUE(root->screenLock()); + + // Change it back to false - the screen lock is released + executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "screenLock"}, {"value", false}}, true); + ASSERT_FALSE(root->screenLock()); + + // Pause the media playback + executeCommand("ControlMedia", {{"componentId", "MyVideo"}, {"command", "pause"}}, true); + ASSERT_FALSE(root->screenLock()); + + // Now turn screenLock=TRUE - but since there is no media, it doesn't change + executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "screenLock"}, {"value", true}}, true); + ASSERT_FALSE(root->screenLock()); +} + + +static const char *SCREEN_LOCK_AUTO_PLAY = R"apl( + { + "type": "APL", + "version": "1.7", + "mainTemplate": { + "item": { + "type": "Container", + "items": { + "type": "Video", + "id": "MyVideo", + "screenLock": true, + "autoplay": true, + "source": "track1" + } + } + } + } +)apl"; + +TEST_F(MediaPlayerTest, ScreenLockVideoRemoval) +{ + mediaPlayerFactory->addFakeContent({ + {"track1", 1000, 0, -1}, + }); + + loadDocument(SCREEN_LOCK_AUTO_PLAY); + root->clearPending(); + + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(root->screenLock()); + + // Now remove the component while the video is playing. + executeCommand("RemoveItem", {{"componentId", "MyVideo"}}, true); + ASSERT_FALSE(root->screenLock()); +} + + +static const char *SCREEN_LOCK_MULTIPLE_VIDEOS = R"apl( + { + "type": "APL", + "version": "1.7", + "mainTemplate": { + "item": { + "type": "Container", + "data": ["A", "B", "C"], + "items": { + "type": "Video", + "id": "MyVideo${index}", + "screenLock": true, + "autoplay": true, + "source": "track1" + } + } + } + } +)apl"; + +TEST_F(MediaPlayerTest, MultipleVideos) +{ + mediaPlayerFactory->addFakeContent({ + {"track1", 1000, 0, -1}, + }); + + loadDocument(SCREEN_LOCK_MULTIPLE_VIDEOS); + root->clearPending(); + + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + ASSERT_TRUE(root->screenLock()); + + // Stop the players one by one. Stopping the last one should remove the screen lock. + executeCommand("ControlMedia", {{"componentId", "MyVideo0"}, {"command", "pause"}}, true); + ASSERT_TRUE(root->screenLock()); + + executeCommand("ControlMedia", {{"componentId", "MyVideo1"}, {"command", "pause"}}, true); + ASSERT_TRUE(root->screenLock()); + + executeCommand("ControlMedia", {{"componentId", "MyVideo2"}, {"command", "pause"}}, true); + ASSERT_FALSE(root->screenLock()); + + // Restart a few videos and stop in random order + executeCommand("ControlMedia", {{"componentId", "MyVideo0"}, {"command", "play"}}, false); + ASSERT_TRUE(root->screenLock()); + + executeCommand("ControlMedia", {{"componentId", "MyVideo1"}, {"command", "play"}}, false); + ASSERT_TRUE(root->screenLock()); + + executeCommand("ControlMedia", {{"componentId", "MyVideo1"}, {"command", "pause"}}, true); + ASSERT_TRUE(root->screenLock()); + + executeCommand("ControlMedia", {{"componentId", "MyVideo0"}, {"command", "pause"}}, true); + ASSERT_FALSE(root->screenLock()); +} + + +static const char * PLAY_MEDIA_WITH_SCREEN_LOCK = R"apl( + { + "type": "APL", + "version": "1.7", + "mainTemplate": { + "item": { + "type": "Video", + "id": "MyVideo" + } + } + } +)apl"; + +TEST_F(MediaPlayerTest, VideoWithSequencer) +{ + mediaPlayerFactory->addFakeContent({ + {"track1", 1000, 0, -1}, + }); + + loadDocument(PLAY_MEDIA_WITH_SCREEN_LOCK); + root->clearPending(); + + ASSERT_TRUE(component); + ASSERT_FALSE(root->screenLock()); + + // Play the video on the foreground audio track with a screen lock on the command + executeCommand("PlayMedia", {{"componentId", "MyVideo"}, {"source", "track1"}, {"screenLock", true}}, false); + ASSERT_TRUE(root->screenLock()); + + // Change the component screenLock value to false. Because the PlayMedia command specified + // a screen lock, we continue to hold the screen lock. + executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "screenLock"}, {"value", false}}, true); + ASSERT_TRUE(root->screenLock()); + + // Interrupt the video playback by issuing a new command. This should stop the PlayMedia command + // which will release the screen lock. + executeCommand("Idle", {}, false); + ASSERT_FALSE(root->screenLock()); + + // Calling PlayMedia again without a screen lock does not result in a screen lock + executeCommand("PlayMedia", {{"componentId", "MyVideo"}, {"source", "track1"}}, false); + ASSERT_FALSE(root->screenLock()); + + // Switch the component back to holding a screen lock + executeCommand("SetValue", {{"componentId", "MyVideo"}, {"property", "screenLock"}, {"value", true}}, true); + ASSERT_TRUE(root->screenLock()); + + // And stop it again + executeCommand("Idle", {}, false); + ASSERT_FALSE(root->screenLock()); +} \ No newline at end of file diff --git a/aplcore/unit/primitives/CMakeLists.txt b/aplcore/unit/primitives/CMakeLists.txt index 2a0756d..13892d6 100644 --- a/aplcore/unit/primitives/CMakeLists.txt +++ b/aplcore/unit/primitives/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources_local(unittest unittest_filters.cpp unittest_keyboard.cpp unittest_object.cpp + unittest_pseudolocalizer.cpp unittest_radii.cpp unittest_range.cpp unittest_rect.cpp diff --git a/aplcore/unit/primitives/unittest_object.cpp b/aplcore/unit/primitives/unittest_object.cpp index 025829d..4765173 100644 --- a/aplcore/unit/primitives/unittest_object.cpp +++ b/aplcore/unit/primitives/unittest_object.cpp @@ -921,8 +921,8 @@ TEST_F(DocumentObjectTest, StyledTextCast) TEST_F(DocumentObjectTest, Truthy) { loadDocument(STYLED_TEXT_CAST); - ASSERT_TRUE(Object(ComponentEventSourceWrapper::create(component, "", Object::NULL_OBJECT())).truthy()); - ASSERT_FALSE(Object(ComponentEventSourceWrapper::create(nullptr, "", Object::NULL_OBJECT())).truthy()); + ASSERT_TRUE(Object(ComponentEventSourceWrapper::create(component, "Test", Object::NULL_OBJECT())).truthy()); + ASSERT_FALSE(Object(ComponentEventSourceWrapper::create(nullptr, "Test", Object::NULL_OBJECT())).truthy()); } TEST_F(DocumentObjectTest, LiveDataAccess) diff --git a/aplcore/unit/primitives/unittest_pseudolocalizer.cpp b/aplcore/unit/primitives/unittest_pseudolocalizer.cpp new file mode 100644 index 0000000..ca7297d --- /dev/null +++ b/aplcore/unit/primitives/unittest_pseudolocalizer.cpp @@ -0,0 +1,132 @@ +/** + * 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/pseudolocalizer.h" + +using namespace apl; + +class PseudoLocalizeTest : public ::testing::Test { +public: + PseudoLocalizeTest() { + textTransformer = std::make_shared(); + } + + std::shared_ptr textTransformer; +}; + +static const std::vector PSEUDO_TEST_CASES = {"Hello World", "Testing", "Random", + "String", ""}; +static const std::vector PSEUDO_TEST_CASES_RESPONSE_70_EXPANSION = { + "[--Ħḗḗŀŀǿǿ Ẇǿǿřŀḓ--]", "[-Ŧḗḗşŧīīƞɠ-]", "[-Řȧȧƞḓǿǿḿ-]", "[-Şŧřīīƞɠ--]", "[]"}; +static const std::vector PSEUDO_TEST_CASES_RESPONSE_DEFAULT_EXPANSION = { + "[Ħḗḗŀŀǿǿ Ẇǿǿřŀḓ]", "[Ŧḗḗşŧīīƞɠ]", "[Řȧȧƞḓǿḿ]", "[Şŧřīīƞɠ]", "[]"}; + +TEST_F(PseudoLocalizeTest, transform_HappyCase) { + + auto cnt = 0; + for (const std::string& input : PSEUDO_TEST_CASES) { + auto props = + std::make_shared(ObjectMap{{"enabled", true}, {"expansionPercentage", 70}}); + std::string result = textTransformer->transform(input, props); + + // Assert that the string has been transformed. + EXPECT_EQ(result, PSEUDO_TEST_CASES_RESPONSE_70_EXPANSION.at(cnt++)); + } +} + +TEST_F(PseudoLocalizeTest, transform_SuppledExpansionPercentagelessThan0_DefaultExpansionFactor) { + auto cnt = 0; + for (const std::string& input : PSEUDO_TEST_CASES) { + auto props = + std::make_shared(ObjectMap{{"enabled", true}, {"expansionPercentage", -1}}); + std::string result = textTransformer->transform(input, props); + + // Assert that the length of the result is greater than input + EXPECT_EQ(result, PSEUDO_TEST_CASES_RESPONSE_DEFAULT_EXPANSION.at(cnt++)); + } +} + +TEST_F(PseudoLocalizeTest, transform_SuppledExpansionPercentage0_NoExpansion) { + auto props = + std::make_shared(ObjectMap{{"enabled", true}, {"expansionPercentage", 0}}); + std::string result = textTransformer->transform("input", props); + + // Assert that the length of the result is greater than input + EXPECT_EQ(result, "[īƞƥŭŧ]"); +} + +TEST_F(PseudoLocalizeTest, transform_SuppledExpansionPercentageMoreThan100_DefaultExpansionFactor) { + auto cnt = 0; + for (const std::string& input : PSEUDO_TEST_CASES) { + auto props = + std::make_shared(ObjectMap{{"enabled", true}, {"expansionPercentage", 101}}); + std::string result = textTransformer->transform(input, props); + + // Assert that the string is expanded by default expansion percentage + EXPECT_EQ(result, PSEUDO_TEST_CASES_RESPONSE_DEFAULT_EXPANSION.at(cnt++)); + } +} + +TEST_F(PseudoLocalizeTest, transform_SuppledExpansionPercentageNull_DefaultExpansionFactor) { + auto cnt = 0; + for (const std::string& input : PSEUDO_TEST_CASES) { + auto props = std::make_shared(ObjectMap{{"enabled", true}}); + std::string result = textTransformer->transform(input, props); + + // Assert that the string is expanded by default expansion percentage + EXPECT_EQ(result, PSEUDO_TEST_CASES_RESPONSE_DEFAULT_EXPANSION.at(cnt++)); + } +} + +TEST_F(PseudoLocalizeTest, transform_NullSettings) { + EXPECT_EQ(textTransformer->transform("Hello World", Object::NULL_OBJECT()), "Hello World"); + EXPECT_EQ(textTransformer->transform("", Object::NULL_OBJECT()), ""); +} + +TEST_F(PseudoLocalizeTest, getPseudoLocalString_Disabled) { + auto props = + std::make_shared(ObjectMap{{"enabled", false}, {"expansionPercentage", 40}}); + + std::string result; + result = textTransformer->transform("Hello World", props); + + EXPECT_EQ(result, "Hello World"); +} + +TEST_F(PseudoLocalizeTest, expandString_OddSettingsSuppliedExpansionPercentageNotNumber) { + auto cnt = 0; + for (const std::string& input : PSEUDO_TEST_CASES) { + auto props = std::make_shared( + ObjectMap{{"enabled", true}, {"expansionPercentage", "abc"}}); + std::string result = textTransformer->transform(input, props); + // Assert that the string is expanded by default expansion percentage + EXPECT_EQ(result, PSEUDO_TEST_CASES_RESPONSE_DEFAULT_EXPANSION.at(cnt++)); + } +} + +TEST_F(PseudoLocalizeTest, getPseudoLocalString_OddSettingsInvalidEnabledValue) { + auto props = + std::make_shared(ObjectMap{{"enabled", "gh"}, {"expansionPercentage", 40}}); + std::string result = textTransformer->transform("Hello World", props); + + EXPECT_EQ(result, "[Ħḗḗŀŀǿǿ Ẇǿǿřŀḓ-]"); +} + +TEST_F(PseudoLocalizeTest, getPseudoLocalString_OddSettingsEnabledFlagAbsent) { + auto props = std::make_shared(ObjectMap{{"expansionPercentage", 70}}); + std::string result = textTransformer->transform("Hello World", props); + EXPECT_EQ(result, "Hello World"); +} diff --git a/aplcore/unit/scenegraph/test_sg.h b/aplcore/unit/scenegraph/test_sg.h index ea36a27..d675739 100644 --- a/aplcore/unit/scenegraph/test_sg.h +++ b/aplcore/unit/scenegraph/test_sg.h @@ -87,6 +87,8 @@ class IsAccessibility { IsAccessibility(std::string msg="") : mMsg(std::move(msg)) {} IsAccessibility& label(std::string label) { mLabel = std::move(label); return *this; } IsAccessibility& role(Role role) { mRole = role; return *this; } + IsAccessibility& adjustableValue(std::string value) { mAdjustableValue = value; return *this; } + IsAccessibility& adjustableRange(sg::Accessibility::AdjustableRange range) { mAdjustableRange = range; return *this; } IsAccessibility& action(const std::string& name, const std::string& label, bool enabled) { mActions.emplace_back(sg::Accessibility::Action{name, label, enabled}); @@ -100,6 +102,8 @@ class IsAccessibility { std::string mLabel; Role mRole = apl::kRoleNone; std::vector mActions; + std::string mAdjustableValue; + sg::Accessibility::AdjustableRange mAdjustableRange; }; class IsNode { diff --git a/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp b/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp index d178c9c..881c85c 100644 --- a/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_accessibility.cpp @@ -224,6 +224,65 @@ TEST_F(SGAccessibilityTest, Actions) CheckSendEvent(root, "alpha"); } +static const char *ADJUSTABLE_RANGE = R"apl( +{ + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "accessibilityAdjustableRange":{ + "minValue": 0, + "maxValue": 10, + "currentValue": 5 + } + } + } +} +)apl"; + +TEST_F(SGAccessibilityTest, AdjustableRange) +{ + metrics.size(300, 300); + loadDocument(ADJUSTABLE_RANGE); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}) + .pressable() + .accessibility(IsAccessibility() + .adjustableRange(sg::Accessibility::AdjustableRange{0, 10, 5})))); +} + +static const char *ADJUSTABLE_VALUE = R"apl( +{ + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "items": { + "type": "TouchWrapper", + "accessibilityAdjustableValue": 5 + } + } +} +)apl"; + +TEST_F(SGAccessibilityTest, AdjustableValue) +{ + metrics.size(300, 300); + loadDocument(ADJUSTABLE_VALUE); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 300, 300}) + .pressable() + .accessibility(IsAccessibility() + .adjustableValue("5")))); +} static const char *INTERACTION_CHECKED_ENABLED = R"apl( { @@ -283,6 +342,8 @@ TEST_F(SGAccessibilityTest, Serialize) a->setRole(Role::kRoleAlert); a->appendAction("bounce", "this is a bounce", true); a->appendAction("debounce", "this is not a bounce", false); + a->setAdjustableRange(JsonData(R"({"minValue": 0, "maxValue": 10, "currentValue": 5})").get()); + a->setAdjustableValue("5"); rapidjson::Document document; ASSERT_TRUE(IsEqual(a->serialize(document.GetAllocator()), StringToMapObject(R"apl( @@ -300,7 +361,13 @@ TEST_F(SGAccessibilityTest, Serialize) "label": "this is not a bounce", "enabled": false } - ] + ], + "adjustableRange": { + "minValue": 0, + "maxValue": 10, + "currentValue": 5 + }, + "adjustableValue": "5" } )apl"))); } diff --git a/aplcore/unit/testeventloop.h b/aplcore/unit/testeventloop.h index 1b9dfae..53c0934 100644 --- a/aplcore/unit/testeventloop.h +++ b/aplcore/unit/testeventloop.h @@ -1317,7 +1317,7 @@ HandlePointerEvent(const RootContextPtr& root, PointerEventType type, const Poin inline ::testing::AssertionResult -compareTransformApprox(const Transform2D& left, const Transform2D& right, float delta = 0.001F) { +compareTransformApprox(const Transform2D& left, const Transform2D& right, float delta = 0.1F) { auto leftComponents = left.get(); auto rightComponents = right.get(); diff --git a/aplcore/unit/touch/unittest_native_gestures_pager.cpp b/aplcore/unit/touch/unittest_native_gestures_pager.cpp index 7a064ba..2c7d512 100644 --- a/aplcore/unit/touch/unittest_native_gestures_pager.cpp +++ b/aplcore/unit/touch/unittest_native_gestures_pager.cpp @@ -3601,4 +3601,80 @@ TEST_F(NativeGesturesPagerTest, LiveDataTargetOrCurrentRemove) ASSERT_EQ("yellow2", component->getDisplayedChildAt(0)->getId()); ASSERT_EQ(10, component->getChildCount()); ASSERT_EQ(Object(Transform2D()), component->getDisplayedChildAt(0)->getCalculated(kPropertyTransform)); -} \ No newline at end of file +} + +static const char * NESTED_PAGER_OFFSET = R"apl({ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": [ + { + "type": "Container", + "width": "100vw", + "height": "100vh", + "direction": "row", + "items": [ + { + "type": "Pager", + "position": "absolute", + "left": "10vw", + "pageDirection": "horizontal", + "width": "90vw", + "height": "100vh", + "data": [ + "${Array.range(10)}" + ], + "items": [ + { + "type": "Frame", + "width": "100%", + "height": "100%", + "background": "${index % 2 == 0 ? 'red' : 'green'}" + } + ] + } + ] + } + ] + } +})apl"; + +// Ensure that pointer positions transformed properly when pager is offset. +TEST_F(NativeGesturesPagerTest, NestedPagerOffset) +{ + metrics.size(400,400); + loadDocument(NESTED_PAGER_OFFSET); + + auto pager = component->getChildAt(0); + + auto currentChild = pager->getChildAt(0); + auto nextChild = pager->getChildAt(1); + + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerDown, Point(400,10))); + advanceTime(50); + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(250,10))); + advanceTime(50); + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerMove, Point(100,10))); + root->clearPending(); + + ASSERT_TRUE(CheckTransform(Transform2D::translateX(-300), currentChild)); + ASSERT_TRUE(CheckTransform(Transform2D::translateX(60), nextChild)); + + + root->handlePointerEvent(PointerEvent(PointerEventType::kPointerUp, Point(100,10))); + advanceTime(1); + + ASSERT_TRUE(CheckTransform(Transform2D::translateX(-300.1), currentChild)); + ASSERT_TRUE(CheckTransform(Transform2D::translateX(60), nextChild)); + + + // Advance to "almost finished" + advanceTime(300); + ASSERT_TRUE(CheckTransform(Transform2D::translateX(-330.1), currentChild)); + ASSERT_TRUE(CheckTransform(Transform2D::translateX(29.9), nextChild)); + + // And finish + advanceTime(300); + ASSERT_TRUE(CheckTransform(Transform2D::translateX(0), currentChild)); + ASSERT_TRUE(CheckTransform(Transform2D::translateX(0), nextChild)); +} diff --git a/aplcore/unit/utils/CMakeLists.txt b/aplcore/unit/utils/CMakeLists.txt index 68ad2c1..fdce01f 100644 --- a/aplcore/unit/utils/CMakeLists.txt +++ b/aplcore/unit/utils/CMakeLists.txt @@ -21,6 +21,7 @@ target_sources_local(unittest unittest_ringbuffer.cpp unittest_scopeddequeue.cpp unittest_scopedset.cpp + unittest_screenlockholder.cpp unittest_session.cpp unittest_stringfunctions.cpp unittest_url.cpp diff --git a/aplcore/unit/utils/unittest_scopeddequeue.cpp b/aplcore/unit/utils/unittest_scopeddequeue.cpp index 4049447..7e43768 100644 --- a/aplcore/unit/utils/unittest_scopeddequeue.cpp +++ b/aplcore/unit/utils/unittest_scopeddequeue.cpp @@ -87,10 +87,6 @@ TEST_F(ScopedDequeueTest, ExtractScope) comp.emplace_back(2, 3); comp.emplace_back(2, 4); - std::remove_if(comp.begin(), comp.end(), [](const std::pair& item) { - return item.first == 1; - }); - ASSERT_EQ(comp, scopedDequeue->getAll()); ASSERT_EQ(2, scopedDequeue->size()); diff --git a/aplcore/unit/utils/unittest_screenlockholder.cpp b/aplcore/unit/utils/unittest_screenlockholder.cpp new file mode 100644 index 0000000..f4a87f6 --- /dev/null +++ b/aplcore/unit/utils/unittest_screenlockholder.cpp @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#include "../testeventloop.h" +#include "apl/utils/screenlockholder.h" + +using namespace apl; + +class ScreenLockHolderTest : public DocumentWrapper {}; + +static const char *SIMPLE_DOCUMENT = R"apl( +{ + "type": "APL", + "version": "2023.3", + "mainTemplate": { + "items": { + "type": "Frame", + "id": "FRAME" + } + } +} +)apl"; + +TEST_F(ScreenLockHolderTest, Basic) +{ + loadDocument(SIMPLE_DOCUMENT); + ASSERT_FALSE(root->screenLock()); + + // Create a static block. When this block terminates the lock is released. + { + ScreenLockHolder lock(component->getContext()); + ASSERT_FALSE(root->screenLock()); + + lock.take(); + ASSERT_TRUE(root->screenLock()); + } + + ASSERT_FALSE(root->screenLock()); +} + +TEST_F(ScreenLockHolderTest, MultipleTakes) +{ + loadDocument(SIMPLE_DOCUMENT); + ASSERT_FALSE(root->screenLock()); + + // Create a static block. When this block terminates the lock is released. + { + ScreenLockHolder lock(component->getContext()); + ASSERT_FALSE(root->screenLock()); + + lock.take(); + ASSERT_TRUE(root->screenLock()); + + lock.take(); // Take a second time + ASSERT_TRUE(root->screenLock()); + } + + ASSERT_FALSE(root->screenLock()); +} + +TEST_F(ScreenLockHolderTest, MultipleReleases) +{ + loadDocument(SIMPLE_DOCUMENT); + ASSERT_FALSE(root->screenLock()); + + // Create a static block. When this block terminates the lock is released. + { + ScreenLockHolder lock(component->getContext()); + ASSERT_FALSE(root->screenLock()); + + lock.take(); + ASSERT_TRUE(root->screenLock()); + + lock.take(); // Take it a second time + ASSERT_TRUE(root->screenLock()); + + lock.release(); // Release it and it should still be gone + ASSERT_FALSE(root->screenLock()); + + lock.release(); // Release it again; it should still be gone + ASSERT_FALSE(root->screenLock()); + + lock.take(); // Take it back; it should be there (the double release didn't do anything odd) + ASSERT_TRUE(root->screenLock()); + } + + ASSERT_FALSE(root->screenLock()); +} + + +TEST_F(ScreenLockHolderTest, Ensure) +{ + loadDocument(SIMPLE_DOCUMENT); + ASSERT_FALSE(root->screenLock()); + + // Create a static block. When this block terminates the lock is released. + { + ScreenLockHolder lock(component->getContext()); + ASSERT_FALSE(root->screenLock()); + + lock.ensure(true); + ASSERT_TRUE(root->screenLock()); + + lock.ensure(false); + ASSERT_FALSE(root->screenLock()); + + lock.ensure(false); + ASSERT_FALSE(root->screenLock()); + + lock.ensure(true); + ASSERT_TRUE(root->screenLock()); + + lock.ensure(true); + ASSERT_TRUE(root->screenLock()); + } + + ASSERT_FALSE(root->screenLock()); +} \ No newline at end of file diff --git a/doc/objecttype.puml b/doc/objecttype.puml new file mode 100644 index 0000000..e724a2d --- /dev/null +++ b/doc/objecttype.puml @@ -0,0 +1,103 @@ +@startuml + +abstract class ArrayFreeMixin #ffafcc +abstract class MapFreeMixin #ffafcc + +abstract class ObjectType #ffc8dd +abstract class BaseObjectType #ffc8dd +abstract class SimpleObjectType #ffc8dd +abstract class ReferenceHolderObjectType #ffc8dd +abstract class PointerHolderObjectType #ffc8dd +abstract class SimplePointerHolderObjectType #ffc8dd +abstract class ContainerObjectType #ffc8dd +abstract class AbstractMapObjectType #ffc8dd +abstract class MapLikeObjectType #ffc8dd +abstract class ArrayObjectType #ffc8dd +abstract class MapObjectType #ffc8dd + +abstract class DimensionObjectType #cdb4db +class AutoDimensionObjectType #cdb4db +class RelativeDimensionObjectType #cdb4db +class AbsoluteDimensionObjectType #cdb4db + +class AccessibilityAction::ObjectType #a2d2ff +class Array::ObjectType #cdb4db +class Boolean::ObjectType #cdb4db +class BoundSymbol::ObjectType #bde0fe +class ByteCode::ObjectType #a2d2ff +class Color::ObjectType #cdb4db +class ComponentEventWrapper::ObjectType #bde0fe +class ContextWrapper::ObjectType #bde0fe +class Easing::ObjectType #a2d2ff +class Filter::ObjectType #bde0fe +class Function::ObjectType #a2d2ff +class Gradient::ObjectType #bde0fe +class Graphic::ObjectType #a2d2ff +class GraphicFilter::ObjectType #bde0fe +class GraphicPattern::ObjectType #a2d2ff +class LiveArrayObject::ObjectType #cdb4db +class LiveMapObject::ObjectType #bde0fe +class Map::ObjectType #bde0fe +class MediaSource::ObjectType #bde0fe +class Null::ObjectType #cdb4db +class Number::ObjectType #cdb4db +class Radii::ObjectType #bde0fe +class Range::ObjectType #bde0fe +class Rect::ObjectType #bde0fe +class String::ObjectType #cdb4db +class StyledText::ObjectType #bde0fe +class Transform::ObjectType #a2d2ff +class Transform2D::ObjectType #bde0fe +class URLRequest::ObjectType #bde0fe + +ObjectType <-- BaseObjectType +BaseObjectType <-- SimpleObjectType +MapFreeMixin <-- SimpleObjectType +ArrayFreeMixin <-- SimpleObjectType +SimpleObjectType <-- ReferenceHolderObjectType +SimpleObjectType <-- DimensionObjectType +SimpleObjectType <-- Null::ObjectType +SimpleObjectType <-- Boolean::ObjectType +SimpleObjectType <-- Number::ObjectType +SimpleObjectType <-- String::ObjectType +DimensionObjectType <-- RelativeDimensionObjectType +DimensionObjectType <-- AbsoluteDimensionObjectType +BaseObjectType <-- PointerHolderObjectType +SimpleObjectType <-- AutoDimensionObjectType +SimpleObjectType <-- Color::ObjectType +PointerHolderObjectType <-- ContainerObjectType +ContainerObjectType <-- AbstractMapObjectType +ArrayFreeMixin <-- AbstractMapObjectType +AbstractMapObjectType <-- MapLikeObjectType +AbstractMapObjectType <-- MapObjectType +MapLikeObjectType <-- ComponentEventWrapper::ObjectType +MapObjectType <-- Map::ObjectType +ContainerObjectType <-- ArrayObjectType +MapFreeMixin <-- ArrayObjectType +SimplePointerHolderObjectType <-- ByteCode::ObjectType +ReferenceHolderObjectType <-- Gradient::ObjectType +ReferenceHolderObjectType <-- StyledText::ObjectType +ReferenceHolderObjectType <-- URLRequest::ObjectType +ReferenceHolderObjectType <-- Radii::ObjectType +ReferenceHolderObjectType <-- Transform2D::ObjectType +ReferenceHolderObjectType <-- Range::ObjectType +ReferenceHolderObjectType <-- BoundSymbol::ObjectType +ReferenceHolderObjectType <-- MediaSource::ObjectType +ArrayObjectType <-- Array::ObjectType +ReferenceHolderObjectType <-- Filter::ObjectType +ReferenceHolderObjectType <-- Rect::ObjectType +PointerHolderObjectType <-- SimplePointerHolderObjectType +MapFreeMixin <-- SimplePointerHolderObjectType +ArrayFreeMixin <-- SimplePointerHolderObjectType +SimplePointerHolderObjectType <-- GraphicPattern::ObjectType +SimplePointerHolderObjectType <-- Graphic::ObjectType +SimplePointerHolderObjectType <-- Easing::ObjectType +SimplePointerHolderObjectType <-- AccessibilityAction::ObjectType +SimplePointerHolderObjectType <-- Function::ObjectType +ReferenceHolderObjectType <-- GraphicFilter::ObjectType +SimplePointerHolderObjectType <-- Transform::ObjectType +MapObjectType <-- LiveMapObject::ObjectType +ArrayObjectType <-- LiveArrayObject::ObjectType +MapLikeObjectType <-- ContextWrapper::ObjectType + +@enduml \ No newline at end of file diff --git a/extensions/alexaext/CMakeLists.txt b/extensions/alexaext/CMakeLists.txt index 7061e44..c1ee0d6 100644 --- a/extensions/alexaext/CMakeLists.txt +++ b/extensions/alexaext/CMakeLists.txt @@ -24,22 +24,33 @@ project(AlexaExt LANGUAGES CXX C) -add_library(alexaext STATIC - src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp - src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp - src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp - src/APLMetricsExtension/AplMetricsExtension.cpp - src/APLWebflowExtension/AplWebflowBase.cpp - src/APLWebflowExtension/AplWebflowExtension.cpp - src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp - src/APLAttentionSystemExtension/AplAttentionSystemExtension.cpp - src/executor.cpp - src/extensionmessage.cpp - src/extensionregistrar.cpp - src/localextensionproxy.cpp - src/random.cpp - src/sessiondescriptor.cpp - ) +include(target_sources_local.cmake) + +if (BUILD_SHARED) + add_library(alexaext SHARED) +else() + add_library(alexaext STATIC) +endif() + + +target_sources_local(alexaext PRIVATE + src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp + src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp + src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp + src/APLMetricsExtension/AplMetricsExtension.cpp + src/APLWebflowExtension/AplWebflowBase.cpp + src/APLWebflowExtension/AplWebflowExtension.cpp + src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp + src/APLAttentionSystemExtension/AplAttentionSystemExtension.cpp + src/executor.cpp + src/extensionmessage.cpp + src/extensionregistrar.cpp + src/localextensionproxy.cpp + src/random.cpp + src/sessiondescriptor.cpp + src/threadsafeextensionproxy.cpp + src/threadsafeextensionregistrar.cpp + ) if (BUILD_SHARED OR ENABLE_PIC) set_target_properties(alexaext @@ -59,7 +70,9 @@ target_include_directories(alexaext $ ) -target_link_libraries(alexaext PUBLIC rapidjson-apl) +if (NOT USE_SYSTEM_RAPIDJSON) + target_link_libraries(alexaext PUBLIC rapidjson-apl) +endif() target_compile_options(alexaext PRIVATE diff --git a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h index 2d47a4c..c0e1a03 100644 --- a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h +++ b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h @@ -86,6 +86,14 @@ class AplAudioPlayerExtension */ void updatePlaybackProgress(int offset); + /** + * Call to update the audioItemId apl::LiveMap. + * It is expected that this is called on every "Play" directive. + * + * @param audioItemId Unique id to identify audioItem + */ + void updateCurrentAudioItemId(const std::string& audioItemId); + /** * This method will do nothing * @deprecated The extension generates it's own token on extension registration. @@ -113,6 +121,7 @@ class AplAudioPlayerExtension /// The @c apl::LiveMap activity and offset for AudioPlayer playbackState data. std::string mPlaybackStateActivity; int mPlaybackStateOffset = 0; + std::string mAudioItemId{""}; /// The map of activity to activity state std::unordered_map, diff --git a/extensions/alexaext/include/alexaext/alexaext.h b/extensions/alexaext/include/alexaext/alexaext.h index 895a9c6..6f0eb2f 100644 --- a/extensions/alexaext/include/alexaext/alexaext.h +++ b/extensions/alexaext/include/alexaext/alexaext.h @@ -45,6 +45,8 @@ #include "extensionregistrar.h" #include "localextensionproxy.h" #include "sessiondescriptor.h" +#include "threadsafeextensionproxy.h" +#include "threadsafeextensionregistrar.h" #include "types.h" #include "APLAudioPlayerExtension/AplAudioPlayerExtension.h" #include "APLAudioNormalizationExtension/AplAudioNormalizationExtension.h" diff --git a/extensions/alexaext/include/alexaext/extensionmessage.h b/extensions/alexaext/include/alexaext/extensionmessage.h index 51cb6e4..e18f2e8 100644 --- a/extensions/alexaext/include/alexaext/extensionmessage.h +++ b/extensions/alexaext/include/alexaext/extensionmessage.h @@ -774,7 +774,7 @@ class LiveDataOperation { protected: - explicit LiveDataOperation(rapidjson::MemoryPoolAllocator<>* allocator) + explicit LiveDataOperation(rapidjson::Document::AllocatorType* allocator) : mAllocator(allocator), mValue(std::make_shared()) {} diff --git a/extensions/alexaext/include/alexaext/extensionregistrar.h b/extensions/alexaext/include/alexaext/extensionregistrar.h index 725cfb6..8cc669e 100644 --- a/extensions/alexaext/include/alexaext/extensionregistrar.h +++ b/extensions/alexaext/include/alexaext/extensionregistrar.h @@ -29,6 +29,8 @@ namespace alexaext { * Default implementation of ExtensionProvider, maintained by the runtime. * Provides a registry of directly registered extension URI to extension proxy plus provider * delegation. + * + * Note: This class is not thread-safe. For a thread-safe implementation use @code ThreadSafeExtensionRegistrar. */ class ExtensionRegistrar : public ExtensionProvider { diff --git a/extensions/alexaext/include/alexaext/localextensionproxy.h b/extensions/alexaext/include/alexaext/localextensionproxy.h index 5d1da83..9ab7245 100644 --- a/extensions/alexaext/include/alexaext/localextensionproxy.h +++ b/extensions/alexaext/include/alexaext/localextensionproxy.h @@ -32,6 +32,8 @@ namespace alexaext { /** * Default implementation of Extension proxy, used for built-in extensions. This * class forwards all calls from the extension framework directly to the extension. + * + * Note: This class is not thread-safe. For a thread-safe implementation use @code ThreadSafeExtensionProxy. */ class LocalExtensionProxy final : public ExtensionProxy, public std::enable_shared_from_this { diff --git a/extensions/alexaext/include/alexaext/threadsafeextensionproxy.h b/extensions/alexaext/include/alexaext/threadsafeextensionproxy.h new file mode 100644 index 0000000..80db0cc --- /dev/null +++ b/extensions/alexaext/include/alexaext/threadsafeextensionproxy.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. + */ + +#ifndef APL_THREADSAFEEXTENSIONPROXY_H +#define APL_THREADSAFEEXTENSIONPROXY_H + +#include +#include +#include + +#include "executor.h" +#include "extensionproxy.h" + +namespace alexaext { + +// forward declare +class ThreadSafeExtensionProxy; +using ThreadSafeExtensionProxyPtr = std::shared_ptr; +using ThreadSafeExtensionProxyWPtr = std::weak_ptr; + +/** + * A thread safe implementation of ExtensionProxy. This class can be invoked on multiple threads and forwards events to + * the extension through an executor. The executor should run tasks serially on a background thread to avoid blocking the + * core processing thread. + * + * Note: this implementation only invokes Activity-based apis for Extensions. + */ +class ThreadSafeExtensionProxy : public ExtensionProxy, + public std::enable_shared_from_this { +public: + /** + * Create a shared ptr to a ThreadSafeExtensionProxy. + * + * @param extension the extension to delegate calls to. + * @param executor the executor to run extension functions on. Defaults to synchronous execution. + * @return + */ + static ThreadSafeExtensionProxyPtr create(const ExtensionPtr& extension, const ExecutorPtr& executor = Executor::getSynchronousExecutor()) { return std::make_shared(extension, executor); } + + /** + * Constructor. Use @code create as this object inherits from std::enable_shared_from_this. + * + * @param extension the extension to delegate calls to. + * @param executor the executor to run extension functions on. + */ + explicit ThreadSafeExtensionProxy(const ExtensionPtr& extension, const ExecutorPtr& executor); + + std::set getURIs() const final { return mExtension->getURIs(); }; + bool getRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest, + RegistrationSuccessActivityCallback&& success, + RegistrationFailureActivityCallback&& error) final; + bool initializeExtension(const std::string& uri) final; + bool isInitialized(const std::string& uri) const final; + bool invokeCommand(const ActivityDescriptor& activity, + const rapidjson::Value& command, + CommandSuccessActivityCallback&& success, + CommandFailureActivityCallback&& error) final; + void registerEventCallback(const ActivityDescriptor& activity, Extension::EventActivityCallback&& callback) final; + void registerLiveDataUpdateCallback(const ActivityDescriptor& activity, Extension::LiveDataUpdateActivityCallback&& callback) final; + void onRegistered(const ActivityDescriptor& activity) final; + void onUnregistered(const ActivityDescriptor& activity) final; + bool sendComponentMessage(const ActivityDescriptor &activity, const rapidjson::Value &message) final; + void onResourceReady(const ActivityDescriptor& activity, const ResourceHolderPtr& resourceHolder) final; + void onSessionStarted(const SessionDescriptor& session) final; + void onSessionEnded(const SessionDescriptor& session) final; + void onForeground(const ActivityDescriptor& activity) final; + void onBackground(const ActivityDescriptor& activity) final; + void onHidden(const ActivityDescriptor& activity) final; + +private: + using EventCallbacks = std::vector; + using LiveDataCallbacks = std::vector; + struct ActivityContext { + EventCallbacks eventCallbacks; + LiveDataCallbacks liveDataCallbacks; + }; + using ActivityContextPtr = std::shared_ptr; + + void enqueueTaskOnExtension(std::function task); + ActivityContextPtr ensureActivityContext(const ActivityDescriptor& activity); + +private: + const ExtensionPtr mExtension; + const ExecutorPtr mExecutor; + mutable std::mutex mInitializationMutex; + bool mInitialized = false; + std::mutex mActivitiesMutex; + std::map, ActivityDescriptor::Compare> mActivities; +}; + +} // namespace alexaext + +#endif // APL_THREADSAFEEXTENSIONPROXY_H diff --git a/extensions/alexaext/include/alexaext/threadsafeextensionregistrar.h b/extensions/alexaext/include/alexaext/threadsafeextensionregistrar.h new file mode 100644 index 0000000..f4567c0 --- /dev/null +++ b/extensions/alexaext/include/alexaext/threadsafeextensionregistrar.h @@ -0,0 +1,79 @@ +/** + * 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_THREADSAFEEXTENSIONREGISTRAR_H +#define APL_THREADSAFEEXTENSIONREGISTRAR_H + +#include +#include +#include +#include +#include + +#include "extensionprovider.h" +#include "extensionproxy.h" + +namespace alexaext { + +/** + * Thread-safe implementation of ExtensionProvider, maintained by the runtime. + * + * Provides a registry of directly registered extension URI to extension proxy plus provider + * delegation. + */ +class ThreadSafeExtensionRegistrar : public ExtensionProvider { + +public: + /** + * Construct a ThreadSafeExtensionRegistrar with the following providers and proxies. + * + * @param providers a set of providers to delegate to. + * @param proxies a set of already created proxies. + */ + explicit ThreadSafeExtensionRegistrar(const std::set& providers, const std::set& proxies); + + /** + * 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 or is available through any of known providers. + * + * @param uri The requsted extension URI. + * @return true if the extension is registered. + */ + bool hasExtension(const std::string& uri) override; + + /** + * 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 or provider-held extension. + */ + ExtensionProxyPtr getExtension(const std::string& uri) override; + +private: + const std::set mProviders; + std::mutex mExtensionMutex; + std::map mExtensions; +}; + +using ThreadSafeExtensionRegistrarPtr = std::shared_ptr; + +} // namespace alexaext + +#endif // APL_THREADSAFEEXTENSIONREGISTRAR_H diff --git a/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp b/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp index 5901e78..c713887 100644 --- a/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp +++ b/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp @@ -49,11 +49,13 @@ static const char *COMMAND_SKIP_BACKWARD_NAME = "SkipBackward"; // Events static const char *EVENTHANDLER_ON_PLAYER_ACTIVITY_UPDATED_NAME = "OnPlayerActivityUpdated"; +static const char *EVENTHANDLER_ON_TRACK_CHANGED_NAME = "OnTrackChanged"; // Data Types static const char *DATA_TYPE_PLAYBACK_STATE = "playbackState"; // spec named static const char *PROPERTY_PLAYER_ACTIVITY = "playerActivity"; static const char *PROPERTY_OFFSET = "offset"; +static const char *PROPERTY_AUDIO_ITEM_ID = "audioItemId"; static const char *DATA_TYPE_SEEK_POSITION = "SeekToPositionType"; static const char *DATA_TYPE_TOGGLE = "ToggleType"; static const char *PROPERTY_TOGGLE_NAME = "name"; @@ -274,6 +276,7 @@ AplAudioPlayerExtension::createRegistration(const ActivityDescriptor& activity, }); }) .event(EVENTHANDLER_ON_PLAYER_ACTIVITY_UPDATED_NAME) + .event(EVENTHANDLER_ON_TRACK_CHANGED_NAME) .command(COMMAND_PLAY_NAME) .command(COMMAND_PAUSE_NAME) .command(COMMAND_PREVIOUS_NAME) @@ -505,6 +508,33 @@ AplAudioPlayerExtension::updatePlaybackProgress(int offset) publishLiveData(); } +void +AplAudioPlayerExtension::updateCurrentAudioItemId(const std::string& audioItemId) +{ + { + std::lock_guard lock(mStateMutex); + mAudioItemId = audioItemId; + } + + auto event = Event("1.0").uri(URI).target(URI) + .name(EVENTHANDLER_ON_TRACK_CHANGED_NAME) + .property(PROPERTY_AUDIO_ITEM_ID, audioItemId); + publishLiveData(); + + // Make a list of activities to update with the lock + std::vector activitiesToUpdate; + { + std::lock_guard lock(mStateMutex); + for (const auto &it: mActivityStateMap) { + activitiesToUpdate.emplace_back(it.first); + } + } + + for (const auto &activity: activitiesToUpdate) { + invokeExtensionEventHandler(activity, event); + } +} + void AplAudioPlayerExtension::setActivePresentationSession(const std::string &id, const std::string &skillId) { @@ -533,6 +563,9 @@ AplAudioPlayerExtension::publishLiveData() }) .liveDataMapUpdate([&](LiveDataMapOperation& operation) { operation.type("Set").key(PROPERTY_OFFSET).item(mPlaybackStateOffset); + }) + .liveDataMapUpdate([&](LiveDataMapOperation& operation) { + operation.type("Set").key(PROPERTY_AUDIO_ITEM_ID).item(mAudioItemId); }); updates.emplace(it.first, liveDataUpdate); diff --git a/extensions/alexaext/src/threadsafeextensionproxy.cpp b/extensions/alexaext/src/threadsafeextensionproxy.cpp new file mode 100644 index 0000000..15dcecf --- /dev/null +++ b/extensions/alexaext/src/threadsafeextensionproxy.cpp @@ -0,0 +1,225 @@ +/** +* 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/threadsafeextensionproxy.h" + +namespace alexaext { + +ThreadSafeExtensionProxy::ThreadSafeExtensionProxy(const ExtensionPtr &extension, + const ExecutorPtr &executor) : mExtension(extension), mExecutor(executor) {} + +bool +ThreadSafeExtensionProxy::getRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest, + RegistrationSuccessActivityCallback&& success, + RegistrationFailureActivityCallback&& error) +{ + ensureActivityContext(activity); + auto request = std::make_shared(); + request->CopyFrom(registrationRequest, request->GetAllocator()); + enqueueTaskOnExtension([activity, request, success, error](const ExtensionPtr& extension) { + auto registration = extension->createRegistration(activity, *request); + success(activity, registration); + }); + + return true; +} + +bool +ThreadSafeExtensionProxy::initializeExtension(const std::string& uri) +{ + std::lock_guard lock(mInitializationMutex); + if (mInitialized) return true; + mInitialized = true; + + ThreadSafeExtensionProxyWPtr weakThis = shared_from_this(); + mExtension->registerLiveDataUpdateCallback([weakThis](const ActivityDescriptor& activity, const rapidjson::Value& liveDataUpdate) { + auto self = weakThis.lock(); + if (!self) return; + + LiveDataCallbacks callbacks = self->ensureActivityContext(activity)->liveDataCallbacks; + for (const auto& callback : callbacks) { + callback(activity, liveDataUpdate); + } + }); + + mExtension->registerEventCallback([weakThis](const ActivityDescriptor& activity, const rapidjson::Value& event) { + auto self = weakThis.lock(); + if (!self) return; + + EventCallbacks callbacks = self->ensureActivityContext(activity)->eventCallbacks; + for (const auto& callback : callbacks) { + callback(activity, event); + } + }); + + return true; +} + +bool +ThreadSafeExtensionProxy::isInitialized(const std::string& uri) const +{ + std::lock_guard lock(mInitializationMutex); + return mInitialized; +} + +bool +ThreadSafeExtensionProxy::invokeCommand(const ActivityDescriptor& activity, + const rapidjson::Value& command, + CommandSuccessActivityCallback&& success, + CommandFailureActivityCallback&& error) +{ + auto cmd = std::make_shared(); + cmd->CopyFrom(command, cmd->GetAllocator()); + auto task = [activity, cmd, success, error](const ExtensionPtr& extension) { + auto commandId = (int) Command::ID().Get(*cmd)->GetDouble(); + auto result = extension->invokeCommand(activity, *cmd); + if (!result) { + rapidjson::Document fail = CommandFailure("1.0") + .uri(activity.getURI()) + .id(commandId) + .errorCode(kErrorFailedCommand) + .errorMessage(sErrorMessage[kErrorFailedCommand] + std::to_string(commandId)); + error(activity, fail); + return; + } + + rapidjson::Document win = CommandSuccess("1.0") + .uri(activity.getURI()) + .id(commandId); + success(activity, win); + }; + enqueueTaskOnExtension(task); + + return true; +} + +void +ThreadSafeExtensionProxy::registerEventCallback(const ActivityDescriptor& activity, Extension::EventActivityCallback&& callback) +{ + auto activityContext = ensureActivityContext(activity); + activityContext->eventCallbacks.emplace_back(callback); +} + +void +ThreadSafeExtensionProxy::registerLiveDataUpdateCallback(const ActivityDescriptor& activity, Extension::LiveDataUpdateActivityCallback&& callback) +{ + auto activityContext = ensureActivityContext(activity); + activityContext->liveDataCallbacks.emplace_back(callback); +} + +void +ThreadSafeExtensionProxy::onRegistered(const ActivityDescriptor& activity) +{ + enqueueTaskOnExtension([activity](const ExtensionPtr& extension) { + extension->onActivityRegistered(activity); + }); +} + +void +ThreadSafeExtensionProxy::onUnregistered(const ActivityDescriptor& activity) +{ + std::lock_guard lock(mActivitiesMutex); + mActivities.erase(activity); + + enqueueTaskOnExtension([activity](const ExtensionPtr& extension) { + extension->onActivityUnregistered(activity); + }); +} + +bool +ThreadSafeExtensionProxy::sendComponentMessage(const ActivityDescriptor &activity, const rapidjson::Value &message) +{ + auto msg = std::make_shared(); + msg->CopyFrom(message, msg->GetAllocator()); + enqueueTaskOnExtension([activity, msg](const ExtensionPtr& extension) { + extension->updateComponent(activity, *msg); + }); + return true; +} + +void +ThreadSafeExtensionProxy::onResourceReady(const ActivityDescriptor& activity, const ResourceHolderPtr& resourceHolder) +{ + enqueueTaskOnExtension([activity, resourceHolder](const ExtensionPtr& extension) { + extension->onResourceReady(activity, resourceHolder); + }); +} + +void +ThreadSafeExtensionProxy::onSessionStarted(const SessionDescriptor& session) +{ + enqueueTaskOnExtension([session](const ExtensionPtr& extension) { + extension->onSessionStarted(session); + }); +} + +void +ThreadSafeExtensionProxy::onSessionEnded(const SessionDescriptor& session) +{ + enqueueTaskOnExtension([session](const ExtensionPtr& extension) { + extension->onSessionEnded(session); + }); +} + +void +ThreadSafeExtensionProxy::onForeground(const ActivityDescriptor& activity) +{ + enqueueTaskOnExtension([activity](const ExtensionPtr& extension) { + extension->onForeground(activity); + }); +} + +void +ThreadSafeExtensionProxy::onBackground(const ActivityDescriptor& activity) +{ + enqueueTaskOnExtension([activity](const ExtensionPtr& extension) { + extension->onBackground(activity); + }); +} + +void +ThreadSafeExtensionProxy::onHidden(const ActivityDescriptor& activity) +{ + enqueueTaskOnExtension([activity](const ExtensionPtr& extension) { + extension->onHidden(activity); + }); +} + +void +ThreadSafeExtensionProxy::enqueueTaskOnExtension(std::function task) +{ + ThreadSafeExtensionProxyWPtr weakThis = shared_from_this(); + mExecutor->enqueueTask([weakThis, task]() { + auto self = weakThis.lock(); + if (!self) return; + + task(self->mExtension); + }); +} + +ThreadSafeExtensionProxy::ActivityContextPtr +ThreadSafeExtensionProxy::ensureActivityContext(const alexaext::ActivityDescriptor& activity) +{ + std::lock_guard lock(mActivitiesMutex); + auto it = mActivities.find(activity); + if (it == mActivities.end()) { + mActivities.emplace(activity, std::make_shared()); + } + + return mActivities.find(activity)->second; +} + +} // namespace alexaext \ No newline at end of file diff --git a/extensions/alexaext/src/threadsafeextensionregistrar.cpp b/extensions/alexaext/src/threadsafeextensionregistrar.cpp new file mode 100644 index 0000000..7cf7e70 --- /dev/null +++ b/extensions/alexaext/src/threadsafeextensionregistrar.cpp @@ -0,0 +1,78 @@ +/** + * 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/threadsafeextensionregistrar.h" + +#include + +namespace alexaext { + +ThreadSafeExtensionRegistrar::ThreadSafeExtensionRegistrar(const std::set& providers, const std::set& proxies) : mProviders(providers) { + std::lock_guard lock(mExtensionMutex); + for (const auto& proxy : proxies) { + for (const auto& uri : proxy->getURIs()) { + mExtensions.emplace(uri, proxy); + } + } +} + +bool +ThreadSafeExtensionRegistrar::hasExtension(const std::string& uri) +{ + // Check if extension is cached first + { + std::lock_guard lock(mExtensionMutex); + if (mExtensions.count(uri) > 0) return true; + } + + // Now check if any providers support it + return std::any_of(mProviders.cbegin(), mProviders.cend(), [&uri](const ExtensionProviderPtr& it) { + return it->hasExtension(uri); + }); +} + +ExtensionProxyPtr +ThreadSafeExtensionRegistrar::getExtension(const std::string& uri) +{ + // Check if extension is cached first. + ExtensionProxyPtr proxy = nullptr; + { + std::lock_guard lock(mExtensionMutex); + auto it = mExtensions.find(uri); + if (it != mExtensions.end()) { + proxy = it->second; + } + } + + // Now go to providers + if (!proxy) { + auto provider = std::find_if(mProviders.cbegin(), mProviders.cend(), [&uri](const ExtensionProviderPtr& it) { + return it->hasExtension(uri); + }); + + if (provider != mProviders.cend()) { + proxy = provider->get()->getExtension(uri); + if (proxy) { + std::lock_guard lock(mExtensionMutex); + mExtensions.emplace(uri, proxy); + } + } + } + + if (proxy && !proxy->isInitialized(uri)) proxy->initializeExtension(uri); + return proxy; +} + +} // namespace alexaext \ No newline at end of file diff --git a/extensions/alexaext/target_sources_local.cmake b/extensions/alexaext/target_sources_local.cmake new file mode 100644 index 0000000..6163be8 --- /dev/null +++ b/extensions/alexaext/target_sources_local.cmake @@ -0,0 +1,34 @@ +# Helper function for allowing relative paths to be used in target_sources command +# with CMake versions 3.12 and earlier. +# +# Source: https://crascit.com/2016/01/31/enhanced-source-file-handling-with-target_sources/ + +# Do not use this command with source files that have been auto-generated. +function(target_sources_local target) + if (POLICY CMP0076) + # New behavior is available, so just forward to it by ensuring + # that we have the policy set to request the new behavior, but + # don't change the policy setting for the calling scope + cmake_policy(PUSH) + cmake_policy(SET CMP0076 NEW) + target_sources(${target} ${ARGN}) + cmake_policy(POP) + return() + endif() + + # Must be using CMake 3.12 or earlier, so simulate the new behavior + unset(_srcList) + get_target_property(_targetSourceDir ${target} SOURCE_DIR) + + foreach(src ${ARGN}) + if (NOT src STREQUAL "PRIVATE" AND + NOT src STREQUAL "PUBLIC" AND + NOT src STREQUAL "INTERFACE" AND + NOT IS_ABSOLUTE "${src}") + # Relative path to source, prepend relative to where target was defined + file(RELATIVE_PATH src "${_targetSourceDir}" "${CMAKE_CURRENT_LIST_DIR}/${src}") + endif() + list(APPEND _srcList ${src}) + endforeach() + target_sources(${target} ${_srcList}) +endfunction() \ No newline at end of file diff --git a/extensions/unit/CMakeLists.txt b/extensions/unit/CMakeLists.txt index b48448b..b273334 100644 --- a/extensions/unit/CMakeLists.txt +++ b/extensions/unit/CMakeLists.txt @@ -34,6 +34,7 @@ add_executable(alexaext-unittest unittest_random.cpp unittest_resource_provider.cpp unittest_session_descriptor.cpp + unittest_threadsafe_extension_registrar.cpp ) target_link_libraries(alexaext-unittest diff --git a/extensions/unit/unittest_apl_audio_player.cpp b/extensions/unit/unittest_apl_audio_player.cpp index 3a50eaa..c2b1b18 100644 --- a/extensions/unit/unittest_apl_audio_player.cpp +++ b/extensions/unit/unittest_apl_audio_player.cpp @@ -337,6 +337,7 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationEvents) // FullSet event handler for audio player auto expectedHandlerSet = std::set(); expectedHandlerSet.insert("OnPlayerActivityUpdated"); + expectedHandlerSet.insert("OnTrackChanged"); ASSERT_TRUE(events->IsArray() && events->Size() == expectedHandlerSet.size()); // should have all event handlers defined @@ -455,7 +456,7 @@ TEST_F(AplAudioPlayerExtensionTest, GetLiveDataObjectsSuccess) GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); - ASSERT_TRUE(ops->IsArray() && ops->Size() == 2); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 3); ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "playerActivity", "STOPPED")); ASSERT_TRUE(CheckLiveData(ops->GetArray()[1], "Set", "offset", 0)); }); @@ -582,7 +583,7 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandSeekToPositionSuccess) GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); - ASSERT_TRUE(ops->IsArray() && ops->Size() == 2); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 3); ASSERT_TRUE(CheckLiveData(ops->GetArray()[1], "Set", "offset", 42)); }); auto invoke = mExtension->invokeCommand(activity, command); @@ -1073,7 +1074,7 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlaybackProgressSuccess) GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); - ASSERT_TRUE(ops->IsArray() && ops->Size() == 2); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 3); ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "playerActivity", "STOPPED")); ASSERT_TRUE(CheckLiveData(ops->GetArray()[1], "Set", "offset", 100)); }); @@ -1082,6 +1083,57 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlaybackProgressSuccess) ASSERT_TRUE(gotUpdate); } +/** + * Currently playing track info change updates live data. + */ +TEST_F(AplAudioPlayerExtensionTest, updateCurrentAudioItemId) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + bool gotUpdate = false; + mExtension->registerLiveDataUpdateCallback( + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + gotUpdate = true; + ASSERT_STREQ("LiveDataUpdate", + GetWithDefault(RegistrationSuccess::METHOD(), liveDataUpdate, "")); + ASSERT_STREQ("aplext:audioplayer:10", + GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); + const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); + ASSERT_TRUE(ops); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 3); + ASSERT_TRUE(CheckLiveData(ops->GetArray()[2], "Set", "audioItemId", "testAudioItemId")); + }); + + mExtension->updateCurrentAudioItemId("testAudioItemId"); + ASSERT_TRUE(gotUpdate); +} + +/** + * Currently playing track info change updates live data. + */ +TEST_F(AplAudioPlayerExtensionTest, updateCurrentlyPlayingTrackInfoEventSuccess) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + bool gotUpdate = false; + mExtension->registerEventCallback( + [&](const std::string &uri, const rapidjson::Value &event) { + gotUpdate = true; + ASSERT_STREQ("Event", + GetWithDefault(Event::METHOD(), event, "")); + ASSERT_STREQ("aplext:audioplayer:10", + GetWithDefault(Event::TARGET(), event, "")); + auto payload = Event::PAYLOAD().Get(event); + ASSERT_STREQ("testAudioItemId", + GetWithDefault("audioItemId", payload, "")); + }); + + mExtension->updateCurrentAudioItemId("testAudioItemId"); + ASSERT_TRUE(gotUpdate); +} + /** * Playback state change updates live data. */ @@ -1100,7 +1152,7 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityLiveDataSuccess) GetWithDefault(LiveDataUpdate::TARGET(), liveDataUpdate, "")); const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); ASSERT_TRUE(ops); - ASSERT_TRUE(ops->IsArray() && ops->Size() == 2); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 3); ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "playerActivity", "PLAYING")); ASSERT_TRUE(CheckLiveData(ops->GetArray()[1], "Set", "offset", 100)); }); diff --git a/extensions/unit/unittest_extension_lifecycle.cpp b/extensions/unit/unittest_extension_lifecycle.cpp index 94975cd..1c91431 100644 --- a/extensions/unit/unittest_extension_lifecycle.cpp +++ b/extensions/unit/unittest_extension_lifecycle.cpp @@ -285,7 +285,7 @@ class ExtensionLifecycleTest : public ::testing::Test { legacyProxy = std::make_shared(legacyExtension); extension = std::make_shared(); - proxy = std::make_shared(extension); + proxy = ThreadSafeExtensionProxy::create(extension); } std::shared_ptr legacyExtension; @@ -688,54 +688,4 @@ TEST_F(ExtensionLifecycleTest, UpdateComponentLegacy) { 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_threadsafe_extension_registrar.cpp b/extensions/unit/unittest_threadsafe_extension_registrar.cpp new file mode 100644 index 0000000..798ee53 --- /dev/null +++ b/extensions/unit/unittest_threadsafe_extension_registrar.cpp @@ -0,0 +1,141 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. +*/ + +#include +#include + +#include "gtest/gtest.h" + +using namespace alexaext; +using namespace rapidjson; + +/** +* Test class; +*/ +class ThreadSafeExtensionRegistrarTest : public ::testing::Test { +public: + ThreadSafeExtensionRegistrarPtr 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; +}; + +TEST_F(ThreadSafeExtensionRegistrarTest, BasicLocallyRegisteredProxy) { + auto test1 = std::make_shared("test1"); + auto test2 = std::make_shared("test2"); + + extRegistrar = std::make_shared(std::set(), std::set{test1, 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(ThreadSafeExtensionRegistrarTest, 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 = std::make_shared(std::set{tp1, tp2}, + std::set{test1, 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(ThreadSafeExtensionRegistrarTest, ReturnsSame) { + auto test1 = std::make_shared("test1"); + auto tp1 = std::make_shared("provider1"); + + extRegistrar = std::make_shared(std::set{tp1}, + std::set{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