From 6f7473d2db0fc7d0fc7dd4777eb5132e8773be16 Mon Sep 17 00:00:00 2001 From: Achal Saini Date: Wed, 26 Jun 2024 11:28:20 -0700 Subject: [PATCH] APL-CORE: June 2024 Release of APL 2024.2 compilant core engine (2024.2.0) For more details on this release refer to CHANGELOG.md To learn about APL see: https://developer.amazon.com/docs/alexa-presentation-language/understand-apl.html --- CHANGELOG.md | 19 + android/.gitignore | 17 + android/build.gradle | 44 + android/buildSrc/build.gradle | 7 + .../com/amazon/apl/android/CMakeTask.java | 95 + android/coreengine/.gitignore | 2 + android/coreengine/CMakeLists.txt | 158 ++ android/coreengine/build.gradle | 131 + .../coreengine/src/main/AndroidManifest.xml | 4 + .../src/main/cpp/alexaext/include/.gitignore | 2 + .../src/main/cpp/apl/include/.gitignore | 2 + .../src/main/cpp/aplconfig/include/.gitignore | 2 + .../main/cpp/aplsgconfig/include/.gitignore | 2 + .../src/main/cpp/rapidjson/include/.gitignore | 2 + .../main/java/com/amazon/apl/enums/.gitignore | 1 + android/gradle-version | 1 + android/gradle.properties | 19 + android/gradle/wrapper/gradle-wrapper.jar | Bin 0 -> 56177 bytes android/gradlew | 177 ++ android/gradlew.bat | 84 + android/settings.gradle | 6 + aplcore/CMakeLists.txt | 5 +- .../include/apl/action/importpackageaction.h | 45 + aplcore/include/apl/apl.h | 12 +- aplcore/include/apl/command/commandfactory.h | 2 +- .../include/apl/command/commandproperties.h | 5 + .../apl/command/importpackagecommand.h | 39 + aplcore/include/apl/common.h | 11 + aplcore/include/apl/component/component.h | 19 +- .../apl/component/componentproperties.h | 20 +- aplcore/include/apl/component/corecomponent.h | 116 +- .../include/apl/component/edittextcomponent.h | 31 +- aplcore/include/apl/component/hostcomponent.h | 2 +- .../include/apl/component/imagecomponent.h | 2 +- aplcore/include/apl/component/textcomponent.h | 28 +- .../include/apl/component/textmeasurement.h | 4 +- .../apl/component/vectorgraphiccomponent.h | 2 +- aplcore/include/apl/content/aplversion.h | 8 +- .../include/apl/content/configurationchange.h | 6 + aplcore/include/apl/content/content.h | 71 +- aplcore/include/apl/content/importref.h | 32 +- aplcore/include/apl/content/importrequest.h | 38 +- aplcore/include/apl/content/jsondata.h | 75 +- aplcore/include/apl/content/package.h | 5 - aplcore/include/apl/content/packagemanager.h | 138 ++ aplcore/include/apl/content/packageresolver.h | 133 + .../apl/content/pendingimportpackage.h | 164 ++ aplcore/include/apl/content/rootconfig.h | 16 + aplcore/include/apl/content/settings.h | 5 + aplcore/include/apl/content/sharedjsondata.h | 180 ++ .../apl/document/coredocumentcontext.h | 3 + .../apl/document/documentcontextdata.h | 15 +- aplcore/include/apl/engine/bindingchange.h | 9 + aplcore/include/apl/engine/builder.h | 22 +- aplcore/include/apl/engine/context.h | 12 +- aplcore/include/apl/engine/corerootcontext.h | 5 +- aplcore/include/apl/engine/evaluate.h | 101 +- aplcore/include/apl/engine/rebuilddependant.h | 74 + aplcore/include/apl/engine/rootcontext.h | 8 + .../include/apl/engine/sharedcontextdata.h | 15 +- .../include/apl/engine/visibilitymanager.h | 1 + aplcore/include/apl/livedata/livedataobject.h | 6 +- .../include/apl/primitives/boundsymbolset.h | 3 + aplcore/include/apl/primitives/object.h | 1 + aplcore/include/apl/primitives/objectdata.h | 13 + .../apl/primitives/textmeasurerequest.h | 6 +- aplcore/include/apl/scenegraph/common.h | 1 + aplcore/include/apl/scenegraph/node.h | 1 - aplcore/include/apl/scenegraph/path.h | 1 - aplcore/include/apl/scenegraph/textchunk.h | 2 + aplcore/include/apl/scenegraph/textlayout.h | 2 + .../include/apl/scenegraph/textlayoutcache.h | 35 + .../include/apl/scenegraph/textmeasurement.h | 37 +- .../include/apl/scenegraph/textproperties.h | 1 + aplcore/include/apl/touch/pointermanager.h | 1 + aplcore/include/apl/utils/constants.h | 51 + aplcore/include/apl/utils/counter.h | 3 + aplcore/include/apl/utils/flags.h | 51 + .../include/apl/utils/stickychildrentree.h | 2 +- aplcore/include/apl/utils/weakcache.h | 2 +- .../include/apl/versioning/semanticgrammar.h | 260 ++ .../include/apl/versioning/semanticpattern.h | 88 + .../include/apl/versioning/semanticversion.h | 112 + aplcore/src/action/CMakeLists.txt | 1 + aplcore/src/action/importpackageaction.cpp | 82 + aplcore/src/action/speakitemaction.cpp | 2 +- aplcore/src/command/CMakeLists.txt | 1 + aplcore/src/command/commandproperties.cpp | 5 + aplcore/src/command/corecommand.cpp | 2 + aplcore/src/command/importpackagecommand.cpp | 95 + aplcore/src/component/componentproperties.cpp | 8 + aplcore/src/component/corecomponent.cpp | 392 ++- aplcore/src/component/edittextcomponent.cpp | 160 +- aplcore/src/component/framecomponent.cpp | 2 - aplcore/src/component/hostcomponent.cpp | 6 +- aplcore/src/component/imagecomponent.cpp | 4 +- .../multichildscrollablecomponent.cpp | 3 +- aplcore/src/component/pagercomponent.cpp | 5 +- aplcore/src/component/scrollablecomponent.cpp | 2 + aplcore/src/component/selector.cpp | 9 +- aplcore/src/component/textcomponent.cpp | 310 +-- .../src/component/vectorgraphiccomponent.cpp | 5 +- aplcore/src/component/videocomponent.cpp | 5 +- aplcore/src/content/CMakeLists.txt | 3 + aplcore/src/content/aplversion.cpp | 1 + aplcore/src/content/configurationchange.cpp | 10 +- aplcore/src/content/content.cpp | 451 +--- aplcore/src/content/importrequest.cpp | 58 +- aplcore/src/content/jsondata.cpp | 9 +- aplcore/src/content/packageresolver.cpp | 150 ++ aplcore/src/content/pendingimportpackage.cpp | 358 +++ aplcore/src/content/rootconfig.cpp | 1 + aplcore/src/content/sharedjsondata.cpp | 42 + aplcore/src/document/coredocumentcontext.cpp | 116 +- aplcore/src/document/documentcontextdata.cpp | 8 +- aplcore/src/engine/bindingchange.cpp | 43 +- aplcore/src/engine/builder.cpp | 393 ++- aplcore/src/engine/context.cpp | 8 +- aplcore/src/engine/corerootcontext.cpp | 31 +- aplcore/src/engine/layoutmanager.cpp | 2 +- aplcore/src/engine/sharedcontextdata.cpp | 19 +- aplcore/src/engine/visibilitymanager.cpp | 21 +- aplcore/src/extension/extensionclient.cpp | 41 +- aplcore/src/extension/extensionmediator.cpp | 7 +- aplcore/src/graphic/graphic.cpp | 8 +- aplcore/src/graphic/graphicelementtext.cpp | 2 +- aplcore/src/livedata/layoutrebuilder.cpp | 89 +- aplcore/src/primitives/gradient.cpp | 33 +- aplcore/src/primitives/mediasource.cpp | 21 +- aplcore/src/primitives/object.cpp | 18 +- aplcore/src/scenegraph/CMakeLists.txt | 48 +- aplcore/src/scenegraph/textproperties.cpp | 19 + aplcore/src/touch/gesture.cpp | 6 +- .../src/touch/gestures/swipeawaygesture.cpp | 18 +- aplcore/src/touch/pointermanager.cpp | 5 + aplcore/src/touch/utils/pagemovehandler.cpp | 5 +- aplcore/src/utils/searchvisitor.cpp | 4 +- aplcore/src/utils/stickychildrentree.cpp | 4 +- aplcore/src/versioning/CMakeLists.txt | 19 + aplcore/src/versioning/semanticgrammar.cpp | 29 + aplcore/src/versioning/semanticpattern.cpp | 141 ++ aplcore/src/versioning/semanticversion.cpp | 127 + aplcore/unit/CMakeLists.txt | 3 + aplcore/unit/command/CMakeLists.txt | 1 + .../unittest_command_importpackage.cpp | 700 ++++++ .../unit/command/unittest_serialize_event.cpp | 44 +- .../component/unittest_component_events.cpp | 14 +- .../unittest_edit_text_component.cpp | 33 + .../component/unittest_host_component.cpp | 81 + aplcore/unit/component/unittest_serialize.cpp | 4 + aplcore/unit/content/CMakeLists.txt | 3 + aplcore/unit/content/packagegenerator.h | 73 + aplcore/unit/content/testpackagemanager.cpp | 79 + aplcore/unit/content/testpackagemanager.h | 57 + aplcore/unit/content/unittest_document.cpp | 118 +- aplcore/unit/content/unittest_jsondata.cpp | 51 + .../unit/content/unittest_packagemanager.cpp | 1045 ++++++++ aplcore/unit/content/unittest_packages.cpp | 300 ++- aplcore/unit/content/unittest_rootconfig.cpp | 15 + .../unit/content/unittest_sharedjsondata.cpp | 118 + aplcore/unit/datagrammar/unittest_grammar.cpp | 40 +- .../unit/datagrammar/unittest_grammar_map.cpp | 10 +- aplcore/unit/embed/CMakeLists.txt | 1 + .../embed/unittest_embedded_importpackage.cpp | 251 ++ .../embed/unittest_embedded_lifecycle.cpp | 108 + aplcore/unit/engine/CMakeLists.txt | 2 + aplcore/unit/engine/unittest_builder_bind.cpp | 167 ++ aplcore/unit/engine/unittest_context.cpp | 6 +- .../engine/unittest_context_apl_version.cpp | 2 +- .../unit/engine/unittest_layout_handler.cpp | 269 ++ .../engine/unittest_reactive_rebuilds.cpp | 2177 +++++++++++++++++ aplcore/unit/engine/unittest_visibility.cpp | 73 + .../extension/unittest_extension_mediator.cpp | 386 ++- .../unittest_requested_extension.cpp | 45 +- aplcore/unit/primitives/unittest_filters.cpp | 20 +- aplcore/unit/primitives/unittest_object.cpp | 182 +- aplcore/unit/scenegraph/test_sg.cpp | 87 - aplcore/unit/scenegraph/test_sg.h | 163 +- aplcore/unit/scenegraph/unittest_sg_text.cpp | 217 +- aplcore/unit/test_sg_textmeasure.cpp | 105 + aplcore/unit/test_sg_textmeasure.h | 241 ++ aplcore/unit/testeventloop.cpp | 34 +- aplcore/unit/testeventloop.h | 80 +- aplcore/unit/text/CMakeLists.txt | 17 + aplcore/unit/text/unittest_text_layout.cpp | 538 ++++ .../touch/unittest_native_gestures_pager.cpp | 8 - .../unittest_native_gestures_scrollable.cpp | 44 +- aplcore/unit/touch/unittest_pointer.cpp | 152 ++ aplcore/unit/utils/CMakeLists.txt | 1 + aplcore/unit/utils/unittest_flags.cpp | 109 + aplcore/unit/versioning/CMakeLists.txt | 18 + .../versioning/unittest_semantic_pattern.cpp | 149 ++ .../versioning/unittest_semantic_version.cpp | 153 ++ bin/apl-header-inclusion-validation.sh | 6 + doc/core_objects.puml | 1 + extensions/alexaext/CMakeLists.txt | 1 + .../AplAudioPlayerExtension.h | 47 + ...AplAudioPlayerExtensionObserverInterface.h | 7 + .../AplMetricsExtensionV2.h | 144 ++ .../DestinationFactoryInterface.h | 68 + .../DestinationInterface.h | 47 + .../APLMetricsExtensionV2/MetricData.h | 49 + .../APLMetricsExtensionV2/MetricTracker.h | 108 + .../alexaext/include/alexaext/alexaext.h | 30 +- .../AplAudioPlayerExtension.cpp | 177 +- .../AplMetricsExtensionV2.cpp | 486 ++++ extensions/unit/CMakeLists.txt | 1 + extensions/unit/unittest_apl_audio_player.cpp | 161 +- extensions/unit/unittest_apl_metricV2.cpp | 890 +++++++ 209 files changed, 15540 insertions(+), 1677 deletions(-) create mode 100644 android/.gitignore create mode 100644 android/build.gradle create mode 100644 android/buildSrc/build.gradle create mode 100644 android/buildSrc/src/main/java/com/amazon/apl/android/CMakeTask.java create mode 100644 android/coreengine/.gitignore create mode 100644 android/coreengine/CMakeLists.txt create mode 100644 android/coreengine/build.gradle create mode 100644 android/coreengine/src/main/AndroidManifest.xml create mode 100644 android/coreengine/src/main/cpp/alexaext/include/.gitignore create mode 100644 android/coreengine/src/main/cpp/apl/include/.gitignore create mode 100644 android/coreengine/src/main/cpp/aplconfig/include/.gitignore create mode 100644 android/coreengine/src/main/cpp/aplsgconfig/include/.gitignore create mode 100644 android/coreengine/src/main/cpp/rapidjson/include/.gitignore create mode 100644 android/coreengine/src/main/java/com/amazon/apl/enums/.gitignore create mode 100644 android/gradle-version create mode 100644 android/gradle.properties create mode 100644 android/gradle/wrapper/gradle-wrapper.jar create mode 100755 android/gradlew create mode 100644 android/gradlew.bat create mode 100644 android/settings.gradle create mode 100644 aplcore/include/apl/action/importpackageaction.h create mode 100644 aplcore/include/apl/command/importpackagecommand.h create mode 100644 aplcore/include/apl/content/packagemanager.h create mode 100644 aplcore/include/apl/content/packageresolver.h create mode 100644 aplcore/include/apl/content/pendingimportpackage.h create mode 100644 aplcore/include/apl/content/sharedjsondata.h create mode 100644 aplcore/include/apl/engine/rebuilddependant.h create mode 100644 aplcore/include/apl/scenegraph/textlayoutcache.h create mode 100644 aplcore/include/apl/utils/constants.h create mode 100644 aplcore/include/apl/utils/flags.h create mode 100644 aplcore/include/apl/versioning/semanticgrammar.h create mode 100644 aplcore/include/apl/versioning/semanticpattern.h create mode 100644 aplcore/include/apl/versioning/semanticversion.h create mode 100644 aplcore/src/action/importpackageaction.cpp create mode 100644 aplcore/src/command/importpackagecommand.cpp create mode 100644 aplcore/src/content/packageresolver.cpp create mode 100644 aplcore/src/content/pendingimportpackage.cpp create mode 100644 aplcore/src/content/sharedjsondata.cpp create mode 100644 aplcore/src/versioning/CMakeLists.txt create mode 100644 aplcore/src/versioning/semanticgrammar.cpp create mode 100644 aplcore/src/versioning/semanticpattern.cpp create mode 100644 aplcore/src/versioning/semanticversion.cpp create mode 100644 aplcore/unit/command/unittest_command_importpackage.cpp create mode 100644 aplcore/unit/content/packagegenerator.h create mode 100644 aplcore/unit/content/testpackagemanager.cpp create mode 100644 aplcore/unit/content/testpackagemanager.h create mode 100644 aplcore/unit/content/unittest_packagemanager.cpp create mode 100644 aplcore/unit/content/unittest_sharedjsondata.cpp create mode 100644 aplcore/unit/embed/unittest_embedded_importpackage.cpp create mode 100644 aplcore/unit/engine/unittest_layout_handler.cpp create mode 100644 aplcore/unit/engine/unittest_reactive_rebuilds.cpp create mode 100644 aplcore/unit/test_sg_textmeasure.cpp create mode 100644 aplcore/unit/test_sg_textmeasure.h create mode 100644 aplcore/unit/text/CMakeLists.txt create mode 100644 aplcore/unit/text/unittest_text_layout.cpp create mode 100644 aplcore/unit/utils/unittest_flags.cpp create mode 100644 aplcore/unit/versioning/CMakeLists.txt create mode 100644 aplcore/unit/versioning/unittest_semantic_pattern.cpp create mode 100644 aplcore/unit/versioning/unittest_semantic_version.cpp create mode 100644 extensions/alexaext/include/alexaext/APLMetricsExtensionV2/AplMetricsExtensionV2.h create mode 100644 extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationFactoryInterface.h create mode 100644 extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationInterface.h create mode 100644 extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricData.h create mode 100644 extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricTracker.h create mode 100644 extensions/alexaext/src/APLMetricsExtensionV2/AplMetricsExtensionV2.cpp create mode 100644 extensions/unit/unittest_apl_metricV2.cpp diff --git a/CHANGELOG.md b/CHANGELOG.md index 719f9b3..7c583cb 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,24 @@ # Changelog +## [2024.2] + +This release adds support for version 2024.2 of the APL specification. + +### Added + +- Add "onLayout" handler for components. +- Add 'allowForward' and 'allowBackwards' properties to the event context of ScrollView, Sequence and GridSequence. +- Add 'screenLock' property to the video component +- Add text metrics to text component’s "onTextLayout" handler. +- Add -experimentalIsReactive and Reactive Conditional Inflation. +- Add 'pointerEvents' to control pointer events consumption rules. +- Add PackageManager API to handle imports. + +### Changed + +- Bug fixes +- Performance improvements + ## [2024.1] This release adds support for version 2024.1 of the APL specification. diff --git a/android/.gitignore b/android/.gitignore new file mode 100644 index 0000000..cca6dd6 --- /dev/null +++ b/android/.gitignore @@ -0,0 +1,17 @@ +.gradle +**/*.iml +apl/.externalNativeBuild/ +local.properties +captures/ +build/ +.git +build +.idea/ +.vscode +.DS_Store +**/.cxx/ +jacoco.exec +# Project-specific cache directory generated by Gradle +/.gradle/ +# Generated by run-gradlew +gradle/wrapper/gradle-wrapper.properties diff --git a/android/build.gradle b/android/build.gradle new file mode 100644 index 0000000..a241252 --- /dev/null +++ b/android/build.gradle @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +// Top-level build file where you can add configuration options common to all sub-projects/modules. + +def version = "2024.2.0"; + +buildscript { + repositories { + google() + jcenter() + } + dependencies { + classpath 'com.android.tools.build:gradle:8.0.2' + classpath 'org.jacoco:org.jacoco.core:0.8.8' + + // NOTE: Do not place your application dependencies here; they belong + // in the individual module build.gradle files + } +} + +subprojects { + apply plugin: 'maven-publish' + + if (System.getenv("VERSION")) { + project.version = System.getenv("VERSION") + } else if (System.getenv("BUILD_ID")) { + project.version = "${version}." + System.getenv("BUILD_ID") + } else { + project.version = "${version}." + System.currentTimeMillis() + } + project.group = "com.amazon.apl.android" + + repositories { + google() + jcenter() + } +} + +task clean(type: Delete) { + delete rootProject.buildDir +} diff --git a/android/buildSrc/build.gradle b/android/buildSrc/build.gradle new file mode 100644 index 0000000..598184f --- /dev/null +++ b/android/buildSrc/build.gradle @@ -0,0 +1,7 @@ +plugins { + id 'java' +} + +dependencies { + implementation gradleApi() +} \ No newline at end of file diff --git a/android/buildSrc/src/main/java/com/amazon/apl/android/CMakeTask.java b/android/buildSrc/src/main/java/com/amazon/apl/android/CMakeTask.java new file mode 100644 index 0000000..c1de14c --- /dev/null +++ b/android/buildSrc/src/main/java/com/amazon/apl/android/CMakeTask.java @@ -0,0 +1,95 @@ +package com.amazon.apl.android; + +import org.gradle.api.Action; +import org.gradle.api.DefaultTask; +import org.gradle.api.Task; +import org.gradle.api.file.RegularFile; +import org.gradle.api.plugins.JavaPluginExtension; +import org.gradle.api.provider.Property; +import org.gradle.api.provider.Provider; +import org.gradle.api.tasks.TaskAction; +import org.gradle.api.tasks.OutputDirectory; +import org.gradle.api.file.RegularFileProperty; +import org.gradle.api.tasks.compile.JavaCompile; +import org.gradle.jvm.toolchain.JavaCompiler; +import org.gradle.jvm.toolchain.JavaLanguageVersion; +import org.gradle.jvm.toolchain.JavaLauncher; +import org.gradle.jvm.toolchain.JavaToolchainService; +import org.gradle.jvm.toolchain.JavaToolchainSpec; +import org.gradle.jvm.toolchain.JvmImplementation; +import org.gradle.jvm.toolchain.JvmVendorSpec; +import org.gradle.process.ExecOperations; + +import java.io.File; +import java.util.ArrayList; +import java.util.List; +import java.util.Set; + +import javax.inject.Inject; + +class CMakeTask extends DefaultTask { + private final ExecOperations execOperations; + private List cmakeArgs = new ArrayList<>(); + private List makeTargets = new ArrayList<>(); + + private RegularFile buildDirectory; + + @OutputDirectory + public RegularFileProperty getBuildFolder() { + return getProject().getObjects().fileProperty() + .convention(buildDirectory); + } + + @Inject + public CMakeTask(ExecOperations execOperations) { + this.execOperations = execOperations; + cmakeArgs.add("../../../../"); + + buildDirectory = getProject().getLayout().getProjectDirectory().file(".cxx/cmake/debug/host/"); + } + + public void cmakeArgs(String... args) { + cmakeArgs.addAll(List.of(args)); + } + + public void makeTargets(String... targets) { + makeTargets.addAll(List.of(targets)); + } + + @TaskAction + public void run() { + String hostDir = buildDirectory.getAsFile().getAbsolutePath(); + + File folder = new File(hostDir); + if (!folder.exists()) { + folder.mkdirs(); + } + + // Find a java 17 jdk. The embedded one in Android Studio doesn't include JNI. + JavaToolchainService service = getProject().getExtensions().getByType(JavaToolchainService.class); + Provider compiler = service.compilerFor(javaToolchainSpec -> javaToolchainSpec.getLanguageVersion().set(JavaLanguageVersion.of(17))); + + if (!compiler.isPresent()) { + throw new RuntimeException("Unable to find a suitable jdk on the system."); + } + + String jdkPath = compiler.get().getMetadata().getInstallationPath().getAsFile().getAbsolutePath(); + + getProject().getLogger().error(jdkPath); + + execOperations.exec(execSpec -> { + execSpec.workingDir(hostDir); + execSpec.commandLine("cmake"); + execSpec.args(cmakeArgs); + execSpec.environment("JAVA_HOME", jdkPath); + }); + + execOperations.exec(execSpec -> { + execSpec.workingDir(hostDir); + execSpec.commandLine("make"); + execSpec.args(makeTargets); + execSpec.environment("JAVA_HOME", jdkPath); + }); + } +} + diff --git a/android/coreengine/.gitignore b/android/coreengine/.gitignore new file mode 100644 index 0000000..9ff2a37 --- /dev/null +++ b/android/coreengine/.gitignore @@ -0,0 +1,2 @@ +/build +/.cxx diff --git a/android/coreengine/CMakeLists.txt b/android/coreengine/CMakeLists.txt new file mode 100644 index 0000000..a4859e8 --- /dev/null +++ b/android/coreengine/CMakeLists.txt @@ -0,0 +1,158 @@ +# For more information about using CMake with Android Studio, read the +# documentation: https://d.android.com/studio/projects/add-native-code.html + +# Sets the minimum version of CMake required to build the native library. +include(FetchContent OPTIONAL RESULT_VARIABLE HAS_FETCH_CONTENT) + +cmake_minimum_required(VERSION 3.18.1) +set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) +set(CMAKE_CXX_STANDARD 11) +set(CMAKE_CXX_STANDARD_REQUIRED ON) +set(CMAKE_POSITION_INDEPENDENT_CODE ON) +project (apl-android-native VERSION 1.0.0 LANGUAGES C CXX) + +set(APL_PROJECT_DIR ${APL_PROJECT_DIR}) + +if (DEFINED SCENE_GRAPH) + set(ENABLE_SCENEGRAPH ${SCENE_GRAPH}) +endif() + +# Tell core to compile alexa extensions. +set(ENABLE_ALEXAEXTENSIONS ON) +set(BUILD_ALEXAEXTENSIONS ON) +set(USE_PROVIDED_YOGA_INLINE ON) +set(ENABLE_PIC ON) + +FetchContent_Declare( + aplcore + SOURCE_DIR ${APL_PROJECT_DIR} +) +FetchContent_MakeAvailable(aplcore) + +set(RAPIDJSON_INCLUDE "${CMAKE_BINARY_DIR}/_deps/rapidjson-src/include") +set(ENUMGEN_BIN "${CMAKE_BINARY_DIR}/tools/enumgen") + +add_custom_target(generate-android-enums ALL + COMMAND cd ${APL_PROJECT_DIR} && ${ENUMGEN_BIN} + -f "AnimationQuality" + -f "AudioPlayerEventType" + -f "BlendMode" + -f "ComponentType" + -f "ContainerDirection" + -f "DimensionType" + -f "Display" + -f "DisplayState" + -f "EventAudioTrack" + -f "EventControlMediaCommand" + -f "EventDirection" + -f "EventHighlightMode" + -f "EventProperty" + -f "EventReason" + -f "EventScrollAlign" + -f "EventType" + -f "EventMediaType" + -f "FilterType" + -f "FilterProperty" + -f "FlexboxAlign" + -f "FlexboxJustifyContent" + -f "FocusDirection" + -f "FontStyle" + -f "GradientProperty" + -f "GradientSpreadMethod" + -f "GradientType" + -f "GradientUnits" + -f "GraphicTextAnchor" + -f "GraphicElementType" + -f "GraphicLayoutDirection" + -f "GraphicLineCap" + -f "GraphicLineJoin" + -f "GraphicPropertyKey" + -f "GraphicFilterType" + -f "GraphicFilterProperty" + -f "GraphicScale" + -f "GraphicScale" + -f "ImageAlign" + -f "ImageCount" + -f "ImageScale" + -f "KeyHandlerType" + -f "LayoutDirection" + -f "MediaPlayerEventType" + -f "Navigation" + -f "NoiseFilterKind" + -f "Position" + -f "PointerEventType" + -f "PointerType" + -f "PropertyKey" + -f "RootProperty" + -f "ScreenShape" + -f "ScrollDirection" + -f "SpanAttributeName" + -f "SpanType" + -f "Snap" + -f "SpeechMarkType" + -f "TextAlign" + -f "TextAlignVertical" + -f "TextTrackType" + -f "TokenType" + -f "TrackState" + -f "UpdateType" + -f "VectorGraphicAlign" + -f "VectorGraphicScale" + -f "VideoScale" + -f "ViewportMode" + -f "AudioTrack" + -f "KeyboardType" + -f "SubmitKeyType" + -f "ScreenMode" + -f "Role" + -f "ExtensionComponentResourceState" + -l java -p com.amazon.apl.enums -o ${CMAKE_CURRENT_SOURCE_DIR}/src/main/java/com/amazon/apl/enums + ${APL_PROJECT_DIR}/aplcore/include/apl/action/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/animation/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/audio/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/command/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/component/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/content/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/datagrammar/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/document/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/engine/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/graphic/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/media/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/primitives/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/time/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/utils/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/touch/*.h + ${APL_PROJECT_DIR}/aplcore/include/apl/focus/*.h + DEPENDS enumgen + ) + +get_target_property(RAPIDJSON_INCLUDE rapidjson-apl INTERFACE_INCLUDE_DIRECTORIES) +add_custom_target(rapidjson ALL + COMMAND ${CMAKE_COMMAND} -E copy_directory ${RAPIDJSON_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/rapidjson/include + ) + +get_target_property(APL_INCLUDE apl INTERFACE_INCLUDE_DIRECTORIES) +get_target_property(ALEXAEXT_INCLUDE alexaext INTERFACE_INCLUDE_DIRECTORIES) + +add_custom_target(copy-headers + COMMAND ${CMAKE_COMMAND} -E copy_directory ${APL_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/apl/include + COMMAND ${CMAKE_COMMAND} -E copy_directory ${ALEXAEXT_INCLUDE} ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/alexaext/include + DEPENDS apl alexaext + ) + +if (ENABLE_SCENEGRAPH) + add_custom_target(copy-config + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/apl/include/apl/apl_config.h ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/aplsgconfig/include/apl/apl_config.h + DEPENDS copy-headers) +else() + add_custom_target(copy-config + COMMAND ${CMAKE_COMMAND} -E copy ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/apl/include/apl/apl_config.h ${CMAKE_CURRENT_SOURCE_DIR}/src/main/cpp/aplconfig/include/apl/apl_config.h + DEPENDS copy-headers) +endif() + +if (NOT ANDROID) + # Ensure jni.h is found + find_package(JNI REQUIRED) + include_directories(${JAVA_INCLUDE_PATH}) + include_directories(${JAVA_INCLUDE_PATH2}) +endif() diff --git a/android/coreengine/build.gradle b/android/coreengine/build.gradle new file mode 100644 index 0000000..da1bf5d --- /dev/null +++ b/android/coreengine/build.gradle @@ -0,0 +1,131 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +import org.apache.tools.ant.taskdefs.condition.Os + +apply plugin: 'com.android.library' +apply plugin: 'maven-publish' + +ext { + cmakeProjectPath = projectDir.absolutePath + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cmakeProjectPath = cmakeProjectPath.replace('\\', '/') + } + + rootDirectory = "${cmakeProjectPath}/../../" + cmakeArg = "-DAPL_PROJECT_DIR=${rootDirectory}" +} + +android { + namespace 'com.amazon.apl.android.coreengine' + compileSdk 33 + ndkVersion "23.0.7599858" + buildToolsVersion = "33.0.0" + + defaultConfig { + minSdkVersion 22 + targetSdkVersion 33 + externalNativeBuild { + cmake { + // Sets optional flags for the C++ compiler. + cppFlags "-std=c++11", "-fno-rtti", "-fno-exceptions" + // Build the APL Core JNI library (excludes all other targets) + targets "apl", "alexaext", "generate-android-enums", "rapidjson", "copy-headers", "copy-config" + // Enable APL Core JNI build, and be verbose. + arguments cmakeArg + } + } + } + + buildTypes { + releaseWithSceneGraph { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + matchingFallbacks = ['release'] + externalNativeBuild { + cmake { + arguments cmakeArg, "-DSCENE_GRAPH=ON" + } + } + } + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + externalNativeBuild { + cmake { + version "3.18.1" + + // Tells Gradle to find the root CMake APL build script. path is relative to + // the directory containing the module's build.gradle file. Gradle requires this + // build script to designate a CMake project as a build dependency and + // pull native sources into the Android project. + path "CMakeLists.txt" + } + } + publishing { + multipleVariants { + allVariants() + } + } + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + buildFeatures { + prefabPublishing true + } + prefab { + apl { + headers "src/main/cpp/apl/include" + } + alexaext { + headers "${project.property('rootDirectory')}/extensions/alexaext/include/" + } + rapidjson { + headers "src/main/cpp/rapidjson/include" + headerOnly true + } + } +} + +dependencies { +} + +publishing { + publications { + release(MavenPublication) { + afterEvaluate { + from components.default + } + } + } +} + +task buildHostBinary(type: com.amazon.apl.android.CMakeTask) { + cmakeArgs cmakeArg + makeTargets "apl", "alexaext" +} + +project.afterEvaluate { + // Dump configuration settings + println "APL Core Directory: " + cmakeArg + println "Android SDK Directory: " + android.sdkDirectory.path + println "Android NDK Directory: " + android.ndkDirectory.path + + // Since we generate enums using CMake, the java compilation has to occur after. + compileDebugJavaWithJavac.dependsOn externalNativeBuildDebug + compileReleaseJavaWithJavac.dependsOn externalNativeBuildRelease +} + +task release(dependsOn: ['build', 'publish', 'buildHostBinary']) { + doLast { + copy { + from '.cxx/cmake/debug/host/' + into '../build' + } + } +} diff --git a/android/coreengine/src/main/AndroidManifest.xml b/android/coreengine/src/main/AndroidManifest.xml new file mode 100644 index 0000000..a5918e6 --- /dev/null +++ b/android/coreengine/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/android/coreengine/src/main/cpp/alexaext/include/.gitignore b/android/coreengine/src/main/cpp/alexaext/include/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/android/coreengine/src/main/cpp/alexaext/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/android/coreengine/src/main/cpp/apl/include/.gitignore b/android/coreengine/src/main/cpp/apl/include/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/android/coreengine/src/main/cpp/apl/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/android/coreengine/src/main/cpp/aplconfig/include/.gitignore b/android/coreengine/src/main/cpp/aplconfig/include/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/android/coreengine/src/main/cpp/aplconfig/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/android/coreengine/src/main/cpp/aplsgconfig/include/.gitignore b/android/coreengine/src/main/cpp/aplsgconfig/include/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/android/coreengine/src/main/cpp/aplsgconfig/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/android/coreengine/src/main/cpp/rapidjson/include/.gitignore b/android/coreengine/src/main/cpp/rapidjson/include/.gitignore new file mode 100644 index 0000000..c96a04f --- /dev/null +++ b/android/coreengine/src/main/cpp/rapidjson/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/android/coreengine/src/main/java/com/amazon/apl/enums/.gitignore b/android/coreengine/src/main/java/com/amazon/apl/enums/.gitignore new file mode 100644 index 0000000..2a9bbbd --- /dev/null +++ b/android/coreengine/src/main/java/com/amazon/apl/enums/.gitignore @@ -0,0 +1 @@ +*.java \ No newline at end of file diff --git a/android/gradle-version b/android/gradle-version new file mode 100644 index 0000000..cf02201 --- /dev/null +++ b/android/gradle-version @@ -0,0 +1 @@ +8.3 diff --git a/android/gradle.properties b/android/gradle.properties new file mode 100644 index 0000000..74ff54a --- /dev/null +++ b/android/gradle.properties @@ -0,0 +1,19 @@ +# +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# SPDX-License-Identifier: Apache-2.0 +# + +# Project-wide Gradle settings. +# IDE (e.g. Android Studio) users: +# Gradle settings configured through the IDE *will override* +# any settings specified in this file. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx1536m +# When configured, Gradle will run in incubating parallel mode. +# This option should only be used with decoupled projects. More details, visit +# http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects +# org.gradle.parallel=true +android.useAndroidX=true diff --git a/android/gradle/wrapper/gradle-wrapper.jar b/android/gradle/wrapper/gradle-wrapper.jar new file mode 100644 index 0000000000000000000000000000000000000000..13536770052936a92b204cc34e72284a03a6903c GIT binary patch literal 56177 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 i;x|p)^~-yfPOPa3(|vBayXKz \(.*\)$'` + if expr "$link" : '/.*' > /dev/null; then + PRG="$link" + else + PRG=`dirname "$PRG"`"/$link" + fi +done +SAVED="`pwd`" +cd "`dirname \"$PRG\"`/" >/dev/null +APP_HOME="`pwd -P`" +cd "$SAVED" >/dev/null + +APP_NAME="Gradle" +APP_BASE_NAME=`basename "$0"` + +# Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +DEFAULT_JVM_OPTS="" + +# Use the maximum available, or set MAX_FD != -1 to use that value. +MAX_FD="maximum" + +warn () { + echo "$*" +} + +die () { + echo + echo "$*" + echo + exit 1 +} + +# OS specific support (must be 'true' or 'false'). +cygwin=false +msys=false +darwin=false +nonstop=false +case "`uname`" in + CYGWIN* ) + cygwin=true + ;; + Darwin* ) + darwin=true + ;; + MINGW* ) + msys=true + ;; + NONSTOP* ) + nonstop=true + ;; +esac + +CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar + +# Determine the Java command to use to start the JVM. +if [ -n "$JAVA_HOME" ] ; then + if [ -x "$JAVA_HOME/jre/sh/java" ] ; then + # IBM's JDK on AIX uses strange locations for the executables + JAVACMD="$JAVA_HOME/jre/sh/java" + else + JAVACMD="$JAVA_HOME/bin/java" + fi + if [ ! -x "$JAVACMD" ] ; then + die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." + fi +else + JAVACMD="java" + which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. + +Please set the JAVA_HOME variable in your environment to match the +location of your Java installation." +fi + +# Increase the maximum file descriptors if we can. +if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then + MAX_FD_LIMIT=`ulimit -H -n` + if [ $? -eq 0 ] ; then + if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then + MAX_FD="$MAX_FD_LIMIT" + fi + ulimit -n $MAX_FD + if [ $? -ne 0 ] ; then + warn "Could not set maximum file descriptor limit: $MAX_FD" + fi + else + warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" + fi +fi + +# For Darwin, add options to specify how the application appears in the dock +if $darwin; then + GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" +fi + +# For Cygwin, switch paths to Windows format before running java +if $cygwin ; then + APP_HOME=`cygpath --path --mixed "$APP_HOME"` + CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` + JAVACMD=`cygpath --unix "$JAVACMD"` + + # We build the pattern for arguments to be converted via cygpath + ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` + SEP="" + for dir in $ROOTDIRSRAW ; do + ROOTDIRS="$ROOTDIRS$SEP$dir" + SEP="|" + done + OURCYGPATTERN="(^($ROOTDIRS))" + # Add a user-defined pattern to the cygpath arguments + if [ "$GRADLE_CYGPATTERN" != "" ] ; then + OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" + fi + # Now convert the arguments - kludge to limit ourselves to /bin/sh + i=0 + for arg in "$@" ; do + CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` + CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option + + if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition + eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` + else + eval `echo args$i`="\"$arg\"" + fi + i=$((i+1)) + done + case $i in + (0) set -- ;; + (1) set -- "$args0" ;; + (2) set -- "$args0" "$args1" ;; + (3) set -- "$args0" "$args1" "$args2" ;; + (4) set -- "$args0" "$args1" "$args2" "$args3" ;; + (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; + (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; + (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; + (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; + (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; + esac +fi + +# Escape application args +save () { + for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done + echo " " +} +APP_ARGS=$(save "$@") + +# Collect all arguments for the java command, following the shell quoting and substitution rules +eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" + +# by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong +if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then + cd "$(dirname "$0")" +fi + +exec "$JAVACMD" "$@" diff --git a/android/gradlew.bat b/android/gradlew.bat new file mode 100644 index 0000000..f955316 --- /dev/null +++ b/android/gradlew.bat @@ -0,0 +1,84 @@ +@if "%DEBUG%" == "" @echo off +@rem ########################################################################## +@rem +@rem Gradle startup script for Windows +@rem +@rem ########################################################################## + +@rem Set local scope for the variables with windows NT shell +if "%OS%"=="Windows_NT" setlocal + +set DIRNAME=%~dp0 +if "%DIRNAME%" == "" set DIRNAME=. +set APP_BASE_NAME=%~n0 +set APP_HOME=%DIRNAME% + +@rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. +set DEFAULT_JVM_OPTS= + +@rem Find java.exe +if defined JAVA_HOME goto findJavaFromJavaHome + +set JAVA_EXE=java.exe +%JAVA_EXE% -version >NUL 2>&1 +if "%ERRORLEVEL%" == "0" goto init + +echo. +echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:findJavaFromJavaHome +set JAVA_HOME=%JAVA_HOME:"=% +set JAVA_EXE=%JAVA_HOME%/bin/java.exe + +if exist "%JAVA_EXE%" goto init + +echo. +echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% +echo. +echo Please set the JAVA_HOME variable in your environment to match the +echo location of your Java installation. + +goto fail + +:init +@rem Get command-line arguments, handling Windows variants + +if not "%OS%" == "Windows_NT" goto win9xME_args + +:win9xME_args +@rem Slurp the command line arguments. +set CMD_LINE_ARGS= +set _SKIP=2 + +:win9xME_args_slurp +if "x%~1" == "x" goto execute + +set CMD_LINE_ARGS=%* + +:execute +@rem Setup the command line + +set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar + +@rem Execute Gradle +"%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% + +:end +@rem End local scope for the variables with windows NT shell +if "%ERRORLEVEL%"=="0" goto mainEnd + +:fail +rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of +rem the _cmd.exe /c_ return code! +if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 +exit /b 1 + +:mainEnd +if "%OS%"=="Windows_NT" endlocal + +:omega diff --git a/android/settings.gradle b/android/settings.gradle new file mode 100644 index 0000000..5568bc1 --- /dev/null +++ b/android/settings.gradle @@ -0,0 +1,6 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +include ':coreengine' diff --git a/aplcore/CMakeLists.txt b/aplcore/CMakeLists.txt index d83005c..ebf04f8 100644 --- a/aplcore/CMakeLists.txt +++ b/aplcore/CMakeLists.txt @@ -86,9 +86,8 @@ add_subdirectory(src/scaling) add_subdirectory(src/time) add_subdirectory(src/touch) add_subdirectory(src/utils) -if(ENABLE_SCENEGRAPH) - add_subdirectory(src/scenegraph) -endif(ENABLE_SCENEGRAPH) +add_subdirectory(src/versioning) +add_subdirectory(src/scenegraph) set( PUBLIC_HEADER_LIST diff --git a/aplcore/include/apl/action/importpackageaction.h b/aplcore/include/apl/action/importpackageaction.h new file mode 100644 index 0000000..ea9f8aa --- /dev/null +++ b/aplcore/include/apl/action/importpackageaction.h @@ -0,0 +1,45 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef APL_IMPORT_PACKAGE_ACTION_H +#define APL_IMPORT_PACKAGE_ACTION_H + +#include "apl/action/action.h" + +namespace apl { + +class CoreCommand; + +class ImportPackageAction : public Action { +public: + static std::shared_ptr make(const TimersPtr& timers, + const std::shared_ptr& command, + ActionPtr&& startAction); + + ImportPackageAction(const TimersPtr& timers, + const std::shared_ptr& command, + ActionPtr&& startAction); + + void onLoad(const std::string& version); + void onFail(const std::string& nameVersionSource, const std::string& errorMessage, int code); + +private: + std::shared_ptr mCommand; + ActionPtr mCurrentAction; +}; + +} // namespace apl + +#endif // APL_IMPORT_PACKAGE_ACTION_H diff --git a/aplcore/include/apl/apl.h b/aplcore/include/apl/apl.h index 2714d26..2d09603 100644 --- a/aplcore/include/apl/apl.h +++ b/aplcore/include/apl/apl.h @@ -40,7 +40,9 @@ #include "apl/content/jsondata.h" #include "apl/content/metrics.h" #include "apl/content/package.h" +#include "apl/content/packagemanager.h" #include "apl/content/rootconfig.h" +#include "apl/content/sharedjsondata.h" #include "apl/datasource/datasourceconnection.h" #include "apl/datasource/datasourceprovider.h" #include "apl/document/documentcontext.h" @@ -76,6 +78,11 @@ #include "apl/utils/localemethods.h" #include "apl/utils/log.h" #include "apl/utils/session.h" +#include "apl/scenegraph/edittextbox.h" +#include "apl/scenegraph/textchunk.h" +#include "apl/scenegraph/textlayout.h" +#include "apl/scenegraph/textproperties.h" +#include "apl/scenegraph/textmeasurement.h" #ifdef SCENEGRAPH #include "apl/scenegraph/accessibility.h" #include "apl/scenegraph/edittextconfig.h" @@ -88,11 +95,6 @@ #include "apl/scenegraph/path.h" #include "apl/scenegraph/pathop.h" #include "apl/scenegraph/scenegraph.h" -#include "apl/scenegraph/edittextbox.h" -#include "apl/scenegraph/textchunk.h" -#include "apl/scenegraph/textlayout.h" -#include "apl/scenegraph/textmeasurement.h" -#include "apl/scenegraph/textproperties.h" #endif // SCENEGRAPH #ifdef ALEXAEXTENSIONS diff --git a/aplcore/include/apl/command/commandfactory.h b/aplcore/include/apl/command/commandfactory.h index 90e55e9..b02d166 100644 --- a/aplcore/include/apl/command/commandfactory.h +++ b/aplcore/include/apl/command/commandfactory.h @@ -21,7 +21,7 @@ #include "apl/action/action.h" #include "apl/command/command.h" #include "apl/engine/context.h" -#include "apl/component/corecomponent.h" +#include "apl/engine/properties.h" #include "apl/primitives/object.h" #include "apl/primitives/commanddata.h" diff --git a/aplcore/include/apl/command/commandproperties.h b/aplcore/include/apl/command/commandproperties.h index f479778..c980085 100644 --- a/aplcore/include/apl/command/commandproperties.h +++ b/aplcore/include/apl/command/commandproperties.h @@ -49,6 +49,7 @@ enum CommandType { kCommandTypeInsertItem, kCommandTypeRemoveItem, kCommandTypeLog, + kCommandTypeImportPackage, }; enum CommandScrollAlign { @@ -104,6 +105,7 @@ enum CommandLogLevel { }; enum CommandPropertyKey { + kCommandPropertyAccept, kCommandPropertyAlign, kCommandPropertyArguments, kCommandPropertyAt, @@ -128,7 +130,9 @@ enum CommandPropertyKey { kCommandPropertyLevel, kCommandPropertyMessage, kCommandPropertyMinimumDwellTime, + kCommandPropertyName, kCommandPropertyOnFail, + kCommandPropertyOnLoad, kCommandPropertyOtherwise, kCommandPropertyPosition, kCommandPropertyPreservedSequencers, @@ -144,6 +148,7 @@ enum CommandPropertyKey { kCommandPropertyTargetDuration, kCommandPropertyTransitionDuration, kCommandPropertyValue, + kCommandPropertyVersion, }; extern Bimap sCommandNameBimap; diff --git a/aplcore/include/apl/command/importpackagecommand.h b/aplcore/include/apl/command/importpackagecommand.h new file mode 100644 index 0000000..4a55ea9 --- /dev/null +++ b/aplcore/include/apl/command/importpackagecommand.h @@ -0,0 +1,39 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef APL_IMPORTPACKAGECOMMAND_H +#define APL_IMPORTPACKAGECOMMAND_H + +#include "apl/command/corecommand.h" + +namespace apl { + +class ImportPackageCommand : public TemplatedCommand { +public: + COMMAND_CONSTRUCTOR(ImportPackageCommand); + + const CommandPropDefSet& propDefSet() const override; + + CommandType type() const override { return kCommandTypeImportPackage; } + + ActionPtr execute(const TimersPtr& timers, bool fastMode) override; + +private: + PackageResolverPtr mPackageResolver; +}; + +} // namespace apl + +#endif // APL_IMPORTPACKAGECOMMAND_H diff --git a/aplcore/include/apl/common.h b/aplcore/include/apl/common.h index 443ea13..ac89f13 100644 --- a/aplcore/include/apl/common.h +++ b/aplcore/include/apl/common.h @@ -95,8 +95,14 @@ class MediaObject; class MediaPlayer; class MediaPlayerFactory; class Package; +class PackageManager; +class PackageResolver; +class PendingImportPackage; class RootConfig; class RootContext; +class SharedJsonData; +class SemanticPattern; +class SemanticVersion; class Session; class Settings; class SharedContextData; @@ -146,8 +152,13 @@ using MediaObjectPtr = std::shared_ptr; using MediaPlayerFactoryPtr = std::shared_ptr; using MediaPlayerPtr = std::shared_ptr; using PackagePtr = std::shared_ptr; +using PackageManagerPtr = std::shared_ptr; +using PackageResolverPtr = std::shared_ptr; +using PendingImportPackagePtr = std::shared_ptr; using RootConfigPtr = std::shared_ptr; using RootContextPtr = std::shared_ptr; +using SemanticPatternPtr = std::shared_ptr; +using SemanticVersionPtr = std::shared_ptr; using SessionPtr = std::shared_ptr; using SettingsPtr = std::shared_ptr; using SharedContextDataPtr = std::shared_ptr; diff --git a/aplcore/include/apl/component/component.h b/aplcore/include/apl/component/component.h index 8af5a3f..2d3f0a4 100644 --- a/aplcore/include/apl/component/component.h +++ b/aplcore/include/apl/component/component.h @@ -29,6 +29,7 @@ #include "apl/primitives/rect.h" #include "apl/utils/counter.h" #include "apl/utils/deprecated.h" +#include "apl/utils/flags.h" #include "apl/utils/userdata.h" #include "apl/utils/visitor.h" @@ -253,12 +254,12 @@ class Component : public UIDObject, * @return True if this component was properly created with all required * properties specified. */ - bool isValid() { return (mFlags & kComponentFlagInvalid) == 0; } + bool isValid() { return !mFlags.isSet(kComponentFlagInvalid); } /** * @return True if this component has been inflated and should now run event handlers on a SetValue or equivalent. */ - bool allowEventHandlers() { return (mFlags & kComponentFlagAllowEventHandlers) != 0; } + bool allowEventHandlers() { return mFlags.isSet(kComponentFlagAllowEventHandlers);} /** * An update message from the viewhost. This method is used for all updates that take @@ -516,16 +517,16 @@ class Component : public UIDObject, friend streamer& operator<<(streamer&, const Component&); friend class Builder; - std::string mId; - CalculatedPropertyMap mCalculated; // Current calculated object properties - std::set mDirty; + std::string mId; + CalculatedPropertyMap mCalculated; // Current calculated object properties + std::set mDirty; - enum { - kComponentFlagInvalid = 0x01, // Marks a component missing a required property - kComponentFlagAllowEventHandlers = 0x02, // Event handlers don't run when the component is first inflated + enum ComponentFlags : uint8_t { + kComponentFlagInvalid = 1u << 0, // Marks a component missing a required property + kComponentFlagAllowEventHandlers = 1u << 1, // Event handlers don't run when the component is first inflated }; - unsigned int mFlags = 0; + Flags mFlags; }; } // namespace apl diff --git a/aplcore/include/apl/component/componentproperties.h b/aplcore/include/apl/component/componentproperties.h index 788f962..6465fe9 100644 --- a/aplcore/include/apl/component/componentproperties.h +++ b/aplcore/include/apl/component/componentproperties.h @@ -357,6 +357,16 @@ enum ExtensionComponentResourceState { kResourceError }; +/** + * Pointer events handling + */ +enum PointerEvents { + /// Component can be the target of the pointer events + kPointerEventsAuto, + /// The component and its children will never be target of touch events + kPointerEventsNone, +}; + enum PropertyKey { // NOTE: ScrollDirection is placed early in the list so that it loads BEFORE height and width (see SequenceComponent.cpp) /// SequenceComponent scrolling direction (see #ScrollDirection) @@ -563,9 +573,12 @@ enum PropertyKey { kPropertyOnFail, /// ActionableComponent handler when focus is gained kPropertyOnFocus, + /// Component handler invoked on mount and on layout changes. + kPropertyOnLayout, /// MediaComponent handler for when the media loads kPropertyOnLoad, - /// Component or Document handler for first display of the component or document + /// Component or Document handler for first time when it's inflated and mounted + /// to the hierarchy. kPropertyOnMount, /// TouchableComponent handler for move kPropertyOnMove, @@ -587,6 +600,8 @@ enum PropertyKey { kPropertyOnSubmit, /// EditTextComponent Commands to execute when the text changes from a user event. kPropertyOnTextChange, + /// Component handler invoked on text layout changes. + kPropertyOnTextLayout, /// TouchableComponent handler for up kPropertyOnUp, /// VideoComponent handler for video time updates @@ -629,6 +644,8 @@ enum PropertyKey { kPropertyPlayingState, /// ContainerComponent child absolute or relative position (see #Position) kPropertyPosition, + /// Controls whether the component can be the target of touch events + kPropertyPointerEvents, /// Component properties to preserve over configuration changes kPropertyPreserve, /// TextComponent range for karaoke target @@ -771,6 +788,7 @@ extern Bimap sFlexboxJustifyContentMap; extern Bimap sFlexboxWrapMap; extern Bimap sNumberingMap; extern Bimap sPositionMap; +extern Bimap sPointerEventsMap; extern Bimap sScrollDirectionMap; extern Bimap sNavigationMap; extern Bimap sAudioTrackMap; diff --git a/aplcore/include/apl/component/corecomponent.h b/aplcore/include/apl/component/corecomponent.h index bc3fc57..7acd0c2 100644 --- a/aplcore/include/apl/component/corecomponent.h +++ b/aplcore/include/apl/component/corecomponent.h @@ -31,6 +31,7 @@ #include "apl/primitives/keyboard.h" #include "apl/primitives/size.h" #include "apl/primitives/transform2d.h" +#include "apl/utils/flags.h" #ifdef SCENEGRAPH #include "apl/scenegraph/common.h" @@ -351,7 +352,7 @@ class CoreComponent : public Component, /** * @return inheritParentState property */ - bool getInheritParentState() const { return mInheritParentState; } + bool getInheritParentState() const { return mCoreFlags.isSet(kCoreComponentFlagInheritParentState); } /** * @return The value for this component. Used by the SendEvent "components" array. @@ -590,14 +591,15 @@ class CoreComponent : public Component, /** * Walk the hierarchy updating child boundaries. * @param useDirtyFlag true to notify runtime about changes with dirty properties - * @param first true for first layout for current template + * @param first if this is the first layout for current template, false otherwise */ virtual void processLayoutChanges(bool useDirtyFlag, bool first); /** * After a layout has been completed, call this to execute any actions that may occur after a layout + * @param first if this is the first layout for current template, false otherwise */ - virtual void postProcessLayoutChanges(); + virtual void postProcessLayoutChanges(bool first); /** * Update the event object map with additional properties. These fill out "event.XXX" values other @@ -683,7 +685,7 @@ class CoreComponent : public Component, /** * @return true when the component has been disallowed by the runtime, false otherwise. */ - bool isDisallowed() const { return mIsDisallowed; } + bool isDisallowed() const { return mCoreFlags.isSet(kCoreComponentFlagIsDisallowed); } /** * Calculate real opacity of component. @@ -908,7 +910,7 @@ class CoreComponent : public Component, * parent when they need to determine whether their own cached transform is stale. This has the advantage * of scaling with the depth of a component in the tree, and not the total size of the tree. */ - void markGlobalToLocalTransformStale() { mGlobalToLocalIsStale = true; } + void markGlobalToLocalTransformStale() { mCoreFlags.set(kCoreComponentFlagGlobalToLocalIsStale); } /** * Check if component can consume focus event coming from particular direction (by taking focus or performing some @@ -1007,6 +1009,30 @@ class CoreComponent : public Component, */ void deregisterFromVisibilityTracking(); + /** + * Add child as valid visibility target. + * @param child this component's child. + */ + void addDownstreamVisibilityTarget(const CoreComponentPtr& child); + + /** + * Queue up item rebuild + * @param childContext current child context. + */ + void scheduleRebuildChange(const ContextPtr& childContext); + + /** + * Apply all pending rebuild changes. + */ + void processRebuildChanges(); + + /** + * Stash child-related context holding rebuild dependency. Only required when there are no + * existing child to hold the same (initially evaluated to nothing). + * @param context context to stash. + */ + void stashRebuildContext(const ContextPtr& context); + #ifdef SCENEGRAPH /** * @return The current scene graph node. @@ -1019,6 +1045,14 @@ class CoreComponent : public Component, void updateSceneGraph(sg::SceneGraphUpdates& sceneGraph); #endif // SCENEGRAPH + // Test only +#ifdef DEBUG_MEMORY_USE + bool isTempCacheClean() const { + return (!mChildrenChanges || mChildrenChanges->empty()) && + (!mPendingRebuildChanges || mPendingRebuildChanges->empty()); + } +#endif + protected: // internal, do not call directly virtual bool insertChild(const CoreComponentPtr& child, size_t index, bool useDirtyFlag); @@ -1076,7 +1110,7 @@ class CoreComponent : public Component, /** * @return hash of properties that could affect TextMeasurement. */ - std::string textMeasurementHash() const; + size_t textMeasurementHash() const; /** * Update text measurement hash @@ -1178,6 +1212,24 @@ class CoreComponent : public Component, */ bool isParentOf(const CoreComponentPtr& child); + /** + * Measure callback + * @param width Requested width + * @param widthMode Width measure mode + * @param height Requested height + * @param heightMode Height measure mode + * @return Yoga size of measured text + */ + virtual YGSize textMeasure(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode); + + /** + * Text baseline callback + * @param width Requested width + * @param height Requested height + * @return Baseline width. + */ + virtual float textBaseline(float width, float height); + private: friend streamer& operator<<(streamer&, const Component&); @@ -1246,20 +1298,41 @@ class CoreComponent : public Component, */ void ensureGlobalToLocalTransform(); - YGSize textMeasureInternal(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode); - float textBaselineInternal(float width, float height); - void fixAccessibilityActions(); void processChildrenChanges(); - void addDownstreamVisibilityTarget(const CoreComponentPtr& child); void removeDownstreamVisibilityTarget(const CoreComponentPtr& child); + /** + * Rebuild children set based on previously scheduled changes. Multi-child components defined + * through items set have constant elements (indexes/ordinals/etc) which may require a full + * rebuild. + * @see #scheduleRebuildChange(const ContextPtr& childContext) + */ + void rebuildItems(); + + /** + * Replace particular child with an new rebuilt one. + */ + void replaceChild(const ObjectArray& items, const CoreComponentPtr& child, const ContextPtr& childContext, int originIndex, int childIndex); + static std::string toStringAction(ChildChangeAction action); protected: - bool mInheritParentState; + /** + * Various flags used by component. + */ + enum CoreComponentFlags : uint8_t { + kCoreComponentFlagInheritParentState = 1u << 0, + kCoreComponentFlagDisplayedChildrenStale = 1u << 1, + kCoreComponentFlagIsDisallowed = 1u << 2, + kCoreComponentFlagGlobalToLocalIsStale = 1u << 3, + kCoreComponentFlagTextMeasurementHashStale = 1u << 4, + kCoreComponentFlagVisualHashStale = 1u << 5, + kCoreComponentFlagAccessibilityDirty = 1u << 6, + }; + State mState; // Operating state (pressed, checked, etc) std::string mStyle; // Name of the current STYLE Properties mProperties; // Assigned properties from JSON @@ -1271,8 +1344,7 @@ class CoreComponent : public Component, Path mPath; std::shared_ptr mRebuilder; Size mLayoutSize; - bool mDisplayedChildrenStale; - bool mIsDisallowed; + Flags mCoreFlags; #ifdef SCENEGRAPH sg::LayerPtr mSceneGraphLayer; #endif // SCENEGRAPH @@ -1281,22 +1353,24 @@ class CoreComponent : public Component, // The members below are used to store cached values for performance reasons, and not part of // the state of this component. struct ChildChange { - CoreComponentPtr component; + std::weak_ptr component; + std::string uid; ChildChangeAction action; size_t index; }; - std::vector mChildrenChanges; - Transform2D mGlobalToLocal; - bool mGlobalToLocalIsStale; Point mStickyOffset; - bool mTextMeasurementHashStale; - bool mVisualHashStale; - std::string mTextMeasurementHash; + size_t mTextMeasurementHash; timeout_id mTickHandlerId = 0; - bool mAccessibilityDirty = false; + + /// Permanent caches std::unique_ptr> mAffectedByVisibilityChange; + std::unique_ptr> mStashedRebuildCtxs; + + /// Temporary caches + std::unique_ptr> mChildrenChanges; + std::unique_ptr> mPendingRebuildChanges; }; } // namespace apl diff --git a/aplcore/include/apl/component/edittextcomponent.h b/aplcore/include/apl/component/edittextcomponent.h index 41e2ba5..b2396fb 100644 --- a/aplcore/include/apl/component/edittextcomponent.h +++ b/aplcore/include/apl/component/edittextcomponent.h @@ -20,9 +20,9 @@ #include "apl/apl_config.h" #include "apl/component/actionablecomponent.h" +#include "apl/scenegraph/common.h" #ifdef SCENEGRAPH #include "apl/scenegraph/edittext.h" -#include "apl/scenegraph/textproperties.h" #include "apl/utils/principal_ptr.h" #endif // SCENEGRAPH @@ -54,31 +54,38 @@ class EditTextComponent : public ActionableComponent { PointerCaptureStatus processPointerEvent(const PointerEvent& event, apl_time_t timestamp, bool onlyProcessGestures) override; void executeOnBlur() override; void executeOnFocus() override; - -#ifdef SCENEGRAPH - // Common scene graph handling - sg::LayerPtr constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) override; - bool updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) override; + YGSize textMeasure(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) override; + float textBaseline(float width, float height) override; private: YGSize measureEditText(MeasureRequest&& request); float baselineText(float width, float height); bool ensureEditTextBox(); - bool ensureEditConfig(); bool ensureEditTextProperties(); - bool ensureHintLayout(); private: MeasureRequest mLastMeasureRequest; - // This is created once with the scene graph and used to communicate with the view host - principal_ptr mEditText; - // These configure the edit control. They are nulled when an internal value changes and - // re-created during the scene graph update (or measure pass, for edit text box) + // re-created during the text measure pass sg::EditTextBoxPtr mEditTextBox; sg::TextPropertiesPtr mEditTextProperties; + +#ifdef SCENEGRAPH +private: + bool ensureHintLayout(); + bool ensureEditConfig(); + +protected: + // Common scene graph handling + sg::LayerPtr constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) override; + bool updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) override; + +private: + // This is created once with the scene graph and used to communicate with the view host + principal_ptr mEditText; + sg::EditTextConfigPtr mEditTextConfig; // These configure the hint display. They are nulled when an internal values changes and diff --git a/aplcore/include/apl/component/hostcomponent.h b/aplcore/include/apl/component/hostcomponent.h index 4b8f83e..95b47cc 100644 --- a/aplcore/include/apl/component/hostcomponent.h +++ b/aplcore/include/apl/component/hostcomponent.h @@ -47,7 +47,7 @@ class HostComponent : public ActionableComponent { void processLayoutChanges(bool useDirtyFlag, bool first) override; - void postProcessLayoutChanges() override; + void postProcessLayoutChanges(bool first) override; /** * Reinflate contained document. diff --git a/aplcore/include/apl/component/imagecomponent.h b/aplcore/include/apl/component/imagecomponent.h index e9bd1b0..5b70291 100644 --- a/aplcore/include/apl/component/imagecomponent.h +++ b/aplcore/include/apl/component/imagecomponent.h @@ -29,7 +29,7 @@ class ImageComponent : public CoreComponent, ComponentType getType() const override { return kComponentTypeImage; } - void postProcessLayoutChanges() override; + void postProcessLayoutChanges(bool first) override; protected: const EventPropertyMap& eventPropertyMap() const override; diff --git a/aplcore/include/apl/component/textcomponent.h b/aplcore/include/apl/component/textcomponent.h index b86b88d..61694cf 100644 --- a/aplcore/include/apl/component/textcomponent.h +++ b/aplcore/include/apl/component/textcomponent.h @@ -17,10 +17,8 @@ #define _APL_TEXT_COMPONENT_H #include "apl/component/corecomponent.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/textproperties.h" -#endif // SCENEGRAPH #include "apl/primitives/range.h" +#include "apl/scenegraph/textproperties.h" namespace apl { @@ -36,8 +34,6 @@ class TextComponent : public CoreComponent { Object getValue() const override; void preLayoutProcessing(bool useDirtyFlag) override; - rapidjson::Value serializeMeasure(rapidjson::Document::AllocatorType& allocator) const; - #ifdef SCENEGRAPH sg::TextLayoutPtr getTextLayout() const { return mLayout; } @@ -48,6 +44,10 @@ class TextComponent : public CoreComponent { private: void updateTextAlign(bool useDirtyFlag); + void ensureTextProperties(); + void ensureTextLayout(); + void onTextLayout(); + void postProcessLayoutChanges(bool first) override; protected: void handleLayoutDirectionChange(bool useDirtyFlag) override { updateTextAlign(useDirtyFlag); }; @@ -58,22 +58,24 @@ class TextComponent : public CoreComponent { std::string getVisualContextType() const override; + YGSize textMeasure(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) override; + float textBaseline(float width, float height) override; + +private: + sg::TextPropertiesPtr mTextProperties; + sg::TextChunkPtr mTextChunk; + sg::TextLayoutPtr mLayout; + bool mLayoutPossiblyStale; + #ifdef SCENEGRAPH +protected: // Common scene graph handling sg::LayerPtr constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) override; bool updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) override; private: Point calcSceneGraphOffset() const; - YGSize measureText(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode); - float baselineText(float width, float height); - void ensureSGTextLayout(); - void ensureTextProperties(); void populateTextNodes(sg::Node *transform); - - sg::TextPropertiesPtr mTextProperties; - sg::TextChunkPtr mTextChunk; - sg::TextLayoutPtr mLayout; #endif // SCENEGRAPH }; diff --git a/aplcore/include/apl/component/textmeasurement.h b/aplcore/include/apl/component/textmeasurement.h index be838b7..dfb9100 100644 --- a/aplcore/include/apl/component/textmeasurement.h +++ b/aplcore/include/apl/component/textmeasurement.h @@ -108,9 +108,7 @@ class TextMeasurement { float width, float height ) = 0; -#ifdef SCENEGRAPH - virtual bool sceneGraphCompatible() const { return false; } -#endif // SCENEGRAPH + virtual bool layoutCompatible() const { return false; } }; } // namespace apl diff --git a/aplcore/include/apl/component/vectorgraphiccomponent.h b/aplcore/include/apl/component/vectorgraphiccomponent.h index 878245a..e78e5dd 100644 --- a/aplcore/include/apl/component/vectorgraphiccomponent.h +++ b/aplcore/include/apl/component/vectorgraphiccomponent.h @@ -33,7 +33,7 @@ class VectorGraphicComponent : public TouchableComponent, bool updateGraphic(const GraphicContentPtr& json) override; void clearDirty() override; std::shared_ptr createTouchEventProperties(const Point &point) const override; - void postProcessLayoutChanges() override; + void postProcessLayoutChanges(bool first) override; bool isFocusable() const override; bool isActionable() const override; diff --git a/aplcore/include/apl/content/aplversion.h b/aplcore/include/apl/content/aplversion.h index a2b6c03..957bb0b 100644 --- a/aplcore/include/apl/content/aplversion.h +++ b/aplcore/include/apl/content/aplversion.h @@ -40,6 +40,7 @@ class APLVersion { kAPLVersion20232 = 0x1U << 13, /// Support version 2023.2 kAPLVersion20233 = 0x1U << 14, /// Support version 2023.3 kAPLVersion20241 = 0x1U << 15, /// Support version 2024.1 + kAPLVersion20242 = 0x1U << 16, /// Support version 2024.1 kAPLVersion10to11 = kAPLVersion10 | kAPLVersion11, /// Convenience ranges from 1.0 to latest, kAPLVersion10to12 = kAPLVersion10to11 | kAPLVersion12, kAPLVersion10to13 = kAPLVersion10to12 | kAPLVersion13, @@ -55,9 +56,10 @@ class APLVersion { kAPLVersion20231to20232 = kAPLVersion20222to20231 | kAPLVersion20232, kAPLVersion20232to20233 = kAPLVersion20231to20232 | kAPLVersion20233, kAPLVersion20233to20241 = kAPLVersion20232to20233 | kAPLVersion20241, - kAPLVersionLatest = kAPLVersion20233to20241, /// Support the most recent engine version - kAPLVersionDefault = kAPLVersion20233to20241, /// Default value - kAPLVersionReported = kAPLVersion20241, /// Default reported version + kAPLVersion20241to20242 = kAPLVersion20233to20241 | kAPLVersion20242, + kAPLVersionLatest = kAPLVersion20241to20242, /// Support the most recent engine version + kAPLVersionDefault = kAPLVersion20241to20242, /// Default value + kAPLVersionReported = kAPLVersion20242, /// Default reported version kAPLVersionAny = 0xffffffff, /// Support any versions in the list }; diff --git a/aplcore/include/apl/content/configurationchange.h b/aplcore/include/apl/content/configurationchange.h index b089f83..9f2ba58 100644 --- a/aplcore/include/apl/content/configurationchange.h +++ b/aplcore/include/apl/content/configurationchange.h @@ -231,6 +231,12 @@ class ConfigurationChange { */ RootConfig mergeRootConfig(const RootConfig& oldRootConfig) const; + /** + * Applies this configuration change to a root config object + * @param rootConfig The root config to apply this change to + */ + void applyToRootConfig(RootConfig& rootConfig) const; + /** * Merge this configuration change into an environment object bag. * @param oldEnvironment The old environment to merge with this change diff --git a/aplcore/include/apl/content/content.h b/aplcore/include/apl/content/content.h index 18c3cf2..b6bc0d9 100644 --- a/aplcore/include/apl/content/content.h +++ b/aplcore/include/apl/content/content.h @@ -20,6 +20,7 @@ #include "apl/content/extensionrequest.h" #include "apl/content/metrics.h" #include "apl/content/package.h" +#include "apl/content/packagemanager.h" #include "apl/content/settings.h" #include "apl/engine/properties.h" #include "apl/utils/counter.h" @@ -40,21 +41,8 @@ class RootConfig; * auto content = Content::create( document ); * if (!content) * return; // Failed to create the document - * if (checkRequests(content)) - * return READY_TO_GO; * - * // When a package comes in, call: - * content->addPackage(request, data); - * if (checkRequests(content)) - * return READY_TO_GO - * - * // Helper method to check for new packages that are needed - * bool checkRequests(ContentPtr& content) { - * for (ImportRequest request : content->getRequestedPackages()) - * // Request package "request" - * - * return content->isReady(); - * } + * content->load(std::move(handleSuccess), std::move(handleFailure)); * * The other aspect of content is connecting the named APL document parameters * with actual data sets. Use the addData() method to wire up parameter names @@ -94,6 +82,16 @@ class Content : public Counter, static ContentPtr create(JsonData&& document, const SessionPtr& session, const Metrics& metrics, const RootConfig& config); + using SuccessCallback = std::function; + using FailureCallback = std::function; + + /** + * Have this content load its imported packages from the PackageManager supplied via RootConfig. + * @param onSuccess the callback for when the content is ready. + * @param onFailure the callback for when the content failed to load. + */ + void load(SuccessCallback&& onSuccess, FailureCallback&& onFailure); + /** * Refresh content with new (or finally known) parameters. * @param metrics Viewport metrics. @@ -121,6 +119,8 @@ class Content : public Counter, PackagePtr getPackage(const std::string& name) const; /** + * @deprecated Supply a PackageManager and use load(SuccessCallback, FailureCallback). + * * Retrieve a set of packages that have been requested. This method only returns an * individual package a single time. Once it has been called, the "requested" packages * are moved internally into a "pending" list of packages. @@ -130,9 +130,11 @@ class Content : public Counter, std::set getRequestedPackages(); /** + * @deprecated Supply a PackageManager and use load(SuccessCallback, FailureCallback). + * * @return true if this document is waiting for a number of packages to be loaded. */ - bool isWaiting() const { return mRequested.size() > 0 || mPending.size() > 0; } + bool isWaiting() const; /** * @return True if this content is complete and ready to be inflated. @@ -145,6 +147,7 @@ class Content : public Counter, bool isError() const { return mState == State::ERROR; } /** + * @deprecated Supply a PackageManager and use load(SuccessCallback, FailureCallback). * Add a requested package to the document. * @param request The requested package import structure. * @param raw Parsed data for the package. @@ -257,6 +260,11 @@ class Content : public Counter, */ bool isMutable() const { return mEvaluationContext != nullptr; } + /** + * Document-wide setting to enable reactive conditional handling. + */ + bool reactiveConditionalInflation() const; + private: // Non-public methods used by other classes friend class CoreDocumentContext; @@ -279,24 +287,15 @@ class Content : public Counter, private: // Private internal methods void init(bool supportsEvaluation); - void addImportList(Package& package); - bool addImport( - Package& package, - const rapidjson::Value& value, - const std::string& name = "", - const std::string& version = "", - const std::set& loadAfter = {}); - void addExtensions(Package& package); + void addExtensions(const Package& package); void updateStatus(); void loadExtensionSettings(); - bool orderDependencyList(); - bool addToDependencyList(std::vector& ordered, std::set& inProgress, const PackagePtr& package); bool allowAdd(const std::string& name); std::string extractTheme(const Metrics& metrics) const; static ContentPtr create(JsonData&& document, const SessionPtr& session, const Metrics& metrics, const RootConfig& config, bool supportsEvaluation); Object extractBackground(const Context& evaluationContext) const; - void loadPackage(const ImportRef& ref, const PackagePtr& package); + bool isManual() const { return mConfig.getPackageManager() == nullptr; } private: enum State { @@ -305,6 +304,19 @@ class Content : public Counter, ERROR }; + /** + * Implementation of PackageManager for backwards compatibility. + */ + class ContentPackageManager : public PackageManager { + public: + void loadPackage(const PackageRequestPtr& packageRequest) override + { + mRequested.emplace(packageRequest->request()); + } + + std::set mRequested; + }; + private: SessionPtr mSession; PackagePtr mMainPackage; @@ -318,11 +330,10 @@ class Content : public Counter, RootConfig mConfig; ContextPtr mEvaluationContext; - std::set mRequested; - std::set mPending; - std::map mLoaded; - std::map mStashed; + PackageResolverPtr mPackageResolver; + std::shared_ptr mContentPackageManager; std::vector mOrderedDependencies; + PendingImportPackagePtr mCurrentPendingImports; std::map mParameterValues; std::vector mMainParameters; // Requested by the main template diff --git a/aplcore/include/apl/content/importref.h b/aplcore/include/apl/content/importref.h index 5758657..a633b4e 100644 --- a/aplcore/include/apl/content/importref.h +++ b/aplcore/include/apl/content/importref.h @@ -19,6 +19,9 @@ #include #include +#include "apl/versioning/semanticversion.h" +#include "apl/versioning/semanticpattern.h" + namespace apl { /** @@ -32,15 +35,17 @@ class ImportRef { ImportRef( const std::string& name, const std::string& version) - : ImportRef(name, version, "", std::set()) + : ImportRef(name, version, "", std::set(), nullptr, nullptr) {} ImportRef( const std::string& name, const std::string& version, const std::string& source, - const std::set& loadAfter) - : mName(name), mVersion(version), mSource(source), mLoadAfter(loadAfter) + const std::set& loadAfter, + const SemanticVersionPtr& semanticVersion, + const SemanticPatternPtr& acceptPattern) + : mName(name), mVersion(version), mSource(source), mLoadAfter(loadAfter), mSemanticVersion(semanticVersion), mAcceptPattern(acceptPattern) {} ImportRef(const ImportRef&) = default; @@ -52,8 +57,27 @@ class ImportRef { const std::string& version() const { return mVersion; } const std::string& source() const { return mSource; } const std::set& loadAfter() const { return mLoadAfter; } + const SemanticVersionPtr& semanticVersion() const { return mSemanticVersion; } + const SemanticPatternPtr& acceptPattern() const { return mAcceptPattern; } std::string toString() const { return mName + ":" + mVersion; } + /** + * Determines if this import is an acceptable replacement for the other import. + * + * @param other the import reference that may accept this one as a replacement + * @return true if this import reference is acceptable as a replacement for the other import. + */ + bool isAcceptableReplacementFor(const ImportRef& other) const { + // If names differ then other is not acceptable. + if (mName != other.mName) return false; + + // If this has no semantic version or the other does not have an accept pattern, then versions + // must match exactly. + if (!mSemanticVersion || !other.mAcceptPattern) return mVersion == other.mVersion; + + return other.mAcceptPattern->match(mSemanticVersion); + } + bool operator==(const ImportRef& other) const { return this->compare(other) == 0; } bool operator!=(const ImportRef& other) const { return this->compare(other) != 0; } bool operator<(const ImportRef& other) const { return this->compare(other) < 0; } @@ -68,6 +92,8 @@ class ImportRef { std::string mVersion; std::string mSource; std::set mLoadAfter; + SemanticVersionPtr mSemanticVersion; + SemanticPatternPtr mAcceptPattern; }; } // namespace apl diff --git a/aplcore/include/apl/content/importrequest.h b/aplcore/include/apl/content/importrequest.h index 91a9ff4..ad3aa44 100644 --- a/aplcore/include/apl/content/importrequest.h +++ b/aplcore/include/apl/content/importrequest.h @@ -22,6 +22,9 @@ #include "apl/content/importref.h" #include "rapidjson/document.h" +#include "apl/versioning/semanticpattern.h" +#include "apl/versioning/semanticversion.h" + namespace apl { class Object; @@ -32,26 +35,33 @@ class Object; */ class ImportRequest { public: + ImportRequest(); + /** - * Create ImportRequest. + * Creates an ImportRequest reusing existing requests that satisfy the accept criteria. * @param value JSON with package import specification. * @param context data binding context. * @param commonName name to be used if none specified in *value*. * @param commonVersion version to be used if none specified in *value*. * @param commonLoadAfter loadAfter to be used if none specified in *value*. + * @param commonAccept accept to be used if none specified in *value*. + * @param session session for reporting errors parsing version and accept * @return ImportRequest. May be invalid, use #isValid() to check. */ static ImportRequest create(const rapidjson::Value& value, const ContextPtr& context, - const std::string& commonName, - const std::string& commonVersion, - const std::set& commonLoadAfter); + const SessionPtr& session, + const std::string& commonName = "", + const std::string& commonVersion = "", + const std::set& commonLoadAfter = {}, + const std::string& commonAccept = ""); - ImportRequest(); ImportRequest(const std::string& name, const std::string& version, const std::string& source, - const std::set& loadAfter); + const std::set& loadAfter, + const SemanticVersionPtr& semanticVersion, + const SemanticPatternPtr& acceptPattern); ImportRequest(const ImportRequest&) = default; ImportRequest(ImportRequest&&) = default; @@ -69,8 +79,24 @@ class ImportRequest { uint32_t getUniqueId() const { return mUniqueId; } const std::string& source() const { return mReference.source(); } + /** + * Determines if this import is an acceptable replacement for the other import. + * + * Runtimes will want to keep a registry of cached imports by name to determine if a given cached + * import satisfies a new import request to prevent a network call. See the APL specification + * regarding import requests and accept. + * + * @param other the import request that may accept this one as a replacement. + * @return true if this import reference is acceptable as a replacement for the other import. + */ + bool isAcceptableReplacementFor(const ImportRequest& other) const { return mReference.isAcceptableReplacementFor(other.reference()); } + static std::pair extractNameAndVersion(const rapidjson::Value& value, const ContextPtr& context); static std::set extractLoadAfter(const rapidjson::Value& value, const ContextPtr& context); + static std::string extractAccept(const rapidjson::Value& value, const ContextPtr& context); + +private: + static std::string extractString(const std::string& key, const rapidjson::Value& value, const ContextPtr& context); private: ImportRef mReference; diff --git a/aplcore/include/apl/content/jsondata.h b/aplcore/include/apl/content/jsondata.h index 83f4762..44b2f0c 100644 --- a/aplcore/include/apl/content/jsondata.h +++ b/aplcore/include/apl/content/jsondata.h @@ -21,6 +21,8 @@ #include "rapidjson/document.h" #include "rapidjson/error/en.h" +#include "apl/content/sharedjsondata.h" + namespace apl { class Object; @@ -35,7 +37,7 @@ class Object; */ class JsonData { private: - enum Type { kDocument, kValue, kNullPtr }; + enum Type { kShared, kValue, kNullPtr }; public: /** @@ -43,8 +45,26 @@ class JsonData { * @param document A RapidJSON document */ JsonData(rapidjson::Document&& document) - : mDocument(std::move(document)), - mType(kDocument) + : mSharedJson(SharedJsonData(std::move(document))), + mType(kShared) + {} + + /** + * Initialize by moving an existing SharedJsonData. + * @param sharedJson A SharedJsonData to move in + */ + JsonData(SharedJsonData&& sharedJson) + : mSharedJson(std::move(sharedJson)), + mType(kShared) + {} + + /** + * Initialize by copying an existing SharedJsonData. + * @param sharedJson A SharedJsonData to copy from + */ + JsonData(const SharedJsonData& sharedJson) + : mSharedJson(sharedJson), + mType(kShared) {} /** @@ -52,6 +72,8 @@ class JsonData { * is not copied, so another agent must keep it alive during the * lifespan of this object. * @param value A RapidJSON value + * @deprecated Use SharedJsonData(const std::shared_ptr&, + const rapidjson::Pointer& pointer) instead */ JsonData(const rapidjson::Value& value) : mValuePtr(&value), @@ -63,11 +85,9 @@ class JsonData { * @param raw The string */ JsonData(const std::string& raw) - : mType(kDocument) - { - mDocument.Parse(raw.c_str()); - } + : mSharedJson(raw), + mType(kShared) + {} /** * Initialize by parsing a raw string. The string may be released @@ -75,34 +95,17 @@ class JsonData { * @param raw The string */ JsonData(const char *raw) - : mType(raw ? kDocument : kNullPtr) - { - if (raw != nullptr) - mDocument.Parse(raw); - } - - /** - * Initialize by parsing a raw string in situ. The string may be - * modified. Another agent must keep the raw string in memory until - * this object is destroyed. - * @param raw The string - */ - JsonData(char *raw) - : mType(raw ? kDocument : kNullPtr) - { - if (raw != nullptr) - mDocument.ParseInsitu(raw); - } + : mSharedJson(raw), + mType(kShared) + {} /** * @return True if this appears to be a valid JSON object. */ operator bool() const { switch (mType) { - case kDocument: - return !mDocument.HasParseError(); + case kShared: + return mSharedJson; case kValue: return true; case kNullPtr: @@ -114,17 +117,15 @@ class JsonData { /** * @return The offset of the first parse error. */ - size_t offset() const { - return mType == kDocument ? mDocument.GetErrorOffset() : 0; - } + size_t offset() const { return mType == kShared ? mSharedJson.offset() : 0; } /** * @return The human-readable error state of the parser. */ const char * error() const { switch (mType) { - case kDocument: - return rapidjson::GetParseError_En(mDocument.GetParseError()); + case kShared: + return mSharedJson.error(); case kValue: return "Value-constructed; no error"; case kNullPtr: @@ -142,7 +143,7 @@ class JsonData { /** * @return A reference to the top-level rapidjson Value. */ - const rapidjson::Value& get() const { return mType == kValue ? *mValuePtr : mDocument; } + const rapidjson::Value& get() const { return mType == kValue ? *mValuePtr : mSharedJson.get(); } /** * @return JSON serialized to a string. @@ -155,7 +156,7 @@ class JsonData { std::string toDebugString() const; private: - rapidjson::Document mDocument; + SharedJsonData mSharedJson; const rapidjson::Value *mValuePtr = nullptr; Type mType; }; diff --git a/aplcore/include/apl/content/package.h b/aplcore/include/apl/content/package.h index fa62b12..254d681 100644 --- a/aplcore/include/apl/content/package.h +++ b/aplcore/include/apl/content/package.h @@ -78,14 +78,9 @@ class Package : public Counter, friend class Content; friend class RootContext; - void addDependency(const ImportRef& ref) { mDependencies.push_back(ref); } - - const std::vector& getDependencies() const { return mDependencies; } - private: std::string mName; const JsonData mJson; - std::vector mDependencies; }; } // namespace apl diff --git a/aplcore/include/apl/content/packagemanager.h b/aplcore/include/apl/content/packagemanager.h new file mode 100644 index 0000000..1edc0bd --- /dev/null +++ b/aplcore/include/apl/content/packagemanager.h @@ -0,0 +1,138 @@ +/** + * 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_PACKAGEMANAGER_H +#define APL_PACKAGEMANAGER_H + +#include +#include + +#include "apl/content/importrequest.h" +#include "apl/content/sharedjsondata.h" + +namespace apl { + +/** + * Package Manager responsible for responding to Import Requests. + * + * The viewhost should pass in an implementation of this class to resolve import requests for the + * APL document. Furthermore, it is advisable to maintain a global package cache to reuse + * SharedJsonData for multiple of the same ImportRequest. + * + * The accept property allows for an existing cached package to be used in place of the requested Import. + * Therefore, when receiving a new request, check if it can match against other cached imports with + * the same name using the {@code isAcceptableReplacementFor(thisRequest)} check to send an acceptable package. + * + * For example: + * + * virtual void loadPackage(const PackageRequestPtr& packageRequest) override { + * ImportRequest& request = packageRequest->request(); + * + * // try to find exact match of name/version first + * if (auto packageData = mCache.get(request) { + * packageRequest->succeed(packageData); + * return; + * } + * + * // otherwise find a best match according to accept + * std::vector> matchedNames = mCache.getPackagesWithSameName(request.reference().name()); + * for (const auto& requestPackagePair : matchedNames) { + * if (requestPackagePair.first.isAcceptableReplacementFor(request)) { + * packageRequest->succeed(requestPackagePair.second); + * return; + * } + * } + * } + * + */ +class PackageManager { +public: + virtual ~PackageManager() = default; + /** + * Callback invoked when package is loaded. + * @param request The original import request. + * @param packageData The package JSON data. + */ + using SuccessCallback = std::function; + + /** + * Callback invoked when package cannot be loaded successfully. + * @param request The original import request. + * @param errorMessage The error message. + * @param errorCode The error code. + */ + using FailureCallback = std::function; + + /** + * A package request. + * + * @param request ImportRequest object containing package metadata. + * @param onSuccess Callback to invoke when package loaded. + * @param onFailure Callback to invoke when package failed to load. + */ + class PackageRequest { + public: + PackageRequest(const ImportRequest& request, SuccessCallback&& onSuccess, FailureCallback&& onFailure): + mRequest(request), + mOnSuccess(onSuccess), + mOnFailure(onFailure) {} + + /** + * @return the import request. + */ + const ImportRequest& request() { return mRequest; } + + /** + * Succeed the request with the package SharedJsonData. + * @param sharedJsonData + */ + void succeed(const SharedJsonData& sharedJsonData) { + mOnSuccess(mRequest, sharedJsonData); + clear(); + } + + /** + * Fail the request. + */ + void fail(const std::string& errorMessage, int code) { + mOnFailure(mRequest, errorMessage, code); + clear(); + } + + private: + void clear() { + mOnFailure = [](const ImportRequest&, const std::string& errorMessage, int errorCode){}; + mOnSuccess = [](const ImportRequest&, const SharedJsonData&){}; + } + + private: + const ImportRequest mRequest; + SuccessCallback mOnSuccess; + FailureCallback mOnFailure; + }; + + using PackageRequestPtr = std::shared_ptr; + + /** + * Request a package be imported. + * @param packageRequest The Package Request. + */ + virtual void loadPackage(const PackageRequestPtr& packageRequest) = 0; +}; + +} // namespace apl + + +#endif // APL_PACKAGEMANAGER_H diff --git a/aplcore/include/apl/content/packageresolver.h b/aplcore/include/apl/content/packageresolver.h new file mode 100644 index 0000000..6905daa --- /dev/null +++ b/aplcore/include/apl/content/packageresolver.h @@ -0,0 +1,133 @@ +/** + * 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_PACKAGERESOLVER_H +#define APL_PACKAGERESOLVER_H + +#include +#include +#include +#include + +#include "apl/common.h" +#include "apl/content/package.h" +#include "apl/content/packagemanager.h" +#include "apl/content/pendingimportpackage.h" + +namespace apl { + +/** + * Manages resolving the requested imports for a package. + */ +class PackageResolver : public std::enable_shared_from_this { +public: + /// Callback with the ordered list of packages. + using SuccessCallback = std::function&& ordered)>; + /// Callback for if one or more packages were unable to be downloaded or they could not be ordered. + using FailureCallback = std::function; + /// Callback for when packages are added. + using PackageAddedCallback = std::function; + + /** + * Creates a PackageResolver for resolving all the imports from a root Package. + * @param packageManager the package manager for retrieving requested imports. + * @param session the session for error logging. + * @return a pointer to a PackageResolver + */ + static PackageResolverPtr create(const PackageManagerPtr& packageManager, + const SessionPtr& session) + { + return std::make_shared(packageManager, session); + } + + /** + * Do not call this directly. Instead use create(const PackageManagerPtr& packageManager, const SessionPtr& session). + * @param packageManager the package manager for retrieving requested imports. + * @param session the session for error logging. + */ + PackageResolver(const PackageManagerPtr& packageManager, const SessionPtr& session) : mPackageManager(packageManager), mSession(session) {} + + /** + * Loads the packages that are requested from the root package. + * + * @param evaluationContext the context for evaluating imports (may be nullptr) + * @param session the session for logging + * @param root the root package which may have imports + * @param loadedPackages a vector of already loaded packages + * @param onSuccess the success callback for when all imports have been successfully + * resolved and ordered + * @param onFailure the failure callback for if one or more imports have not resolved + * or they cannot be ordered + * @param onPackageAdded a callback for handling package added updates. + * @return A pointer for the pending load. + */ + PendingImportPackagePtr load(const ContextPtr& evaluationContext, + const SessionPtr& session, + const PackagePtr& root, + const std::vector& loadedPackages, + SuccessCallback&& onSuccess, + FailureCallback&& onFailure, + PackageAddedCallback&& onPackageAdded = [](const Package& package){}); + + /** + * Loads the packages that are requested from the import request. + * + * @param evaluationContext the context for evaluating imports + * @param session the session + * @param request the import request + * @param onSuccess the success callback for when all imports have been successfully + * resolved and ordered + * @param onFailure the failure callback for if one or more imports have not resolved + * or they cannot be ordered + */ + void load(const ContextPtr& evaluationContext, + const SessionPtr& session, + const ImportRequest& request, + SuccessCallback&& onSuccess, + FailureCallback&& onFailure); + +private: + friend class Content; + + struct PendingLoad { + PendingImportPackagePtr pendingImport; + SuccessCallback onSuccess; + FailureCallback onFailure; + PackageAddedCallback onPackageAddedListener; + }; + + void setPackageManager(const PackageManagerPtr& packageManager) { mPackageManager = packageManager; } + void onPackageFailure(const ImportRequest& request, const std::string& errorMessage, int errorCode); + void onPackageLoaded(const ImportRequest& request, const SharedJsonData& jsonData) { + onPackageLoaded(request, JsonData(jsonData)); + } + PackagePtr createPackage(const ImportRef& ref, const SessionPtr& session, SharedJsonData&& jsonData) { + return createPackage(ref, session, JsonData(jsonData)); + } + void addPackage(const ImportRequest& request, const PackagePtr& package); + /// TODO delete these when Content no longer uses addPackage(ImportRequest&, JsonData&&) + void onPackageLoaded(const ImportRequest& request, JsonData&& jsonData); + PackagePtr createPackage(const ImportRef& ref, const SessionPtr& session, JsonData&& jsonData); + void loadRequested(PendingImportPackage& pending); + +private: + PackageManagerPtr mPackageManager; + SessionPtr mSession; + PendingLoad mPending; +}; + +} // namespace apl + +#endif // APL_PACKAGERESOLVER_H diff --git a/aplcore/include/apl/content/pendingimportpackage.h b/aplcore/include/apl/content/pendingimportpackage.h new file mode 100644 index 0000000..38836a0 --- /dev/null +++ b/aplcore/include/apl/content/pendingimportpackage.h @@ -0,0 +1,164 @@ +/** + * 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_PENDINGIMPORTPACKAGE_H +#define APL_PENDINGIMPORTPACKAGE_H + +#include +#include +#include +#include +#include +#include + +#include "apl/common.h" +#include "apl/content/packageresolver.h" +#include "apl/utils/path.h" +#include "apl/utils/session.h" + +namespace apl { + +/** + * Encapsulates state needed to resolve the dependencies of a Package (imports). + */ +class PendingImportPackage { +public: + /** + * Creates a PendingImportPackage + * @param context the evaluation context for imports (may be nullptr) + * @param session the session + * @param root the root package + * @param preLoaded any pre-loaded packages + */ + PendingImportPackage(const ContextPtr& context, const SessionPtr& session, const PackagePtr& root, const std::vector& preLoaded) : + mContext(context), + mSession(session), + mRoot(root), + mPreLoaded(preLoaded) + { + addImportList(*mRoot); + updateStatus(); + } + + /** + * Creates a PendingImportPackage + * @param context the evaluation context for imports (may be nullptr) + * @param session the session + * @param root the request + */ + PendingImportPackage(const ContextPtr& context, const SessionPtr& session, const ImportRequest& request) : + mContext(context), + mSession(session) + { + mRequested.emplace(request); + } + + /** + * Adds a package. + * @param request the import request + * @param package the package to add + */ + void addPackage(const ImportRequest& request, const PackagePtr& package); + + /** + * @return if the package tree is satisfied and well ordered + */ + bool isReady() const { return mState == State::READY; } + + /** + * @return if the package tree is not well ordered or added packages are not properly defined. + */ + bool isError() const { return mState == State::ERROR; } + + /* + * @return the failing request if there is one. + */ + const ImportRef& getFailedRequestReference() const { return mFailedRequestReference; } + + /** + * @return the error string if state is error. + */ + const std::string& getError() const { return mError; } + + /** + * @param request the import request + * @return checks if a package is pending for the import request. + */ + bool isPackagePending(const ImportRequest& request) { return mPending.count(request) > 0; } + + /** + * @return the set of requested packages and clears it. + */ + std::set getRequestedPackages(); + + /** + * @return move the ordered dependencies out of this object. + */ + std::vector&& moveOrderedDependencies() { return std::move(mOrderedDependencies); } + + /** + * @return a reference to the root package. + */ + PackagePtr getRoot() const { return mRoot; } + + /** + * Return a pre-loaded package by name. + * @param packageName the stashed package name + * @return a pre-loaded package if it exists, or nullptr. + */ + PackagePtr getPreLoadedPackage(const std::string& packageName) const; + +private: + void addImportList(Package& package); + bool addImport(Package& package, + const rapidjson::Value& value, + const std::string& name = "", + const std::string& version = "", + const std::set& loadAfter = {}, + const std::string& accept = ""); + bool addToDependencyList(std::vector& ordered, std::set& inProgress, const PackagePtr& package); + void updateStatus(); + void setError(const std::string& error); + bool orderDependencyList(); + ImportRequest createOrGetSuitableRequest(const ImportRequest& request); + +private: + enum class State { + LOADING, + READY, + ERROR + }; + + ContextPtr mContext; + SessionPtr mSession; + PackagePtr mRoot; + std::set mRequested; + std::set mPending; + std::map mLoaded; + std::map> mDependencies; + // Map of import name to created imports with that name. When creating an import, we look up + // this map first and return an existing one if it satisfies the import request. + std::map> mNameImportRequestMap; + std::vector mOrderedDependencies; + State mState = State::LOADING; + std::string mError = ""; + ImportRef mFailedRequestReference; + // list of preloaded packages to reuse + std::vector mPreLoaded; +}; + +} + +#endif // APL_PENDINGIMPORTPACKAGE_H diff --git a/aplcore/include/apl/content/rootconfig.h b/aplcore/include/apl/content/rootconfig.h index 13e5a46..7354971 100644 --- a/aplcore/include/apl/content/rootconfig.h +++ b/aplcore/include/apl/content/rootconfig.h @@ -172,6 +172,16 @@ class RootConfig { return *this; } + /** + * Specify the package manager used for loading imported packages. + * @param packageManager The package manager object. + * @return This object for chaining. + */ + RootConfig& packageManager(const PackageManagerPtr& packageManager) { + mPackageManager = packageManager; + return *this; + } + /** * Specify the media player factory used for creating media players for video * @param mediaPlayerFactory The media player factory object. @@ -936,6 +946,11 @@ class RootConfig { */ MediaManagerPtr getMediaManager() const { return mMediaManager; } + /** + * @return The configured package manager object + */ + PackageManagerPtr getPackageManager() const { return mPackageManager; } + /** * @return The configured media player factory */ @@ -1492,6 +1507,7 @@ class RootConfig { TextMeasurementPtr mTextMeasurement; DocumentManagerPtr mDocumentManager; MediaManagerPtr mMediaManager; + PackageManagerPtr mPackageManager; MediaPlayerFactoryPtr mMediaPlayerFactory; AudioPlayerFactoryPtr mAudioPlayerFactory; #ifdef SCENEGRAPH diff --git a/aplcore/include/apl/content/settings.h b/aplcore/include/apl/content/settings.h index 139e8e6..3114578 100644 --- a/aplcore/include/apl/content/settings.h +++ b/aplcore/include/apl/content/settings.h @@ -55,6 +55,11 @@ class Settings { return config.getDefaultIdleTimeout(); } + bool reactiveConditionalInflation(const RootConfig & config) const { + Object value = getValue("-experimentalIsReactive"); + return value.asBoolean(); + } + /** * @deprecated removed in APL 1.4 * @return Recommended time in milliseconds that the document should be kept on the screen diff --git a/aplcore/include/apl/content/sharedjsondata.h b/aplcore/include/apl/content/sharedjsondata.h new file mode 100644 index 0000000..0cbd3b0 --- /dev/null +++ b/aplcore/include/apl/content/sharedjsondata.h @@ -0,0 +1,180 @@ +/** + * 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_SAFE_JSON_H +#define _APL_SAFE_JSON_H + +#include +#include + +#include "rapidjson/document.h" +#include "rapidjson/error/en.h" +#include "rapidjson/pointer.h" +#include "rapidjson/stringbuffer.h" + +namespace apl { + +/** + * Wrapper class for holding JSON data. + * + * There are a variety of ways of receiving JSON data including loading + * directly from a string or character pointer, loading from a parsed file, + * and loading from within a directive. This wrapper class holds the parsed + * JSON data with a consistent surface area. Unlike the JsonData class, the + * life cycle of the JSON data is extended to this object. + */ +class SharedJsonData { + +public: + /** + * Initialize by moving an existing JSON document. + * @param document A RapidJSON document + */ + explicit SharedJsonData(rapidjson::Document&& document) + : mDocument(std::make_shared(std::move(document))) + { + mValuePtr = mDocument.get(); + } + + /** + * Initialize by sharing an existing JSON document. + * @param document A RapidJSON document + */ + explicit SharedJsonData(const std::shared_ptr& document) + : mDocument(document) + { + assert(mDocument); + mValuePtr = mDocument.get(); + } + + /** + * Initialize by reference to a portion of an existing JSON document. + * @param document A RapidJSON document + * @param pointer Pointer to an object of the JSON document + */ + SharedJsonData(const std::shared_ptr& document, + const rapidjson::Pointer& pointer) + : mDocument(document) + { + assert(mDocument); + + if (!pointer.IsValid()) { + mError = "Bad rapidjson::Pointer: Code " + std::to_string(pointer.GetParseErrorCode()) + + " at " + std::to_string(pointer.GetParseErrorOffset()); + mDocument = nullptr; + return; + } + + mValuePtr = rapidjson::GetValueByPointer(*mDocument, pointer); + if (!mValuePtr) { + rapidjson::StringBuffer sb; + pointer.Stringify(sb); + mError = "Invalid pointer path: " + std::string(sb.GetString()); + mDocument = nullptr; + } + } + + /** + * Initialize by parsing a std::string. + * @param raw The string + */ + explicit SharedJsonData(const std::string& raw) + : mDocument(std::make_shared()) + { + mDocument->Parse(raw.c_str()); + mValuePtr = mDocument.get(); + } + + /** + * Initialize by parsing a raw string. The string may be released + * immediately. + * @param raw The string + */ + explicit SharedJsonData(const char *raw) + { + if (!raw) { + mError = "Nullptr"; + return; + } + + mDocument = std::make_shared(); + mDocument->Parse(raw); + mValuePtr = mDocument.get(); + } + + /** + * @return True if this appears to be a valid JSON object. + */ + operator bool() const { + return mDocument && mError.empty() && !mDocument->HasParseError(); + } + + /** + * @return The offset of the first parse error. + */ + size_t offset() const { + if (!mDocument) return 0; + if (!mError.empty()) return 0; + return mDocument->GetErrorOffset(); + } + + /** + * @return The human-readable error state of the parser. + */ + const char * error() const { + if (!mError.empty()) return mError.c_str(); + if (!mDocument) return "Nullptr"; + return rapidjson::GetParseError_En(mDocument->GetParseError()); + } + + /** + * @return The shared_ptr of Document that ensures the life cycle. + */ + const std::shared_ptr& getSharedDoc() const { return mDocument; } + + /** + * @return A reference to the top-level rapidjson Value. + */ + const rapidjson::Value& get() const { + assert(mValuePtr); + return *mValuePtr; + } + + /** + * @return JSON serialized to a string. + */ + std::string toString() const; + + /** + * @return Readable string representation of data for debug. + */ + std::string toDebugString() const; + +private: + friend class JsonData; + SharedJsonData() = default; + +private: + // mDocument is used to hold the strong reference of the JSON data + std::shared_ptr mDocument; + const rapidjson::Value* mValuePtr = nullptr; + std::string mError; +}; + +} // namespace APL + +#endif //_APL_SAFE_JSON_H diff --git a/aplcore/include/apl/document/coredocumentcontext.h b/aplcore/include/apl/document/coredocumentcontext.h index e685557..02c30bd 100644 --- a/aplcore/include/apl/document/coredocumentcontext.h +++ b/aplcore/include/apl/document/coredocumentcontext.h @@ -281,6 +281,7 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr #ifdef ALEXAEXTENSIONS friend class ExtensionMediator; #endif + friend class ImportPackageCommand; void init(const Metrics& metrics, const RootConfig& config, @@ -291,6 +292,8 @@ class CoreDocumentContext : public DocumentContext, public std::enable_shared_fr const APLVersion& compatibilityVersion); bool verifyTypeField(const std::vector& ordered, bool enforce); ObjectMapPtr createDocumentEventProperties(const std::string& handler) const; + void processPackagesIntoContext(const std::vector packages); + bool isPackageProcessed(const std::string& packageName) const { return mCore->mProcessedPackages.count(packageName) > 0; } private: friend class CoreRootContext; diff --git a/aplcore/include/apl/document/documentcontextdata.h b/aplcore/include/apl/document/documentcontextdata.h index 9962028..09c00d2 100644 --- a/aplcore/include/apl/document/documentcontextdata.h +++ b/aplcore/include/apl/document/documentcontextdata.h @@ -30,6 +30,13 @@ namespace apl { +#ifndef SCENEGRAPH +namespace sg { +class TextLayoutCache; +class TextPropertiesCache; +} +#endif // SCENEGRAPH + class ExtensionManager; class FocusManager; class HoverManager; @@ -144,12 +151,15 @@ class DocumentContextData : public ContextData, public std::enable_shared_from_t */ DocumentContextPtr documentContext() const { return mDocument.lock(); } -#ifdef SCENEGRAPH + /** + * @return A cache of TextLayout + */ + sg::TextLayoutCache& textLayoutCache(); + /** * @return A cache of TextProperties */ sg::TextPropertiesCache& textPropertiesCache(); -#endif // SCENEGRAPH double getWidth() const { return mMetrics.getWidth(); } double getHeight() const { return mMetrics.getHeight(); } @@ -190,6 +200,7 @@ class DocumentContextData : public ContextData, public std::enable_shared_from_t WeakPtrSet mPendingOnMounts; std::set mDirtyVisualContext; std::set mDirtyDatasourceContext; + std::map mProcessedPackages; #ifdef ALEXAEXTENSIONS std::queue mExtensionEvents; #endif diff --git a/aplcore/include/apl/engine/bindingchange.h b/aplcore/include/apl/engine/bindingchange.h index 0c516bb..1f811f8 100644 --- a/aplcore/include/apl/engine/bindingchange.h +++ b/aplcore/include/apl/engine/bindingchange.h @@ -66,6 +66,15 @@ class BindingChange { using BindingChangePtr = std::shared_ptr; +/** + * Check if binding with provided name valid in provided context. + * @param context Context. + * @param binding binding definition. + * @param name Binding name. + * @return true if valid, false otherwise. + */ +extern bool isValidBinding(const ContextPtr& context, const Object& binding, const std::string& name); + /** * Process the "bind" variable in a component or graphic element and add all bound values * to the data-binding context. diff --git a/aplcore/include/apl/engine/builder.h b/aplcore/include/apl/engine/builder.h index e1167ab..aafce36 100644 --- a/aplcore/include/apl/engine/builder.h +++ b/aplcore/include/apl/engine/builder.h @@ -16,10 +16,12 @@ #ifndef _APL_BUILDER_H #define _APL_BUILDER_H -#include "apl/component/corecomponent.h" +#include "apl/common.h" +#include "apl/engine/properties.h" namespace apl { +class LayoutRebuilder; class Path; class InsertItemCommand; @@ -32,6 +34,7 @@ using MakeComponentFunc = std::function& symbolIgnoreList = {}); + + // Generate different child contexts + static ContextPtr createFirstItemContext(const ContextPtr& parent); + static ContextPtr createLastItemContext(const ContextPtr& parent); + static ContextPtr createIndexItemContext(const ContextPtr& parent, int sourceIndex, int itemIndex, size_t numberOfItems, bool numbered, int ordinal); private: - MakeComponentFunc findComponentBuilderFunc(const ContextPtr& context, const std::string &type); + MakeComponentFunc findComponentBuilderFunc(const ContextPtr& context, const std::string &type) const; CoreComponentPtr mOld; }; diff --git a/aplcore/include/apl/engine/context.h b/aplcore/include/apl/engine/context.h index e0b950e..13882f1 100644 --- a/aplcore/include/apl/engine/context.h +++ b/aplcore/include/apl/engine/context.h @@ -28,16 +28,13 @@ #include "apl/engine/styleinstance.h" #include "apl/primitives/object.h" #include "apl/primitives/textmeasurerequest.h" +#include "apl/scenegraph/common.h" #include "apl/utils/counter.h" #include "apl/utils/localemethods.h" #include "apl/utils/lrucache.h" #include "apl/utils/noncopyable.h" #include "apl/utils/path.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH - namespace apl { class DataSourceConnection; @@ -618,12 +615,15 @@ class Context : public RecalculateTarget, */ DocumentContextPtr documentContext() const; -#ifdef SCENEGRAPH + /** + * @return A cache of TextLayout + */ + sg::TextLayoutCache& textLayoutCache() const; + /** * @return A cache of TextProperties */ sg::TextPropertiesCache& textPropertiesCache() const; -#endif // SCENEGRAPH void pushEvent(Event&& event); diff --git a/aplcore/include/apl/engine/corerootcontext.h b/aplcore/include/apl/engine/corerootcontext.h index d857141..90f5ab8 100644 --- a/aplcore/include/apl/engine/corerootcontext.h +++ b/aplcore/include/apl/engine/corerootcontext.h @@ -41,11 +41,9 @@ class CoreRootContext : public std::enable_shared_from_this, pu /** * Public constructor. Use the ::create method instead. - * @param metrics Display metrics - * @param content Processed APL content data * @param config Configuration information */ - CoreRootContext(const Metrics& metrics, const ContentPtr& content, const RootConfig& config); + CoreRootContext(const RootConfig& config); ~CoreRootContext() override; @@ -94,6 +92,7 @@ class CoreRootContext : public std::enable_shared_from_this, pu Info info() const override; void updateCursorPosition(Point cursorPosition) override; bool handlePointerEvent(const PointerEvent& pointerEvent) override; + bool handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t timestamp) override; bool handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) override; const RootConfig& getRootConfig() const override; std::string getTheme() const override; diff --git a/aplcore/include/apl/engine/evaluate.h b/aplcore/include/apl/engine/evaluate.h index bd0867e..bd03511 100644 --- a/aplcore/include/apl/engine/evaluate.h +++ b/aplcore/include/apl/engine/evaluate.h @@ -16,6 +16,8 @@ #ifndef _APL_EVALUATE_H #define _APL_EVALUATE_H +#include + #include "apl/engine/binding.h" #include "apl/primitives/boundsymbolset.h" #include "apl/primitives/object.h" @@ -79,7 +81,7 @@ struct ParseResult { */ struct ApplyResult { Object value; /// The calculated value after all data-binding expressions are evaluated - BoundSymbolSet symbols; // The bound symbols used when calculating the value. + BoundSymbolSet symbols; /// The bound symbols used when calculating the value. }; @@ -125,37 +127,108 @@ int propertyAsInt(const Context& context, const Object& object, const char *name Object propertyAsObject(const Context& context, const Object& object, const char *name); Object propertyAsRecursive(const Context& context, const Object& object, const char *name); + /** - * Look up a mapped property. Return (T) -1 if the property is invalid. If the property - * is not specified, return defValue. + * Look up a mapped property. Return a pair of { T, bool } of the calculated value and a boolean + * indicating if the calculated value is valid. The calculated value is only valid if (a) the + * property existed in the item and (b) the property value maps correctly to one of the defined + * values in the bimap. + * * @tparam T The enumerated type (should map to int). * @param context The data-binding context. * @param item The host object which contains the property (must be a map). * @param name The name of the property. - * @param defValue The default value to return if the property is not defined. * @param bimap The bimap to use to map from valid name to value. - * @return The property value or -1 if the property is invalid. + * @return std::pair{ value, bool } where the bool is true only if the property was valid. */ template -T propertyAsMapped(const Context& context, - const Object& item, - const char *name, - T defValue, - const Bimap& bimap) +std::pair +requiredMappedProperty(const Context& context, + const Object& item, + const char *name, + const Bimap& bimap) { - if (!item.isMap()) - return static_cast(-1); + if (!item.isMap() || !item.has(name)) + return { static_cast(-1), false }; + + auto s = evaluate(context, item.get(name)).asString(); + if (s.empty()) + return { static_cast(-1), false }; + + auto it = bimap.find(s); + if (it == bimap.endBtoA()) + return { static_cast(-1), false }; - if (!item.has(name)) + return { it->second, true }; +} + +/** + * Look up a mapped property. Return the value of the mapped property if it is found and valid. + * Otherwise return the default value. + * + * @tparam T The enumerated type (should map to int). + * @param context The data-binding context. + * @param item The host object which contains the property (must be a map). + * @param name The name of the property. + * @param defValue The default value. + * @param bimap The bimap to use to map from valid name to value. + * @return The mapped value or the default value. + */ +template +T +optionalMappedProperty(const Context& context, + const Object& item, + const char *name, + T defValue, + const Bimap& bimap) +{ + if (!item.isMap() || !item.has(name)) return defValue; auto s = evaluate(context, item.get(name)).asString(); if (s.empty()) return defValue; - return bimap.get(s, static_cast(-1)); + return bimap.get(s, defValue); } + +/** + * Look up a mapped property. If the property isn't specified, return the default value. + * If it is specified, but invalid, return an error. + * + * @tparam T The enumerated type (should map to int). + * @param context The data-binding context. + * @param item The host object which contains the property (must be a map). + * @param name The name of the property. + * @param defValue The default value. + * @param bimap The bimap to use to map from valid name to value. + * @return A pair of (value, boolean), where the boolean is true if the value is valid. + */ +template +std::pair +optionalStrictMappedProperty(const Context& context, + const Object& item, + const char *name, + T defValue, + const Bimap& bimap) +{ + if (!item.isMap() || !item.has(name)) + return { defValue, true }; + + auto s = evaluate(context, item.get(name)).asString(); + if (s.empty()) + return { defValue, false }; + + auto it = bimap.find(s); + if (it == bimap.endBtoA()) + return { defValue, false }; + + return { it->second, true }; +} + + + } // Namespace apl #endif // _APL_EVALUATE_H diff --git a/aplcore/include/apl/engine/rebuilddependant.h b/aplcore/include/apl/engine/rebuilddependant.h new file mode 100644 index 0000000..b4ad382 --- /dev/null +++ b/aplcore/include/apl/engine/rebuilddependant.h @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + * + */ + +#ifndef _APL_REBUILD_DEPENDANT_H +#define _APL_REBUILD_DEPENDANT_H + +#include +#include + +#include "apl/engine/dependant.h" +#include "apl/engine/evaluate.h" + +namespace apl { + +class RebuildDependant : public Dependant { +public: + static void create(const std::shared_ptr& parentComponent, + const ContextPtr& downstream, + BoundSymbolSet symbols) { + assert(!symbols.empty()); + assert(parentComponent); + + auto dependant = std::make_shared(parentComponent, + downstream, + sBindingFunctions.at(kBindingTypeBoolean), + std::move(symbols)); + dependant->attach(); + // Register under "pseudo" upstream to disambiguate from any other upstreams. + downstream->addUpstream("_SPECIAL_WHEN_CONDITIONAL", dependant); + } + + RebuildDependant(const CoreComponentPtr& parentComponent, + const ContextPtr& downstream, + BindingFunction bindingFunction, + BoundSymbolSet symbols) + : Dependant(Object::NULL_OBJECT(), + downstream, + std::move(bindingFunction), + std::move(symbols)), + mParentComponent(parentComponent), + mDownstreamContext(downstream) + {} + + ~RebuildDependant() override = default; + + void recalculate(bool useDirtyFlag) override { + auto parentComponent = mParentComponent.lock(); + auto downstreamCtx = mDownstreamContext.lock(); + if (parentComponent && downstreamCtx) { + parentComponent->scheduleRebuildChange(downstreamCtx); + } + } + +private: + std::weak_ptr mParentComponent; + std::weak_ptr mDownstreamContext; +}; + +} // namespace apl + +#endif //_APL_REBUILD_DEPENDANT_H diff --git a/aplcore/include/apl/engine/rootcontext.h b/aplcore/include/apl/engine/rootcontext.h index f43cc43..6a9a391 100644 --- a/aplcore/include/apl/engine/rootcontext.h +++ b/aplcore/include/apl/engine/rootcontext.h @@ -369,6 +369,14 @@ class RootContext : public UserData, */ virtual bool handlePointerEvent(const PointerEvent& pointerEvent) = 0; + /** + * Handle a given PointerEvent with coordinates relative to the viewport. + * @param pointerEvent The pointer event to handle. + * @param timestamp The pointer timestamp. + * @return true if was consumed and should not be passed through any platform handling. + */ + virtual bool handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t timestamp) = 0; + /** * An update message from the viewhost called when a key is pressed. The * keyboard message is directed to the focused component, or the document diff --git a/aplcore/include/apl/engine/sharedcontextdata.h b/aplcore/include/apl/engine/sharedcontextdata.h index 89535da..02b90b8 100644 --- a/aplcore/include/apl/engine/sharedcontextdata.h +++ b/aplcore/include/apl/engine/sharedcontextdata.h @@ -27,14 +27,11 @@ #include "apl/engine/styles.h" #include "apl/primitives/size.h" #include "apl/primitives/textmeasurerequest.h" +#include "apl/scenegraph/common.h" #include "apl/utils/counter.h" #include "apl/utils/lrucache.h" #include "apl/utils/scopedset.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/common.h" -#endif // SCENEGRAPH - namespace apl { class DocumentRegistrar; @@ -163,12 +160,15 @@ class SharedContextData : public NonCopyable, public Counter, */ LruCache& cachedBaselines() { return mCachedBaselines; } -#ifdef SCENEGRAPH + /** + * @return A cache of TextLayout + */ + sg::TextLayoutCache& textLayoutCache() { return *mTextLayoutCache; } + /** * @return A cache of TextProperties */ sg::TextPropertiesCache& textPropertiesCache() { return *mTextPropertiesCache; } -#endif // SCENEGRAPH private: std::string mRequestedVersion; @@ -196,9 +196,8 @@ class SharedContextData : public NonCopyable, public Counter, LruCache mCachedMeasures; LruCache mCachedBaselines; -#ifdef SCENEGRAPH + std::unique_ptr mTextLayoutCache; std::unique_ptr mTextPropertiesCache; -#endif // SCENEGRAPH }; diff --git a/aplcore/include/apl/engine/visibilitymanager.h b/aplcore/include/apl/engine/visibilitymanager.h index 25a8f8f..eb08aa5 100644 --- a/aplcore/include/apl/engine/visibilitymanager.h +++ b/aplcore/include/apl/engine/visibilitymanager.h @@ -64,6 +64,7 @@ class VisibilityManager { WeakPtrMap mTrackedComponentVisibility; WeakPtrSet mDirtyVisibility; + WeakPtrSet mRegistrationQueue; }; } // namespace apl diff --git a/aplcore/include/apl/livedata/livedataobject.h b/aplcore/include/apl/livedata/livedataobject.h index 50ad77a..2b9a605 100644 --- a/aplcore/include/apl/livedata/livedataobject.h +++ b/aplcore/include/apl/livedata/livedataobject.h @@ -116,6 +116,11 @@ class LiveDataObject : public BaseArrayData, */ void removeFlushCallback(int token); + /** + * Explicitly mark LiveObject as dirty + */ + void markDirty(); + // ObjectData overrides. bool operator==(const ObjectData& rhs) const override { // In progress of changes propagation. Considered not-equal for comparisons. @@ -125,7 +130,6 @@ class LiveDataObject : public BaseArrayData, protected: LiveDataObject(const ContextPtr& context, const std::string& key) : mContext(context), mKey(key) {} - void markDirty(); protected: std::weak_ptr mContext; diff --git a/aplcore/include/apl/primitives/boundsymbolset.h b/aplcore/include/apl/primitives/boundsymbolset.h index 9c9f246..8211ffd 100644 --- a/aplcore/include/apl/primitives/boundsymbolset.h +++ b/aplcore/include/apl/primitives/boundsymbolset.h @@ -41,6 +41,9 @@ class BoundSymbolSet { } void emplace(const BoundSymbol& boundSymbol); + void merge(const BoundSymbolSet& other) { + for (const auto& symbol : other.mSymbols) emplace(symbol); + } bool empty() const { return mSymbols.empty(); } size_t size() const { return mSymbols.size(); } diff --git a/aplcore/include/apl/primitives/object.h b/aplcore/include/apl/primitives/object.h index bd6fcb0..d4a9ea1 100644 --- a/aplcore/include/apl/primitives/object.h +++ b/aplcore/include/apl/primitives/object.h @@ -132,6 +132,7 @@ class Object Object(const Dimension& dimension); Object(const rapidjson::Value& v); Object(rapidjson::Document&& doc); + Object(const SharedJsonData& json); template< class T, diff --git a/aplcore/include/apl/primitives/objectdata.h b/aplcore/include/apl/primitives/objectdata.h index 28c56f1..81e2db3 100644 --- a/aplcore/include/apl/primitives/objectdata.h +++ b/aplcore/include/apl/primitives/objectdata.h @@ -518,6 +518,19 @@ class JSONData : public JSONBaseData { /****************************************************************************/ +class JSONSharedData : public JSONData { +public: + JSONSharedData(std::shared_ptr doc, + const rapidjson::Value *value) + : JSONData(value), + mDoc(std::move(doc)) { assert(value); } + +private: + const std::shared_ptr mDoc; +}; + +/****************************************************************************/ + class JSONDocumentData : public JSONBaseData { public: JSONDocumentData(rapidjson::Document&& doc) : mDoc(std::move(doc)) {} diff --git a/aplcore/include/apl/primitives/textmeasurerequest.h b/aplcore/include/apl/primitives/textmeasurerequest.h index 3bbd717..c6f6ca7 100644 --- a/aplcore/include/apl/primitives/textmeasurerequest.h +++ b/aplcore/include/apl/primitives/textmeasurerequest.h @@ -34,10 +34,10 @@ struct TextMeasureRequest { YGMeasureMode widthMode; float height; YGMeasureMode heightMode; - std::string paramHash; + size_t paramHash; std::size_t hash() const { - auto result = std::hash{}(paramHash); + auto result = paramHash; hashCombine(result, width); hashCombine(result, widthMode); hashCombine(result, height); @@ -67,7 +67,7 @@ struct TextMeasureRequest { result += "widthMode=" + std::to_string(widthMode) + ","; result += "height=" + sutil::to_string(height) + ","; result += "heightMode=" + std::to_string(heightMode) + ","; - result += "paramHash=" + paramHash + ">"; + result += "paramHash=" + std::to_string(paramHash) + ">"; return result; } }; diff --git a/aplcore/include/apl/scenegraph/common.h b/aplcore/include/apl/scenegraph/common.h index c695f79..02473b6 100644 --- a/aplcore/include/apl/scenegraph/common.h +++ b/aplcore/include/apl/scenegraph/common.h @@ -33,6 +33,7 @@ class TextChunk; class TextProperties; class TextPropertiesCache; class TextLayout; +class TextLayoutCache; class Accessibility; class Filter; diff --git a/aplcore/include/apl/scenegraph/node.h b/aplcore/include/apl/scenegraph/node.h index 67b37ff..a606cef 100644 --- a/aplcore/include/apl/scenegraph/node.h +++ b/aplcore/include/apl/scenegraph/node.h @@ -22,7 +22,6 @@ #include "apl/common.h" #include "apl/scenegraph/common.h" #include "apl/component/componentproperties.h" -#include "apl/component/textmeasurement.h" #include "apl/primitives/range.h" #include "apl/primitives/rect.h" #include "apl/primitives/transform2d.h" diff --git a/aplcore/include/apl/scenegraph/path.h b/aplcore/include/apl/scenegraph/path.h index 3f689d6..9346856 100644 --- a/aplcore/include/apl/scenegraph/path.h +++ b/aplcore/include/apl/scenegraph/path.h @@ -20,7 +20,6 @@ #include #include "apl/scenegraph/common.h" -#include "apl/component/textmeasurement.h" #include "apl/primitives/rect.h" #include "apl/primitives/roundedrect.h" #include "apl/utils/counter.h" diff --git a/aplcore/include/apl/scenegraph/textchunk.h b/aplcore/include/apl/scenegraph/textchunk.h index 07fac03..7f8dfca 100644 --- a/aplcore/include/apl/scenegraph/textchunk.h +++ b/aplcore/include/apl/scenegraph/textchunk.h @@ -37,6 +37,8 @@ class TextChunk : public UserData { const StyledText& styledText() const { return mStyledText; } + std::size_t hash() const { return std::hash{}(mStyledText.asString()); } + private: StyledText mStyledText; }; diff --git a/aplcore/include/apl/scenegraph/textlayout.h b/aplcore/include/apl/scenegraph/textlayout.h index e77bb6f..de35344 100644 --- a/aplcore/include/apl/scenegraph/textlayout.h +++ b/aplcore/include/apl/scenegraph/textlayout.h @@ -39,7 +39,9 @@ class TextLayout { virtual Size getSize() const = 0; virtual float getBaseline() const = 0; virtual int getLineCount() const = 0; + virtual std::string getLaidOutText() const { return ""; } virtual std::string toDebugString() const = 0; + virtual bool isTruncated() const { return false; } virtual unsigned int getByteLength() const = 0; virtual Range getLineRangeFromByteRange(Range byteRange) const = 0; diff --git a/aplcore/include/apl/scenegraph/textlayoutcache.h b/aplcore/include/apl/scenegraph/textlayoutcache.h new file mode 100644 index 0000000..ae6e98b --- /dev/null +++ b/aplcore/include/apl/scenegraph/textlayoutcache.h @@ -0,0 +1,35 @@ +/** +* 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_LAYOUT_CACHE_H +#define _APL_TEXT_LAYOUT_CACHE_H + +#include "apl/scenegraph/common.h" +#include "apl/utils/weakcache.h" + +namespace apl { +namespace sg { + +/** +* Basic cache implementation for the TextLayout cache. +* We wrap this in a class rather than directly exposing WeakCache to give ourselves +* options for changing it in the future. +*/ +class TextLayoutCache : public WeakCache {}; + +} // namespace sg +} // namespace apl + +#endif // _APL_TEXT_PROPERTIES_CACHE_H diff --git a/aplcore/include/apl/scenegraph/textmeasurement.h b/aplcore/include/apl/scenegraph/textmeasurement.h index e6ee6ff..b25dd15 100644 --- a/aplcore/include/apl/scenegraph/textmeasurement.h +++ b/aplcore/include/apl/scenegraph/textmeasurement.h @@ -27,19 +27,48 @@ namespace sg { */ class TextMeasurement : public apl::TextMeasurement { public: + + // Viewhost to implement one of these two definitions. The method with a component pointer + // is a temporary definition to support usage of the new TextMeasurement API before + // implementation is fully migrated to scenegraph. virtual sg::TextLayoutPtr layout( const TextChunkPtr& chunk, const TextPropertiesPtr& textProperties, float width, MeasureMode widthMode, float height, - MeasureMode heightMode ) = 0; + MeasureMode heightMode ) { return nullptr; }; + + // Expect this definition to be deprecated when Scenegraph is available. + virtual sg::TextLayoutPtr layout(Component *component, + const TextChunkPtr& chunk, + const TextPropertiesPtr& textProperties, + float width, + MeasureMode widthMode, + float height, + MeasureMode heightMode ) { + return layout(chunk, textProperties, width, widthMode, height, heightMode); + }; + // Viewhost to implement one of these two definitions. The method with a component pointer + // is a temporary definition to support usage of the new TextMeasurement API before + // implementation is fully migrated to scenegraph. virtual sg::EditTextBoxPtr box( int size, const TextPropertiesPtr& textProperties, float width, MeasureMode widthMode, float height, - MeasureMode heightMode ) = 0; + MeasureMode heightMode ) { return nullptr; } + + // Expect this definition to be deprecated when Scenegraph is available. + virtual sg::EditTextBoxPtr box(Component *component, + int size, + const TextPropertiesPtr& textProperties, + float width, + MeasureMode widthMode, + float height, + MeasureMode heightMode ) { + return box(size, textProperties, width, widthMode, height, heightMode); + }; // These functions are provided for backwards compatibility. They should not be used LayoutSize measure(Component *component, @@ -48,9 +77,7 @@ class TextMeasurement : public apl::TextMeasurement { float baseline(Component *component, float width, float height) override { return 0; } -#ifdef SCENEGRAPH - bool sceneGraphCompatible() const override { return true; } -#endif // SCENEGRAPH + bool layoutCompatible() const override { return true; } }; } // namespace sg diff --git a/aplcore/include/apl/scenegraph/textproperties.h b/aplcore/include/apl/scenegraph/textproperties.h index 6d93884..00e2962 100644 --- a/aplcore/include/apl/scenegraph/textproperties.h +++ b/aplcore/include/apl/scenegraph/textproperties.h @@ -59,6 +59,7 @@ class TextProperties : public UserData { friend bool operator==(const TextProperties& lhs, const TextProperties& rhs); friend bool operator!=(const TextProperties& lhs, const TextProperties& rhs); + size_t hash() const; rapidjson::Value serialize(rapidjson::Document::AllocatorType& allocator) const; diff --git a/aplcore/include/apl/touch/pointermanager.h b/aplcore/include/apl/touch/pointermanager.h index 46b2077..fe51e7e 100644 --- a/aplcore/include/apl/touch/pointermanager.h +++ b/aplcore/include/apl/touch/pointermanager.h @@ -144,6 +144,7 @@ class PointerManager { HoverManager& mHoverManager; std::shared_ptr mActivePointer; std::shared_ptr mLastActivePointer; + apl_time_t mLastestPointerTimeStamp = 0; }; } diff --git a/aplcore/include/apl/utils/constants.h b/aplcore/include/apl/utils/constants.h new file mode 100644 index 0000000..37b77fe --- /dev/null +++ b/aplcore/include/apl/utils/constants.h @@ -0,0 +1,51 @@ +/** +* 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_CONSTANTS_H +#define _APL_CONSTANTS_H + +#include + +namespace apl { + +/*****************************************************************/ + +const std::string COMPONENT_UID = "uid"; +const std::string COMPONENT_INDEX = "index"; +const std::string COMPONENT_ORDINAL = "ordinal"; +const std::string COMPONENT_LENGTH = "length"; +const std::string COMPONENT_DATA_INDEX = "dataIndex"; +const std::string COMPONENT_DATA = "data"; +const std::string COMPONENT_CONTEXT_SOURCE = "__source"; +const std::string COMPONENT_CONTEXT_NAME = "__name"; + +/*****************************************************************/ + +/// Context belongs to "firstItem" expanded component +const std::string REBUILD_IS_FIRST_ITEM = "__isFirstItem"; +/// Context belongs to "lastItem" expanded component +const std::string REBUILD_IS_LAST_ITEM = "__isLastItem"; +/// Object key for firstItem definition reference +const std::string REBUILD_FIRST_ITEMS = "__firstItems"; +/// Object key for lastItem definition reference +const std::string REBUILD_LAST_ITEMS = "__lastItems"; +/// Object key for item definition reference +const std::string REBUILD_ITEMS = "__items"; +/// Source index value for non-live data controlled children +const std::string REBUILD_SOURCE_INDEX = "__sourceIndex"; + +} // namespace apl + +#endif //_APL_CONSTANTS_H diff --git a/aplcore/include/apl/utils/counter.h b/aplcore/include/apl/utils/counter.h index 74acfef..4aa9955 100644 --- a/aplcore/include/apl/utils/counter.h +++ b/aplcore/include/apl/utils/counter.h @@ -48,6 +48,9 @@ struct CounterPair { size_type destroyed; CounterPair(size_type created, size_type destroyed) : created(created), destroyed(destroyed) {} + + size_type alive() const { return created - destroyed; } + bool operator==(const CounterPair& rhs) { return created - destroyed == rhs.created - rhs.destroyed; } diff --git a/aplcore/include/apl/utils/flags.h b/aplcore/include/apl/utils/flags.h new file mode 100644 index 0000000..25f3e21 --- /dev/null +++ b/aplcore/include/apl/utils/flags.h @@ -0,0 +1,51 @@ +/** +* 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_FLAGS_H +#define _APL_FLAGS_H + +namespace apl { + +/** + * Simple bitset flags container. + * @tparam T enum type for flag set. + */ +template< + class T, + class Storage = typename std::underlying_type::type> +class Flags { +public: + static_assert(std::is_enum::value, "Requires enum type."); + + constexpr Flags() : mFlags(0) {} + constexpr explicit Flags(Storage initialValue) : mFlags(initialValue) {} + + void set(T flag) { mFlags |= flag; } + void clear(T flag) { mFlags &= ~flag; } + bool isSet(T flag) const { return (mFlags & flag) == flag; } + bool checkAndClear(T flag) { + auto result = isSet(flag); + clear(flag); + return result; + } + +private: + + Storage mFlags; +}; + +} + +#endif //_APL_FLAGS_H diff --git a/aplcore/include/apl/utils/stickychildrentree.h b/aplcore/include/apl/utils/stickychildrentree.h index 891a03d..dc458f4 100644 --- a/aplcore/include/apl/utils/stickychildrentree.h +++ b/aplcore/include/apl/utils/stickychildrentree.h @@ -17,10 +17,10 @@ #define APL_STICKYCHILDRENTREE_H #include "apl/common.h" -#include "apl/component/corecomponent.h" namespace apl { +class StickyChildrenTree; class StickyNode; using StickyChildrenTreePtr = std::shared_ptr; /** diff --git a/aplcore/include/apl/utils/weakcache.h b/aplcore/include/apl/utils/weakcache.h index f70a3f7..a5fd4ec 100644 --- a/aplcore/include/apl/utils/weakcache.h +++ b/aplcore/include/apl/utils/weakcache.h @@ -22,7 +22,7 @@ namespace apl { /** - * A weak cache is a map from a unique string to a weak_ptr. + * A weak cache is a map from a unique key to a weak_ptr. * As strong pointers are released the entries in the cache become invalid. * Periodically running the "clean()" method will remove those invalid entries. * diff --git a/aplcore/include/apl/versioning/semanticgrammar.h b/aplcore/include/apl/versioning/semanticgrammar.h new file mode 100644 index 0000000..0f58253 --- /dev/null +++ b/aplcore/include/apl/versioning/semanticgrammar.h @@ -0,0 +1,260 @@ +/** + * 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_SEMANTIC_GRAMMAR_H +#define APL_SEMANTIC_GRAMMAR_H + +#include + +#include "apl/datagrammar/grammarpolyfill.h" + +#include "apl/versioning/semanticversion.h" +#include "apl/versioning/semanticpattern.h" + +namespace apl { + +namespace svgrammar { + +/** + * Grammar for formatting semantic versions and semantic version pattern matches + * + * The semantic versioning definition: https://semver.org/ + * The NPM semantic versioning calculator: https://semver.npmjs.com/ + * Semantic versioning cheat sheet: https://devhints.io/semver + */ +namespace pegtl = tao::TAO_PEGTL_NAMESPACE; +using namespace pegtl; + +struct req_ws : plus> {}; +struct ws : star> {}; + +struct zero : seq, not_at> {}; +struct non_zero_number : seq, star> {}; +struct number : sor {}; +struct sym_dot : one<'.'> {}; +struct sym_plus : one<'+'> {}; +struct sym_minus : one<'-'> {}; + +struct version : must> {}; + +struct alnum_character : ranges<'a', 'z', 'A', 'Z', '0', '9', '-'> {}; +struct alnum_identifier : plus {}; +struct prerelease_identifier : sor {}; +struct prerelease : opt> {}; + +struct build_identifier : plus {}; +struct build : opt> {}; + +struct basic_semvar : seq {}; +struct semver : must {}; + +// Dependency matching + +struct sym_equal : one<'='> {}; +struct sym_gt : one<'>'> {}; +struct sym_lt : one<'<'> {}; +struct sym_ge : string<'>', '='> {}; +struct sym_le : string<'<', '='> {}; +struct sym_or : string<'|', '|'> {}; + +struct op : sor {}; +struct operand : basic_semvar {}; +struct base_pattern : seq, operand> {}; +struct and_pattern : list {}; +struct or_pattern : list> {}; +struct pattern : must {}; + +// ****** Encode string offset and len into uint32_t ******** + +const uint32_t kSemanticStringFlag = 0x80000000; +inline bool isEncodedString(uint32_t value) { return (value & kSemanticStringFlag) != 0; } +inline bool numberFits(uint32_t value) { return (value & kSemanticStringFlag) == 0; } +inline uint32_t encodeString(uint8_t offset, uint8_t len) { return offset << 8 | len | kSemanticStringFlag; } +inline uint8_t encodedOffset(uint32_t value) { return ((value >> 8) & 0xff); } +inline uint8_t encodedLen(uint32_t value) { return value & 0xff; } + +// ******************** Data Structures ********************* + +struct semvar_pattern_state : fail_state { + std::vector versions; + std::vector operators; + SemanticPattern::OpType op = SemanticPattern::OpType::kSemanticOpEquals; // Default operator +}; + +struct semvar_state : fail_state { + template + explicit semvar_state(const Input& in): start(in.current()) {} + + // Entry point when we're switching from Semantic Pattern to Semantic Version matching + template + semvar_state(const Input& in, semvar_pattern_state& /* unused */) + : semvar_state(in) {} + + // Called when a semantic version has matched and we're switching back to Semantic Pattern matching + template + void success(const Input& in, semvar_pattern_state& pattern) + { + pattern.versions.emplace_back( + std::make_shared(std::move(elements), std::string(start, in.current()))); + } + + std::vector elements; + std::string string; + const char *start = nullptr; +}; + +// ============== Semantic Version Rules =================== + +template +struct sv_action : pegtl::nothing {}; + +template<> +struct sv_action { + template + static void apply(const Input& in, semvar_state& state) + { + if (state.failed) return; + + auto value = std::stoul(in.string()); + // Check if the number is too large + if (!numberFits(value)) + state.fail("Numeric value too large", in); + else + state.elements.emplace_back(value); + } +}; + +template<> +struct sv_action { + template + static void apply(const Input& in, semvar_state& state) + { + if (state.failed) return; + // Encode the string offset and length in the lower two bytes and set the "it's a string" flag. + state.elements.emplace_back(encodeString(std::distance(state.start, in.begin()), + std::distance(in.begin(), in.end()))); + } +}; + +template<> +struct sv_action { + template + static void apply(const Input& in, semvar_state& state) + { + if (state.failed) return; + // We've finished matching the version. If the minor or patch version wasn't supplied, set them to 0. + while (state.elements.size() < 3) + state.elements.emplace_back(0); + } +}; + +template<> +struct sv_action { + template + static void apply(const Input& in, semvar_state& state) + { + if (state.failed) return; + // Store the string containing the actual semantic version (this avoids any whitespace) + state.string = in.string(); + } +}; + +// ===================== Semantic Pattern Rules ============================== + +template +struct sp_action : pegtl::nothing { +}; + +// Operand parsing switches to the SemanticVersion rules and state. +template<> +struct sp_action : change_action_and_state { + static void apply0(semvar_state& /*unused*/) {} +}; + +template<> +struct sp_action { + template + static void apply(const Input& in, semvar_pattern_state& state) + { + if (state.failed) return; + state.operators.emplace_back(state.op); + state.op = SemanticPattern::OpType::kSemanticOpEquals; // Reset the op back to the default. + } +}; + +template<> +struct sp_action { + static void apply0(semvar_pattern_state& state) + { + if (state.failed) return; + state.operators.emplace_back(SemanticPattern::OpType::kSemanticOpOr); + } +}; + +template<> +struct sp_action { + static void apply0(semvar_pattern_state& state) + { + if (state.failed) return; + state.op = SemanticPattern::OpType::kSemanticOpLessThanOrEquals; + } +}; + +template<> +struct sp_action { + static void apply0(semvar_pattern_state& state) + { + if (state.failed) return; + state.op = SemanticPattern::OpType::kSemanticOpLessThan; + } +}; + +template<> +struct sp_action { + static void apply0(semvar_pattern_state& state) + { + if (state.failed) return; + state.op = SemanticPattern::OpType::kSemanticOpGreaterThanOrEquals; + } +}; + +template<> +struct sp_action { + static void apply0(semvar_pattern_state& state) + { + if (state.failed) return; + state.op = SemanticPattern::OpType::kSemanticOpGreaterThan; + } +}; + + +// ****************** Error messages ******************* + +template +struct sv_control : public apl_control { + static const std::string error_message; + + template + static void raise(const Input& in, States&& ... st) + { + apl_control::fail(in, st...); + } +}; + +} // namespace svgrammar + +} // namespace apl + +#endif //APL_SEMANTIC_GRAMMAR_H diff --git a/aplcore/include/apl/versioning/semanticpattern.h b/aplcore/include/apl/versioning/semanticpattern.h new file mode 100644 index 0000000..1c73d32 --- /dev/null +++ b/aplcore/include/apl/versioning/semanticpattern.h @@ -0,0 +1,88 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_SEMANTIC_PATTERN_H +#define _APL_SEMANTIC_PATTERN_H + +#include +#include +#include + +#include "apl/common.h" + +namespace apl { + +/** + * A Semantic Pattern specifies a valid range or set of ranges of semantic version strings. + * This is a subset of the traditional semantic patterns that only supports comparison operators + * and Boolean OR statements. + * + * Valid examples include: + * + * 1.3.2 + * =1.3.2 + * >1.3.0 + * >=1.3.0 <2.0.0 + * >1.3.1-alpha || >1.3.2-beta <2.0 + */ +class SemanticPattern { +public: + enum OpType { + kSemanticOpEquals, + kSemanticOpGreaterThan, + kSemanticOpGreaterThanOrEquals, + kSemanticOpLessThan, + kSemanticOpLessThanOrEquals, + kSemanticOpOr, + }; + + /** + * Static constructor for semantic patterns + * @param session A session object for error reporting. + * @param string The string version of the semantic pattern. + * @return The semantic pattern or null if it fails to parse. + */ + static SemanticPatternPtr create(const SessionPtr& session, const std::string& string); + + /** + * Return true if this semantic version matches the semantic pattern. + * @param version A semantic version of a package. + * @return True if the version falls within the valid range. + */ + bool match(const SemanticVersionPtr& version) const; + + /** + * @return A debugging string showing the internal structure of the pattern. + */ + std::string toDebugString() const; + + /** + * Constructor. Generally you should use the SemanticPattern::create() method instead. + * @param versions An array of the semantic versions in the pattern. + * @param ops An array of operations + */ + SemanticPattern(std::vector versions, std::vector ops) + : mVersions(std::move(versions)), + mOperators(std::move(ops)) + {} + +private: + std::vector mVersions; + std::vector mOperators; +}; + +} // namespace apl + +#endif // _APL_SEMANTIC_PATTERN_H \ No newline at end of file diff --git a/aplcore/include/apl/versioning/semanticversion.h b/aplcore/include/apl/versioning/semanticversion.h new file mode 100644 index 0000000..c201595 --- /dev/null +++ b/aplcore/include/apl/versioning/semanticversion.h @@ -0,0 +1,112 @@ +/** + * 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_SEMANTIC_VERSION_H +#define _APL_SEMANTIC_VERSION_H + +#include +#include +#include + +#include "apl/common.h" + +namespace apl { + +/** + * A SemanticVersion object parses a Semantic Version + * string and stores it in a compact format suitable for comparison operations with other + * semantic versions. + * + * The major, minor, and patch of a semantic version are numbers. The optional prerelease + * section ("-") contains an array of dot-separated numbers and/or strings. The optional build + * section ("+") is ignored for version comparisons. + * + * Internally we store each of the major, minor, patch, and prerelease elements in a single + * array of uint32_t. Numeric values are stored as simple unsigned integers. String values + * are encoded as a uint8_t offset from the start of the string and a uint8_t length of the + * element. The high-order bit is used as a string flag. The implications of this design + * are (a) the original string is limited to 255 characters and (b) the largest supported numeric + * value is 2^31. + * + * For convenience, if the minor and patch values are omitted they are assumed to be 0. + * The toDebugString() method returns internal calculated array. + * + * Examples of valid strings: 1.0.0 + * 2.13 (resolves to 2.13.0) + * 10-alpha.2+build2234 (resolves to 10.0.0."alpha".2) + */ +class SemanticVersion { +public: + /** + * Call this to create a new semantic version object from a string. If the semantic + * version is invalid, a nullptr will be returned and an error message logged on the + * session. + * + * @param session Console session for error logs. + * @param string The string to parse as a semantic version. + * @return The parsed object or nullptr. + */ + static SemanticVersionPtr create(const SessionPtr& session, const std::string& string); + + /** + * Internal constructor. Do not use. + */ + SemanticVersion(std::vector elements, std::string string) + : mElements(std::move(elements)), mString(std::move(string)) {} + + /** + * @return True if this semantic version has no prerelease elements. + */ + bool simple() const { return mElements.size() == 3; } + + /** + * @return True if these semantic versions match on MAJOR.MINOR.PATCH. The + * prerelease and build elements are ignored. + */ + bool versionMatch(const SemanticVersion& other) const; + + /** + * @return A debugging string showing the inner parts of the semantic version + * including major, minor, patch, and prerelease elements. The build + * elements are not included. + */ + std::string toDebugString() const; + + /** + * Compare with another SemanticVersion. Return <0 if this version is ordered before, + * 0 if they are equal (excluding build) and >0 if this version is ordered after. Note + * that versions with prerelease elements are ordered before the actual release. For + * example: 1.0.0-alpha < 1.0.0-alpha.2 < 1.0.0 < 1.0.1-beta.2 < 1.0.1 + * + * @param other The version to compare with. + * @return <0, 0, or >0. + */ + int compare(const SemanticVersion& other) const; + + bool operator==(const SemanticVersion& other) const { return compare(other) == 0; } + bool operator!=(const SemanticVersion& other) const { return compare(other) != 0; } + bool operator<(const SemanticVersion& other) const { return compare(other) < 0; } + bool operator<=(const SemanticVersion& other) const { return compare(other) <= 0; } + bool operator>(const SemanticVersion& other) const { return compare(other) > 0; } + bool operator>=(const SemanticVersion& other) const { return compare(other) >= 0; } + +private: + std::vector mElements; + std::string mString; +}; + +} // namespace apl + +#endif // _APL_SEMANTIC_VERSION_H diff --git a/aplcore/src/action/CMakeLists.txt b/aplcore/src/action/CMakeLists.txt index 76c5cf4..195e677 100644 --- a/aplcore/src/action/CMakeLists.txt +++ b/aplcore/src/action/CMakeLists.txt @@ -22,6 +22,7 @@ target_sources_local(apl delayaction.cpp documentaction.cpp extensioneventaction.cpp + importpackageaction.cpp openurlaction.cpp playmediaaction.cpp resourceholdingaction.cpp diff --git a/aplcore/src/action/importpackageaction.cpp b/aplcore/src/action/importpackageaction.cpp new file mode 100644 index 0000000..2279b53 --- /dev/null +++ b/aplcore/src/action/importpackageaction.cpp @@ -0,0 +1,82 @@ +/** + * 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/action/importpackageaction.h" + +#include "apl/command/arraycommand.h" + +namespace apl { + +ImportPackageAction::ImportPackageAction(const TimersPtr& timers, + const std::shared_ptr& command, + ActionPtr&& startAction) + : Action(timers), + mCommand(command), + mCurrentAction(std::move(startAction)) {} + +std::shared_ptr +ImportPackageAction::make(const TimersPtr& timers, + const std::shared_ptr& command, + ActionPtr&& startAction) +{ + auto ptr = std::make_shared(timers, command, std::move(startAction)); + return ptr; +} + +void +ImportPackageAction::onLoad(const std::string& version) +{ + resolve(); + auto event = std::make_shared(); + event->emplace("version", version); + auto eventMap = mCommand->context()->opt("event").getMap(); + for (const auto& existing: eventMap) { + event->emplace(existing.first, existing.second); + } + + ContextPtr context = Context::createFromParent(mCommand->context()); + context->putConstant("event", event); + + ArrayCommand::create(context, + {mCommand->getValue(kCommandPropertyOnLoad), mCommand->data()}, + mCommand->base(), + Properties(mCommand->properties()), + mCommand->sequencer())->execute(timers(), true); +} + +void +ImportPackageAction::onFail(const std::string& nameVersionSource, const std::string& errorMessage, int code) +{ + resolve(); + auto event = std::make_shared(); + event->emplace("value", nameVersionSource); + event->emplace("error", errorMessage); + event->emplace("errorCode", code); + auto eventMap = mCommand->context()->opt("event").getMap(); + for (const auto& existing: eventMap) { + event->emplace(existing.first, existing.second); + } + + ContextPtr context = Context::createFromParent(mCommand->context()); + context->putConstant("event", event); + + ArrayCommand::create(context, + {mCommand->getValue(kCommandPropertyOnFail), mCommand->data()}, + mCommand->base(), + Properties(mCommand->properties()), + mCommand->sequencer())->execute(timers(), true); +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/action/speakitemaction.cpp b/aplcore/src/action/speakitemaction.cpp index f7bec6a..085ca48 100644 --- a/aplcore/src/action/speakitemaction.cpp +++ b/aplcore/src/action/speakitemaction.cpp @@ -593,7 +593,7 @@ SpeakItemAction::SpeakItemAction(const TimersPtr& timers, const std::shared_ptr< #ifdef SCENEGRAPH // If a sg::TextMeasurement is installed, we use that to select the text to highlight // Otherwise we use an event mechanism to set highlighted lines - if (rootConfig.getMeasure()->sceneGraphCompatible()) + if (rootConfig.getMeasure()->layoutCompatible()) mPrivate = std::make_unique(); else #endif // SCENEGRAPH diff --git a/aplcore/src/command/CMakeLists.txt b/aplcore/src/command/CMakeLists.txt index b9d1f57..4ede69d 100644 --- a/aplcore/src/command/CMakeLists.txt +++ b/aplcore/src/command/CMakeLists.txt @@ -26,6 +26,7 @@ target_sources_local(apl documentcommand.cpp extensioneventcommand.cpp finishcommand.cpp + importpackagecommand.cpp insertitemcommand.cpp logcommand.cpp openurlcommand.cpp diff --git a/aplcore/src/command/commandproperties.cpp b/aplcore/src/command/commandproperties.cpp index 478dff6..9cdd9ea 100644 --- a/aplcore/src/command/commandproperties.cpp +++ b/aplcore/src/command/commandproperties.cpp @@ -44,9 +44,11 @@ Bimap sCommandNameBimap = { {kCommandTypeInsertItem, "InsertItem"}, {kCommandTypeRemoveItem, "RemoveItem"}, {kCommandTypeLog, "Log"}, + {kCommandTypeImportPackage, "ImportPackage"}, }; Bimap sCommandPropertyBimap = { + {kCommandPropertyAccept, "accept"}, {kCommandPropertyAlign, "align"}, {kCommandPropertyArguments, "arguments"}, {kCommandPropertyAt, "at"}, @@ -72,7 +74,9 @@ Bimap sCommandPropertyBimap = { {kCommandPropertyLevel, "level"}, {kCommandPropertyMessage, "message"}, {kCommandPropertyMinimumDwellTime, "minimumDwellTime"}, + {kCommandPropertyName, "name"}, {kCommandPropertyOnFail, "onFail"}, + {kCommandPropertyOnLoad, "onLoad"}, {kCommandPropertyOtherwise, "otherwise"}, {kCommandPropertyPosition, "position"}, {kCommandPropertyPreservedSequencers, "preservedSequencers"}, @@ -89,6 +93,7 @@ Bimap sCommandPropertyBimap = { {kCommandPropertyTransitionDuration, "transitionDuration"}, {kCommandPropertyValue, "value"}, {kCommandPropertyValue, "values"}, + {kCommandPropertyVersion, "version"}, }; Bimap sCommandAlignMap = { diff --git a/aplcore/src/command/corecommand.cpp b/aplcore/src/command/corecommand.cpp index ec453f4..d4a7595 100644 --- a/aplcore/src/command/corecommand.cpp +++ b/aplcore/src/command/corecommand.cpp @@ -42,6 +42,7 @@ #include "apl/command/setvaluecommand.h" #include "apl/command/speakitemcommand.h" #include "apl/command/speaklistcommand.h" +#include "apl/command/importpackagecommand.h" #include "apl/component/componenteventtargetwrapper.h" #include "apl/component/selector.h" #include "apl/engine/layoutmanager.h" @@ -333,6 +334,7 @@ std::map sCommandCreatorMap = { {kCommandTypeReinflate, ReinflateCommand::create}, {kCommandTypeInsertItem, InsertItemCommand::create}, {kCommandTypeRemoveItem, RemoveItemCommand::create}, + {kCommandTypeImportPackage, ImportPackageCommand::create}, }; } diff --git a/aplcore/src/command/importpackagecommand.cpp b/aplcore/src/command/importpackagecommand.cpp new file mode 100644 index 0000000..834d6de --- /dev/null +++ b/aplcore/src/command/importpackagecommand.cpp @@ -0,0 +1,95 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/command/importpackagecommand.h" + +#include "apl/action/importpackageaction.h" +#include "apl/content/packageresolver.h" +#include "apl/content/importrequest.h" +#include "apl/utils/session.h" +#include "apl/document/coredocumentcontext.h" + +namespace apl { + +const CommandPropDefSet& +ImportPackageCommand::propDefSet() const { + static CommandPropDefSet sImportPackageCommandProperties(CoreCommand::propDefSet(), { + { kCommandPropertyAccept, "", asString}, + { kCommandPropertyName, "", asString, kPropRequired}, + { kCommandPropertyOnFail, Object::EMPTY_ARRAY(), asArray}, + { kCommandPropertyOnLoad, Object::EMPTY_ARRAY(), asArray}, + { kCommandPropertySource, "", asString}, + { kCommandPropertyVersion, "", asString, kPropRequired}, + }); + + return sImportPackageCommandProperties; +} + +ActionPtr +ImportPackageCommand::execute(const TimersPtr& timers, bool fastMode) { + if (!calculateProperties()) + return nullptr; + + if (fastMode) { + CONSOLE(mContext) << "Ignoring ImportPackage command in fast mode"; + return nullptr; + } + + ActionRef actionRef{nullptr}; + auto action = Action::make(timers, [&actionRef](ActionRef ref) { actionRef = ref; }); + auto importPackageAction = ImportPackageAction::make( + timers, std::static_pointer_cast(shared_from_this()), std::move(action)); + + auto session = mContext->session(); + auto version = getValue(kCommandPropertyVersion).getString(); + auto accept = getValue(kCommandPropertyAccept).asString(); + auto acceptPattern = + accept.empty() ? nullptr : SemanticPattern::create(session, accept); + auto semanticVersion = SemanticVersion::create(session, version); + + ImportRequest request = + ImportRequest(getValue(kCommandPropertyName).getString(), version, + getValue(kCommandPropertySource).getString(), std::set(), + semanticVersion, acceptPattern); + + auto coreDocumentContext = CoreDocumentContext::cast(mContext->documentContext()); + if (coreDocumentContext->isPackageProcessed(request.reference().toString())) { + importPackageAction->onLoad(request.reference().version()); + return nullptr; + } + + auto evaluationContext = coreDocumentContext->contextPtr(); + auto packageManager = mContext->getRootConfig().getPackageManager(); + if (!packageManager) { + importPackageAction->onFail(getValue(kCommandPropertyName).getString(), "ImportPackage command is unsupported by this runtime.", 400); + return nullptr; + } + + mPackageResolver = PackageResolver::create(packageManager, mContext->session()); + mPackageResolver->load( + evaluationContext, session, request, + [coreDocumentContext, importPackageAction, version](std::vector&& ordered) { + coreDocumentContext->processPackagesIntoContext(ordered); + importPackageAction->onLoad(version); + }, + [importPackageAction](const ImportRef& ref, const std::string& errorMessage, int errorCode) { + auto nameVersionSource = ref.toString() + ":" + ref.source(); + importPackageAction->onFail(nameVersionSource, errorMessage, errorCode); + }); + + return importPackageAction; +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/component/componentproperties.cpp b/aplcore/src/component/componentproperties.cpp index 49126cc..5cdbde7 100644 --- a/aplcore/src/component/componentproperties.cpp +++ b/aplcore/src/component/componentproperties.cpp @@ -128,6 +128,11 @@ Bimap sPositionMap = { {kPositionSticky, "sticky"} }; +Bimap sPointerEventsMap = { + {kPointerEventsAuto, "auto"}, + {kPointerEventsNone, "none"} +}; + Bimap sScrollDirectionMap = { {kScrollDirectionHorizontal, "horizontal"}, {kScrollDirectionVertical, "vertical"} @@ -400,6 +405,7 @@ Bimap sComponentPropertyBimap = { {kPropertyOnFail, "onFail"}, {kPropertyResourceOnFatalError, "onFatalError"}, {kPropertyOnFocus, "onFocus"}, + {kPropertyOnLayout, "onLayout"}, {kPropertyOnLoad, "onLoad"}, {kPropertyOnMount, "onMount"}, {kPropertyOnMove, "onMove"}, @@ -413,6 +419,7 @@ Bimap sComponentPropertyBimap = { {kPropertyOnPress, "onPress"}, {kPropertyOnSubmit, "onSubmit"}, {kPropertyOnTextChange, "onTextChange"}, + {kPropertyOnTextLayout, "onTextLayout"}, {kPropertyOnTimeUpdate, "onTimeUpdate"}, {kPropertyOnTrackUpdate, "onTrackUpdate"}, {kPropertyOnUp, "onUp"}, @@ -433,6 +440,7 @@ Bimap sComponentPropertyBimap = { {kPropertyParameters, "parameter"}, {kPropertyPlayingState, "playingState"}, {kPropertyPosition, "position"}, + {kPropertyPointerEvents, "pointerEvents"}, {kPropertyPreserve, "preserve"}, {kPropertyRangeKaraokeTarget, "_rangeKaraokeTarget"}, {kPropertyResourceId, "resourceId"}, diff --git a/aplcore/src/component/corecomponent.cpp b/aplcore/src/component/corecomponent.cpp index 8e7a05f..1088a21 100644 --- a/aplcore/src/component/corecomponent.cpp +++ b/aplcore/src/component/corecomponent.cpp @@ -24,11 +24,14 @@ #include "apl/component/componenteventtargetwrapper.h" #include "apl/component/componentpropdef.h" #include "apl/component/yogaproperties.h" +#include "apl/content/content.h" #include "apl/content/rootconfig.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/builder.h" #include "apl/engine/contextwrapper.h" #include "apl/engine/hovermanager.h" #include "apl/engine/layoutmanager.h" +#include "apl/engine/rebuilddependant.h" #include "apl/engine/typeddependant.h" #include "apl/engine/visibilitymanager.h" #include "apl/focus/focusmanager.h" @@ -41,6 +44,7 @@ #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" #include "apl/touch/pointerevent.h" +#include "apl/utils/constants.h" #include "apl/utils/hash.h" #include "apl/utils/make_unique.h" #include "apl/utils/searchvisitor.h" @@ -64,8 +68,6 @@ const std::string VISUAL_CONTEXT_TYPE_EMPTY = "empty"; /*****************************************************************/ -const std::string CHILDREN_CHANGE_UID = "uid"; -const std::string CHILDREN_CHANGE_INDEX = "index"; const std::string CHILDREN_CHANGE_ACTION = "action"; const std::string CHILDREN_CHANGE_CHANGES = "changes"; @@ -114,17 +116,21 @@ CoreComponent::CoreComponent(const ContextPtr& context, Properties&& properties, const Path& path) : Component(context, properties.asLabel(*context, "id")), - mInheritParentState(properties.asBoolean(*context, "inheritParentState", false)), mStyle(properties.asString(*context, "style", "")), mProperties(std::move(properties)), mParent(nullptr), mYGNodeRef(YGNodeNewWithConfig(context->ygconfig())), - mPath(path), - mDisplayedChildrenStale(true), - mIsDisallowed(false), - mGlobalToLocalIsStale(true), - mTextMeasurementHashStale(true), - mVisualHashStale(true) { + mPath(path) +{ + mCoreFlags = Flags( + kCoreComponentFlagDisplayedChildrenStale | + kCoreComponentFlagGlobalToLocalIsStale | + kCoreComponentFlagTextMeasurementHashStale | + kCoreComponentFlagVisualHashStale); + + if (mProperties.asBoolean(*context, "inheritParentState", false)) + mCoreFlags.set(kCoreComponentFlagInheritParentState); + YGNodeSetContext(mYGNodeRef, this); } @@ -177,7 +183,7 @@ CoreComponent::initialize() mCalculated.set(kPropertyNotifyChildrenChanged, Object::EMPTY_MUTABLE_ARRAY()); // Fix up the state variables that can be assigned as a property - if (mInheritParentState && mParent) + if (mParent && mCoreFlags.isSet(kCoreComponentFlagInheritParentState)) mState = mParent->getState(); // Assign the built-in properties. @@ -350,7 +356,7 @@ CoreComponent::attachedToParent(const CoreComponentPtr& parent) assignProperties(*layoutPropDefSet); // Update our state. - if (mInheritParentState) { + if (mCoreFlags.isSet(kCoreComponentFlagInheritParentState)) { updateMixedStateProperty(kPropertyChecked, mParent->getCalculated(kPropertyChecked).asBoolean()); updateMixedStateProperty(kPropertyDisabled, mParent->getCalculated(kPropertyDisabled).asBoolean()); updateInheritedState(); @@ -417,7 +423,8 @@ CoreComponent::appendChild(const ComponentPtr& child, bool useDirtyFlag) void CoreComponent::notifyChildChanged(size_t index, const CoreComponentPtr& component, ChildChangeAction action) { - mChildrenChanges.emplace_back(ChildChange{component, action, index}); + if (!mChildrenChanges) mChildrenChanges = std::make_unique>(); + mChildrenChanges->emplace_back(ChildChange{component, component->getUniqueId(), action, index}); // Mark component as dirty so required processing will take place mContext->setDirty(shared_from_this()); } @@ -443,7 +450,7 @@ CoreComponent::markDisplayedChildrenStale(bool useDirtyFlag) { // Children visibility can't be stale if component can't have one. if (multiChild() || singleChild()) { - mDisplayedChildrenStale = true; + mCoreFlags.set(kCoreComponentFlagDisplayedChildrenStale); if (useDirtyFlag) setDirty(kPropertyNotifyChildrenChanged); } } @@ -475,8 +482,7 @@ CoreComponent::isDisplayedChild(const CoreComponent& child) const void CoreComponent::ensureDisplayedChildren() { - - if (!mDisplayedChildrenStale) + if (!mCoreFlags.isSet(kCoreComponentFlagDisplayedChildrenStale)) return; // clear previous calculations @@ -524,7 +530,7 @@ CoreComponent::ensureDisplayedChildren() // Insert the sticky elements at the end mDisplayedChildren.insert(mDisplayedChildren.end(), sticky.begin(), sticky.end()); - mDisplayedChildrenStale = false; + mCoreFlags.clear(kCoreComponentFlagDisplayedChildrenStale); } bool @@ -567,7 +573,6 @@ CoreComponent::insertChild(const CoreComponentPtr& child, size_t index, bool use // Register component for visibility calculation considerations, if required coreChild->registerForVisibilityTrackingIfRequired(); - setVisibilityDirty(); // Update the position: sticky components tree auto p = stickyfunctions::getAncestorHorizontalAndVerticalScrollable(coreChild); @@ -933,7 +938,7 @@ CoreComponent::assignProperties(const ComponentPropDefSet& propDefSet) else { // Make sure this wasn't a required property if ((pd.flags & kPropRequired) != 0) { - mFlags |= kComponentFlagInvalid; + mFlags.set(kComponentFlagInvalid); CONSOLE(mContext) << "Missing required property: " << pd.names; } @@ -972,7 +977,7 @@ CoreComponent::assignProperties(const ComponentPropDefSet& propDefSet) void CoreComponent::handlePropertyChange(const ComponentPropDef& def, const Object& value) { // If the mixed state inherits from the parent, we block the change. - if ((def.flags & kPropMixedState) != 0 && mInheritParentState) + if ((def.flags & kPropMixedState) != 0 && mCoreFlags.isSet(kCoreComponentFlagInheritParentState)) return; auto previous = getCalculated(def.key); @@ -1017,11 +1022,11 @@ CoreComponent::handlePropertyChange(const ComponentPropDef& def, const Object& v // Properties with the kPropTextHash flag the text measurement hash as dirty if ((def.flags & kPropTextHash) != 0) - mTextMeasurementHashStale = true; + mCoreFlags.set(kCoreComponentFlagTextMeasurementHashStale); // Properties with kPropVisualHash flag the visual hash is dirty if ((def.flags & kPropVisualHash) != 0) - mVisualHashStale = true; + mCoreFlags.set(kCoreComponentFlagVisualHashStale); if ((def.flags & kPropAccessibility) != 0) markAccessibilityDirty(); @@ -1079,7 +1084,7 @@ CoreComponent::getLayoutRoot() { void CoreComponent::updateMixedStateProperty(apl::PropertyKey key, bool value) { - if (!mInheritParentState || value == mCalculated.get(key).asBoolean()) + if (!mCoreFlags.isSet(kCoreComponentFlagInheritParentState) || value == mCalculated.get(key).asBoolean()) return; mCalculated.set(key, value); @@ -1183,8 +1188,235 @@ CoreComponent::setProperty(PropertyKey key, const Object& value) return false; } +/** + * Walk the list of children and change as appropriate, only makes sense when mPendingRebuildChanges + * non-empty. + * + * 1. Skip through all items in the original item list. + * 2. If in changed list - create/remove/recreate + * 3. Any item that got mis-aligned index after the changed one is recreated. + * 4. If nothing changed - item stays the same. + */ +void +CoreComponent::rebuildItems() +{ + if (!mPendingRebuildChanges || mPendingRebuildChanges->empty()) return; + + auto childPath = getPathObject(); + auto itemsObject = getContext()->opt(REBUILD_ITEMS); + + if (!itemsObject.isArray() || itemsObject.empty()) return; + + int currentChildIndex = 0; + int currentReportedIndex = 0; + + bool numbered = getCalculated(kPropertyNumbered).asBoolean(); + auto length = itemsObject.size(); + int ordinal = 1; + + if (!mChildren.empty() && getCoreChildAt(0)->getContext()->has(REBUILD_IS_FIRST_ITEM)) + currentChildIndex++; + + for (int i = 0; i < itemsObject.size(); i++) { + CoreComponentPtr old; + int oldIndex = -1; + + if (currentChildIndex < mChildren.size()) { + auto child = getCoreChildAt(currentChildIndex); + auto sourceIndexObject = child->getContext()->opt(REBUILD_SOURCE_INDEX); + // Break out if have no sourceIndex - likely last item. + if (sourceIndexObject.isNull()) + break; + + if (sourceIndexObject.asInt() == i) { + old = child; + oldIndex = old->getContext()->opt(COMPONENT_INDEX).asInt(); + } + } + + CoreComponentPtr replaceChild; + ContextPtr childContext; + + // If index is in change queue - create new context for rebuild + if (mPendingRebuildChanges->count(i)) { + childContext = Builder::createIndexItemContext(getContext(), i, currentReportedIndex, length, + numbered, ordinal); + } else if (old) { + // Index is aligned with expected - just skip (fast). If not - rebuild. + if (oldIndex == currentReportedIndex) { + replaceChild = old; + } else { + childContext = Builder::createIndexItemContext(getContext(), i, currentReportedIndex, length, + numbered, ordinal); + } + } + + // Handle rebuild + if (childContext && !replaceChild) { + if (mStashedRebuildCtxs) { + auto stash = mStashedRebuildCtxs->find(i); + if (stash != mStashedRebuildCtxs->end()) { + mStashedRebuildCtxs->erase(stash); + } + } + + auto items = arrayifyAsObject(*childContext, itemsObject.at(i)).getArray(); + replaceChild = Builder(nullptr).expandSingleComponentFromArray( + childContext, items, Properties(), shared_from_corecomponent(), childPath, + shouldBeFullyInflated(currentChildIndex), true, oldIndex == currentReportedIndex ? old : nullptr); + if (old && replaceChild != old) + old->remove(); + + if (replaceChild && replaceChild->isValid()) + insertChild(replaceChild, currentChildIndex, true); + + Builder::registerRebuildDependencyIfRequired(shared_from_corecomponent(), childContext, + items, replaceChild != nullptr); + } + + // And advance common counters + if (replaceChild && replaceChild->isValid()) { + currentChildIndex++; + currentReportedIndex++; + + if (numbered) { + int numbering = replaceChild->getCalculated(kPropertyNumbering).getInteger(); + if (numbering == kNumberingNormal) ordinal++; + else if (numbering == kNumberingReset) ordinal = 1; + } + } + } +} + void -CoreComponent::setProperty( const std::string& key, const Object& value ) +CoreComponent::scheduleRebuildChange(const ContextPtr& childContext) +{ + int originIndex = 0; + + if (childContext->opt(REBUILD_IS_FIRST_ITEM).asBoolean()) { + originIndex = -1; + } else if (childContext->opt(REBUILD_IS_LAST_ITEM).asBoolean()) { + originIndex = INT_MAX; + } else { + // If children are live data controlled - pass to rebuilder for evaluation. Marking array as + // dirty will trigger dataIndex-reconciled rebuild. + if (mRebuilder) { + mRebuilder->getBackingArray()->markDirty(); + return; + } + originIndex = childContext->opt(REBUILD_SOURCE_INDEX).asInt(); + } + + if (!mPendingRebuildChanges) mPendingRebuildChanges = std::make_unique>(); + mPendingRebuildChanges->emplace(originIndex); + getContext()->setDirty(shared_from_this()); +} + +void +CoreComponent::processRebuildChanges() +{ + if (!mPendingRebuildChanges || mPendingRebuildChanges->empty()) return; + + // Process first + auto firstIt = mPendingRebuildChanges->find(-1); + if (firstIt != mPendingRebuildChanges->end()) { + auto items = mContext->opt(REBUILD_FIRST_ITEMS); + auto firstChild = mChildren.at(0); + auto old = firstChild->getContext()->has(REBUILD_IS_FIRST_ITEM) ? firstChild : nullptr; + + replaceChild(items.getArray(), old, Builder::createFirstItemContext(mContext), -1, 0); + + mPendingRebuildChanges->erase(firstIt); + } + + if (!mPendingRebuildChanges->empty()) { + if (singleChild()) { + auto it = mPendingRebuildChanges->begin(); + auto items = mContext->opt(REBUILD_ITEMS); + CoreComponentPtr old; + if (getChildCount()) + old = mChildren.at(0); + + replaceChild( + items.getArray(), + old, + Builder::createIndexItemContext(mContext, 0, 0, items.size(), false, 0), + *it, + 0); + } + else { + rebuildItems(); + } + + // Process last + auto lastIt = mPendingRebuildChanges->find(INT_MAX); + if (lastIt != mPendingRebuildChanges->end()) { + auto items = mContext->opt(REBUILD_LAST_ITEMS); + auto lastChild = mChildren.at(mChildren.size() - 1); + auto old = lastChild->getContext()->has(REBUILD_IS_LAST_ITEM) ? lastChild : nullptr; + + replaceChild( + items.getArray(), + old, + Builder::createLastItemContext(mContext), + INT_MAX, + old ? mChildren.size() - 1 : mChildren.size()); + } + } + + mPendingRebuildChanges->clear(); + mPendingRebuildChanges = nullptr; +} + +void +CoreComponent::stashRebuildContext(const ContextPtr& context) +{ + int index = 0; + if (context->opt(REBUILD_IS_FIRST_ITEM).asBoolean()) { + index = -1; + } else if (context->opt(REBUILD_IS_LAST_ITEM).asBoolean()) { + index = INT_MAX; + } else { + auto sourceIndex = context->opt(REBUILD_SOURCE_INDEX); + if (sourceIndex.isNumber()) { + index = sourceIndex.asInt(); + } else { + index = context->opt(COMPONENT_DATA_INDEX).asInt(); + } + } + if (!mStashedRebuildCtxs) mStashedRebuildCtxs = std::make_unique>(); + mStashedRebuildCtxs->emplace(index, context); +} + +void +CoreComponent::replaceChild(const ObjectArray& items, const CoreComponentPtr& child, const ContextPtr& childContext, int originIndex, int childIndex) +{ + // Remove context stashed for this origin (if any) + if (mStashedRebuildCtxs) { + auto stash = mStashedRebuildCtxs->find(originIndex); + if (stash != mStashedRebuildCtxs->end()) { + mStashedRebuildCtxs->erase(stash); + } + } + + // There are no need to re-attach dependency. We are reusing same context. + auto replaceChild = Builder(shared_from_corecomponent()).expandSingleComponentFromArray( + childContext, items, Properties(), shared_from_corecomponent(), getPathObject(), + shouldBeFullyInflated(childIndex), true, child); + if (replaceChild != child) { + if (child) removeChildAt(childIndex, true); + + if (replaceChild && replaceChild->isValid()) { + // Add new one with reused context + insertChild(replaceChild, childIndex, true); + } + } + + Builder::registerRebuildDependencyIfRequired(shared_from_corecomponent(), childContext, items, replaceChild != nullptr); +} + +void +CoreComponent::setProperty(const std::string& key, const Object& value) { if (sComponentPropertyBimap.has(key) && setProperty(static_cast(sComponentPropertyBimap.at(key)), value)) @@ -1357,7 +1589,7 @@ CoreComponent::updateStyle() updateStyleInternal(stylePtr, *layoutPDS); } for (const auto& child : mChildren) { - if (child->mInheritParentState) + if (child->mCoreFlags.isSet(kCoreComponentFlagInheritParentState)) child->updateStyle(); } } @@ -1372,7 +1604,7 @@ CoreComponent::updateStyle() void CoreComponent::setState( StateProperty stateProperty, bool value ) { - if (mInheritParentState) { + if (mCoreFlags.isSet(kCoreComponentFlagInheritParentState)) { CONSOLE(mContext) << "Cannot assign state properties to a child that inherits parent state"; return; } @@ -1417,7 +1649,9 @@ CoreComponent::setDirty( PropertyKey key ) if (mDirty.emplace(key).second) { mContext->setDirty(shared_from_this()); - if (!isVisualContextDirty() || !mTextMeasurementHashStale || !mVisualHashStale) { + if (!isVisualContextDirty() || + !mCoreFlags.isSet(kCoreComponentFlagTextMeasurementHashStale) || + !mCoreFlags.isSet(kCoreComponentFlagVisualHashStale)) { auto def = propDefSet().find(key); if (def == propDefSet().end()) return; @@ -1432,13 +1666,13 @@ CoreComponent::setDirty( PropertyKey key ) setVisibilityDirty(); // Set text measurement hash as stale - if (!mTextMeasurementHashStale && (def->second.flags & kPropTextHash)) { - mTextMeasurementHashStale = true; + if (!mCoreFlags.isSet(kCoreComponentFlagTextMeasurementHashStale) && (def->second.flags & kPropTextHash)) { + mCoreFlags.set(kCoreComponentFlagTextMeasurementHashStale); } // Set visual hash as stale - if (!mVisualHashStale && (def->second.flags & kPropVisualHash)) { - mVisualHashStale = true; + if (!mCoreFlags.isSet(kCoreComponentFlagVisualHashStale) && (def->second.flags & kPropVisualHash)) { + mCoreFlags.set(kCoreComponentFlagVisualHashStale); } if (def->second.flags & kPropAccessibility) { @@ -1478,7 +1712,7 @@ CoreComponent::getParentIfInDocument() const void CoreComponent::updateInheritedState() { - if (!mInheritParentState || !mParent) + if (!mCoreFlags.isSet(kCoreComponentFlagInheritParentState) || !mParent) return; mState = mParent->getState(); @@ -1543,7 +1777,7 @@ CoreComponent::shouldPropagateLayoutChanges() const return !mChildren.empty() && static_cast(getCalculated(kPropertyDisplay).getInteger()) != kDisplayNone; } -std::string +size_t CoreComponent::textMeasurementHash() const { return mTextMeasurementHash; @@ -1553,8 +1787,7 @@ void CoreComponent::fixTextMeasurementHash() { auto& pds = propDefSet(); - if (!mTextMeasurementHashStale) return; - mTextMeasurementHashStale = false; + if (!mCoreFlags.checkAndClear(kCoreComponentFlagTextMeasurementHashStale)) return; size_t hash = 0; for (const auto& cpd : pds) { @@ -1565,14 +1798,13 @@ CoreComponent::fixTextMeasurementHash() } // Need to keep as string, as double or int will lead to loss of precision. - mTextMeasurementHash = std::to_string(hash); + mTextMeasurementHash = hash; } void CoreComponent::fixVisualHash(bool useDirtyFlag) { - if (!mVisualHashStale) return; - mVisualHashStale = false; + if (!mCoreFlags.checkAndClear(kCoreComponentFlagVisualHashStale)) return; size_t hash = 0; for (const auto& cpd : propDefSet()) { @@ -1698,6 +1930,8 @@ CoreComponent::postClearPending() // Process and report DOM (not layout) changes. processChildrenChanges(); refreshAccessibilityActions(true); + + processRebuildChanges(); } std::string @@ -1707,16 +1941,16 @@ CoreComponent::toStringAction(ChildChangeAction action) { void CoreComponent::processChildrenChanges() { - if (mChildrenChanges.empty()) + if (!mChildrenChanges || mChildrenChanges->empty()) return; // Report children changes to the runtime { auto& changes = mCalculated.get(kPropertyNotifyChildrenChanged).getMutableArray(); - for (const auto& c : mChildrenChanges) { + for (const auto& c : *mChildrenChanges) { auto change = std::make_shared(); - change->emplace(CHILDREN_CHANGE_INDEX, c.index); - change->emplace(CHILDREN_CHANGE_UID, c.component->getUniqueId()); + change->emplace(COMPONENT_INDEX, c.index); + change->emplace(COMPONENT_UID, c.uid); change->emplace(CHILDREN_CHANGE_ACTION, toStringAction(c.action)); changes.emplace_back(change); } @@ -1728,38 +1962,50 @@ CoreComponent::processChildrenChanges() { auto commands = mCalculated.get(kPropertyOnChildrenChanged); if (multiChild() && !commands.empty()) { auto handlerChanges = std::make_shared(); - for (const auto& c : mChildrenChanges) { + for (const auto& c : *mChildrenChanges) { auto change = std::make_shared(); - if (c.action == kChildChangeActionInsert) - change->emplace( - CHILDREN_CHANGE_INDEX, - getChildIndex(c.component) - ); - change->emplace(CHILDREN_CHANGE_UID, c.component->getUniqueId()); + if (c.action == kChildChangeActionInsert) { + auto comp = c.component.lock(); + if (comp) change->emplace(COMPONENT_INDEX, getChildIndex(comp)); + } + change->emplace(COMPONENT_UID, c.uid); change->emplace(CHILDREN_CHANGE_ACTION, toStringAction(c.action)); handlerChanges->emplace_back(change); } auto changesMap = std::make_shared(); changesMap->emplace(CHILDREN_CHANGE_CHANGES, handlerChanges); + changesMap->emplace(COMPONENT_LENGTH, getChildCount()); mContext->sequencer().executeCommands( commands, createEventContext("ChildrenChanged", changesMap, getValue()), shared_from_corecomponent(), true); } - mChildrenChanges.clear(); + mChildrenChanges->clear(); } void -CoreComponent::postProcessLayoutChanges() +CoreComponent::postProcessLayoutChanges(bool first) { // Mark this component as having been laid out at least once - mFlags |= kComponentFlagAllowEventHandlers; + mFlags.set(kComponentFlagAllowEventHandlers); + + auto commands = mCalculated.get(kPropertyOnLayout); + // Notify document about layout changes + if (!commands.empty() && (isDirty(kPropertyBounds) || first)) { + auto propMap = std::make_shared(); + auto bounds = getCalculated(kPropertyBounds).get(); + propMap->emplace("height", bounds.getHeight()); + propMap->emplace("width", bounds.getWidth()); + propMap->emplace("x", bounds.getX()); + propMap->emplace("y", bounds.getY()); + mContext->sequencer().executeCommands(commands, createEventContext("Layout", propMap, getValue()), shared_from_corecomponent(), true); + } for (auto& child : mChildren) if (child->isAttached()) - child->postProcessLayoutChanges(); + child->postProcessLayoutChanges(first); // update the displayed children ensureDisplayedChildren(); @@ -2032,7 +2278,7 @@ CoreComponent::serializeAll(rapidjson::Document::AllocatorType& allocator) const component.AddMember("type", rapidjson::StringRef(sComponentTypeBimap.at(getType()).c_str()), allocator); component.AddMember("__id", rapidjson::Value(mId.c_str(), allocator), allocator); - component.AddMember("__inheritParentState", mInheritParentState, allocator); + component.AddMember("__inheritParentState", mCoreFlags.isSet(kCoreComponentFlagInheritParentState), allocator); component.AddMember("__style", rapidjson::Value(mStyle.c_str(), allocator), allocator); component.AddMember("__path", rapidjson::Value(mPath.toString().c_str(), allocator), allocator); @@ -2242,8 +2488,6 @@ 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(); } } @@ -2304,7 +2548,7 @@ CoreComponent::getTags(rapidjson::Value &outMap, rapidjson::Document::AllocatorT bool actionable = false; bool checked = mState.get(kStateChecked); bool disabled = mState.get(kStateDisabled); - if(checked && !mInheritParentState) { + if(checked && !mCoreFlags.isSet(kCoreComponentFlagInheritParentState)) { outMap.AddMember("checked", checked, allocator); } @@ -2319,12 +2563,12 @@ CoreComponent::getTags(rapidjson::Value &outMap, rapidjson::Document::AllocatorT if(mParent && mParent->scrollable() && mParent->multiChild()) { rapidjson::Value listItem(rapidjson::kObjectType); - listItem.AddMember("index", mContext->opt("index").asInt(), allocator); + listItem.AddMember("index", mContext->opt(COMPONENT_INDEX).asInt(), allocator); outMap.AddMember("listItem", listItem, allocator); } - if(mParent && mParent->getCalculated(kPropertyNumbered).truthy() && mContext->has("ordinal")) { - outMap.AddMember("ordinal", mContext->opt("ordinal").asInt(), allocator); + if(mParent && mParent->getCalculated(kPropertyNumbered).truthy() && mContext->has(COMPONENT_ORDINAL)) { + outMap.AddMember("ordinal", mContext->opt(COMPONENT_ORDINAL).asInt(), allocator); } if(!getCalculated(kPropertySpeech).empty()) { @@ -2376,7 +2620,7 @@ bool CoreComponent::isDisplayable() const { return (getCalculated(kPropertyDisplay).asInt() == kDisplayNormal) && (getCalculated(kPropertyOpacity).asNumber() > 0) - && !mIsDisallowed; + && !mCoreFlags.isSet(kCoreComponentFlagIsDisallowed); } void @@ -2516,22 +2760,22 @@ CoreComponent::executeEventHandler(const std::string& event, const Object& comma } YGSize -CoreComponent::textMeasureInternal(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) +CoreComponent::textMeasure(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) { APL_TRACE_BLOCK("CoreComponent:textMeasureInternal"); // Recalculate visual hash if marked as stale fixVisualHash(true); - auto componentHash = textMeasurementHash(); + auto textPropertiesHash = textMeasurementHash(); LOG_IF(DEBUG_MEASUREMENT).session(mContext) << "Measuring: " << getUniqueId() - << " hash: " << componentHash + << " hash: " << textPropertiesHash << " width: " << width << " widthMode: " << widthMode << " height: " << height << " heightMode: " << heightMode; - TextMeasureRequest tmr = {width, widthMode, height, heightMode, componentHash}; + TextMeasureRequest tmr = {width, widthMode, height, heightMode, textPropertiesHash}; auto& measuresCache = getContext()->cachedMeasures(); if (measuresCache.has(tmr)) { return measuresCache.get(tmr); @@ -2548,7 +2792,7 @@ CoreComponent::textMeasureInternal(float width, YGMeasureMode widthMode, float h } float -CoreComponent::textBaselineInternal(float width, float height) +CoreComponent::textBaseline(float width, float height) { APL_TRACE_BEGIN("CoreComponent:textBaselineInternal"); TextMeasureRequest tmr = { @@ -2579,7 +2823,7 @@ CoreComponent::textMeasureFunc( YGNodeRef node, { auto *component = static_cast(node->getContext()); assert(component); - return component->textMeasureInternal(width, widthMode, height, heightMode); + return component->textMeasure(width, widthMode, height, heightMode); } float @@ -2587,7 +2831,7 @@ CoreComponent::textBaselineFunc( YGNodeRef node, float width, float height ) { auto *component = static_cast(node->getContext()); assert(component); - return component->textBaselineInternal(width, height); + return component->textBaseline(width, height); } // Old style static actions. Just copy defined and implicit to the output. @@ -2621,7 +2865,7 @@ CoreComponent::markAccessibilityDirty() return; mContext->setDirty(shared_from_this()); - mAccessibilityDirty = true; + mCoreFlags.set(kCoreComponentFlagAccessibilityDirty); } // New style dynamic actions @@ -2631,8 +2875,7 @@ CoreComponent::refreshAccessibilityActions(bool useDirtyFlag) if (!getRootConfig().experimentalFeatureEnabled(RootConfig::kExperimentalFeatureDynamicAccessibilityActions)) return; - if (!mAccessibilityDirty) return; - mAccessibilityDirty = false; + if (!mCoreFlags.checkAndClear(kCoreComponentFlagAccessibilityDirty)) return; auto current = getCalculated(kPropertyAccessibilityActions).getArray(); if (getCalculated(kPropertyDisabled).asBoolean()) { @@ -2688,6 +2931,8 @@ CoreComponent::propDefSet() const { kPropDynamic}, {kPropertyAccessibilityActions, Object::EMPTY_ARRAY(), asArray, kPropOut | kPropAccessibility}, {kPropertyAccessibilityActionsAssigned, Object::EMPTY_ARRAY(), asArray, kPropIn}, + {kPropertyBackground, Color(), asFill, kPropOut | + kPropVisualHash}, {kPropertyBounds, Rect(0,0,0,0), nullptr, kPropOut | kPropVisualContext | kPropVisibility | @@ -2740,6 +2985,7 @@ CoreComponent::propDefSet() const { kPropDynamic | kPropStyled, yn::setMinWidth}, {kPropertyOnChildrenChanged, Object::EMPTY_ARRAY(), asCommand, kPropIn}, + {kPropertyOnLayout, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyOnMount, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyOnSpeechMark, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyOpacity, 1.0, asOpacity, kPropInOut | @@ -2807,6 +3053,8 @@ CoreComponent::propDefSet() const { kPropVisibility}, {kPropertyVisualHash, "", asString, kPropOut | kPropRuntimeState}, + {kPropertyPointerEvents, kPointerEventsAuto, sPointerEventsMap, kPropIn | + kPropDynamic}, }); return sCommonComponentProperties; @@ -2893,7 +3141,7 @@ CoreComponent::ensureGlobalToLocalTransform() { mParent->ensureGlobalToLocalTransform(); } - if (!mGlobalToLocalIsStale) { + if (!mCoreFlags.isSet(kCoreComponentFlagGlobalToLocalIsStale)) { return; } @@ -2928,7 +3176,7 @@ CoreComponent::ensureGlobalToLocalTransform() { } } - mGlobalToLocalIsStale = false; + mCoreFlags.clear(kCoreComponentFlagGlobalToLocalIsStale); } const Transform2D& diff --git a/aplcore/src/component/edittextcomponent.cpp b/aplcore/src/component/edittextcomponent.cpp index 8f382b1..9d3b588 100644 --- a/aplcore/src/component/edittextcomponent.cpp +++ b/aplcore/src/component/edittextcomponent.cpp @@ -21,19 +21,20 @@ #include "apl/engine/event.h" #include "apl/focus/focusmanager.h" #include "apl/primitives/unicode.h" +#include "apl/scenegraph/edittextbox.h" +#include "apl/scenegraph/edittextconfig.h" +#include "apl/scenegraph/textchunk.h" +#include "apl/scenegraph/textlayout.h" +#include "apl/scenegraph/textmeasurement.h" +#include "apl/scenegraph/textproperties.h" +#include "apl/scenegraph/utilities.h" #include "apl/time/sequencer.h" #include "apl/touch/pointerevent.h" #ifdef SCENEGRAPH #include "apl/scenegraph/builder.h" #include "apl/scenegraph/edittext.h" -#include "apl/scenegraph/edittextbox.h" -#include "apl/scenegraph/edittextconfig.h" #include "apl/scenegraph/edittextfactory.h" #include "apl/scenegraph/scenegraphupdates.h" -#include "apl/scenegraph/textchunk.h" -#include "apl/scenegraph/textlayout.h" -#include "apl/scenegraph/textmeasurement.h" -#include "apl/scenegraph/utilities.h" #endif // SCENEGRAPH namespace apl { @@ -53,35 +54,42 @@ EditTextComponent::EditTextComponent(const ContextPtr& context, const Path& path) : ActionableComponent(context, std::move(properties), path) { - mIsDisallowed = context->getRootConfig().getProperty(RootProperty::kDisallowEditText).asBoolean(); -#ifdef SCENEGRAPH - static auto sgTextMeasureFunc = [](YGNodeRef node, float width, YGMeasureMode widthMode, - float height, YGMeasureMode heightMode) -> YGSize { - // TODO: Hash this properly so we don't call it multiple times - auto self = static_cast(node->getContext()); - return self->measureEditText( - MeasureRequest(width, toMeasureMode(widthMode), height, toMeasureMode(heightMode))); - }; - - static auto sgTextBaselineFunc = [](YGNodeRef node, float width, float height) -> float { - auto self = static_cast(node->getContext()); - return self->baselineText(width, height); - }; + if (context->getRootConfig().getProperty(RootProperty::kDisallowEditText).asBoolean()) + mCoreFlags.set(kCoreComponentFlagIsDisallowed); + YGNodeSetMeasureFunc(mYGNodeRef, textMeasureFunc); + YGNodeSetBaselineFunc(mYGNodeRef, textBaselineFunc); +#ifdef SCENEGRAPH if (context->getRootConfig().getEditTextFactory()) { - YGNodeSetMeasureFunc(mYGNodeRef, sgTextMeasureFunc); - YGNodeSetBaselineFunc(mYGNodeRef, sgTextBaselineFunc); YGNodeSetNodeType(mYGNodeRef, YGNodeTypeDefault); } else { #endif // SCENEGRAPH - YGNodeSetMeasureFunc(mYGNodeRef, textMeasureFunc); - YGNodeSetBaselineFunc(mYGNodeRef, textBaselineFunc); YGNodeSetNodeType(mYGNodeRef, YGNodeTypeText); #ifdef SCENEGRAPH } #endif // SCENEGRAPH } +YGSize +EditTextComponent::textMeasure(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) +{ + if (mContext->measure()->layoutCompatible()) { + return measureEditText(MeasureRequest(width, toMeasureMode(widthMode), height, toMeasureMode(heightMode))); + } else { + return ActionableComponent::textMeasure(width, widthMode, height, heightMode); + } +} + +float +EditTextComponent::textBaseline(float width, float height) +{ + if (mContext->measure()->layoutCompatible()) { + return baselineText(width, height); + } else { + return ActionableComponent::textBaseline(width, height); + } +} + /* * Initial assignment of properties. Don't set any dirty flags here; this * all should be running in the constructor. @@ -190,10 +198,10 @@ EditTextComponent::propDefSet() const }; static auto styleOrWeightChanged = [](Component& component) -> void { -#ifdef SCENEGRAPH auto& self = static_cast(component); self.mEditTextProperties = nullptr; self.mEditTextBox = nullptr; +#ifdef SCENEGRAPH self.mEditTextConfig = nullptr; if (!self.mLastMeasureRequest.isExact()) @@ -202,12 +210,12 @@ EditTextComponent::propDefSet() const }; static auto familyOrSizeChanged = [](Component& component) -> void { -#ifdef SCENEGRAPH auto& self = static_cast(component); - self.mHintLayout = nullptr; - self.mHintTextProperties = nullptr; self.mEditTextProperties = nullptr; self.mEditTextBox = nullptr; +#ifdef SCENEGRAPH + self.mHintLayout = nullptr; + self.mHintTextProperties = nullptr; self.mEditTextConfig = nullptr; if (!self.mLastMeasureRequest.isExact()) @@ -216,10 +224,9 @@ EditTextComponent::propDefSet() const }; static auto sizeChanged = [](Component& component) -> void { -#ifdef SCENEGRAPH auto& self = static_cast(component); self.mEditTextBox = nullptr; - +#ifdef SCENEGRAPH if (!self.mLastMeasureRequest.isExact()) YGNodeMarkDirty(self.mYGNodeRef); #endif // SCENEGRAPH @@ -382,7 +389,6 @@ EditTextComponent::executeOnFocus() { #endif // SCENEGRAPH } -#ifdef SCENEGRAPH YGSize EditTextComponent::measureEditText(MeasureRequest&& request) { @@ -394,10 +400,11 @@ EditTextComponent::measureEditText(MeasureRequest&& request) if (!mEditTextBox) { ensureEditTextProperties(); - assert(mContext->measure()->sceneGraphCompatible()); + assert(mContext->measure()->layoutCompatible()); auto measure = std::static_pointer_cast(mContext->measure()); mEditTextBox = measure->box( + this, getCalculated(kPropertySize).getInteger(), mEditTextProperties, mLastMeasureRequest.width(), @@ -420,6 +427,50 @@ EditTextComponent::baselineText(float width, float height) return mEditTextBox ? mEditTextBox->getBaseline() : 0.0f; } +bool +EditTextComponent::ensureEditTextBox() +{ + if (mEditTextBox) + return false; + + ensureEditTextProperties(); + + assert(mContext->measure()->layoutCompatible()); + auto measure = std::static_pointer_cast(mContext->measure()); + + const auto& innerBounds = getCalculated(kPropertyInnerBounds).get(); + + mEditTextBox = measure->box(this, + getCalculated(kPropertySize).getInteger(), + mEditTextProperties, + innerBounds.getWidth(), + MeasureMode::AtMost, + innerBounds.getHeight(), + MeasureMode::AtMost); + + return true; +} + +bool +EditTextComponent::ensureEditTextProperties() +{ + if (mEditTextProperties) + return false; + + mEditTextProperties = sg::TextProperties::create( + mContext->textPropertiesCache(), + sg::splitFontString(mContext->getRootConfig(), + mContext->session(), + getCalculated(kPropertyFontFamily).getString()), + getCalculated(kPropertyFontSize).asFloat(), + getCalculated(kPropertyFontStyle).asEnum(), + getCalculated(kPropertyLang).getString(), + getCalculated(kPropertyFontWeight).getInteger()); + + return true; +} + +#ifdef SCENEGRAPH /* * The EditText scene graph structure: * @@ -601,30 +652,6 @@ EditTextComponent::updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) return result; } - -bool -EditTextComponent::ensureEditTextBox() -{ - if (mEditTextBox) - return false; - - ensureEditTextProperties(); - - assert(mContext->measure()->sceneGraphCompatible()); - auto measure = std::static_pointer_cast(mContext->measure()); - - const auto& innerBounds = getCalculated(kPropertyInnerBounds).get(); - - mEditTextBox = measure->box(getCalculated(kPropertySize).getInteger(), - mEditTextProperties, - innerBounds.getWidth(), - MeasureMode::AtMost, - innerBounds.getHeight(), - MeasureMode::AtMost); - - return true; -} - bool EditTextComponent::ensureEditConfig() { @@ -648,25 +675,6 @@ EditTextComponent::ensureEditConfig() return true; } -bool -EditTextComponent::ensureEditTextProperties() -{ - if (mEditTextProperties) - return false; - - mEditTextProperties = sg::TextProperties::create( - mContext->textPropertiesCache(), - sg::splitFontString(mContext->getRootConfig(), - mContext->session(), - getCalculated(kPropertyFontFamily).getString()), - getCalculated(kPropertyFontSize).asFloat(), - getCalculated(kPropertyFontStyle).asEnum(), - getCalculated(kPropertyLang).getString(), - getCalculated(kPropertyFontWeight).getInteger()); - - return true; -} - /** * Ensure that the hint layout has been constructed. This should only be called after a layout * pass so that we have a valid "innerBounds" property. @@ -700,7 +708,7 @@ EditTextComponent::ensureHintLayout() auto borderWidth = getCalculated(kPropertyBorderWidth).asFloat(); auto innerBounds = getCalculated(kPropertyInnerBounds).get().inset(borderWidth); - assert(mContext->measure()->sceneGraphCompatible()); + assert(mContext->measure()->layoutCompatible()); auto measure = std::static_pointer_cast(mContext->measure()); mHintLayout = diff --git a/aplcore/src/component/framecomponent.cpp b/aplcore/src/component/framecomponent.cpp index e6fad41..b3cf7ba 100644 --- a/aplcore/src/component/framecomponent.cpp +++ b/aplcore/src/component/framecomponent.cpp @@ -97,8 +97,6 @@ FrameComponent::propDefSet() const {kPropertyBackgroundAssigned, Object::NULL_OBJECT(), asFill, kPropIn | kPropStyled | kPropDynamic, inlineFixBackground}, - {kPropertyBackground, Color(), asFill, kPropOut | - kPropVisualHash}, {kPropertyBorderRadii, Radii(), nullptr, kPropOut | kPropVisualHash}, {kPropertyBorderColor, Color(), asColor, kPropInOut | diff --git a/aplcore/src/component/hostcomponent.cpp b/aplcore/src/component/hostcomponent.cpp index 5a67d44..32c1fa4 100644 --- a/aplcore/src/component/hostcomponent.cpp +++ b/aplcore/src/component/hostcomponent.cpp @@ -249,6 +249,7 @@ HostComponent::initializeEmbedded(EmbeddedRequestSuccessResponse&& response) onFailHandler(url, "Embedded document failed to inflate"); return nullptr; } + mCalculated.set(kPropertyBackground, embedded->content()->getBackground()); auto coreTop = CoreComponent::cast(embedded->topComponent()); @@ -317,6 +318,7 @@ HostComponent::reinflate() // Resolving asynchronously, required to get extensions resolved. embedded->content()->refresh(metrics, config); + mCalculated.set(kPropertyBackground, embedded->content()->getBackground()); if (embedded->content()->isWaiting()) { auto timers = std::static_pointer_cast(config.getTimeManager()); @@ -636,9 +638,9 @@ HostComponent::processLayoutChanges(bool useDirtyFlag, bool first) } void -HostComponent::postProcessLayoutChanges() +HostComponent::postProcessLayoutChanges(bool first) { - ActionableComponent::postProcessLayoutChanges(); + ActionableComponent::postProcessLayoutChanges(first); if (mNeedToRequestDocument) requestEmbedded(); } diff --git a/aplcore/src/component/imagecomponent.cpp b/aplcore/src/component/imagecomponent.cpp index 18e8fd2..e6396eb 100644 --- a/aplcore/src/component/imagecomponent.cpp +++ b/aplcore/src/component/imagecomponent.cpp @@ -123,9 +123,9 @@ ImageComponent::getVisualContextType() const } void -ImageComponent::postProcessLayoutChanges() +ImageComponent::postProcessLayoutChanges(bool first) { - CoreComponent::postProcessLayoutChanges(); + CoreComponent::postProcessLayoutChanges(first); MediaComponentTrait::postProcessLayoutChanges(); } diff --git a/aplcore/src/component/multichildscrollablecomponent.cpp b/aplcore/src/component/multichildscrollablecomponent.cpp index fc046b7..8d04d6b 100644 --- a/aplcore/src/component/multichildscrollablecomponent.cpp +++ b/aplcore/src/component/multichildscrollablecomponent.cpp @@ -22,6 +22,7 @@ #include "apl/livedata/layoutrebuilder.h" #include "apl/time/sequencer.h" #include "apl/time/timemanager.h" +#include "apl/utils/constants.h" #include "apl/utils/session.h" #include "apl/utils/tracing.h" @@ -657,7 +658,7 @@ MultiChildScrollableComponent::getTags(rapidjson::Value& outMap, rapidjson::Docu auto highestOrdinalSeen = -1; for(int i = mIndexesSeen.lowerBound(); i<= mIndexesSeen.upperBound(); i++) { - auto ordinal = mChildren.at(i)->getContext()->opt("ordinal"); + auto ordinal = mChildren.at(i)->getContext()->opt(COMPONENT_ORDINAL); if(ordinal.isNull()) continue; diff --git a/aplcore/src/component/pagercomponent.cpp b/aplcore/src/component/pagercomponent.cpp index e32468d..9d5778c 100644 --- a/aplcore/src/component/pagercomponent.cpp +++ b/aplcore/src/component/pagercomponent.cpp @@ -534,10 +534,7 @@ PagerComponent::allowBackwards() const void PagerComponent::ensureDisplayedChildren() { - if (mChildren.empty() || !mDisplayedChildrenStale) - return; - - mDisplayedChildrenStale = false; + if (mChildren.empty() || !mCoreFlags.checkAndClear(kCoreComponentFlagDisplayedChildrenStale)) return; // clear previous calculations mDisplayedChildren.clear(); diff --git a/aplcore/src/component/scrollablecomponent.cpp b/aplcore/src/component/scrollablecomponent.cpp index 6395e6c..44c0900 100644 --- a/aplcore/src/component/scrollablecomponent.cpp +++ b/aplcore/src/component/scrollablecomponent.cpp @@ -200,6 +200,8 @@ ScrollableComponent::eventPropertyMap() const CoreComponent::eventPropertyMap(), { {"position", [](const CoreComponent *c) { return c->getValue(); }}, + {"allowForward", [](const CoreComponent *c) { return c->allowForward(); }}, + {"allowBackwards", [](const CoreComponent *c) { return c->allowBackwards(); }}, }); return sScrollableEventProperties; diff --git a/aplcore/src/component/selector.cpp b/aplcore/src/component/selector.cpp index d4aec80..b1ebb55 100644 --- a/aplcore/src/component/selector.cpp +++ b/aplcore/src/component/selector.cpp @@ -21,6 +21,7 @@ #include "apl/component/corecomponent.h" #include "apl/datagrammar/grammarpolyfill.h" #include "apl/document/documentcontextdata.h" +#include "apl/utils/constants.h" #include "apl/utils/make_unique.h" #include "apl/utils/session.h" @@ -383,16 +384,16 @@ static bool matchComponentType(const CoreComponentPtr& component, const std::str { // The first context should have __source=component auto context = component->getContext(); - assert(context->opt("__source").asString() == "component"); + assert(context->opt(COMPONENT_CONTEXT_SOURCE).asString() == "component"); // Check the __name property for a quick match - auto name = context->opt("__name"); + auto name = context->opt(COMPONENT_CONTEXT_NAME); if (name.isString() && name.getString() == type) return true; // Search upwards through the context for a layout that matches. while ((context = context->parent()) != nullptr) { - auto source = context->opt("__source"); + auto source = context->opt(COMPONENT_CONTEXT_SOURCE); if (!source.isString()) return false; @@ -401,7 +402,7 @@ static bool matchComponentType(const CoreComponentPtr& component, const std::str return false; // We match if a containing layout has this name - if (source.getString() == "layout" && context->opt("__name").asString() == type) + if (source.getString() == "layout" && context->opt(COMPONENT_CONTEXT_NAME).asString() == type) return true; } diff --git a/aplcore/src/component/textcomponent.cpp b/aplcore/src/component/textcomponent.cpp index e4edbcf..8a1593c 100644 --- a/aplcore/src/component/textcomponent.cpp +++ b/aplcore/src/component/textcomponent.cpp @@ -19,16 +19,19 @@ #include "apl/component/textmeasurement.h" #include "apl/content/rootconfig.h" #include "apl/primitives/styledtext.h" +#include "apl/time/sequencer.h" #include "apl/utils/session.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/builder.h" -#include "apl/scenegraph/scenegraph.h" #include "apl/scenegraph/textchunk.h" #include "apl/scenegraph/textlayout.h" +#include "apl/scenegraph/textlayoutcache.h" #include "apl/scenegraph/textmeasurement.h" #include "apl/scenegraph/textproperties.h" #include "apl/scenegraph/utilities.h" + +#ifdef SCENEGRAPH +#include "apl/scenegraph/builder.h" +#include "apl/scenegraph/scenegraph.h" #endif // SCENEGRAPH namespace apl { @@ -47,31 +50,11 @@ TextComponent::create(const ContextPtr& context, TextComponent::TextComponent(const ContextPtr& context, Properties&& properties, const Path& path) - : CoreComponent(context, std::move(properties), path) + : CoreComponent(context, std::move(properties), path), + mLayoutPossiblyStale(false) { -#ifdef SCENEGRAPH - static auto sgTextMeasureFunc = [](YGNodeRef node, float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) -> YGSize { - // TODO: Hash this properly so we don't call it multiple times - auto self = static_cast(node->getContext()); - return self->measureText(width, widthMode, height, heightMode); - }; - - static auto sgTextBaselineFunc = [](YGNodeRef node, float width, float height) -> float { - auto self = static_cast(node->getContext()); - return self->baselineText(width, height); - }; - - if (mContext->measure()->sceneGraphCompatible()) { - YGNodeSetMeasureFunc(mYGNodeRef, sgTextMeasureFunc); - YGNodeSetBaselineFunc(mYGNodeRef, sgTextBaselineFunc); - } else { -#endif // SCENEGRAPH - YGNodeSetMeasureFunc(mYGNodeRef, textMeasureFunc); - YGNodeSetBaselineFunc(mYGNodeRef, textBaselineFunc); -#ifdef SCENEGRAPH - } -#endif // SCENEGRAPH - + YGNodeSetMeasureFunc(mYGNodeRef, textMeasureFunc); + YGNodeSetBaselineFunc(mYGNodeRef, textBaselineFunc); YGNodeSetNodeType(mYGNodeRef, YGNodeTypeText); } @@ -97,28 +80,22 @@ TextComponent::propDefSet() const }; static auto fixTextAlignTrigger = [](Component& component) -> void { - auto& coreComp = (TextComponent&)component; - coreComp.updateTextAlign(true); -#ifdef SCENEGRAPH - coreComp.mTextProperties = nullptr; - coreComp.mLayout = nullptr; -#endif // SCENEGRAPH + auto& textComp = (TextComponent&)component; + textComp.updateTextAlign(true); + textComp.mTextProperties = nullptr; + textComp.mLayout = nullptr; }; static auto fixTextTrigger = [](Component& component) -> void { -#ifdef SCENEGRAPH - auto& coreComp = (TextComponent&)component; - coreComp.mTextProperties = nullptr; - coreComp.mLayout = nullptr; -#endif // SCENEGRAPH + auto& textComp = (TextComponent&)component; + textComp.mTextProperties = nullptr; + textComp.mLayout = nullptr; }; static auto fixTextChunkTrigger = [](Component& component) -> void { -#ifdef SCENEGRAPH - auto& coreComp = (TextComponent&)component; - coreComp.mTextChunk = nullptr; - coreComp.mLayout = nullptr; -#endif // SCENEGRAPH + auto& textComp = (TextComponent&)component; + textComp.mTextChunk = nullptr; + textComp.mLayout = nullptr; }; static ComponentPropDefSet sTextComponentProperties(CoreComponent::propDefSet(), { @@ -134,6 +111,7 @@ TextComponent::propDefSet() const {kPropertyLetterSpacing, Dimension(0), asAbsoluteDimension, kPropInOut | kPropLayout | kPropStyled | kPropTextHash | kPropVisualHash, fixTextTrigger}, {kPropertyLineHeight, 1.25, asNonNegativeNumber, kPropInOut | kPropLayout | kPropStyled | kPropTextHash | kPropVisualHash, fixTextTrigger}, {kPropertyMaxLines, 0, asInteger, kPropInOut | kPropLayout | kPropStyled | kPropTextHash | kPropVisualHash, fixTextTrigger}, + {kPropertyOnTextLayout, Object::EMPTY_ARRAY(), asCommand, kPropIn}, {kPropertyText, StyledText::EMPTY(), asStyledText, kPropInOut | kPropLayout | kPropDynamic | kPropVisualContext | kPropTextHash | kPropVisualHash, fixTextChunkTrigger}, {kPropertyTextAlign, kTextAlignAuto, sTextAlignMap, kPropOut | kPropTextHash | kPropVisualHash, fixTextTrigger}, {kPropertyTextAlignAssigned, kTextAlignAuto, sTextAlignMap, kPropIn | kPropLayout | kPropStyled | kPropDynamic, fixTextAlignTrigger}, @@ -287,28 +265,136 @@ TextComponent::assignProperties(const ComponentPropDefSet& propDefSet) fixTextMeasurementHash(); } -rapidjson::Value -TextComponent::serializeMeasure(rapidjson::Document::AllocatorType& allocator) const +std::string +TextComponent::getVisualContextType() const { - rapidjson::Value component(rapidjson::kObjectType); + return getValue().empty() ? VISUAL_CONTEXT_TYPE_EMPTY : VISUAL_CONTEXT_TYPE_TEXT; +} - component.AddMember("id", rapidjson::Value(mUniqueId.c_str(), allocator).Move(), allocator); +YGSize +TextComponent::textMeasure(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) +{ + if (mContext->measure()->layoutCompatible()) { + auto tm = std::static_pointer_cast(mContext->measure()); - for (const auto& pds : propDefSet()) { - if ((pds.second.flags & kPropLayout) != 0) - component.AddMember( - rapidjson::StringRef(pds.second.names[0].c_str()), // We assume long-lived strings here - mCalculated.get(pds.first).serialize(allocator), - allocator); + ensureTextProperties(); + + auto hash = mTextProperties->hash(); + hashCombine(hash, mTextChunk->hash()); + TextMeasureRequest tmr = {width, widthMode, height, heightMode, hash}; + auto& layoutCache = getContext()->textLayoutCache(); + auto layout = layoutCache.find(tmr); + if (layout) { + mLayout = layout; + } else { + mLayout = tm->layout(this, mTextChunk, mTextProperties, width, toMeasureMode(widthMode), + height, toMeasureMode(heightMode)); + layoutCache.insert(tmr, mLayout); + } + + if (!mLayout) + return {0, 0}; // No text, no layout + + auto size = mLayout->getSize(); + + onTextLayout(); + + return YGSize{size.getWidth(), size.getHeight()}; + } else { + return CoreComponent::textMeasure(width, widthMode, height, heightMode); } +} + +float +TextComponent::textBaseline(float width, float height) +{ + if (mContext->measure()->layoutCompatible()) { + // Make the large assumption that Yoga needs baseline information with a text layout + if (!mLayout) { + auto tm = std::static_pointer_cast(mContext->measure()); + ensureTextProperties(); + mLayout = tm->layout(this, mTextChunk, mTextProperties, width, MeasureMode::Undefined, + height, MeasureMode::Undefined); + } - return component; + return mLayout ? mLayout->getBaseline() : 0; + } else { + return CoreComponent::textBaseline(width, height); + } } -std::string -TextComponent::getVisualContextType() const +void +TextComponent::ensureTextLayout() { - return getValue().empty() ? VISUAL_CONTEXT_TYPE_EMPTY : VISUAL_CONTEXT_TYPE_TEXT; + if (!mContext->measure()->layoutCompatible()) return; + + // Having a layout is no guarantee that the layout is correct. Yoga caches previous layout calculations + // for efficiency. If the layout exists, you also need to check that it matches the desired size and scaling modes. + if (mLayout) { + if (!mLayoutPossiblyStale) return; + mLayoutPossiblyStale = false; + + // Yoga rounds text box sizes. They should be rounded UP (to ensure no clipping). + // They round to the nearest pixel dimension. We'll assume that if the layout size + // is within 2 pixels of the measured size and _larger_ than the measured size, then + // we are okay and don't need to re-layout. + auto s = mLayout->getSize(); + auto dw = mContext->dpToPx(YGNodeLayoutGetWidth(mYGNodeRef) - s.getWidth()); + auto dh = mContext->dpToPx(YGNodeLayoutGetHeight(mYGNodeRef) - s.getHeight()); + + if (dw >= 0.0 && dw <= 2.0 && dh >= 0.0 && dh <= 2.0) + return; + } + + auto measure = std::static_pointer_cast(mContext->measure()); + + const auto& innerBounds = getCalculated(kPropertyInnerBounds).get(); + + ensureTextProperties(); + mLayout = measure->layout(this, mTextChunk, mTextProperties, innerBounds.getWidth(), + MeasureMode::AtMost, innerBounds.getHeight(), MeasureMode::AtMost); + + if (!mLayout) return; + + onTextLayout(); +} + +void +TextComponent::ensureTextProperties() +{ + if (!mTextChunk) + mTextChunk = sg::TextChunk::create(getCalculated(kPropertyText).get()); + + if (!mTextProperties) + mTextProperties = sg::TextProperties::create( + mContext->textPropertiesCache(), + sg::splitFontString(mContext->getRootConfig(), + mContext->session(), + getCalculated(kPropertyFontFamily).getString()), + getCalculated(kPropertyFontSize).asFloat(), + getCalculated(kPropertyFontStyle).asEnum(), + getCalculated(kPropertyLang).getString(), + getCalculated(kPropertyFontWeight).getInteger(), + getCalculated(kPropertyLetterSpacing).asFloat(), + getCalculated(kPropertyLineHeight).asFloat(), + getCalculated(kPropertyMaxLines).getInteger(), + getCalculated(kPropertyTextAlign).asEnum(), + getCalculated(kPropertyTextAlignVertical).asEnum()); +} + +void +TextComponent::onTextLayout() { + assert(mLayout); + auto commands = mCalculated.get(kPropertyOnTextLayout); + if (!commands.empty()) { + auto propMap = std::make_shared(); + auto size = mLayout->getSize(); + propMap->emplace("isTruncated", mLayout->isTruncated()); + propMap->emplace("laidOutText", mLayout->getLaidOutText()); + propMap->emplace("textWidth", size.getWidth()); + propMap->emplace("textHeight", size.getHeight()); + mContext->sequencer().executeCommands(commands, createEventContext("TextLayout", propMap, getValue()), shared_from_corecomponent(), true); + } } #ifdef SCENEGRAPH @@ -366,7 +452,9 @@ TextComponent::constructSceneGraphLayer(sg::SceneGraphUpdates& sceneGraph) { auto layer = CoreComponent::constructSceneGraphLayer(sceneGraph); assert(layer); - ensureSGTextLayout(); + // Non-layout measure does not work for SG. + assert(mContext->measure()->layoutCompatible()); + ensureTextLayout(); auto transform = sg::transform(calcSceneGraphOffset(), nullptr); populateTextNodes(sg::TransformNode::cast(transform)); @@ -409,7 +497,7 @@ TextComponent::updateSceneGraphInternal(sg::SceneGraphUpdates& sceneGraph) auto* transform = sg::TransformNode::cast(mSceneGraphLayer->content()); auto* text = sg::TextNode::cast(transform->child()); - ensureSGTextLayout(); + ensureTextLayout(); const bool fixText = text->getTextLayout() != mLayout; const bool fixColor = isDirty(kPropertyColor); @@ -478,92 +566,10 @@ TextComponent::populateTextNodes(sg::Node *transform) } } -YGSize -TextComponent::measureText(float width, YGMeasureMode widthMode, float height, YGMeasureMode heightMode) -{ - assert(mContext->measure()->sceneGraphCompatible()); - auto *tm = (sg::TextMeasurement *)(mContext->measure().get()); - - // TODO: Hash and check for cache hits - ensureTextProperties(); - mLayout = tm->layout(mTextChunk, mTextProperties, width, toMeasureMode(widthMode), - height, toMeasureMode(heightMode)); - if (!mLayout) - return {0, 0}; // No text, no layout - - auto size = mLayout->getSize(); - return YGSize{size.getWidth(), size.getHeight()}; -} - -float -TextComponent::baselineText(float width, float height) -{ - // Make the large assumption that Yoga needs baseline information with a text layout - if (!mLayout) { - assert(mContext->measure()->sceneGraphCompatible()); - auto *tm = (sg::TextMeasurement *)(mContext->measure().get()); - ensureTextProperties(); - mLayout = tm->layout(mTextChunk, mTextProperties, width, MeasureMode::Undefined, - height, MeasureMode::Undefined); - } - - return mLayout ? mLayout->getBaseline() : 0; -} - -void -TextComponent::ensureSGTextLayout() -{ - // Having a layout is no guarantee that the layout is correct. Yoga caches previous layout calculations - // for efficiency. If the layout exists, you also need to check that it matches the desired size and scaling modes. - if (mLayout) { - // Yoga rounds text box sizes. They should be rounded UP (to ensure no clipping). - // They round to the nearest pixel dimension. We'll assume that if the layout size - // is within 2 pixels of the measured size and _larger_ than the measured size, then - // we are okay and don't need to re-layout. - auto s = mLayout->getSize(); - auto dw = mContext->dpToPx(YGNodeLayoutGetWidth(mYGNodeRef) - s.getWidth()); - auto dh = mContext->dpToPx(YGNodeLayoutGetHeight(mYGNodeRef) - s.getHeight()); - - if (dw >= 0.0 && dw <= 2.0 && dh >= 0.0 && dh <= 2.0) - return; - } - - assert(mContext->measure()->sceneGraphCompatible()); - auto measure = std::static_pointer_cast(mContext->measure()); - - const auto& innerBounds = getCalculated(kPropertyInnerBounds).get(); - - ensureTextProperties(); - mLayout = measure->layout(mTextChunk, mTextProperties, innerBounds.getWidth(), - MeasureMode::AtMost, innerBounds.getHeight(), MeasureMode::AtMost); -} - -void -TextComponent::ensureTextProperties() -{ - if (!mTextChunk) - mTextChunk = sg::TextChunk::create(getCalculated(kPropertyText).get()); - - if (!mTextProperties) - mTextProperties = sg::TextProperties::create( - mContext->textPropertiesCache(), - sg::splitFontString(mContext->getRootConfig(), - mContext->session(), - getCalculated(kPropertyFontFamily).getString()), - getCalculated(kPropertyFontSize).asFloat(), - getCalculated(kPropertyFontStyle).asEnum(), - getCalculated(kPropertyLang).getString(), - getCalculated(kPropertyFontWeight).getInteger(), - getCalculated(kPropertyLetterSpacing).asFloat(), - getCalculated(kPropertyLineHeight).asFloat(), - getCalculated(kPropertyMaxLines).getInteger(), - getCalculated(kPropertyTextAlign).asEnum(), - getCalculated(kPropertyTextAlignVertical).asEnum()); -} - bool TextComponent::setKaraokeLine(Range byteRange) { + assert(mContext->measure()->layoutCompatible()); const auto& previousLineRange = mCalculated.get(kPropertyRangeKaraokeTarget).get(); if (byteRange.empty()) { @@ -576,9 +582,8 @@ TextComponent::setKaraokeLine(Range byteRange) } // We have to ensure we have a text layout so we can find the start and end of lines - ensureSGTextLayout(); + ensureTextLayout(); auto lineRange = mLayout->getLineRangeFromByteRange(byteRange); - if (previousLineRange != lineRange) { mCalculated.set(kPropertyRangeKaraokeTarget, std::move(lineRange)); setDirty(kPropertyRangeKaraokeTarget); @@ -595,9 +600,24 @@ TextComponent::getKaraokeBounds() if (range.empty()) return {}; - ensureSGTextLayout(); + ensureTextLayout(); return mLayout->getBoundingBoxForLines(range); } #endif // SCENEGRAPH +void +TextComponent::postProcessLayoutChanges(bool first) +{ + CoreComponent::postProcessLayoutChanges(first); + if (isDirty(kPropertyInnerBounds)) { + // Text layout MAY need to be changed. + mLayoutPossiblyStale = true; + } + + // If text layout was not populated during layout - report it now + if (!mCalculated.get(kPropertyOnTextLayout).empty()) { + ensureTextLayout(); + } +} + } // namespace apl diff --git a/aplcore/src/component/vectorgraphiccomponent.cpp b/aplcore/src/component/vectorgraphiccomponent.cpp index 75b8fbd..e325fa1 100644 --- a/aplcore/src/component/vectorgraphiccomponent.cpp +++ b/aplcore/src/component/vectorgraphiccomponent.cpp @@ -446,8 +446,9 @@ VectorGraphicComponent::getSources() { } void -VectorGraphicComponent::postProcessLayoutChanges() { - CoreComponent::postProcessLayoutChanges(); +VectorGraphicComponent::postProcessLayoutChanges(bool first) +{ + CoreComponent::postProcessLayoutChanges(first); MediaComponentTrait::postProcessLayoutChanges(); } diff --git a/aplcore/src/component/videocomponent.cpp b/aplcore/src/component/videocomponent.cpp index cb7cf6c..e8da743 100644 --- a/aplcore/src/component/videocomponent.cpp +++ b/aplcore/src/component/videocomponent.cpp @@ -177,9 +177,10 @@ VideoComponent::VideoComponent(const ContextPtr& context, mMediaSequencer("VIDEO"+getUniqueId()), mScreenLock(mContext) { - mIsDisallowed = context->getRootConfig().getProperty(RootProperty::kDisallowVideo).asBoolean(); + if (context->getRootConfig().getProperty(RootProperty::kDisallowVideo).asBoolean()) + mCoreFlags.set(kCoreComponentFlagIsDisallowed); - if (!mIsDisallowed) { + if (!mCoreFlags.isSet(kCoreComponentFlagIsDisallowed)) { mMediaPlayer = mContext->mediaPlayerFactory().createPlayer( [this] (MediaPlayerEventType eventType, const MediaState& mediaState) { playerCallback(eventType, mediaState); diff --git a/aplcore/src/content/CMakeLists.txt b/aplcore/src/content/CMakeLists.txt index 9aa0879..1994131 100644 --- a/aplcore/src/content/CMakeLists.txt +++ b/aplcore/src/content/CMakeLists.txt @@ -23,7 +23,10 @@ target_sources_local(apl jsondata.cpp metrics.cpp package.cpp + packageresolver.cpp + pendingimportpackage.cpp rootconfig.cpp rootproperties.cpp + sharedjsondata.cpp viewport.cpp ) \ No newline at end of file diff --git a/aplcore/src/content/aplversion.cpp b/aplcore/src/content/aplversion.cpp index b759ccc..3d61880 100644 --- a/aplcore/src/content/aplversion.cpp +++ b/aplcore/src/content/aplversion.cpp @@ -37,6 +37,7 @@ static const Bimap sVersionMap = { { APLVersion::kAPLVersion20232, "2023.2" }, { APLVersion::kAPLVersion20233, "2023.3" }, { APLVersion::kAPLVersion20241, "2024.1" }, + { APLVersion::kAPLVersion20242, "2024.2" }, }; bool diff --git a/aplcore/src/content/configurationchange.cpp b/aplcore/src/content/configurationchange.cpp index f458186..52b623e 100644 --- a/aplcore/src/content/configurationchange.cpp +++ b/aplcore/src/content/configurationchange.cpp @@ -42,6 +42,14 @@ ConfigurationChange::mergeRootConfig(const RootConfig& oldRootConfig) const { auto rootConfig = oldRootConfig; + applyToRootConfig(rootConfig); + + return rootConfig; +} + +void +ConfigurationChange::applyToRootConfig(RootConfig& rootConfig) const +{ if ((mFlags & kConfigurationChangeScreenMode) != 0) rootConfig.set(RootProperty::kScreenMode, mScreenMode); @@ -59,8 +67,6 @@ ConfigurationChange::mergeRootConfig(const RootConfig& oldRootConfig) const rootConfig.setEnvironmentValue(prop.first, prop.second); } } - - return rootConfig; } ObjectMap diff --git a/aplcore/src/content/content.cpp b/aplcore/src/content/content.cpp index f210f4f..2f1baa7 100644 --- a/aplcore/src/content/content.cpp +++ b/aplcore/src/content/content.cpp @@ -25,6 +25,7 @@ #include "apl/content/jsondata.h" #include "apl/content/metrics.h" #include "apl/content/package.h" +#include "apl/content/packageresolver.h" #include "apl/content/settings.h" #include "apl/embed/embedrequest.h" #include "apl/engine/arrayify.h" @@ -38,17 +39,10 @@ namespace apl { static const bool DEBUG_CONTENT = false; -const char* DOCUMENT_IMPORT = "import"; const char* DOCUMENT_MAIN_TEMPLATE = "mainTemplate"; const char* DOCUMENT_ENVIRONMENT = "environment"; const char* DOCUMENT_LANGUAGE = "lang"; const char* DOCUMENT_LAYOUT_DIRECTION = "layoutDirection"; -const char* PACKAGE_TYPE = "type"; -const char* PACKAGE_TYPE_PACKAGE = "package"; -const char* PACKAGE_TYPE_ONEOF = "oneOf"; -const char* PACKAGE_TYPE_ALLOF = "allOf"; -const char* PACKAGE_OTHERWISE = "otherwise"; -const char* PACKAGE_ITEMS = "items"; ContentPtr Content::create(JsonData&& document) @@ -108,6 +102,54 @@ Content::Content(const SessionPtr& session, mConfig(rootConfig) {} +void +Content::load(SuccessCallback&& onSuccess, FailureCallback&& onFailure) +{ + if (mState == ERROR) { + onFailure(); + return; + } + + // If we're already loaded, then invoke the success callback right away. + if (mState == READY && !mOrderedDependencies.empty()) { + onSuccess(); + return; + } + + if (mContentPackageManager != nullptr) { + mContentPackageManager->mRequested.clear(); + } + + auto weakSelf = std::weak_ptr(shared_from_this()); + mCurrentPendingImports = mPackageResolver->load( + mEvaluationContext, + mSession, + mMainPackage, + mOrderedDependencies, + [weakSelf, onSuccess](std::vector&& ordered) { + auto self = weakSelf.lock(); + if (!self) return; + + self->mOrderedDependencies = std::move(ordered); + self->updateStatus(); + onSuccess(); + }, + [weakSelf, onFailure](const ImportRef& request, const std::string& errorMessage, int errorCode) { + auto self = weakSelf.lock(); + if (!self) return; + + self->mState = ERROR; + CONSOLE(self->mSession) << "Content could not load requested packages."; + onFailure(); + }, + [weakSelf](const Package& package) { + auto self = weakSelf.lock(); + if (!self) return; + + self->addExtensions(package); + }); +} + void Content::refresh(const Metrics& metrics, const RootConfig& config) { @@ -124,24 +166,19 @@ Content::refresh(const Metrics& metrics, const RootConfig& config) getSession()); mExtensionRequests.clear(); - mMainPackage->mDependencies.clear(); + mState = LOADING; - for (const auto& pkg : mLoaded) { - pkg.second->mDependencies.clear(); - mStashed.emplace(pkg); + // Update the package manager in our package resolver. It may be new. + if (isManual()) { + mPackageResolver->setPackageManager(mContentPackageManager); + } else { + mPackageResolver->setPackageManager(mConfig.getPackageManager()); } - mLoaded.clear(); - mPending.clear(); - mRequested.clear(); - mOrderedDependencies.clear(); - - mState = LOADING; - - addImportList(*mMainPackage); addExtensions(*mMainPackage); - - updateStatus(); + if (isManual()) { + load([] {}, [] {}); + } } void @@ -159,14 +196,17 @@ Content::init(bool supportsEvaluation) mEvaluationContext = Context::createContentEvaluationContext( mMetrics, mConfig, mMainPackage->version(), extractTheme(mMetrics), getSession()); + if (isManual()) { + mContentPackageManager = std::make_shared(); + mPackageResolver = PackageResolver::create(mContentPackageManager, mSession); + } else { + mPackageResolver = PackageResolver::create(mConfig.getPackageManager(), mSession); + } // First chance where we can extract settings. Set up the session. auto diagnosticLabel = getDocumentSettings()->getValue("-diagnosticLabel").asString(); mSession->setLogIdPrefix(diagnosticLabel); LOG(LogLevel::kInfo).session(mSession) << "Initializing experience using " << std::string(sCoreRepositoryVersion); - addImportList(*mMainPackage); - addExtensions(*mMainPackage); - // Extract the array of main template parameters mMainParameters = ParameterArray::parameterNames(mMainTemplate); @@ -186,7 +226,11 @@ Content::init(bool supportsEvaluation) if (mPendingParameters.emplace(m).second) mAllParameters.emplace_back(m); - updateStatus(); + addExtensions(*mMainPackage); + // Start loading in manual mode + if (isManual()) { + load([]{}, []{}); + } } PackagePtr @@ -195,81 +239,35 @@ Content::getPackage(const std::string& name) const if (name == Path::MAIN) return mMainPackage; - auto it = std::find_if(mLoaded.begin(), mLoaded.end(), [&name](const std::pair& ref) { - return ref.first.toString() == name; + auto it = std::find_if(mOrderedDependencies.begin(), mOrderedDependencies.end(), [&name](const PackagePtr& ref) { + return ref->name() == name; }); - return it != mLoaded.end() ? it->second : nullptr; + return it != mOrderedDependencies.end() ? *it : nullptr; } std::set Content::getRequestedPackages() { - mPending.insert(mRequested.begin(), mRequested.end()); - auto result = mRequested; - mRequested.clear(); - return result; + if (!isManual()) return {}; + + auto requested = mContentPackageManager->mRequested; + mContentPackageManager->mRequested.clear(); + return requested; } -void -Content::loadPackage(const ImportRef& ref, const PackagePtr& package) +bool +Content::isWaiting() const { - LOG_IF(DEBUG_CONTENT).session(mSession) << "Adding package: " << &package; - mLoaded.emplace(ref, package); - addExtensions(*package); - addImportList(*package); + return !isError() && mCurrentPendingImports && !mCurrentPendingImports->isReady(); } void Content::addPackage(const ImportRequest& request, JsonData&& raw) { - if (mState != LOADING) - return; - - // If the package data is invalid, set the error state - if (!raw) { - CONSOLE(mSession).log("Package %s (%s) parse error offset=%u: %s", - request.reference().name().c_str(), - request.reference().version().c_str(), - raw.offset(), raw.error()); - mState = ERROR; - return; - } + if (!isManual()) return; - // We expect packages to be objects, erase from the requested set - if (!raw.get().IsObject()) { - CONSOLE(mSession).log("Package %s (%s) is not a JSON object", - request.reference().name().c_str(), - request.reference().version().c_str()); - mState = ERROR; - return; - } - for (auto it = mRequested.begin(); it != mRequested.end();) { - if (it->reference() == request.reference()) - it = mRequested.erase(it); - else - it++; - } - - // Erase from the pending set - for (auto it = mPending.begin(); it != mPending.end();) { - if (it->reference() == request.reference()) - it = mPending.erase(it); - else - it++; - } - - // Insert into the mLoaded list. Note that json has been moved - auto ptr = Package::create(mSession, request.reference().toString(), std::move(raw)); - if (!ptr) { - LOG(LogLevel::kError).session(mSession) << "Package " << request.reference().name() - << " (" << request.reference().version() << ") could not be moved to the loaded list."; - mState = ERROR; - return; - } - - loadPackage(request.reference(), ptr); - updateStatus(); + mPackageResolver->onPackageLoaded(request, std::move(raw)); } void Content::addData(const std::string& name, JsonData&& raw) @@ -318,8 +316,12 @@ std::vector Content::getLoadedPackageNames() const { std::vector result; - for (const auto& m : mLoaded) - result.push_back(m.second->name()); + for (const auto& m : mOrderedDependencies) { + if (m->name() != Path::MAIN) { + result.push_back(m->name()); + } + } + return result; } @@ -343,141 +345,7 @@ Content::getMainProperties(Properties& out) const } void -Content::addImportList(Package& package) -{ - LOG_IF(DEBUG_CONTENT).session(mSession) << "addImportList " << &package; - - const rapidjson::Value& value = package.json(); - - auto it = value.FindMember(DOCUMENT_IMPORT); - if (it != value.MemberEnd()) { - if (!it->value.IsArray()) { - CONSOLE(mSession).log("%s: Document import property should be an array", package.name().c_str()); - mState = ERROR; - return; - } - for (const auto& v : it->value.GetArray()) - addImport(package, v); - } -} - -bool -Content::addImport(Package& package, - const rapidjson::Value& value, - const std::string& name, - const std::string& version, - const std::set& loadAfter) -{ - LOG_IF(DEBUG_CONTENT).session(mSession) << "addImport " << &package; - - if (mState == ERROR) return false; - - if (!value.IsObject()) { - CONSOLE(mSession).log("Invalid import record in document"); - mState = ERROR; - return false; - } - - // Check for conditionality, only if context available. - if (mEvaluationContext) { - auto it_when = value.FindMember("when"); - if (it_when != value.MemberEnd()) { - auto evaluatedWhen = evaluate(*mEvaluationContext, it_when->value.GetString()); - if (!evaluatedWhen.asBoolean()) return false; - } - } - - auto typeIt = value.FindMember(PACKAGE_TYPE); - std::string type = PACKAGE_TYPE_PACKAGE; - if (typeIt != value.MemberEnd() && typeIt->value.IsString()) { - type = typeIt->value.GetString(); - } - - if (type == PACKAGE_TYPE_ONEOF) { - auto sIt = value.FindMember(PACKAGE_ITEMS); - if (sIt != value.MemberEnd() && sIt->value.IsArray()) { - // Expansion. Can use common name/version/loadAfter. - auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mEvaluationContext); - auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mEvaluationContext); - for (const auto& s : sIt->value.GetArray()) { - if (addImport(package, s, - commonNameAndVersion.first.empty() ? name : commonNameAndVersion.first, - commonNameAndVersion.second.empty() ? version : commonNameAndVersion.second, - commonLoadAfter.empty() ? loadAfter : commonLoadAfter)) - return true; - } - } else { - CONSOLE(mSession).log("%s: Missing items field for the oneOf import", package.name().c_str()); - mState = ERROR; - return false; - } - - // If no imports were matched - use otherwise - auto otherwiseIt = value.FindMember(PACKAGE_OTHERWISE); - if (otherwiseIt != value.MemberEnd() && otherwiseIt->value.IsArray()) { - // Expansion. Can use common name/version/loadAfter. - auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mEvaluationContext); - auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mEvaluationContext); - for (const auto& s : otherwiseIt->value.GetArray()) { - if (!addImport(package, s, commonNameAndVersion.first, commonNameAndVersion.second, commonLoadAfter)) { - CONSOLE(mSession).log("%s: Otherwise imports failed", package.name().c_str()); - mState = ERROR; - return false; - } - } - } - - // Nothing was done, which is kinda fine - return true; - } else if (type == PACKAGE_TYPE_ALLOF) { - auto sIt = value.FindMember(PACKAGE_ITEMS); - if (sIt != value.MemberEnd() && sIt->value.IsArray()) { - auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mEvaluationContext); - auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mEvaluationContext); - for (const auto& s : sIt->value.GetArray()) { - addImport(package, s, - commonNameAndVersion.first.empty() ? name : commonNameAndVersion.first, - commonNameAndVersion.second.empty() ? version : commonNameAndVersion.second, - commonLoadAfter.empty() ? loadAfter : commonLoadAfter); - } - } else { - CONSOLE(mSession).log("%s: Missing items field for the allOf import", package.name().c_str()); - mState = ERROR; - return false; - } - - return true; - } - - ImportRequest request = ImportRequest::create(value, mEvaluationContext, name, version, loadAfter); - if (!request.isValid()) { - CONSOLE(mSession).log("Malformed package import record"); - mState = ERROR; - return false; - } - - package.addDependency(request.reference()); - - if (mRequested.find(request) == mRequested.end() && - mPending.find(request) == mPending.end() && - mLoaded.find(request.reference()) == mLoaded.end()) { - - // Reuse if was already resolved - auto stashed = mStashed.find(request.reference()); - if (stashed != mStashed.end()) { - loadPackage(request.reference(), stashed->second); - return true; - } - - // It is a new request - mRequested.insert(std::move(request)); - } - - return true; -} - -void -Content::addExtensions(Package& package) +Content::addExtensions(const Package& package) { const auto features = arrayifyProperty(package.json(), "extension", "extensions"); for (const auto& feature : features) { @@ -525,13 +393,8 @@ Content::addExtensions(Package& package) void Content::updateStatus() { - if (mState == LOADING && mPendingParameters.empty() && mRequested.empty() && mPending.empty()) { - // Content is ready if the dependency list is successfully ordered, otherwise there is an error. - if (orderDependencyList()) { - mState = READY; - } else { - mState = ERROR; - } + if (mPendingParameters.empty() && !isWaiting()) { + mState = READY; } } @@ -730,6 +593,12 @@ Content::getDocumentSettings() const return settings; } +bool +Content::reactiveConditionalInflation() const +{ + return getDocumentSettings()->reactiveConditionalInflation(mConfig); +} + std::set Content::getExtensionRequests() const { @@ -748,8 +617,12 @@ Content::getExtensionRequestsV2() const Object Content::getExtensionSettings(const std::string& uri) { - if (!isReady()) { - CONSOLE(mSession).log("Settings for extension name='%s' cannot be returned. The document is not Ready.", + if (isError()) { + CONSOLE(mSession).log("Settings for extension name='%s' cannot be returned. The document has an error.", + uri.c_str()); + return Object::NULL_OBJECT(); + } else if (isWaiting()) { + CONSOLE(mSession).log("Settings for extension name='%s' cannot be returned. The document is waiting for packages.", uri.c_str()); return Object::NULL_OBJECT(); } @@ -771,122 +644,4 @@ Content::getExtensionSettings(const std::string& uri) return Object::NULL_OBJECT(); } - -/** - * Create a deterministic order for all packages. - */ -bool -Content::orderDependencyList() -{ - std::set inProgress; - bool isOrdered = addToDependencyList(mOrderedDependencies, inProgress, mMainPackage); - if (!isOrdered) - CONSOLE(mSession).log("Failure to order packages"); - mStashed.clear(); - return isOrdered; -} - - -/** - * Traverse the dependencies of a package and create a deterministic order. - */ -bool -Content::addToDependencyList(std::vector& ordered, - std::set& inProgress, - const PackagePtr& package) -{ - LOG_IF(DEBUG_CONTENT).session(mSession) << "addToDependencyList " << package - << " dependency count=" << package->getDependencies().size(); - - inProgress.insert(package); // For dependency loop detection - - auto pds = package->getDependencies(); - auto depQueue = std::queue(); - for (const auto& pd : pds) depQueue.emplace(pd); - - std::set available; - std::set> delayed; - size_t circularCounter = 0; - while (!depQueue.empty()) { - auto ref = depQueue.front(); - depQueue.pop(); - - auto needDeps = false; - - // Check to see if package has load dependency and it was already included - for (const auto& dep : ref.loadAfter()) { - if (!ref.loadAfter().empty() && !available.count(dep)) { - // Check if we have anything else to load - if (depQueue.empty()) { - CONSOLE(mSession).log("Required loadAfter package not available %s for %s", - dep.c_str(), ref.name().c_str()); - return false; - } - - // Check if we have reverse dep - if (delayed.count(std::make_pair(dep, ref.name()))) { - CONSOLE(mSession).log("Circular package loadAfter dependency between %s and %s", - ref.name().c_str(), dep.c_str()); - return false; - } - - delayed.emplace(ref.name(), dep); - depQueue.emplace(ref); - needDeps = true; - } - } - - if (circularCounter > depQueue.size()) { - CONSOLE(mSession).log("Circular package loadAfter dependency chain"); - return false; - } - - circularCounter++; - - if (needDeps) continue; - - // Reset counter - circularCounter = 0; - - LOG_IF(DEBUG_CONTENT).session(mSession) << "checking child " << ref.toString(); - - // Convert the reference into a loaded PackagePtr - const auto& pkg = mLoaded.find(ref); - if (pkg == mLoaded.end()) { - LOG(LogLevel::kError).session(mSession) << "Missing package '" << ref.name() - << "' in the loaded set"; - return false; - } - - const PackagePtr& child = pkg->second; - - // Check if it is already in the dependency list (someone else included it first) - auto it = std::find(ordered.begin(), ordered.end(), child); - if (it != ordered.end()) { - LOG_IF(DEBUG_CONTENT).session(mSession) << "child package " << ref.toString() - << " already in dependency list"; - continue; - } - - // Check for a circular dependency - if (inProgress.count(child)) { - CONSOLE(mSession).log("Circular package dependency '%s'", ref.name().c_str()); - return false; - } - - if (!addToDependencyList(ordered, inProgress, child)) { - LOG_IF(DEBUG_CONTENT).session(mSession) << "returning false with child package " << child->name(); - return false; - } - available.emplace(ref.name()); - } - - LOG_IF(DEBUG_CONTENT).session(mSession) << "Pushing package " << package - << " onto ordered list"; - ordered.push_back(package); - inProgress.erase(package); - return true; -} - - } // namespace apl diff --git a/aplcore/src/content/importrequest.cpp b/aplcore/src/content/importrequest.cpp index 90e70a2..22bc89a 100644 --- a/aplcore/src/content/importrequest.cpp +++ b/aplcore/src/content/importrequest.cpp @@ -24,13 +24,16 @@ static const char *IMPORT_NAME = "name"; static const char *IMPORT_VERSION = "version"; static const char *IMPORT_SOURCE = "source"; static const char *IMPORT_LOAD_AFTER = "loadAfter"; +static const char *IMPORT_ACCEPT = "accept"; ImportRequest ImportRequest::create(const rapidjson::Value& value, const ContextPtr& context, + const SessionPtr& session, const std::string& commonName, const std::string& commonVersion, - const std::set& commonLoadAfter) + const std::set& commonLoadAfter, + const std::string& commonAccept) { if (value.IsObject()) { auto nameAndVersion = extractNameAndVersion(value, context); @@ -55,7 +58,17 @@ ImportRequest::create(const rapidjson::Value& value, loadAfter = loadAfter.empty() ? commonLoadAfter : loadAfter; if (loadAfter.count(nameAndVersion.first)) return {}; - return {name, version, source, loadAfter}; + std::string accept; + auto it = value.FindMember(IMPORT_ACCEPT); + if (it != value.MemberEnd()) { + accept = it->value.GetString(); + if (context) accept = evaluate(*context, accept).asString(); + } + accept = accept.empty() ? commonAccept : accept; + auto semanticVersion = SemanticVersion::create(session, version); + auto acceptPattern = accept.empty() ? nullptr : SemanticPattern::create(session, accept); + + return ImportRequest(name, version, source, loadAfter, semanticVersion, acceptPattern); } return {}; @@ -66,29 +79,17 @@ ImportRequest::ImportRequest() : mValid(false), mUniqueId(ImportRequest::sNextId ImportRequest::ImportRequest(const std::string& name, const std::string& version, const std::string& source, - const std::set& loadAfter) - : mReference(name, version, source, loadAfter), mValid(true), mUniqueId(ImportRequest::sNextId++) + const std::set& loadAfter, + const SemanticVersionPtr& semanticVersion, + const SemanticPatternPtr& acceptPattern) + : mReference(name, version, source, loadAfter, semanticVersion, acceptPattern), mValid(true), mUniqueId(ImportRequest::sNextId++) { } std::pair ImportRequest::extractNameAndVersion(const rapidjson::Value& value, const ContextPtr& context) { - std::string name; - std::string version; - - auto it_name = value.FindMember(IMPORT_NAME); - if (it_name != value.MemberEnd()) { - name = it_name->value.GetString(); - if (context) name = evaluate(*context, name).asString(); - } - - auto it_version = value.FindMember(IMPORT_VERSION); - if (it_version != value.MemberEnd()) { - version = it_version->value.GetString(); - if (context) version = evaluate(*context, version).asString(); - } - return std::make_pair(name, version); + return std::make_pair(extractString(IMPORT_NAME, value, context), extractString(IMPORT_VERSION, value, context)); } std::set @@ -115,6 +116,25 @@ ImportRequest::extractLoadAfter(const rapidjson::Value& value, const ContextPtr& return result; } +std::string +ImportRequest::extractAccept(const rapidjson::Value& value, const ContextPtr& context) +{ + return extractString(IMPORT_ACCEPT, value, context); +} + +std::string +ImportRequest::extractString(const std::string& key, const rapidjson::Value& value, const ContextPtr& context) +{ + std::string result; + auto it_name = value.FindMember(key.c_str()); + if (it_name != value.MemberEnd()) { + result = it_name->value.GetString(); + if (context) result = evaluate(*context, result).asString(); + } + + return result; +} + uint32_t ImportRequest::sNextId = 0; } // namespace apl \ No newline at end of file diff --git a/aplcore/src/content/jsondata.cpp b/aplcore/src/content/jsondata.cpp index 1145631..aa74aec 100644 --- a/aplcore/src/content/jsondata.cpp +++ b/aplcore/src/content/jsondata.cpp @@ -48,9 +48,14 @@ JsonData::moveToObject() mValuePtr = nullptr; return obj; } - else { + else if (mType == kShared) { mType = kNullPtr; - return {std::move(mDocument) }; + Object obj(mSharedJson); + mSharedJson = {}; + return obj; + } + else { + return {}; } } diff --git a/aplcore/src/content/packageresolver.cpp b/aplcore/src/content/packageresolver.cpp new file mode 100644 index 0000000..7d34c70 --- /dev/null +++ b/aplcore/src/content/packageresolver.cpp @@ -0,0 +1,150 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "apl/content/packageresolver.h" + +#include "apl/utils/make_unique.h" + +namespace apl { + +PendingImportPackagePtr +PackageResolver::load(const ContextPtr& evaluationContext, + const SessionPtr& session, + const PackagePtr& root, + const std::vector& stashed, + SuccessCallback&& onSuccess, + FailureCallback&& onFailure, + PackageAddedCallback&& onPackageAdded) +{ + auto pending = std::make_shared(evaluationContext, session, root, stashed); + if (pending->isReady()) { + onSuccess(pending->moveOrderedDependencies()); + return nullptr; + } else if (pending->isError()) { + onFailure(pending->getFailedRequestReference(), pending->getError(), 400); + return nullptr; + } + + mPending = {pending, std::move(onSuccess), std::move(onFailure), std::move(onPackageAdded)}; + loadRequested(*pending); + return pending; +} + +void +PackageResolver::load(const ContextPtr& evaluationContext, + const apl::SessionPtr& session, + const ImportRequest& request, + SuccessCallback&& onSuccess, + FailureCallback&& onFailure) +{ + auto pending = std::make_shared(evaluationContext, session, request); + mPending = {pending, std::move(onSuccess), std::move(onFailure), [](const Package& package){}}; + loadRequested(*pending); +} + +void +PackageResolver::onPackageLoaded(const ImportRequest& request, JsonData&& jsonData) +{ + auto ptr = createPackage(request.reference(), mSession, std::move(jsonData)); + if (!ptr) { + onPackageFailure(request, "Package unable to be parsed.", 400); + return; + } + + addPackage(request, ptr); +} + +void +PackageResolver::addPackage(const ImportRequest& request, const PackagePtr& package) +{ + if (mPending.pendingImport == nullptr) return; + if (!mPending.pendingImport->isPackagePending(request)) return; + + auto pendingImport = mPending.pendingImport; + pendingImport->addPackage(request, package); + mPending.onPackageAddedListener(*package); + + if (pendingImport->isReady()) { + mPending.onSuccess(pendingImport->moveOrderedDependencies()); + mPending = {}; + } else if (pendingImport->isError()) { + mPending.onFailure(pendingImport->getFailedRequestReference(), pendingImport->getError(), 400); + mPending = {}; + } else { + loadRequested(*pendingImport); + } +} + +void +PackageResolver::onPackageFailure(const ImportRequest& request, const std::string& errorMessage, int errorCode) +{ + if (mPending.pendingImport == nullptr) return; + if (!mPending.pendingImport->isPackagePending(request)) return; + + mPending.onFailure(request.reference(), errorMessage, errorCode); + mPending = {}; +} + +PackagePtr +PackageResolver::createPackage(const ImportRef& ref, const SessionPtr& session, JsonData&& jsonData) +{ + // If the package data is invalid, set the error state + if (!jsonData) { + CONSOLE(session).log("Package %s (%s) parse error offset=%u: %s", + ref.name().c_str(), + ref.version().c_str(), + jsonData.offset(), jsonData.error()); + return nullptr; + } + + auto ptr = Package::create(session, ref.toString(), std::move(jsonData)); + if (!ptr) { + LOG(LogLevel::kError).session(session) + << "Package " << ref.name() << " (" << ref.version() + << ") is invalid."; + return nullptr; + } + + return ptr; +} + +void +PackageResolver::loadRequested(PendingImportPackage& pending) +{ + auto weakSelf = std::weak_ptr(shared_from_this()); + for (const auto& request : pending.getRequestedPackages()) { + if (auto stashed = pending.getPreLoadedPackage(request.reference().toString())) { + addPackage(request, stashed); + continue ; + } + + auto packageRequest = std::make_shared( + request, + [weakSelf](const ImportRequest& request, const SharedJsonData& jsonData) { + auto self = weakSelf.lock(); + + self->onPackageLoaded(request, jsonData); + }, + [weakSelf](const ImportRequest& request, const std::string& errorMessage, int code) { + auto self = weakSelf.lock(); + + self->onPackageFailure(request, errorMessage, code); + }); + + mPackageManager->loadPackage(packageRequest); + } +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/content/pendingimportpackage.cpp b/aplcore/src/content/pendingimportpackage.cpp new file mode 100644 index 0000000..aa1b9f7 --- /dev/null +++ b/aplcore/src/content/pendingimportpackage.cpp @@ -0,0 +1,358 @@ +/** + * 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/content/pendingimportpackage.h" + +#include + +#include "apl/engine/evaluate.h" + +namespace apl { + +const bool DEBUG_IMPORT_PACKAGE = false; +const char* DOCUMENT_IMPORT = "import"; +const char* PACKAGE_TYPE = "type"; +const char* PACKAGE_TYPE_PACKAGE = "package"; +const char* PACKAGE_TYPE_ONEOF = "oneOf"; +const char* PACKAGE_TYPE_ALLOF = "allOf"; +const char* PACKAGE_OTHERWISE = "otherwise"; +const char* PACKAGE_ITEMS = "items"; +const char* PACKAGE_WHEN = "when"; + +std::set +PendingImportPackage::getRequestedPackages() +{ + mPending.insert(mRequested.begin(), mRequested.end()); + auto result = mRequested; + mRequested.clear(); + return result; +} + +void +PendingImportPackage::addPackage(const ImportRequest& request, const PackagePtr& package) +{ + if (mRoot == nullptr) { + mRoot = package; + } + // Erase from the pending set + mPending.erase(request); + mLoaded.emplace(request.reference(), package); + addImportList(*package); + if (isError()) { + mFailedRequestReference = request.reference(); + } + updateStatus(); +} + +void +PendingImportPackage::addImportList(Package& package) +{ + const rapidjson::Value& value = package.json(); + + auto it = value.FindMember(DOCUMENT_IMPORT); + if (it != value.MemberEnd()) { + if (!it->value.IsArray()) { + setError("Document import property should be an array"); + return; + } + for (const auto& v : it->value.GetArray()) + addImport(package, v); + } +} + +bool +PendingImportPackage::addImport(Package& package, + const rapidjson::Value& value, + const std::string& name, + const std::string& version, + const std::set& loadAfter, + const std::string& accept) +{ + LOG_IF(DEBUG_IMPORT_PACKAGE).session(mSession) << "addImport " << &package; + + if (mState == State::ERROR) return false; + + if (!value.IsObject()) { + setError("Invalid import record in document"); + return false; + } + + // Check for conditionality, only if context available. + if (mContext) { + auto it_when = value.FindMember(PACKAGE_WHEN); + if (it_when != value.MemberEnd()) { + auto evaluatedWhen = evaluate(*mContext, it_when->value.GetString()); + if (!evaluatedWhen.asBoolean()) return false; + } + } + + auto typeIt = value.FindMember(PACKAGE_TYPE); + std::string type = PACKAGE_TYPE_PACKAGE; + if (typeIt != value.MemberEnd() && typeIt->value.IsString()) { + type = typeIt->value.GetString(); + } + + if (type == PACKAGE_TYPE_ONEOF) { + auto sIt = value.FindMember(PACKAGE_ITEMS); + if (sIt != value.MemberEnd() && sIt->value.IsArray()) { + // Expansion. Can use common name/version/loadAfter/accept. + auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mContext); + auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mContext); + auto commonAccept = ImportRequest::extractAccept(value, mContext); + for (const auto& s : sIt->value.GetArray()) { + if (addImport(package, s, + commonNameAndVersion.first.empty() ? name : commonNameAndVersion.first, + commonNameAndVersion.second.empty() ? version : commonNameAndVersion.second, + commonLoadAfter.empty() ? loadAfter : commonLoadAfter, + commonAccept.empty() ? accept : commonAccept)) + return true; + } + } else { + setError("Missing items field for the oneOf import"); + return false; + } + + // If no imports were matched - use otherwise + auto otherwiseIt = value.FindMember(PACKAGE_OTHERWISE); + if (otherwiseIt != value.MemberEnd() && otherwiseIt->value.IsArray()) { + // Expansion. Can use common name/version/loadAfter/accept. + auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mContext); + auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mContext); + auto commonAccept = ImportRequest::extractAccept(value, mContext); + for (const auto& s : otherwiseIt->value.GetArray()) { + if (!addImport(package, s, commonNameAndVersion.first, commonNameAndVersion.second, commonLoadAfter, commonAccept)) { + setError("Otherwise imports failed"); + return false; + } + } + } + + // Nothing was done, which is kinda fine + return true; + } else if (type == PACKAGE_TYPE_ALLOF) { + auto sIt = value.FindMember(PACKAGE_ITEMS); + if (sIt != value.MemberEnd() && sIt->value.IsArray()) { + auto commonNameAndVersion = ImportRequest::extractNameAndVersion(value, mContext); + auto commonLoadAfter = ImportRequest::extractLoadAfter(value, mContext); + auto commonAccept = ImportRequest::extractAccept(value, mContext); + for (const auto& s : sIt->value.GetArray()) { + addImport(package, s, + commonNameAndVersion.first.empty() ? name : commonNameAndVersion.first, + commonNameAndVersion.second.empty() ? version : commonNameAndVersion.second, + commonLoadAfter.empty() ? loadAfter : commonLoadAfter, + commonAccept.empty() ? accept : commonAccept); + } + } else { + setError("Missing items field for the allOf import"); + return false; + } + + return true; + } + + ImportRequest request = ImportRequest::create(value, mContext, mSession, name, version, loadAfter, accept); + if (!request.isValid()) { + setError("Malformed package import record"); + return false; + } + + // Create a suitable request. + request = createOrGetSuitableRequest(request); + auto dependenciesIt = mDependencies.find(package.name()); + if (dependenciesIt == mDependencies.end()) { + dependenciesIt = mDependencies.emplace(package.name(), std::vector()).first; + } + dependenciesIt->second.push_back(request.reference()); + + if (mRequested.find(request) == mRequested.end() && + mPending.find(request) == mPending.end() && + mLoaded.find(request.reference()) == mLoaded.end()) { + + mRequested.insert(std::move(request)); + } + + return true; +} + +bool +PendingImportPackage::addToDependencyList(std::vector& ordered, + std::set& inProgress, + const PackagePtr& package) +{ + std::vector pds; + auto dependenciesIt = mDependencies.find(package->name()); + if (dependenciesIt != mDependencies.end()) { + pds = dependenciesIt->second; + } + + LOG_IF(DEBUG_IMPORT_PACKAGE).session(mSession) << "addToDependencyList " << package + << " dependency count=" << pds.size(); + inProgress.insert(package); // For dependency loop detection + + auto depQueue = std::queue(); + for (const auto& pd : pds) depQueue.emplace(pd); + + std::set available; + std::set> delayed; + size_t circularCounter = 0; + while (!depQueue.empty()) { + auto ref = depQueue.front(); + depQueue.pop(); + + auto needDeps = false; + + // Check to see if package has load dependency and it was already included + for (const auto& dep : ref.loadAfter()) { + if (!ref.loadAfter().empty() && !available.count(dep)) { + // Check if we have anything else to load + if (depQueue.empty()) { + CONSOLE(mSession).log("Required loadAfter package not available %s for %s", + dep.c_str(), ref.name().c_str()); + mFailedRequestReference = ref; + return false; + } + + // Check if we have reverse dep + if (delayed.count(std::make_pair(dep, ref.name()))) { + CONSOLE(mSession).log("Circular package loadAfter dependency between %s and %s", + ref.name().c_str(), dep.c_str()); + mFailedRequestReference = ref; + return false; + } + + delayed.emplace(ref.name(), dep); + depQueue.emplace(ref); + needDeps = true; + } + } + + if (circularCounter > depQueue.size()) { + CONSOLE(mSession).log("Circular package loadAfter dependency chain"); + mFailedRequestReference = ref; + return false; + } + + circularCounter++; + + if (needDeps) continue; + + // Reset counter + circularCounter = 0; + + LOG_IF(DEBUG_IMPORT_PACKAGE).session(mSession) << "checking child " << ref.toString(); + + // Convert the reference into a loaded PackagePtr + const auto& pkg = mLoaded.find(ref); + if (pkg == mLoaded.end()) { + assert(false); + LOG(LogLevel::kError).session(mSession) << "Missing package '" << ref.name() + << "' in the loaded set"; + return false; + } + + const PackagePtr& child = pkg->second; + + // Check if it is already in the dependency list (someone else included it first) + auto it = std::find(ordered.begin(), ordered.end(), child); + if (it != ordered.end()) { + LOG_IF(DEBUG_IMPORT_PACKAGE).session(mSession) << "child package " << ref.toString() + << " already in dependency list"; + continue; + } + + // Check for a circular dependency + if (inProgress.count(child)) { + CONSOLE(mSession).log("Circular package dependency '%s'", ref.name().c_str()); + mFailedRequestReference = ref; + return false; + } + + if (!addToDependencyList(ordered, inProgress, child)) { + LOG_IF(DEBUG_IMPORT_PACKAGE).session(mSession) << "returning false with child package " << child->name(); + return false; + } + available.emplace(ref.name()); + } + + LOG_IF(DEBUG_IMPORT_PACKAGE).session(mSession) << "Pushing package " << package + << " onto ordered list"; + ordered.push_back(package); + inProgress.erase(package); + return true; +} + +void +PendingImportPackage::updateStatus() +{ + if (mState == State::LOADING && mRequested.empty() && mPending.empty()) { + // Content is ready if the dependency list is successfully ordered, otherwise there is an error. + if (orderDependencyList()) { + mState = State::READY; + } else { + setError("Failure to order packages"); + } + } +} + +bool +PendingImportPackage::orderDependencyList() +{ + std::set inProgress; + bool isOrdered = addToDependencyList(mOrderedDependencies, inProgress, mRoot); + return isOrdered; +} + +void PendingImportPackage::setError(const std::string& error) +{ + CONSOLE(mSession) << error; + mError = error; + mState = State::ERROR; + mPending.clear(); + mRequested.clear(); + mLoaded.clear(); +} + +ImportRequest +PendingImportPackage::createOrGetSuitableRequest(const ImportRequest& request) +{ + auto existingRequests = mNameImportRequestMap.find(request.reference().name()); + if (existingRequests == mNameImportRequestMap.end()) { + existingRequests = mNameImportRequestMap.emplace(request.reference().name(), std::vector()).first; + } + // Look for a request that has the same name and whose version matches the accept pattern. + for (const auto& requestsWithSameName : existingRequests->second) { + if (requestsWithSameName.isAcceptableReplacementFor(request)) { + return requestsWithSameName; + } + } + + // No existing requests satisfy it, so stash the request and return it + existingRequests->second.push_back(request); + return request; +} + +PackagePtr +PendingImportPackage::getPreLoadedPackage(const std::string& packageName) const +{ + for (const auto& package : mPreLoaded) { + if (packageName == package->name()) { + return package; + } + } + + return nullptr; +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/content/rootconfig.cpp b/aplcore/src/content/rootconfig.cpp index acff171..3273d9b 100644 --- a/aplcore/src/content/rootconfig.cpp +++ b/aplcore/src/content/rootconfig.cpp @@ -327,6 +327,7 @@ RootConfig::copy() const copy->mediaPlayerFactory(getMediaPlayerFactory()); copy->measure(getMeasure()); copy->experimentalFeatures(getExperimentalFeatures()); + copy->packageManager(getPackageManager()); for (auto key : sCopyableConfigProperties) { copy->set(key, getProperty(key)); diff --git a/aplcore/src/content/sharedjsondata.cpp b/aplcore/src/content/sharedjsondata.cpp new file mode 100644 index 0000000..7e7390d --- /dev/null +++ b/aplcore/src/content/sharedjsondata.cpp @@ -0,0 +1,42 @@ +/** + * 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/content/sharedjsondata.h" +#include "rapidjson/writer.h" +#include "rapidjson/stringbuffer.h" + +#include "apl/primitives/object.h" + +namespace apl { + +std::string +SharedJsonData::toDebugString() const +{ + return "SharedJsonData<" + toString() + ">"; +} + +std::string +SharedJsonData::toString() const +{ + if (!*this) + return "INVALID"; + + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writer writer(buffer); + get().Accept(writer); + return buffer.GetString(); +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/document/coredocumentcontext.cpp b/aplcore/src/document/coredocumentcontext.cpp index 6108f5a..3969944 100644 --- a/aplcore/src/document/coredocumentcontext.cpp +++ b/aplcore/src/document/coredocumentcontext.cpp @@ -568,8 +568,6 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) return false; } - bool trackProvenance = mCore->rootConfig().getProperty(RootProperty::kTrackProvenance).getBoolean(); - // Read settings { APL_TRACE_BEGIN("DocumentContext:readSettings"); @@ -577,9 +575,69 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) APL_TRACE_END("DocumentContext:readSettings"); } + // Process the contents the packages into context + processPackagesIntoContext(ordered); + + // Identify all registered event handlers in all ordered documents + APL_TRACE_BEGIN("DocumentContext:processExtensionHandlers"); + auto& em = mCore->extensionManager(); + for (const auto& handler : em.getEventHandlerDefinitions()) { + for (const auto& child : ordered) { + const auto& json = child->json(); + auto h = json.FindMember(handler.first.c_str()); + if (h != json.MemberEnd()) { + auto oldHandler = em.findHandler(handler.second); + if (!oldHandler.isNull()) + CONSOLE(mContext) << "Overwriting existing command handler " << handler.first; + em.addEventHandler(handler.second, asCommand(*mContext, evaluate(*mContext, h->value))); + } + } + } + APL_TRACE_END("DocumentContext:processExtensionHandlers"); + + // Inflate the top component + Properties properties; + + APL_TRACE_BEGIN("DocumentContext:retrieveProperties"); + mContent->getMainProperties(properties); + APL_TRACE_END("DocumentContext:retrieveProperties"); + + mCore->mTop = Builder(top).inflate(mContext, properties, mContent->getMainTemplate()); + + if (!mCore->mTop) + return false; + + mCore->mTop->registerForVisibilityTrackingIfRequired(); + mCore->mTop->markGlobalToLocalTransformStale(); + +#ifdef ALEXAEXTENSIONS + // Bind to the extension mediator + // TODO ExtensionMediator is an experimental class facilitating message passing to and from extensions. + // TODO The mediator class should be replaced by direct messaging between extensions and ExtensionManager + auto extensionMediator = mCore->rootConfig().getExtensionMediator(); + if (extensionMediator) { + extensionMediator->bindContext(shared_from_this()); + } +#endif + + return true; +} + +void +CoreDocumentContext::processPackagesIntoContext(const std::vector packages) { + // Filter out any packages we've already processed as more may be added dynamically. + std::vector toProcess; + for (const auto& package : packages) { + if (mCore->mProcessedPackages.emplace(package->name(), package).second) { + toProcess.push_back(package); + } + } + + bool trackProvenance = mCore->rootConfig().getProperty(RootProperty::kTrackProvenance).getBoolean(); + // Resource processing: APL_TRACE_BEGIN("DocumentContext:processResources"); - for (const auto& child : ordered) { + for (const auto& child : toProcess) { const auto& json = child->json(); const auto path = Path(trackProvenance ? child->name() : std::string()); addNamedResourcesBlock(*mContext, json, path, "resources"); @@ -588,7 +646,7 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) // Style processing APL_TRACE_BEGIN("DocumentContext:processStyles"); - for (const auto& child : ordered) { + for (const auto& child : toProcess) { const auto& json = child->json(); const auto path = Path(trackProvenance ? child->name() : std::string()); @@ -600,7 +658,7 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) // Layout processing APL_TRACE_BEGIN("DocumentContext:processLayouts"); - for (const auto& child : ordered) { + for (const auto& child : toProcess) { const auto& json = child->json(); const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("layouts"); @@ -616,7 +674,7 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) // Command processing APL_TRACE_BEGIN("DocumentContext:processCommands"); - for (const auto& child : ordered) { + for (const auto& child : toProcess) { const auto& json = child->json(); const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("commands"); @@ -632,7 +690,7 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) // Graphics processing APL_TRACE_BEGIN("DocumentContext:processGraphics"); - for (const auto& child : ordered) { + for (const auto& child : toProcess) { const auto& json = child->json(); const auto path = Path(trackProvenance ? child->name() : std::string()).addObject("graphics"); @@ -645,50 +703,6 @@ CoreDocumentContext::setup(const CoreComponentPtr& top) } } APL_TRACE_END("DocumentContext:processGraphics"); - - // Identify all registered event handlers in all ordered documents - APL_TRACE_BEGIN("DocumentContext:processExtensionHandlers"); - auto& em = mCore->extensionManager(); - for (const auto& handler : em.getEventHandlerDefinitions()) { - for (const auto& child : ordered) { - const auto& json = child->json(); - auto h = json.FindMember(handler.first.c_str()); - if (h != json.MemberEnd()) { - auto oldHandler = em.findHandler(handler.second); - if (!oldHandler.isNull()) - CONSOLE(mContext) << "Overwriting existing command handler " << handler.first; - em.addEventHandler(handler.second, asCommand(*mContext, evaluate(*mContext, h->value))); - } - } - } - APL_TRACE_END("DocumentContext:processExtensionHandlers"); - - // Inflate the top component - Properties properties; - - APL_TRACE_BEGIN("DocumentContext:retrieveProperties"); - mContent->getMainProperties(properties); - APL_TRACE_END("DocumentContext:retrieveProperties"); - - mCore->mTop = Builder(top).inflate(mContext, properties, mContent->getMainTemplate()); - - if (!mCore->mTop) - return false; - - mCore->mTop->registerForVisibilityTrackingIfRequired(); - mCore->mTop->markGlobalToLocalTransformStale(); - -#ifdef ALEXAEXTENSIONS - // Bind to the extension mediator - // TODO ExtensionMediator is an experimental class facilitating message passing to and from extensions. - // TODO The mediator class should be replaced by direct messaging between extensions and ExtensionManager - auto extensionMediator = mCore->rootConfig().getExtensionMediator(); - if (extensionMediator) { - extensionMediator->bindContext(shared_from_this()); - } -#endif - - return true; } bool diff --git a/aplcore/src/document/documentcontextdata.cpp b/aplcore/src/document/documentcontextdata.cpp index 4780fc8..12b3398 100644 --- a/aplcore/src/document/documentcontextdata.cpp +++ b/aplcore/src/document/documentcontextdata.cpp @@ -26,10 +26,6 @@ #include "apl/touch/pointermanager.h" #include "apl/utils/make_unique.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/textpropertiescache.h" -#endif // SCENEGRAPH - namespace apl { /** @@ -128,9 +124,7 @@ LruCache& DocumentContextData::cachedMeasures() { re LruCache& DocumentContextData::cachedBaselines() { return mSharedData->cachedBaselines(); } void DocumentContextData::setDirty(const ComponentPtr& component) { mSharedData->dirtyComponents().emplace(shared_from_this(), component); } void DocumentContextData::clearDirty(const ComponentPtr& component) { mSharedData->dirtyComponents().eraseValue(component); } - -#ifdef SCENEGRAPH +sg::TextLayoutCache& DocumentContextData::textLayoutCache() { return mSharedData->textLayoutCache(); } sg::TextPropertiesCache& DocumentContextData::textPropertiesCache() { return mSharedData->textPropertiesCache(); } -#endif // SCENEGRAPH } // namespace apl diff --git a/aplcore/src/engine/bindingchange.cpp b/aplcore/src/engine/bindingchange.cpp index de58ed3..5b2c6c6 100644 --- a/aplcore/src/engine/bindingchange.cpp +++ b/aplcore/src/engine/bindingchange.cpp @@ -25,6 +25,26 @@ namespace apl { +bool +isValidBinding(const ContextPtr& context, const Object& binding, const std::string& name) +{ + if (!isValidIdentifier(name)) { + CONSOLE(context) << "Invalid binding name '" << name << "'"; + return false; + } + + if (!binding.has("value")) { + CONSOLE(context) << "Binding '" << name << "' did not specify a value"; + return false; + } + + if (context->hasLocal(name)) { + CONSOLE(context) << "Attempted to bind to pre-existing property '" << name << "'"; + return false; + } + return true; +} + std::vector attachBindings(const ContextPtr& context, const Object& item, std::function makeBCP) { @@ -34,26 +54,15 @@ attachBindings(const ContextPtr& context, const Object& item, std::functionhasLocal(name)) { - CONSOLE(context) << "Attempted to bind to pre-existing property '" << name << "'"; - continue; - } + if (!isValidBinding(context, binding, 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 bindingType = optionalMappedProperty(*context, binding, "type", + kBindingTypeAny, sBindingMap); + auto bindingFunc = sBindingFunctions.at(bindingType); + auto value = bindingFunc(*context, result.value); BindingChangePtr ptr; if (makeBCP) { @@ -69,7 +78,7 @@ attachBindings(const ContextPtr& context, const Object& item, std::functionputUserWriteable(name, bindingFunc(*context, result.value), ptr); + context->putUserWriteable(name, value, ptr); if (!result.symbols.empty()) ContextDependant::create(context, name, std::move(result.expression), context, diff --git a/aplcore/src/engine/builder.cpp b/aplcore/src/engine/builder.cpp index 5b8b251..1181918 100644 --- a/aplcore/src/engine/builder.cpp +++ b/aplcore/src/engine/builder.cpp @@ -30,13 +30,16 @@ #include "apl/component/touchwrappercomponent.h" #include "apl/component/vectorgraphiccomponent.h" #include "apl/component/videocomponent.h" +#include "apl/content/content.h" #include "apl/content/rootconfig.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/arrayify.h" #include "apl/engine/binding.h" #include "apl/engine/context.h" #include "apl/engine/evaluate.h" #include "apl/engine/parameterarray.h" #include "apl/engine/properties.h" +#include "apl/engine/rebuilddependant.h" #include "apl/engine/typeddependant.h" #include "apl/extension/extensioncomponent.h" #include "apl/extension/extensionmanager.h" @@ -45,6 +48,7 @@ #include "apl/livedata/livearrayobject.h" #include "apl/primitives/object.h" #include "apl/time/sequencer.h" +#include "apl/utils/constants.h" #include "apl/utils/identifier.h" #include "apl/utils/log.h" #include "apl/utils/path.h" @@ -55,6 +59,9 @@ namespace apl { const bool DEBUG_BUILDER = false; +const char* WHEN_FIELD = "when"; +const char* ITEM_INDEX = "__itemIndex"; + static const std::map sComponentMap = { {"Container", ContainerComponent::create}, {"Text", TextComponent::create}, @@ -100,6 +107,55 @@ class BindingChangeImpl : public BindingChange { std::weak_ptr mComponent; }; +/** + * Register child rebuild dependency against provided context if: + * * Setting enabled to do so. + * * When conditional present. + * * Symbols extracted are not in the ignore list. + */ +void +Builder::registerRebuildDependencyIfRequired( + const CoreComponentPtr& parent, + const ContextPtr& childContext, + const ObjectArray& items, + bool hasChild, + const std::set& symbolIgnoreList) +{ + if (!CoreDocumentContext::cast(childContext->documentContext()) + ->content() + ->reactiveConditionalInflation()) + return; + + if (!parent || !childContext) return; + + auto symbols = BoundSymbolSet(); + for (const auto& item : items) { + if (!item.isMap()) + continue; + + if (item.has(WHEN_FIELD)) { + auto value = item.opt(WHEN_FIELD, Object::NULL_OBJECT()); + if (value.isString()) { + auto result = parseAndEvaluate(*childContext, value.getString(), false); + if (!symbolIgnoreList.empty()) { + for (auto & symbol : result.symbols) + if (!symbolIgnoreList.count(symbol.getName())) + symbols.emplace(symbol); + } else { + symbols.merge(result.symbols); + } + } + } + } + + if (!symbols.empty()) { + RebuildDependant::create(parent, childContext, std::move(symbols)); + // We only need to keep this one if no existing child will keep a context. Avoids having + // un-required memory growth. + if (!hasChild) parent->stashRebuildContext(childContext); + } +} + void Builder::populateSingleChildLayout(const ContextPtr& context, const Object& item, @@ -110,16 +166,55 @@ Builder::populateSingleChildLayout(const ContextPtr& context, { LOG_IF(DEBUG_BUILDER).session(context) << "call"; APL_TRACE_BLOCK("Builder:populateSingleChildLayout"); + + auto items = arrayifyProperty(*context, item, "item", "items"); auto child = expandSingleComponentFromArray(context, - arrayifyProperty(*context, item, "item", "items"), + items, Properties(), layout, path.addProperty(item, "item", "items"), fullBuild, useDirtyFlag); + + registerRebuildDependencyIfRequired(layout, context, items, child != nullptr); + if (CoreDocumentContext::cast(context->documentContext()) + ->content() + ->reactiveConditionalInflation()) + context->putConstant(REBUILD_ITEMS, std::make_shared(items)); + layout->appendChild(child, useDirtyFlag); } +ContextPtr +Builder::createFirstItemContext(const ContextPtr& parent) +{ + auto childContext = Context::createFromParent(parent); + childContext->putConstant(REBUILD_IS_FIRST_ITEM, true); + return childContext; +} + +ContextPtr +Builder::createLastItemContext(const ContextPtr& parent) +{ + auto childContext = Context::createFromParent(parent); + childContext->putConstant(REBUILD_IS_LAST_ITEM, true); + return childContext; +} + +ContextPtr +Builder::createIndexItemContext(const ContextPtr& parent, int sourceIndex, int itemIndex, size_t numberOfItems, bool numbered, int ordinal) +{ + auto childContext = Context::createFromParent(parent); + childContext->putConstant(COMPONENT_CONTEXT_SOURCE, COMPONENT_INDEX); + childContext->putConstant(REBUILD_SOURCE_INDEX, sourceIndex); + childContext->putConstant(COMPONENT_INDEX, itemIndex); + childContext->putConstant(COMPONENT_LENGTH, numberOfItems); + if (numbered) + childContext->putConstant(COMPONENT_ORDINAL, ordinal); + + return childContext; +} + void Builder::populateLayoutComponent(const ContextPtr& context, const Object& item, @@ -131,101 +226,114 @@ Builder::populateLayoutComponent(const ContextPtr& context, LOG_IF(DEBUG_BUILDER).session(context) << path; APL_TRACE_BLOCK("Builder:populateLayoutComponent"); - auto child = expandSingleComponentFromArray(context, - arrayifyProperty(*context, item, "firstItem"), - Properties(), - layout, - path.addProperty(item, "firstItem"), - fullBuild, - useDirtyFlag); bool hasFirstItem = false; - if (child && child->isValid()) { - hasFirstItem = true; - layout->appendChild(child, useDirtyFlag); + auto firstItems = arrayifyProperty(*context, item, "firstItem"); + if (!firstItems.empty()) { + auto childContext = createFirstItemContext(context); + auto child = expandSingleComponentFromArray(childContext, firstItems, Properties(), layout, + path.addProperty(item, "firstItem"), fullBuild, + useDirtyFlag); + + registerRebuildDependencyIfRequired(layout, childContext, firstItems, child != nullptr); + if (CoreDocumentContext::cast(context->documentContext()) + ->content() + ->reactiveConditionalInflation()) + layout->getContext()->putConstant(REBUILD_FIRST_ITEMS, std::make_shared(firstItems)); + + if (child && child->isValid()) { + hasFirstItem = true; + layout->appendChild(child, useDirtyFlag); + } } bool numbered = layout->getCalculated(kPropertyNumbered).asBoolean(); int ordinal = 1; - int index = 0; std::shared_ptr layoutBuilder = nullptr; // Reserve space for now. In the future, move all logic in const auto items = arrayifyProperty(*context, item, "item", "items"); if (!items.empty()) { auto childPath = path.addProperty(item, "item", "items"); - auto data = arrayifyPropertyAsObject(*context, item, "data"); - + auto data = arrayifyPropertyAsObject(*context, item, COMPONENT_DATA); auto liveData = data.getLiveDataObject(); - if (liveData && liveData->asArray()) { - layoutBuilder = LayoutRebuilder::create(context, layout, mOld, liveData->asArray(), items, childPath, numbered); - layoutBuilder->build(useDirtyFlag); - } - else { - auto dataItems = evaluateNested(*context, data); - if (!dataItems.empty()) { - LOG_IF(DEBUG_BUILDER).session(context) << "data size=" << dataItems.size(); - - // Transform data into LiveData and use rebuilder to have more control over its content. - auto rawArray = ObjectArray(); - for (const auto& dataItem : dataItems.getArray()) { - rawArray.emplace_back(dataItem); - } - liveData = LiveDataObject::create( - LiveArray::create(std::move(rawArray)), - context, - "__data" + layout->getUniqueId()); - layoutBuilder = LayoutRebuilder::create(context, layout, mOld, liveData->asArray(), items, childPath, numbered); - layoutBuilder->build(useDirtyFlag); + if (!data.empty() || liveData) { + if (!liveData || !liveData->asArray()) { + auto dataItems = evaluateNested(*context, data); + if (!dataItems.empty()) { + LOG_IF(DEBUG_BUILDER).session(context) << "data size=" << dataItems.size(); + + // Transform data into LiveData and use rebuilder to have more control over its content. + auto rawArray = ObjectArray(); + for (const auto& dataItem : dataItems.getArray()) { + rawArray.emplace_back(dataItem); + } + liveData = LiveDataObject::create(LiveArray::create(std::move(rawArray)), + context, "__data" + layout->getUniqueId()); + } } - else { - LOG_IF(DEBUG_BUILDER).session(context) << "items size=" << items.size(); - auto length = items.size(); - for (int i = 0; i < length; i++) { - const auto& element = items.at(i); - auto childContext = Context::createFromParent(context); - childContext->putConstant("__source", "index"); - childContext->putConstant("index", index); - childContext->putConstant("length", length); - if (numbered) - childContext->putConstant("ordinal", ordinal); - - // TODO: Numbered, spacing, ordinal changes - child = expandSingleComponentFromArray(childContext, - arrayify(*context, element), - Properties(), - layout, - childPath.addIndex(i), - fullBuild, - useDirtyFlag); - // TODO: Full or not full here? - if (child && child->isValid()) { - layout->appendChild(child, useDirtyFlag); - index++; - - if (numbered) { - int numbering = child->getCalculated(kPropertyNumbering).getInteger(); - if (numbering == kNumberingNormal) ordinal++; - else if (numbering == kNumberingReset) ordinal = 1; - } + + layoutBuilder = LayoutRebuilder::create(context, layout, mOld, liveData->asArray(), items, childPath, numbered); + layoutBuilder->build(useDirtyFlag); + } else { + LOG_IF(DEBUG_BUILDER).session(context) << "items size=" << items.size(); + + auto length = items.size(); + int childIndex = 0; + for (int i = 0; i < length; i++) { + const auto& element = items.at(i); + auto childContext = createIndexItemContext(context, i, childIndex, length, numbered, ordinal); + + // TODO: Numbered, spacing, ordinal changes + auto arrayifiedElement = arrayify(*context, element); + auto child = expandSingleComponentFromArray(childContext, + arrayifiedElement, + Properties(), + layout, + childPath.addIndex(i), + fullBuild, + useDirtyFlag); + + registerRebuildDependencyIfRequired(layout, childContext, arrayifiedElement, child != nullptr); + + // TODO: Full or not full here? + if (child && child->isValid()) { + layout->appendChild(child, useDirtyFlag); + childIndex++; + + if (numbered) { + int numbering = child->getCalculated(kPropertyNumbering).getInteger(); + if (numbering == kNumberingNormal) ordinal++; + else if (numbering == kNumberingReset) ordinal = 1; } } } + + if (CoreDocumentContext::cast(context->documentContext()) + ->content() + ->reactiveConditionalInflation()) + layout->getContext()->putConstant(REBUILD_ITEMS, std::make_shared(items)); } } - child = expandSingleComponentFromArray(context, - arrayifyProperty(*context, item, "lastItem"), - Properties(), - layout, - path.addProperty(item, "lastItem"), - fullBuild, - useDirtyFlag); - bool hasLastItem = false; - if (child && child->isValid()) { - hasLastItem = true; - layout->appendChild(child, useDirtyFlag); + auto lastItems = arrayifyProperty(*context, item, "lastItem"); + if (!lastItems.empty()) { + auto childContext = createLastItemContext(context); + auto child = expandSingleComponentFromArray(childContext, lastItems, Properties(), layout, + path.addProperty(item, "lastItem"), fullBuild, + useDirtyFlag); + + registerRebuildDependencyIfRequired(layout, childContext, lastItems, child != nullptr); + if (CoreDocumentContext::cast(context->documentContext()) + ->content() + ->reactiveConditionalInflation()) + layout->getContext()->putConstant(REBUILD_LAST_ITEMS, std::make_shared(lastItems)); + + if (child && child->isValid()) { + hasLastItem = true; + layout->appendChild(child, useDirtyFlag); + } } // Chance to get final child dependent set-up before actual layout happened. @@ -236,7 +344,8 @@ Builder::populateLayoutComponent(const ContextPtr& context, } MakeComponentFunc -Builder::findComponentBuilderFunc(const ContextPtr& context, const std::string &type) { +Builder::findComponentBuilderFunc(const ContextPtr& context, const std::string &type) const +{ auto method = sComponentMap.find(type); if (method != sComponentMap.end()) { return method->second; @@ -282,8 +391,8 @@ Builder::expandSingleComponent(const ContextPtr& context, // Create a new context and fill out the binding ContextPtr expanded = Context::createFromParent(context); - expanded->putConstant("__source", "component"); - expanded->putConstant("__name", type); + expanded->putConstant(COMPONENT_CONTEXT_SOURCE, "component"); + expanded->putConstant(COMPONENT_CONTEXT_NAME, type); auto bindingChangeList = attachBindings(expanded, item, BindingChangeImpl::create); @@ -346,6 +455,79 @@ Builder::expandSingleComponent(const ContextPtr& context, return nullptr; } +Path +Builder::simulateLocalPath(const Path& parentPath, const ContextPtr& context, const Object& parentItem, const Properties& properties) const +{ + APL_TRACE_BLOCK("Builder:simulateLocalPath"); + auto simulatedPath = Path(parentPath); + + auto innerItem = parentItem; + auto innerContext = Context::createFromParent(context); + auto innerProperties = properties; + + do { + std::string type = propertyAsString(*innerContext, innerItem, "type"); + + // Actual component expansion, no need to proceed. + if (findComponentBuilderFunc(innerContext, type)) break; + + auto resource = context->getLayout(type); + // Unknown type/layout. Return empty Path. + if (resource.empty()) return Path(); + + innerProperties.emplace(innerItem); + + // Switch to Layout path + simulatedPath = resource.path(); + const auto& layout = resource.json(); + + ParameterArray params(layout); + for (const auto& param : params) { + LOG_IF(DEBUG_BUILDER).session(context) << "Parsing parameter: " << param.name; + innerProperties.addToContext(innerContext, param, true); + } + + // Attach bindings "statically" to not create un-needed dependencies + { + auto bindings = arrayifyProperty(*innerContext, layout, "bind"); + for (const auto& binding : bindings) { + auto name = propertyAsString(*innerContext, binding, "name"); + if (!isValidBinding(context, binding, name)) continue; + + // Extract the binding as an optional node tree. + auto result = evaluateNested(*innerContext, binding.get("value")); + auto bindingType = optionalMappedProperty(*innerContext, binding, "type", + kBindingTypeAny, sBindingMap); + auto bindingFunc = sBindingFunctions.at(bindingType); + + innerContext->putConstant(name, bindingFunc(*innerContext, result)); + } + } + + simulatedPath = simulatedPath.addProperty(layout, "item", "items"); + auto items = arrayifyProperty(*innerContext, layout, "item", "items"); + int index = 0; + for (; index < items.size(); index++) { + const auto& item = items.at(index); + if (!item.isMap()) + continue; + + if (propertyAsBoolean(*innerContext, item, WHEN_FIELD, true)) { + innerItem = item; + simulatedPath = simulatedPath.addIndex(index); + break; + } + } + + // No selection available, break out "early" + if (index >= items.size()) break; + + innerContext = Context::createFromParent(innerContext); + } while (true); + + return simulatedPath; +} + /** * Expand a single component from a "when" list of possible components * @@ -364,20 +546,48 @@ Builder::expandSingleComponentFromArray(const ContextPtr& context, const CoreComponentPtr& parent, const Path& path, bool fullBuild, - bool useDirtyFlag) + bool useDirtyFlag, + const CoreComponentPtr& old) { LOG_IF(DEBUG_BUILDER).session(context) << path; + CoreComponentPtr result = nullptr; for (int index = 0; index < items.size(); index++) { const auto& item = items.at(index); if (!item.isMap()) continue; - if (propertyAsBoolean(*context, item, "when", true)) { - return expandSingleComponent(context, item, std::move(properties), parent, path.addIndex(index), fullBuild, useDirtyFlag); + if (propertyAsBoolean(*context, item, WHEN_FIELD, true)) { + if (old) { + auto oldItemIndex = context->opt(ITEM_INDEX).getInteger(); + if (oldItemIndex == index) { + // See if same definition "root" + auto oldPathString = old->getPathObject().toString(); + auto parentPathString = path.toString(); + auto simulatedPath = simulateLocalPath(path, context, item, properties).toString(); + + if (path.toString().size() < oldPathString.size() && + (oldPathString.substr(0, parentPathString.size()) == parentPathString || + oldPathString == simulatedPath)) { + result = old; + } + } + } + + if (!result) + result = expandSingleComponent(context, item, std::move(properties), + parent, path.addIndex(index), fullBuild, + useDirtyFlag); + + if (result != nullptr && result != old) { + // Record index of selected item + context->remove(ITEM_INDEX); + context->putSystemWriteable(ITEM_INDEX, index); + } + break; } } - return nullptr; + return result; } /** @@ -389,6 +599,7 @@ Builder::expandSingleComponentFromArray(const ContextPtr& context, * @param properties The user-specified properties for this layout. * @param layout The JSON definition of the layout object. * @param parent The parent component of this layout. + * @param path Parent path. * @param fullBuild Build full tree. * @param useDirtyFlag true to notify runtime about changes with dirty properties */ @@ -412,8 +623,8 @@ Builder::expandLayout(const std::string& name, // Build a new context for this layout. ContextPtr cptr = Context::createFromParent(context); - cptr->putConstant("__source", "layout"); - cptr->putConstant("__name", name); + cptr->putConstant(COMPONENT_CONTEXT_SOURCE, "layout"); + cptr->putConstant(COMPONENT_CONTEXT_NAME, name); // Add each parameter to the context. It's either going to come from // a property or its default value. This will remove the matching property from @@ -432,13 +643,15 @@ Builder::expandLayout(const std::string& name, LOG(LogLevel::kDebug).session(context) << m.first << ": " << m.second; } } + auto items = arrayifyProperty(*cptr, layout, "item", "items"); auto component = expandSingleComponentFromArray(cptr, - arrayifyProperty(*cptr, layout, "item", "items"), - std::move(properties), - parent, - path.addProperty(layout, "item", "items"), - fullBuild, - useDirtyFlag); + items, + std::move(properties), + parent, + path.addProperty(layout, "item", "items"), + fullBuild, + useDirtyFlag); + registerRebuildDependencyIfRequired(parent, cptr, items, component != nullptr); if (component) { // Assign the component to any bindings that had a "onChange" method for (const auto& m : bindingChangeList) diff --git a/aplcore/src/engine/context.cpp b/aplcore/src/engine/context.cpp index 1a36cdc..d308920 100644 --- a/aplcore/src/engine/context.cpp +++ b/aplcore/src/engine/context.cpp @@ -548,13 +548,17 @@ Context::documentContext() const return documentContextData(mCore)->documentContext(); } -#ifdef SCENEGRAPH +sg::TextLayoutCache& +Context::textLayoutCache() const +{ + return documentContextData(mCore)->textLayoutCache(); +} + sg::TextPropertiesCache& Context::textPropertiesCache() const { return documentContextData(mCore)->textPropertiesCache(); } -#endif // SCENEGRAPH ComponentPtr Context::inflate(const rapidjson::Value& component) diff --git a/aplcore/src/engine/corerootcontext.cpp b/aplcore/src/engine/corerootcontext.cpp index 9dda871..2a6ffab 100644 --- a/aplcore/src/engine/corerootcontext.cpp +++ b/aplcore/src/engine/corerootcontext.cpp @@ -57,15 +57,18 @@ CoreRootContext::create(const Metrics& metrics, const RootConfig& config, std::function callback) { + // Fill any unresolved parameters with empty objects. + auto pendingParametersCopy = content->getPendingParameters(); + for (const auto& parameter : pendingParametersCopy) { + content->addObjectData(parameter, Object::NULL_OBJECT()); + } + if (!content->isReady()) { LOG(LogLevel::kError).session(content) << "Attempting to create root context with illegal content"; return nullptr; } - auto root = std::make_shared( - metrics, - content, - config); + auto root = std::make_shared(config); root->init(metrics, config, content); @@ -77,9 +80,7 @@ CoreRootContext::create(const Metrics& metrics, return root; } -CoreRootContext::CoreRootContext(const Metrics& metrics, - const ContentPtr& content, - const RootConfig& config) +CoreRootContext::CoreRootContext(const RootConfig& config) : mTimeManager(config.getTimeManager()), mDisplayState(static_cast(config.getProperty(RootProperty::kInitialDisplayState).getInteger())) { @@ -343,6 +344,9 @@ CoreRootContext::cancelExecution() { assert(mTopDocument); mTopDocument->mCore->sequencer().reset(); + mShared->documentRegistrar().forEach([](const CoreDocumentContextPtr& document) { + return document->mCore->sequencer().reset(); + }); } ComponentPtr @@ -521,9 +525,20 @@ CoreRootContext::handleKeyboard(KeyHandlerType type, const Keyboard &keyboard) bool CoreRootContext::handlePointerEvent(const PointerEvent& pointerEvent) +{ + return handlePointerEvent(pointerEvent, mTimeManager->currentTime()); +} + +bool +CoreRootContext::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t timestamp) { assert(mShared); - return mShared->pointerManager().handlePointerEvent(pointerEvent, mTimeManager->currentTime()); + if (timestamp < currentTime()) { + LOG(LogLevel::kWarn) << "Pointer event ignored as timestamp before last tick. Pointer timestamp: " + << timestamp << " Tick timestamp: " << currentTime(); + return false; + } + return mShared->pointerManager().handlePointerEvent(pointerEvent, timestamp); } const RootConfig& diff --git a/aplcore/src/engine/layoutmanager.cpp b/aplcore/src/engine/layoutmanager.cpp index 3981a64..e2a3f2b 100644 --- a/aplcore/src/engine/layoutmanager.cpp +++ b/aplcore/src/engine/layoutmanager.cpp @@ -158,7 +158,7 @@ LayoutManager::layout(bool useDirtyFlag, bool first) // After layout has completed we mark individual components as allowing event handlers for (const auto& m : laidOut) - m->postProcessLayoutChanges(); + m->postProcessLayoutChanges(first); } void diff --git a/aplcore/src/engine/sharedcontextdata.cpp b/aplcore/src/engine/sharedcontextdata.cpp index 3cfb9ae..84cf664 100644 --- a/aplcore/src/engine/sharedcontextdata.cpp +++ b/aplcore/src/engine/sharedcontextdata.cpp @@ -15,6 +15,7 @@ #include "apl/engine/sharedcontextdata.h" +#include "apl/content/content.h" #include "apl/embed/documentregistrar.h" #include "apl/engine/dependantmanager.h" #include "apl/engine/eventmanager.h" @@ -26,13 +27,11 @@ #include "apl/engine/visibilitymanager.h" #include "apl/focus/focusmanager.h" #include "apl/media/mediamanager.h" +#include "apl/scenegraph/textlayoutcache.h" +#include "apl/scenegraph/textpropertiescache.h" #include "apl/touch/pointermanager.h" #include "apl/utils/make_unique.h" -#ifdef SCENEGRAPH -#include "apl/scenegraph/textpropertiescache.h" -#endif // SCENEGRAPH - namespace apl { static const bool DEBUG_YG_PRINT_TREE = false; @@ -91,11 +90,9 @@ SharedContextData::SharedContextData(const CoreRootContextPtr& root, mYGConfigRef(YGConfigNew()), mTextMeasurement(config.getMeasure()), mCachedMeasures(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()), - mCachedBaselines(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()) -#ifdef SCENEGRAPH - , + mCachedBaselines(config.getProperty(RootProperty::kTextMeasurementCacheLimit).getInteger()), + mTextLayoutCache(new sg::TextLayoutCache()), mTextPropertiesCache(new sg::TextPropertiesCache()) -#endif // SCENEGRAPH { YGConfigSetPrintTreeFlag(mYGConfigRef, DEBUG_YG_PRINT_TREE); YGConfigSetLogger(mYGConfigRef, ygLogger); @@ -109,11 +106,9 @@ SharedContextData::SharedContextData(const RootConfig& config) mYGConfigRef(YGConfigNew()), mTextMeasurement(config.getMeasure()), mCachedMeasures(0), - mCachedBaselines(0) -#ifdef SCENEGRAPH - , + mCachedBaselines(0), + mTextLayoutCache(new sg::TextLayoutCache()), mTextPropertiesCache(new sg::TextPropertiesCache()) -#endif // SCENEGRAPH {} SharedContextData::~SharedContextData() { diff --git a/aplcore/src/engine/visibilitymanager.cpp b/aplcore/src/engine/visibilitymanager.cpp index 9b2dd73..eab762f 100644 --- a/aplcore/src/engine/visibilitymanager.cpp +++ b/aplcore/src/engine/visibilitymanager.cpp @@ -24,8 +24,10 @@ namespace apl { void VisibilityManager::registerForUpdates(const CoreComponentPtr& component) { - // Stash tracked component itself - mTrackedComponentVisibility.emplace(component, VisibilityState{-1, -1}); + // Add component to registration queue. We can't really create a tree properly until component + // requested for registration ultimately attached to the hierarchy root, and this only happens + // when all children processed. + mRegistrationQueue.emplace(component); } void @@ -47,6 +49,21 @@ VisibilityManager::markDirty(const CoreComponentPtr& component) void VisibilityManager::processVisibilityChanges() { + // Register any new added components. + for (const auto& weak : mRegistrationQueue) { + auto component = weak.lock(); + if (!component) continue; + + mTrackedComponentVisibility.emplace(component, VisibilityState{-1, -1}); + auto parent = CoreComponent::cast(component->getParent()); + + if (parent) parent->addDownstreamVisibilityTarget(component); + + markDirty(component); + } + + mRegistrationQueue.clear(); + for (const auto& cc : mDirtyVisibility) { auto it = mTrackedComponentVisibility.find(cc); if (it == mTrackedComponentVisibility.end()) continue; diff --git a/aplcore/src/extension/extensionclient.cpp b/aplcore/src/extension/extensionclient.cpp index 8226a3f..56cf9ea 100644 --- a/aplcore/src/extension/extensionclient.cpp +++ b/aplcore/src/extension/extensionclient.cpp @@ -197,13 +197,17 @@ ExtensionClient::processMessageInternal(const CoreDocumentContextPtr& documentCo const auto& context = documentContext ? documentContext->context() : mInternalRootConfig->evaluationContext(); auto evaluated = Object(std::move(message).get()); - auto method = propertyAsMapped(context, evaluated, "method", static_cast(-1), sExtensionMethodBimap); + auto method = requiredMappedProperty(context, evaluated, "method", sExtensionMethodBimap); + if (!method.second) { + CONSOLE(mSession).log("Missing or invalid method property"); + return false; + } if (!mRegistered) { if (mRegistrationProcessed) { CONSOLE(mSession).log("Can't process message after failed registration."); return false; - } else if (method != kExtensionMethodRegisterSuccess && method != kExtensionMethodRegisterFailure) { + } else if (method.first != kExtensionMethodRegisterSuccess && method.first != kExtensionMethodRegisterFailure) { CONSOLE(mSession).log("Can't process message before registration."); return false; } @@ -221,7 +225,7 @@ ExtensionClient::processMessageInternal(const CoreDocumentContextPtr& documentCo } bool result = true; - switch (method) { + switch (method.first) { case kExtensionMethodRegisterSuccess: result = processRegistrationResponse(context, evaluated); // FALL_THROUGH @@ -571,9 +575,9 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd auto& dataRef = mLiveData.at(name.getString()); for (const auto& operation : operations.getArray()) { - auto type = propertyAsMapped(context, operation, "type", - static_cast(-1), sExtensionLiveDataUpdateTypeBimap); - if (type == static_cast(-1)) { + auto type = requiredMappedProperty( + context, operation, "type", sExtensionLiveDataUpdateTypeBimap); + if (!type.second) { CONSOLE(mSession) << "Wrong operation type for=" << name; return false; } @@ -581,10 +585,10 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd bool result; switch (dataRef.objectType) { case kExtensionLiveDataTypeObject: - result = updateLiveMap(type, dataRef, operation); + result = updateLiveMap(type.first, dataRef, operation); break; case kExtensionLiveDataTypeArray: - result = updateLiveArray(type, dataRef, operation); + result = updateLiveArray(type.first, dataRef, operation); break; default: result = false; @@ -594,7 +598,7 @@ ExtensionClient::processLiveDataUpdate(const Context& context, const Object& upd if (!result) { CONSOLE(mSession) << "LiveMap operation failed=" << dataRef.name << " operation=" - << sExtensionLiveDataUpdateTypeBimap.at(type); + << sExtensionLiveDataUpdateTypeBimap.at(type.first); } else { dataRef.hasPendingUpdate = true; } @@ -783,10 +787,8 @@ ExtensionClient::readExtensionTypes(const Context& context, const Object& types) continue; } else { defValue = propertyAsRecursive(context, ps, "default"); - ptype = propertyAsMapped(context, ps, "type", kBindingTypeAny, sBindingMap); - if (!sBindingMap.has(ptype)) { - ptype = kBindingTypeAny; - } + ptype = optionalMappedProperty(context, ps, "type", kBindingTypeAny, + sBindingMap); preq = propertyAsBoolean(context, ps, "required", false); } @@ -887,8 +889,9 @@ ExtensionClient::readExtensionEventHandlers(const Context& context, const Object CONSOLE(mSession).log("Invalid extension event handler for extension=%s", mUri.c_str()); return false; } else { - auto mode = propertyAsMapped(context, handler, "mode", - kExtensionEventExecutionModeFast, sExtensionEventExecutionModeBimap); + auto mode = optionalMappedProperty( + context, handler, "mode", kExtensionEventExecutionModeFast, + sExtensionEventExecutionModeBimap); mSchema.eventModes.emplace(name.asString(), mode); mSchema.eventHandlers.emplace_back(ExtensionEventHandler(mUri, name.asString())); } @@ -1063,7 +1066,7 @@ ExtensionClient::readExtensionComponentEventHandlers(const Context& context, return false; } else { - auto mode = propertyAsMapped( + auto mode = optionalMappedProperty( context, handler, "mode", kExtensionEventExecutionModeFast, sExtensionEventExecutionModeBimap); mSchema.eventModes.emplace(name.asString(), mode); @@ -1127,10 +1130,8 @@ ExtensionClient::readExtensionComponentDefinitions(const Context& context, const continue; } else { defValue = propertyAsObject(context, ps, "default"); - ptype = propertyAsMapped(context, ps, "type", kBindingTypeAny, sBindingMap); - if (!sBindingMap.has(ptype)) { - ptype = kBindingTypeAny; - } + ptype = optionalMappedProperty(context, ps, "type", + kBindingTypeAny, sBindingMap); preq = propertyAsBoolean(context, ps, "required", false); } diff --git a/aplcore/src/extension/extensionmediator.cpp b/aplcore/src/extension/extensionmediator.cpp index e0c4199..1918a63 100644 --- a/aplcore/src/extension/extensionmediator.cpp +++ b/aplcore/src/extension/extensionmediator.cpp @@ -573,8 +573,11 @@ ExtensionMediator::loadExtensions(const RootConfigPtr& rootConfig, const Content void ExtensionMediator::loadExtensions(const ObjectMap& flagMap, const ContentPtr& content, const std::set* grantedExtensions) { - if (!content->isReady()) { - CONSOLE(content) << "Cannot load extensions when Content is not ready"; + if (content->isError()) { + CONSOLE(content) << "Cannot load extensions when Content has errors"; + return; + } else if (content->isWaiting()) { + CONSOLE(content) << "Cannot load extensions when Content is waiting for packages"; return; } diff --git a/aplcore/src/graphic/graphic.cpp b/aplcore/src/graphic/graphic.cpp index 552a8fa..7520b03 100644 --- a/aplcore/src/graphic/graphic.cpp +++ b/aplcore/src/graphic/graphic.cpp @@ -78,14 +78,14 @@ Graphic::create(const ContextPtr& context, LOG_IF(DEBUG_GRAPHIC).session(context) << "Creating graphic data=" << context->opt("data").toDebugString(); // Check and extract the version - auto version = propertyAsMapped(*context, json, "version", -1, sGraphicVersionBimap); - if (version == -1) { + auto version = requiredMappedProperty(*context, json, "version", sGraphicVersionBimap); + if (!version.second) { CONSOLE(context) << "Illegal graphics version"; return nullptr; } - LOG_IF(DEBUG_GRAPHIC).session(context) << "Found version" << version; + LOG_IF(DEBUG_GRAPHIC).session(context) << "Found version" << version.first; - auto graphic = std::make_shared(context, json, static_cast(version)); + auto graphic = std::make_shared(context, json, static_cast(version.first)); graphic->initialize(context, json, std::move(properties), component, path, styledPtr); return graphic; } diff --git a/aplcore/src/graphic/graphicelementtext.cpp b/aplcore/src/graphic/graphicelementtext.cpp index 1ffe6ed..2ccc91c 100644 --- a/aplcore/src/graphic/graphicelementtext.cpp +++ b/aplcore/src/graphic/graphicelementtext.cpp @@ -333,7 +333,7 @@ GraphicElementText::ensureSGTextLayout() if (!context) return; - assert(mContext->measure()->sceneGraphCompatible()); + assert(mContext->measure()->layoutCompatible()); auto measure = std::static_pointer_cast(mContext->measure()); ensureTextProperties(); diff --git a/aplcore/src/livedata/layoutrebuilder.cpp b/aplcore/src/livedata/layoutrebuilder.cpp index 60263ea..5772749 100644 --- a/aplcore/src/livedata/layoutrebuilder.cpp +++ b/aplcore/src/livedata/layoutrebuilder.cpp @@ -16,12 +16,17 @@ #include "apl/livedata/layoutrebuilder.h" #include "apl/component/corecomponent.h" +#include "apl/content/content.h" +#include "apl/document/coredocumentcontext.h" #include "apl/engine/builder.h" #include "apl/livedata/livearraychange.h" #include "apl/livedata/livearrayobject.h" +#include "apl/utils/constants.h" namespace apl { +const std::string COMPONENT_REBUILDER_TOKEN = "_token"; + static const bool DEBUG_WALKER = false; /** @@ -64,7 +69,7 @@ class ChildWalker { LOG_IF(DEBUG_WALKER).session(mLayout) << " oldIndex=" << oldIndex << " mIndex=" << mIndex << " total=" << mLayout->getChildCount(); while (mIndex < mLayout->getChildCount()) { auto child = mLayout->getCoreChildAt(mIndex); - auto dataIndex = child->getContext()->opt("dataIndex"); + auto dataIndex = child->getContext()->opt(COMPONENT_DATA_INDEX); if (dataIndex.isNumber()) { auto index = dataIndex.getInteger(); if (index >= oldIndex) { @@ -101,7 +106,7 @@ class ChildWalker { inline ContextPtr findToken(const CoreComponentPtr& component, int token) { auto c = component->getContext(); - auto p = c->find("_token"); + auto p = c->find(COMPONENT_REBUILDER_TOKEN); auto value = p.object().value(); if (value.isNumber() && value.getInteger() == token) return p.context(); @@ -161,11 +166,11 @@ LayoutRebuilder::buildBaseChildContext(const LiveArrayObjectPtr& array, size_t d auto length = array->size(); const auto& data = array->at(dataIndex); auto childContext = Context::createFromParent(mContext); - childContext->putSystemWriteable("data", data); // This can be changed - childContext->putSystemWriteable("index", insertIndex); - childContext->putSystemWriteable("length", length); - childContext->putSystemWriteable("dataIndex", dataIndex); // This is an addition - childContext->putSystemWriteable("_token", mRebuilderToken); // Drop a token for later sanity checking + childContext->putSystemWriteable(COMPONENT_DATA, data); // This can be changed + childContext->putSystemWriteable(COMPONENT_INDEX, insertIndex); + childContext->putSystemWriteable(COMPONENT_LENGTH, length); + childContext->putSystemWriteable(COMPONENT_DATA_INDEX, dataIndex); // This is an addition + childContext->putSystemWriteable(COMPONENT_REBUILDER_TOKEN, mRebuilderToken); // Drop a token for later sanity checking return childContext; } @@ -194,7 +199,7 @@ LayoutRebuilder::build(bool useDirtyFlag) auto childContext = buildBaseChildContext(array, dataIndex, index); if (mNumbered) - childContext->putSystemWriteable("ordinal", ordinal); + childContext->putSystemWriteable(COMPONENT_ORDINAL, ordinal); auto child = Builder(old).expandSingleComponentFromArray(childContext, mItems, @@ -203,6 +208,9 @@ LayoutRebuilder::build(bool useDirtyFlag) mChildPath, layout->shouldBeFullyInflated(index), useDirtyFlag); + + Builder::registerRebuildDependencyIfRequired(layout, childContext, mItems, child != nullptr, {COMPONENT_DATA}); + if (child && child->isValid()) { layout->appendChild(child, useDirtyFlag); index++; @@ -250,19 +258,25 @@ LayoutRebuilder::rebuild() auto oldIndex = p.first; auto needsRefresh = p.second; - if (oldIndex == -1) { // Insert a new child - this one doesn't exist + auto reactiveHandling = CoreDocumentContext::cast(mContext->documentContext()) + ->content() + ->reactiveConditionalInflation(); + + if (oldIndex == -1 || (reactiveHandling && !walker.advanceUntil(oldIndex))) { // Insert a new child - this one doesn't exist auto childContext = buildBaseChildContext(array, newIndex, index); if (mNumbered) - childContext->putConstant("ordinal", ordinal); - - auto child = Builder(old).expandSingleComponentFromArray(childContext, - mItems, - Properties(), - layout, - mChildPath, - layout->shouldBeFullyInflated(index), - true); + childContext->putConstant(COMPONENT_ORDINAL, ordinal); + + auto child = Builder(old).expandSingleComponentFromArray( + childContext, + mItems, + Properties(), + layout, + mChildPath, + layout->shouldBeFullyInflated(index), + true); + Builder::registerRebuildDependencyIfRequired(layout, childContext, mItems, child != nullptr, {COMPONENT_DATA}); if (child && child->isValid()) { layout->insertChild(child, index + (mHasFirstItem ? 1 : 0), true); index++; @@ -279,18 +293,39 @@ LayoutRebuilder::rebuild() auto child = walker.currentChild(); auto childContext = findToken(child, mRebuilderToken); // Search up through contexts to find the right one to modify if (childContext) { + auto isRemove = false; // TODO: Can we optimize recalculation by batching these and only changing some of them? - childContext->systemUpdateAndRecalculate("index", index, true); + childContext->systemUpdateAndRecalculate(COMPONENT_INDEX, index, true); if (needsRefresh) - childContext->systemUpdateAndRecalculate("data", data, true); - - childContext->systemUpdateAndRecalculate("length", array->size(), true); - childContext->systemUpdateAndRecalculate("dataIndex", newIndex, true); - childContext->systemUpdateAndRecalculate("ordinal", ordinal, true); + childContext->systemUpdateAndRecalculate(COMPONENT_DATA, data, true); + + childContext->systemUpdateAndRecalculate(COMPONENT_LENGTH, array->size(), true); + childContext->systemUpdateAndRecalculate(COMPONENT_DATA_INDEX, newIndex, true); + childContext->systemUpdateAndRecalculate(COMPONENT_ORDINAL, ordinal, true); + + if (reactiveHandling) { + // Should be fully inflated if old currently attached and in ensured range + auto fullBuild = (child && child->isAttached()) || layout->shouldBeFullyInflated(index); + // No need to register dependency here. Existing children reuse context + auto newChild = Builder(layout).expandSingleComponentFromArray( + childContext, mItems, Properties(), layout, mChildPath, + fullBuild, true, child); + if (newChild != child) { + if (child) layout->removeChildAt(index, true); + if (newChild && newChild->isValid()) { + // Add new one with reused context + layout->insertChild(newChild, index, true); + } else { + isRemove = true; + } + } + } - index += 1; - walker.advance(); + if (!isRemove) { + walker.advance(); + index += 1; + } if (mNumbered) { int numbering = child->getCalculated(kPropertyNumbering).getInteger(); @@ -318,7 +353,7 @@ LayoutRebuilder::notifyItemOnScreen(int idx) return; auto child = mLayout.lock()->getCoreChildAt(idx); - auto dataIndex = child->getContext()->opt("dataIndex"); + auto dataIndex = child->getContext()->opt(COMPONENT_DATA_INDEX); if (dataIndex.isNumber()) { array->ensure(dataIndex.getInteger()); } diff --git a/aplcore/src/primitives/gradient.cpp b/aplcore/src/primitives/gradient.cpp index 9dde72b..a069fbf 100644 --- a/aplcore/src/primitives/gradient.cpp +++ b/aplcore/src/primitives/gradient.cpp @@ -97,15 +97,24 @@ Gradient::create(const Context& context, const Object& object, bool avg) if (!object.isMap()) return Object::NULL_OBJECT(); - if (avg && !object.has("type")) { - CONSOLE(context) << "Type field is required in AVG gradient"; - return Object::NULL_OBJECT(); + auto type = GradientType::LINEAR; + if (avg) { + // An avg gradient is required to specify the type + auto result = requiredMappedProperty(context, object, "type", sGradientTypeMap); + if (!result.second) { + CONSOLE(context) << "Type field is required in AVG gradient"; + return Object::NULL_OBJECT(); + } + type = result.first; } - - auto type = propertyAsMapped(context, object, "type", LINEAR, sGradientTypeMap); - if (type < 0) { - CONSOLE(context) << "Unrecognized type field in gradient"; - return Object::NULL_OBJECT(); + else { + // A regular gradient defaults to "linear"' + auto result = optionalStrictMappedProperty(context, object, "type", LINEAR, sGradientTypeMap); + if (!result.second) { + CONSOLE(context) << "Unrecognized type field in gradient"; + return Object::NULL_OBJECT(); + } + type = result.first; } // Extract and evaluate the color range @@ -157,8 +166,8 @@ Gradient::create(const Context& context, const Object& object, bool avg) properties.emplace(kGradientPropertyColorRange, std::move(colors)); properties.emplace(kGradientPropertyInputRange, std::move(inputs)); - auto units = propertyAsMapped(context, object, "units", - kGradientUnitsBoundingBox, sGradientUnitsMap); + auto units = optionalMappedProperty( + context, object, "units", kGradientUnitsBoundingBox, sGradientUnitsMap); properties.emplace(kGradientPropertyUnits, units); // AVG specific handling @@ -171,8 +180,8 @@ Gradient::create(const Context& context, const Object& object, bool avg) double y2 = 1.0; if (avg) { - spreadMethod = propertyAsMapped(context, object, "spreadMethod", PAD, - sGradientSpreadMethodMap); + spreadMethod = optionalMappedProperty( + context, object, "spreadMethod", PAD, sGradientSpreadMethodMap); x1 = propertyAsDouble(context, object, "x1", 0.0); x2 = propertyAsDouble(context, object, "x2", 1.0); y1 = propertyAsDouble(context, object, "y1", 0.0); diff --git a/aplcore/src/primitives/mediasource.cpp b/aplcore/src/primitives/mediasource.cpp index c2edd3b..1680fc0 100644 --- a/aplcore/src/primitives/mediasource.cpp +++ b/aplcore/src/primitives/mediasource.cpp @@ -63,7 +63,7 @@ MediaSource::create(const Context& context, const Object& object) if (!object.isMap()) return Object::NULL_OBJECT(); - std::string url = propertyAsString(context, object, "url"); + auto url = propertyAsString(context, object, "url"); if(url.empty()) { CONSOLE(context) << "Media Source has no URL defined."; return Object::NULL_OBJECT(); @@ -82,34 +82,33 @@ MediaSource::create(const Context& context, const Object& object) continue; } - auto type = propertyAsMapped(context, m, "type", static_cast(-1), sTextTrackTypeMap); - if (type < 0) { + auto type = requiredMappedProperty(context, m, "type", sTextTrackTypeMap); + if (!type.second) { CONSOLE(context) << "Unrecognized type field in Text Track"; continue; } - std::string url = propertyAsString(context, m, "url"); - if (url.empty()) { + // TODO: Why is this here? + auto dummyURL = propertyAsString(context, m, "url"); + if (dummyURL.empty()) { CONSOLE(context) << "Text Track has no URL defined."; continue; } - std::string description = propertyAsString(context, m, "description"); - tracks.push_back(TextTrack{ - static_cast(type), + type.first, URLRequest::create(context, m).get().getUrl(), - description + propertyAsString(context, m, "description") }); } - return Object(MediaSource(URLRequest::create(context, object).get(), + return MediaSource(URLRequest::create(context, object).get(), description, duration, repeatCount, entities, offset, - tracks)); + tracks); } std::string diff --git a/aplcore/src/primitives/object.cpp b/aplcore/src/primitives/object.cpp index ae39790..38c8f2f 100644 --- a/aplcore/src/primitives/object.cpp +++ b/aplcore/src/primitives/object.cpp @@ -17,6 +17,7 @@ #include +#include "apl/content/sharedjsondata.h" #include "apl/engine/context.h" #include "apl/primitives/boundsymbol.h" #include "apl/primitives/color.h" @@ -335,9 +336,18 @@ Object::Object(const rapidjson::Value& value) : mType(Null::ObjectType::instance } } -Object::Object(rapidjson::Document&& value) : mType(Null::ObjectType::instance()) +Object::Object(rapidjson::Document&& value) + : Object(SharedJsonData(std::move(value))) { - if (OBJECT_DEBUG) LOG(LogLevel::kDebug) << "Object constructor value: " << this; +} + +Object::Object(const SharedJsonData& doc) : mType(Null::ObjectType::instance()) +{ + if (OBJECT_DEBUG) LOG(LogLevel::kDebug) << "Object constructor SharedJsonData: " << this; + + if (!doc) return; + + const rapidjson::Value& value = doc.get(); switch(value.GetType()) { case rapidjson::kNullType: @@ -361,11 +371,11 @@ Object::Object(rapidjson::Document&& value) : mType(Null::ObjectType::instance() break; case rapidjson::kObjectType: mType = Map::ObjectType::instance(); - new(&mU.data) std::shared_ptr(std::make_shared(std::move(value))); + new(&mU.data) std::shared_ptr(std::make_shared(doc.getSharedDoc(), &value)); break; case rapidjson::kArrayType: mType = Array::ObjectType::instance(); - new(&mU.data) std::shared_ptr(std::make_shared(std::move(value))); + new(&mU.data) std::shared_ptr(std::make_shared(doc.getSharedDoc(), &value)); break; } } diff --git a/aplcore/src/scenegraph/CMakeLists.txt b/aplcore/src/scenegraph/CMakeLists.txt index c78b78a..12d4b48 100644 --- a/aplcore/src/scenegraph/CMakeLists.txt +++ b/aplcore/src/scenegraph/CMakeLists.txt @@ -11,22 +11,32 @@ # express or implied. See the License for the specific language governing # permissions and limitations under the License. -target_sources_local(apl - PRIVATE - accessibility.cpp - builder.cpp - edittextconfig.cpp - filter.cpp - layer.cpp - node.cpp - graphicfragment.cpp - paint.cpp - path.cpp - pathop.cpp - pathbounds.cpp - pathparser.cpp - scenegraph.cpp - scenegraphupdates.cpp - textproperties.cpp - utilities.cpp - ) \ No newline at end of file +if(ENABLE_SCENEGRAPH) + target_sources_local(apl + PRIVATE + accessibility.cpp + builder.cpp + edittextconfig.cpp + filter.cpp + layer.cpp + node.cpp + graphicfragment.cpp + paint.cpp + path.cpp + pathop.cpp + pathbounds.cpp + pathparser.cpp + scenegraph.cpp + scenegraphupdates.cpp + textproperties.cpp + utilities.cpp + ) +else() + target_sources_local(apl + PRIVATE + edittextconfig.cpp + textproperties.cpp + utilities.cpp + ) +endif(ENABLE_SCENEGRAPH) + diff --git a/aplcore/src/scenegraph/textproperties.cpp b/aplcore/src/scenegraph/textproperties.cpp index e79a65c..0f7067e 100644 --- a/aplcore/src/scenegraph/textproperties.cpp +++ b/aplcore/src/scenegraph/textproperties.cpp @@ -87,6 +87,25 @@ TextProperties::create(TextPropertiesCache& cache, return properties; } +size_t +TextProperties::hash() const +{ + size_t hash = 0; + for (const auto& m : mFontFamily) + hashCombine(hash, m); + hashCombine(hash, mFontSize); + hashCombine(hash, static_cast(mFontStyle)); + hashCombine(hash, mLanguage); + hashCombine(hash, mFontWeight); + hashCombine(hash, mLetterSpacing); + hashCombine(hash, mLineHeight); + hashCombine(hash, mMaxLines); + hashCombine(hash, static_cast(mTextAlign)); + hashCombine(hash, static_cast(mTextAlignVertical)); + + return hash; +} + bool operator==(const TextProperties& lhs, const TextProperties& rhs) { diff --git a/aplcore/src/touch/gesture.cpp b/aplcore/src/touch/gesture.cpp index 5cb5cf6..adc54f3 100644 --- a/aplcore/src/touch/gesture.cpp +++ b/aplcore/src/touch/gesture.cpp @@ -56,13 +56,13 @@ Gesture::create(const ActionablePtr& actionable, const Object& object) auto contextPtr = actionable->getContext(); - auto type = propertyAsMapped(*contextPtr, object, "type", kGestureTypeDoublePress, sGestureTypeBimap); - if (type == static_cast(-1)) { + auto type = requiredMappedProperty(*contextPtr, object, "type", sGestureTypeBimap); + if (!type.second) { CONSOLE(*contextPtr) << "Unrecognized type field in gesture handler"; return nullptr; } - auto method = sGestureFunctions.find(type); + auto method = sGestureFunctions.find(type.first); if (method != sGestureFunctions.end()) { return method->second(actionable, *contextPtr, object); } diff --git a/aplcore/src/touch/gestures/swipeawaygesture.cpp b/aplcore/src/touch/gestures/swipeawaygesture.cpp index 4a6399e..6cc257f 100644 --- a/aplcore/src/touch/gestures/swipeawaygesture.cpp +++ b/aplcore/src/touch/gestures/swipeawaygesture.cpp @@ -48,15 +48,10 @@ SwipeAwayGesture::create(const ActionablePtr& actionable, const Context& context return nullptr; } - auto action = propertyAsMapped(context, object, "action", kSwipeAwayActionSlide, - sSwipeAwayActionTypeBimap); - if (action == static_cast(-1)) { - CONSOLE(context) << "Unrecognized action field in SwipeAway gesture handler"; - return nullptr; - } - - auto direction = propertyAsMapped(context, object, "direction", -1, sSwipeDirectionMap); - if (direction < 0) { + auto action = optionalMappedProperty( + context, object, "action", kSwipeAwayActionSlide, sSwipeAwayActionTypeBimap); + auto direction = requiredMappedProperty(context, object, "direction", sSwipeDirectionMap); + if (!direction.second) { CONSOLE(context) << "Unrecognized direction field in SwipeAway gesture handler"; return nullptr; } @@ -65,8 +60,9 @@ SwipeAwayGesture::create(const ActionablePtr& actionable, const Context& context Object onSwipeDone = arrayifyPropertyAsObject(context, object, "onSwipeDone"); Object items = arrayifyPropertyAsObject(context, object, "item", "items"); - return std::make_shared(actionable, action, static_cast(direction), - std::move(onSwipeMove), std::move(onSwipeDone), std::move(items)); + return std::make_shared( + actionable, action, static_cast(direction.first), std::move(onSwipeMove), + std::move(onSwipeDone), std::move(items)); } SwipeAwayGesture::SwipeAwayGesture(const ActionablePtr& actionable, SwipeAwayActionType action, diff --git a/aplcore/src/touch/pointermanager.cpp b/aplcore/src/touch/pointermanager.cpp index b08d16c..330c961 100644 --- a/aplcore/src/touch/pointermanager.cpp +++ b/aplcore/src/touch/pointermanager.cpp @@ -122,6 +122,7 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t ActionableComponentPtr target = nullptr; // save the last active pointer before it gets updated auto previousPointer = mLastActivePointer; + mLastestPointerTimeStamp = timestamp; switch (pointerEvent.pointerEventType) { case kPointerDown: @@ -196,6 +197,10 @@ PointerManager::handlePointerEvent(const PointerEvent& pointerEvent, apl_time_t void PointerManager::handleTimeUpdate(apl_time_t timestamp) { + if (mLastestPointerTimeStamp > timestamp) { + LOG(LogLevel::kWarn) << "TimeUpdate pointer proccessed later than expected. Latest Pointer TimeStamp: " + << mLastestPointerTimeStamp << " TimeUpdate timestamp: " << timestamp; + } sendEventToTarget(kPointerTimeUpdate, mLastActivePointer, timestamp); } diff --git a/aplcore/src/touch/utils/pagemovehandler.cpp b/aplcore/src/touch/utils/pagemovehandler.cpp index 7acc3b3..62aa72d 100644 --- a/aplcore/src/touch/utils/pagemovehandler.cpp +++ b/aplcore/src/touch/utils/pagemovehandler.cpp @@ -64,8 +64,9 @@ PageMoveHandler::create( } auto commands = arrayifyPropertyAsObject(*eventContext, object, "commands"); - auto action = propertyAsMapped(*eventContext, object, "drawOrder", - kPageMoveDrawOrderHigherAbove, sPageMoveDrawOrderBimap); + auto action = optionalMappedProperty(*eventContext, object, "drawOrder", + kPageMoveDrawOrderHigherAbove, + sPageMoveDrawOrderBimap); return std::make_unique(std::move(commands), action, swipeDirection, pageDirection, currentChild, nextChild); diff --git a/aplcore/src/utils/searchvisitor.cpp b/aplcore/src/utils/searchvisitor.cpp index a8bda02..f770953 100644 --- a/aplcore/src/utils/searchvisitor.cpp +++ b/aplcore/src/utils/searchvisitor.cpp @@ -16,6 +16,7 @@ #include "apl/component/corecomponent.h" #include "apl/content/rootconfig.h" +#include "apl/engine/propdef.h" #include "apl/utils/visitor.h" #include "apl/utils/searchvisitor.h" @@ -90,7 +91,8 @@ TouchableAtPosition::universalCondition(const CoreComponent& component, const Point& pointInCurrent) { return component.containsLocalPosition(pointInCurrent) && component.getCalculated(kPropertyDisplay).asInt() == kDisplayNormal - && component.getCalculated(kPropertyOpacity).asNumber() > 0.0; + && component.getCalculated(kPropertyOpacity).asNumber() > 0.0 + && !(component.getCalculated(kPropertyPointerEvents) == kPointerEventsNone); } bool diff --git a/aplcore/src/utils/stickychildrentree.cpp b/aplcore/src/utils/stickychildrentree.cpp index 6185bf4..ff83127 100644 --- a/aplcore/src/utils/stickychildrentree.cpp +++ b/aplcore/src/utils/stickychildrentree.cpp @@ -13,9 +13,11 @@ * permissions and limitations under the License. */ -#include "apl/utils/stickyfunctions.h" #include "apl/utils/stickychildrentree.h" +#include "apl/component/corecomponent.h" +#include "apl/utils/stickyfunctions.h" + namespace apl { /* diff --git a/aplcore/src/versioning/CMakeLists.txt b/aplcore/src/versioning/CMakeLists.txt new file mode 100644 index 0000000..e8453f0 --- /dev/null +++ b/aplcore/src/versioning/CMakeLists.txt @@ -0,0 +1,19 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +target_sources_local(apl + PRIVATE + semanticgrammar.cpp + semanticpattern.cpp + semanticversion.cpp +) diff --git a/aplcore/src/versioning/semanticgrammar.cpp b/aplcore/src/versioning/semanticgrammar.cpp new file mode 100644 index 0000000..07e1b42 --- /dev/null +++ b/aplcore/src/versioning/semanticgrammar.cpp @@ -0,0 +1,29 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include + +#include "apl/versioning/semanticgrammar.h" + +namespace apl { + +namespace svgrammar { + +template<> const std::string sv_control::error_message = "expected at least one digit"; +template<> const std::string sv_control::error_message = "unexpected end"; + +} // namespace svgrammar + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/versioning/semanticpattern.cpp b/aplcore/src/versioning/semanticpattern.cpp new file mode 100644 index 0000000..764e592 --- /dev/null +++ b/aplcore/src/versioning/semanticpattern.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 "apl/versioning/semanticgrammar.h" +#include "apl/versioning/semanticpattern.h" +#include "apl/utils/session.h" + +namespace apl { + +SemanticPatternPtr +SemanticPattern::create(const SessionPtr& session, const std::string& string) +{ + if (string.size() == 0) { + CONSOLE(session) << "Empty pattern string"; + return nullptr; + } + + svgrammar::semvar_pattern_state state; + pegtl::memory_input<> in(string, ""); + + if (!pegtl::parse(in, state) || state.failed) { + CONSOLE(session) << "Error parsing semantic pattern '" << string << "', " << state.what(); + return nullptr; + } + + return std::make_shared(state.versions, state.operators); +} + +/** + * In a comparison operation for pattern matching, the presence or absence of a prerelease is important. + * The examples in this table have already passed the match() criteria where the semantic version is + * in the correct range for the pattern. However semantic versioning has the additional criteria that + * if the version to be checked has prerelease elements, then the MAJOR.MINOR.PATCH numbers must match as well. + * + * This table summaries the conditions: + * + * Pattern Version to check + * ======= ================ + * >1.2.0 1.3.0 -> TRUE The checked version has no prerelease elements. + * >1.2-alpha 1.3.0 -> TRUE The checked version has no prerelease elements. + * >1.2.0 1.2.1-alpha -> FALSE The pattern has no prerelease, and the version does. + * >1.2-alpha 1.2.1-alpha -> FALSE Both have prerelease elements, but MAJOR.MINOR.PATCH doesn't match + * >1.2-alpha 1.2.0-alpha.2 -> TRUE Both have prerelease elements, and MAJOR.MINOR.PATCH matches (1.2.0) + */ + +static bool +checkForValidPrerelease(const SemanticVersion& pattern, const SemanticVersion& version) +{ + if (version.simple()) + return true; + + if (pattern.simple()) + return false; + + return pattern.versionMatch(version); +} + + +bool +SemanticPattern::match(const SemanticVersionPtr& version) const +{ + if (version == nullptr) + return false; + + bool matched = true; // Assume matched to start + auto iter = mVersions.begin(); + + for (const auto& op : mOperators) { + switch (op) { + case kSemanticOpEquals: { + const auto operand = *iter++; + matched = matched && version->compare(*operand) == 0; + } + break; + case kSemanticOpGreaterThan: { + const auto operand = *iter++; + matched = matched && version->compare(*operand) > 0 && checkForValidPrerelease(*operand, *version); + } + break; + case kSemanticOpGreaterThanOrEquals: { + const auto operand = *iter++; + matched = matched && version->compare(*operand) >= 0 && checkForValidPrerelease(*operand, *version); + } + break; + case kSemanticOpLessThan: { + const auto operand = *iter++; + matched = matched && version->compare(*operand) < 0 && checkForValidPrerelease(*operand, *version); + } + break; + case kSemanticOpLessThanOrEquals: { + const auto operand = *iter++; + matched = matched && version->compare(*operand) <= 0 && checkForValidPrerelease(*operand, *version); + } + break; + case kSemanticOpOr: + if (matched) + return true; + matched = true; // Start over again assuming a true match + break; + } + } + + return matched; +} + +std::string +SemanticPattern::toDebugString() const +{ + static std::vector OPS_STRINGS = { "=", ">", ">=", "<", "<=", "||" }; + std::string result; + auto iterOperands = mVersions.begin(); + + for (const auto& op : mOperators) { + if (!result.empty()) + result += ' '; + result += OPS_STRINGS[op]; + if (op != kSemanticOpOr) { + result += (*iterOperands)->toDebugString(); + iterOperands++; + } + } + + return result; +} + + +} // namespace apl \ No newline at end of file diff --git a/aplcore/src/versioning/semanticversion.cpp b/aplcore/src/versioning/semanticversion.cpp new file mode 100644 index 0000000..705683f --- /dev/null +++ b/aplcore/src/versioning/semanticversion.cpp @@ -0,0 +1,127 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include + +#include "apl/versioning/semanticgrammar.h" +#include "apl/versioning/semanticversion.h" +#include "apl/utils/session.h" + +namespace apl { + +namespace pegtl = tao::TAO_PEGTL_NAMESPACE; + +// Convenience function to extract a pointer to the start of an embedded string +static const char *encodedStart(const std::string& string, uint32_t value) +{ + assert(svgrammar::isEncodedString(value)); + return string.c_str() + svgrammar::encodedOffset(value); +} + +// Convenience function to retrieve an embedded string +static std::string encodedToString(const std::string& string, uint32_t value) +{ + if (svgrammar::isEncodedString(value)) + return '\'' + string.substr(svgrammar::encodedOffset(value), svgrammar::encodedLen(value)) + '\''; + else + return std::to_string(value); +} + +SemanticVersionPtr +SemanticVersion::create(const SessionPtr& session, const std::string& string) +{ + if (string.size() <= 0) { + CONSOLE(session) << "Empty version string"; + return nullptr; + } + + if (string.size() > std::numeric_limits::max()) { + CONSOLE(session) << "Version string too long"; + return nullptr; + } + + pegtl::memory_input<> in(string, ""); + svgrammar::semvar_state state(in); + + if (!pegtl::parse(in, state) || + state.failed) { + CONSOLE(session) << "Error parsing semantic version '" << string << "', " << state.what(); + return nullptr; + } + + assert(state.elements.size() >= 3); + return std::make_shared(std::move(state.elements), std::move(state.string)); +} + +bool +SemanticVersion::versionMatch(const SemanticVersion& other) const +{ + assert(mElements.size() >= 3); + assert(other.mElements.size() >= 3); + return mElements.at(0) == other.mElements.at(0) && + mElements.at(1) == other.mElements.at(1) && + mElements.at(2) == other.mElements.at(2); +} + +std::string +SemanticVersion::toDebugString() const +{ + std::string result; + for (const auto& m : mElements) { + if (!result.empty()) + result += "."; + result += encodedToString(mString, m); + } + return result; +} + +int +SemanticVersion::compare(const SemanticVersion& other) const +{ + const auto selfCount = mElements.size(); + const auto otherCount = other.mElements.size(); + const auto maxCount = std::min(selfCount, otherCount); + for (auto i = 0; i < maxCount; i++) { + const auto& a = mElements.at(i); + const auto& b = other.mElements.at(i); + if (!svgrammar::isEncodedString(a)) { // This is a number + if (svgrammar::isEncodedString(b)) return -1; // Numbers are less than strings + // Numeric comparison + if (a < b) return -1; + if (a > b) return +1; + } else { + if (!svgrammar::isEncodedString(b)) return 1; // Strings are bigger than numbers + // String comparison + const char *ptr_a = encodedStart(mString, a); + const char *ptr_b = encodedStart(other.mString, b); + const auto len_a = svgrammar::encodedLen(a); + const auto len_b = svgrammar::encodedLen(b); + const auto jmax = std::min(len_a, len_b); + for (auto j = 0; j < jmax; j++) { + if (*ptr_a < *ptr_b) return -1; + if (*ptr_a > *ptr_b) return +1; + ++ptr_a; + ++ptr_b; + } + if (len_a < len_b) return -1; + if (len_a > len_b) return +1; + } + } + if (selfCount < otherCount) return (selfCount > 3 ? -1 : +1); // "1.0.0-rc" < "1.0.0-rc.3" < "1.0.0" + if (selfCount > otherCount) return (otherCount > 3 ? +1 : -1); + return 0; +} + +} // namespace apl \ No newline at end of file diff --git a/aplcore/unit/CMakeLists.txt b/aplcore/unit/CMakeLists.txt index 44c33de..b46d4a4 100644 --- a/aplcore/unit/CMakeLists.txt +++ b/aplcore/unit/CMakeLists.txt @@ -17,6 +17,7 @@ message("Adding APL Core unit test target") # Google testing add_executable( unittest + test_sg_textmeasure.cpp testeventloop.cpp debugtools.cpp unittest_simpletextmeasurement.cpp @@ -46,9 +47,11 @@ add_subdirectory(livedata) add_subdirectory(media) add_subdirectory(primitives) add_subdirectory(scaling) +add_subdirectory(text) add_subdirectory(time) add_subdirectory(touch) add_subdirectory(utils) +add_subdirectory(versioning) if(ENABLE_SCENEGRAPH) add_subdirectory(scenegraph) endif(ENABLE_SCENEGRAPH) diff --git a/aplcore/unit/command/CMakeLists.txt b/aplcore/unit/command/CMakeLists.txt index e9845f6..2dd2335 100644 --- a/aplcore/unit/command/CMakeLists.txt +++ b/aplcore/unit/command/CMakeLists.txt @@ -18,6 +18,7 @@ target_sources_local(unittest unittest_command_array.cpp unittest_command_document.cpp unittest_command_event_binding.cpp + unittest_command_importpackage.cpp unittest_command_insertitem.cpp unittest_command_log.cpp unittest_command_macros.cpp diff --git a/aplcore/unit/command/unittest_command_importpackage.cpp b/aplcore/unit/command/unittest_command_importpackage.cpp new file mode 100644 index 0000000..a81672d --- /dev/null +++ b/aplcore/unit/command/unittest_command_importpackage.cpp @@ -0,0 +1,700 @@ +/** + * 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 "../content/testpackagemanager.h" + +using namespace apl; + +class ImportPackageTest: public CommandTest{}; + +static const char *IMPORT_PACKAGE_COMMAND_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": [], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "ImportPackage", + "name": "packageName", + "version": "1.0", + "source": "sourceUri" + } + ], + "items": [ + { + "type": "Text", + "text": "ImportPackage test template" + } + ] + }, + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "${@testStringImport}" + } + } + ], + "items": [ + { + "type": "Text", + "text": "insertItem runner" + } + ] + } + ] + } + } +} +)"; + +static const char *PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2023.3", + "description": "test package definition", + "resources": [ + { + "string": { + "testStringImport": "wow, nice string" + } + } + ], + "import": [], + "layouts": {} +} +)"; + +TEST_F(ImportPackageTest, SuccessfulPackageImport) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + config->packageManager(testPackageManager); + createContent(IMPORT_PACKAGE_COMMAND_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); + + // Trigger InsertItem - using content from dynamically loaded package + performClick(0, 10); + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("wow, nice string", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); +} + +static const char *IMPORT_PACKAGE_WITH_ONFAIL_ONLOAD_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": [ + { + "type": "ImportPackage", + "name": "packageName", + "version": "1.0", + "source": "sourceUri", + "onFail": [ + { + "type": "Log", + "message": "onFail handler command", + "arguments": [ + "${event.value}", + "${event.error}", + "${event.errorCode}" + ] + } + ], + "onLoad": [ + { + "type": "Log", + "message": "onLoad handler command", + "arguments": [ + "${event.version}" + ] + } + ], + "accept": ">0.1.10-beta.3" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [] + } + } +} +)"; + +TEST_F(ImportPackageTest, onFail) { + auto testPackageManager = std::make_shared(); + config->packageManager(testPackageManager); + createContent(IMPORT_PACKAGE_WITH_ONFAIL_ONLOAD_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + testPackageManager->fail(testPackageManager->get("packageName:1.0")); + loop->advanceToEnd(); + + ASSERT_EQ(1, session->logCommandMessages.size()); + + auto m = session->logCommandMessages[0]; + ASSERT_EQ("onFail handler command", m.text); + ASSERT_NE("onLoad handler command", m.text); + auto args = m.arguments.getArray(); + ASSERT_EQ("packageName:1.0:sourceUri", args[0].getString()); + ASSERT_EQ("Package not found.", args[1].getString()); + ASSERT_EQ(404, args[2].getInteger()); +} + +TEST_F(ImportPackageTest, onLoad) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + config->packageManager(testPackageManager); + + createContent(IMPORT_PACKAGE_WITH_ONFAIL_ONLOAD_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + loop->advanceToEnd(); + + auto m = session->logCommandMessages[0]; + ASSERT_EQ("onLoad handler command", m.text); + ASSERT_NE("onFail handler command", m.text); + ASSERT_EQ("1.0", m.arguments.getArray()[0].getString()); +} + +static const char *IMPORT_PACKAGE_WITH_MULTIPLE_SAME_IMPORTS_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": 100, + "height": 100, + "bind": [ + { "name": "I", "value": 0 } + ], + "onPress": [ + { + "type": "SetValue", + "property": "I", + "value": "${I + 1}" + }, + { + "type": "ImportPackage", + "name": "packageName", + "version": "1.0", + "sequencer": "DynamicLoader_${I}", + "source": "sourceUri", + "onFail": [ + { + "type": "Log", + "message": "onFail handler command" + } + ], + "onLoad": [ + { + "type": "Log", + "message": "onLoad handler command" + } + ], + "accept": ">0.1.10-beta.3" + } + ] + } + ] + } + } +} +)"; + +TEST_F(ImportPackageTest, multipleSameImports) { + auto testPackageManager = std::make_shared(); + config->packageManager(testPackageManager); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + createContent(IMPORT_PACKAGE_WITH_MULTIPLE_SAME_IMPORTS_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + loop->advanceToEnd(); + + performTap(1, 1); + loop->advanceToEnd(); + performTap(1, 1); + loop->advanceToEnd(); + performTap(1, 1); + loop->advanceToEnd(); + + ASSERT_EQ(3, session->logCommandMessages.size()); + for (const auto& logCommandMessage : session->logCommandMessages) { + ASSERT_EQ("onLoad handler command", logCommandMessage.text); + } +} + +static const char *IMPORT_PACKAGE_WITH_DIAMOND_DEPENDENCY_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": { + "type": "Parallel", + "commands": [ + { + "type": "ImportPackage", + "name": "A", + "version": "1.0", + "onFail": [ + { + "type": "Log", + "message": "onFail handler command A" + } + ], + "onLoad": [ + { + "type": "SetValue", + "componentId": "A", + "property": "text", + "value": "${@A}" + } + ], + "accept": ">0.1.10-beta.3" + }, + { + "type": "ImportPackage", + "name": "B", + "version": "1.0", + "onFail": [ + { + "type": "Log", + "message": "onFail handler command B" + } + ], + "onLoad": [ + { + "type": "SetValue", + "componentId": "B", + "property": "text", + "value": "${@B}" + } + ], + "accept": ">0.1.10-beta.3" + } + ] + }, + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "Text", + "id": "A" + }, + { + "type": "Text", + "id": "B" + } + ] + } + } +} +)"; + +const char *PACKAGE_A = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "C", + "version": "1.0" + } + ], + "resources": [ + { + "strings": { + "A": "This is A" + } + } + ] +})apl"; + +const char *PACKAGE_B = R"apl({ + "type": "APL", + "version": "1.1", + "import": [ + { + "name": "C", + "version": "1.0" + } + ], + "resources": [ + { + "strings": { + "B": "This is B" + } + } + ] +})apl"; + +const char *PACKAGE_C = R"apl({ + "type": "APL", + "version": "1.1", + "resources": [ + { + "strings": { + "C": "This is C" + } + } + ] +})apl"; + +TEST_F(ImportPackageTest, DiamondDependencyPackageImport) { + auto testPackageManager = std::make_shared(); + config->packageManager(testPackageManager); + + createContent(IMPORT_PACKAGE_WITH_DIAMOND_DEPENDENCY_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + loop->advanceToEnd(); + + testPackageManager->succeed(testPackageManager->get("A:1.0"), SharedJsonData(PACKAGE_A)); + testPackageManager->succeed(testPackageManager->get("B:1.0"), SharedJsonData(PACKAGE_B)); + + auto top = root->topComponent(); + // Request of C still pending, onLoad pending, no text is displayed + ASSERT_EQ("", top->getChildAt(0)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("", top->getChildAt(1)->getCalculated(apl::kPropertyText).asString()); + + testPackageManager->succeed(testPackageManager->get("C:1.0"), SharedJsonData(PACKAGE_C)); + testPackageManager->succeed(testPackageManager->get("C:1.0"), SharedJsonData(PACKAGE_C)); + loop->advanceToEnd(); + + ASSERT_EQ("This is A", top->getChildAt(0)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("This is B", top->getChildAt(1)->getCalculated(apl::kPropertyText).asString()); +} + +TEST_F(ImportPackageTest, NoPackageManager) { + loadDocument(IMPORT_PACKAGE_WITH_ONFAIL_ONLOAD_DOC); + + loop->advanceToEnd(); + + ASSERT_EQ(1, session->logCommandMessages.size()); + + auto m = session->logCommandMessages[0]; + ASSERT_EQ("onFail handler command", m.text); + ASSERT_NE("onLoad handler command", m.text); +} + +TEST_F(ImportPackageTest, MultipleSameImportsDoesntReprocess) { + auto testPackageManager = std::make_shared(); + + config->packageManager(testPackageManager); + createContent(IMPORT_PACKAGE_COMMAND_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); + + ASSERT_EQ(1, testPackageManager->getUnresolvedRequests().size()); + testPackageManager->succeed(testPackageManager->get("packageName:1.0"), SharedJsonData(PACKAGE_JSON)); + + // Send it again and we shouldn't re-trigger a load since it's cached + performClick(0, 0); + loop->advanceToEnd(); + ASSERT_EQ(0, testPackageManager->getUnresolvedRequests().size()); + + // Trigger InsertItem - using content from dynamically loaded package + performClick(0, 10); + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("wow, nice string", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); +} + +static const char *IMPORT_PACKAGE_FAST_MODE = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": [], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "onDown": [ + { + "type": "ImportPackage", + "name": "packageName", + "version": "1.0", + "source": "sourceUri" + } + ], + "items": [ + { + "type": "Text", + "text": "ImportPackage test template" + } + ] + }, + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "${@testStringImport}" + } + } + ], + "items": [ + { + "type": "Text", + "text": "insertItem runner" + } + ] + } + ] + } + } +} +)"; + +TEST_F(ImportPackageTest, ImportPackageIgnoredInFastMode) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + config->packageManager(testPackageManager); + createContent(IMPORT_PACKAGE_FAST_MODE, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command which is ignored + performClick(0, 0); + loop->advanceToEnd(); + + ASSERT_EQ(0, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(0, testPackageManager->getUnresolvedRequests().size()); + ASSERT_TRUE(session->checkAndClear()); + + // Trigger InsertItem + performClick(0, 10); + loop->advanceToEnd(); + + // Verify resource didn't load + auto top = root->topComponent(); + ASSERT_EQ("", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); +} + +static const char *DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "Container" + } + } +} +)"; + +TEST_F(ImportPackageTest, ImportPackageCommandMissingRequired) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + config->packageManager(testPackageManager); + createContent(DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + auto commands = Object(JsonData(R"( + [{ + "type": "ImportPackage", + "version": "1.0", + "source": "sourceUri" + }])").moveToObject()); + executeCommands(commands, false); + loop->advanceToEnd(); + + ASSERT_EQ(0, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(0, testPackageManager->getUnresolvedRequests().size()); + ASSERT_TRUE(session->checkAndClear()); + + commands = Object(JsonData(R"( + [{ + "type": "ImportPackage", + "name": "packageName", + "source": "sourceUri" + }])").moveToObject()); + executeCommands(commands, false); + loop->advanceToEnd(); + + ASSERT_EQ(0, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(0, testPackageManager->getUnresolvedRequests().size()); + ASSERT_TRUE(session->checkAndClear()); +} + +static const char *BAD_PACKAGE = R"( +{ + "type": "APL", + "version": "2024.1", + "import": "improper imports" +} +)"; + +TEST_F(ImportPackageTest, BadPackageFailsCommand) +{ + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", BAD_PACKAGE); + + config->packageManager(testPackageManager); + createContent(DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + auto commands = Object(JsonData(R"( + [{ + "type": "ImportPackage", + "version": "1.0", + "name": "packageName", + "source": "sourceUri", + "onFail": [ + { + "type": "Log", + "message": "onFail handler command", + "arguments": [ + "${event.value}", + "${event.error}", + "${event.errorCode}" + ] + } + ] + }])").moveToObject()); + executeCommands(commands, false); + loop->advanceToEnd(); + + ASSERT_EQ(1, session->logCommandMessages.size()); + + auto m = session->logCommandMessages[0]; + ASSERT_EQ("onFail handler command", m.text); + ASSERT_EQ("Document import property should be an array", m.arguments.getArray()[1].getString()); + ASSERT_TRUE(session->checkAndClear()); +} + +static const char *IMPORT_BAD = R"( +{ + "type": "APL", + "version": "2024.1", + "import": [ + { + "name": "bad", + "version": "2.0" + } + ] +} +)"; + +TEST_F(ImportPackageTest, NestedBadPackageFailsCommand) +{ + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", IMPORT_BAD); + testPackageManager->putPackage("bad:2.0", BAD_PACKAGE); + + config->packageManager(testPackageManager); + createContent(DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + auto commands = Object(JsonData(R"( + [{ + "type": "ImportPackage", + "version": "1.0", + "name": "packageName", + "source": "sourceUri", + "onFail": [ + { + "type": "Log", + "message": "onFail handler command", + "arguments": [ + "${event.value}", + "${event.error}", + "${event.errorCode}" + ] + } + ] + }])").moveToObject()); + executeCommands(commands, false); + loop->advanceToEnd(); + + ASSERT_EQ(1, session->logCommandMessages.size()); + + auto m = session->logCommandMessages[0]; + ASSERT_EQ("onFail handler command", m.text); + ASSERT_EQ("bad:2.0:", m.arguments.getArray()[0].getString()); + ASSERT_EQ("Document import property should be an array", m.arguments.getArray()[1].getString()); + ASSERT_TRUE(session->checkAndClear()); +} \ No newline at end of file diff --git a/aplcore/unit/command/unittest_serialize_event.cpp b/aplcore/unit/command/unittest_serialize_event.cpp index b859e06..651e6b1 100644 --- a/aplcore/unit/command/unittest_serialize_event.cpp +++ b/aplcore/unit/command/unittest_serialize_event.cpp @@ -754,7 +754,9 @@ TEST_F(SerializeEventTest, ScrollViewDocument) { "value": 0.5, "handler": "Scroll", "uid": "[EXISTS]", - "position": 0.5 + "position": 0.5, + "allowForward": true, + "allowBackwards": true } } )")); @@ -824,7 +826,9 @@ TEST_F(SerializeEventTest, GridSequenceScrollEvent) { "firstVisibleChild": 1, "firstFullyVisibleChild": 2, "lastFullyVisibleChild": 4, - "lastVisibleChild": 5 + "lastVisibleChild": 5, + "allowForward": true, + "allowBackwards": true } } )")); @@ -854,7 +858,9 @@ TEST_F(SerializeEventTest, GridSequenceScrollEvent) { "firstVisibleChild": 6, "firstFullyVisibleChild": 6, "lastFullyVisibleChild": 9, - "lastVisibleChild": 9 + "lastVisibleChild": 9, + "allowForward": true, + "allowBackwards": true } } )")); @@ -885,7 +891,9 @@ TEST_F(SerializeEventTest, GridSequenceScrollEvent) { "firstVisibleChild": 8, "firstFullyVisibleChild": 8, "lastFullyVisibleChild": 11, - "lastVisibleChild": 11 + "lastVisibleChild": 11, + "allowForward": false, + "allowBackwards": true } } )")); @@ -973,7 +981,9 @@ TEST_F(SerializeEventTest, GridSequenceOpacityUpEvent) { "firstVisibleChild": -1, "firstFullyVisibleChild": -1, "lastFullyVisibleChild": -1, - "lastVisibleChild": -1 + "lastVisibleChild": -1, + "allowForward": true, + "allowBackwards": false } } )")); @@ -1005,7 +1015,9 @@ TEST_F(SerializeEventTest, GridSequenceOpacityUpEvent) { "firstVisibleChild": 0, "firstFullyVisibleChild": 0, "lastFullyVisibleChild": 3, - "lastVisibleChild": 3 + "lastVisibleChild": 3, + "allowForward": true, + "allowBackwards": false } } )")); @@ -1076,7 +1088,9 @@ TEST_F(SerializeEventTest, GridSequenceZeroOpacityScrollEvent) { "firstVisibleChild": -1, "firstFullyVisibleChild": -1, "lastFullyVisibleChild": -1, - "lastVisibleChild": -1 + "lastVisibleChild": -1, + "allowForward": true, + "allowBackwards": true } } )")); @@ -1146,7 +1160,9 @@ TEST_F(SerializeEventTest, GridSequenceMultiChildEvent) { "firstVisibleChild": 0, "firstFullyVisibleChild": 4, "lastFullyVisibleChild": 7, - "lastVisibleChild": 9 + "lastVisibleChild": 9, + "allowForward": true, + "allowBackwards": true } } )")); @@ -1209,7 +1225,9 @@ TEST_F(SerializeEventTest, SequenceDocument) { "firstVisibleChild": 1, "firstFullyVisibleChild": 1, "lastFullyVisibleChild": 1, - "lastVisibleChild": 2 + "lastVisibleChild": 2, + "allowForward": true, + "allowBackwards": true } } )")); @@ -2116,7 +2134,9 @@ TEST_F(SerializeEventTest, TargetScrollView) { "pressed": false, "type": "ScrollView", "uid": "[EXISTS]", - "width": 100.0 + "width": 100.0, + "allowForward": true, + "allowBackwards": true } } )")); @@ -2217,7 +2237,9 @@ TEST_F(SerializeEventTest, TargetSequence) { "firstVisibleChild": 2, "firstFullyVisibleChild": -1, "lastFullyVisibleChild": -1, - "lastVisibleChild": 3 + "lastVisibleChild": 3, + "allowForward": true, + "allowBackwards": true } } )")); diff --git a/aplcore/unit/component/unittest_component_events.cpp b/aplcore/unit/component/unittest_component_events.cpp index 7f4e5c8..5c1aa79 100644 --- a/aplcore/unit/component/unittest_component_events.cpp +++ b/aplcore/unit/component/unittest_component_events.cpp @@ -1214,7 +1214,8 @@ static const char *CHILDREN_CHANGED = R"apl({ "arguments": [ "${event.source.handler}", "${data.index ? data.index : 0}", - "${data.action}" + "${data.action}", + "${event.length}" ], "components": [ "textComp" ] } @@ -1247,14 +1248,14 @@ TEST_F(ComponentEventsTest, ChildrenChanged) root->clearPending(); advanceTime(500); - ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "insert")); + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "insert", 1)); executeCommands(apl::Object(doc), false); root->clearPending(); advanceTime(500); - ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 1, "insert")); + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 1, "insert", 2)); executeCommands(apl::Object(doc), false); @@ -1272,9 +1273,10 @@ TEST_F(ComponentEventsTest, ChildrenChanged) root->clearPending(); advanceTime(500); - ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 1, "insert")); - ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 2, "insert")); - ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "remove")); + // Single event group + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 1, "insert", 3)); + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 2, "insert", 3)); + ASSERT_TRUE(CheckSendEvent(root, "ChildrenChanged", 0, "remove", 3)); } /** diff --git a/aplcore/unit/component/unittest_edit_text_component.cpp b/aplcore/unit/component/unittest_edit_text_component.cpp index 29f7e61..3adda4d 100644 --- a/aplcore/unit/component/unittest_edit_text_component.cpp +++ b/aplcore/unit/component/unittest_edit_text_component.cpp @@ -1006,4 +1006,37 @@ TEST_F(EditTextComponentTest, ComponentDisplayedWhenDisallowEditTextFalse) { // Displayed ASSERT_EQ(1, component->getDisplayedChildCount()); ASSERT_EQ(kComponentTypeEditText, component->getDisplayedChildAt(0)->getType()); +} + +static const char* EDIT_TEXT_AUTOSIZED = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "direction": "row", + "items": [ + { + "type": "EditText", + "id": "EDITTEXT", + "text": "hello", + "size": 8, + "shrink": 1.0 + } + ] + } + } +})"; + +TEST_F(EditTextComponentTest, EditTextAutosize) { + loadDocument(EDIT_TEXT_AUTOSIZED); + + auto et = root->findComponentById("EDITTEXT"); + ASSERT_EQ(Size(80, 800), et->getCalculated(apl::kPropertyBounds).get().getSize()); + + // Change FontSize and ensure resize happened + executeCommands(JsonData(R"([{ "type": "SetValue", "componentId": "EDITTEXT", "property": "fontSize", "value": 60 }])").moveToObject(), false); + advanceTime(100); + + ASSERT_EQ(Size(120, 800), et->getCalculated(apl::kPropertyBounds).get().getSize()); } \ No newline at end of file diff --git a/aplcore/unit/component/unittest_host_component.cpp b/aplcore/unit/component/unittest_host_component.cpp index 2c57674..fc253a2 100644 --- a/aplcore/unit/component/unittest_host_component.cpp +++ b/aplcore/unit/component/unittest_host_component.cpp @@ -1107,3 +1107,84 @@ TEST_F(HostComponentTest, ExperimentalFeaturesCopiedCorrectly) { ASSERT_EQ(hostExperimentalFeatures, childExperimentalFeatures); } +TEST_F(HostComponentTest, DefaultBackgroundPropertyAppliedToEmbedded) { + nominalLoadHostAndEmbedded(); + ASSERT_TRUE(host); + + auto content = Content::create(EMBEDDED_DEFAULT, makeDefaultSession(), metrics, *config); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + ASSERT_TRUE(IsEqual(Color(), host->getCalculated(kPropertyBackground))); +} + +static const char* BLUE_BACKGROUND_EMBEDDED = R"({ + "type": "APL", + "version": "2022.3", + "background": "blue", + "mainTemplate": { + "item": { + "type": "Container", + "id": "embeddedTop", + "item": { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + } + } + } +})"; + +TEST_F(HostComponentTest, ColorBackgroundPropertyAppliedToEmbedded) { + loadDocument(); + ASSERT_TRUE(host); + + auto content = Content::create(BLUE_BACKGROUND_EMBEDDED, makeDefaultSession(), metrics, *config); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + ASSERT_TRUE(IsEqual(Color(Color::BLUE), host->getCalculated(kPropertyBackground))); +} + +static const char* GRADIENT_BACKGROUND_EMBEDDED = R"({ + "type": "APL", + "version": "2022.3", + "background": { + "type": "linear", + "colorRange": [ + "blue", + "white" + ], + "inputRange": [ + 0, + 1 + ], + "angle": 90 + }, + "mainTemplate": { + "item": { + "type": "Container", + "id": "embeddedTop", + "item": { + "type": "Text", + "id": "embeddedText", + "value": "Hello, World!" + } + } + } +})"; + +TEST_F(HostComponentTest, GradientBackgroundPropertyAppliedToEmbedded) { + loadDocument(); + ASSERT_TRUE(host); + + auto content = Content::create(GRADIENT_BACKGROUND_EMBEDDED, makeDefaultSession(), metrics, *config); + auto embeddedDoc = documentManager->succeed("embeddedDocumentUrl", content, true); + + auto background = host->getCalculated(kPropertyBackground); + ASSERT_TRUE(background.is()); + + auto gradient = background.get(); + ASSERT_EQ(Gradient::GradientType::LINEAR, gradient.getType()); + ASSERT_EQ(90, gradient.getProperty(kGradientPropertyAngle).getInteger()); + ASSERT_EQ(std::vector({Color(Color::BLUE), Color(Color::WHITE)}), gradient.getProperty(kGradientPropertyColorRange).getArray()); + ASSERT_EQ(std::vector({0.0, 1.0}), gradient.getProperty(kGradientPropertyInputRange).getArray()); +} \ No newline at end of file diff --git a/aplcore/unit/component/unittest_serialize.cpp b/aplcore/unit/component/unittest_serialize.cpp index 96948a0..2daf74b 100644 --- a/aplcore/unit/component/unittest_serialize.cpp +++ b/aplcore/unit/component/unittest_serialize.cpp @@ -417,6 +417,7 @@ const static char *SERIALIZE_ALL_RESULT = R"({ "_actions": [], "action": [], "accessibilityLabel": "", + "_background": "#00000000", "_bounds": [ 0, 0, @@ -457,8 +458,10 @@ const static char *SERIALIZE_ALL_RESULT = R"({ "minHeight": 0, "minWidth": 0, "onChildrenChanged": [], + "onLayout": [], "onMount": [], "onSpeechMark": [], + "onTextLayout": [], "opacity": 1, "padding": [], "paddingBottom": null, @@ -468,6 +471,7 @@ const static char *SERIALIZE_ALL_RESULT = R"({ "paddingTop": null, "paddingStart": null, "preserve": [], + "pointerEvents": "auto", "role": "none", "shadowColor": "#00000000", "shadowHorizontalOffset": 0, diff --git a/aplcore/unit/content/CMakeLists.txt b/aplcore/unit/content/CMakeLists.txt index 3a0cab6..25721aa 100644 --- a/aplcore/unit/content/CMakeLists.txt +++ b/aplcore/unit/content/CMakeLists.txt @@ -13,12 +13,15 @@ target_sources_local(unittest PRIVATE + testpackagemanager.cpp unittest_apl.cpp unittest_directive.cpp unittest_document.cpp unittest_document_background.cpp unittest_jsondata.cpp unittest_metrics.cpp + unittest_packagemanager.cpp unittest_packages.cpp unittest_rootconfig.cpp + unittest_sharedjsondata.cpp ) \ No newline at end of file diff --git a/aplcore/unit/content/packagegenerator.h b/aplcore/unit/content/packagegenerator.h new file mode 100644 index 0000000..980d559 --- /dev/null +++ b/aplcore/unit/content/packagegenerator.h @@ -0,0 +1,73 @@ +/** + * 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_PACKAGEGENERATOR_H +#define APL_PACKAGEGENERATOR_H + +#include "rapidjson/rapidjson.h" +#include +#include +#include + +static std::string +makeTestPackage(const std::vector& dependencies, std::map stringMap) +{ + const char *FAKE_MAIN_TEMPLATE = R"apl({ + "item": { + "type": "Text" + } + })apl"; + + rapidjson::Document doc; + doc.SetObject(); + auto& allocator = doc.GetAllocator(); + + doc.AddMember("type", "APL", allocator); + doc.AddMember("version", "1.1", allocator); + + // Add imports + rapidjson::Value imports(rapidjson::kArrayType); + for (const char *it : dependencies) { + rapidjson::Value importBlock(rapidjson::kObjectType); + importBlock.AddMember("name", rapidjson::StringRef(it), allocator); + importBlock.AddMember("version", "1.0", allocator); + imports.PushBack(importBlock, allocator); + } + doc.AddMember("import", imports, allocator); + + // Add resources + rapidjson::Value resources(rapidjson::kArrayType); + rapidjson::Value resourceBlock(rapidjson::kObjectType); + rapidjson::Value resourceStrings(rapidjson::kObjectType); + for (auto it : stringMap) + resourceStrings.AddMember(rapidjson::StringRef(it.first), rapidjson::StringRef(it.second), allocator); + resourceBlock.AddMember("strings", resourceStrings, allocator); + resources.PushBack(resourceBlock, allocator); + doc.AddMember("resources", resources, allocator); + + // Add a mainTemplate section (just in case) + rapidjson::Document mainTemplate; + mainTemplate.Parse(FAKE_MAIN_TEMPLATE); + doc.AddMember("mainTemplate", mainTemplate, allocator); + + // Convert to a string + rapidjson::StringBuffer buffer; + buffer.Clear(); + rapidjson::Writer writer(buffer); + doc.Accept(writer); + return {buffer.GetString()}; +} + +#endif // APL_PACKAGEGENERATOR_H diff --git a/aplcore/unit/content/testpackagemanager.cpp b/aplcore/unit/content/testpackagemanager.cpp new file mode 100644 index 0000000..1b8c649 --- /dev/null +++ b/aplcore/unit/content/testpackagemanager.cpp @@ -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. + */ + +#include "testpackagemanager.h" + +#include + +using namespace apl; + +void +TestPackageManager::loadPackage(const PackageRequestPtr& packageRequest) +{ + requests.push_back(packageRequest); + auto packageIt = packages.find(packageRequest->request().reference().toString()); + if (packageIt != packages.end()) { + succeed(packageRequest->request(), SharedJsonData(packageIt->second)); + } +} + +void +TestPackageManager::succeed(const ImportRequest& request, SharedJsonData&& jsonData) +{ + PackageRequestPtr packageRequest = nullptr; + for (auto &it : requests) { + if (it->request() == request) { + packageRequest = it; + break; + } + } + + if (packageRequest) { + resolvedRequests.push_back(request); + requests.erase(std::remove(requests.begin(), requests.end(), packageRequest), requests.end()); + packageRequest->succeed(std::move(jsonData)); + } +} + +void +TestPackageManager::fail(const apl::ImportRequest& request) +{ + PackageRequestPtr packageRequest = nullptr; + for (auto &it : requests) { + if (it->request() == request) { + packageRequest = it; + break; + } + } + + if (packageRequest) { + resolvedRequests.push_back(request); + requests.erase(std::remove(requests.begin(), requests.end(), packageRequest), requests.end()); + packageRequest->fail("Package not found.", 404); + } +} + +ImportRequest +TestPackageManager::get(const std::string& packageName) const +{ + for (const auto & request : requests) { + if (request->request().reference().toString() == packageName) { + return request->request(); + } + } + + return {}; +} + diff --git a/aplcore/unit/content/testpackagemanager.h b/aplcore/unit/content/testpackagemanager.h new file mode 100644 index 0000000..e2c11e4 --- /dev/null +++ b/aplcore/unit/content/testpackagemanager.h @@ -0,0 +1,57 @@ +/** +* Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +* +* Licensed under the Apache License, Version 2.0 (the "License"). +* You may not use this file except in compliance with the License. +* A copy of the License is located at +* +* http://aws.amazon.com/apache2.0/ +* +* or in the "license" file accompanying this file. This file is distributed +* on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +* express or implied. See the License for the specific language governing +* permissions and limitations under the License. + */ + +#ifndef APL_TEST_PACKAGE_MANAGER_H +#define APL_TEST_PACKAGE_MANAGER_H + +#include +#include +#include +#include + +#include "apl/content/packagemanager.h" + +namespace apl +{ + +class TestPackageManager : public PackageManager +{ +public: + void loadPackage(const PackageRequestPtr& packageRequest) override; + + void putPackage(const std::string& packageName, const std::string& packageData) { + packages.emplace(packageName, packageData); + } + + void succeed(const ImportRequest &request, + SharedJsonData&& jsonData); + + void fail(const ImportRequest &request); + + ImportRequest get(const std::string &packageName) const; + + const std::vector& getUnresolvedRequests() const { return requests; } + + int getResolvedRequestCount() const { return resolvedRequests.size(); } + +private: + std::map packages; + std::vector requests; + std::vector resolvedRequests; +}; + +} // namespace apl + +#endif // APL_TEST_PACKAGE_MANAGER_H diff --git a/aplcore/unit/content/unittest_document.cpp b/aplcore/unit/content/unittest_document.cpp index 59b68b0..2a2071c 100644 --- a/aplcore/unit/content/unittest_document.cpp +++ b/aplcore/unit/content/unittest_document.cpp @@ -25,6 +25,8 @@ #include "apl/engine/context.h" #include "apl/engine/rootcontext.h" +#include "packagegenerator.h" + using namespace apl; const char *BASIC_DOC = R"apl({ @@ -35,7 +37,8 @@ const char *BASIC_DOC = R"apl({ "payload" ], "item": { - "type": "Text" + "type": "Text", + "text": "${payload}" } } })apl"; @@ -59,6 +62,8 @@ TEST(DocumentTest, Load) ASSERT_TRUE(doc); ASSERT_EQ(15000, content->getDocumentSettings()->idleTimeout(config)); + + ASSERT_EQ("duck", doc->topComponent()->getCalculated(kPropertyText).asString()); } const char *BASIC_DOC_NO_TYPE_FIELD = R"apl({ @@ -396,8 +401,12 @@ TEST(DocumentTest, IncompatibleImportVersion) ASSERT_EQ("1.1", content->getAPLVersion()); + content->addData("payload", "{}"); + auto config = RootConfig(); + config.enforceAPLVersion(APLVersion::kAPLVersionAny); + auto m = Metrics().size(1024,800).theme("dark"); - auto doc = RootContext::create(m, content); + auto doc = RootContext::create(m, content, config); ASSERT_FALSE(doc); } @@ -674,7 +683,7 @@ TEST(DocumentTest, MultipleDependencies) ASSERT_EQ(Object("Original_B"), context->opt("@overwrite_B")); ASSERT_EQ(Object("B"), context->opt("@overwrite_C")); - auto expected = std::vector{ "A:2.2", "B:1.0", "C:1.5" }; + auto expected = std::vector{ "C:1.5", "A:2.2", "B:1.0" }; ASSERT_EQ(expected, doc->getLoadedPackageNames()); } @@ -767,55 +776,6 @@ TEST(DocumentTest, Duplicate) ASSERT_EQ(expected, doc->getLoadedPackageNames()); } -const char *FAKE_MAIN_TEMPLATE = R"apl({ - "item": { - "type": "Text" - } -})apl"; - -static std::string -makeTestPackage(const std::vector& dependencies, std::map stringMap) -{ - rapidjson::Document doc; - doc.SetObject(); - auto& allocator = doc.GetAllocator(); - - doc.AddMember("type", "APL", allocator); - doc.AddMember("version", "1.1", allocator); - - // Add imports - rapidjson::Value imports(rapidjson::kArrayType); - for (const char *it : dependencies) { - rapidjson::Value importBlock(rapidjson::kObjectType); - importBlock.AddMember("name", rapidjson::StringRef(it), allocator); - importBlock.AddMember("version", "1.0", allocator); - imports.PushBack(importBlock, allocator); - } - doc.AddMember("import", imports, allocator); - - // Add resources - rapidjson::Value resources(rapidjson::kArrayType); - rapidjson::Value resourceBlock(rapidjson::kObjectType); - rapidjson::Value resourceStrings(rapidjson::kObjectType); - for (auto it : stringMap) - resourceStrings.AddMember(rapidjson::StringRef(it.first), rapidjson::StringRef(it.second), allocator); - resourceBlock.AddMember("strings", resourceStrings, allocator); - resources.PushBack(resourceBlock, allocator); - doc.AddMember("resources", resources, allocator); - - // Add a mainTemplate section (just in case) - rapidjson::Document mainTemplate; - mainTemplate.Parse(FAKE_MAIN_TEMPLATE); - doc.AddMember("mainTemplate", mainTemplate, allocator); - - // Convert to a string - rapidjson::StringBuffer buffer; - buffer.Clear(); - rapidjson::Writer writer(buffer); - doc.Accept(writer); - return {buffer.GetString()}; -} - TEST(DocumentTest, Generated) { auto m = Metrics().size(1024,800).theme("dark"); @@ -890,7 +850,6 @@ TEST(DocumentTest, Loop) } ASSERT_TRUE(content->isError()); - } TEST(DocumentTest, NonReversal) @@ -1396,7 +1355,7 @@ TEST(DocumentTest, NestedRepeatedImportPendingDoesNotRequest) ASSERT_FALSE(doc->isWaiting()); ASSERT_TRUE(doc->isReady()); - auto expected = std::vector{ "A:1.0", "B:1.0" }; + auto expected = std::vector{ "B:1.0", "A:1.0" }; ASSERT_EQ(expected, doc->getLoadedPackageNames()); } @@ -1428,7 +1387,7 @@ TEST(DocumentTest, NestedRepeatedImportLoadedDoesNotRequest) ASSERT_FALSE(doc->isWaiting()); ASSERT_TRUE(doc->isReady()); - auto expected = std::vector{ "A:1.0", "B:1.0" }; + auto expected = std::vector{ "B:1.0", "A:1.0" }; ASSERT_EQ(expected, doc->getLoadedPackageNames()); } @@ -1491,4 +1450,53 @@ TEST(DocumentTest, DocumentContent) ASSERT_TRUE(rc); ASSERT_EQ(rc->topDocument()->content(), content); +} + +TEST(DocumentTest, WrongPackageAdded) +{ + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + + auto session = makeDefaultSession(); + auto content = Content::create(json, session, Metrics(), RootConfig()); + ASSERT_TRUE(content); + + content->addPackage(ImportRequest("B", "1.0", "", {}, nullptr, nullptr), makeTestPackage({},{})); + + ASSERT_FALSE(content->isReady()); +} + +TEST(DocumentTest, WrongBadPackageAdded) +{ + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + + auto session = makeDefaultSession(); + auto content = Content::create(json, session, Metrics(), RootConfig()); + ASSERT_TRUE(content); + + content->addPackage(ImportRequest("B", "1.0", "", {}, nullptr, nullptr), ""); + + ASSERT_FALSE(content->isReady()); +} + +TEST(DocumentTest, MissingParametersEmpty) +{ + auto content = Content::create(BASIC_DOC, makeDefaultSession()); + + ASSERT_FALSE(content->isReady()); + ASSERT_FALSE(content->isWaiting()); + ASSERT_FALSE(content->isError()); + + ASSERT_EQ(1, content->getParameterCount()); + ASSERT_EQ(std::string("payload"), content->getParameterAt(0)); + ASSERT_FALSE(content->isReady()); + + auto m = Metrics().size(1024,800).theme("dark"); + auto config = RootConfig().set(RootProperty::kDefaultIdleTimeout, 15000); + auto doc = RootContext::create(m, content, config); + + ASSERT_TRUE(doc); + ASSERT_EQ(15000, content->getDocumentSettings()->idleTimeout(config)); + + // Empty object == empty string + ASSERT_EQ("", doc->topComponent()->getCalculated(kPropertyText).asString()); } \ No newline at end of file diff --git a/aplcore/unit/content/unittest_jsondata.cpp b/aplcore/unit/content/unittest_jsondata.cpp index 5405a59..99dd3af 100644 --- a/aplcore/unit/content/unittest_jsondata.cpp +++ b/aplcore/unit/content/unittest_jsondata.cpp @@ -46,6 +46,8 @@ TEST(JsonData, ValueReference) ASSERT_TRUE(data); // Always valid, even if the parse failed ASSERT_STREQ(m.isvalid ? m.expected : "null", data.toString().c_str()); + ASSERT_STREQ("Value-constructed; no error", data.error()); + ASSERT_EQ(0, data.offset()); } } @@ -59,6 +61,42 @@ TEST(JsonData, MoveDocument) if (m.isvalid) { ASSERT_STREQ(m.expected, data.toString().c_str()); } + else { + ASSERT_GT(strlen(data.error()), 0); + ASSERT_EQ(m.offset, data.offset()); + ASSERT_STREQ("INVALID", data.toString().c_str()); + } + } +} + +TEST(JsonData, MoveFromSharedJson) +{ + for (const auto& m : sTestCases) { + auto doc = std::make_shared(); + doc->Parse(m.original); + auto data = JsonData(SharedJsonData(doc)); + ASSERT_EQ(m.isvalid, (bool) data); + if (m.isvalid) { + ASSERT_STREQ(m.expected, data.toString().c_str()); + } + else { + ASSERT_EQ(m.offset, data.offset()); + ASSERT_STREQ("INVALID", data.toString().c_str()); + } + } +} + +TEST(JsonData, CopyFromSharedJson) +{ + for (const auto& m : sTestCases) { + auto doc = std::make_shared(); + doc->Parse(m.original); + SharedJsonData sjson(doc); + auto data = JsonData(sjson); + ASSERT_EQ(m.isvalid, (bool) data); + if (m.isvalid) { + ASSERT_STREQ(m.expected, data.toString().c_str()); + } else { ASSERT_EQ(m.offset, data.offset()); ASSERT_STREQ("INVALID", data.toString().c_str()); @@ -108,6 +146,7 @@ TEST(JsonData, Char) ASSERT_STREQ(m.expected, data.toString().c_str()); } else { + ASSERT_GT(strlen(data.error()), 0); ASSERT_EQ(m.offset, data.offset()); ASSERT_STREQ("INVALID", data.toString().c_str()); } @@ -119,8 +158,20 @@ TEST(JsonData, NullPointer) auto data = JsonData((char *) nullptr); ASSERT_FALSE(data); ASSERT_STREQ("INVALID", data.toString().c_str()); + ASSERT_STREQ("Nullptr", data.error()); data = JsonData((const char *)nullptr); ASSERT_FALSE(data); ASSERT_STREQ("INVALID", data.toString().c_str()); + ASSERT_STREQ("Nullptr", data.error()); + + data = JsonData("{}"); + auto _ = data.moveToObject(); + ASSERT_FALSE(data); + ASSERT_STREQ("INVALID", data.toString().c_str()); + ASSERT_STREQ("Nullptr", data.error()); + ASSERT_EQ(0, data.offset()); + + auto nullObj = data.moveToObject(); + ASSERT_TRUE(nullObj.isNull()); } \ No newline at end of file diff --git a/aplcore/unit/content/unittest_packagemanager.cpp b/aplcore/unit/content/unittest_packagemanager.cpp new file mode 100644 index 0000000..302ca47 --- /dev/null +++ b/aplcore/unit/content/unittest_packagemanager.cpp @@ -0,0 +1,1045 @@ +/** + * 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 "testpackagemanager.h" +#include "packagegenerator.h" + +using namespace apl; + +class PackageManagerTest : public DocumentWrapper {}; + +const char* B_IMPORT = R"apl({ + "type": "APL", + "version": "1.0", + "resources": [ + { + "strings": { + "B": "B" + } + } + ] +})apl"; + +TEST_F(PackageManagerTest, RepeatedImportDifferentSources) +{ + const char* REPEATED_IMPORT_DIFFERENT_SOURCES = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "B", + "version": "1.0", + "source": "custom.json" + }, + { + "name": "B", + "version": "1.0", + "source": "other.json" + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } + })apl"; + + auto testPackageManager = std::make_shared(); + auto config = RootConfig().packageManager(testPackageManager); + auto doc = Content::create(REPEATED_IMPORT_DIFFERENT_SOURCES, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(doc); + bool successCalled = false; + doc->load([&]() { successCalled = true;}, + []{}); + + ASSERT_FALSE(successCalled); + ASSERT_EQ(1, testPackageManager->getUnresolvedRequests().size()); + auto request = testPackageManager->get("B:1.0"); + ASSERT_TRUE(request.isValid()); + ASSERT_EQ("custom.json", request.source()); + + testPackageManager->succeed(request, SharedJsonData(B_IMPORT)); + ASSERT_TRUE(successCalled); + auto expected = std::vector{ "B:1.0" }; + ASSERT_EQ(expected, doc->getLoadedPackageNames()); +} + +TEST_F(PackageManagerTest, DeepLoop) +{ + std::map packageMap = { + { "A", makeTestPackage({"B", "C"}, {}) }, + { "B", makeTestPackage({"C", "D"}, {}) }, + { "C", makeTestPackage({"D"}, {}) }, + { "D", makeTestPackage({"A"}, {}) } + }; + + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + auto testPackageManager = std::make_shared(); + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, session, Metrics(), config); + ASSERT_TRUE(content); + + bool failureCalled = false; + content->load([]() {}, + [&]{ failureCalled = true; }); + + ASSERT_FALSE(failureCalled); + ASSERT_EQ(1, testPackageManager->getUnresolvedRequests().size()); + + auto requestA = testPackageManager->get("A:1.0"); + testPackageManager->succeed(requestA, SharedJsonData(packageMap.find("A")->second)); + ASSERT_EQ(1, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(2, testPackageManager->getUnresolvedRequests().size()); + + auto requestB = testPackageManager->get("B:1.0"); + auto requestC = testPackageManager->get("C:1.0"); + testPackageManager->succeed(requestB, SharedJsonData(packageMap.find("B")->second)); + ASSERT_EQ(2, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(2, testPackageManager->getUnresolvedRequests().size()); + + auto requestD = testPackageManager->get("D:1.0"); + testPackageManager->succeed(requestC, SharedJsonData(packageMap.find("C")->second)); + ASSERT_EQ(3, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(1, testPackageManager->getUnresolvedRequests().size()); + + testPackageManager->succeed(requestD, SharedJsonData(packageMap.find("D")->second)); + ASSERT_EQ(4, testPackageManager->getResolvedRequestCount()); + ASSERT_EQ(0, testPackageManager->getUnresolvedRequests().size()); + ASSERT_TRUE(failureCalled); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(PackageManagerTest, Loop) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A", "B"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({"B"}, {{"testA", "A"}}); + auto pkg_b = makeTestPackage({"A"}, {{"testB", "B"}}); + testPackageManager->putPackage("A:1.0", pkg_a); + testPackageManager->putPackage("B:1.0", pkg_b); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, session, Metrics(), config); + ASSERT_TRUE(content); + + bool failureCalled = false; + content->load([](){}, + [&]{ failureCalled = true; }); + + ASSERT_TRUE(failureCalled); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(PackageManagerTest, NonReversal) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A", "B"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({}, {{"testA", "A"}, {"testB", "A"}}); + auto pkg_b = makeTestPackage({"A"}, {{"testB", "B"}}); + testPackageManager->putPackage("A:1.0", pkg_a); + testPackageManager->putPackage("B:1.0", pkg_b); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(content); + + bool successCalled = false; + content->load([&]() { successCalled = true; }, + []{}); + ASSERT_TRUE(successCalled); + ASSERT_TRUE(content->getPackage("A:1.0")); + ASSERT_TRUE(content->getPackage("B:1.0")); + + auto doc = RootContext::create(Metrics(), content); + ASSERT_TRUE(doc); + auto context = doc->contextPtr(); + ASSERT_TRUE(context); + + ASSERT_EQ(3, doc->info().resources().size()); + ASSERT_EQ(Object("value"), context->opt("@test")); + ASSERT_EQ(Object("A"), context->opt("@testA")); + ASSERT_EQ(Object("B"), context->opt("@testB")); // B depends on A, so B overrides A +} + +TEST_F(PackageManagerTest, Reversal) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A", "B"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({"B"}, {{"testA", "A"}, {"testB", "A"}}); + auto pkg_b = makeTestPackage({}, {{"testB", "B"}}); + testPackageManager->putPackage("A:1.0", pkg_a); + testPackageManager->putPackage("B:1.0", pkg_b); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(content); + + bool successCalled = false; + content->load([&]() { successCalled = true; }, + []{}); + ASSERT_TRUE(successCalled); + ASSERT_TRUE(content->getPackage("A:1.0")); + ASSERT_TRUE(content->getPackage("B:1.0")); + + auto doc = RootContext::create(Metrics(), content); + ASSERT_TRUE(doc); + auto context = doc->contextPtr(); + ASSERT_TRUE(context); + + ASSERT_EQ(3, doc->info().resources().size()); + ASSERT_EQ(Object("value"), context->opt("@test")); + ASSERT_EQ(Object("A"), context->opt("@testA")); + ASSERT_EQ(Object("A"), context->opt("@testB")); // A depends on B, so A overrides B +} + +TEST_F(PackageManagerTest, Diamond) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A", "B"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({"C"}, {{"testA", "A"}}); + auto pkg_b = makeTestPackage({"C"}, {{"testB", "B"}}); + auto pkg_c = makeTestPackage({}, {{"testC", "C"}, {"testA", "C"}, {"testB", "C"}}); + testPackageManager->putPackage("A:1.0", pkg_a); + testPackageManager->putPackage("B:1.0", pkg_b); + testPackageManager->putPackage("C:1.0", pkg_c); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(content); + + bool successCalled = false; + content->load([&]() { successCalled = true; }, + []{}); + ASSERT_TRUE(successCalled); + ASSERT_TRUE(content->getPackage("A:1.0")); + ASSERT_TRUE(content->getPackage("B:1.0")); + ASSERT_TRUE(content->getPackage("C:1.0")); + + auto doc = RootContext::create(Metrics(), content); + ASSERT_TRUE(doc); + auto context = doc->contextPtr(); + ASSERT_TRUE(context); + + ASSERT_EQ(4, doc->info().resources().size()); + ASSERT_EQ(Object("value"), context->opt("@test")); + ASSERT_EQ(Object("A"), context->opt("@testA")); + ASSERT_EQ(Object("B"), context->opt("@testB")); + ASSERT_EQ(Object("C"), context->opt("@testC")); +} + +TEST_F(PackageManagerTest, ChangeConfigAfterContentInitialization) +{ + const char *THEME_BASED_NESTED_INCLUDE = R"apl({ + "type": "APL", + "version": "2023.3", + "onConfigChange": { + "type": "Reinflate" + }, + "import": [ + { + "name": "StyledFrame", + "version": "1.0" + } + ], + "mainTemplate": { + "item": { + "type": "StyledFrame" + } + } + })apl"; + + const char *STYLED_FRAME_OVERRIDE_DEPENDS = R"apl({ + "type": "APL", + "version": "2023.3", + "import": [ + { + "name": "conditional", + "version": "1.2", + "when": "${environment.hasMagic == 'magic'}", + "loadAfter": "dbasic" + }, + { + "name": "dbasic", + "description": "force it to to be requested later", + "version": "1.2" + } + ], + "layouts": { + "StyledFrame": { + "item": { + "type": "Frame", + "width": "100%", + "height": "100%", + "backgroundColor": "@MyRed" + } + } + } + })apl"; + + const char *BASIC = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyRed": "#ff0101ff" + } + } + ] + })apl"; + + const char *CONDITIONAL = R"apl({ + "type": "APL", + "version": "2023.3", + "resources": [ + { + "colors": { + "MyRed": "#ff0000ff" + } + } + ] + })apl"; + + + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("StyledFrame:1.0", STYLED_FRAME_OVERRIDE_DEPENDS); + testPackageManager->putPackage("dbasic:1.2", BASIC); + testPackageManager->putPackage("conditional:1.2", CONDITIONAL); + + auto config = RootConfig::create(); + config->packageManager(testPackageManager); + + auto content = Content::create(THEME_BASED_NESTED_INCLUDE, session, Metrics(), *config); + ASSERT_TRUE(content); + + int successCalled = 0; + content->load([&]() { successCalled++; }, + []{}); + + ASSERT_EQ(1, successCalled); + ASSERT_TRUE(content->isReady()); + + // Config (or metrics/or both) changed when RootContext creation possible. Should still account + // for it. + config->setEnvironmentValue("hasMagic", "magic"); + content->refresh(Metrics(), *config); + + content->load([&]() { successCalled++; }, + []{}); + + ASSERT_EQ(2, successCalled); + ASSERT_TRUE(content->isReady()); + + auto expectedOrder = std::vector { + "dbasic:1.2", + "conditional:1.2", + "StyledFrame:1.0", + }; + + ASSERT_EQ(expectedOrder, content->getLoadedPackageNames()); +} + +TEST_F(PackageManagerTest, PackageFailure) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({"C"}, {{"testA", "A"}}); + testPackageManager->putPackage("A:1.0", pkg_a); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, session, Metrics(), config); + ASSERT_TRUE(content); + + bool failureCalled = false; + content->load([]{}, + [&]() { failureCalled = true; }); + + testPackageManager->fail(testPackageManager->get("C:1.0")); + ASSERT_TRUE(failureCalled); + ASSERT_TRUE(ConsoleMessage()); +} + +TEST_F(PackageManagerTest, LoadedContentStillSucceeds) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({}, {}); + testPackageManager->putPackage("A:1.0", pkg_a); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(content); + + int successCount = 0; + content->load([&]() { successCount++; }, + []{}); + + ASSERT_EQ(1, successCount); + auto expected = std::vector{ "A:1.0" }; + ASSERT_EQ(expected, content->getLoadedPackageNames()); + + content->load([&]() { successCount++; }, + []{}); + ASSERT_EQ(2, successCount); + ASSERT_EQ(expected, content->getLoadedPackageNames()); +} + +TEST_F(PackageManagerTest, LoadedContentWhilePendingInvokesSecondLambda) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({}, {}); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(content); + + bool successA = false; + bool successB = false; + auto lamdbaA = [&]() { successA = true; }; + auto lamdbaB= [&]() { successB = true; }; + // Two loads are triggered, so first one is canceled and second one will trigger. + content->load(lamdbaA, + []{}); + + content->load(lamdbaB, + []{}); + + testPackageManager->succeed(testPackageManager->get("A:1.0"), SharedJsonData(pkg_a)); + ASSERT_FALSE(successA); + ASSERT_TRUE(successB); +} + +TEST_F(PackageManagerTest, BadPackage) +{ + auto badPackages = std::vector{ + "", + R"apl({ + "version": "1.1" + })apl", + R"apl({ + "type": "APL" + })apl", + R"apl({ + "type": "APL", + "version": "1.1", + "import": "foo" + })apl", + R"apl({ + "type": "APL", + "version": "1.1", + "import": ["foo"] + })apl", + }; + + for (const auto& badPackage: badPackages) { + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + testPackageManager->putPackage("A:1.0", badPackage); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, session, Metrics(), config); + ASSERT_TRUE(content); + + bool failureCalled = false; + content->load([] {}, [&]() { failureCalled = true; }); + + ASSERT_TRUE(failureCalled); + ASSERT_TRUE(content->isError()); + ASSERT_TRUE(ConsoleMessage()); + } +} + +TEST_F(PackageManagerTest, ContentAddsWrongPackage) +{ + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + + auto content = Content::create(json, session, Metrics(), RootConfig()); + ASSERT_TRUE(content); + + content->addPackage(ImportRequest("B", "1.0", "", {}, nullptr, nullptr), makeTestPackage({},{})); + + ASSERT_TRUE(LogMessage()); +} + +TEST_F(PackageManagerTest, CanceledContent) +{ + auto testPackageManager = std::make_shared(); + auto json = makeTestPackage({"A"}, {{"test", "value"}}); + auto pkg_a = makeTestPackage({"B"}, {}); + auto pkg_b = makeTestPackage({}, {}); + + auto config = RootConfig().packageManager(testPackageManager); + auto content = Content::create(json, makeDefaultSession(), Metrics(), config); + ASSERT_TRUE(content); + + bool successA = false; + bool successB = false; + auto lamdbaA = [&]() { successA = true; }; + auto lamdbaB= [&]() { successB = true; }; + + content->load(lamdbaA, + []{}); + + testPackageManager->succeed(testPackageManager->get("A:1.0"), SharedJsonData(pkg_a)); + + content->load(lamdbaB, + []{}); + + // Only second lambda runs + testPackageManager->succeed(testPackageManager->get("A:1.0"), SharedJsonData(pkg_a)); + testPackageManager->succeed(testPackageManager->get("B:1.0"), SharedJsonData(pkg_b)); + ASSERT_FALSE(successA); + ASSERT_TRUE(successB); +} + +static const char *IMPORT_PACKAGE_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": [], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "ImportPackage", + "name": "levelone", + "version": "1.0", + "source": "levelonesource" + } + ], + "items": [ + { + "type": "Text", + "text": "ImportPackage test template" + } + ] + }, + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "delay": 1000, + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "@leveloneString" + } + }, + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "@leveltwoString" + } + }, + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "@leveltwoStringAgain" + } + }, + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "@levelthreeString" + } + } + ], + "items": [ + { + "type": "Text", + "text": "insertItem runner" + } + ] + } + ] + } + } +} +)"; + +static const char *LEVEL_ONE_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "import": [ + { + "name": "leveltwo", + "version": "1.0" + } + ], + "resources": [ + { + "string": { + "leveloneString": "parent package loaded" + } + } + ] +} +)"; + +static const char *LEVEL_TWO_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ + { + "string": { + "leveltwoString": "child package loaded" + } + } + ] +} +)"; + +TEST_F(PackageManagerTest, SingleOneLevelNestedPackageImport) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("levelone:1.0", LEVEL_ONE_PACKAGE_JSON); + testPackageManager->putPackage("leveltwo:1.0", LEVEL_TWO_PACKAGE_JSON); + + config->packageManager(testPackageManager); + createContent(IMPORT_PACKAGE_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); + + // Trigger InsertItem - using content from dynamically loaded package + performClick(0, 10); + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("parent package loaded", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("child package loaded", top->getChildAt(3)->getCalculated(apl::kPropertyText).asString()); +} + +static const char *LEVEL_ONE_DUAL_IMPORT_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "import": [ + { + "name": "leveltwo", + "version": "1.0" + }, + { + "name": "leveltwoagain", + "version": "1.0" + } + ], + "resources": [ + { + "string": { + "leveloneString": "parent package loaded" + } + } + ] +} +)"; + +static const char *LEVEL_TWO_AGAIN_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ + { + "string": { + "leveltwoStringAgain": "child package loaded, again!" + } + } + ] +} +)"; + + +TEST_F(PackageManagerTest, DualOneLevelNestedPackageImport) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("levelone:1.0", LEVEL_ONE_DUAL_IMPORT_PACKAGE_JSON); + testPackageManager->putPackage("leveltwo:1.0", LEVEL_TWO_PACKAGE_JSON); + testPackageManager->putPackage("leveltwoagain:1.0", LEVEL_TWO_AGAIN_PACKAGE_JSON); + + config->packageManager(testPackageManager); + + createContent(IMPORT_PACKAGE_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); + + // Trigger InsertItem - using content from dynamically loaded package + performClick(0, 10); + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("parent package loaded", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("child package loaded", top->getChildAt(3)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("child package loaded, again!", top->getChildAt(4)->getCalculated(apl::kPropertyText).asString()); +} + + +static const char *LEVEL_TWO_TARGETING_THREE_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "import": [ + { + "name": "levelthree", + "version": "1.0" + } + ], + "resources": [ + { + "string": { + "leveltwoString": "child package loaded" + } + } + ] +} +)"; + +static const char *LEVEL_THREE_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ + { + "string": { + "levelthreeString": "strings all the way down" + } + } + ] +} +)"; + + +TEST_F(PackageManagerTest, TwoLevelNestedPackageImport) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("levelone:1.0", LEVEL_ONE_PACKAGE_JSON); + testPackageManager->putPackage("leveltwo:1.0", LEVEL_TWO_TARGETING_THREE_PACKAGE_JSON); + testPackageManager->putPackage("levelthree:1.0", LEVEL_THREE_PACKAGE_JSON); + config->packageManager(testPackageManager); + + createContent(IMPORT_PACKAGE_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + ASSERT_TRUE(component); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); + + // Trigger InsertItem - using content from dynamically loaded package + performClick(0, 10); + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("parent package loaded", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("child package loaded", top->getChildAt(3)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("strings all the way down", top->getChildAt(5)->getCalculated(apl::kPropertyText).asString()); +} + + +static const char *DYNAMIC_IMPORT_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": [ + { + "type": "ImportPackage", + "name": "levelone", + "version": "1.0", + "source": "levelonesource" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "ImportPackage", + "name": "levelone", + "version": "1.0", + "source": "levelonepossiblynewsource" + } + ], + "items": [ + { + "type": "Text", + "text": "ImportPackage test template" + } + ] + } + ] + } + } +} +)"; + +static const char *STATIC_IMPORT_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "import": [ + { + "name": "levelone", + "version": "1.0" + } + ], + "onMount": [], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "ImportPackage", + "name": "levelone", + "version": "1.0", + "source": "levelonesource" + } + ], + "items": [ + { + "type": "Text", + "text": "duplicate package import protection test" + } + ] + } + ] + } + } +} +)"; + +static const char *BASIC_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ + { + "string": { + "basic": "basic" + } + } + ] +} +)"; + + +TEST_F(PackageManagerTest, RequestPackageAlreadyStaticallyImported) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("levelone:1.0", BASIC_PACKAGE_JSON); + config->packageManager(testPackageManager); + + createContent(STATIC_IMPORT_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); +} + +TEST_F(PackageManagerTest, RequestPackageAlreadyDynamicallyImported) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("levelone:1.0", BASIC_PACKAGE_JSON); + config->packageManager(testPackageManager); + + createContent(DYNAMIC_IMPORT_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); + + // Send the ImportPackage command + performClick(0, 0); + loop->advanceToEnd(); +} + +static const char *CONSECUTIVE_IMPORTS_DOC = R"( +{ + "type": "APL", + "version": "2024.1", + "onMount": [], + "mainTemplate": { + "item": { + "type": "Container", + "id": "mainContainer", + "items": [ + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "ImportPackage", + "name": "firstPackage", + "version": "1.0", + "source": "sourceone" + }, + { + "type": "ImportPackage", + "name": "secondPackage", + "version": "1.0", + "source": "sourcetwo" + }, + { + "type": "ImportPackage", + "name": "thirdPackage", + "version": "1.0", + "source": "sourcethree" + } + ], + "items": [ + { + "type": "Text", + "text": "consecutive package import test" + } + ] + }, + { + "type": "TouchWrapper", + "width": "100%", + "onPress": [ + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "${@first}" + } + }, + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "@second" + } + }, + { + "type": "InsertItem", + "componentId": "mainContainer", + "item": { + "type": "Text", + "text": "@third" + } + } + ], + "items": [ + { + "type": "Text", + "text": "InsertItem touchwrapper" + } + ] + } + ] + } + } +} +)"; + + +static const char *FIRST_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ { "string": { "first": "first" } } ] +} +)"; + +static const char *SECOND_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ { "string": { "second": "second" } } ] +} +)"; + +static const char *THIRD_PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2024.1", + "resources": [ { "string": { "third": "third" } } ] +} +)"; + +TEST_F(PackageManagerTest, ConsecutiveDynamicImports) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("firstPackage:1.0", FIRST_PACKAGE_JSON); + testPackageManager->putPackage("secondPackage:1.0", SECOND_PACKAGE_JSON); + testPackageManager->putPackage("thirdPackage:1.0", THIRD_PACKAGE_JSON); + + config->packageManager(testPackageManager); + + createContent(CONSECUTIVE_IMPORTS_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + + // Trigger ImportPackages + performClick(0, 0); + loop->advanceToEnd(); + + // Trigger InsertItems + performClick(0, 10); + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("first", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("second", top->getChildAt(3)->getCalculated(apl::kPropertyText).asString()); + ASSERT_EQ("third", top->getChildAt(4)->getCalculated(apl::kPropertyText).asString()); +} \ No newline at end of file diff --git a/aplcore/unit/content/unittest_packages.cpp b/aplcore/unit/content/unittest_packages.cpp index 845b9d3..6a2398f 100644 --- a/aplcore/unit/content/unittest_packages.cpp +++ b/aplcore/unit/content/unittest_packages.cpp @@ -18,6 +18,7 @@ #include "../testeventloop.h" #include "../embed/testdocumentmanager.h" +#include "testpackagemanager.h" using namespace apl; @@ -533,22 +534,22 @@ static const char *MULTI_DEPENDS = R"apl({ "import": [ { "name": "A", - "version": "A", + "version": "1.0", "loadAfter": "B" }, { "name": "B", - "version": "B", + "version": "1.0", "loadAfter": [ "C", "D" ] }, { "name": "C", - "version": "C", + "version": "1.0", "loadAfter": "D" }, { "name": "D", - "version": "D" + "version": "1.0" } ], "mainTemplate": { @@ -560,10 +561,10 @@ static const char *MULTI_DEPENDS = R"apl({ TEST_F(PackagesTest, MultiDepends) { - add("A:A", BASIC); - add("B:B", BASIC); - add("C:C", BASIC); - add("D:D", BASIC); + add("A:1.0", BASIC); + add("B:1.0", BASIC); + add("C:1.0", BASIC); + add("D:1.0", BASIC); content = Content::create(MULTI_DEPENDS, session, metrics, *config); ASSERT_TRUE(content); ASSERT_TRUE(content->isWaiting()); @@ -760,6 +761,7 @@ TEST_F(PackagesTest, RefreshUsesStashedPackages) // Refresh it content->refresh(metrics, *config); + ASSERT_FALSE(content->isWaiting()); // Use of stashed packages means no re-processing needed ASSERT_TRUE(content->isReady()); @@ -1446,6 +1448,9 @@ static const char *THEME_BASED_CONDITIONAL = R"apl({ } }, "mainTemplate": { + "parameters": [ + "MyParams" + ], "item": { "type": "StyledFrame", "id": "magicFrame" @@ -1470,15 +1475,18 @@ TEST_F(PackagesTest, ConditionalEmbeddedReinflateTheme) ASSERT_TRUE(root); auto embeddedContent = Content::create(THEME_BASED_CONDITIONAL, session); + ASSERT_TRUE(process(embeddedContent)); + ASSERT_FALSE(embeddedContent->isWaiting()); ASSERT_TRUE(documentManager->getUnresolvedRequests().size()); - auto request = documentManager->get("embeddedDocumentUrl").lock(); auto documentConfig = DocumentConfig::create(); embeddedContent->refresh(*request, documentConfig); // Content becomes "Waiting again" ASSERT_TRUE(embeddedContent->isWaiting()); + ASSERT_FALSE(embeddedContent->isReady()); + // Re-resolve ASSERT_TRUE(process(embeddedContent)); ASSERT_TRUE(embeddedContent->isReady()); @@ -2323,6 +2331,280 @@ TEST_F(PackagesTest, LongCircularLoadDependency) ASSERT_TRUE(session->checkAndClear("Failure to order packages")); } +const char* ACCEPT_ALL_OF = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "type": "allOf", + "accept": ">1.0", + "items": [ + { + "name": "A", + "version": "1.2" + }, + { + "name": "B", + "version": "1.3" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST_F(PackagesTest, CommonAccept) +{ + auto requestA = ImportRequest("A", "1.1", "", {}, SemanticVersion::create(session, "1.1"), nullptr); + auto requestB = ImportRequest("B", "1.5", "", {}, SemanticVersion::create(session, "1.5"), nullptr); + + content = Content::create(ACCEPT_ALL_OF, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + for (const auto& request : requested) { + if (requestA.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + if (requestB.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + } + ASSERT_TRUE(content->isReady()); +} + +const char* ACCEPT_ALL_OF_DEEP = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "type": "allOf", + "accept": ">1.0", + "items": [ + { + "name": "A", + "version": "1.2" + }, + { + "type": "allOf", + "items": [ + { + "name": "B", + "version": "1.3" + } + ] + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST_F(PackagesTest, CommonAcceptDeep) +{ + auto requestA = ImportRequest("A", "1.1", "", {}, SemanticVersion::create(session, "1.1"), nullptr); + auto requestB = ImportRequest("B", "1.5", "", {}, SemanticVersion::create(session, "1.5"), nullptr); + + content = Content::create(ACCEPT_ALL_OF_DEEP, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + for (const auto& request : requested) { + if (requestA.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + if (requestB.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + } + ASSERT_TRUE(content->isReady()); +} + +const char* ACCEPT_ALL_OF_DEEP_DIFFERENT_ACCEPT = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "type": "allOf", + "accept": ">1.0", + "items": [ + { + "name": "A", + "version": "1.2" + }, + { + "type": "allOf", + "accept": ">0.5", + "items": [ + { + "name": "B", + "version": "0.9" + } + ] + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST_F(PackagesTest, CommonAcceptDeepDifferent) +{ + auto requestA = ImportRequest("A", "1.1", "", {}, SemanticVersion::create(session, "1.1"), nullptr); + auto requestB = ImportRequest("B", "0.8", "", {}, SemanticVersion::create(session, "0.8"), nullptr); + + content = Content::create(ACCEPT_ALL_OF_DEEP_DIFFERENT_ACCEPT, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + for (const auto& request : requested) { + if (requestA.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + if (requestB.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + } + ASSERT_TRUE(content->isReady()); +} + +const char* ACCEPT_ALL_OF_OVERRIDE_ACCEPT = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "type": "allOf", + "accept": ">1.0", + "items": [ + { + "name": "A", + "version": "1.2" + }, + { + "name": "B", + "version": "0.9", + "accept": "<1.0" + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST_F(PackagesTest, CommonAcceptOverrideAccept) +{ + auto requestA = ImportRequest("A", "1.1", "", {}, SemanticVersion::create(session, "1.1"), nullptr); + auto requestB = ImportRequest("B", "0.8", "", {}, SemanticVersion::create(session, "0.8"), nullptr); + + content = Content::create(ACCEPT_ALL_OF_OVERRIDE_ACCEPT, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + for (const auto& request : requested) { + if (requestA.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + if (requestB.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + } + ASSERT_TRUE(content->isReady()); +} + +const char* ACCEPT_ALREADY_REQUESTED = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "A", + "version": "1.2" + }, + { + "name": "A", + "version": "0.9", + "accept": ">1.0" + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +TEST_F(PackagesTest, AlreadyRequestedAcceptedVersion) +{ + auto requestA = ImportRequest("A", "1.2", "", {}, SemanticVersion::create(session, "1.2"), nullptr); + + content = Content::create(ACCEPT_ALREADY_REQUESTED, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + for (const auto& request : requested) { + if (requestA.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + } + ASSERT_TRUE(content->isReady()); +} + +const char* ACCEPT_ALREADY_LOADED = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "A", + "version": "1.2" + }, + { + "name": "B", + "version": "1.2" + } + ], + "mainTemplate": { + "item": { + "type": "Text" + } + } +})apl"; + +const char* PACKAGE_ALREADY_LOADED = R"apl({ + "type": "APL", + "version": "1.0", + "import": [ + { + "name": "B", + "version": "1.3", + "accept": ">1.0" + } + ] +})apl"; + +TEST_F(PackagesTest, AcceptAlreadyLoaded) +{ + auto requestA = ImportRequest("A", "1.2", "", {}, SemanticVersion::create(session, "1.2"), nullptr); + auto requestB = ImportRequest("B", "1.2", "", {}, SemanticVersion::create(session, "1.2"), nullptr); + + content = Content::create(ACCEPT_ALREADY_LOADED, session, metrics, *config); + ASSERT_TRUE(content); + ASSERT_TRUE(content->isWaiting()); + auto requested = content->getRequestedPackages(); + for (const auto& request : requested) { + if (requestA.isAcceptableReplacementFor(request)) + content->addPackage(request, PACKAGE_ALREADY_LOADED); + if (requestB.isAcceptableReplacementFor(request)) + content->addPackage(request, BASIC); + } + ASSERT_TRUE(content->isReady()); +} + #ifdef ALEXAEXTENSIONS // Extensions support diff --git a/aplcore/unit/content/unittest_rootconfig.cpp b/aplcore/unit/content/unittest_rootconfig.cpp index 907ef45..b012909 100644 --- a/aplcore/unit/content/unittest_rootconfig.cpp +++ b/aplcore/unit/content/unittest_rootconfig.cpp @@ -36,6 +36,21 @@ TEST(RootConfigTest, CustomEnvironmentProperties) ASSERT_TRUE(rootConfig.getEnvironmentValues().find("environment") == rootConfig.getEnvironmentValues().end()); } +TEST(RootConfigTest, ApplyConfigurationChange) +{ + RootConfig rootConfig; + ASSERT_FALSE(rootConfig.getProperty(RootProperty::kDisallowVideo).asBoolean()); + + ConfigurationChange configurationChange; + configurationChange.environmentValue("number", 42); + configurationChange.disallowVideo(true); + + configurationChange.applyToRootConfig(rootConfig); + + ASSERT_EQ(42, rootConfig.getEnvironmentValues().at("number").asInt()); + ASSERT_TRUE(rootConfig.getProperty(RootProperty::kDisallowVideo).asBoolean()); +} + TEST(RootConfigTest, CannotShadowExistingNames) { RootConfig rootConfig; diff --git a/aplcore/unit/content/unittest_sharedjsondata.cpp b/aplcore/unit/content/unittest_sharedjsondata.cpp new file mode 100644 index 0000000..b8be1cd --- /dev/null +++ b/aplcore/unit/content/unittest_sharedjsondata.cpp @@ -0,0 +1,118 @@ +/** + * 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/content/sharedjsondata.h" + +using namespace apl; + +static const char * TEST_JSON_DATA = R"apl({ + "A": { + "B": "c" + } +})apl"; + +TEST(SharedJsonData, CreateFromDocument) +{ + std::shared_ptr data; + { + rapidjson::Document doc; + doc.Parse(TEST_JSON_DATA); + data = std::make_shared(std::move(doc)); + } + + ASSERT_TRUE(*data); + ASSERT_STREQ(R"({"A":{"B":"c"}})", data->toString().c_str()); +} + +TEST(SharedJsonData, CreateFromSharedDocument) +{ + std::shared_ptr data; + { + auto doc = std::make_shared(); + doc->Parse(TEST_JSON_DATA); + data = std::make_shared(doc); + } + + ASSERT_TRUE(*data); + ASSERT_STREQ(R"({"A":{"B":"c"}})", data->toString().c_str()); +} + +TEST(SharedJsonData, CreateFromDocumentWithPath) +{ + std::shared_ptr data; + { + auto doc = std::make_shared(); + doc->Parse(TEST_JSON_DATA); + rapidjson::Pointer ptr("/A"); + data = std::make_shared(doc, ptr); + } + + ASSERT_TRUE(*data); + ASSERT_STREQ(R"({"B":"c"})", data->toString().c_str()); +} + +TEST(SharedJsonData, CreateFromDocumentWithInvalidPointer) +{ + std::shared_ptr data; + { + auto doc = std::make_shared(); + doc->Parse(TEST_JSON_DATA); + rapidjson::Pointer ptr("X/"); + data = std::make_shared(doc, ptr); + } + + ASSERT_FALSE(*data); + ASSERT_STREQ("INVALID", data->toString().c_str()); + ASSERT_STREQ("Bad rapidjson::Pointer: Code 1 at 0", data->error()); +} + +TEST(SharedJsonData, CreateFromDocumentWithInvalidPath) +{ + std::shared_ptr data; + { + auto doc = std::make_shared(); + doc->Parse(TEST_JSON_DATA); + rapidjson::Pointer ptr("/X"); + data = std::make_shared(doc, ptr); + } + + ASSERT_FALSE(*data); + ASSERT_STREQ("INVALID", data->toString().c_str()); + ASSERT_STREQ("Invalid pointer path: /X", data->error()); +} + +TEST(SharedJsonData, CreateFromString) +{ + std::shared_ptr data; + { + std::string s = TEST_JSON_DATA; + data = std::make_shared(s); + } + + ASSERT_TRUE(*data); + ASSERT_STREQ(R"({"A":{"B":"c"}})", data->toString().c_str()); +} + +TEST(SharedJsonData, CreateFromCString) +{ + std::shared_ptr data; + { + data = std::make_shared(TEST_JSON_DATA); + } + + ASSERT_TRUE(*data); + ASSERT_STREQ(R"({"A":{"B":"c"}})", data->toString().c_str()); +} diff --git a/aplcore/unit/datagrammar/unittest_grammar.cpp b/aplcore/unit/datagrammar/unittest_grammar.cpp index 6c64259..2fdceee 100644 --- a/aplcore/unit/datagrammar/unittest_grammar.cpp +++ b/aplcore/unit/datagrammar/unittest_grammar.cpp @@ -921,11 +921,41 @@ TEST_F(GrammarTest, PropertyAsMapped) map->emplace("wrong", "wrong"); auto obj = Object(map); - ASSERT_EQ(kTestMappingOne, propertyAsMapped(*c, obj, "one", kTestMappingDefault, sTestMappingBimap)); - ASSERT_EQ(kTestMappingTwo, propertyAsMapped(*c, obj, "two", kTestMappingDefault, sTestMappingBimap)); - ASSERT_EQ(kTestMappingDefault, propertyAsMapped(*c, obj, "empty", kTestMappingDefault, sTestMappingBimap)); - ASSERT_EQ(-1, propertyAsMapped(*c, obj, "wrong", kTestMappingDefault, sTestMappingBimap)); - ASSERT_EQ(kTestMappingDefault, propertyAsMapped(*c, obj, "none", kTestMappingDefault, sTestMappingBimap)); + // Required properties + ASSERT_EQ((std::pair(kTestMappingOne, true)), + requiredMappedProperty(*c, obj, "one", sTestMappingBimap)); + ASSERT_EQ((std::pair(kTestMappingTwo, true)), + requiredMappedProperty(*c, obj, "two", sTestMappingBimap)); + ASSERT_EQ((std::pair(static_cast(-1), false)), + requiredMappedProperty(*c, obj, "empty", sTestMappingBimap)); + ASSERT_EQ((std::pair(static_cast(-1), false)), + requiredMappedProperty(*c, obj, "wrong", sTestMappingBimap)); + ASSERT_EQ((std::pair(static_cast(-1), false)), + requiredMappedProperty(*c, obj, "none", sTestMappingBimap)); + + // Properties with default values + ASSERT_EQ(kTestMappingOne, optionalMappedProperty( + *c, obj, "one", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ(kTestMappingTwo, optionalMappedProperty( + *c, obj, "two", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ(kTestMappingDefault, optionalMappedProperty( + *c, obj, "empty", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ(kTestMappingDefault, optionalMappedProperty( + *c, obj, "wrong", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ(kTestMappingDefault, optionalMappedProperty( + *c, obj, "none", kTestMappingDefault, sTestMappingBimap)); + + // Strict checking with default values + ASSERT_EQ((std::pair(kTestMappingOne, true)), + optionalStrictMappedProperty(*c, obj, "one", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ((std::pair(kTestMappingTwo, true)), + optionalStrictMappedProperty(*c, obj, "two", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ((std::pair(kTestMappingDefault, false)), + optionalStrictMappedProperty(*c, obj, "empty", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ((std::pair(kTestMappingDefault, false)), + optionalStrictMappedProperty(*c, obj, "wrong", kTestMappingDefault, sTestMappingBimap)); + ASSERT_EQ((std::pair(kTestMappingDefault, true)), + optionalStrictMappedProperty(*c, obj, "none", kTestMappingDefault, sTestMappingBimap)); } diff --git a/aplcore/unit/datagrammar/unittest_grammar_map.cpp b/aplcore/unit/datagrammar/unittest_grammar_map.cpp index f653160..81bdaf9 100644 --- a/aplcore/unit/datagrammar/unittest_grammar_map.cpp +++ b/aplcore/unit/datagrammar/unittest_grammar_map.cpp @@ -213,7 +213,10 @@ TEST_F(MapGrammarTest, OnScrollSourceEventKeys) executeCommand("Scroll", {{"componentId", ":root"}, {"distance", 1}}, false); advanceTime(300); - ASSERT_TRUE(CheckSendEvent(root, "bind", + ASSERT_TRUE(CheckSendEvent(root, + "allowBackwards", + "allowForward", + "bind", "checked", "disabled", "focused", @@ -261,7 +264,10 @@ TEST_F(MapGrammarTest, OnScrollTargetEventKeys) executeCommand("Scroll", {{"componentId", ":root"}, {"distance", 1}}, false); advanceTime(300); - ASSERT_TRUE(CheckSendEvent(root, "bind", + ASSERT_TRUE(CheckSendEvent(root, + "allowBackwards", + "allowForward", + "bind", "checked", "disabled", "focused", diff --git a/aplcore/unit/embed/CMakeLists.txt b/aplcore/unit/embed/CMakeLists.txt index 319866f..0670076 100644 --- a/aplcore/unit/embed/CMakeLists.txt +++ b/aplcore/unit/embed/CMakeLists.txt @@ -16,6 +16,7 @@ target_sources_local(unittest testdocumentmanager.cpp unittest_documentcreate.cpp unittest_embedded_extensions.cpp + unittest_embedded_importpackage.cpp unittest_embedded_lifecycle.cpp unittest_embedded_reinflate.cpp unittest_rootcontexttargeting.cpp diff --git a/aplcore/unit/embed/unittest_embedded_importpackage.cpp b/aplcore/unit/embed/unittest_embedded_importpackage.cpp new file mode 100644 index 0000000..c2840e3 --- /dev/null +++ b/aplcore/unit/embed/unittest_embedded_importpackage.cpp @@ -0,0 +1,251 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include + +#include "../testeventloop.h" +#include "../content/testpackagemanager.h" +#include "../embed/testdocumentmanager.h" + +using namespace apl; + +class EmbeddedImportPackageTest : public DocumentWrapper { +public: + EmbeddedImportPackageTest() + : documentManager(std::make_shared()) + { + config->documentManager(std::static_pointer_cast(documentManager)); + } + +protected: + std::shared_ptr documentManager; +}; + +static const char* HOST_DOC = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Container", + "entities": "ROOT", + "id": "top", + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl", + "onLoad": [ + { + "type": "ImportPackage", + "sequencer": "ImportSequencer", + "name": "packageName", + "version": "1.0", + "source": "sourceUri", + "onLoad": [ + { + "type": "InsertItem", + "componentId": "top", + "item": { + "type": "Text", + "text": "${@testStringImport}" + } + } + ] + } + ] + } + } + } +})"; + +static const char* EMBEDDED_DOC = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${@testStringImport}", + "entities": "EMBEDDED" + } + } +})"; + +static const char *PACKAGE_JSON = R"( +{ + "type": "APL", + "version": "2023.3", + "description": "test package definition", + "resources": [ + { + "string": { + "testStringImport": "wow, nice string" + } + } + ], + "import": [], + "layouts": {} +} +)"; + +TEST_F(EmbeddedImportPackageTest, HostImportPackagedNotAddedToChildContext) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + config->packageManager(testPackageManager); + + createContent(HOST_DOC, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(EMBEDDED_DOC, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + + loop->advanceToEnd(); + + auto top = root->topComponent(); + ASSERT_EQ("wow, nice string", top->getChildAt(1)->getCalculated(apl::kPropertyText).asString()); + + // Verifies the embedded text can't read the resources from parent context + auto embeddedText = top->getChildAt(0)->getChildAt(0); + ASSERT_EQ("", embeddedText->getCalculated(apl::kPropertyText).asString()); +} + +static const char* HOST_DOC_NO_REQUEST = R"({ + "type": "APL", + "version": "2023.1", + "onConfigChange": { + "type": "Reinflate" + }, + "mainTemplate": { + "item": { + "type": "Container", + "entities": "ROOT", + "id": "top", + "item": [ + { + "type": "Host", + "width": "100", + "height": "100", + "id": "hostComponent", + "entities": "HOST", + "source": "embeddedDocumentUrl" + }, + { + "type": "TouchWrapper", + "width": "100", + "height": "100", + "onPress": { + "type": "InsertItem", + "componentId": ":root", + "item": { + "type": "Text", + "text": "${@testStringImport}" + } + } + } + ] + } + } +})"; + +static const char* EMBEDDED_DOC_REQUEST = R"({ + "type": "APL", + "version": "2023.2", + "onMount": [ + { + "type": "ImportPackage", + "sequencer": "ImportSequencer", + "name": "packageName", + "version": "1.0", + "source": "sourceUri", + "onLoad": [ + { + "type": "InsertItem", + "componentId": ":root", + "item": { + "type": "Text", + "text": "${@testStringImport}" + } + } + ] + } + ], + "mainTemplate": { + "item": { + "type": "Container" + } + } +})"; + +TEST_F(EmbeddedImportPackageTest, ChildImportPackageNotAddedToHost) { + auto testPackageManager = std::make_shared(); + testPackageManager->putPackage("packageName:1.0", PACKAGE_JSON); + + config->packageManager(testPackageManager); + + createContent(HOST_DOC_NO_REQUEST, "{}", true); + content->load([]{}, []{}); + inflate(); + ASSERT_TRUE(root); + rootDocument = root->topDocument(); + // While it inflates embedded document requested. + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + // When document retrieved - create content with new session (console session management is up + // to runtime/viewhost) + auto embeddedSession = std::make_shared(); + auto content = Content::create(EMBEDDED_DOC_REQUEST, embeddedSession); + // Load any packages if required and check if ready. + ASSERT_TRUE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + loop->advanceToEnd(); + + auto top = root->topComponent(); + + // Verifies the embedded text cant read the resources from new package + auto embeddedText = top->getChildAt(0)->getChildAt(0)->getChildAt(0); + ASSERT_EQ("wow, nice string", embeddedText->getCalculated(apl::kPropertyText).asString()); + + // Verifies the host document can't read the resources of embedded package + performTap(1, 101); + ASSERT_EQ("", top->getChildAt(2)->getCalculated(apl::kPropertyText).asString()); +} \ No newline at end of file diff --git a/aplcore/unit/embed/unittest_embedded_lifecycle.cpp b/aplcore/unit/embed/unittest_embedded_lifecycle.cpp index 6d3f982..2068759 100644 --- a/aplcore/unit/embed/unittest_embedded_lifecycle.cpp +++ b/aplcore/unit/embed/unittest_embedded_lifecycle.cpp @@ -435,6 +435,37 @@ TEST_F(EmbeddedLifecycleTest, Finish) { ASSERT_EQ(kEventTypeFinish, root->popEvent().getType()); } +TEST_F(EmbeddedLifecycleTest, EmbeddedDocCommandCancelExecution) { + loadDocument(HOST_DOC); + + auto content = Content::create(EMBEDDED_DOC, session); + ASSERT_TRUE(content->isReady()); + + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + root->clearDirty(); + + auto cmd = JsonData(R"([{ + "type": "AnimateItem", + "componentId": "embeddedText", + "duration": "3000", + "easing": "linear", + "value": [ + { + "property": "opacity", + "to": "0.0" + } + ] + }])"); + ASSERT_TRUE(cmd); + + auto command = embeddedDocumentContext->executeCommands(cmd.get(), false); + root->cancelExecution(); + ASSERT_TRUE(command->isTerminated()); +} + const static char *PARENT_VC = R"({ "children": [ { @@ -1444,3 +1475,80 @@ TEST_F(EmbeddedLifecycleTest, ComplexScrollable) ASSERT_TRUE(CheckComponent(component->getCoreChildAt(2)->getCoreChildAt(0), 200, 20)); ASSERT_TRUE(CheckComponent(component->getCoreChildAt(3)->getCoreChildAt(0), 100, 10)); } + +static const char* HOST_WITH_PARAMETERS = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "item": { + "type": "Host", + "width": "100%", + "height": "100%", + "id": "hostComponent", + "source": "embeddedDocumentUrl", + "parameters": { + "ResolveMeFromHost": "World" + }, + "onLoad": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["LOADED"] + } + ], + "onFail": [ + { + "type": "SendEvent", + "sequencer": "SEND_EVENTER", + "arguments": ["FAILED"] + } + ] + } + } + } +})"; + +static const char* EMBEDDED_WITH_PARAMETERS = R"({ + "type": "APL", + "version": "2023.2", + "mainTemplate": { + "parameters": [ + "ResolveMeFromRuntime", + "ResolveMeFromHost", + "IAmUnusedYouKnow" + ], + "item": { + "type": "Text", + "id": "embeddedText", + "text": "${ResolveMeFromRuntime}, ${ResolveMeFromHost}${IAmUnusedYouKnow}" + } + } +})"; + +TEST_F(EmbeddedLifecycleTest, ParameterResolution) +{ + session = std::make_shared(); + loadDocument(HOST_WITH_PARAMETERS); + + auto requestWeak = documentManager->get("embeddedDocumentUrl"); + ASSERT_TRUE(requestWeak.lock()); + auto request = requestWeak.lock(); + ASSERT_EQ(request->getUrlRequest().getUrl(), "embeddedDocumentUrl"); + + auto embeddedSession = std::make_shared(); + auto content = Content::create(EMBEDDED_WITH_PARAMETERS, embeddedSession); + // Resolve what we have + content->addObjectData("ResolveMeFromRuntime", "Hello"); + // Still needs more + ASSERT_FALSE(content->isReady()); + + // Now request can be answered. + auto embeddedDocumentContext = documentManager->succeed("embeddedDocumentUrl", content, true); + ASSERT_TRUE(embeddedDocumentContext); + ASSERT_TRUE(CheckSendEvent(root, "LOADED")); + + auto embeddedText = root->findComponentById("embeddedText"); + ASSERT_EQ("Hello, World", embeddedText->getCalculated(kPropertyText).asString()); +} \ No newline at end of file diff --git a/aplcore/unit/engine/CMakeLists.txt b/aplcore/unit/engine/CMakeLists.txt index e2103bd..9805d9b 100644 --- a/aplcore/unit/engine/CMakeLists.txt +++ b/aplcore/unit/engine/CMakeLists.txt @@ -31,9 +31,11 @@ target_sources_local(unittest unittest_event_manager.cpp unittest_hover.cpp unittest_keyboard_manager.cpp + unittest_layout_handler.cpp unittest_layouts.cpp unittest_memory.cpp unittest_propdef.cpp + unittest_reactive_rebuilds.cpp unittest_resources.cpp unittest_styles.cpp unittest_visibility.cpp diff --git a/aplcore/unit/engine/unittest_builder_bind.cpp b/aplcore/unit/engine/unittest_builder_bind.cpp index 6494c05..fddd1d3 100644 --- a/aplcore/unit/engine/unittest_builder_bind.cpp +++ b/aplcore/unit/engine/unittest_builder_bind.cpp @@ -335,6 +335,173 @@ TEST_F(BuilderBindTest, MissingValue) ASSERT_TRUE(ConsoleMessage()); } +static const char *BINDING_TYPE = R"apl( +{ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "items": { + "type": "Container", + "bind": { + "name": "DataType", + "value": "${data}" + }, + "items": { + "type": "Text", + "bind": { + "name": "X", + "value": "${data}", + "type": "${DataType}" + }, + "text": "${DataType} ${data} ${X}" + }, + "data": [ + "", + "fuzzy ducks", + 23, + 23.3, + false, + true, + "23dp", + "23.3dp", + "green", + "#f3d423", + null + ] + }, + "data": [ + "any", + "boolean", + "string", + "number", + "dimension", + "color", + "foo" + ] + } + } +} +)apl"; + +static const std::vector> BINDING_TYPE_EXPECTED = { + { + "any ", + "any fuzzy ducks fuzzy ducks", + "any 23 23", + "any 23.3 23.3", + "any false false", + "any true true", + "any 23dp 23dp", + "any 23.3dp 23.3dp", + "any green green", + "any #f3d423 #f3d423", + "any ", + }, + { + "boolean false", + "boolean fuzzy ducks true", + "boolean 23 true", + "boolean 23.3 true", + "boolean false false", + "boolean true true", + "boolean 23dp true", + "boolean 23.3dp true", + "boolean green true", + "boolean #f3d423 true", + "boolean false", + }, + { + "string ", + "string fuzzy ducks fuzzy ducks", + "string 23 23", + "string 23.3 23.3", + "string false false", + "string true true", + "string 23dp 23dp", + "string 23.3dp 23.3dp", + "string green green", + "string #f3d423 #f3d423", + "string ", + }, + { + "number nan", + "number fuzzy ducks nan", + "number 23 23", + "number 23.3 23.3", + "number false 0", + "number true 1", + "number 23dp 23", + "number 23.3dp 23.3", + "number green nan", + "number #f3d423 nan", + "number nan", + }, + { + "dimension 0dp", + "dimension fuzzy ducks 0dp", + "dimension 23 23dp", + "dimension 23.3 23.3dp", + "dimension false 0dp", + "dimension true 0dp", + "dimension 23dp 23dp", + "dimension 23.3dp 23.3dp", + "dimension green 0dp", + "dimension #f3d423 0dp", + "dimension 0dp", + }, + { + "color #00000000", + "color fuzzy ducks #00000000", + "color 23 #00000017", + "color 23.3 #00000017", + "color false #00000000", + "color true #00000000", + "color 23dp #00000000", + "color 23.3dp #00000000", + "color green #008000ff", + "color #f3d423 #f3d423ff", + "color #00000000", + }, + { + "foo ", + "foo fuzzy ducks fuzzy ducks", + "foo 23 23", + "foo 23.3 23.3", + "foo false false", + "foo true true", + "foo 23dp 23dp", + "foo 23.3dp 23.3dp", + "foo green green", + "foo #f3d423 #f3d423", + "foo ", + }, +}; + +TEST_F(BuilderBindTest, BindingTypes) +{ + loadDocument(BINDING_TYPE); + ASSERT_TRUE(component); + + auto f = [&](int x, int y){ + return component->getChildAt(x)->getChildAt(y)->getCalculated(kPropertyText).asString(); + }; + + ASSERT_EQ(BINDING_TYPE_EXPECTED.size(), component->getChildCount()); + + for (int i = 0 ; i < BINDING_TYPE_EXPECTED.size() ; i++) { + const auto& expectedArray = BINDING_TYPE_EXPECTED.at(i); + ASSERT_EQ(expectedArray.size(), component->getChildAt(i)->getChildCount()); + for (int j = 0 ; j < expectedArray.size() ; j++) { + ASSERT_EQ(expectedArray.at(j), f(i, j)) << i << " " << j; + } + } + + // Clear console messages + session->clear(); +} + static const char *ON_CHANGE = R"apl( { "type": "APL", diff --git a/aplcore/unit/engine/unittest_context.cpp b/aplcore/unit/engine/unittest_context.cpp index 82fe996..f5f94ce 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("2024.1", env.get("aplVersion").asString()); + EXPECT_EQ("2024.2", 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("2024.1", env.get("documentAPLVersion").asString()); + EXPECT_EQ("2024.2", 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("2024.1", env.get("aplVersion").asString()); + EXPECT_EQ("2024.2", 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 4cae49b..1bcaab3 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("2024.1", evaluate(*context, "${environment.aplVersion}"))); + ASSERT_TRUE(IsEqual("2024.2", evaluate(*context, "${environment.aplVersion}"))); ASSERT_TRUE(IsEqual("1.9", evaluate(*context, "${environment.documentAPLVersion}"))); // The document background is evaluated is a special data-binding context diff --git a/aplcore/unit/engine/unittest_layout_handler.cpp b/aplcore/unit/engine/unittest_layout_handler.cpp new file mode 100644 index 0000000..b591355 --- /dev/null +++ b/aplcore/unit/engine/unittest_layout_handler.cpp @@ -0,0 +1,269 @@ +/** + * 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 LayoutHandlerTest : public DocumentWrapper {}; + +static const char *BASIC_TEST = R"({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": [ + { + "type": "Container", + "id": "parent", + "height": "100%", + "width": "100%", + "direction": "row", + "onLayout": { + "type": "SendEvent", + "sequencer": "LAYOUT_EVENT", + "arguments": [ + "${event.source.id}", + "${event.width}", + "${event.height}", + "${event.x}", + "${event.y}" + ] + }, + "items": [ + { + "type": "Frame", + "id": "f1", + "height": "50%", + "width": "200", + "background": "red", + "onLayout": { + "type": "SendEvent", + "sequencer": "LAYOUT_EVENT", + "arguments": [ + "${event.source.id}", + "${event.width}", + "${event.height}", + "${event.x}", + "${event.y}" + ] + } + }, + { + "type": "Frame", + "id": "f2", + "height": "50%", + "width": "30%", + "background": "green", + "onLayout": { + "type": "SendEvent", + "sequencer": "LAYOUT_EVENT", + "arguments": [ + "${event.source.id}", + "${event.width}", + "${event.height}", + "${event.x}", + "${event.y}" + ] + } + }, + { + "type": "Text", + "id": "f3", + "height": "50%", + "width": "auto", + "maxLines": 1, + "text": "Verry terrible text which does not fit.", + "onLayout": { + "type": "SendEvent", + "sequencer": "LAYOUT_EVENT", + "arguments": [ + "${event.source.id}", + "${event.width}", + "${event.height}", + "${event.x}", + "${event.y}" + ] + } + } + ] + } + ] + } +})"; + +TEST_F(LayoutHandlerTest, Basic) +{ + metrics.size(600, 600); + + loadDocument(BASIC_TEST); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "parent", 600, 600, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f1", 200, 300, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f2", 180, 300, 200, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f3", 390, 300, 380, 0)); +} + +TEST_F(LayoutHandlerTest, FireOnRelayout) +{ + metrics.size(600, 600); + + loadDocument(BASIC_TEST); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "parent", 600, 600, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f1", 200, 300, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f2", 180, 300, 200, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f3", 390, 300, 380, 0)); + + executeCommand("SetValue", {{"componentId", "f2"}, {"property", "width"}, {"value", 200}}, true); + advanceTime(1); + + ASSERT_TRUE(CheckSendEvent(root, "f2", 200, 300, 200, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f3", 390, 300, 400, 0)); +} + +TEST_F(LayoutHandlerTest, NoHandlerOnNoChange) +{ + metrics.size(600, 600); + + loadDocument(BASIC_TEST); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "parent", 600, 600, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f1", 200, 300, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f2", 180, 300, 200, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f3", 390, 300, 380, 0)); + + executeCommand("SetValue", {{"componentId", "f3"}, {"property", "width"}, {"value", 390}}, true); + advanceTime(1); + + ASSERT_FALSE(CheckSendEvent(root)); +} + +static const char *SCROLLABLE = R"({ + "type": "APL", + "version": "2023.3", + "theme": "dark", + "mainTemplate": { + "items": [ + { + "type": "Sequence", + "id": "parent", + "height": 200, + "width": 200, + "onMount": { + "type": "SendEvent", + "sequencer": "MOUNT_EVENT", + "arguments": [ "${event.source.id}" ] + }, + "onLayout": { + "type": "SendEvent", + "sequencer": "LAYOUT_EVENT", + "arguments": [ + "${event.source.id}", + "${event.width}", + "${event.height}", + "${event.x}", + "${event.y}" + ] + }, + "data": [1, 2, 3, 4, 5, 6], + "items": [ + { + "type": "Frame", + "id": "f${data}", + "height": 100, + "width": "100%", + "background": "red", + "onMount": { + "type": "SendEvent", + "sequencer": "MOUNT_EVENT", + "arguments": [ "${event.source.id}" ] + }, + "onLayout": { + "type": "SendEvent", + "sequencer": "LAYOUT_EVENT", + "arguments": [ + "${event.source.id}", + "${event.width}", + "${event.height}", + "${event.x}", + "${event.y}" + ] + } + } + ] + } + ] + } +})"; + +TEST_F(LayoutHandlerTest, LazyInflationAndLayout) +{ + metrics.size(600, 600); + config->set(apl::RootProperty::kSequenceChildCache, 1); + + loadDocument(SCROLLABLE); + + ASSERT_TRUE(component); + + // Let's judge it against onMount. Initial ones happen pretty much at the same time. + // Only first page laid out + ASSERT_TRUE(CheckSendEvent(root, "parent", 200, 200, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f1", 200, 100, 0, 0)); + ASSERT_TRUE(CheckSendEvent(root, "f2", 200, 100, 0, 100)); + ASSERT_TRUE(CheckSendEvent(root, "f3", 200, 100, 0, 200)); + + ASSERT_TRUE(CheckSendEvent(root, "parent")); + ASSERT_TRUE(CheckSendEvent(root, "f1")); + ASSERT_TRUE(CheckSendEvent(root, "f2")); + ASSERT_TRUE(CheckSendEvent(root, "f3")); + ASSERT_TRUE(CheckSendEvent(root, "f4")); + ASSERT_TRUE(CheckSendEvent(root, "f5")); + ASSERT_TRUE(CheckSendEvent(root, "f6")); + + // Next frame + advanceTime(1); + + // Lazilly laid out + ASSERT_TRUE(CheckSendEvent(root, "f4", 200, 100, 0, 300)); + ASSERT_TRUE(CheckSendEvent(root, "f5", 200, 100, 0, 400)); + + root->clearDirty(); + + // Scroll a bit + ASSERT_EQ(Point(), component->scrollPosition()); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(0,100), false)); + advanceTime(200); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,50), true)); + ASSERT_EQ(Point(0, 50), component->scrollPosition()); + advanceTime(200); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,0), true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(0,0), true)); + advanceTime(2600); + + ASSERT_TRUE(CheckDirty(component, kPropertyScrollPosition, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + + // Laid out after scrolling. + ASSERT_TRUE(CheckSendEvent(root, "f6", 200, 100, 0, 500)); +} diff --git a/aplcore/unit/engine/unittest_reactive_rebuilds.cpp b/aplcore/unit/engine/unittest_reactive_rebuilds.cpp new file mode 100644 index 0000000..e8e3b35 --- /dev/null +++ b/aplcore/unit/engine/unittest_reactive_rebuilds.cpp @@ -0,0 +1,2177 @@ +/** +* 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 ReactiveRebuilds : public DocumentWrapper {}; + +inline + ::testing::AssertionResult + VerifyChild(const CoreComponentPtr& component, int index, uint32_t expectedColor, int expectedIndex, int expectedOrdinal = -1) { + auto color = component->getChildAt(index)->getCalculated(kPropertyBackground).getColor(); + if (color != expectedColor) + return ::testing::AssertionFailure() << "Color mismatch: " << color << " != " << expectedColor; + + auto actualIndex = component->getCoreChildAt(index)->getContext()->opt("index").asInt(); + if (expectedIndex >= 0 && actualIndex != expectedIndex) + return ::testing::AssertionFailure() << "Index mismatch: " << actualIndex << " != " << expectedIndex; + + auto actualOrdinal = component->getCoreChildAt(index)->getContext()->opt("ordinal").asInt(); + if (expectedOrdinal >= 0 && actualOrdinal != expectedOrdinal) + return ::testing::AssertionFailure() << "Ordinal mismatch: " << actualOrdinal << " != " << expectedOrdinal; + + return ::testing::AssertionSuccess(); +} + +static const char *DYNAMIC_CONDITIONAL_ITEMS = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "parameters": ["Item0", "Item1", "Item2", "Item3", "Item4"], + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "bind": [ + { "name": "SelectorItem0", "value": "${Item0}" }, + { "name": "SelectorItem1", "value": "${Item1}" }, + { "name": "SelectorItem2", "value": "${Item2}" }, + { "name": "SelectorItem3", "value": "${Item3}" }, + { "name": "SelectorItem4", "value": "${Item4}" } + ], + "numbered": true, + "items": [ + { + "when": "${SelectorItem0 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem0 == 2}", + "type": "Frame", + "background": "green" + }, + { + "when": "${SelectorItem1 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem2 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem3 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem3 == 2}", + "type": "Frame", + "background": "green" + }, + { + "when": "${SelectorItem4 == 1}", + "type": "Frame", + "background": "red" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalItems) { + loadDocument(DYNAMIC_CONDITIONAL_ITEMS, R"({ + "Item0": 1, + "Item1": 1, + "Item2": 0, + "Item3": 2, + "Item4": 1 + })"); + + auto initialDependentCount = getAliveCountersFor("Dependant"); + auto initialContextCount = getAliveCountersFor("Context"); + + ASSERT_TRUE(component); + ASSERT_EQ(4, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1, 2)); + ASSERT_TRUE(VerifyChild(component, 2, 0x008000FF, 2, 3)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3, 4)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 2 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem3" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(4, component->getChildCount()); + + clearDirty(); + root->clearVisualContextDirty(); + + ASSERT_TRUE(VerifyChild(component, 0, 0x008000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1, 2)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2, 3)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3, 4)); + + initialDependentCount -= 2;// Two links to params broken + initialContextCount += 1; // Command action ctx + + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + ASSERT_TRUE(CheckAliveCountersNotChanged("Context", initialContextCount)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 1 } + }, + false); + + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(4, component->getChildCount()); + + clearDirty(); + root->clearVisualContextDirty(); + + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1, 2)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2, 3)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3, 4)); + + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + ASSERT_TRUE(CheckAliveCountersNotChanged("Context", initialContextCount)); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalItemsDissapear) { + loadDocument(DYNAMIC_CONDITIONAL_ITEMS, R"({ + "Item0": 1, + "Item1": 1, + "Item2": 0, + "Item3": 2, + "Item4": 1 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(4, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1, 2)); + ASSERT_TRUE(VerifyChild(component, 2, 0x008000FF, 2, 3)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3, 4)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 0 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem3" }, + { "value", 0 } + }, + false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1, 2)); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalItemsAppear) { + loadDocument(DYNAMIC_CONDITIONAL_ITEMS, R"({ + "Item0": 0, + "Item1": 1, + "Item2": 0, + "Item3": 2, + "Item4": 1 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 1, 2)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2, 3)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 2 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem2" }, + { "value", 1 } + }, + false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(5, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0x008000FF, 0, 1)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1, 2)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2, 3)); + ASSERT_TRUE(VerifyChild(component, 3, 0x008000FF, 3, 4)); + ASSERT_TRUE(VerifyChild(component, 4, 0xFF0000FF, 4, 5)); +} + +static const char *DYNAMIC_CONDITIONAL_FIRST_LAST = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "parameters": [ "First", "Item0", "Item1", "Item2", "Item3", "Item4", "Last" ], + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "bind": [ + { "name": "SelectorFirst", "value": "${First}" }, + { "name": "SelectorItem0", "value": "${Item0}" }, + { "name": "SelectorItem1", "value": "${Item1}" }, + { "name": "SelectorItem2", "value": "${Item2}" }, + { "name": "SelectorItem3", "value": "${Item3}" }, + { "name": "SelectorItem4", "value": "${Item4}" }, + { "name": "SelectorLast", "value": "${Last}" } + ], + "firstItem": [ + { + "when": "${SelectorFirst == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorFirst == 2}", + "type": "Frame", + "background": "green" + } + ], + "items": [ + { + "when": "${SelectorItem0 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem0 == 2}", + "type": "Frame", + "background": "green" + }, + { + "when": "${SelectorItem1 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem2 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem3 == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorItem3 == 2}", + "type": "Frame", + "background": "green" + }, + { + "when": "${SelectorItem4 == 1}", + "type": "Frame", + "background": "red" + } + ], + "lastItem": [ + { + "when": "${SelectorLast == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorLast == 2}", + "type": "Frame", + "background": "green" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalFirstLast) { + loadDocument(DYNAMIC_CONDITIONAL_FIRST_LAST, R"({ + "First": 1, + "Item0": 1, + "Item1": 0, + "Item2": 0, + "Item3": 1, + "Item4": 0, + "Last": 1 + })"); + + auto initialDependentCount = getAliveCountersFor("Dependant"); + auto initialContextCount = getAliveCountersFor("Context"); + + ASSERT_TRUE(component); + ASSERT_EQ(4, component->getChildCount()); + + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 2 } + }, + false); + advanceTime(1); + + initialDependentCount -= 1;// One link to params broken + initialContextCount += 1; // Command action ctx + + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + ASSERT_TRUE(CheckAliveCountersNotChanged("Context", initialContextCount)); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 2 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 2 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + clearDirty(); + root->clearVisualContextDirty(); + + ASSERT_EQ(0x008000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_EQ(0x008000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + initialDependentCount -= 2;// Two links to params broken + + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + ASSERT_TRUE(CheckAliveCountersNotChanged("Context", initialContextCount)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 1 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + clearDirty(); + root->clearVisualContextDirty(); + + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + ASSERT_TRUE(CheckAliveCountersNotChanged("Context", initialContextCount)); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalFirstLastDisappear) { + loadDocument(DYNAMIC_CONDITIONAL_FIRST_LAST, R"({ + "First": 1, + "Item0": 1, + "Item1": 1, + "Item2": 0, + "Item3": 2, + "Item4": 0, + "Last": 1 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(5, component->getChildCount()); + + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_TRUE(VerifyChild(component, 3, 0x008000FF, 2)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(4)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem1" }, + { "value", 0 } + }, + false); + advanceTime(1); + + ASSERT_EQ(4, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0x008000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 0 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 0 } + }, + false); + advanceTime(1); + + ASSERT_EQ(2, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 1)); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalFirstLastAppear) { + loadDocument(DYNAMIC_CONDITIONAL_FIRST_LAST, R"({ + "First": 0, + "Item0": 0, + "Item1": 0, + "Item2": 0, + "Item3": 2, + "Item4": 0, + "Last": 0 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(VerifyChild(component, 0, 0x008000FF, 0)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem2" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_EQ(2, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 1)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 1 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 2 } + }, + false); + advanceTime(1); + + ASSERT_EQ(4, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0x008000FF, 1)); + ASSERT_EQ(0x008000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); +} + +static const char *DYNAMIC_CONDITIONAL_LAYOUT_FIRST_LAST = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "Semaphore": { + "parameters": [ + { + "name": "Selector", + "type": "number" + } + ], + "items": [ + { + "when": "${Selector == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${Selector == 2}", + "type": "Frame", + "background": "green" + } + ] + } + }, + "mainTemplate": { + "parameters": [ "First", "Item0", "Item1", "Item2", "Item3", "Item4", "Last" ], + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "bind": [ + { "name": "SelectorFirst", "value": "${First}" }, + { "name": "SelectorItem0", "value": "${Item0}" }, + { "name": "SelectorItem1", "value": "${Item1}" }, + { "name": "SelectorItem2", "value": "${Item2}" }, + { "name": "SelectorItem3", "value": "${Item3}" }, + { "name": "SelectorItem4", "value": "${Item4}" }, + { "name": "SelectorLast", "value": "${Last}" } + ], + "firstItem": [ + { + "type": "Semaphore", + "Selector": "${SelectorFirst}" + } + ], + "items": [ + { + "type": "Semaphore", + "Selector": "${SelectorItem0}" + }, + { + "type": "Semaphore", + "Selector": "${SelectorItem1}" + }, + { + "type": "Semaphore", + "Selector": "${SelectorItem2}" + }, + { + "type": "Semaphore", + "Selector": "${SelectorItem3}" + }, + { + "type": "Semaphore", + "Selector": "${SelectorItem4}" + } + ], + "lastItem": [ + { + "type": "Semaphore", + "Selector": "${SelectorLast}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalLayoutsFirstLast) { + loadDocument(DYNAMIC_CONDITIONAL_LAYOUT_FIRST_LAST, R"({ + "First": 1, + "Item0": 1, + "Item1": 0, + "Item2": 0, + "Item3": 1, + "Item4": 0, + "Last": 1 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(4, component->getChildCount()); + + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 2 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 2 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 2 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0x008000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0x008000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalLayoutsDisappear) { + loadDocument(DYNAMIC_CONDITIONAL_LAYOUT_FIRST_LAST, R"({ + "First": 1, + "Item0": 1, + "Item1": 0, + "Item2": 0, + "Item3": 1, + "Item4": 0, + "Last": 1 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(4, component->getChildCount()); + + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 0 } + }, + false); + advanceTime(1); + + ASSERT_EQ(3, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(2)->getCalculated(kPropertyBackground).getColor()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 0 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 0 } + }, + false); + advanceTime(1); + + ASSERT_EQ(1, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0)); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalLayoutsAppear) { + loadDocument(DYNAMIC_CONDITIONAL_LAYOUT_FIRST_LAST, R"({ + "First": 0, + "Item0": 0, + "Item1": 0, + "Item2": 0, + "Item3": 1, + "Item4": 0, + "Last": 0 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(1, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_EQ(2, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 1 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_EQ(4, component->getChildCount()); + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); +} + +static const char *DYNAMIC_CONDITIONAL_DEEP_LAYOUT = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "SemaphoreDeepNegative": { + "parameters": [ + { + "name": "SelectorDeep", + "type": "number" + } + ], + "items": [ + { + "when": "${SelectorDeep == -1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${SelectorDeep == -2}", + "type": "Frame", + "background": "green" + } + ] + }, + "SemaphoreDeepPositive": { + "parameters": [ + { + "name": "SelectorDeep", + "type": "number" + } + ], + "items": [ + { + "when": "${SelectorDeep == 1}", + "type": "Frame", + "background": "blue" + }, + { + "when": "${SelectorDeep == 2}", + "type": "Frame", + "background": "yellow" + } + ] + }, + "Semaphore": { + "parameters": [ + { + "name": "Selector", + "type": "number" + } + ], + "items": [ + { + "when": "${Selector < 0}", + "type": "SemaphoreDeepNegative", + "SelectorDeep": "${Selector}" + }, + { + "when": "${Selector > 0}", + "type": "SemaphoreDeepPositive", + "SelectorDeep": "${Selector}" + } + ] + } + }, + "mainTemplate": { + "parameters": [ "First", "Item0", "Item1", "Last" ], + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "bind": [ + { "name": "SelectorFirst", "value": "${First}" }, + { "name": "Selector0", "value": "${Item0}" }, + { "name": "Selector1", "value": "${Item1}" }, + { "name": "SelectorLast", "value": "${Last}" } + ], + "firstItem": [ + { + "type": "Semaphore", + "Selector": "${SelectorFirst}" + } + ], + "items": [ + { + "type": "Semaphore", + "Selector": "${Selector0}" + }, + { + "type": "Semaphore", + "Selector": "${Selector1}" + } + ], + "lastItem": [ + { + "type": "Semaphore", + "Selector": "${SelectorLast}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalDeepLayout) { + loadDocument(DYNAMIC_CONDITIONAL_DEEP_LAYOUT, R"({ + "First": -1, + "Item0": -1, + "Item1": -1, + "Last": -1 + })"); + ASSERT_TRUE(component); + ASSERT_EQ(0xFF0000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + auto initialDependentCount = getAliveCountersFor("Dependant"); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", -2 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0x008000FF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + clearDirty(); + root->clearVisualContextDirty(); + initialDependentCount -= 1; + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorFirst" }, + { "value", 1 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "Item0" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0x0000FFFF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0x0000FFFF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 1)); + ASSERT_EQ(0xFF0000FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + clearDirty(); + root->clearVisualContextDirty(); + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorLast" }, + { "value", 2 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "Item1" }, + { "value", 2 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_EQ(0x0000FFFF, component->getChildAt(0)->getCalculated(kPropertyBackground).getColor()); + ASSERT_TRUE(VerifyChild(component, 1, 0x0000FFFF, 0)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFFFF00FF, 1)); + ASSERT_EQ(0xFFFF00FF, component->getChildAt(3)->getCalculated(kPropertyBackground).getColor()); + + clearDirty(); + root->clearVisualContextDirty(); + initialDependentCount -= 1; + ASSERT_TRUE(CheckAliveCountersNotChanged("Dependant", initialDependentCount)); +} + +static const char *SEMI_DYNAMIC_CONTAINER = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "Semaphore": { + "parameters": [ + { + "name": "Selector", + "type": "number" + } + ], + "items": [ + { + "when": "${Selector == 1}", + "type": "Frame", + "background": "red" + }, + { + "when": "${Selector >= 2}", + "type": "Frame", + "background": "green" + } + ] + } + }, + "mainTemplate": { + "parameters": [ "Item0", "Item1" ], + "item": { + "type": "Container", + "height": "100%", + "width": "100%", + "bind": [ + { "name": "SelectorItem0", "value": "${Item0}" }, + { "name": "SelectorItem1", "value": "${Item1}" } + ], + "items": [ + { + "type": "Semaphore", + "Selector": "${SelectorItem0}" + }, + { + "type": "Semaphore", + "Selector": "${SelectorItem1}" + }, + { + "type": "Semaphore", + "Selector": 1 + }, + { + "type": "Semaphore", + "Selector": 1 + }, + { + "type": "Semaphore", + "Selector": 1 + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, SemiDynamicOptimizationNoChange) { + loadDocument(SEMI_DYNAMIC_CONTAINER, R"({ + "Item0": 2, + "Item1": 2 + })"); + + ASSERT_TRUE(component); + ASSERT_EQ(5, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0x008000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 1)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3)); + ASSERT_TRUE(VerifyChild(component, 4, 0xFF0000FF, 4)); + + auto child0 = component->getChildAt(0); + auto child1 = component->getChildAt(1); + auto child2 = component->getChildAt(2); + auto child3 = component->getChildAt(3); + auto child4 = component->getChildAt(4); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem1" }, + { "value", 3 } + }, + false); + advanceTime(1); + + ASSERT_FALSE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(VerifyChild(component, 0, 0x008000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0x008000FF, 1)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3)); + ASSERT_TRUE(VerifyChild(component, 4, 0xFF0000FF, 4)); + + ASSERT_EQ(child0, component->getChildAt(0)); + ASSERT_EQ(child1, component->getChildAt(1)); + ASSERT_EQ(child2, component->getChildAt(2)); + ASSERT_EQ(child3, component->getChildAt(3)); + ASSERT_EQ(child4, component->getChildAt(4)); +} + +TEST_F(ReactiveRebuilds, SemiDynamicOptimizationReorder) { + loadDocument(SEMI_DYNAMIC_CONTAINER, R"({ + "Item0": 2, + "Item1": 0 + })"); + + ASSERT_TRUE(component); + ASSERT_EQ(4, component->getChildCount()); + + ASSERT_TRUE(VerifyChild(component, 0, 0x008000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3)); + + auto child1 = component->getChildAt(1); + auto child2 = component->getChildAt(2); + auto child3 = component->getChildAt(3); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem0" }, + { "value", 0 } + }, + false); + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "SelectorItem1" }, + { "value", 1 } + }, + false); + advanceTime(1); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + ASSERT_TRUE(VerifyChild(component, 0, 0xFF0000FF, 0)); + ASSERT_TRUE(VerifyChild(component, 1, 0xFF0000FF, 1)); + ASSERT_TRUE(VerifyChild(component, 2, 0xFF0000FF, 2)); + ASSERT_TRUE(VerifyChild(component, 3, 0xFF0000FF, 3)); + + ASSERT_EQ(child1, component->getChildAt(1)); + ASSERT_EQ(child2, component->getChildAt(2)); + ASSERT_EQ(child3, component->getChildAt(3)); +} + +static const char *FRAME_CONDITIONAL_CHILD_OLD = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "items": { + "type": "Frame", + "bind": [ + { "name": "Selector", "value": false } + ], + "item": [ + { + "when": "${Selector}", + "type": "Text", + "text": "Selected" + }, + { + "type": "Text", + "text": "Selector: ${Selector}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalChildOld) +{ + loadDocument(FRAME_CONDITIONAL_CHILD_OLD); + + ASSERT_EQ("Selector: false", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Selector"}, { "value", true } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ("Selector: true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); +} + +static const char *FRAME_CONDITIONAL_CHILD = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "items": { + "type": "Frame", + "bind": [ + { "name": "Selector", "value": false } + ], + "item": [ + { + "when": "${Selector}", + "type": "Text", + "text": "Selected" + }, + { + "type": "Text", + "text": "Selector: ${Selector}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalChild) +{ + loadDocument(FRAME_CONDITIONAL_CHILD); + + ASSERT_EQ("Selector: false", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Selector"}, { "value", true } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); +} + +static const char *FRAME_CONDITIONAL_LAYOUT = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "Selected": { + "items": [ + { + "type": "Text", + "text": "Selected" + } + ] + }, + "KindaSelected": { + "parameters": [ + { + "name": "Selector", + "type": "boolean" + } + ], + "items": [ + { + "type": "Text", + "text": "Selector: ${Selector}" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "bind": [ + { "name": "Input", "value": false } + ], + "item": [ + { + "when": "${Input}", + "type": "Selected" + }, + { + "type": "KindaSelected", + "Selector": "${Input}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalLayout) +{ + loadDocument(FRAME_CONDITIONAL_LAYOUT); + + ASSERT_EQ("Selector: false", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Input"}, { "value", true } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); +} + +static const char *FRAME_CONDITIONAL_EMPTY = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "parameters": [ "InputSelector" ], + "items": { + "type": "Frame", + "bind": [ + { "name": "Selector", "value": "${InputSelector}" } + ], + "item": [ + { + "when": "${Selector}", + "type": "Text", + "text": "Selected" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalChildDisappear) +{ + loadDocument(FRAME_CONDITIONAL_EMPTY, R"({ "InputSelector": true })"); + + ASSERT_EQ(1, component->getChildCount()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Selector"}, { "value", false } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(0, component->getChildCount()); +} + +TEST_F(ReactiveRebuilds, FrameConditionalChildAppear) +{ + loadDocument(FRAME_CONDITIONAL_EMPTY, R"({ "InputSelector": false })"); + + ASSERT_EQ(0, component->getChildCount()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Selector"}, { "value", true } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(1, component->getChildCount()); +} + +static const char *FRAME_CONDITIONAL_DEEP_LAYOUT = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "Selector": { + "parameters": [ + { + "name": "Selector", + "type": "boolean" + } + ], + "items": [ + { + "when": "${Selector}", + "type": "Text", + "text": "Selected" + }, + { + "type": "Text", + "text": "Selector: ${Selector}" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "bind": [ + { "name": "Input", "value": false } + ], + "item": [ + { + "type": "Selector", + "Selector": "${Input}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalDeepLayout) +{ + loadDocument(FRAME_CONDITIONAL_DEEP_LAYOUT); + + ASSERT_EQ("Selector: false", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Input"}, { "value", true } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); +} + +static const char *FRAME_CONDITIONAL_NO_CHANGE = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "items": { + "type": "Frame", + "bind": [ + { "name": "Selector", "value": 1 } + ], + "item": [ + { + "when": "${Selector > 0}", + "type": "Text", + "text": "Selected" + }, + { + "type": "Text", + "text": "Selector: ${Selector}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalNoChange) +{ + loadDocument(FRAME_CONDITIONAL_NO_CHANGE); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Selector"}, { "value", 2 } }, false); + root->clearPending(); + + ASSERT_FALSE(CheckDirty(component, kPropertyNotifyChildrenChanged)); +} + +static const char *FRAME_CONDITIONAL_LAYOUT_NO_CHANGE = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "Selector": { + "parameters": [ + { + "name": "Selector", + "type": "number" + } + ], + "items": [ + { + "when": "${Selector > 0}", + "type": "Text", + "text": "Selected" + }, + { + "type": "Text", + "text": "Selector: ${Selector}" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Frame", + "bind": [ + { "name": "Input", "value": 1 } + ], + "item": [ + { + "type": "Selector", + "Selector": "${Input}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalLayoutNoChange) +{ + loadDocument(FRAME_CONDITIONAL_LAYOUT_NO_CHANGE); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Input"}, { "value", 2 } }, false); + root->clearPending(); + + ASSERT_FALSE(CheckDirty(component, kPropertyNotifyChildrenChanged)); +} + +static const char *FRAME_CONDITIONAL_LAYOUT_EXISTENCE = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "Selector": { + "parameters": [ + { + "name": "Selector", + "type": "boolean" + } + ], + "items": [ + { + "when": "${Selector}", + "type": "Text", + "text": "Selected" + } + ] + } + }, + "mainTemplate": { + "parameters": [ "InputParameter" ], + "items": { + "type": "Frame", + "bind": [ + { "name": "Input", "value": "${InputParameter}" } + ], + "item": [ + { + "type": "Selector", + "Selector": "${Input}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, FrameConditionalLayoutAppear) +{ + loadDocument(FRAME_CONDITIONAL_LAYOUT_EXISTENCE, R"( { "InputParameter": false } )"); + + ASSERT_EQ(0, component->getChildCount()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Input"}, { "value", true } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); +} + +TEST_F(ReactiveRebuilds, FrameConditionalLayoutDisappear) +{ + loadDocument(FRAME_CONDITIONAL_LAYOUT_EXISTENCE, R"( { "InputParameter": true } )"); + + ASSERT_EQ("Selected", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + + executeCommand("SetValue", { { "componentId", ":root" }, { "property", "Input"}, { "value", false } }, false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(0, component->getChildCount()); +} + +static const char *SAME_ITEM_NOT_RECREATED = R"apl({ + "type": "APL", + "version": "2024.1", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "item": { + "type": "Frame", + "bind": [ + { "name": "Flag", "value": 1 } + ], + "height": 300, + "width": 200, + "item": [ + { + "when": "${Flag > 0}", + "type": "Frame", + "height": "100%", + "width": "100%", + "background": "red" + + }, + { + "type": "Frame", + "height": "100%", + "width": "100%", + "background": "blue" + } + ] + } + } +})apl"; + +TEST_F(ReactiveRebuilds, SameItemNotRecalculate) { + loadDocument(SAME_ITEM_NOT_RECREATED); + + ASSERT_TRUE(component); + + auto childId = component->getCoreChildAt(0)->getUniqueId(); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "Flag" }, + { "value", 2 } + }, + false); + + advanceTime(17); + + ASSERT_EQ(childId, component->getCoreChildAt(0)->getUniqueId()); +} + +static const char *DYNAMIC_CONDITIONAL_OLD = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "data": "${TestArray}", + "items": [ + { + "when": "${data > 1}", + "type": "Text", + "text": "Definitely more than 1 : ${data > 1}" + }, + { + "type": "Text", + "text": "Maybe 1 : ${data == 1}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalOld) { + auto myArray = LiveArray::create(ObjectArray{1, 101, 1}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_OLD); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 1); + myArray->update(2, 101); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component)); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : false", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : false", component->getChildAt(2)->getCalculated(kPropertyText).asString()); +} + +static const char *DYNAMIC_CONDITIONAL_DEEP_OLD = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "data": "${TestArray}", + "items": [ + { + "type": "Frame", + "item": [ + { + "when": "${data > 1}", + "type": "Text", + "text": "Definitely more than 1 : ${data > 1}" + }, + { + "type": "Text", + "text": "Maybe 1 : ${data == 1}" + } + ] + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalDeepOld) { + auto myArray = LiveArray::create(ObjectArray{1, 101, 1}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_DEEP_OLD); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getChildAt(0)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 1); + myArray->update(2, 101); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component)); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : false", component->getChildAt(1)->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : false", component->getChildAt(2)->getChildAt(0)->getCalculated(kPropertyText).asString()); +} + +static const char *DYNAMIC_CONDITIONAL_CHANGE = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "item": { + "type": "Container", + "data": "${TestArray}", + "items": [ + { + "when": "${data > 1}", + "type": "Text", + "text": "Definitely more than 1 : ${data > 1}" + }, + { + "when": "${data == 1}", + "type": "Text", + "text": "Maybe 1 : ${data == 1}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalNew) { + auto myArray = LiveArray::create(ObjectArray{1, 101, 1}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_CHANGE); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + auto unchangedUid = component->getChildAt(0)->getUniqueId(); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 1); + myArray->update(2, 101); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(unchangedUid, component->getChildAt(0)->getUniqueId()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 101); + myArray->update(2, 1); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(unchangedUid, component->getChildAt(0)->getUniqueId()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalDisappear) { + auto myArray = LiveArray::create(ObjectArray{1, 101, 1}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_CHANGE); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 0); + myArray->update(2, 101); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(2, component->getChildCount()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); +} + +TEST_F(ReactiveRebuilds, DynamicConditionalAppear) { + auto myArray = LiveArray::create(ObjectArray{1, 0, 101}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_CHANGE); + ASSERT_TRUE(component); + ASSERT_EQ(2, component->getChildCount()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 1); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(3, component->getChildCount()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); +} + +static const char *DYNAMIC_CONDITIONAL_LAYOUT = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "layouts": { + "TestLayout": { + "parameters": [ + { + "name": "MaybeOne", + "type": "number" + } + ], + "bind": [ { "name": "Moar", "value": "${MaybeOne > 1}" } ], + "item": [ + { + "when": "${Moar}", + "type": "Text", + "text": "Definitely more than 1 : ${Moar}" + }, + { + "type": "Text", + "text": "Maybe 1 : ${MaybeOne == 1}" + } + ] + } + }, + "mainTemplate": { + "item": { + "type": "Container", + "data": "${TestArray}", + "items": { + "type": "TestLayout", + "MaybeOne": "${data}" + } + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalLayout) { + auto myArray = LiveArray::create(ObjectArray{1, 101, 1}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_LAYOUT); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + auto unchangedUid = component->getChildAt(0)->getUniqueId(); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); + + myArray->update(1, 1); + myArray->update(2, 101); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(unchangedUid, component->getChildAt(0)->getUniqueId()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); +} + +static const char *DYNAMIC_CONDITIONAL_EXTERNALLY_DEPENDANT = R"({ + "type": "APL", + "version": "2024.2", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "bind": [ + { "name": "Selector", "value": true } + ], + "item": { + "type": "Container", + "data": "${TestArray}", + "items": [ + { + "when": "${data > 1 && Selector}", + "type": "Text", + "text": "Definitely more than 1 : ${data > 1}" + }, + { + "when": "${data >= 1}", + "type": "Text", + "text": "Maybe 1 : ${data == 1}" + } + ] + } + } +})"; + +TEST_F(ReactiveRebuilds, DynamicConditionalExternallyDependant) { + auto myArray = LiveArray::create(ObjectArray{1, 101, 1}); + config->liveData("TestArray", myArray); + + loadDocument(DYNAMIC_CONDITIONAL_EXTERNALLY_DEPENDANT); + ASSERT_TRUE(component); + ASSERT_EQ(3, component->getChildCount()); + + auto unchangedUid = component->getChildAt(0)->getUniqueId(); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Definitely more than 1 : true", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "Selector" }, + { "value", false} + }, + false); + root->clearPending(); + + ASSERT_TRUE(CheckDirty(component, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(unchangedUid, component->getChildAt(0)->getUniqueId()); + + ASSERT_EQ("Maybe 1 : true", component->getChildAt(0)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : false", component->getChildAt(1)->getCalculated(kPropertyText).asString()); + ASSERT_EQ("Maybe 1 : true", component->getChildAt(2)->getCalculated(kPropertyText).asString()); +} + +static const char *PROPERTY_PRESERVE_SINGLE = R"apl({ + "type": "APL", + "version": "2024.1", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "item": { + "type": "Frame", + "bind": [ + { "name": "SequenceItems", "value": [ "red", "blue", "green", "yellow", "purple", "cyan" ] }, + { "name": "VerticalOrientation", "value": true } + ], + "height": 300, + "width": 200, + "item": [ + { + "when": "${VerticalOrientation}", + "type": "Sequence", + "id": "Scrollable", + "preserve": ["scrollOffset"], + "scrollDirection": "vertical", + "height": "100%", + "width": "100%", + "data": "${SequenceItems}", + "item": { + "type": "Frame", + "width": "100%", + "height": 100, + "background": "${data}", + "item": { + "type": "Text", + "id": "Indicator${index}", + "width": "100%", + "height": "100%", + "text": "${data} : ${index}" + } + } + }, + { + "type": "Sequence", + "id": "Scrollable", + "preserve": ["scrollOffset"], + "scrollDirection": "horizontal", + "height": "100%", + "width": "100%", + "data": "${SequenceItems}", + "item": { + "type": "Frame", + "width": 100, + "height": "100%", + "background": "${data}", + "item": { + "type": "Text", + "id": "Indicator${index}", + "width": "100%", + "height": "100%", + "text": "${data} : ${index}" + } + } + } + ] + } + } +})apl"; + +TEST_F(ReactiveRebuilds, PropertyReserveSingle) { + loadDocument(PROPERTY_PRESERVE_SINGLE); + + ASSERT_TRUE(component); + + auto sequence = component->getCoreChildAt(0); + ASSERT_EQ(apl::kScrollDirectionVertical, sequence->getCalculated(apl::kPropertyScrollDirection).asInt()); + ASSERT_EQ(0, sequence->scrollPosition().getY()); + + executeCommand( + "Scroll", + { + {"componentId", "Scrollable"}, + {"distance", 1}, + {"screenLock", true} + }, + false); + advanceTime(2000); + ASSERT_EQ(300, sequence->scrollPosition().getY()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "VerticalOrientation" }, + { "value", false } + }, + false); + + // Needs two frames. First processes any changes, another one performs layout. Why? To avoid + // infinite layout cycle in one frame. + advanceTime(17); + advanceTime(17); + + sequence = component->getCoreChildAt(0); + ASSERT_EQ(apl::kScrollDirectionHorizontal, sequence->getCalculated(apl::kPropertyScrollDirection).asInt()); + ASSERT_EQ(300, sequence->scrollPosition().getX()); +} + +static const char *PROPERTY_PRESERVE_DATA = R"apl({ + "type": "APL", + "version": "2024.1", + "settings": { + "-experimentalIsReactive": true + }, + "mainTemplate": { + "item": { + "type": "Sequence", + "bind": [ + { "name": "Frameless", "value": true } + ], + "id": "Container", + "scrollDirection": "vertical", + "height": 500, + "width": 500, + "data": [ + "red", + "blue", + "green", + "yellow", + "purple" + ], + "items": [ + { + "when": "${Frameless}", + "type": "Text", + "preserve": ["text"], + "id": "Indicator${index}", + "width": "100%", + "height": 100, + "text": "${data} : ${index}" + }, + { + "type": "Frame", + "width": "100%", + "height": 100, + "background": "${data}", + "item": { + "type": "Text", + "preserve": ["text"], + "id": "Indicator${index}", + "width": "100%", + "height": "100%", + "text": "${data} : ${index}" + } + } + ] + } + } +})apl"; + +TEST_F(ReactiveRebuilds, PropertyReserveData) { + loadDocument(PROPERTY_PRESERVE_DATA); + + ASSERT_TRUE(component); + + auto comp = component->findComponentById("Indicator2", false); + auto uid = comp->getUniqueId(); + ASSERT_EQ("green : 2", comp->getCalculated(apl::kPropertyText).asString()); + + executeCommand( + "SetValue", + { + { "componentId", "Indicator2" }, + { "property", "text" }, + { "value", "Replaced" } + }, + false); + + advanceTime(17); + + comp = component->findComponentById("Indicator2", false); + ASSERT_EQ(uid, comp->getUniqueId()); + ASSERT_EQ("Replaced", comp->getCalculated(apl::kPropertyText).asString()); + + executeCommand( + "SetValue", + { + { "componentId", ":root" }, + { "property", "Frameless" }, + { "value", false } + }, + false); + + advanceTime(17); + + comp = component->findComponentById("Indicator2", false); + ASSERT_NE(uid, comp->getUniqueId()); + ASSERT_EQ("Replaced", comp->getCalculated(apl::kPropertyText).asString()); +} diff --git a/aplcore/unit/engine/unittest_visibility.cpp b/aplcore/unit/engine/unittest_visibility.cpp index 8fff1dd..a5c2918 100644 --- a/aplcore/unit/engine/unittest_visibility.cpp +++ b/aplcore/unit/engine/unittest_visibility.cpp @@ -28,6 +28,7 @@ class ViewabilityTest : public DocumentWrapper { } bool collectVisibilityChanges() { + root->clearPending(); auto hasChanges = false; while (root->hasEvent()) { auto event = root->popEvent(); @@ -678,3 +679,75 @@ TEST_F(ViewabilityTest, EventOrdering) ASSERT_FALSE(CheckVisibilityChange("boxred:1:0.75")); ASSERT_TRUE(CheckVisibilityChange("boxgreen:1:0.75")); } + +static const char *SIMPLE_SCROLL_VIEW = R"({ + "type": "APL", + "version": "2024.1", + "mainTemplate": { + "item": { + "type": "Container", + "width": "100%", + "height": "100%", + "bind": [ + { + "name": "Percentage", + "value": -1 + } + ], + "items": [ + { + "type": "Text", + "text": "${Percentage}" + }, + { + "type": "ScrollView", + "width": "100%", + "height": 500, + "item": { + "type": "Container", + "width": "100%", + "height": 1000, + "items": { + "type": "Frame", + "width": 100, + "height": 100, + "position": "absolute", + "top": 500, + "borderWidth": 2, + "borderColor": "blue", + "handleVisibilityChange": { + "commands": [ + { + "type": "SetValue", + "componentId": ":root", + "property": "Percentage", + "value": "${event.visibleRegionPercentage}" + } + ] + } + } + } + } + ] + } + } +})"; + +TEST_F(ViewabilityTest, SimpleScrollView) +{ + loadDocument(SIMPLE_SCROLL_VIEW); + + ASSERT_TRUE(component); + advanceTime(16); + + ASSERT_EQ("0", component->getChildAt(0)->getCalculated(apl::kPropertyText).asString()); + + executeCommand("Scroll", {{"componentId", ":root:child(1)"}, {"distance", 2}}, false); + + // Advance 100 frames + for (auto i = 0; i < 100; i++) { + advanceTime(16); + } + + ASSERT_EQ("1", component->getChildAt(0)->getCalculated(apl::kPropertyText).asString()); +} \ No newline at end of file diff --git a/aplcore/unit/extension/unittest_extension_mediator.cpp b/aplcore/unit/extension/unittest_extension_mediator.cpp index b15cb73..7acbb3a 100644 --- a/aplcore/unit/extension/unittest_extension_mediator.cpp +++ b/aplcore/unit/extension/unittest_extension_mediator.cpp @@ -1582,6 +1582,341 @@ TEST_F(ExtensionMediatorTest, AudioPlayerIntegration) { ASSERT_TRUE(IsEqual("123", activityOffset->getCalculated(kPropertyText).get().getText())); } +static const char* AUDIO_PLAYER_11 = R"( +{ + "type": "APL", + "version": "2024.1", + "extensions": [ + { + "name": "AudioPlayer", + "uri": "aplext:audioplayer:11" + } + ], + "settings": { + "AudioPlayer": { + "playbackStateName": "playerStatus", + "musicAnalysisBindings": { + "bindingName": "musicAnalysisStatus", + "requestedNormalizedEnergies": 5 + } + } + }, + "AudioPlayer:OnPlayerActivityUpdated": [ + { + "type": "SetValue", + "componentId": "ActivityTxt", + "property": "text", + "value": "${playerActivity}" + }, + { + "type": "SetValue", + "componentId": "OffsetTxt", + "property": "text", + "value": "${offset}" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "items": [ + { + "type": "TouchWrapper", + "id": "Touch", + "width": "100%", + "height": "100%", + "onPress": [ + { + "when": "${playerStatus.playerActivity == 'PLAYING'}", + "type": "AudioPlayer:Pause" + }, + { + "when": "${playerStatus.playerActivity == 'PAUSED'}", + "type": "AudioPlayer:Play" + } + ] + }, + { + "type": "Text", + "id": "ActivityTxt" + }, + { + "type": "Text", + "id": "OffsetTxt" + }, + { + "type": "Text", + "id": "bpmText", + "when": "${environment.extension.AudioPlayer.musicAnalysis}", + "text": "${musicAnalysisStatus.beatsPerMinute}" + } + ] + } + } +} +)"; + +TEST_F(ExtensionMediatorTest, AudioPlayerV11Integration) { + + createProvider(); + auto stub = std::make_shared(); + auto extension = std::make_shared<::AudioPlayer::AplAudioPlayerExtension>(stub); + extension->setMusicAnalysisDetails(true, 5); + extensionProvider->registerExtension(std::make_shared(extension)); + loadExtensions(AUDIO_PLAYER_11); + + inflate(); + + // Validate the Extension environment + auto env = context->extensionManager().getEnvironment(); + ASSERT_EQ(1, env->size()); + ASSERT_EQ(1, env->count("AudioPlayer")); + ASSERT_TRUE(env->at("AudioPlayer").truthy()); + ASSERT_TRUE(evaluate(*context, "${environment.extension.AudioPlayer}").isMap()); + ASSERT_TRUE(IsEqual("APLAudioPlayerExtension-1.1", evaluate(*context, "${environment.extension.AudioPlayer.version}"))); + ASSERT_TRUE(IsEqual(true, evaluate(*context, "${environment.extension.AudioPlayer.musicAnalysis}"))); + ASSERT_TRUE(IsEqual(5, evaluate(*context, "${environment.extension.AudioPlayer.maxNormalizedEnergies}"))); + + // Validate presence of command and event handler definitions + auto commands = context->extensionManager().getCommandDefinitions(); + ASSERT_EQ(11, commands.size()); + auto handlers = context->extensionManager().getEventHandlerDefinitions(); + ASSERT_EQ(2, handlers.size()); + + // Validate Live Data + auto trackers = context->dataManager().trackers(); + ASSERT_EQ(2, trackers.size()); + extension->updatePlayerActivity("PLAYING", 123); + std::vector normalizedEnergies{1}; + extension->updateMusicAnalysis(20, normalizedEnergies); + ASSERT_FALSE(ConsoleMessage()); + root->clearPending(); + + ASSERT_TRUE(evaluate(*context, "${playerStatus}").isTrueMap()); + ASSERT_TRUE(IsEqual("PLAYING", evaluate(*context, "${playerStatus.playerActivity}"))); + ASSERT_TRUE(IsEqual(123, evaluate(*context, "${playerStatus.offset}"))); + + ASSERT_TRUE(IsEqual(20, evaluate(*context, "${musicAnalysisStatus.beatsPerMinute}"))); + + auto activityText = root->findComponentById("ActivityTxt"); + ASSERT_TRUE(activityText); + auto activityOffset = root->findComponentById("OffsetTxt"); + ASSERT_TRUE(activityOffset); + auto touch = root->findComponentById("Touch"); + ASSERT_TRUE(touch); + auto bpmText = root->findComponentById("bpmText"); + ASSERT_TRUE(bpmText); + + // Basic data is loaded + ASSERT_TRUE(IsEqual("PLAYING", activityText->getCalculated(kPropertyText).get().getText())); + ASSERT_TRUE(IsEqual("123", activityOffset->getCalculated(kPropertyText).get().getText())); + ASSERT_TRUE(IsEqual("20", bpmText->getCalculated(kPropertyText).get().getText())); +} + +TEST_F(ExtensionMediatorTest, AudioPlayerV11NoMusicAnalysisIntegration) { + + createProvider(); + auto stub = std::make_shared(); + auto extension = std::make_shared<::AudioPlayer::AplAudioPlayerExtension>(stub); + extension->setMusicAnalysisDetails(false); + extensionProvider->registerExtension(std::make_shared(extension)); + loadExtensions(AUDIO_PLAYER_11); + + inflate(); + + // Validate the Extension environment + auto env = context->extensionManager().getEnvironment(); + ASSERT_EQ(1, env->size()); + ASSERT_EQ(1, env->count("AudioPlayer")); + ASSERT_TRUE(env->at("AudioPlayer").truthy()); + ASSERT_TRUE(evaluate(*context, "${environment.extension.AudioPlayer}").isMap()); + ASSERT_TRUE(IsEqual("APLAudioPlayerExtension-1.1", evaluate(*context, "${environment.extension.AudioPlayer.version}"))); + ASSERT_TRUE(IsEqual(false, evaluate(*context, "${environment.extension.AudioPlayer.musicAnalysis}"))); + ASSERT_TRUE(IsEqual(0, evaluate(*context, "${environment.extension.AudioPlayer.maxNormalizedEnergies}"))); + + // Validate presence of command and event handler definitions + auto commands = context->extensionManager().getCommandDefinitions(); + ASSERT_EQ(11, commands.size()); + auto handlers = context->extensionManager().getEventHandlerDefinitions(); + ASSERT_EQ(2, handlers.size()); + + // Validate Live Data + auto trackers = context->dataManager().trackers(); + ASSERT_EQ(1, trackers.size()); + extension->updatePlayerActivity("PLAYING", 123); + ASSERT_FALSE(ConsoleMessage()); + root->clearPending(); + + ASSERT_TRUE(evaluate(*context, "${playerStatus}").isTrueMap()); + ASSERT_TRUE(IsEqual("PLAYING", evaluate(*context, "${playerStatus.playerActivity}"))); + ASSERT_TRUE(IsEqual(123, evaluate(*context, "${playerStatus.offset}"))); + + + auto activityText = root->findComponentById("ActivityTxt"); + ASSERT_TRUE(activityText); + auto activityOffset = root->findComponentById("OffsetTxt"); + ASSERT_TRUE(activityOffset); + auto touch = root->findComponentById("Touch"); + ASSERT_TRUE(touch); + auto bpmText = root->findComponentById("bpmText"); + ASSERT_FALSE(bpmText); + + // Basic data is loaded + ASSERT_TRUE(IsEqual("PLAYING", activityText->getCalculated(kPropertyText).get().getText())); + ASSERT_TRUE(IsEqual("123", activityOffset->getCalculated(kPropertyText).get().getText())); +} + +static const char* AUDIO_PLAYER_10_11 = R"( +{ + "type": "APL", + "version": "2024.1", + "extensions": [ + { + "name": "AudioPlayer", + "uri": "aplext:audioplayer:10" + }, + { + "name": "AudioPlayer11", + "uri": "aplext:audioplayer:11" + } + ], + "settings": { + "AudioPlayer": { + "playbackStateName": "playerStatus" + }, + "AudioPlayer11": { + "playbackStateName": "playerStatus11", + "musicAnalysisBindings": { + "bindingName": "musicAnalysisStatus", + "requestedNormalizedEnergies": 5 + } + } + }, + "AudioPlayer:OnPlayerActivityUpdated": [ + { + "type": "SetValue", + "componentId": "ActivityTxt", + "property": "text", + "value": "${playerActivity}" + }, + { + "type": "SetValue", + "componentId": "OffsetTxt", + "property": "text", + "value": "${offset}" + } + ], + "mainTemplate": { + "item": { + "type": "Container", + "items": [ + { + "type": "TouchWrapper", + "id": "Touch", + "width": "100%", + "height": "100%", + "onPress": [ + { + "when": "${playerStatus.playerActivity == 'PLAYING'}", + "type": "AudioPlayer:Pause" + }, + { + "when": "${playerStatus.playerActivity == 'PAUSED'}", + "type": "AudioPlayer:Play" + } + ] + }, + { + "type": "Text", + "id": "ActivityTxt" + }, + { + "type": "Text", + "id": "OffsetTxt" + }, + { + "type": "Text", + "id": "playerActivity11", + "text": "${playerStatus11.playerActivity}" + }, + { + "type": "Text", + "id": "bpmText", + "when": "${environment.extension.AudioPlayer11.musicAnalysis}", + "text": "${musicAnalysisStatus.beatsPerMinute}" + } + ] + } + } +} +)"; + +TEST_F(ExtensionMediatorTest, AudioPlayerV10And11Integration) { + + createProvider(); + auto stub = std::make_shared(); + auto extension = std::make_shared<::AudioPlayer::AplAudioPlayerExtension>(stub); + extension->setMusicAnalysisDetails(true, 5); + extensionProvider->registerExtension(std::make_shared(extension)); + loadExtensions(AUDIO_PLAYER_10_11); + + inflate(); + + // Validate the Extension environment + auto env = context->extensionManager().getEnvironment(); + ASSERT_EQ(2, env->size()); + ASSERT_EQ(1, env->count("AudioPlayer")); + ASSERT_EQ(1, env->count("AudioPlayer11")); + ASSERT_TRUE(env->at("AudioPlayer").truthy()); + ASSERT_TRUE(env->at("AudioPlayer11").truthy()); + ASSERT_TRUE(evaluate(*context, "${environment.extension.AudioPlayer}").isMap()); + ASSERT_TRUE(evaluate(*context, "${environment.extension.AudioPlayer11}").isMap()); + ASSERT_TRUE(IsEqual("APLAudioPlayerExtension-1.0", evaluate(*context, "${environment.extension.AudioPlayer.version}"))); + ASSERT_TRUE(IsEqual("APLAudioPlayerExtension-1.1", evaluate(*context, "${environment.extension.AudioPlayer11.version}"))); + ASSERT_TRUE(IsEqual(true, evaluate(*context, "${environment.extension.AudioPlayer11.musicAnalysis}"))); + ASSERT_TRUE(IsEqual(5, evaluate(*context, "${environment.extension.AudioPlayer11.maxNormalizedEnergies}"))); + + // Validate presence of command and event handler definitions. + auto commands = context->extensionManager().getCommandDefinitions(); + ASSERT_EQ(22, commands.size()); + auto handlers = context->extensionManager().getEventHandlerDefinitions(); + ASSERT_EQ(4, handlers.size()); + + // Validate Live Data + auto trackers = context->dataManager().trackers(); + ASSERT_EQ(3, trackers.size()); + extension->updatePlayerActivity("PLAYING", 123); + std::vector normalizedEnergies{1}; + extension->updateMusicAnalysis(20, normalizedEnergies); + ASSERT_TRUE(ConsoleMessage()); + root->clearPending(); + + ASSERT_TRUE(evaluate(*context, "${playerStatus}").isTrueMap()); + ASSERT_TRUE(evaluate(*context, "${playerStatus11}").isTrueMap()); + ASSERT_TRUE(IsEqual("PLAYING", evaluate(*context, "${playerStatus.playerActivity}"))); + ASSERT_TRUE(IsEqual("PLAYING", evaluate(*context, "${playerStatus11.playerActivity}"))); + ASSERT_TRUE(IsEqual(123, evaluate(*context, "${playerStatus.offset}"))); + ASSERT_TRUE(IsEqual(123, evaluate(*context, "${playerStatus11.offset}"))); + + ASSERT_TRUE(IsEqual(20, evaluate(*context, "${musicAnalysisStatus.beatsPerMinute}"))); + + auto activityText = root->findComponentById("ActivityTxt"); + ASSERT_TRUE(activityText); + auto activityOffset = root->findComponentById("OffsetTxt"); + ASSERT_TRUE(activityOffset); + auto touch = root->findComponentById("Touch"); + ASSERT_TRUE(touch); + auto activityText11 = root->findComponentById("playerActivity11"); + ASSERT_TRUE(activityText11); + auto bpmText = root->findComponentById("bpmText"); + ASSERT_TRUE(bpmText); + + // Basic data is loaded + ASSERT_TRUE(IsEqual("PLAYING", activityText->getCalculated(kPropertyText).get().getText())); + ASSERT_TRUE(IsEqual("123", activityOffset->getCalculated(kPropertyText).get().getText())); + ASSERT_TRUE(IsEqual("PLAYING", activityText11->getCalculated(kPropertyText).get().getText())); + ASSERT_TRUE(IsEqual("20", bpmText->getCalculated(kPropertyText).get().getText())); +} + class SimpleExtensionTestAdapter : public ExtensionBase { public: SimpleExtensionTestAdapter(const std::string& uri, const std::string& registrationMessage) @@ -2142,7 +2477,7 @@ TEST_F(ExtensionMediatorTest, LoadAllGranted) { } -TEST_F(ExtensionMediatorTest, LoadContentNotReady) { +TEST_F(ExtensionMediatorTest, LoadContentIsWaiting) { createProvider(); auto adapter = std::make_shared(TEST_EXTENSION_URI, true, true); @@ -2157,6 +2492,12 @@ TEST_F(ExtensionMediatorTest, LoadContentNotReady) { { "type": "APL", "version": "1.1", + "import": [ + { + "name": "C", + "version": "1.0" + } + ], "mainTemplate": { "parameters": [ "payload" @@ -2169,16 +2510,53 @@ TEST_F(ExtensionMediatorTest, LoadContentNotReady) { )"; createContent(DOC, nullptr); - ASSERT_FALSE(content->isReady()); + ASSERT_TRUE(content->isWaiting()); - // when content ready, unspecified grant list means all extensions granted - // without ready content load not attempted + // Loading should skip due to waiting state. mediator->loadExtensions(ObjectMap{}, content); ASSERT_TRUE(ConsoleMessage()); ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); } +TEST_F(ExtensionMediatorTest, LoadContentHasError) { + createProvider(); + + auto adapter = std::make_shared(TEST_EXTENSION_URI, true, true); + extensionProvider->registerExtension(adapter); + + // Experimental feature required + config->enableExperimentalFeature(RootConfig::kExperimentalFeatureExtensionProvider) + .extensionProvider(extensionProvider) + .extensionMediator(mediator); + + const char* DOC = R"( + { + "type": "APL", + "version": "1.1", + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Text" + } + } + } + )"; + + createContent(DOC, nullptr); + + // This will transition the content to error. + content->addData("payload", nullptr); + ASSERT_TRUE(content->isError()); + + // Loading should skip due to error state. + mediator->loadExtensions(ObjectMap{}, content); + + ASSERT_TRUE(ConsoleMessage()); + ASSERT_FALSE(adapter->isInitialized(TEST_EXTENSION_URI)); +} static const char* SIMPLE_COMPONENT_DOC = R"({ diff --git a/aplcore/unit/extension/unittest_requested_extension.cpp b/aplcore/unit/extension/unittest_requested_extension.cpp index d0cb54d..5d9d361 100644 --- a/aplcore/unit/extension/unittest_requested_extension.cpp +++ b/aplcore/unit/extension/unittest_requested_extension.cpp @@ -724,6 +724,29 @@ static const char* SETTINGS_WITH_PACKAGE = R"({ } })"; +static const char* SETTINGS_WITH_PARAMETER = R"({ + "type": "APL", + "version": "1.2", + "extension": { + "uri": "URI1", + "name": "foo" + }, + "settings": { + "foo": { + "keyA": "main-A", + "keyB": "main-B" + } + }, + "mainTemplate": { + "parameters": [ + "payload" + ], + "item": { + "type": "Text" + } + } +})"; + const char* SETTINGS_PKG1 = R"({ "type": "APL", "version": "1.1", @@ -757,11 +780,29 @@ const char* SETTINGS_PKG2 = R"({ /** * Extension settings cannot be accessed before document is ready. */ -TEST_F(RequestedExtensionTest, SettingsNotReady) { +TEST_F(RequestedExtensionTest, SettingsIsWaiting) { content = Content::create(SETTINGS_WITH_PACKAGE, session); - ASSERT_FALSE(content->isReady()); + ASSERT_TRUE(content->isWaiting()); + + // verify settings on the named extension + auto es = content->getExtensionSettings("URI1"); + ASSERT_TRUE(IsEqual(Object::NULL_OBJECT(), es)); + ASSERT_TRUE(ConsoleMessage()); +} + +/** + * Extension settings cannot be accessed before document is ready. + */ +TEST_F(RequestedExtensionTest, SettingsIsError) { + + content = Content::create(SETTINGS_WITH_PARAMETER, session); + + // This causes content to transition to the error state. + content->addData("payload", nullptr); + + ASSERT_TRUE(content->isError()); // verify settings on the named extension auto es = content->getExtensionSettings("URI1"); diff --git a/aplcore/unit/primitives/unittest_filters.cpp b/aplcore/unit/primitives/unittest_filters.cpp index fa31cd9..073a382 100644 --- a/aplcore/unit/primitives/unittest_filters.cpp +++ b/aplcore/unit/primitives/unittest_filters.cpp @@ -165,15 +165,17 @@ struct GradientFilterTest { }; std::vector GRADIENT_TESTS = { - { // Minimal gradient + { + // Minimal gradient R"({"type":"Gradient", "gradient": {"type": "linear", "colorRange":["blue", "red"]}})", Gradient::GradientType::LINEAR, std::vector{Color(Color::BLUE), Color(Color::RED)}, std::vector{0, 1}, }, - { // Bad gradient - need to specify an actual gradient + { + // Bad gradient - need to specify an actual gradient R"({"type": "Gradient"})", - static_cast(-1), // Mark a bad gradient + static_cast(-1), // Mark a bad gradient {}, {} }, @@ -183,9 +185,17 @@ std::vector GRADIENT_TESTS = { std::vector{Color(Color::GREEN), Color(Color::RED)}, std::vector{0, 1}, }, - { // Invalid gradient - one that does not have a type + { + // Invalid gradient - the type is incorrect R"({"type":"Gradient", "gradient": {"type": "odd", "colorRange":["green", "red"]}})", - static_cast(-1), // Mark a bad gradient + static_cast(-1), // Mark a bad gradient + {}, + {} + }, + { + // Invalid gradient - one that does not have a color range + R"({"type":"Gradient", "gradient": {"type": "odd"}})", + static_cast(-1), // Mark a bad gradient {}, {} } diff --git a/aplcore/unit/primitives/unittest_object.cpp b/aplcore/unit/primitives/unittest_object.cpp index 4765173..950b803 100644 --- a/aplcore/unit/primitives/unittest_object.cpp +++ b/aplcore/unit/primitives/unittest_object.cpp @@ -25,6 +25,7 @@ #include "apl/component/component.h" #include "apl/component/componenteventsourcewrapper.h" #include "apl/content/metrics.h" +#include "apl/content/sharedjsondata.h" #include "apl/engine/arrayify.h" #include "apl/engine/context.h" #include "apl/livedata/livearrayobject.h" @@ -198,6 +199,61 @@ TEST(ObjectTest, RapidJson) ASSERT_EQ(2, o.get("a").getDouble()); } +TEST(ObjectTest, SharedJson) +{ + auto d0 = std::make_shared(rapidjson::kObjectType); + d0->AddMember("a", rapidjson::Value(rapidjson::kNullType).Move(), d0->GetAllocator()); + SharedJsonData v0(d0, rapidjson::Pointer("/a")); + Object o0(v0); + ASSERT_TRUE(o0.isNull()); + + o0 = Object(SharedJsonData(nullptr)); + ASSERT_TRUE(o0.isNull()); + + auto d1 = std::make_shared(rapidjson::kObjectType); + d1->AddMember("a", rapidjson::Value(10).Move(), d1->GetAllocator()); + SharedJsonData v1(d1, rapidjson::Pointer("/a")); + Object o1(v1); + ASSERT_TRUE(o1.isNumber()); + ASSERT_EQ(10, o1.getInteger()); + + auto d2 = std::make_shared(rapidjson::kObjectType); + d2->AddMember("a", rapidjson::Value("twelve").Move(), d2->GetAllocator()); + SharedJsonData v2(d2, rapidjson::Pointer("/a")); + Object o2(v2); + ASSERT_TRUE(o2.isString()); + ASSERT_STREQ("twelve", o2.getString().c_str()); + + auto d3 = std::make_shared(rapidjson::kObjectType); + d3->AddMember("a", rapidjson::Value(true).Move(), d3->GetAllocator()); + SharedJsonData v3(d3, rapidjson::Pointer("/a")); + Object o3(v3); + ASSERT_TRUE(o3.isBoolean()); + ASSERT_TRUE(o3.getBoolean()); + + auto d4 = std::make_shared(rapidjson::kObjectType); + d4->AddMember("a", rapidjson::Value(false).Move(), d4->GetAllocator()); + SharedJsonData v4(d4, rapidjson::Pointer("/a")); + Object o4(v4); + ASSERT_TRUE(o4.isBoolean()); + ASSERT_FALSE(o4.getBoolean()); + + auto d5 = std::make_shared(); + d5->Parse("[1,2,3]"); + SharedJsonData v5(d5); + Object o5(v5); + ASSERT_EQ(3, o5.size()); + ASSERT_EQ(1, o5.at(0).getInteger()); + + auto d6 = std::make_shared(); + d6->Parse(R"({"a":2,"b": 4})"); + SharedJsonData v6(d6); + Object o6(v6); + ASSERT_TRUE(o6.isMap()); + ASSERT_EQ(2, o6.size()); + ASSERT_EQ(2, o6.get("a").getDouble()); +} + TEST(ObjectTest, Color) { class TestSession : public Session { @@ -299,69 +355,69 @@ TEST(ObjectTest, Gradient) ASSERT_EQ(0xff0000ff, a.get().getProperty(kGradientPropertyColorRange).at(0).getColor()); } -const char *BAD_CASES = - "{" - " \"badType\": {" - " \"type\": \"fuzzy\"," - " \"colorRange\": [" - " \"red\"," - " \"green\"" - " ]" - " }," - " \"tooShort\": {" - " \"type\": \"linear\"," - " \"colorRange\": [" - " \"red\"" - " ]" - " }," - " \"mismatchedRange\": {" - " \"type\": \"radial\"," - " \"colorRange\": [" - " \"red\"," - " \"blue\"," - " \"green\"," - " \"purple\"" - " ]," - " \"inputRange\": [" - " 0," - " 0.5," - " 1" - " ]" - " }," - " \"rangeOutOfBounds\": {" - " \"type\": \"linear\"," - " \"colorRange\": [" - " \"red\"," - " \"blue\"" - " ]," - " \"inputRange\": [" - " 0," - " 1.2" - " ]" - " }," - " \"rangeOutOfBounds2\": {" - " \"type\": \"linear\"," - " \"colorRange\": [" - " \"red\"," - " \"blue\"" - " ]," - " \"inputRange\": [" - " -0.3," - " 1.0" - " ]" - " }," - " \"rangeMisordered\": {" - " \"type\": \"linear\"," - " \"colorRange\": [" - " \"red\"," - " \"blue\"" - " ]," - " \"inputRange\": [" - " 1," - " 0" - " ]" - " }" - "}"; +const char *BAD_CASES = R"( +{ + "badType": { + "type": "fuzzy", + "colorRange": [ + "red", + "green" + ] + }, + "tooShort": { + "type": "linear", + "colorRange": [ + "red" + ] + }, + "mismatchedRange": { + "type": "radial", + "colorRange": [ + "red", + "blue", + "green", + "purple" + ], + "inputRange": [ + 0, + 0.5, + 1 + ] + }, + "rangeOutOfBounds": { + "type": "linear", + "colorRange": [ + "red", + "blue" + ], + "inputRange": [ + 0, + 1.2 + ] + }, + "rangeOutOfBounds2": { + "type": "linear", + "colorRange": [ + "red", + "blue" + ], + "inputRange": [ + -0.3, + 1.0 + ] + }, + "rangeMisordered": { + "type": "linear", + "colorRange": [ + "red", + "blue" + ], + "inputRange": [ + 1, + 0 + ] + } +})"; TEST(ObjectTest, MalformedGradient) diff --git a/aplcore/unit/scenegraph/test_sg.cpp b/aplcore/unit/scenegraph/test_sg.cpp index ebf8fd2..9c6287c 100644 --- a/aplcore/unit/scenegraph/test_sg.cpp +++ b/aplcore/unit/scenegraph/test_sg.cpp @@ -1113,90 +1113,3 @@ DumpSceneGraph(const sg::SceneGraphPtr& ptr) std::cout << "Null scene graph" << std::endl; std::cout << "__END_SCENE_GRAPH__" << std::endl; } - - -sg::TextLayoutPtr -MyTestMeasurement::layout(const sg::TextChunkPtr& textChunk, - const sg::TextPropertiesPtr& textProperties, - float width, - MeasureMode widthMode, - float height, - MeasureMode heightMode) -{ - auto text = textChunk->styledText().asString(); - - // Assume all characters are squares - float cw = textProperties->fontSize(); - float ch = cw; - - auto layout = std::make_shared(ch * 0.8); // Sets the baseline - - // Break up into lines. We don't do anything clever about spaces - std::string::size_type charactersPerLine = - (widthMode == MeasureMode::Undefined ? std::numeric_limits::max() - : static_cast(width / cw)); - - std::string::size_type position = 0; - const auto maxLines = textProperties->maxLines(); - while (position < text.size() && (maxLines == 0 || layout->getLineCount() < maxLines)) { - auto npos = std::min(charactersPerLine, text.size() - position); - layout->addLine(text.substr(position, npos), {cw * npos, ch}); - position += npos; - } - - // At this point the text layout has a "minimum" size that wraps the existing - switch (widthMode) { - case MeasureMode::Exactly: - layout->setWidth(width, textProperties->textAlign()); - break; - case MeasureMode::AtMost: - layout->setWidth(layout->getSize().getWidth(), textProperties->textAlign()); - break; - default: // Don't do anything here - break; - } - - switch (heightMode) { - case MeasureMode::Exactly: - layout->setHeight(height, textProperties->textAlignVertical()); - break; - case MeasureMode::AtMost: - layout->setHeight(layout->getSize().getHeight(), textProperties->textAlignVertical()); - break; - default: - break; - } - - return layout; -} - -float fixMeasuredDimension(float target, float specified, MeasureMode mode) -{ - switch (mode) { - case Exactly: - return specified; - case Undefined: - return target; - case AtMost: - return std::min(specified, target); - } - return 0.0f; -} - -sg::EditTextBoxPtr -MyTestMeasurement::box(int size, - const sg::TextPropertiesPtr& textProperties, - float width, - MeasureMode widthMode, - float height, - MeasureMode heightMode) -{ - // Assume all characters are squares - float cw = textProperties->fontSize(); - float ch = cw; - - // At this point the text layout has a "minimum" size that wraps the existing - return std::make_shared(Size(fixMeasuredDimension(cw * size, width, widthMode), - fixMeasuredDimension(ch, height, heightMode)), - ch * 0.8); -} \ No newline at end of file diff --git a/aplcore/unit/scenegraph/test_sg.h b/aplcore/unit/scenegraph/test_sg.h index d675739..cdfede3 100644 --- a/aplcore/unit/scenegraph/test_sg.h +++ b/aplcore/unit/scenegraph/test_sg.h @@ -26,9 +26,7 @@ #include "apl/scenegraph/path.h" #include "apl/scenegraph/pathop.h" #include "apl/scenegraph/scenegraph.h" -#include "apl/scenegraph/edittextbox.h" -#include "apl/scenegraph/textlayout.h" -#include "apl/scenegraph/textmeasurement.h" +#include "../test_sg_textmeasure.h" using namespace apl; @@ -330,163 +328,4 @@ void DumpSceneGraph(const sg::LayerPtr& ptr, int inset=0); void DumpSceneGraph(const sg::GraphicFragmentPtr& fragment, int inset=0); void DumpSceneGraph(const sg::SceneGraphPtr& ptr); -// Test classes for text layout - -class MyTestLayout : public sg::TextLayout { - struct Line { - std::string text; - Rect rect; - Range range; - }; - -public: - ~MyTestLayout() override = default; - - explicit MyTestLayout(float baseline) - : mBaseline(baseline) - {} - - // Add an additional line of text with a given size. - // The next line is placed below the previous lines - void addLine(const std::string& text, Size size) { - auto start = static_cast(mText.size()); - auto end = start + static_cast(text.size()) - 1; - mLines.emplace_back( - Line{text, {0, mSize.getHeight(), size.getWidth(), size.getHeight()}, {start, end}}); - - auto width = std::max(mSize.getWidth(), size.getWidth()); - auto height = mSize.getHeight() + size.getHeight(); - mSize = {width, height}; - mText += text; - } - - // Update the width of the layout and adjust the horizontal alignment of each line - void setWidth(float width, TextAlign align) { - switch (align) { - case kTextAlignAuto: // We'll assume LTR languages for this testing - case kTextAlignLeft: // The text boxes are by default aligned left, so there is nothing to do - case kTextAlignStart: - break; - case kTextAlignCenter: - for (auto& line : mLines) { - auto dx = width / 2 - line.rect.getCenterX(); - line.rect.offset(dx, 0); - } - break; - case kTextAlignRight: - case kTextAlignEnd: - for (auto& line : mLines) { - auto dx = width - line.rect.getRight(); - line.rect.offset(dx, 0); - } - break; - } - - mSize = { width, mSize.getHeight() }; - } - - // Update the height of the layout and adjust the vertical alignment of each line - void setHeight(float height, TextAlignVertical align) { - switch (align) { - case kTextAlignVerticalAuto: // Assume default top alignment - case kTextAlignVerticalTop: // Nothing to do here - break; - case kTextAlignVerticalCenter: - if (mLines.size() > 1) { - auto coveringRect = mLines.end()->rect.extend(mLines.begin()->rect); - auto dy = height / 2 - coveringRect.getCenterY(); - for (auto& line : mLines) - line.rect.offset(0, dy); - } - break; - case kTextAlignVerticalBottom: - if (mLines.size() > 1) { - auto coveringRect = mLines.end()->rect.extend(mLines.begin()->rect); - auto dy = height - coveringRect.getBottom(); - for (auto& line : mLines) - line.rect.offset(0, dy); - } - break; - } - - mSize = { mSize.getWidth(), height }; - } - - bool empty() const override { return mText.empty(); } - Size getSize() const override { return mSize; } - float getBaseline() const override { return mBaseline; } - int getLineCount() const override { return static_cast(mLines.size()); } - std::string toDebugString() const override { return mText; } - unsigned int getByteLength() const override { return mText.size(); } - - Range getLineRangeFromByteRange(Range byteRange) const override { - if (byteRange.empty()) - return {}; - - auto it_lower = std::lower_bound( - mLines.begin(), mLines.end(), byteRange.lowerBound(), - [](const Line& line, int bottom) { return line.range.upperBound() < bottom; }); - - if (it_lower == mLines.end()) - return {}; - - auto it_upper = std::upper_bound( - it_lower, mLines.end(), byteRange.upperBound(), - [](int top, const Line& line) { return top < line.range.lowerBound(); }); - - return {static_cast(std::distance(mLines.begin(), it_lower)), - static_cast(std::distance(mLines.begin(), it_upper)) - 1}; - } - - Rect getBoundingBoxForLines(Range lineRange) const override { - if (mLines.empty()) - return {}; - - auto range = Range(0, mLines.size() - 1); - if (!lineRange.empty()) - range = range.intersectWith(lineRange); - - Rect result; - for (auto line : range) - result = result.extend(mLines.at(line).rect); - - return result; - } - -private: - std::string mText; - Size mSize; - float mBaseline; - std::vector mLines; -}; - -class MyTestBox : public sg::EditTextBox { -public: - MyTestBox(Size size, float baseline) - : mSize(size), mBaseline(baseline) {} - - Size getSize() const override { return mSize; } - float getBaseline() const override { return mBaseline; } - -private: - Size mSize; - float mBaseline; -}; - -/** - * Fake text measurement logic. Each character is assumed to be a square the size of the font - */ -class MyTestMeasurement : public sg::TextMeasurement { -public: - ~MyTestMeasurement() override = default; - - sg::TextLayoutPtr layout(const sg::TextChunkPtr& textChunk, - const sg::TextPropertiesPtr& textProperties, float width, - MeasureMode widthMode, float height, MeasureMode heightMode) override; - - sg::EditTextBoxPtr box(int size, const sg::TextPropertiesPtr& textProperties, float width, - MeasureMode widthMode, float height, MeasureMode heightMode) override; -}; - - #endif // _APL_TEST_SG_H diff --git a/aplcore/unit/scenegraph/unittest_sg_text.cpp b/aplcore/unit/scenegraph/unittest_sg_text.cpp index e8420f3..a6f105a 100644 --- a/aplcore/unit/scenegraph/unittest_sg_text.cpp +++ b/aplcore/unit/scenegraph/unittest_sg_text.cpp @@ -22,8 +22,10 @@ using namespace apl; class SGTextTest : public DocumentWrapper { public: SGTextTest() : DocumentWrapper() { - config->measure(std::make_shared()); + config->measure(measurement); } + + std::shared_ptr measurement = std::make_shared(); }; struct SplitTestCase { @@ -408,7 +410,8 @@ static const char *CHANGING_SIZE = R"apl( "type": "Container", "id": "BOX", "width": 200, - "height": 200, + "height": "auto", + "maxHeight": 600, "item": { "type": "Text", "text": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", @@ -428,7 +431,7 @@ TEST_F(SGTextTest, ChangingSize) { auto sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( - sg, IsLayer(Rect{0, 0, 200, 200}) + sg, IsLayer(Rect{0, 0, 200, 240}) .child(IsLayer(Rect{0, 0, 200, 240}) // 5 characters per line .content(IsTransformNode().child( IsTextNode() @@ -442,7 +445,7 @@ TEST_F(SGTextTest, ChangingSize) { sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( - sg, IsLayer(Rect{0, 0, 100, 200}) + sg, IsLayer(Rect{0, 0, 100, 520}) .dirty(sg::Layer::kFlagSizeChanged) .child(IsLayer(Rect{0, 0, 100, 40 * 13}) // 2 characters per line .dirty(sg::Layer::kFlagSizeChanged | @@ -459,7 +462,7 @@ TEST_F(SGTextTest, ChangingSize) { sg = root->getSceneGraph(); ASSERT_TRUE(CheckSceneGraph( - sg, IsLayer(Rect{0, 0, 200, 200}) + sg, IsLayer(Rect{0, 0, 200, 240}) .dirty(sg::Layer::kFlagSizeChanged) .child(IsLayer(Rect{0, 0, 200, 240}) // 5 characters per line .dirty(sg::Layer::kFlagSizeChanged | @@ -471,6 +474,44 @@ TEST_F(SGTextTest, ChangingSize) { .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); } +static const char *LIMIT_SIZE = R"apl( +{ + "type": "APL", + "version": "1.8", + "mainTemplate": { + "item": { + "type": "Container", + "id": "BOX", + "width": 200, + "height": "auto", + "maxHeight": 200, + "item": { + "type": "Text", + "text": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "fontSize": 40, + "color": "blue", + "width": "100%" + } + } + } +} +)apl"; + +TEST_F(SGTextTest, LimitSize) { + loadDocument(LIMIT_SIZE); + ASSERT_TRUE(component); + + auto sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 200}) + .child(IsLayer(Rect{0, 0, 200, 200}) // 5 characters per line + .content(IsTransformNode().child( + IsTextNode() + .measuredSize({200,200}) + .text("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + .pathOp(IsFillOp(IsColorPaint(Color::BLUE)))))))); +} static const char * RESIZE = R"apl( { @@ -506,3 +547,169 @@ TEST_F(SGTextTest, Resize) { .content(IsTransformNode().child(IsTextNode().text("Hello").pathOp( IsFillOp(IsColorPaint(Color::RED))))))); } + +const char* FIXED_SIZE_LAYOUT_REQUESTS = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Text", + "id": "TEXT", + "text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.", + "width": 400, + "height": 400, + "fontSize": 20 + } + } +})"; + +TEST_F(SGTextTest, FixedLayoutRequestedOnce) { + loadDocument(FIXED_SIZE_LAYOUT_REQUESTS); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 400, 400}) + .content(IsTransformNode().child( + IsTextNode() + .measuredSize({400,120}) + .text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.") + .pathOp(IsFillOp(IsColorPaint(0xFAFAFAFF))))))); + + ASSERT_EQ(1, measurement->getLayoutCount()); + + executeCommand("SetValue", {{"componentId", "TEXT"}, {"property", "color"}, {"value", "red"}}, false); + advanceTime(17); + + sg = root->getSceneGraph(); + // Layout hasn't changed, only paint. No request required. + ASSERT_EQ(1, measurement->getLayoutCount()); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 400, 400}) + .dirty(sg::Layer::kFlagRedrawContent) + .content(IsTransformNode().child( + IsTextNode() + .measuredSize({400,120}) + .text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.") + .pathOp(IsFillOp(IsColorPaint(Color::RED))))))); +} + +const char* IDENTICAL_LAYOUTS_NO_REQUESTS = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "id": "BOX", + "width": 200, + "height": "auto", + "maxHeight": 800, + "data": [1, 2], + "item": { + "type": "Text", + "text": "ABCDEFGHIJKLMNOPQRSTUVWXYZ", + "fontSize": 40, + "color": "blue", + "width": "100%" + } + } + } +})"; + +TEST_F(SGTextTest, IdenticalLayoutsRequestedOnce) { + loadDocument(IDENTICAL_LAYOUTS_NO_REQUESTS); + + auto sg = root->getSceneGraph(); + + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 200, 480}) + .children({ + IsLayer(Rect{0, 0, 200, 240}) // 5 characters per line + .content(IsTransformNode().child( + IsTextNode() + .measuredSize({200,240}) + .text("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))), + IsLayer(Rect{0, 240, 200, 240}) // 5 characters per line + .content(IsTransformNode().child( + IsTextNode() + .measuredSize({200,240}) + .text("ABCDEFGHIJKLMNOPQRSTUVWXYZ") + .pathOp(IsFillOp(IsColorPaint(Color::BLUE))))) + }))); + + ASSERT_EQ(1, measurement->getLayoutCount()); +} + +const char* AUTOSIZE_WITH_EVENT = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "width": 400, + "height": 400, + "bind": [ + { + "name": "LongText", + "value": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa." + } + ], + "items": [ + { + "bind": [ + { + "name": "FontSize", + "value": 40 + } + ], + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "50%", + "fontSize": "${FontSize}", + "onTextLayout": [ + { + "when": "${event.isTruncated && event.source.bind.FontSize > 10}", + "type": "SetValue", + "property": "FontSize", + "value": "${event.source.bind.FontSize - 10}" + }, + { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + ] + } + ] + } + } +})"; + +TEST_F(SGTextTest, TextLayoutAutosizeFixed) { + loadDocument(AUTOSIZE_WITH_EVENT); + + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscin", true, 400, 200)); + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligul", true, 390, 200)); + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.", false, 400, 120)); + ASSERT_FALSE(CheckSendEvent(root)); + + auto sg = root->getSceneGraph(); + ASSERT_TRUE(CheckSceneGraph( + sg, IsLayer(Rect{0, 0, 400, 400}) + .child(IsLayer(Rect{0, 0, 400, 200}) + .content(IsTransformNode().child( + IsTextNode() + .measuredSize({400,120}) + .text("Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.") + .pathOp(IsFillOp(IsColorPaint(0xFAFAFAFF)))))))); + + +} diff --git a/aplcore/unit/test_sg_textmeasure.cpp b/aplcore/unit/test_sg_textmeasure.cpp new file mode 100644 index 0000000..f4ffe84 --- /dev/null +++ b/aplcore/unit/test_sg_textmeasure.cpp @@ -0,0 +1,105 @@ +/** + * 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 "test_sg_textmeasure.h" +#include "apl/scenegraph/textchunk.h" +#include "apl/scenegraph/textproperties.h" + +sg::TextLayoutPtr +MyTestMeasurement::layout(const sg::TextChunkPtr& textChunk, + const sg::TextPropertiesPtr& textProperties, + float width, + MeasureMode widthMode, + float height, + MeasureMode heightMode) +{ + mLayoutCounter++; + auto text = textChunk->styledText().asString(); + + // Assume all characters are squares + float cw = textProperties->fontSize(); + float ch = cw; + + auto layout = std::make_shared(ch * 0.8); // Sets the baseline + + // Break up into lines. We don't do anything clever about spaces + std::string::size_type charactersPerLine = + (widthMode == MeasureMode::Undefined ? std::numeric_limits::max() + : static_cast(width / cw)); + + std::string::size_type position = 0; + const auto maxLines = textProperties->maxLines(); + while (position < text.size() && (maxLines == 0 || layout->getLineCount() < maxLines)) { + auto npos = std::min(charactersPerLine, text.size() - position); + layout->addLine(text.substr(position, npos), {cw * npos, ch}); + position += npos; + } + + // At this point the text layout has a "minimum" size that wraps the existing + switch (widthMode) { + case MeasureMode::Exactly: + layout->setWidth(width, textProperties->textAlign()); + break; + case MeasureMode::AtMost: + layout->setWidth(layout->getSize().getWidth(), textProperties->textAlign()); + break; + default: // Don't do anything here + break; + } + + switch (heightMode) { + case MeasureMode::Exactly: + layout->setHeight(height, textProperties->textAlignVertical()); + break; + case MeasureMode::AtMost: + layout->setHeight(std::min(layout->getSize().getHeight(), height), textProperties->textAlignVertical()); + break; + default: + break; + } + + return layout; +} + +float fixMeasuredDimension(float target, float specified, MeasureMode mode) +{ + switch (mode) { + case Exactly: + return specified; + case Undefined: + return target; + case AtMost: + return std::min(specified, target); + } + return 0.0f; +} + +sg::EditTextBoxPtr +MyTestMeasurement::box(int size, + const sg::TextPropertiesPtr& textProperties, + float width, + MeasureMode widthMode, + float height, + MeasureMode heightMode) +{ + // Assume all characters are squares + float cw = textProperties->fontSize(); + float ch = cw; + + // At this point the text layout has a "minimum" size that wraps the existing + return std::make_shared(Size(fixMeasuredDimension(cw * size, width, widthMode), + fixMeasuredDimension(ch, height, heightMode)), + ch * 0.8); +} \ No newline at end of file diff --git a/aplcore/unit/test_sg_textmeasure.h b/aplcore/unit/test_sg_textmeasure.h new file mode 100644 index 0000000..48412e5 --- /dev/null +++ b/aplcore/unit/test_sg_textmeasure.h @@ -0,0 +1,241 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef _APL_TEST_SG_TEXTMEASURE_H +#define _APL_TEST_SG_TEXTMEASURE_H + +#include "gtest/gtest.h" +#include "apl/component/component.h" +#include "apl/component/componentproperties.h" +#include "apl/scenegraph/edittextbox.h" +#include "apl/scenegraph/textlayout.h" +#include "apl/scenegraph/textmeasurement.h" + +using namespace apl; + +class MyTestLayout : public sg::TextLayout { + struct Line { + std::string text; + Rect rect; + Range range; + }; + +public: + ~MyTestLayout() override = default; + + explicit MyTestLayout(float baseline) + : mBaseline(baseline), + mTruncated(false) + {} + + // Add an additional line of text with a given size. + // The next line is placed below the previous lines + void addLine(const std::string& text, Size size) { + auto start = static_cast(mText.size()); + auto end = start + static_cast(text.size()) - 1; + mLines.emplace_back( + Line{text, {0, mSize.getHeight(), size.getWidth(), size.getHeight()}, {start, end}}); + + auto width = std::max(mSize.getWidth(), size.getWidth()); + auto height = mSize.getHeight() + size.getHeight(); + mSize = {width, height}; + mText += text; + } + + // Update the width of the layout and adjust the horizontal alignment of each line + void setWidth(float width, TextAlign align) { + switch (align) { + case kTextAlignAuto: // We'll assume LTR languages for this testing + case kTextAlignLeft: // The text boxes are by default aligned left, so there is nothing to do + case kTextAlignStart: + break; + case kTextAlignCenter: + for (auto& line : mLines) { + auto dx = width / 2 - line.rect.getCenterX(); + line.rect.offset(dx, 0); + } + break; + case kTextAlignRight: + case kTextAlignEnd: + for (auto& line : mLines) { + auto dx = width - line.rect.getRight(); + line.rect.offset(dx, 0); + } + break; + } + + mSize = { width, mSize.getHeight() }; + } + + // Update the height of the layout and adjust the vertical alignment of each line + void setHeight(float height, TextAlignVertical align) { + switch (align) { + case kTextAlignVerticalAuto: // Assume default top alignment + case kTextAlignVerticalTop: // Nothing to do here + break; + case kTextAlignVerticalCenter: + if (mLines.size() > 1) { + auto coveringRect = mLines.end()->rect.extend(mLines.begin()->rect); + auto dy = height / 2 - coveringRect.getCenterY(); + for (auto& line : mLines) + line.rect.offset(0, dy); + } + break; + case kTextAlignVerticalBottom: + if (mLines.size() > 1) { + auto coveringRect = mLines.end()->rect.extend(mLines.begin()->rect); + auto dy = height - coveringRect.getBottom(); + for (auto& line : mLines) + line.rect.offset(0, dy); + } + break; + } + + if (height < mSize.getHeight()) mTruncated = true; + + mSize = { mSize.getWidth(), height }; + } + + bool empty() const override { return mText.empty(); } + Size getSize() const override { return mSize; } + float getBaseline() const override { return mBaseline; } + int getLineCount() const override { return static_cast(mLines.size()); } + std::string toDebugString() const override { return mText; } + unsigned int getByteLength() const override { return mText.size(); } + + Range getLineRangeFromByteRange(Range byteRange) const override { + if (byteRange.empty()) + return {}; + + auto it_lower = std::lower_bound( + mLines.begin(), mLines.end(), byteRange.lowerBound(), + [](const Line& line, int bottom) { return line.range.upperBound() < bottom; }); + + if (it_lower == mLines.end()) + return {}; + + auto it_upper = std::upper_bound( + it_lower, mLines.end(), byteRange.upperBound(), + [](int top, const Line& line) { return top < line.range.lowerBound(); }); + + return {static_cast(std::distance(mLines.begin(), it_lower)), + static_cast(std::distance(mLines.begin(), it_upper)) - 1}; + } + + Rect getBoundingBoxForLines(Range lineRange) const override { + if (mLines.empty()) + return {}; + + auto range = Range(0, mLines.size() - 1); + if (!lineRange.empty()) + range = range.intersectWith(lineRange); + + Rect result; + for (auto line : range) + result = result.extend(mLines.at(line).rect); + + return result; + } + + std::string getLaidOutText() const override { + std::string result; + for (auto& line : mLines) { + if (line.rect.getY() + line.rect.getHeight() > mSize.getHeight()) break; + result.append(line.text); + } + + return result; + } + bool isTruncated() const override { return mTruncated; } + +private: + std::string mText; + Size mSize; + float mBaseline; + std::vector mLines; + bool mTruncated; +}; + +class MyTestBox : public sg::EditTextBox { +public: + MyTestBox(Size size, float baseline) + : mSize(size), mBaseline(baseline) {} + + Size getSize() const override { return mSize; } + float getBaseline() const override { return mBaseline; } + +private: + Size mSize; + float mBaseline; +}; + +/** + * Fake text measurement logic. Each character is assumed to be a square the size of the font + */ +class MyTestMeasurement : public sg::TextMeasurement { +public: + ~MyTestMeasurement() override = default; + + sg::TextLayoutPtr layout(const sg::TextChunkPtr& textChunk, + const sg::TextPropertiesPtr& textProperties, float width, + MeasureMode widthMode, float height, MeasureMode heightMode) override; + + sg::EditTextBoxPtr box(int size, const sg::TextPropertiesPtr& textProperties, float width, + MeasureMode widthMode, float height, MeasureMode heightMode) override; + + /** + * @return Number of times layout was requested + */ + int getLayoutCount() const { return mLayoutCounter; } + +private: + int mLayoutCounter = 0; +}; + +/** + * Mimics a viewhost that wants to store Layout objects in the Component UserData. This is a + * demonstration of how viewhosts may want to behave while this API is available but scenegraph + * migration has not been completed. + */ +class LayoutReuseMeasurement : public MyTestMeasurement { + using MyTestMeasurement::layout; + using MyTestMeasurement::box; + + sg::TextLayoutPtr layout(Component *component, + const sg::TextChunkPtr& chunk, + const sg::TextPropertiesPtr& textProperties, + float width, + MeasureMode widthMode, + float height, + MeasureMode heightMode) override { + auto textLayout = layout(chunk, textProperties, width, widthMode, height, heightMode); + component->setUserData(textLayout.get()); + return textLayout; + } + + sg::EditTextBoxPtr box(Component *component, + int size, + const sg::TextPropertiesPtr& textProperties, + float width, + MeasureMode widthMode, + float height, + MeasureMode heightMode) override { + auto editBox = box(size, textProperties, width, widthMode, height, heightMode); + component->setUserData(editBox.get()); + return editBox; + } +}; + +#endif // _APL_TEST_SG_TEXTMEASURE_H diff --git a/aplcore/unit/testeventloop.cpp b/aplcore/unit/testeventloop.cpp index 9489a38..1b309c5 100644 --- a/aplcore/unit/testeventloop.cpp +++ b/aplcore/unit/testeventloop.cpp @@ -95,6 +95,29 @@ getMemoryCounterMap() { } #endif +static inline size_t getTextLength(Component* component) { + size_t len = component->getCalculated(kPropertyText).asString().size(); + const auto& size = component->getCalculated(kPropertySize); + if (size.isNumber()) { + len = size.asInt(); + } + + return len; +} + +static inline Size getSymbolSize(Component* component, Size& baseSize) { + const auto& fontSize = component->getCalculated(kPropertyFontSize); + + Size symbolSize = {10, 10}; + if (fontSize.isDimension()) { + symbolSize = Size{ + (float)fontSize.asNumber() / 40 * baseSize.getWidth(), + (float)fontSize.asNumber() / 40 * baseSize.getHeight()}; + } + + return symbolSize; +} + /** * Replicates (as closely as possible) process used in viewhosts for text measurement, but with * every symbol being 10x10 square. Doesn't account for line breaks. @@ -106,10 +129,11 @@ SimpleTextMeasurement::measure(Component *component, float height, MeasureMode heightMode) { - auto len = component->getCalculated(kPropertyText).asString().size(); // Number of characters - float singleLineWidth = static_cast(len) * 10; + auto len = getTextLength(component); // Number of characters + Size symbolSize = getSymbolSize(component, mSymbolSize); + float singleLineWidth = static_cast(len) * symbolSize.getWidth(); float resultingWidth = 0; - auto workingWidth = 10 * std::floor(width / 10); // width clamped to symbol size + auto workingWidth = symbolSize.getWidth() * std::floor(width / symbolSize.getWidth()); // width clamped to symbol size // There are 3 MeasureModes: // 1. Exactly - text should fit into provided metric, requested metric is reported as resulting @@ -133,12 +157,12 @@ SimpleTextMeasurement::measure(Component *component, break; } - auto charactersPerLine = std::min(resultingWidth, workingWidth) / 10; + auto charactersPerLine = std::min(resultingWidth, workingWidth) / symbolSize.getWidth(); // Can't layout as line can't hold single character. if (charactersPerLine <= 0) return {resultingWidth, 0}; - float workingHeight = 10 * std::ceil(static_cast(len) / charactersPerLine); + float workingHeight = symbolSize.getHeight() * std::ceil(static_cast(len) / charactersPerLine); float resultingHeight = 0; switch (heightMode) { case MeasureMode::Exactly: diff --git a/aplcore/unit/testeventloop.h b/aplcore/unit/testeventloop.h index 53c0934..95893f0 100644 --- a/aplcore/unit/testeventloop.h +++ b/aplcore/unit/testeventloop.h @@ -43,9 +43,14 @@ namespace apl { class SimpleTextMeasurement : public TextMeasurement { public: + SimpleTextMeasurement(int symbolWidth = 10, int symbolHeight = 10) + : mSymbolSize(symbolWidth, symbolHeight) {} + LayoutSize measure(Component *component, float width, MeasureMode widthMode, float height, MeasureMode heightMode) override; float baseline(Component *component, float width, float height) override; +private: + Size mSymbolSize; }; class SpyTextMeasure : public TextMeasurement { @@ -241,6 +246,32 @@ class MemoryWrapper : public ::testing::Test { return logBridge->checkAndClear(); } +#ifdef DEBUG_MEMORY_USE + static size_t getAliveCountersFor(const std::string& name) { + return getMemoryCounterMap().at(name)().alive(); + } + + static + ::testing::AssertionResult + CheckAliveCountersNotChanged(const std::string& name, size_t initial) { + auto alive = getAliveCountersFor(name); + if (initial != alive) + return ::testing::AssertionFailure() << "Initial: " << initial << ", current: " << alive; + + return ::testing::AssertionSuccess(); + } +#else + static size_t getAliveCountersFor(const std::string& name) { + return 0; + } + + static + ::testing::AssertionResult + CheckAliveCountersNotChanged(const std::string& name, int initial) { + return ::testing::AssertionSuccess();; + } +#endif + protected: void TearDown() override { @@ -405,6 +436,23 @@ class DocumentWrapper : public ActionWrapper { * destructed. */ void TearDown() override { +#ifdef DEBUG_MEMORY_USE + if (root) root->clearPending(); + if (component) { + // Collect all components and check for cache leaks + auto clist = std::queue(); + clist.emplace(component); + + while (!clist.empty()) { + auto comp = clist.front(); + clist.pop(); + + ASSERT_TRUE(component->isTempCacheClean()) << "Component: " << component->getUniqueId() << " has stale cache."; + + for (int i = 0; i < comp->getChildCount(); i++) clist.emplace(comp->getCoreChildAt(i)); + } + } +#endif // DEBUG_MEMORY_USE component = nullptr; rootDocument = nullptr; @@ -492,17 +540,14 @@ class DocumentWrapper : public ActionWrapper { if (!data) return; - // If the content calls for a single parameter named "payload", we assign the data directly - if (content->getParameterCount() == 1 && content->getParameterAt(0) == "payload") { - content->addData("payload", data); - } - else { // Otherwise, we require "data" to be a JSON object where the keys match the requested parameters - rawData = std::make_unique(data); - ASSERT_TRUE(rawData->get().IsObject()); - for (auto it = rawData->get().MemberBegin() ; - it != rawData->get().MemberEnd() ; - it++) { - content->addData(it->name.GetString(), it->value); + rawData = std::make_unique(data); + for (int i = 0; i < content->getParameterCount(); i++) { + auto parameterName = content->getParameterAt(i); + auto it = rawData->get().FindMember(parameterName.c_str()); + if (it != rawData->get().MemberEnd()) { + content->addData(parameterName, it->value); + } else if (parameterName == "payload") { + content->addData(parameterName, data); } } } @@ -1315,6 +1360,19 @@ HandlePointerEvent(const RootContextPtr& root, PointerEventType type, const Poin return ::testing::AssertionSuccess(); } +template +::testing::AssertionResult +HandlePointerEvent(const RootContextPtr& root, PointerEventType type, const Point& point, apl_time_t timestamp, bool consumed, Args... args) { + if (root->handlePointerEvent(PointerEvent(type, point), timestamp) != consumed) + return ::testing::AssertionFailure() << "Event consumption mismatch."; + + if (sizeof...(Args) > 0) { + return CheckSendEvent(root, args...); + } + + return ::testing::AssertionSuccess(); +} + inline ::testing::AssertionResult compareTransformApprox(const Transform2D& left, const Transform2D& right, float delta = 0.1F) { diff --git a/aplcore/unit/text/CMakeLists.txt b/aplcore/unit/text/CMakeLists.txt new file mode 100644 index 0000000..3a34848 --- /dev/null +++ b/aplcore/unit/text/CMakeLists.txt @@ -0,0 +1,17 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +target_sources_local(unittest + PRIVATE + unittest_text_layout.cpp + ) \ No newline at end of file diff --git a/aplcore/unit/text/unittest_text_layout.cpp b/aplcore/unit/text/unittest_text_layout.cpp new file mode 100644 index 0000000..b1cf472 --- /dev/null +++ b/aplcore/unit/text/unittest_text_layout.cpp @@ -0,0 +1,538 @@ +/** + * 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 "../test_sg_textmeasure.h" + +using namespace apl; + +class TextLayoutTest : public DocumentWrapper {}; + +static const char* TEXT_MEASURE_LAYOUT = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "width": 500, + "height": 500, + "items": [ + { + "type": "Text", + "id": "AutoHeight", + "width": "100%", + "height": "auto", + "text": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa." + } + ] + } + } +})"; + +TEST_F(TextLayoutTest, OldMeasure) { + config->measure(std::make_shared(40, 40)); + + loadDocument(TEXT_MEASURE_LAYOUT); + + auto t = root->findComponentById("AutoHeight"); + auto s = t->getCalculated(apl::kPropertyBounds).get().getSize(); + ASSERT_EQ(500, s.getWidth()); + ASSERT_EQ(360, s.getHeight()); +} + +TEST_F(TextLayoutTest, LayoutMeasure) { + config->measure(std::make_shared()); + + loadDocument(TEXT_MEASURE_LAYOUT); + + auto t = root->findComponentById("AutoHeight"); + auto s = t->getCalculated(apl::kPropertyBounds).get().getSize(); + ASSERT_EQ(500, s.getWidth()); + ASSERT_EQ(360, s.getHeight()); +} + +static const char* TEXT_LAYOUT_TEST_START = R"({ + "type": "APL", + "version": "2023.2", + "theme": "dark", + "mainTemplate": { + "items": { + "type": "Container", + "width": 400, + "height": 400, + "bind": [ + { + "name": "LongText", + "value": "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa." + } + ], + "items": [ +)"; + +static const char* TEXT_LAYOUT_TEST_END = R"( + ] + } + } +})"; + +TEST_F(TextLayoutTest, TextLayoutNoEventWhenNoLayout) { + const char* TEST = R"({ + "type": "ScrollView", + "width": "100%", + "height": "50%", + "item": { + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "auto", + "onTextLayout": { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + } + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_FALSE(CheckSendEvent(root)); +} + +TEST_F(TextLayoutTest, TextLayoutFixed) { + config->measure(std::make_shared()); + + const char* TEST = R"({ + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "50%", + "onTextLayout": { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscin", true, 400, 200)); +} + +TEST_F(TextLayoutTest, TextLayoutFixedNoLayoutWhenNoEvent) { + auto measure = std::make_shared(); + config->measure(measure); + + const char* TEST = R"({ + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "50%" + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_EQ(0, measure->getLayoutCount()); +} + +TEST_F(TextLayoutTest, TextLayoutAtMax) { + config->measure(std::make_shared()); + + const char* TEST = R"({ + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "auto", + "maxHeight": "12.5%", + "onTextLayout": { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsu", true, 400, 50)); +} + +TEST_F(TextLayoutTest, TextLayoutUndefined) { + config->measure(std::make_shared()); + + const char* TEST = R"({ + "type": "ScrollView", + "width": "100%", + "height": "50%", + "item": { + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "auto", + "onTextLayout": { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + } + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.", false, 400, 440)); +} + +TEST_F(TextLayoutTest, TextLayoutAutosize) { + config->measure(std::make_shared()); + + const char* TEST = R"({ + "bind": [ + { + "name": "FontSize", + "value": 40 + } + ], + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "auto", + "maxHeight": "50%", + "fontSize": "${FontSize}", + "onTextLayout": [ + { + "when": "${event.isTruncated}", + "type": "SetValue", + "property": "FontSize", + "value": "${event.source.bind.FontSize - 10}" + }, + { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + ] + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscin", true, 400, 200)); + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligul", true, 390, 200)); + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.", false, 400, 120)); +} + +TEST_F(TextLayoutTest, TextLayoutAutosizeFixed) { + config->measure(std::make_shared()); + + const char* TEST = R"({ + "bind": [ + { + "name": "FontSize", + "value": 40 + } + ], + "type": "Text", + "text": "${LongText}", + "width": "100%", + "height": "50%", + "fontSize": "${FontSize}", + "onTextLayout": [ + { + "when": "${event.isTruncated && event.source.bind.FontSize > 10}", + "type": "SetValue", + "property": "FontSize", + "value": "${event.source.bind.FontSize - 10}" + }, + { + "type": "SendEvent", + "sequencer": "EVENTER", + "arguments": [ + "${event.laidOutText}", + "${event.isTruncated}", + "${event.textWidth}", + "${event.textHeight}" + ] + } + ] + })"; + + std::string doc(TEXT_LAYOUT_TEST_START); + doc.append(TEST); + doc.append(TEXT_LAYOUT_TEST_END); + + loadDocument(doc.c_str()); + + ASSERT_TRUE(component); + + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscin", true, 400, 200)); + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligul", true, 390, 200)); + ASSERT_TRUE(CheckSendEvent(root, "Lorem ipsum dolor sit amet, consectetuer adipiscing elit. Aenean commodo ligula eget dolor. Aenean massa.", false, 400, 120)); +} + +const static char *BASELINE_TEST = R"({ + "type": "APL", + "version": "1.0", + "mainTemplate": { + "items": { + "type": "Container", + "width": "100%", + "height": "100%", + "direction": "row", + "alignItems": "baseline", + "items": { + "type": "Text", + "fontSize": 10, + "text": "${data}" + }, + "data": [ + "Single line", + "Double line
Double line", + "Triple line
Triple line
Triple line" + ] + } + } +})"; + +TEST_F(TextLayoutTest, BaselineTest) +{ + config->measure(std::make_shared()); + + loadDocument(BASELINE_TEST); + ASSERT_EQ(Rect(0,0,1024,800), component->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(3, component->getChildCount()); + + // Test TextLayout does not handle multilines + auto child = component->getChildAt(0); + LOG(LogLevel::kError) << child->getCalculated(kPropertyBounds).get(); + ASSERT_EQ(Rect(0, 0, 110, 10), child->getCalculated(kPropertyBounds).get()); + + child = component->getChildAt(1); + LOG(LogLevel::kError) << child->getCalculated(kPropertyBounds).get(); + ASSERT_EQ(Rect(110, 0, 260, 10), child->getCalculated(kPropertyBounds).get()); + + child = component->getChildAt(2); + LOG(LogLevel::kError) << child->getCalculated(kPropertyBounds).get(); + ASSERT_EQ(Rect(370, 0, 410, 10), child->getCalculated(kPropertyBounds).get()); +} + +static const char* EDITTEXT_LAYOUT = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Frame", + "height": "100%", + "width": "100%", + "borderWidth": 2, + "item": { + "type": "EditText", + "height": "auto", + "width": "auto", + "text": "Hello", + "size": 3, + "color": "#000000" + } + } + } +})"; + +/** + * Test text measurement for EditText component + */ +TEST_F(TextLayoutTest, EditTextMeasurement) { + metrics.size(400, 400); + config->measure(std::make_shared()); + + loadDocument(EDITTEXT_LAYOUT); + ASSERT_TRUE(root); + + // Check the layout + auto top = root->topComponent().get(); + ASSERT_EQ(Rect(0, 0, 400, 400), top->getCalculated(kPropertyBounds).get()); + auto editText = top->getChildAt(0); + ASSERT_EQ(Rect(2, 2, 120, 40), editText->getCalculated(kPropertyBounds).get()); +} + +static const char* EDIT_TEXT_AUTOSIZED = R"({ + "type": "APL", + "version": "2024.2", + "mainTemplate": { + "item": { + "type": "Container", + "direction": "row", + "items": [ + { + "type": "EditText", + "id": "EDITTEXT", + "text": "hello", + "size": 8, + "shrink": 1.0 + } + ] + } + } +})"; + +TEST_F(TextLayoutTest, EditTextAutosize) { + metrics.size(600, 600); + config->measure(std::make_shared()); + + loadDocument(EDIT_TEXT_AUTOSIZED); + + auto et = root->findComponentById("EDITTEXT"); + ASSERT_EQ(Size(320, 600), et->getCalculated(apl::kPropertyBounds).get().getSize()); + + // Change FontSize and ensure resize happened + executeCommands(JsonData(R"([{ "type": "SetValue", "componentId": "EDITTEXT", "property": "fontSize", "value": 60 }])").moveToObject(), false); + advanceTime(100); + + ASSERT_EQ(Size(480, 600), et->getCalculated(apl::kPropertyBounds).get().getSize()); +} + +const static char *BASELINE_EDITTEXT_TEST = R"( +{ + "type":"APL", + "version":"1.4", + "mainTemplate":{ + "items":{ + "type":"Container", + "width":"100%", + "height":"100%", + "direction":"row", + "alignItems":"baseline", + "items":{ + "type":"EditText", + "fontSize": 10, + "text":"${data}" + }, + "data":[ + "Short", + "Mid size text test.", + "This is long text test for measure size.", + "This is long text test for measure size. Last test text." + ] + } + } +} +)"; + +TEST_F(TextLayoutTest, BaselineEditTextTest) +{ + config->measure(std::make_shared()); + + loadDocument(BASELINE_EDITTEXT_TEST); + ASSERT_EQ(Rect(0,0,1024,800), component->getCalculated(kPropertyBounds).get()); + ASSERT_EQ(4, component->getChildCount()); + + auto child = component->getChildAt(0); // First child is one line + ASSERT_EQ(Rect(0, 0, 80, 10), child->getCalculated(kPropertyBounds).get()); + + child = component->getChildAt(1); // First child is one line + ASSERT_EQ(Rect(80, 0, 80, 10), child->getCalculated(kPropertyBounds).get()); + + child = component->getChildAt(2); // First child is one line + ASSERT_EQ(Rect(160, 0, 80, 10), child->getCalculated(kPropertyBounds).get()); + + child = component->getChildAt(3); // First child is one line + ASSERT_EQ(Rect(240, 0, 80, 10), child->getCalculated(kPropertyBounds).get()); +} + +TEST_F(TextLayoutTest, LayoutReusePossible) { + config->measure(std::make_shared()); + + loadDocument(TEXT_MEASURE_LAYOUT); + + auto t = root->findComponentById("AutoHeight"); + MyTestLayout* cachedLayout = t->getUserData(); + ASSERT_TRUE(cachedLayout); + auto layoutSize = cachedLayout->getSize(); + auto componentSize = t->getCalculated(apl::kPropertyBounds).get().getSize(); + ASSERT_EQ(layoutSize.getWidth(), componentSize.getWidth()); + ASSERT_EQ(layoutSize.getHeight(), componentSize.getHeight()); + +} + +TEST_F(TextLayoutTest, BoxReusePossible) { + config->measure(std::make_shared()); + + loadDocument(EDITTEXT_LAYOUT); + auto top = root->topComponent().get(); + auto editText = top->getChildAt(0); + MyTestBox* cachedLayout = editText->getUserData(); + ASSERT_TRUE(cachedLayout); + auto layoutSize = cachedLayout->getSize(); + auto componentSize = editText->getCalculated(apl::kPropertyBounds).get().getSize(); + ASSERT_EQ(layoutSize.getWidth(), componentSize.getWidth()); + ASSERT_EQ(layoutSize.getHeight(), componentSize.getHeight()); +} diff --git a/aplcore/unit/touch/unittest_native_gestures_pager.cpp b/aplcore/unit/touch/unittest_native_gestures_pager.cpp index 2c7d512..6d8674a 100644 --- a/aplcore/unit/touch/unittest_native_gestures_pager.cpp +++ b/aplcore/unit/touch/unittest_native_gestures_pager.cpp @@ -933,7 +933,6 @@ static const char *PAGER_DEFAULT_DATA = R"apl({ TEST_F(NativeGesturesPagerTest, PageFlingLeftDefault) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_DEFAULT_DATA); - ASSERT_TRUE(ConsoleMessage()); // Extra "do" data advanceTime(10); root->clearDirty(); @@ -996,7 +995,6 @@ TEST_F(NativeGesturesPagerTest, PageFlingLeftDefault) TEST_F(NativeGesturesPagerTest, PageFlingChangeOfNav) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_DEFAULT_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data // Set page to last component->update(kUpdatePagerPosition, 11); @@ -1127,7 +1125,6 @@ TEST_F(NativeGesturesPagerTest, PageFlingChangeOfNav) TEST_F(NativeGesturesPagerTest, PageFlingLeftDefaultRTL) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_DEFAULT_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data advanceTime(10); root->clearDirty(); @@ -1203,7 +1200,6 @@ TEST_F(NativeGesturesPagerTest, PageFlingLeftDefaultRTL) TEST_F(NativeGesturesPagerTest, PageFlingRightDefault) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_DEFAULT_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data advanceTime(10); root->clearDirty(); @@ -1276,7 +1272,6 @@ TEST_F(NativeGesturesPagerTest, PageFlingRightDefault) TEST_F(NativeGesturesPagerTest, PageFlingRightDefaultRTL) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_DEFAULT_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data advanceTime(10); root->clearDirty(); @@ -1354,7 +1349,6 @@ static const char *PAGER_VERTICAL_DATA = R"apl({ TEST_F(NativeGesturesPagerTest, PageFlingUpDefault) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_VERTICAL_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data advanceTime(10); root->clearDirty(); @@ -1484,7 +1478,6 @@ NativeGesturesPagerTest::pageFlingDownDefaultTest(NativeGesturesPagerTest& self) TEST_F(NativeGesturesPagerTest, PageFlingDownDefault) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_VERTICAL_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data advanceTime(10); root->clearDirty(); @@ -1497,7 +1490,6 @@ TEST_F(NativeGesturesPagerTest, PageFlingDownDefault) TEST_F(NativeGesturesPagerTest, PageFlingDownDefaultRTL) { loadDocument(PAGER_TEST_DEFAULT_ANIMATION, PAGER_VERTICAL_DATA); - ASSERT_TRUE(ConsoleMessage()); // Incorrect arguments for data advanceTime(10); root->clearDirty(); diff --git a/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp b/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp index 0b47cac..07cc275 100644 --- a/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp +++ b/aplcore/unit/touch/unittest_native_gestures_scrollable.cpp @@ -152,6 +152,48 @@ TEST_F(NativeGesturesScrollableTest, Scroll) ASSERT_EQ(Point(0, 0), component->scrollPosition()); } +TEST_F(NativeGesturesScrollableTest, ScrollWithTimeStampFromVH) +{ + loadDocument(SCROLL_TEST); + + ASSERT_EQ(Point(), component->scrollPosition()); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(0,100), root->currentTime(), false, "onDown:green1")); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,50), root->currentTime()+20,true, "onMove:green1")); + ASSERT_TRUE(CheckSendEvent(root, "onCancel:green1")); + ASSERT_EQ(Point(0, 50), component->scrollPosition()); + advanceTime(100); + // Drop pointer due to timestamp before last tick + ASSERT_FALSE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,0), root->currentTime()-20, true)); + + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,0), root->currentTime()+20, true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(0,0), root->currentTime()+50, true)); + + ASSERT_TRUE(CheckDirty(component, kPropertyScrollPosition, kPropertyNotifyChildrenChanged)); + + ASSERT_EQ(Point(0, 100), component->scrollPosition()); + + advanceTime(2600); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,100), root->currentTime()+20, false)); + + // Scroll back up + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerDown, Point(0,100), root->currentTime()+50, false, "onDown:red6")); + advanceTime(100); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,150), root->currentTime()+20, true, "onMove:red6")); + ASSERT_TRUE(CheckSendEvent(root, "onCancel:red6")); + advanceTime(100); + // Drop pointer due to timestamp before last tick + ASSERT_FALSE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,0), root->currentTime()-20, true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerMove, Point(0,200), root->currentTime()+20, true)); + ASSERT_TRUE(HandlePointerEvent(root, PointerEventType::kPointerUp, Point(0,200), root->currentTime()+50, true)); + + ASSERT_TRUE(CheckDirty(component, kPropertyScrollPosition, kPropertyNotifyChildrenChanged)); + + advanceTime(2600); + ASSERT_EQ(Point(0, 0), component->scrollPosition()); +} + TEST_F(NativeGesturesScrollableTest, ScrollRotated) { loadDocument(SCROLL_TEST); @@ -2628,7 +2670,7 @@ TEST_F(NativeGesturesScrollableTest, DisplayConditional) ASSERT_TRUE(root->handlePointerEvent(PointerEvent(apl::kPointerUp, Point(10, 100)))); advanceTime(50); - ASSERT_EQ(Point(0,180), scrollable->scrollPosition()); + ASSERT_EQ(Point(0,176), scrollable->scrollPosition()); } static const char *NESTED_SCROLL_VIEWS = R"({ diff --git a/aplcore/unit/touch/unittest_pointer.cpp b/aplcore/unit/touch/unittest_pointer.cpp index e7ee815..493f7d5 100644 --- a/aplcore/unit/touch/unittest_pointer.cpp +++ b/aplcore/unit/touch/unittest_pointer.cpp @@ -1272,4 +1272,156 @@ TEST_F(PointerTest, TouchWrapperInheritsState) { auto event = root->popEvent(); ASSERT_EQ(event.getType(), kEventTypeSendEvent); +} + +static const char *POINTER_EVENTS = R"({ + "type": "APL", + "version": "2024.2", + "theme": "dark", + "graphics": { + "GradientPanel": { + "type": "AVG", + "version": "1.2", + "width": 400, + "height": 100, + "resources": { + "gradients": { + "linearGradient": { + "inputRange": [ + 0, + 0.8 + ], + "colorRange": [ + "#ffffff00", + "#000000ff" + ], + "type": "linear", + "x2": 0, + "y2": 1, + "x1": 0, + "y1": 0 + }, + "radialGradient": { + "inputRange": [ + 0, + 1 + ], + "colorRange": [ + "#ffffffff", + "#ff0000ff" + ], + "type": "radial", + "centerX": 0.6480013075429227, + "centerY": 0.4348329629565578, + "radius": 1 + } + } + }, + "items": [ + { + "type": "path", + "pathData": "M0,0 L400,0 L400,100 L0,100 Z", + "fill": "@linearGradient" + } + ] + } + }, + "mainTemplate": { + "items": { + "type": "Container", + "height": 400, + "width": 400, + "items": [ + { + "type": "Sequence", + "height": "100%", + "width": "100%", + "data": [ + "red", + "blue", + "green", + "yellow", + "gray", + "purple" + ], + "item": { + "type": "Frame", + "height": 125, + "width": "100%", + "background": "${data}" + } + }, + { + "type": "VectorGraphic", + "id": "OverlayEffect", + "position": "absolute", + "height": 100, + "width": "100%", + "top": 300, + "source": "GradientPanel", + "pointerEvents": "none" + } + ] + } + } +})"; + +TEST_F(PointerTest, PointerEvents) { + metrics.size(400, 400); + config->set(RootProperty::kTapOrScrollTimeout, 5); + + loadDocument(POINTER_EVENTS); + + auto sqnc = component->getChildAt(0); + ASSERT_EQ(Point(0,0), sqnc->scrollPosition()); + ASSERT_TRUE(MouseDown(root, 200, 375)); + advanceTime(50); + ASSERT_TRUE(MouseMove(root, 200, 275)); + advanceTime(50); + ASSERT_EQ(Point(0,100), sqnc->scrollPosition()); + ASSERT_TRUE(MouseMove(root, 200, 75)); + ASSERT_TRUE(MouseUp(root, 200, 75)); + ASSERT_EQ(Point(0,300), sqnc->scrollPosition()); + root->clearPending(); +} + +TEST_F(PointerTest, PointerEventsFromEdge) { + metrics.size(400, 400); + + loadDocument(POINTER_EVENTS); + + auto sqnc = component->getChildAt(0); + ASSERT_EQ(Point(0,0), sqnc->scrollPosition()); + ASSERT_TRUE(MouseDown(root, 200, 375)); + advanceTime(50); + ASSERT_TRUE(MouseMove(root, 200, 325)); + advanceTime(50); + ASSERT_TRUE(MouseMove(root, 200, 275)); + advanceTime(50); + ASSERT_TRUE(MouseMove(root, 200, 75)); + ASSERT_TRUE(MouseUp(root, 200, 75)); + ASSERT_EQ(Point(0,300), sqnc->scrollPosition()); + root->clearPending(); +} + +TEST_F(PointerTest, PointerEventsEnable) { + metrics.size(400, 400); + config->set(RootProperty::kTapOrScrollTimeout, 5); + + loadDocument(POINTER_EVENTS); + + executeCommand("SetValue", {{"componentId", "OverlayEffect"}, {"property", "pointerEvents"}, {"value", "auto"}}, true); + root->clearPending(); + + auto sqnc = component->getChildAt(0); + ASSERT_EQ(Point(0,0), sqnc->scrollPosition()); + ASSERT_FALSE(MouseDown(root, 200, 375)); + advanceTime(50); + ASSERT_TRUE(MouseMove(root, 200, 275)); + advanceTime(50); + ASSERT_EQ(Point(0,0), sqnc->scrollPosition()); + ASSERT_TRUE(MouseMove(root, 200, 75)); + ASSERT_TRUE(MouseUp(root, 200, 75)); + ASSERT_EQ(Point(0,0), sqnc->scrollPosition()); + root->clearPending(); } \ No newline at end of file diff --git a/aplcore/unit/utils/CMakeLists.txt b/aplcore/unit/utils/CMakeLists.txt index fdce01f..40e87ab 100644 --- a/aplcore/unit/utils/CMakeLists.txt +++ b/aplcore/unit/utils/CMakeLists.txt @@ -14,6 +14,7 @@ target_sources_local(unittest PRIVATE unittest_encoding.cpp + unittest_flags.cpp unittest_hash.cpp unittest_log.cpp unittest_lrucache.cpp diff --git a/aplcore/unit/utils/unittest_flags.cpp b/aplcore/unit/utils/unittest_flags.cpp new file mode 100644 index 0000000..9b817a6 --- /dev/null +++ b/aplcore/unit/utils/unittest_flags.cpp @@ -0,0 +1,109 @@ +/** + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "gtest/gtest.h" + +#include "apl/utils/flags.h" + +using namespace apl; + +class FlagsTest : public ::testing::Test {}; + +enum TestFlags : uint8_t { + kTestFlag0 = 1u << 0, + kTestFlag1 = 1u << 1, + kTestFlag2 = 1u << 2, + kTestFlag3 = 1u << 3, + kTestFlag4 = 1u << 4, +}; + +TEST_F(FlagsTest, EmptyStart) +{ + auto testFlags = Flags(); + + // Nothing is set + ASSERT_FALSE(testFlags.isSet(kTestFlag0)); + ASSERT_FALSE(testFlags.isSet(kTestFlag1)); + ASSERT_FALSE(testFlags.isSet(kTestFlag2)); + ASSERT_FALSE(testFlags.isSet(kTestFlag3)); + + testFlags.set(kTestFlag1); + testFlags.set(kTestFlag2); + ASSERT_TRUE(testFlags.isSet(kTestFlag1)); + ASSERT_TRUE(testFlags.isSet(kTestFlag2)); + + testFlags.clear(kTestFlag1); + ASSERT_FALSE(testFlags.isSet(kTestFlag1)); +} + +TEST_F(FlagsTest, PrePopulate) +{ + auto testFlags = Flags(kTestFlag0 | kTestFlag2 | kTestFlag4); + + // Nothing is set + ASSERT_TRUE(testFlags.isSet(kTestFlag0)); + ASSERT_FALSE(testFlags.isSet(kTestFlag1)); + ASSERT_TRUE(testFlags.isSet(kTestFlag2)); + ASSERT_FALSE(testFlags.isSet(kTestFlag3)); + ASSERT_TRUE(testFlags.isSet(kTestFlag4)); +} + +TEST_F(FlagsTest, BiggerStorage) +{ + enum TestFlags16 : uint16_t { + kTestFlag16_0 = 1u << 0, + kTestFlag16_7 = 1u << 7, + kTestFlag16_15 = 1u << 15 + }; + + auto testFlags16 = Flags(); + + testFlags16.set(kTestFlag16_0); + testFlags16.set(kTestFlag16_7); + testFlags16.set(kTestFlag16_15); + + // Nothing is set + ASSERT_TRUE(testFlags16.isSet(kTestFlag16_0)); + ASSERT_TRUE(testFlags16.isSet(kTestFlag16_7)); + ASSERT_TRUE(testFlags16.isSet(kTestFlag16_15)); + + + enum TestFlags32 : uint32_t { + kTestFlag32_0 = 1u << 0, + kTestFlag32_15 = 1u << 7, + kTestFlag32_31 = 1u << 31 + }; + + auto testFlags32 = Flags(); + + testFlags32.set(kTestFlag32_0); + testFlags32.set(kTestFlag32_15); + testFlags32.set(kTestFlag32_31); + + // Nothing is set + ASSERT_TRUE(testFlags32.isSet(kTestFlag32_0)); + ASSERT_TRUE(testFlags32.isSet(kTestFlag32_15)); + ASSERT_TRUE(testFlags32.isSet(kTestFlag32_31)); +} + +TEST_F(FlagsTest, CheckAndClear) +{ + auto testFlags = Flags(0xFF); + + // Everything is set + ASSERT_TRUE(testFlags.isSet(kTestFlag2)); + ASSERT_TRUE(testFlags.checkAndClear(kTestFlag2)); + ASSERT_FALSE(testFlags.isSet(kTestFlag2)); +} \ No newline at end of file diff --git a/aplcore/unit/versioning/CMakeLists.txt b/aplcore/unit/versioning/CMakeLists.txt new file mode 100644 index 0000000..4285888 --- /dev/null +++ b/aplcore/unit/versioning/CMakeLists.txt @@ -0,0 +1,18 @@ +# Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. +# +# Licensed under the Apache License, Version 2.0 (the "License"). +# You may not use this file except in compliance with the License. +# A copy of the License is located at +# +# http://aws.amazon.com/apache2.0/ +# +# or in the "license" file accompanying this file. This file is distributed +# on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either +# express or implied. See the License for the specific language governing +# permissions and limitations under the License. + +target_sources_local(unittest + PRIVATE + unittest_semantic_pattern.cpp + unittest_semantic_version.cpp +) \ No newline at end of file diff --git a/aplcore/unit/versioning/unittest_semantic_pattern.cpp b/aplcore/unit/versioning/unittest_semantic_pattern.cpp new file mode 100644 index 0000000..300340d --- /dev/null +++ b/aplcore/unit/versioning/unittest_semantic_pattern.cpp @@ -0,0 +1,149 @@ +/** + * 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/versioning/semanticpattern.h" +#include "apl/versioning/semanticversion.h" + +using namespace apl; + +class SemanticPatternTest : public MemoryWrapper { +}; + +static std::vector> GOOD_PATTERNS = { + { "1", "=1.0.0" }, + { " 1 ", "=1.0.0"}, + { "12.1", "=12.1.0"}, + { "1.3-alpha.v2.12+beta.4444", "=1.3.0.'alpha'.'v2'.12" }, + { ">2.12.4", ">2.12.4"}, + { "<6", "<6.0.0"}, + { "=1.4.2", "=1.4.2"}, + { ">=2+testing", ">=2.0.0"}, + { "<=13-0", "<=13.0.0.0"}, + { "1 || 2", "=1.0.0 || =2.0.0"}, + { ">=2.3||<3", ">=2.3.0 || <3.0.0"}, + { "1.0.4-beta || 1.0.6-beta || <1.3.2-alpha >1", "=1.0.4.'beta' || =1.0.6.'beta' || <1.3.2.'alpha' >1.0.0"} +}; + +TEST_F(SemanticPatternTest, Good) +{ + for (const auto& m : GOOD_PATTERNS) { + auto p = SemanticPattern::create(session, m.first); + ASSERT_TRUE(p) << m.first; + ASSERT_EQ(m.second, p->toDebugString()) << m.first; + } +} + +static std::vector BAD_PATTERNS = { + "", + " b ", + "> 1.2", + "1.b.2", + "!=3.0.5", + "(>1.2 <2)", + ">1.2 && < 2", + ">=1.3.5-alpha.@fuzzy", +}; + +TEST_F(SemanticPatternTest, Bad) +{ + for (const auto& m : BAD_PATTERNS) { + auto p = SemanticPattern::create(session, m); + ASSERT_FALSE(p) << m; + ASSERT_TRUE(ConsoleMessage()) << m; + } +} + +TEST_F(SemanticPatternTest, PatternBasic) +{ + auto p = SemanticPattern::create(session, ">1.0 <2.0.4"); + ASSERT_TRUE(p); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.1.3"))); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "2.0.3"))); + ASSERT_FALSE(p->match(SemanticVersion::create(session, "1.0.0"))); + ASSERT_FALSE(p->match(SemanticVersion::create(session, "2.0.4"))); + ASSERT_FALSE(p->match(nullptr)); +} + +TEST_F(SemanticPatternTest, PatternBasic2) +{ + auto p = SemanticPattern::create(session, ">1.0-alpha"); + ASSERT_TRUE(p); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.0-alpha.2"))); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.1+testbuild"))); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.1"))); +} + +TEST_F(SemanticPatternTest, PatternBasic3) +{ + auto p = SemanticPattern::create(session, "1 || 1.1"); + ASSERT_TRUE(p); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.0.0"))); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.1.0"))); + ASSERT_TRUE(p->match(SemanticVersion::create(session, "1.1.0+testbuild"))); + ASSERT_FALSE(p->match(SemanticVersion::create(session, "1.1.0-b2"))); + ASSERT_FALSE(p->match(nullptr)); +} + +struct PatternTestCase { + std::string pattern; + std::vector good; + std::vector bad; +}; + +static const std::vector SV_PATTERN_TEST = { + {">1.0", + {"1.2", "1.0.1", "32.1"}, + {"1.0", "0.9", "1.0.3-alpha.1"}}, + {">1.0-alpha", // Prerelease matters + {"1.2", "1.0.1", "32.1", "1.0", "1.0-alpha.2"}, + {"0.9", "1.0.0-alpha", "1.0.3-alpha.1"}}, + {">1.0+alpha", // Build should be ignored + {"1.2", "1.0.1", "32.1"}, + {"1.0", "0.9", "1.0.3-alpha.1"}}, + {">=1.0.0 <2.0.0", + {"1.2", "1.0.0", "1.0.1", "1.9999.999"}, + {"0.9", "1.0.0-alpha", "1.0.3-alpha.1", "2.0.0", "234.23.222"}}, + {">2.2.3 || 1.2.3", + {"1.2.3", "2.2.4", "3.1.0"}, + {"1.0", "0.9", "1.2.3-alpha", "1.0.3-alpha.1"}}, + {"1.3.2 || 2.0 || 2.0.1", + {"1.3.2", "2.0.0", "2.0.1"}, + {"1.3", "2.0.2", "2-a"}}, + {"<=2.0.0-alpha", + {"1.3.2", "0.0.0", "2.0-a", "2-02"}, + {"2", "2.0.0-alpha.2", "2-alpha2"}}, +}; + +TEST_F(SemanticPatternTest, PatternTest) +{ + for (const auto& m: SV_PATTERN_TEST) { + auto pattern = SemanticPattern::create(session, m.pattern); + ASSERT_TRUE(pattern) << m.pattern; + + for (const auto& good: m.good) { + auto v = SemanticVersion::create(session, good); + ASSERT_TRUE(v) << m.pattern << " " << good; + ASSERT_TRUE(pattern->match(v)) << m.pattern << " " << good; + } + + for (const auto& bad: m.bad) { + auto v = SemanticVersion::create(session, bad); + ASSERT_TRUE(v) << m.pattern << " " << bad; + ASSERT_FALSE(pattern->match(v)) << m.pattern << " " << bad; + } + } +} \ No newline at end of file diff --git a/aplcore/unit/versioning/unittest_semantic_version.cpp b/aplcore/unit/versioning/unittest_semantic_version.cpp new file mode 100644 index 0000000..05097c4 --- /dev/null +++ b/aplcore/unit/versioning/unittest_semantic_version.cpp @@ -0,0 +1,153 @@ +/** + * 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/versioning/semanticversion.h" + +using namespace apl; + +class SemanticVersionTest : public MemoryWrapper {}; + +TEST_F(SemanticVersionTest, Basic) { + auto test = SemanticVersion::create(session, "1.3.0"); + ASSERT_TRUE(test); + ASSERT_EQ("1.3.0", test->toDebugString()); +} + +static const char * SV_GOOD[] = { + "1", + "1.3.12", + "0.0.4", + " 1.1 ", + "1.3.12 ", + "23.124.0", + "1.2.3-alpha", + "1.2.3-alpha.2+32423", + "0.0.1-alpha-beta-gamma.-.02", + "1.2.3-2147483647", // 2^31 - 1 + "1.0.0-1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0", + // The last entry has 255 characters, which just fits + "1.0.0" // 5 characters + "-1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0" // 80 characters + ".1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0" // 80 characters + ".1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0" // 80 characters + ".1.2.3.4.5" // 10 characters +}; + +TEST_F(SemanticVersionTest, Good) { + for (const auto& m : SV_GOOD) { + auto a = SemanticVersion::create(session, m); + ASSERT_TRUE(a) << m; + } +} + +static const char * SV_BAD[] = { + "", + "v2.2", + "+hello", + "1.2.1+hello?", + "-23.124.0", + "1.2.3-alpha%", // Trailing invalid character '%' + "1.2.3-alpha.2+32423-..234", // The ".." is invalid; there should be something in between + "1-2147483648", // 2^32 doesn't fit. + // The last entry has 256 characters, which is too long + "1.0.10" // 6 characters + "-1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0" // 80 characters + ".1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0" // 80 characters + ".1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0.1.2.3.4.5.6.7.8.9.0" // 80 characters + ".1.2.3.4.5" // 10 characters +}; + +TEST_F(SemanticVersionTest, Bad) { + for (const auto& m : SV_BAD) { + auto a = SemanticVersion::create(session, m); + ASSERT_FALSE(a) << m; + ASSERT_TRUE(ConsoleMessage()) << m; + } +} + +static const std::vector SV_ORDERED = { + "1.0.0-alpha", + "1.0.0-alpha.1", + "1.0-alpha.2", + "1.0.0-alpha.beta", + "1.0.0-beta", + "1.0.0-beta.2", + "1.0.0-beta.11", + "1.0.0-rc.1", + "1.0.0", + "2.0.0-alpha", + "2.0.0", + "2.1.0", + "2.1.1", + "2.2.0", + "2.12.3", + "3-beta", + "3", + "3.1-ALPHA.1", + "3.1.0-ALPHA.2", + "3.1-ALPHA.BETA", + "3.1", + "3.1.1-ALPHA", + "3.1.1", + "4-0", + "4-4", + "4-1235", + "4-00000", // Numerics are less than strings + "11" +}; + +TEST_F(SemanticVersionTest, Ordered) { + for (auto i = 0 ; i < SV_ORDERED.size() - 1 ; i++) { + const auto& sa = SV_ORDERED.at(i); + const auto& sb = SV_ORDERED.at(i+1); + auto a = SemanticVersion::create(session, sa); + auto b = SemanticVersion::create(session, sb); + + ASSERT_TRUE(a) << sa; + ASSERT_TRUE(b) << sb; + ASSERT_TRUE(*a < *b) << sa << " < " << sb; + ASSERT_TRUE(*b > *a) << sb << " > " << sa; + ASSERT_TRUE(*a != *b) << sa << " != " << sb; + ASSERT_FALSE(*a == *b) << sa << " == " << sb; + ASSERT_TRUE(*a <= *b) << sa << " <= " << sb; + ASSERT_TRUE(*b >= *a) << sb << " >= " << sa; + + ASSERT_FALSE(*a < *a) << sa; + ASSERT_FALSE(*a > *a) << sa; + ASSERT_FALSE(*a != *a) << sa; + ASSERT_TRUE(*a == *a) << sa; + ASSERT_TRUE(*a <= *a) << sa; + ASSERT_TRUE(*a >= *a) << sa; + } +} + +static const std::vector> SV_DEBUG_STRING_TEST = { + { "1", "1.0.0" }, + { "2.12", "2.12.0" }, + { "13.0.33", "13.0.33" }, + { "1-a-2", "1.0.0.'a-2'" }, + { "2-a.b-3.234.0.02", "2.0.0.'a'.'b-3'.234.0.'02'" }, + { "0+423.a", "0.0.0" }, +}; + +TEST_F(SemanticVersionTest, DebugString) +{ + for (const auto& m : SV_DEBUG_STRING_TEST) { + auto v = SemanticVersion::create(session, m.first); + ASSERT_TRUE(v) << m.first; + ASSERT_EQ(m.second, v->toDebugString()) << m.first; + } +} diff --git a/bin/apl-header-inclusion-validation.sh b/bin/apl-header-inclusion-validation.sh index 725c428..30b2927 100644 --- a/bin/apl-header-inclusion-validation.sh +++ b/bin/apl-header-inclusion-validation.sh @@ -46,9 +46,11 @@ public_apl_headers=( "apl/content/jsondata.h" "apl/content/metrics.h" "apl/content/package.h" + "apl/content/packagemanager.h" "apl/content/rootconfig.h" "apl/content/rootproperties.h" "apl/content/settings.h" + "apl/content/sharedjsondata.h" "apl/datasource/datasourceconnection.h" "apl/datasource/datasourceprovider.h" "apl/datasource/dynamicindexlistdatasourceprovider.h" @@ -146,6 +148,7 @@ public_apl_headers=( "apl/utils/bimap.h" "apl/utils/counter.h" "apl/utils/deprecated.h" + "apl/utils/flags.h" "apl/utils/localemethods.h" "apl/utils/log.h" "apl/utils/noncopyable.h" @@ -156,11 +159,14 @@ public_apl_headers=( "apl/utils/throw.h" "apl/utils/userdata.h" "apl/utils/visitor.h" + "apl/versioning/semanticversion.h" + "apl/versioning/semanticpattern.h" ) public_external_headers=( "rapidjson/document.h" "rapidjson/error/en.h" + "rapidjson/pointer.h" "rapidjson/stringbuffer.h" "rapidjson/writer.h" ) diff --git a/doc/core_objects.puml b/doc/core_objects.puml index dc2a198..a9d6565 100644 --- a/doc/core_objects.puml +++ b/doc/core_objects.puml @@ -240,6 +240,7 @@ class SharedContextData { int mScreenLockCount LruCache mCachedMeasures; LruCache mCachedBaselines; + std::unique_ptr mTextLayoutCache; std::unique_ptr mTextPropertiesCache; } diff --git a/extensions/alexaext/CMakeLists.txt b/extensions/alexaext/CMakeLists.txt index c1ee0d6..862210d 100644 --- a/extensions/alexaext/CMakeLists.txt +++ b/extensions/alexaext/CMakeLists.txt @@ -38,6 +38,7 @@ target_sources_local(alexaext PRIVATE src/APLAudioNormalizationExtension/AplAudioNormalizationExtension.cpp src/APLE2EEncryptionExtension/AplE2eEncryptionExtension.cpp src/APLMetricsExtension/AplMetricsExtension.cpp + src/APLMetricsExtensionV2/AplMetricsExtensionV2.cpp src/APLWebflowExtension/AplWebflowBase.cpp src/APLWebflowExtension/AplWebflowExtension.cpp src/APLMusicAlarmExtension/AplMusicAlarmExtension.cpp diff --git a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h index c0e1a03..ca72634 100644 --- a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h +++ b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtension.h @@ -32,7 +32,11 @@ namespace alexaext { namespace audioplayer { static const std::string URI = "aplext:audioplayer:10"; +static const std::string URI_11 = "aplext:audioplayer:11"; +static const std::setEXTENSION_URIS({URI, URI_11}); + static const std::string ENVIRONMENT_VERSION = "APLAudioPlayerExtension-1.0"; +static const std::string ENVIRONMENT_VERSION_11 = "APLAudioPlayerExtension-1.1"; /** * An APL Extension designed for bi-directional communication between an @c AudioPlayer and APL document @@ -102,7 +106,21 @@ class AplAudioPlayerExtension */ void setActivePresentationSession(const std::string& id, const std::string& skillId); + /** + * Call to update normalized energies in musicAnalysisState of apl::LiveMap + * It is expected that this function is called after audio features are read from Audio Server during active + * track playback. + * @param beatsPerMinute Beats Per Minute for current track + * @param normalizedEnergies Normalized energies at current track offset + */ + void updateMusicAnalysis(int beatsPerMinute, std::vector normalizedEnergies); + /** + * Call to set music analysis related details in environment properties within assigned extension namespace. + * @param canAnalyze Music Analysis capability + * @param maxNormalizedEnergies Maximum supported normalized energies + */ + void setMusicAnalysisDetails(bool canAnalyze, int maxNormalizedEnergies = 0); protected: @@ -112,6 +130,9 @@ class AplAudioPlayerExtension // Publishes a LiveDataUpdate void publishLiveData(); + //Publish LiveDataUpdate for music analysis + void publishLiveDataForMusicAnalysis(); + protected: /// The @c AplAudioPlayerExtensionObserverInterface observer @@ -122,6 +143,17 @@ class AplAudioPlayerExtension std::string mPlaybackStateActivity; int mPlaybackStateOffset = 0; std::string mAudioItemId{""}; + /// The @c apl::LiveMap beats per minute for AudioPlayer musicAnalysisState data. + int mBeatsPerMinute{0}; + /// The @c apl::LiveMap normalized energies for AudioPlayer musicAnalysisState data. + std::vector mNormalizedEnergies; + /// Holds requested valid URIs of Audio Player Extensions. + std::set mRequestedURIs; + /// Set to true if device runtime is capable of analyzing playback signal. + bool mCanAnalyzeMusic{false}; + /// Holds maximum number of normalized energies that device runtime can provide. + int mMaxNormalizedEnergies{0}; + /// The map of activity to activity state std::unordered_map, @@ -140,6 +172,21 @@ class AplAudioPlayerExtension * @param descriptor The activity descriptor */ std::shared_ptr getOrCreateActivityState(const ActivityDescriptor& activity); + + /** + * Internal function to validate audio player extension URI. + * @param uri The requested audio player extension URI. + * @return True if audio player extension URI is valid. + */ + bool isValidURI(const std::string &uri); + + /** + * Internal function to check if a supported extension URI is requested during registration. + * @param uri Supported extension URI. + * @return True if URI is requested during registration. + */ + bool isURIRequested(const std::string &uri); + }; using AplAudioPlayerExtensionPtr = std::shared_ptr; diff --git a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h index 3883187..bb1e395 100644 --- a/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h +++ b/extensions/alexaext/include/alexaext/APLAudioPlayerExtension/AplAudioPlayerExtensionObserverInterface.h @@ -99,6 +99,13 @@ class AplAudioPlayerExtensionObserverInterface { * https://developer.amazon.com/en-US/docs/alexa/alexa-voice-service/playbackcontroller.html#buttoncommandissued */ virtual void onAudioPlayerSkipBackward() = 0; + + /** + * Used to notify the observer when the extension has read setting for requested + * normalizedEnergies + * @param count Number of requested normalized energies by APL Document. + */ + virtual void onRequestedNormalizedEnergies(int count) {} }; } // namespace audioplayer diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/AplMetricsExtensionV2.h b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/AplMetricsExtensionV2.h new file mode 100644 index 0000000..33da473 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/AplMetricsExtensionV2.h @@ -0,0 +1,144 @@ +/* + * 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_APLMETRICSEXTENSIONV2_H +#define APL_APLMETRICSEXTENSIONV2_H + +#include +#include +#include +#include +#include +#include + +#include + +#include + +#include "DestinationFactoryInterface.h" +#include "DestinationInterface.h" +#include "MetricData.h" +#include "MetricTracker.h" + +namespace alexaext { +namespace metricsExtensionV2 { + +static constexpr char URI_V2[] = "aplext:metrics:20"; +static constexpr char ENVIRONMENT_VERSION_V2[] = "2.0"; + +/** + * The metrics extension that enables generating metrics from APL document. + * + * This extension implements the metric logic and delegates the publishing of metircs to @c + * DestinationInterface. + */ +class AplMetricsExtensionV2 : public alexaext::ExtensionBase, + public std::enable_shared_from_this { +public: + /** + * Constructor + * @param destinationFactoryInterface is the factory class to get the destination to which the + * metric is to published. + * @param executor Extension task executor, publish API's are invoked as + * asynchronous tasks on this. + */ + AplMetricsExtensionV2(const DestinationFactoryInterfacePtr& destinationFactoryInterface, + const alexaext::ExecutorPtr& executor); + + ~AplMetricsExtensionV2() override = default; + + /// @name alexaext::Extension Functions + /// @{ + + rapidjson::Document createRegistration(const alexaext::ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) override; + + bool invokeCommand(const alexaext::ActivityDescriptor& activity, + const rapidjson::Value& command) override; + + void onActivityUnregistered(const ActivityDescriptor& activity) override; + + /// @} + +private: + /** + * Utility to track metric data per activity + */ + class MetricDataList { + public: + MetricDataList(const std::shared_ptr& destinationInterface); + + void publish(Metric&& metric); + + void publish(); + + void createCounter(std::string&& metricName, std::string&& metricId, + Dimensions&& dimensions, int amount); + + void incrementCounter(std::string&& metricId, int amount); + + void startTimer(std::string&& metricName, std::string&& metricId, Dimensions&& dimensions, + Timestamp&& startTime); + + bool stopTimer(const std::string& metricId, Metric& metric, const Timestamp& stopTime); + + private: + std::shared_ptr mDestinationInterface; + std::map> mMetricIdCounterMetricData; + std::unordered_map> mMetricIdTimerData; + }; + + bool addActivity(const alexaext::ActivityDescriptor& activity, + const std::shared_ptr& destinationInterface); + + bool queueTask(Executor::Task&& task); + + std::shared_ptr + removeActivity(const alexaext::ActivityDescriptor& activity); + + std::shared_ptr + getActivityMetrics(const alexaext::ActivityDescriptor& activity); + + bool createCounter(const ActivityDescriptor& activity, std::string&& metricName, + std::string&& metricId, Dimensions&& dimensions, int amount); + + bool incrementCounter(const alexaext::ActivityDescriptor& activity, std::string&& metricId, + const int amount); + + bool startTimer(const alexaext::ActivityDescriptor& activity, std::string&& metricName, + std::string&& metricId, Dimensions&& dimensions, Timestamp&& startTime); + + bool stopTimer(const alexaext::ActivityDescriptor& activity, const std::string& metricId, + const Timestamp& stopTime); + + bool recordValue(const ActivityDescriptor& activity, std::string&& metricName, + Dimensions&& dimensions, int value); + + Dimensions getDimensionMap(const rapidjson::Value& param); + +private: + DestinationFactoryInterfacePtr mDestinationFactory; + std::weak_ptr mExecutor; + std::unordered_map, + alexaext::ActivityDescriptor::Hash> + mActivityMetricKeysMap; +}; + +using AplMetricsExtension2Ptr = std::shared_ptr; + +} // namespace metricsExtensionV2 +} // namespace alexaext + +#endif // APL_APLMETRICSEXTENSIONV2_H diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationFactoryInterface.h b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationFactoryInterface.h new file mode 100644 index 0000000..276d99a --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationFactoryInterface.h @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef APL_DESTINATIONFACTORYINTERFACE_H +#define APL_DESTINATIONFACTORYINTERFACE_H + +#include "DestinationInterface.h" +#include "MetricData.h" +#include +#include + +namespace alexaext { +namespace metricsExtensionV2 { +/** + * The destination factory interface is responsible to create destination + * object as per provided settings. + */ +class DestinationFactoryInterface { +public: + /** + * Destructor + */ + virtual ~DestinationFactoryInterface() = default; + + /** + * Provides an instance of \c DestinationInterface based on the destination type and options + * present inside the Json + * @param settings contains a json object as below: + * { + * "destination": { + * "type": "DESTINATION_TYPE", // String + * "destinationConfig1": "MY_CONFIG_1", + * "destinationConfig2": "MY_CONFIG_2", + * "predefinedKeys": [ + * "DEVICE_LANGUAGE", + * "TIME_ZONE" + * ] + * }, + * "dimensions": { + * "applicationId": "", + * "experienceId": "" + * } + * } + * + * @returns a valid instance of the destnation interface which will honor the provided options + */ + virtual std::shared_ptr + createDestination(const rapidjson::Value& settings) = 0; +}; + +using DestinationFactoryInterfacePtr = std::shared_ptr; + +} // namespace metricsExtensionV2 +} // namespace alexaext + +#endif // APL_DESTINATIONFACTORYINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationInterface.h b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationInterface.h new file mode 100644 index 0000000..5dc7ebb --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/DestinationInterface.h @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef APL_DESTINATIONINTERFACE_H +#define APL_DESTINATIONINTERFACE_H + +#include "MetricData.h" +#include + +namespace alexaext { +namespace metricsExtensionV2 { +/** + * The destination interface is used as interface to all the available destinations + * to publish metrics. + */ +class DestinationInterface { +public: + /** + * Destructor + */ + virtual ~DestinationInterface() = default; + + /** + * Publish the current recorded metrics + */ + virtual void publish(Metric metric) = 0; + /** + * Publish list of recorded metrics + */ + virtual void publish(std::vector metric) = 0; +}; +} // namespace metricsExtensionV2 +} // namespace alexaext + +#endif // APL_DESTINATIONINTERFACE_H diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricData.h b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricData.h new file mode 100644 index 0000000..8568140 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricData.h @@ -0,0 +1,49 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#ifndef APL_METRIC_DATA_H +#define APL_METRIC_DATA_H + +#include +#include + +namespace alexaext { +namespace metricsExtensionV2 { +/** + * This struct is used to store all the captured information about + * a metric. + */ +typedef struct Metric { + + /// name will be used to store metric name. This name will be later used + /// while publishing metric to destination. + std::string name; + + /// dimensions: This will be used if there are additional dimension specific to this metric. + /// If dimension key is already present with document setting, this dimension will override it. + std::map dimensions; + + /// value: To store the value calculated for this mertic + /// For Timer Metric: + /// Value = (endTime - startTime) of metric + /// For Counter Metric + /// Value = Counter + double value; +} Metric; + +} // namespace metricsExtensionV2 +} // namespace alexaext + +#endif // APL_METRIC_DATA_H diff --git a/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricTracker.h b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricTracker.h new file mode 100644 index 0000000..448c259 --- /dev/null +++ b/extensions/alexaext/include/alexaext/APLMetricsExtensionV2/MetricTracker.h @@ -0,0 +1,108 @@ +/* + * 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_METRICTRACKER_H +#define APL_METRICTRACKER_H + +#include "MetricData.h" +#include + +namespace alexaext { +namespace metricsExtensionV2 { + +using Dimensions = std::map; +using Timestamp = std::chrono::steady_clock::time_point; + +/** + * Base class composing Metric structure + */ +class MetricTracker { +public: + /** + * Constructor + */ + MetricTracker(std::string&& metricName, Dimensions&& dimensions, int initialValue) { + mMetric.name = std::move(metricName); + mMetric.dimensions = std::move(dimensions); + mMetric.value = initialValue; + } + /** + * Object to hold metric data + */ + Metric mMetric; +}; + +/** + * CounterMetricTracker for tracking counter metrics. + */ +class CounterMetricTracker : public MetricTracker { +public: + /** + * Constructor + */ + CounterMetricTracker(std::string&& metricName, Dimensions&& dimensions, int initialValue = 0) + : MetricTracker(std::move(metricName), std::move(dimensions), initialValue) {} + + /** + * Increments the value with the given amount. + * @param amount is the amount by which the value should be increments. If @param amount is + * negative, value will be decremented. + */ + void incrementCounter(int amount) { mMetric.value += amount; } + + Metric& getMetric() { return mMetric; } +}; + +/** + * TimerMetricTracker for tracking start time for timer metrics. + */ +class TimerMetricTracker : public MetricTracker { +public: + /** + * Constructor + */ + TimerMetricTracker(std::string&& metricName, Dimensions&& dimensions, Timestamp&& startTime) + : MetricTracker(std::move(metricName), std::move(dimensions), 0), + mStarted(true), + mStartTime(startTime) {} + + /** + * Stop the timer. + * + * @param metric is out param which contains the metric data once stopped + * @param stop captured by the caller. This will be used to calculate the timer metric. + * @return true if stop succeed, false otherwise. + */ + bool stop(Metric& metric, const Timestamp& stopTime) { + if (!mStarted) { + return false; + } + + mStarted = false; + mMetric.value = + std::chrono::duration_cast(stopTime - mStartTime).count(); + + metric = mMetric; + return true; + } + +private: + bool mStarted; + Timestamp mStartTime; +}; +} // namespace metricsExtensionV2 +} // namespace alexaext + +#endif // APL_METRICTRACKER_H \ No newline at end of file diff --git a/extensions/alexaext/include/alexaext/alexaext.h b/extensions/alexaext/include/alexaext/alexaext.h index 6f0eb2f..a9bd997 100644 --- a/extensions/alexaext/include/alexaext/alexaext.h +++ b/extensions/alexaext/include/alexaext/alexaext.h @@ -17,19 +17,19 @@ #define _ALEXAEXT_H /** -* Public facing API for Alexa Extensions -* -* Extensions are optional enhancements to a runtime that provide additional sources of data, -* commands, and event handlers. Extensions must be explicitly requested before they are made -* available. The extension user must ask for each extension that it is interested in and then -* check to see if the requested extension is available on that runtime. -* -* Each extension version has a unique URI. The URI is defined by the extension author. Extension -* authors should follow RFC-3986 Uniform Resource Identifier (URI): Generic Syntax when -* specifying the URI of an extension. A sample extension URI that defines version 1.0 of a -* feature for the Alexa platform, may look like: -* alexaext:myfeature:10 -*/ + * Public facing API for Alexa Extensions + * + * Extensions are optional enhancements to a runtime that provide additional sources of data, + * commands, and event handlers. Extensions must be explicitly requested before they are made + * available. The extension user must ask for each extension that it is interested in and then + * check to see if the requested extension is available on that runtime. + * + * Each extension version has a unique URI. The URI is defined by the extension author. Extension + * authors should follow RFC-3986 Uniform Resource Identifier (URI): Generic Syntax when + * specifying the URI of an extension. A sample extension URI that defines version 1.0 of a + * feature for the Alexa platform, may look like: + * alexaext:myfeature:10 + */ #include "activitydescriptor.h" #include "extension.h" @@ -54,6 +54,10 @@ #include "APLWebflowExtension/AplWebflowExtension.h" #include "APLMusicAlarmExtension/AplMusicAlarmExtension.h" #include "APLMetricsExtension/AplMetricsExtension.h" +#include "APLMetricsExtensionV2/AplMetricsExtensionV2.h" +#include "APLMetricsExtensionV2/DestinationFactoryInterface.h" +#include "APLMetricsExtensionV2/DestinationInterface.h" +#include "APLMetricsExtensionV2/MetricData.h" #include "APLAttentionSystemExtension/AplAttentionSystemExtension.h" #endif //_ALEXAEXT_H diff --git a/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp b/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp index c713887..8a3300e 100644 --- a/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp +++ b/extensions/alexaext/src/APLAudioPlayerExtension/AplAudioPlayerExtension.cpp @@ -31,8 +31,15 @@ static const char *TAG("AplAudioPlayerExtension"); // version of the extension definition Schema static const std::string SCHEMA_VERSION = "1.0"; +// environment set on registration +static const std::string ENVIRONMENT_MUSIC_ANALYSIS = "musicAnalysis"; +static const std::string ENVIRONMENT_MAX_NORMALIZED_ENERGIES = "maxNormalizedEnergies"; + // Settings read on registration static const char *SETTING_PLAYBACK_STATE_NAME = "playbackStateName"; +static const char *SETTING_MUSIC_ANALYSIS_BINDINGS = "musicAnalysisBindings"; +static const std::string SETTING_MUSIC_ANALYSIS_BINDING_NAME = "bindingName"; +static const std::string SETTING_REQUESTED_NORMALIZED_ENERGIES = "requestedNormalizedEnergies"; // Commands static const char *COMMAND_PLAY_NAME = "Play"; @@ -53,9 +60,13 @@ static const char *EVENTHANDLER_ON_TRACK_CHANGED_NAME = "OnTrackChanged"; // Data Types static const char *DATA_TYPE_PLAYBACK_STATE = "playbackState"; // spec named +static const char *DATA_TYPE_MUSIC_ANALYSIS_STATE = "musicAnalysisState"; // 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 *PROPERTY_BPM = "beatsPerMinute"; +static const char *PROPERTY_NORMALIZED_ENERGIES = "normalizedEnergies"; + static const char *DATA_TYPE_SEEK_POSITION = "SeekToPositionType"; static const char *DATA_TYPE_TOGGLE = "ToggleType"; static const char *PROPERTY_TOGGLE_NAME = "name"; @@ -104,6 +115,12 @@ struct AplAudioPlayerExtension::ActivityState { /// The playback state name std::string playbackStateName; + /// The music analysis state name + std::string musicAnalysisStateName; + + /// The requested normalized energies in settings + int requestedNormalizedEnergies{0}; + /// The identifier of the track displaying lyrics. std::string token; @@ -166,10 +183,12 @@ struct AplAudioPlayerExtension::ActivityState { static std::atomic_int sToken(53); AplAudioPlayerExtension::AplAudioPlayerExtension(std::shared_ptr observer) - : alexaext::ExtensionBase(URI), mObserver(std::move(observer)) + : alexaext::ExtensionBase(EXTENSION_URIS), mObserver(std::move(observer)) { mPlaybackStateActivity = "STOPPED"; mPlaybackStateOffset = 0; + mBeatsPerMinute = 0; + mCanAnalyzeMusic = false; } void @@ -183,6 +202,24 @@ AplAudioPlayerExtension::applySettings(const ActivityDescriptor &activity, const if (playbackStateName != settings.MemberEnd() && playbackStateName->value.IsString()) { getOrCreateActivityState(activity)->playbackStateName = settings[SETTING_PLAYBACK_STATE_NAME].GetString(); } + + auto uri = activity.getURI(); + if (uri == URI_11 && mCanAnalyzeMusic) { + auto musicAnalysisStateName = settings.FindMember(SETTING_MUSIC_ANALYSIS_BINDINGS); + if (musicAnalysisStateName != settings.MemberEnd() && musicAnalysisStateName->value.IsObject()) { + const rapidjson::Value& musicAnalysisBindings = settings[SETTING_MUSIC_ANALYSIS_BINDINGS]; + for (auto& binding : musicAnalysisBindings.GetObject()) { + std::string bindingKey = binding.name.GetString(); + if (bindingKey == SETTING_MUSIC_ANALYSIS_BINDING_NAME) { + getOrCreateActivityState(activity)->musicAnalysisStateName = binding.value.GetString(); + } else if (bindingKey == SETTING_REQUESTED_NORMALIZED_ENERGIES) { + auto count = binding.value.GetInt(); + getOrCreateActivityState(activity)->requestedNormalizedEnergies = count; + mObserver->onRequestedNormalizedEnergies(count); + } + } + } + } } rapidjson::Document @@ -190,9 +227,11 @@ AplAudioPlayerExtension::createRegistration(const ActivityDescriptor& activity, const rapidjson::Value ®istrationRequest) { auto uri = activity.getURI(); - if (uri != URI) { + if (!isValidURI(uri)) { return RegistrationFailure::forUnknownURI(uri); } + // Add to requested URIs set + mRequestedURIs.emplace(uri); // extract document assigned settings const auto *settingsValue = RegistrationRequest::SETTINGS().Get(registrationRequest); @@ -204,19 +243,32 @@ AplAudioPlayerExtension::createRegistration(const ActivityDescriptor& activity, // return success with the schema and environment return RegistrationSuccess(SCHEMA_VERSION) - .uri(URI) + .uri(uri) .token(clientToken) - .environment([](Environment &environment) { - environment.version(ENVIRONMENT_VERSION); + .environment([&](Environment &environment) { + if (uri == URI) { + environment.version(ENVIRONMENT_VERSION); + } else if (uri == URI_11) { + environment.version(ENVIRONMENT_VERSION_11) + .property(ENVIRONMENT_MUSIC_ANALYSIS, mCanAnalyzeMusic) + .property(ENVIRONMENT_MAX_NORMALIZED_ENERGIES, mMaxNormalizedEnergies); + } }) .schema("1.0", [&](ExtensionSchema &schema) { - schema.uri(URI) + schema.uri(uri) .dataType(DATA_TYPE_PLAYBACK_STATE, [](TypeSchema &dataTypeSchema) { dataTypeSchema .property(PROPERTY_PLAYER_ACTIVITY, "string") .property(PROPERTY_OFFSET, "number"); - }) - .dataType(DATA_TYPE_SEEK_POSITION, [](TypeSchema &dataTypeSchema) { + }); + if (uri == URI_11 && mCanAnalyzeMusic) { + schema.dataType(DATA_TYPE_MUSIC_ANALYSIS_STATE, [](TypeSchema &dataTypeSchema) { + dataTypeSchema + .property(PROPERTY_BPM, "number") + .property(PROPERTY_NORMALIZED_ENERGIES, "array"); + }); + } + schema.dataType(DATA_TYPE_SEEK_POSITION, [](TypeSchema &dataTypeSchema) { dataTypeSchema.property(PROPERTY_OFFSET, [](TypePropertySchema &propertySchema) { propertySchema.type("number") .required(true) @@ -308,16 +360,34 @@ AplAudioPlayerExtension::createRegistration(const ActivityDescriptor& activity, liveDataSchema.dataType(DATA_TYPE_PLAYBACK_STATE); }); } + if (uri == URI_11 && mCanAnalyzeMusic) { + auto musicAnalysisStateName = getOrCreateActivityState(activity)->musicAnalysisStateName; + if (!musicAnalysisStateName.empty()) { + schema.liveDataMap(musicAnalysisStateName, [](LiveDataSchema &liveDataSchema) { + liveDataSchema.dataType(DATA_TYPE_MUSIC_ANALYSIS_STATE); + }); + } + } }); } +bool +AplAudioPlayerExtension::isValidURI(const std::string &uri) { + return EXTENSION_URIS.find(uri) != EXTENSION_URIS.end(); +} + +bool +AplAudioPlayerExtension::isURIRequested(const std::string &uri) { + return mRequestedURIs.find(uri) != mRequestedURIs.end(); +} + bool AplAudioPlayerExtension::invokeCommand(const ActivityDescriptor& activity, const rapidjson::Value &command) { auto uri = activity.getURI(); // unknown URI - if (uri != URI) + if (!isValidURI(uri)) return false; // no player attached if (!mObserver) @@ -438,6 +508,7 @@ AplAudioPlayerExtension::onActivityUnregistered(const ActivityDescriptor &activi flushLyricData(getOrCreateActivityState(activity)); std::lock_guard lock(mStateMutex); mActivityStateMap.erase(activity); + mRequestedURIs.clear(); } std::shared_ptr @@ -477,11 +548,6 @@ AplAudioPlayerExtension::updatePlayerActivity(const std::string &state, int offs mPlaybackStateActivity = state; mPlaybackStateOffset = offset; } - - auto event = Event("1.0").uri(URI).target(URI) - .name(EVENTHANDLER_ON_PLAYER_ACTIVITY_UPDATED_NAME) - .property(PROPERTY_PLAYER_ACTIVITY, state) - .property(PROPERTY_OFFSET, offset); publishLiveData(); // Make a list of activities to update with the lock @@ -494,6 +560,11 @@ AplAudioPlayerExtension::updatePlayerActivity(const std::string &state, int offs } for (const auto &activity: activitiesToUpdate) { + auto uri = activity.getURI(); + auto event = Event("1.0").uri(uri).target(uri) + .name(EVENTHANDLER_ON_PLAYER_ACTIVITY_UPDATED_NAME) + .property(PROPERTY_PLAYER_ACTIVITY, state) + .property(PROPERTY_OFFSET, offset); invokeExtensionEventHandler(activity, event); } } @@ -508,6 +579,25 @@ AplAudioPlayerExtension::updatePlaybackProgress(int offset) publishLiveData(); } +void +AplAudioPlayerExtension::setMusicAnalysisDetails(bool canAnalyze, int maxNormalizedEnergies) { + mCanAnalyzeMusic = canAnalyze; + mMaxNormalizedEnergies = canAnalyze ? maxNormalizedEnergies : 0; +} + +void +AplAudioPlayerExtension::updateMusicAnalysis(int beatsPerMinute, const std::vector normalizedEnergies) +{ + if (isURIRequested(URI_11) && mCanAnalyzeMusic) { + { + std::lock_guard lock(mStateMutex); + mBeatsPerMinute = beatsPerMinute; + mNormalizedEnergies = normalizedEnergies; + } + publishLiveDataForMusicAnalysis(); + } +} + void AplAudioPlayerExtension::updateCurrentAudioItemId(const std::string& audioItemId) { @@ -515,10 +605,6 @@ 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 @@ -531,6 +617,10 @@ AplAudioPlayerExtension::updateCurrentAudioItemId(const std::string& audioItemId } for (const auto &activity: activitiesToUpdate) { + auto uri = activity.getURI(); + auto event = Event("1.0").uri(uri).target(uri) + .name(EVENTHANDLER_ON_TRACK_CHANGED_NAME) + .property(PROPERTY_AUDIO_ITEM_ID, audioItemId); invokeExtensionEventHandler(activity, event); } } @@ -553,9 +643,10 @@ AplAudioPlayerExtension::publishLiveData() // Publish live data for activities that set the playback state name if (!playbackStateName.empty()) { auto liveDataUpdate = std::make_shared("1.0"); - liveDataUpdate->uri(URI) + auto uri = it.first.getURI(); + liveDataUpdate->uri(uri) .objectName(playbackStateName) - .target(URI) + .target(uri) .liveDataMapUpdate([&](LiveDataMapOperation& operation) { operation.type("Set") .key(PROPERTY_PLAYER_ACTIVITY) @@ -577,3 +668,49 @@ AplAudioPlayerExtension::publishLiveData() invokeLiveDataUpdate(it.first, it.second->getDocument()); } } + +void +AplAudioPlayerExtension::publishLiveDataForMusicAnalysis() +{ + if (!isURIRequested(URI_11) || !mCanAnalyzeMusic) { + return; + } + // Make a list of updates with the lock + std::unordered_map, ActivityDescriptor::Hash> updates; + { + std::lock_guard lock(mStateMutex); + for (const auto &it: mActivityStateMap) { + auto uri = it.first.getURI(); + if (uri != URI_11) { + continue; + } + const auto musicAnalysisStateName = it.second->musicAnalysisStateName; + if (!musicAnalysisStateName.empty()) { + auto liveDataUpdate = std::make_shared("1.0"); + LiveDataUpdate liveUpdate = liveDataUpdate->uri(URI_11) + .objectName(musicAnalysisStateName) + .target(URI_11); + // Beats Per Minute + liveUpdate.liveDataMapUpdate([&](LiveDataMapOperation& operation) { + operation.type("Set").key(PROPERTY_BPM).item(mBeatsPerMinute); + }); + // Normalized Energies + rapidjson::Value normalizedEnergies(rapidjson::kArrayType); + rapidjson::Document::AllocatorType& allocator = liveUpdate.getDocument().GetAllocator(); + for (auto& it : mNormalizedEnergies) { + normalizedEnergies.PushBack(rapidjson::Value().SetDouble(it).Move(), allocator); + } + liveUpdate.liveDataMapUpdate([&](LiveDataMapOperation& operation) { + operation.type("Set") + .key(PROPERTY_NORMALIZED_ENERGIES) + .item(normalizedEnergies); + }); + updates.emplace(it.first, liveDataUpdate); + } + } + } + + for (const auto& it: updates) { + invokeLiveDataUpdate(it.first, it.second->getDocument()); + } +} \ No newline at end of file diff --git a/extensions/alexaext/src/APLMetricsExtensionV2/AplMetricsExtensionV2.cpp b/extensions/alexaext/src/APLMetricsExtensionV2/AplMetricsExtensionV2.cpp new file mode 100644 index 0000000..bcf3605 --- /dev/null +++ b/extensions/alexaext/src/APLMetricsExtensionV2/AplMetricsExtensionV2.cpp @@ -0,0 +1,486 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ +#include +#include + +#include "alexaext/APLMetricsExtensionV2/AplMetricsExtensionV2.h" + +using namespace alexaext; +using namespace alexaext::metrics; +using namespace alexaext::metricsExtensionV2; + +static constexpr char METRIC_DESTINATION_PATH[] = "/destination"; +static constexpr char METRIC_TYPE_PATH[] = "/type"; + +static constexpr char COMMAND_INCREMENTCOUNTER_NAME[] = "IncrementCounter"; +static constexpr char COMMAND_STARTTIMER_NAME[] = "StartTimer"; +static constexpr char COMMAND_STOPTIMER_NAME[] = "StopTimer"; +static constexpr char COMMAND_CREATECOUNTER_NAME[] = "CreateCounter"; +static constexpr char COMMAND_RECORDVALUE_NAME[] = "RecordValue"; + +static constexpr char PROPERTY_METRIC_ID[] = "metricId"; +static constexpr char PROPERTY_METRIC_NAME[] = "metricName"; +static constexpr char PROPERTY_INITIAL_VALUE[] = "initialValue"; +static constexpr char PROPERTY_VALUE[] = "value"; +static constexpr char PROPERTY_METRIC_DIMENSIONS[] = "dimensions"; +static constexpr char PROPERTY_METRIC_DIMENSIONS_PATH[] = "/dimensions"; +static constexpr char PROPERTY_AMOUNT[] = "amount"; + +static constexpr char INCREMENT_COUNTER_DATA_TYPE[] = "IncrementCounterDataType"; +static constexpr char CREATE_COUNTER_DATA_TYPE[] = "CreateCounterDataType"; +static constexpr char START_TIMER_DATA_TYPE[] = "StartTimerDataType"; +static constexpr char STOP_TIMER_DATA_TYPE[] = "StopTimerDataType"; +static constexpr char RECORD_VALUE_DATA_TYPE[] = "RecordValueDataType"; + +static constexpr char STRING_TYPE[] = "String"; +static constexpr char INTEGER_TYPE[] = "Integer"; +static constexpr char OBJECT_TYPE[] = "object"; +static constexpr char DEFAULT_EMPTY_STRING[] = ""; + +static constexpr char SCHEMA_VERSION[] = "1.0"; + +AplMetricsExtensionV2::AplMetricsExtensionV2( + const DestinationFactoryInterfacePtr& destinationFactory, const ExecutorPtr& executor) + : ExtensionBase(URI_V2), mDestinationFactory(destinationFactory), mExecutor(executor) {} + +rapidjson::Document +AplMetricsExtensionV2::createRegistration(const ActivityDescriptor& activity, + const rapidjson::Value& registrationRequest) { + if (activity.getURI() != URI_V2) { + return RegistrationFailure::forUnknownURI(activity.getURI()); + } + + auto* settings = RegistrationRequest::SETTINGS().Get(registrationRequest); + if (!settings || !settings->IsObject()) { + return RegistrationFailure::forInvalidExtensionSchema(activity.getURI()); + } + + // Validate Destination + static const rapidjson::Pointer ptr(METRIC_DESTINATION_PATH); + auto destinationPtr = ptr.Get(*settings); + if (!destinationPtr || !destinationPtr->IsObject()) { + return RegistrationFailure::forInvalidExtensionSchema(activity.getURI()); + } + + static const rapidjson::Pointer metricPathPtr(METRIC_TYPE_PATH); + auto typePtr = metricPathPtr.Get(*destinationPtr); + if (!typePtr || !typePtr->IsString()) { + // Destination type required string is not present. + return RegistrationFailure::forInvalidExtensionSchema(activity.getURI()); + } + + auto destination = mDestinationFactory->createDestination(*settings); + if (!destination) { + return RegistrationFailure::forException(activity.getURI(), + "Unable to setup destination for given type."); + } + + queueTask([this, activity, destination]() mutable { + auto activityMetrics = getActivityMetrics(activity); + if (!activityMetrics) { + addActivity(activity, destination); + } + }); + + return RegistrationSuccess(SCHEMA_VERSION) + .uri(URI_V2) + .token("") + .environment([&](Environment& environment) { environment.version(ENVIRONMENT_VERSION_V2); }) + .schema(SCHEMA_VERSION, [&](ExtensionSchema& schema) { + schema.uri(URI_V2) + .command(COMMAND_STARTTIMER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(START_TIMER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .command(COMMAND_STOPTIMER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(STOP_TIMER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .command(COMMAND_CREATECOUNTER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(CREATE_COUNTER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .command(COMMAND_INCREMENTCOUNTER_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(INCREMENT_COUNTER_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .command(COMMAND_RECORDVALUE_NAME, + [](CommandSchema& commandSchema) { + commandSchema.dataType(RECORD_VALUE_DATA_TYPE); + commandSchema.allowFastMode(true); + }) + .dataType(START_TIMER_DATA_TYPE, + [](TypeSchema& typeSchema) { + typeSchema + .property(PROPERTY_METRIC_ID, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }) + .property( + PROPERTY_METRIC_NAME, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(false); + }) + .property(PROPERTY_METRIC_DIMENSIONS, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(OBJECT_TYPE) + .required(false) + .defaultValue("{}"); + }); + }) + .dataType(STOP_TIMER_DATA_TYPE, + [](TypeSchema& typeSchema) { + typeSchema.property( + PROPERTY_METRIC_ID, [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }); + }) + .dataType( + CREATE_COUNTER_DATA_TYPE, + [](TypeSchema& typeSchema) { + typeSchema + .property(PROPERTY_METRIC_ID, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }) + .property(PROPERTY_METRIC_NAME, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(false); + }) + .property(PROPERTY_INITIAL_VALUE, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(INTEGER_TYPE).defaultValue(0); + }) + .property(PROPERTY_METRIC_DIMENSIONS, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(OBJECT_TYPE) + .required(false) + .defaultValue("{}"); + }); + }) + .dataType(INCREMENT_COUNTER_DATA_TYPE, + [](TypeSchema& typeSchema) { + typeSchema + .property(PROPERTY_METRIC_ID, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }) + .property( + PROPERTY_AMOUNT, [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(INTEGER_TYPE).defaultValue(1); + }); + }) + .dataType(RECORD_VALUE_DATA_TYPE, [](TypeSchema& typeSchema) { + typeSchema + .property(PROPERTY_METRIC_NAME, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(STRING_TYPE).required(true); + }) + .property(PROPERTY_VALUE, + [](TypePropertySchema& typePropertySchema) { + typePropertySchema.type(INTEGER_TYPE).required(true); + }) + .property(PROPERTY_METRIC_DIMENSIONS, [](TypePropertySchema& + typePropertySchema) { + typePropertySchema.type(OBJECT_TYPE).required(false).defaultValue("{}"); + }); + }); + }); +} + +bool +AplMetricsExtensionV2::invokeCommand(const ActivityDescriptor& activity, + const rapidjson::Value& command) { + Timestamp invokeCommandTime = std::chrono::steady_clock::now(); + const std::string commandName = GetWithDefault(Command::NAME(), command, DEFAULT_EMPTY_STRING); + if (commandName.empty()) { + return false; + } + const rapidjson::Value* params = Command::PAYLOAD().Get(command); + + std::string metricId = GetWithDefault(PROPERTY_METRIC_ID, params, DEFAULT_EMPTY_STRING); + if (metricId.empty() && COMMAND_RECORDVALUE_NAME != + commandName) { // If the APL doc explicitly passes empty metricId + return true; // Do not emmit mertic as per spec, but command succeeds. + } + + Dimensions dim; + if (params->HasMember(PROPERTY_METRIC_DIMENSIONS)) { + dim = getDimensionMap(*params); + } + + std::string metricName = + GetWithDefault(PROPERTY_METRIC_NAME, params, + metricId); // If the APL doc explicitly passes empty metricName + if (metricName.empty()) { + metricName = metricId; + } + + if (COMMAND_CREATECOUNTER_NAME == commandName) { + auto amount = GetWithDefault(PROPERTY_INITIAL_VALUE, params, 0); + return queueTask([this, activity, metricName, metricId, dim, amount]() mutable { + createCounter(activity, std::move(metricName), std::move(metricId), std::move(dim), + amount); + }); + } + + if (COMMAND_INCREMENTCOUNTER_NAME == commandName) { + auto amount = GetWithDefault(PROPERTY_AMOUNT, params, 1); + return queueTask([this, activity, metricId, amount]() mutable { + incrementCounter(activity, std::move(metricId), amount); + }); + } + + if (COMMAND_STARTTIMER_NAME == commandName) { + return queueTask([this, activity, metricName, metricId, dim, invokeCommandTime]() mutable { + startTimer(activity, std::move(metricName), std::move(metricId), std::move(dim), + std::move(invokeCommandTime)); + }); + } + + if (COMMAND_STOPTIMER_NAME == commandName) { + return queueTask([this, activity, metricId, invokeCommandTime]() mutable { + stopTimer(activity, metricId, invokeCommandTime); + }); + } + + if (COMMAND_RECORDVALUE_NAME == commandName) { + auto value = GetWithDefault(PROPERTY_VALUE, params, 0); + return queueTask([this, activity, metricName, dim, value]() mutable { + recordValue(activity, std::move(metricName), std::move(dim), value); + }); + } + + return false; +} + +Dimensions +AplMetricsExtensionV2::getDimensionMap(const rapidjson::Value& param) { + Dimensions dim; + static const rapidjson::Pointer ptr(PROPERTY_METRIC_DIMENSIONS_PATH); + auto dimensionPtr = ptr.Get(param); + if (!dimensionPtr || !dimensionPtr->IsObject()) { + return dim; + } + + for (rapidjson::Value::ConstMemberIterator itr = dimensionPtr->MemberBegin(); + itr != dimensionPtr->MemberEnd(); ++itr) { + if (itr->value.IsString()) { + dim[itr->name.GetString()] = itr->value.GetString(); + } + } + return dim; +} + +bool +AplMetricsExtensionV2::recordValue(const ActivityDescriptor& activity, std::string&& metricName, + Dimensions&& dimensions, int value) { + MetricTracker metricInfo(std::move(metricName), std::move(dimensions), value); + + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) { + return false; + } + + activityMetricData->publish(std::move(metricInfo.mMetric)); + return true; +} + +bool +AplMetricsExtensionV2::createCounter(const ActivityDescriptor& activity, std::string&& metricName, + std::string&& metricId, Dimensions&& dimensions, int amount) { + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) { + return false; + } + + activityMetricData->createCounter(std::move(metricName), std::move(metricId), + std::move(dimensions), amount); + return true; +} + +bool +AplMetricsExtensionV2::incrementCounter(const ActivityDescriptor& activity, std::string&& metricId, + const int amount) { + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) { + return false; + } + + activityMetricData->incrementCounter(std::move(metricId), amount); + return true; +} + +bool +AplMetricsExtensionV2::startTimer(const ActivityDescriptor& activity, std::string&& metricName, + std::string&& metricId, Dimensions&& dimensions, + Timestamp&& startTime) { + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) { + return false; + } + + activityMetricData->startTimer(std::move(metricName), std::move(metricId), + std::move(dimensions), std::move(startTime)); + return true; +} + +bool +AplMetricsExtensionV2::stopTimer(const ActivityDescriptor& activity, const std::string& metricId, + const Timestamp& stopTime) { + auto activityMetricData = getActivityMetrics(activity); + if (!activityMetricData) { + return false; + } + + Metric timerMetric; + if (!activityMetricData->stopTimer(metricId, timerMetric, stopTime)) { + return false; + } + + activityMetricData->publish(std::move(timerMetric)); + return true; +} + +void +AplMetricsExtensionV2::onActivityUnregistered(const ActivityDescriptor& activity) { + queueTask([this, activity]() mutable { + auto activtyMetrics = removeActivity(activity); + if (!activtyMetrics) { + return; + } + activtyMetrics->publish(); + }); +} + +bool +AplMetricsExtensionV2::queueTask(Executor::Task&& task) { + auto executor = mExecutor.lock(); + if (!executor) { + return false; + } + + return executor->enqueueTask(std::move(task)); +} + +bool +AplMetricsExtensionV2::addActivity( + const alexaext::ActivityDescriptor& activity, + const std::shared_ptr& destinationInterface) { + if (mActivityMetricKeysMap.find(activity) != mActivityMetricKeysMap.end()) { + // Activity already registered + return false; + } + + mActivityMetricKeysMap[activity] = std::make_shared(destinationInterface); + return true; +} + +std::shared_ptr +AplMetricsExtensionV2::removeActivity(const alexaext::ActivityDescriptor& activity) { + std::shared_ptr ret; + auto itr = mActivityMetricKeysMap.find(activity); + if (itr != mActivityMetricKeysMap.end()) { + ret = itr->second; + mActivityMetricKeysMap.erase(activity); + } + + return ret; +} + +std::shared_ptr +AplMetricsExtensionV2::getActivityMetrics(const alexaext::ActivityDescriptor& activity) { + auto itr = mActivityMetricKeysMap.find(activity); + if (itr != mActivityMetricKeysMap.end()) { + return itr->second; + } + return nullptr; +} + +//================================== +// Definitions from MetricDataList +AplMetricsExtensionV2::MetricDataList::MetricDataList( + const std::shared_ptr& destinationInterface) + : mDestinationInterface(destinationInterface) {} + +void +AplMetricsExtensionV2::MetricDataList::publish(Metric&& metric) { + mDestinationInterface->publish(metric); +} + +void +AplMetricsExtensionV2::MetricDataList::publish() { + std::vector counterMetrics; + counterMetrics.reserve(mMetricIdCounterMetricData.size()); + for (const auto& metric : mMetricIdCounterMetricData) { + counterMetrics.emplace(counterMetrics.end(), metric.second->getMetric()); + } + + if (!counterMetrics.empty()) { + mDestinationInterface->publish(std::move(counterMetrics)); + } + + mMetricIdCounterMetricData.clear(); +} + +void +AplMetricsExtensionV2::MetricDataList::createCounter(std::string&& metricName, + std::string&& metricId, + Dimensions&& dimensions, int amount) { + auto counterTracker = std::make_shared(std::move(metricName), + std::move(dimensions), amount); + mMetricIdCounterMetricData[metricId] = std::move(counterTracker); +} + +void +AplMetricsExtensionV2::MetricDataList::incrementCounter(std::string&& metricId, int amount) { + auto counterMetricItr = mMetricIdCounterMetricData.find(metricId); + if (counterMetricItr == mMetricIdCounterMetricData.end()) { + // If counter not present, create a new counter + // In this case metricId becomes the metricName as well + std::string metricName = metricId; + createCounter(std::move(metricName), std::move(metricId), {}, amount); + } + else { + auto& counterMetricTracker = counterMetricItr->second; + counterMetricTracker->incrementCounter(amount); + } +} + +void +AplMetricsExtensionV2::MetricDataList::startTimer(std::string&& metricName, std::string&& metricId, + Dimensions&& dimensions, Timestamp&& startTime) { + auto timerTracker = std::make_shared( + std::move(metricName), std::move(dimensions), std::move(startTime)); + mMetricIdTimerData[metricId] = std::move(timerTracker); +} + +bool +AplMetricsExtensionV2::MetricDataList::stopTimer(const std::string& metricId, Metric& metric, + const Timestamp& stopTime) { + auto timerMetricItr = mMetricIdTimerData.find(metricId); + if (timerMetricItr == mMetricIdTimerData.end()) { + return false; + } + + auto& timerMetric = timerMetricItr->second; + return timerMetric->stop(metric, stopTime); +} +// Definitions from MetricDataList +//================================== diff --git a/extensions/unit/CMakeLists.txt b/extensions/unit/CMakeLists.txt index b273334..df92de3 100644 --- a/extensions/unit/CMakeLists.txt +++ b/extensions/unit/CMakeLists.txt @@ -22,6 +22,7 @@ add_executable(alexaext-unittest unittest_apl_audio_player.cpp unittest_apl_e2e_encryption.cpp unittest_apl_metric.cpp + unittest_apl_metricV2.cpp unittest_apl_webflow.cpp unittest_apl_music_alarm.cpp unittest_activity_descriptor.cpp diff --git a/extensions/unit/unittest_apl_audio_player.cpp b/extensions/unit/unittest_apl_audio_player.cpp index c2b1b18..8a99c1d 100644 --- a/extensions/unit/unittest_apl_audio_player.cpp +++ b/extensions/unit/unittest_apl_audio_player.cpp @@ -65,12 +65,16 @@ class TestAudioPlayerObserver : public AplAudioPlayerExtensionObserverInterface void onAudioPlayerSkipBackward() override { mCommand = "BACKWARD"; } + void onRequestedNormalizedEnergies(int count) override + { mRequestedNormalizedEnergies = count; } + public: std::string mCommand; double mParaNum; bool mParamBool; std::string mParamJson; // string representation of json std::string mParamString; + int mRequestedNormalizedEnergies; }; class TestAudioPlayerExtension : public AplAudioPlayerExtension { @@ -108,13 +112,21 @@ class AplAudioPlayerExtensionTest : public ::testing::Test { /** * Simple registration for testing event/command/data. */ - ::testing::AssertionResult registerExtension(const alexaext::ActivityDescriptor& activity) + ::testing::AssertionResult registerExtension(const alexaext::ActivityDescriptor& activity, std::string uri = URI) { Document settings(kObjectType); settings.AddMember("playbackStateName", Value("MyPlayBackState"), settings.GetAllocator()); - Document regReq = RegistrationRequest("1.0").uri(URI) + if (uri == URI_11) { + rapidjson::Value musicAnalysisBindings(kObjectType); + musicAnalysisBindings.AddMember("bindingName", "musicAnalysisStatus", settings.GetAllocator()); + musicAnalysisBindings.AddMember("requestedNormalizedEnergies", 5, settings.GetAllocator()); + settings.AddMember("musicAnalysisBindings", musicAnalysisBindings, settings.GetAllocator()); + } + + Document regReq = RegistrationRequest("1.0").uri(uri) .settings(settings); + mExtension->setMusicAnalysisDetails(true, 5); auto registration = mExtension->createRegistration(activity, regReq); mExtension->onActivityRegistered(activity); auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); @@ -225,7 +237,7 @@ TEST_F(AplAudioPlayerExtensionTest, CreateExtension) ASSERT_TRUE(mObserver); ASSERT_TRUE(mExtension); auto supported = mExtension->getURIs(); - ASSERT_EQ(1, supported.size()); + ASSERT_EQ(2, supported.size()); ASSERT_NE(supported.end(), supported.find(URI)); } @@ -421,6 +433,62 @@ TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsHasLiveData) } +/** + * LiveData registration is defined with settings for audio metadata. + */ +TEST_F(AplAudioPlayerExtensionTest, RegistrationSettingsHasAudioMetadataLiveData) +{ + Document settings(kObjectType); + rapidjson::Value musicAnalysisBindings(kObjectType); + musicAnalysisBindings.AddMember("bindingName", "musicAnalysisStatus", settings.GetAllocator()); + musicAnalysisBindings.AddMember("requestedNormalizedEnergies", 5, settings.GetAllocator()); + settings.AddMember("musicAnalysisBindings", musicAnalysisBindings, settings.GetAllocator()); + + auto activity = createActivityDescriptor(URI_11); + Document regReq = RegistrationRequest("1.0").uri(URI_11) + .settings(settings); + mExtension->setMusicAnalysisDetails(true, 5); + auto registration = mExtension->createRegistration(activity, regReq); + ASSERT_STREQ("RegisterSuccess", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + Value *schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + // live data defined + Value *liveData = ExtensionSchema::LIVE_DATA().Get(*schema); + ASSERT_TRUE(liveData); + ASSERT_TRUE(liveData->IsArray() && liveData->Size() == 1); + + const Value& data = (*liveData)[0]; + ASSERT_TRUE(data.IsObject()); + auto name = GetWithDefault(LiveDataSchema::NAME(), data, ""); + ASSERT_STREQ("musicAnalysisStatus", name); + auto type = GetWithDefault(LiveDataSchema::DATA_TYPE(), data, ""); + ASSERT_STREQ("musicAnalysisState", type); + + Value *types = ExtensionSchema::TYPES().Get(*schema); + ASSERT_TRUE(types); + ASSERT_TRUE(liveData->IsArray()); + + Value *stateType = findDataType(types, "musicAnalysisState"); + ASSERT_NE(nullptr, stateType); + ASSERT_TRUE(stateType->IsObject()); + + rapidjson::Document expected; + expected.Parse(R"( + { + "name": "musicAnalysisState", + "properties": { + "beatsPerMinute": "number", + "normalizedEnergies": "array" + } + } + )"); + ASSERT_FALSE(expected.HasParseError()); + ASSERT_TRUE(IsEqual(expected, *stateType)); + +} + /** * Invalid settings on registration are handled and defaults are used. **/ @@ -1056,6 +1124,36 @@ TEST_F(AplAudioPlayerExtensionTest, InvokeCommandAddLyricsDurationInMilliseconds ASSERT_EQ(53, mObserver->mParaNum); } +/** + * Command on null player + */ +TEST_F(AplAudioPlayerExtensionTest, InvokeCommandNullPlayer) +{ + std::shared_ptr testExtension = std::make_shared(nullptr); + auto activity = createActivityDescriptor(); + + auto command = Command("1.0").target(mClientToken) + .uri(URI) + .name("Play"); + auto invoke = testExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); +} + +/** + * Command on invalid URI + */ +TEST_F(AplAudioPlayerExtensionTest, InvokeCommandInvalidURI) +{ + std::string uri = "aplext:audioplayer:50"; + auto activity = createActivityDescriptor(uri); + + auto command = Command("1.0").target(mClientToken) + .uri(uri) + .name("Play"); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); +} + /** * Playback progress change updates live data. */ @@ -1083,6 +1181,40 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlaybackProgressSuccess) ASSERT_TRUE(gotUpdate); } +/** + * Track music analysis update change updates live data. + */ +TEST_F(AplAudioPlayerExtensionTest, UpdateMusicAnalysisSuccess) +{ + auto activity = createActivityDescriptor(URI_11); + ASSERT_TRUE(registerExtension(activity, URI_11)); + + 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:11", + GetWithDefault(RegistrationSuccess::TARGET(), liveDataUpdate, "")); + const Value *ops = LiveDataUpdate::OPERATIONS().Get(liveDataUpdate); + ASSERT_TRUE(ops); + ASSERT_TRUE(ops->IsArray() && ops->Size() == 2); + ASSERT_TRUE(CheckLiveData(ops->GetArray()[0], "Set", "beatsPerMinute", 20)); + ASSERT_TRUE(CheckLiveData(ops->GetArray()[1], "Set", "normalizedEnergies")); + const rapidjson::Value *itm = LiveDataMapOperation::ITEM().Get(ops->GetArray()[1]); + ASSERT_TRUE(itm->IsArray() && itm->Size() == 5); + if (itm->IsArray()) { + for (auto& v : itm->GetArray()) { + ASSERT_TRUE(v == 0.25); + } + } + }); + std::vector normalizedEnergies{ 0.25, 0.25, 0.25, 0.25, 0.25 }; + mExtension->updateMusicAnalysis(20, normalizedEnergies); + ASSERT_TRUE(gotUpdate); +} + /** * Currently playing track info change updates live data. */ @@ -1188,6 +1320,29 @@ TEST_F(AplAudioPlayerExtensionTest, UpdatePlayerActivityEventSuccess) ASSERT_TRUE(gotUpdate); } +/** + * Music analysis update change for lower version + * and disabled runtime is ignored. + */ +TEST_F(AplAudioPlayerExtensionTest, UpdateMusicAnalysisFailure) +{ + auto activity = createActivityDescriptor(); + ASSERT_TRUE(registerExtension(activity)); + + bool gotUpdate = false; + mExtension->registerLiveDataUpdateCallback( + [&](const ActivityDescriptor& activity, const rapidjson::Value &liveDataUpdate) { + gotUpdate = true; + }); + std::vector normalizedEnergies{ 0.25, 0.25, 0.25, 0.25, 0.25 }; + mExtension->updateMusicAnalysis(20, normalizedEnergies); + ASSERT_FALSE(gotUpdate); + + mExtension->setMusicAnalysisDetails(false); + mExtension->updateMusicAnalysis(20, normalizedEnergies); + ASSERT_FALSE(gotUpdate); +} + /** * Invalid updates to playback state and progress are ignored. */ diff --git a/extensions/unit/unittest_apl_metricV2.cpp b/extensions/unit/unittest_apl_metricV2.cpp new file mode 100644 index 0000000..d514ae7 --- /dev/null +++ b/extensions/unit/unittest_apl_metricV2.cpp @@ -0,0 +1,890 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * + * Licensed under the Apache License, Version 2.0 (the "License"). + * You may not use this file except in compliance with the License. + * A copy of the License is located at + * + * http://aws.amazon.com/apache2.0/ + * + * or in the "license" file accompanying this file. This file is distributed + * on an "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either + * express or implied. See the License for the specific language governing + * permissions and limitations under the License. + */ + +#include "gtest/gtest.h" +#include +#include +#include +#include + +#include "alexaext/APLMetricsExtensionV2/AplMetricsExtensionV2.h" +#include "alexaext/APLMetricsExtensionV2/DestinationInterface.h" +#include "alexaext/extensionmessage.h" + +using namespace alexaext; +using namespace alexaext::metricsExtensionV2; +using namespace rapidjson; + +const char* const DESTINATION = "destination"; +const char* const DIMENSIONS = "dimensions"; + +static const char* METRIC_ID = "metricId"; +static const char* AMOUNT = "amount"; + +bool shouldCreateDestinationSucceed; +bool shouldCreateDestinationBeCalled; + +#define AssertPublishMetricForDestination(isQueued, isPublished, destination) \ + { \ + ASSERT_EQ(isQueued, mExecutor->taskQueued); \ + if (destination) { \ + ASSERT_EQ(isPublished, (destination)->metricPublished()); \ + } \ + } + +#define AssertLastPublishMetric(isQueued, isPublished) \ + { AssertPublishMetricForDestination(isQueued, isPublished, mDestFactory->lastDestinationMock); } + +#define resetPublishedFlagsForDestination(destination) \ + { \ + (destination)->publishMetricsCalled = false; \ + (destination)->publishAllMetricsCalled = false; \ + } + +#define resetLastPublishedFlags() \ + { resetPublishedFlagsForDestination(mDestFactory->lastDestinationMock) } + +class DestinationInterfaceMock : public DestinationInterface { +public: + DestinationInterfaceMock() : publishMetricsCalled(false), publishAllMetricsCalled(false) {} + void publish(Metric metric) { + publishMetricsCalled = true; + lastPublishedMetric = metric; + } + + void publish(std::vector metric) { + publishAllMetricsCalled = true; + lastPublishedMetricList = metric; + } + + bool metricPublished() { return publishMetricsCalled || publishAllMetricsCalled; } + + bool publishMetricsCalled; + bool publishAllMetricsCalled; + Metric lastPublishedMetric; + std::vector lastPublishedMetricList; +}; + +class DestinationFactoryInterfaceMock : public DestinationFactoryInterface { +public: + DestinationFactoryInterfaceMock() : createDestinationCalled(false) {} + + std::shared_ptr createDestination(const rapidjson::Value& mSettings) { + createDestinationCalled = true; + if (!shouldCreateDestinationSucceed) { + return nullptr; + } + + lastDestinationMock = std::make_shared(); + return lastDestinationMock; + } + + std::shared_ptr lastDestinationMock; + bool createDestinationCalled; +}; + +class MockExecutor : public Executor { +public: + MockExecutor() { + taskQueued = false; + mExecutorImpl = Executor::getSynchronousExecutor(); + } + bool enqueueTask(Task task) { + taskQueued = true; + return mExecutorImpl->enqueueTask(std::move(task)); + } + + void resetFlag() { taskQueued = false; } + + ExecutorPtr mExecutorImpl; + bool taskQueued; +}; + +class AplMetricsExtension2Test : public ::testing::Test { +public: + void SetUp() override { + shouldCreateDestinationSucceed = true; + shouldCreateDestinationBeCalled = true; + mDestFactory = std::make_shared(); + + mExecutor = std::make_shared(); + mExtension = std::make_shared(mDestFactory, mExecutor); + } + + void TearDown() override { + if (shouldCreateDestinationBeCalled) { + ASSERT_TRUE(mDestFactory->createDestinationCalled); + } + } + + /** + * Simple registration for testing event/command/data. + */ + rapidjson::Document registerExtension(const alexaext::ActivityDescriptor& activity, + const Dimensions& dimensions = Dimensions(), + bool withDestination = true, bool withDimension = true, + bool withDestinationType = true) { + Document metricsSettings(kObjectType); + rapidjson::Value destination(kObjectType); + + if (withDestination) { + metricsSettings.AddMember( + rapidjson::Value(DESTINATION, metricsSettings.GetAllocator()).Move(), + destination.Move(), metricsSettings.GetAllocator()); + + metricsSettings[DESTINATION].AddMember("groupId", "gid", + metricsSettings.GetAllocator()); + metricsSettings[DESTINATION].AddMember("schemaId", "schemaId", + metricsSettings.GetAllocator()); + if (withDestinationType) { + metricsSettings[DESTINATION].AddMember("type", "anyDestinationType", + metricsSettings.GetAllocator()); + } + } + + if (withDimension) { + rapidjson::Value dimension(kObjectType); + metricsSettings.AddMember( + rapidjson::Value(DIMENSIONS, metricsSettings.GetAllocator()).Move(), + dimension.Move(), metricsSettings.GetAllocator()); + + for (auto& dimension : dimensions) { + metricsSettings[DIMENSIONS].AddMember( + rapidjson::Value(dimension.first.c_str(), metricsSettings.GetAllocator()) + .Move(), + rapidjson::Value(dimension.second.c_str(), metricsSettings.GetAllocator()) + .Move(), + metricsSettings.GetAllocator()); + } + } + + Document regReq = RegistrationRequest("2.0").uri(URI_V2).settings(metricsSettings); + return mExtension->createRegistration(activity, regReq); + } + + /** + * Simple utility to create activity descriptors accross + * the tests + */ + ActivityDescriptor createActivityDescriptor(std::string uri = URI_V2) { + // Create Activity + SessionDescriptorPtr sessionPtr = SessionDescriptor::create("TestSessionId"); + ActivityDescriptor activityDescriptor(uri, sessionPtr); + return activityDescriptor; + } + + /** + * Create activity descriptors with a specific session + */ + ActivityDescriptor createActivityDescriptor(SessionDescriptorPtr session, + std::string uri = URI_V2) { + ActivityDescriptor activityDescriptor(uri, session); + return activityDescriptor; + } + + std::shared_ptr mExecutor; + std::shared_ptr mExtension; + std::shared_ptr mDestFactory; +}; + +TEST_F(AplMetricsExtension2Test, RegistrationTest) { + Dimensions dim; + dim["experienceId"] = "photos"; + + auto activity = createActivityDescriptor(); + auto registration = registerExtension(activity, dim); + + auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + FAIL() << "Failed Registration:" << method; + } +} + +TEST_F(AplMetricsExtension2Test, InvalidURI) { + Document regReq = RegistrationRequest("aplext:metrics:INVALID"); + auto reg2 = + mExtension->createRegistration(createActivityDescriptor("aplext:metrics:INVALID"), regReq); + auto method = GetWithDefault(RegistrationSuccess::METHOD(), reg2, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + SUCCEED() << "Invalid URI not accepted" << method; + } + else { + FAIL() << "Registration succeeded" << method; + } + + shouldCreateDestinationBeCalled = false; +} + +TEST_F(AplMetricsExtension2Test, RegistrationWithoutSettings) { + Document regReq = RegistrationRequest(URI_V2); + auto registration = mExtension->createRegistration(createActivityDescriptor(), regReq); + ASSERT_FALSE(registration.HasParseError()); + ASSERT_FALSE(registration.IsNull()); + ASSERT_STREQ("RegisterFailure", + GetWithDefault(RegistrationSuccess::METHOD(), registration, "")); + shouldCreateDestinationBeCalled = false; +} + +TEST_F(AplMetricsExtension2Test, RegistrationWithoutDestination) { + Dimensions dim; + dim["experienceId"] = "photos"; + + auto registration = registerExtension(createActivityDescriptor(), dim, false); + auto method = GetWithDefault(RegistrationSuccess::METHOD(), registration, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + SUCCEED() << "Destination should be present" << method; + } + + shouldCreateDestinationBeCalled = false; +} + +TEST_F(AplMetricsExtension2Test, ReRegistrationTest) { + Dimensions dim; + dim["experienceId"] = "photos"; + + auto activity = createActivityDescriptor(); + auto reg1 = registerExtension(activity, dim); + auto method = GetWithDefault(RegistrationSuccess::METHOD(), reg1, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + FAIL() << "Failed Registration for " << method; + } + ASSERT_TRUE(mDestFactory->createDestinationCalled); + mDestFactory->createDestinationCalled = false; + + auto reg2 = registerExtension(activity, dim); + method = GetWithDefault(RegistrationSuccess::METHOD(), reg2, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + SUCCEED() << "Failed Registration is correct for: " << method; + } +} + +TEST_F(AplMetricsExtension2Test, RegistrationWithEmptyDimensions) { + auto reg1 = registerExtension(createActivityDescriptor()); + + auto method = GetWithDefault(RegistrationSuccess::METHOD(), reg1, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + FAIL() << "Failed Registration:" << method; + } +} + +TEST_F(AplMetricsExtension2Test, RegistrationWithNoDimensions) { + Dimensions dim; + auto reg1 = registerExtension(createActivityDescriptor(), dim, true, false); + + auto method = GetWithDefault(RegistrationSuccess::METHOD(), reg1, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + SUCCEED() << "Empty dimensions are allowed : " << method; + } + + shouldCreateDestinationBeCalled = false; +} + +TEST_F(AplMetricsExtension2Test, RegistrationWithCreateDestinationFailed) { + shouldCreateDestinationSucceed = false; + auto reg1 = registerExtension(createActivityDescriptor()); + + auto method = GetWithDefault(RegistrationSuccess::METHOD(), reg1, "Fail"); + if (std::strcmp("RegisterSuccess", method) != 0) { + SUCCEED() << "Failed Registration:" << method; + } +} + +TEST_F(AplMetricsExtension2Test, TestCreateCounter100Dim) { + auto activity = createActivityDescriptor(); + auto reg = registerExtension(activity); + + std::string _1000Dimensions = "{"; + for (int i = 0; i < 100; i++) { + _1000Dimensions.append("\"key\"=\"so value for the key\""); + } + _1000Dimensions += "}"; + auto command = Command("1.0") + .uri(URI_V2) + .name("CreateCounter") + .property(METRIC_ID, "TestId") + .property("metricName", "testName") + .property("initialValue", 101) + .property("dimensions", _1000Dimensions.c_str()); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + mExtension->onActivityUnregistered(activity); + auto metricList = mDestFactory->lastDestinationMock->lastPublishedMetricList; + ASSERT_EQ(metricList.size(), 1); + ASSERT_EQ(101, metricList[0].value); +} + +TEST_F(AplMetricsExtension2Test, TestCreateCounter) { + auto activity = createActivityDescriptor(); + auto reg = registerExtension(activity); + + auto command = Command("1.0") + .uri(URI_V2) + .name("CreateCounter") + .property(METRIC_ID, "TestId") + .property("metricName", "testName") + .property("initialValue", 101); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + mExtension->onActivityUnregistered(activity); + auto metricList = mDestFactory->lastDestinationMock->lastPublishedMetricList; + ASSERT_EQ(metricList.size(), 1); + ASSERT_EQ(101, metricList[0].value); +} + +TEST_F(AplMetricsExtension2Test, TestIncrementCounter) { + auto activity = createActivityDescriptor(); + registerExtension(activity); + + // Creates counter if not present + auto command = Command("1.0") + .uri(URI_V2) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 3); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + // Test IncrementCounter without amount property + command = Command("1.0") + .uri(URI_V2) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 2); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + // Default Increment by 1 + command = Command("1.0").uri(URI_V2).name("IncrementCounter").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + mExtension->onActivityUnregistered(activity); + + auto metricList = mDestFactory->lastDestinationMock->lastPublishedMetricList; + ASSERT_EQ(6, metricList[0].value); +} + +TEST_F(AplMetricsExtension2Test, TestTimerMetric) { + auto activity = createActivityDescriptor(); + auto reg = registerExtension(activity); + + Document dimensionsDoc(kObjectType); + rapidjson::Value dimension(kObjectType); + dimension.AddMember("dim1", "dimVal1", dimensionsDoc.GetAllocator()); + dimension.AddMember("dim2", "dimVal2", dimensionsDoc.GetAllocator()); + dimension.AddMember("dim3", "dimVal2", dimensionsDoc.GetAllocator()); + dimension.AddMember("dim4", "dimVal2", dimensionsDoc.GetAllocator()); + + auto command = Command("1.0") + .uri(URI_V2) + .name("StartTimer") + .property(METRIC_ID, "TestId") + .property("metricName", "testName") + .property("dimensions", dimension); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + std::this_thread::sleep_for(std::chrono::milliseconds(20)); + + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + auto metric = mDestFactory->lastDestinationMock->lastPublishedMetric; + ASSERT_TRUE(0 == metric.name.compare("testName")); + ASSERT_TRUE(4 == metric.dimensions.size()); +} + +TEST_F(AplMetricsExtension2Test, TestRecordValueMetric) { + auto activity = createActivityDescriptor(); + auto reg = registerExtension(activity); + + Document dimensionsDoc(kObjectType); + rapidjson::Value dimension(kObjectType); + dimension.AddMember("dim1", "dimVal1", dimensionsDoc.GetAllocator()); + + auto command = Command("1.0") + .uri(URI_V2) + .name("RecordValue") + .property("metricName", "valueName") + .property("value", 563) + .property("dimensions", dimension); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + auto metric = mDestFactory->lastDestinationMock->lastPublishedMetric; + ASSERT_TRUE(0 == metric.name.compare("valueName")); + ASSERT_TRUE(563 == metric.value); + ASSERT_TRUE(1 == metric.dimensions.size()); +} + +TEST_F(AplMetricsExtension2Test, TestDimensionsParsing) { + auto activity = createActivityDescriptor(); + auto reg = registerExtension(activity); + + Document dimensionsDoc(kObjectType); + rapidjson::Value dimension(kObjectType); + dimension.AddMember("dim1", "dimVal1", dimensionsDoc.GetAllocator()); + dimension.AddMember("dim2", "dimVal2", dimensionsDoc.GetAllocator()); + dimension.AddMember("dim3", "dimVal3", dimensionsDoc.GetAllocator()); + dimension.AddMember("dim4", "dimVal4", dimensionsDoc.GetAllocator()); + + auto command = Command("1.0") + .uri(URI_V2) + .name("RecordValue") + .property("metricName", "valueName") + .property("value", 563) + .property("dimensions", dimension); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + + auto metric = mDestFactory->lastDestinationMock->lastPublishedMetric; + ASSERT_TRUE(4 == metric.dimensions.size()); + + for (auto dim : metric.dimensions) { + if (dim.first == "dim1") { + ASSERT_STRCASEEQ(dim.second.c_str(), "dimVal1"); + } + else if (dim.first == "dim2") { + ASSERT_STRCASEEQ(dim.second.c_str(), "dimVal2"); + } + else if (dim.first == "dim3") { + ASSERT_STRCASEEQ(dim.second.c_str(), "dimVal3"); + } + else if (dim.first == "dim4") { + ASSERT_STRCASEEQ(dim.second.c_str(), "dimVal4"); + } + else { + FAIL() << "Dimension has value that was not part of command"; + } + } +} + +TEST_F(AplMetricsExtension2Test, RegistrationCommands) { + auto registration = registerExtension(createActivityDescriptor()); + Value* schema = RegistrationSuccess::SCHEMA().Get(registration); + ASSERT_TRUE(schema); + + Value* commands = ExtensionSchema::COMMANDS().Get(*schema); + ASSERT_TRUE(commands); + + auto expectedCommandSet = std::set(); + expectedCommandSet.insert("IncrementCounter"); + expectedCommandSet.insert("StartTimer"); + expectedCommandSet.insert("StopTimer"); + expectedCommandSet.insert("CreateCounter"); + expectedCommandSet.insert("RecordValue"); + ASSERT_TRUE(commands->IsArray() && commands->Size() == expectedCommandSet.size()); + + for (const Value& com : commands->GetArray()) { + ASSERT_TRUE(com.IsObject()); + auto name = GetWithDefault(Command::NAME(), com, "MissingName"); + ASSERT_TRUE(expectedCommandSet.count(name) == 1) << "Unknown Command:" << name; + expectedCommandSet.erase(name); + } + ASSERT_TRUE(expectedCommandSet.empty()); +} + +TEST_F(AplMetricsExtension2Test, TestCommandsWithInvalidActivity) { + auto session = SessionDescriptor::create("TestSessionId"); + auto reg = registerExtension(createActivityDescriptor(session)); + + auto command = + Command("1.0").uri(URI_V2).name("IncrementCounter").property(METRIC_ID, "TestId"); + auto invalidActivity = createActivityDescriptor(session, "aplext:metrics:INVALID"); + auto invoke = mExtension->invokeCommand(invalidActivity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); + + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(invalidActivity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestCommandsWithInvalidSession) { + auto reg = registerExtension(createActivityDescriptor()); + + auto command = + Command("1.0").uri(URI_V2).name("IncrementCounter").property(METRIC_ID, "TestId"); + auto session = SessionDescriptor::create("Session1"); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(session), command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestInvalidCommands) { + auto activity = createActivityDescriptor(); + registerExtension(activity); + + mExecutor->resetFlag(); + // Invalid command name + auto command = Command("1.0") + .uri(URI_V2) + .name("InvalidCommand") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + + //======================== + // MetricId property missing + command = Command("1.0").uri(URI_V2).name("IncrementCounter").property(AMOUNT, 1); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); // fail soft + AssertLastPublishMetric(false, false); + + command = Command("1.0").uri(URI_V2).name("StartTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + + command = Command("1.0").uri(URI_V2).name("StopTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + + command = Command("1.0").uri(URI_V2).name("CreateCounter"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + // + //======================== + + //======================== + // MetricId is empty + command = Command("1.0") + .uri(URI_V2) + .name("IncrementCounter") + .property(METRIC_ID, "") + .property(AMOUNT, 1); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + + command = Command("1.0").uri(URI_V2).property(METRIC_ID, "").name("StartTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + + command = Command("1.0").uri(URI_V2).property(METRIC_ID, "").name("StopTimer"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + + command = Command("1.0").uri(URI_V2).property(METRIC_ID, "").name("CreateCounter"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(false, false); + // + //======================== +} + +TEST_F(AplMetricsExtension2Test, TestTimerMetricCommand) { + auto activity = createActivityDescriptor(); + registerExtension(activity); + + // Test stop without start + mExecutor->resetFlag(); + auto command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId"); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); + + mExecutor->resetFlag(); + command = Command("1.0").uri(URI_V2).name("StartTimer").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); + + mExecutor->resetFlag(); + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, true); + + auto metric = mDestFactory->lastDestinationMock->lastPublishedMetric; + ASSERT_TRUE(0 == metric.name.compare("TestId")); // Metric name should be MeticId + ASSERT_TRUE(0 == metric.dimensions.size()); + + // Stop again + mExecutor->resetFlag(); + resetLastPublishedFlags(); + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); + + mExecutor->resetFlag(); + command = Command("1.0").uri(URI_V2).name("StartTimer").property(METRIC_ID, "TestId"); + invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestMultipleTimerMetricWithinSession) { + auto session1 = SessionDescriptor::create("Session1"); + auto activity1 = createActivityDescriptor(session1); + registerExtension(activity1); + auto destination1 = mDestFactory->lastDestinationMock; + + mExecutor->resetFlag(); + auto command = Command("1.0").uri(URI_V2).name("StartTimer").property(METRIC_ID, "TestId1"); + auto invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + + // Register another activity with same session + auto activity2 = createActivityDescriptor(session1); + registerExtension(activity2); + auto destination2 = mDestFactory->lastDestinationMock; + + // Start another timer in activity1 + mExecutor->resetFlag(); + command = Command("1.0").uri(URI_V2).name("StartTimer").property(METRIC_ID, "TestId2"); + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + + // Stop timer with id: TestId1, but with activity2. It should fail as `TestId1` was started + // by activity1 + mExecutor->resetFlag(); + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId1"); + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + AssertPublishMetricForDestination(true, false, destination2); + + // Stop first timer with id: TestId1 + mExecutor->resetFlag(); + resetPublishedFlagsForDestination(destination1); + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId1"); + invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + AssertPublishMetricForDestination(true, true, destination1); + + // Register another activity with different session and try to stop timer + auto activity3 = createActivityDescriptor(session1); + registerExtension(activity3); + auto destination3 = mDestFactory->lastDestinationMock; + + mExecutor->resetFlag(); + resetPublishedFlagsForDestination(destination3); + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId2"); + invoke = mExtension->invokeCommand(activity3, command); + ASSERT_TRUE(invoke); + AssertPublishMetricForDestination(true, false, destination3); + + // Stop second timer with id: TestId2 + mExecutor->resetFlag(); + resetPublishedFlagsForDestination(destination2); + command = Command("1.0").uri(URI_V2).name("StopTimer").property(METRIC_ID, "TestId2"); + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + AssertPublishMetricForDestination(true, true, destination2); +} + +TEST_F(AplMetricsExtension2Test, TestMultipleCounterMetricWithinSession) { + auto session1 = SessionDescriptor::create("Session1"); + auto activity1 = createActivityDescriptor(session1); + registerExtension(activity1); + auto destination1 = mDestFactory->lastDestinationMock; + + // Increment counter in activity1 + auto command = + Command("1.0").uri(URI_V2).name("IncrementCounter").property(METRIC_ID, "TestId"); + auto invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + + // Register activity2 with same session + auto activity2 = createActivityDescriptor(session1); + registerExtension(activity2); + auto destination2 = mDestFactory->lastDestinationMock; + + // Increment counter in activity2 + // Should create a new counter with same ID for this activity + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + + // Increment counter again in activity1 by amount 2. + command = Command("1.0") + .uri(URI_V2) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 2); + invoke = mExtension->invokeCommand(activity1, command); + ASSERT_TRUE(invoke); + + // Increment counter activity2 by amount 100. + command = Command("1.0") + .uri(URI_V2) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 100); + invoke = mExtension->invokeCommand(activity2, command); + ASSERT_TRUE(invoke); + + // Register another activity with different session and increment counter + auto activity3 = createActivityDescriptor(session1); + registerExtension(activity3); + auto destination3 = mDestFactory->lastDestinationMock; + + command = Command("1.0") + .uri(URI_V2) + .name("IncrementCounter") + .property(METRIC_ID, "TestId") + .property(AMOUNT, 45); + invoke = mExtension->invokeCommand(activity3, command); + ASSERT_TRUE(invoke); + + // End activity + mExtension->onActivityUnregistered(activity1); + mExtension->onActivityUnregistered(activity2); + mExtension->onActivityUnregistered(activity3); + + ASSERT_EQ(1, destination1->lastPublishedMetricList.size()); + ASSERT_EQ(destination1->lastPublishedMetricList[0].name.compare("TestId"), 0); + ASSERT_EQ(destination1->lastPublishedMetricList[0].value, 3); + ASSERT_NE(destination1, destination2); + + ASSERT_EQ(1, destination2->lastPublishedMetricList.size()); + ASSERT_EQ(destination2->lastPublishedMetricList[0].name.compare("TestId"), 0); + ASSERT_EQ(destination2->lastPublishedMetricList[0].value, 101); + ASSERT_NE(destination2, destination3); + + ASSERT_EQ(1, destination3->lastPublishedMetricList.size()); + ASSERT_EQ(destination3->lastPublishedMetricList[0].name.compare("TestId"), 0); + ASSERT_EQ(destination3->lastPublishedMetricList[0].value, 45); +} + +TEST_F(AplMetricsExtension2Test, TestCommandUnRegisteredActivity) { + registerExtension(createActivityDescriptor()); + + auto command = Command("1.0").uri(URI_V2).name("IncrementCounter").property(AMOUNT, 1); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(), command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestEmptyCommandName) { + registerExtension(createActivityDescriptor()); + + auto command = Command("1.0").uri(URI_V2).name(""); + auto invoke = mExtension->invokeCommand(createActivityDescriptor(), command); + ASSERT_FALSE(invoke); +} + +TEST_F(AplMetricsExtension2Test, TestCreateCounterEmptyMetricName) { + auto activity = createActivityDescriptor(); + auto reg = registerExtension(activity); + + auto command = Command("1.0") + .uri(URI_V2) + .name("CreateCounter") + .property(METRIC_ID, "TestId") + .property("metricName", "") + .property("initialValue", 101); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + mExtension->onActivityUnregistered(activity); + auto metricList = mDestFactory->lastDestinationMock->lastPublishedMetricList; + ASSERT_EQ(metricList.size(), 1); + ASSERT_EQ(101, metricList[0].value); +} + +TEST_F(AplMetricsExtension2Test, TestRecordMetricWithNoRegisteredActivity) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + + auto command = Command("1.0") + .uri(URI_V2) + .name("RecordValue") + .property("metricName", "valueName") + .property("value", 563); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestCreateCounterMetricWithNoRegisteredActivity) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + + auto command = Command("1.0") + .uri(URI_V2) + .name("CreateCounter") + .property(METRIC_ID, "TestId") + .property("metricName", "valueName") + .property("initialValue", 101); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestStartTimerMetricWithNoRegisteredActivity) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + + auto command = Command("1.0") + .uri(URI_V2) + .name("StartTimer") + .property(METRIC_ID, "TestId") + .property("metricName", "testName"); + auto invoke = mExtension->invokeCommand(activity, command); + ASSERT_TRUE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestOnActivityUnregisteredWithNoRegisteredActivity) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + mExtension->onActivityUnregistered(activity); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestCreateCounterDestroyedExecutor) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + registerExtension(activity); + + auto command = Command("1.0") + .uri(URI_V2) + .name("CreateCounter") + .property(METRIC_ID, "TestId") + .property("metricName", "valueName") + .property("initialValue", 101); + + std::shared_ptr extension; + { + auto executor = std::make_shared(); + extension = std::make_shared(mDestFactory, executor); + } + auto invoke = extension->invokeCommand(activity, command); + ASSERT_FALSE(invoke); + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestAlreadyRegisteredActivity) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + registerExtension(activity); + registerExtension(activity); + + AssertLastPublishMetric(true, false); +} + +TEST_F(AplMetricsExtension2Test, TestNoDestinationType) { + shouldCreateDestinationBeCalled = false; + auto activity = createActivityDescriptor(); + registerExtension(activity, Dimensions(), true, true, false); + + AssertLastPublishMetric(false, false); +} \ No newline at end of file