diff --git a/.gitignore b/.gitignore index 3aeab808..cca6dd6a 100644 --- a/.gitignore +++ b/.gitignore @@ -9,5 +9,9 @@ build .idea/ .vscode .DS_Store -apl/.cxx/ +**/.cxx/ jacoco.exec +# Project-specific cache directory generated by Gradle +/.gradle/ +# Generated by run-gradlew +gradle/wrapper/gradle-wrapper.properties diff --git a/README.md b/README.md index 388ccd0b..2ebac306 100644 --- a/README.md +++ b/README.md @@ -1,61 +1,58 @@ -# Alexa Presentation Language (APL) ViewHost Android version 2023.3 +# Alexa Presentation Language (APL) ViewHost Android version 2024.1 -APLViewHostAndroid is a view host implementation for the Android Platform. It consists of -a thin JNI layer that interacts with APL Core Engine for component inflation and command -handling, and a native Android layout that maps APL Components to Android Views and ViewGroups. +The APL Android target contains 3 modules -The APL Android Viewhost consists of two elements: - -- APL JNI adapter: 'libaplcore-jni.so' allows in process communication between the +- APL jni adapter: 'libaplcore-jni.so' allows in process communication between the core C++ and Java Android layers. -- APL Android Library: .aar library exposing functionality of the APL spec, for use by Android - applications. This library embeds the Core and JNI libraries. The aar build targets SDK 28 and - supports a minimum SDK 22, it has 2 flavors: - - - apl-release.aar - The release library - - apl-debug.aar - The debug library - -The Android library is built with Gradle. The Gradle CMake integration plugin -will build the APL Core Library and Core JNI dependencies. +- APL Android Library: .aar library exposing functionality of the APL spec, for use by Android + applications. This library embeds the core and jni libraries. The aar build targets SDK 34 and + supports a minimum SDK 22, it has 5 flavors: + + - apl-core-release.aar - The release library + - apl-demo-release.aar - The release library with sample APL layout assets included. + - aplMinSized-release.aar - The release library with size optimizations to minimize download size. + - apl-core-debug.aar - The debug library + - apl-demo-debug.aar - The debug library with sample APL layout assets included. + +The Android library is built with gradle. The Gradle cmake integration plugin +will build the APL core library and core jni dependencies. + +The library ships with a discovery module which adds the extension discovery functionality. The +discovery library has following flavors and should be only used with its compatible flavor of +apl library, when needed: + + - discovery-standard-debug.aar should be used with apl-core-debug.aar + - discovery-standard-release.aar should be used with apl-core-release.aar + - discovery-standardMinsized-release.aar should be used with apl-core-release.aar + - discovery-serviceV2-debug.aar should be used with apl-core-debug.aar + - discovery-serviceV2ls --release.aar should be used with apl-core-release.aar ### Prerequisites -Make sure you have installed: - -- [Android SDK](https://developer.android.com/studio/intro/update) version 28 or higher -- [Android NDK](https://developer.android.com/ndk/guides/#download-ndk) version 22 or higher -- APL Core build dependencies (e.g. one of supported C++ compilers, CMake) -- Ninja (Needed for APLCoreEngine when building from APLViewhostAndroid) - -Setup a directory with the APL Android and APL Core - https://github.com/alexa/apl-core-library - +- [Install NDK](https://developer.android.com/ndk/guides/#download-ndk) version 23.0.7599858 +- [Install Android SDK](https://developer.android.com/studio/intro/update) version 34 or higher +- Install Ninja (Needed for APLCoreEngine when building from APLViewhostAndroid) +- Java 17 (https://adoptium.net/ is recommended) +- Setup a directory with the APL Android and APL Core - https://github.com/alexa/apl-core-library ```bash $ ls -apl-core-library -apl-viewhost-android +APLCoreEngine +APLViewhostAndroid ``` -The APL Core code is required for building the APL Android project. The Gradle build -assumes it is in a sibling folder to the `apl-viewhost-android` project. If the APL Core -code is located elsewhere, Gradle commands must be augmented with `-PaplCoreDir=` -or you can set the value in the `gradle.properties` file. +> The APL Core code is required for building the APL Android project. The Gradle build +> assumes it is in a sibling folder to the APLAndroidViewhost project. If the APL Core +> code is located elsewhere gradle commands must be augmented with `-PaplCoreDir=` +> or set the value in the `gradle.properties` file. ## Building the Android APL Library -Set the Android SDK root environment variable: - -```bash -$ export ANDROID_SDK_ROOT=/Users/YOUR-LOGIN-HERE/Library/Android/sdk/ -``` -Build the Android APL library: +To build the Android APL library: ```bash $ ./gradlew build ``` - -This step will also build the APL Core Library as a dependency. - -To see a full list of Gradle tasks: +To see a full list of gradle tasks: ```bash $ ./gradlew tasks ``` @@ -67,4 +64,20 @@ To see a full list of Gradle tasks: CMake Error at ...APLCoreEngine/thirdparty/thirdparty.cmake:120 (message): CMake step for googletest failed: 1 ``` -The `ninja` build tool needs to be available on the `PATH`. \ No newline at end of file +The `ninja` build tool needs to be available on the `PATH`. + +### Gradle Error: Lombok + +When building _outside_ of Android Studio using `gradlew`, you may get a fatal error from Lombok: +``` +> java.lang.IllegalAccessError: class lombok.javac.apt.LombokProcessor (in unnamed module @0x1b65ad4c) cannot access class com.sun.tools.javac.processing.JavacProcessingEnvironment (in module jdk.compiler) because module jdk.compiler does not export com.sun.tools.javac.processing to unnamed module @0x1b65ad4c +``` + +This may occur because the Lombok library version must be compatible with the +Java compiler. Android Studio uses an internal verson of Java, so the Lombok +library supplied with APLViewhostAndroid is compatible with that Java. +Building from the command line gives you whichever version of Java is in your +path, which you can see with `echo $JAVA_HOME`. If your version of Java is +too new (or too old), the view host will not compile. + +We recommend the Temurin 17 JDK. \ No newline at end of file diff --git a/alexaextjni/CMakeLists.txt b/alexaextjni/CMakeLists.txt new file mode 100644 index 00000000..83bb11c4 --- /dev/null +++ b/alexaextjni/CMakeLists.txt @@ -0,0 +1,84 @@ +# 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) +set(ENABLE_PIC ON) +project (alexaextjni VERSION 1.0.0 LANGUAGES C CXX) + + +# Creates and names a library, sets it as either STATIC +# or SHARED, and provides the relative paths to its source code. +# You can define multiple libraries, and CMake builds them for you. +# Gradle automatically packages shared libraries with your APK. + +add_library( # Sets the name of the library. + alexaextjni + # Sets the library as a static library. + STATIC + src/main/cpp/jniextensionexecutor.cpp + src/main/cpp/jniextensionproxy.cpp + src/main/cpp/jniextensionregistrar.cpp + src/main/cpp/jniextensionresource.cpp) + +# Searches for a specified prebuilt library and stores the path as a +# variable. Because CMake includes system libraries in the search path by +# default, you only need to specify the name of the public NDK library +# you want to add. CMake verifies that the library exists before +# completing its build. + +if (NOT ANDROID) + # Ensure jni.h is found + find_package(JNI REQUIRED) + include_directories(${JAVA_INCLUDE_PATH}) + include_directories(${JAVA_INCLUDE_PATH2}) + + add_library(alexaext STATIC IMPORTED) + set_target_properties(alexaext + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/.cxx/cmake/debug/host/_deps/aplcore-build/extensions/alexaext/libalexaext.a" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/src/main/cpp/alexaext/include" + ) + + add_library(rapidjson INTERFACE) + target_include_directories(rapidjson INTERFACE + # When we're building against RapidJSON, just use the include directory we discovered above + $ + ) + + target_link_libraries(alexaextjni rapidjson alexaext) +else() + find_library( # Sets the name of the path variable. + log-lib + + # Specifies the name of the NDK library that + # you want CMake to locate. + log) + + # Specifies libraries CMake should link to your target library. You + # can link multiple libraries, such as libraries you define in this + # build script, prebuilt third-party libraries, or system libraries. + + target_link_libraries( # Specifies the target library. + alexaextjni + + # Links the target library to the log library + # included in the NDK. + ${log-lib}) + + find_package(coreengine REQUIRED CONFIG) + target_link_libraries(alexaextjni coreengine::alexaext coreengine::rapidjson) +endif() + +# Specifies a path to native header files. +include_directories(src/main/cpp/include) + +# Common lib includes +include_directories(../common/src/main/cpp/include) \ No newline at end of file diff --git a/alexaextjni/build.gradle b/alexaextjni/build.gradle new file mode 100644 index 00000000..4322273a --- /dev/null +++ b/alexaextjni/build.gradle @@ -0,0 +1,95 @@ +/* + * 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' + +ext { + cmakeProjectPath = projectDir.absolutePath + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cmakeProjectPath = cmakeProjectPath.replace('\\', '/') + } + aplAndroidCmakeArgs = "-DCMAKE_VERBOSE_MAKEFILE=ON" +} + +android { + namespace 'com.amazon.alexa.android.extension.alexaextjni' + compileSdk 34 + buildToolsVersion = "33.0.0" + ndkVersion "23.0.7599858" + + defaultConfig { + minSdkVersion 22 + versionCode 1 + versionName "1.0" + targetSdkVersion 34 + + testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" + consumerProguardFiles 'consumer-rules.pro' + + 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 "alexaextjni" + // Enable APL Core JNI build, and be verbose. + arguments aplAndroidCmakeArgs + } + } + } + buildTypes { + release { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + } + } + + externalNativeBuild { + cmake { + version "3.18.1" + path "CMakeLists.txt" + } + } + + buildFeatures { + prefab true + prefabPublishing true + } + prefab { + alexaextjni { + headers "src/main/cpp/include" + } + } +} + +dependencies { + implementation fileTree(dir: 'libs', include: ['*.jar']) + implementation project(":common") + implementation project(':coreengine') +} + +task buildHostJNI(type: com.amazon.apl.android.CMakeTask) { + cmakeArgs aplAndroidCmakeArgs + makeTargets "alexaextjni" + dependsOn ':coreengine:buildHostJNI' +} + +project.afterEvaluate { + // Dump configuration settings + println "APL CMake Args: " + aplAndroidCmakeArgs + println "Android SDK Directory: " + android.sdkDirectory.path + println "Android NDK Directory: " + android.ndkDirectory.path + + // We need to make sure the host jni library is built before any debug or release unit testing. + tasks.preDebugUnitTestBuild.dependsOn(buildHostJNI) + tasks.preReleaseUnitTestBuild.dependsOn(buildHostJNI) + tasks.buildHostJNI.dependsOn(':coreengine:buildHostJNI') + tasks.buildHostJNI.dependsOn(':common:buildHostJNI') +} + +tasks.build.dependsOn(buildHostJNI) + diff --git a/discovery/src/main/cpp/include/jniextensionexecutor.h b/alexaextjni/src/main/cpp/include/jniextensionexecutor.h similarity index 100% rename from discovery/src/main/cpp/include/jniextensionexecutor.h rename to alexaextjni/src/main/cpp/include/jniextensionexecutor.h diff --git a/discovery/src/main/cpp/include/jniextensionproxy.h b/alexaextjni/src/main/cpp/include/jniextensionproxy.h similarity index 100% rename from discovery/src/main/cpp/include/jniextensionproxy.h rename to alexaextjni/src/main/cpp/include/jniextensionproxy.h diff --git a/discovery/src/main/cpp/include/jniextensionregistrar.h b/alexaextjni/src/main/cpp/include/jniextensionregistrar.h similarity index 100% rename from discovery/src/main/cpp/include/jniextensionregistrar.h rename to alexaextjni/src/main/cpp/include/jniextensionregistrar.h diff --git a/discovery/src/main/cpp/include/jniextensionresource.h b/alexaextjni/src/main/cpp/include/jniextensionresource.h similarity index 100% rename from discovery/src/main/cpp/include/jniextensionresource.h rename to alexaextjni/src/main/cpp/include/jniextensionresource.h diff --git a/discovery/src/main/cpp/jniextensionexecutor.cpp b/alexaextjni/src/main/cpp/jniextensionexecutor.cpp similarity index 100% rename from discovery/src/main/cpp/jniextensionexecutor.cpp rename to alexaextjni/src/main/cpp/jniextensionexecutor.cpp diff --git a/discovery/src/main/cpp/jniextensionproxy.cpp b/alexaextjni/src/main/cpp/jniextensionproxy.cpp similarity index 100% rename from discovery/src/main/cpp/jniextensionproxy.cpp rename to alexaextjni/src/main/cpp/jniextensionproxy.cpp diff --git a/discovery/src/main/cpp/jniextensionregistrar.cpp b/alexaextjni/src/main/cpp/jniextensionregistrar.cpp similarity index 100% rename from discovery/src/main/cpp/jniextensionregistrar.cpp rename to alexaextjni/src/main/cpp/jniextensionregistrar.cpp diff --git a/discovery/src/main/cpp/jniextensionresource.cpp b/alexaextjni/src/main/cpp/jniextensionresource.cpp similarity index 100% rename from discovery/src/main/cpp/jniextensionresource.cpp rename to alexaextjni/src/main/cpp/jniextensionresource.cpp diff --git a/apl/.classpath b/apl/.classpath deleted file mode 100644 index eb19361b..00000000 --- a/apl/.classpath +++ /dev/null @@ -1,6 +0,0 @@ - - - - - - diff --git a/apl/.project b/apl/.project deleted file mode 100644 index 78e7ec08..00000000 --- a/apl/.project +++ /dev/null @@ -1,23 +0,0 @@ - - - apl - Project app created by Buildship. - - - - - org.eclipse.jdt.core.javabuilder - - - - - org.eclipse.buildship.core.gradleprojectbuilder - - - - - - org.eclipse.jdt.core.javanature - org.eclipse.buildship.core.gradleprojectnature - - diff --git a/apl/.settings/org.eclipse.buildship.core.prefs b/apl/.settings/org.eclipse.buildship.core.prefs deleted file mode 100644 index b1886adb..00000000 --- a/apl/.settings/org.eclipse.buildship.core.prefs +++ /dev/null @@ -1,2 +0,0 @@ -connection.project.dir=.. -eclipse.preferences.version=1 diff --git a/apl/CMakeLists.txt b/apl/CMakeLists.txt index 0d27714d..01b18f3c 100644 --- a/apl/CMakeLists.txt +++ b/apl/CMakeLists.txt @@ -9,24 +9,15 @@ 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-jni VERSION 1.0.0 LANGUAGES C CXX) - -# set APL Core location -if (NOT APL_CORE_DIR) - message(FATAL_ERROR "Please specify the location of APL Core") -endif () -set(APL_PROJECT_DIR ${APL_CORE_DIR}) - -# Tell core to compile alexa extensions. -set(ENABLE_ALEXAEXTENSIONS ON) -set(BUILD_ALEXAEXTENSIONS ON) set(ENABLE_PIC ON) +option(REMOVE_PRIVATE_EXPORTS "Hide Private Symbols from build" OFF) +option(INCLUDE_ALEXAEXT "Link Alexa Extension JNI" OFF) -FetchContent_Declare( - aplcore - SOURCE_DIR ${APL_CORE_DIR} -) -FetchContent_MakeAvailable(aplcore) +if (DEFINED SCENE_GRAPH) + set(ENABLE_SCENEGRAPH ${SCENE_GRAPH}) +endif() + +project (apl-jni VERSION 1.0.0 LANGUAGES C CXX) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. @@ -75,11 +66,56 @@ add_library( # Specifies a path to native header files. include_directories(src/main/cpp/include) +target_compile_definitions(apl-jni PRIVATE VERSION_NAME="${VERSION_NAME}") + if (NOT ANDROID) # Ensure jni.h is found find_package(JNI REQUIRED) include_directories(${JAVA_INCLUDE_PATH}) include_directories(${JAVA_INCLUDE_PATH2}) + + add_library(aplcore STATIC IMPORTED) + + if (ENABLE_SCENEGRAPH) + list(APPEND APL_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/src/main/cpp/aplsgconfig/include") + else() + list(APPEND APL_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/src/main/cpp/aplconfig/include") + endif() + list(APPEND APL_INCLUDE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/src/main/cpp/apl/include") + + set_target_properties(aplcore + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/.cxx/cmake/debug/host/_deps/aplcore-build/aplcore/libapl.a" + INTERFACE_INCLUDE_DIRECTORIES "${APL_INCLUDE_DIR}" + ) + + add_library(alexaext STATIC IMPORTED) + set_target_properties(alexaext + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/.cxx/cmake/debug/host/_deps/aplcore-build/extensions/alexaext/libalexaext.a" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/src/main/cpp/alexaext/include" + ) + + add_library(rapidjson INTERFACE) + target_include_directories(rapidjson INTERFACE + # When we're building against RapidJSON, just use the include directory we discovered above + $ + ) + if(INCLUDE_ALEXAEXT) + add_library(alexaextjni STATIC IMPORTED) + set_target_properties(alexaextjni + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../alexaextjni/.cxx/cmake/debug/host/libalexaextjni.a" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../alexaextjni/src/main/cpp/include" + ) + target_link_libraries(apl-jni alexaextjni aplcore rapidjson alexaext) + else() + target_link_libraries(apl-jni aplcore rapidjson alexaext) + endif() + else() # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by @@ -101,108 +137,29 @@ else() -ljnigraphics # Links the target library to the log library included in the NDK. ${log-lib}) - - set(ENUMGEN_BIN "${CMAKE_BINARY_DIR}/tools/enumgen") - - add_custom_target(generate-android-enums ALL - COMMAND cd ${APL_CORE_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_CORE_DIR}/aplcore/include/action/*.h - ${APL_CORE_DIR}/aplcore/include/animation/*.h - ${APL_CORE_DIR}/aplcore/include/apl/audio/*.h - ${APL_CORE_DIR}/aplcore/include/apl/command/*.h - ${APL_CORE_DIR}/aplcore/include/apl/component/*.h - ${APL_CORE_DIR}/aplcore/include/apl/content/*.h - ${APL_CORE_DIR}/aplcore/include/apl/datagrammar/*.h - ${APL_CORE_DIR}/aplcore/include/apl/document/*.h - ${APL_CORE_DIR}/aplcore/include/apl/engine/*.h - ${APL_CORE_DIR}/aplcore/include/apl/graphic/*.h - ${APL_CORE_DIR}/aplcore/include/apl/media/*.h - ${APL_CORE_DIR}/aplcore/include/apl/primitives/*.h - ${APL_CORE_DIR}/aplcore/include/apl/time/*.h - ${APL_CORE_DIR}/aplcore/include/apl/utils/*.h - ${APL_CORE_DIR}/aplcore/include/apl/touch/*.h - ${APL_CORE_DIR}/aplcore/include/apl/focus/*.h - DEPENDS enumgen - ) - - add_dependencies(apl-jni generate-android-enums) + # Remove private exports if demanded. + if(REMOVE_PRIVATE_EXPORTS) + # Use a version script (aka export map) to both reduce symbol visibility as + # well as improve symbol relocation performance. Without an export map, + # all C++ symbols are global, even those not accessible using JNI. + # For more info: https://www.akkadia.org/drepper/dsohowto.pdf + set_target_properties( + apl-jni PROPERTIES + LINK_FLAGS "-Wl,--version-script=${CMAKE_CURRENT_SOURCE_DIR}/apl.map" + ) + endif() + + if(INCLUDE_ALEXAEXT) + target_compile_definitions(apl-jni PRIVATE INCLUDE_ALEXAEXT="${VERSION_NAME}") + find_package(alexaextjni REQUIRED CONFIG) + target_link_libraries(apl-jni alexaextjni::alexaextjni) + endif() + + find_package(coreengine REQUIRED CONFIG) + target_link_libraries(apl-jni coreengine::apl coreengine::alexaext coreengine::rapidjson) endif() -target_link_libraries(apl-jni apl alexaext) # Common lib includes include_directories(../common/src/main/cpp/include) diff --git a/apl/apl.map b/apl/apl.map new file mode 100644 index 00000000..7defc8e6 --- /dev/null +++ b/apl/apl.map @@ -0,0 +1,11 @@ + { + global: + # only mark the following explicitly-specified symbols as global + JNI_OnLoad; + JNI_OnUnload; + Java_*; + local: + # hide all others + *; + }; + diff --git a/apl/build.gradle b/apl/build.gradle index 4a3450f8..5574b62f 100644 --- a/apl/build.gradle +++ b/apl/build.gradle @@ -10,7 +10,7 @@ apply plugin: 'jacoco' apply plugin: 'maven-publish' jacoco { - toolVersion = '0.8.2' + toolVersion = '0.8.8' } tasks.withType(Test) { @@ -46,10 +46,6 @@ task jacocoTestReport(type: JacocoReport, dependsOn: ['test']) { "jacoco/*.exec", "outputs/code_coverage/debugAndroidTest/connected/*coverage.ec" ])) - reports { - xml.enabled = true - html.enabled = true - } } ext { @@ -58,19 +54,17 @@ ext { cmakeProjectPath = cmakeProjectPath.replace('\\', '/') } aplAndroidCmakeArgs = "-DCMAKE_VERBOSE_MAKEFILE=ON" - aplCoreDirCmakeArg = "-DAPL_CORE_DIR=${cmakeProjectPath}/../../apl-core-library" - if (project.hasProperty('aplCoreDir')) { - aplCoreDirCmakeArg = "-DAPL_CORE_DIR=" + aplCoreDir - } } android { - compileSdkVersion 31 + namespace "com.amazon.apl.android" + compileSdk 34 ndkVersion "23.0.7599858" - buildToolsVersion "30.0.2" + buildToolsVersion "33.0.0" + defaultConfig { minSdkVersion 22 - targetSdkVersion 31 + targetSdkVersion 34 versionCode 1 versionName "1.0" testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" @@ -81,12 +75,25 @@ android { // 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", "apl-jni" + targets "apl-jni" // Enable APL Core JNI build, and be verbose. - arguments aplCoreDirCmakeArg, aplAndroidCmakeArgs + arguments aplAndroidCmakeArgs } } } + sourceSets { + // Encapsulates configurations for the main source set. + main { + // Changes the directory for Java sources. The default directory is + // 'src/main/java'. + java.srcDirs = ['src/main/java', '../coreengine/src/main/java'] + } + } + publishing { + multipleVariants { + allVariants() + } + } compileOptions { sourceCompatibility JavaVersion.VERSION_1_8 targetCompatibility JavaVersion.VERSION_1_8 @@ -98,14 +105,35 @@ android { buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-core\"") buildConfigField 'boolean', 'DEBUG_LOGGING', 'false' + externalNativeBuild { + cmake { + arguments aplAndroidCmakeArgs, "-DREMOVE_PRIVATE_EXPORTS=OFF", "-DVERSION_NAME=${defaultConfig.versionName}" + } + } + } debug { testCoverageEnabled true debuggable true - aplAndroidCmakeArgs += " -DDEBUG_MEMORY_USE=ON" buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-core\"") buildConfigField 'boolean', 'DEBUG_LOGGING', 'true' + externalNativeBuild { + cmake { + arguments aplAndroidCmakeArgs, "-DDEBUG_MEMORY_USE=ON", "-DVERSION_NAME=${project.version}" + } + } + } + releaseMinSized { + buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") + buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-core\"") + buildConfigField 'boolean', 'DEBUG_LOGGING', 'false' + matchingFallbacks = ['release'] + externalNativeBuild { + cmake { + arguments aplAndroidCmakeArgs, "-DREMOVE_PRIVATE_EXPORTS=ON", "-DINCLUDE_ALEXAEXT=ON", "-DVERSION_NAME=${project.version}" + } + } } } // Temporary fix until alpha10 - "More than one file was found with OS independent path 'META-INF/proguard/androidx-annotations.pro" @@ -135,6 +163,10 @@ android { fatal 'StopShip' disable 'LongLogTag' } + buildFeatures { + buildConfig true + prefab true + } testOptions { animationsDisabled true @@ -145,24 +177,27 @@ android { } dependencies { - compileOnly 'org.projectlombok:lombok:1.18.28' + compileOnly 'org.projectlombok:lombok:1.18.30' implementation fileTree(include: ['*.jar'], dir: 'libs') implementation 'androidx.annotation:annotation:1.4.0' implementation 'androidx.core:core:1.0.0' implementation 'androidx.appcompat:appcompat:1.2.0' implementation 'com.github.bumptech.glide:glide:4.11.0' + implementation project(':coreengine') + implementation project(':alexaextjni') implementation project(':common') implementation(project(':discovery')) { transitive = false } testImplementation 'junit:junit:4.13.2' testImplementation 'org.robolectric:robolectric:4.8.1' testImplementation 'org.robolectric:shadows-httpclient:4.2' - testImplementation 'androidx.test:core:1.1.0' - testImplementation 'androidx.test.ext:junit:1.1.0' + testImplementation 'androidx.test:core:1.4.0' + testImplementation 'androidx.test.ext:junit:1.1.3' testImplementation 'org.mockito:mockito-core:4.7.0' testImplementation 'androidx.test:rules:1.4.0' + testImplementation 'org.json:json:20210307' androidTestImplementation 'org.mockito:mockito-core:3.12.4' - androidTestImplementation 'androidx.test.ext:junit:1.1.0' - androidTestImplementation 'androidx.test:core:1.1.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:core:1.4.0' androidTestImplementation 'androidx.annotation:annotation:1.4.0' androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' androidTestImplementation 'androidx.test:runner:1.4.0' @@ -177,9 +212,14 @@ dependencies { api "com.google.auto.value:auto-value-annotations:1.7" api 'com.google.guava:guava:27.0.1-jre' annotationProcessor "com.google.auto.value:auto-value:1.7" - annotationProcessor 'org.projectlombok:lombok:1.18.28' + annotationProcessor 'org.projectlombok:lombok:1.18.30' } +task buildHostJNI(type: com.amazon.apl.android.CMakeTask) { + cmakeArgs aplAndroidCmakeArgs + makeTargets 'apl-jni' + dependsOn ':coreengine:buildHostJNI',':alexaextjni:buildHostJNI' +} tasks.whenTaskAdded { theTask -> if (theTask.name.startsWith("test")) { @@ -190,28 +230,27 @@ tasks.whenTaskAdded { theTask -> project.afterEvaluate { // Dump configuration settings println "APL CMake Args: " + aplAndroidCmakeArgs - println "APL Core Directory: " + aplCoreDirCmakeArg println "Android SDK Directory: " + android.sdkDirectory.path println "Android NDK Directory: " + android.ndkDirectory.path - // enforce native tools build runs first for enum dependencies - compileDebugJavaWithJavac.dependsOn externalNativeBuildDebug - compileReleaseJavaWithJavac.dependsOn externalNativeBuildRelease - - javaPreCompileDebug.dependsOn externalNativeBuildDebug + // We need to make sure the host jni library is built before any debug or release unit testing. + tasks.buildHostJNI.dependsOn(':coreengine:buildHostJNI') + tasks.buildHostJNI.dependsOn(':alexaextjni:buildHostJNI') + tasks.buildHostJNI.dependsOn(':discovery:buildHostJNI') + tasks.buildHostJNI.dependsOn(':common:buildHostJNI') tasks.test.finalizedBy(jacocoTestReport) } -afterEvaluate { - publishing { - publications { - release(MavenPublication) { - from components.release - pom { - description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + - ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION - } +publishing { + publications { + release(MavenPublication) { + pom { + description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + + ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION + } + afterEvaluate { + from components.default } } } @@ -239,6 +278,7 @@ task release(dependsOn: ['build', 'publish']) { copy { from 'build/outputs/aar' into '../build/apl' + rename 'apl-releaseMinSized.aar', 'aplMinSized-release.aar' } copy { @@ -251,5 +291,10 @@ task release(dependsOn: ['build', 'publish']) { from 'build/outputs/apk/androidTest/debug' into '../build/apl/androidTest' } + + copy { + from 'build/intermediates/merged_native_libs/releaseMinSized/out/lib/' + into '../build/apl/symbols' + } } -} +} \ No newline at end of file diff --git a/apl/proguard-rules.pro b/apl/proguard-rules.pro index f1b42451..cdb5751e 100644 --- a/apl/proguard-rules.pro +++ b/apl/proguard-rules.pro @@ -19,3 +19,4 @@ # If you keep the line number information, uncomment this to # hide the original source file name. #-renamesourcefileattribute SourceFile +-dontwarn lombok.NonNull \ No newline at end of file diff --git a/apl/src/androidTest/java/com/amazon/apl/android/component/DisallowedEditTextViewTest.java b/apl/src/androidTest/java/com/amazon/apl/android/component/DisallowedEditTextViewTest.java deleted file mode 100644 index b3ae6dcd..00000000 --- a/apl/src/androidTest/java/com/amazon/apl/android/component/DisallowedEditTextViewTest.java +++ /dev/null @@ -1,117 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.android.component; - -import androidx.test.espresso.Espresso; - -import com.amazon.apl.android.APLOptions; -import com.amazon.apl.android.RootConfig; -import com.amazon.apl.android.dependencies.ISendEventCallbackV2; -import com.amazon.apl.android.document.AbstractDocViewTest; -import com.amazon.apl.enums.RootProperty; - -import org.junit.AfterClass; -import org.junit.Test; - -import static androidx.test.espresso.Espresso.onView; -import static androidx.test.espresso.action.ViewActions.click; -import static androidx.test.espresso.matcher.ViewMatchers.withId; -import static com.amazon.apl.android.espresso.APLViewActions.finish; -import static com.amazon.apl.android.espresso.APLViewAssertions.isFinished; -import static com.amazon.apl.android.utils.KeyboardHelper.isKeyboardOpen; -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertTrue; -import static org.mockito.Mockito.mock; - -public class DisallowedEditTextViewTest extends AbstractDocViewTest { - private static final String DOC = - " \"type\": \"Frame\",\n" + - " \"width\": \"100vw\",\n" + - " \"height\": \"100vh\",\n" + - " \"backgroundColor\": \"black\",\n" + - " \"items\": {\n" + - " \"type\": \"Container\",\n" + - " \"width\": \"100vw\",\n" + - " \"height\": \"100vh\",\n" + - " \"items\": [\n" + - " {\n" + - " \"type\": \"EditText\",\n" + - " \"id\": \"myEditText\",\n" + - " \"text\": \"My favourite edit text box\"\n" + - " },\n" + - " {\n" + - " \"type\": \"Text\",\n" + - " \"id\": \"myPlainText\",\n" + - " \"text\": \"Some other text that is plain\"\n" + - " }\n" + - " ]\n" + - " }\n" + - " }"; - private final ISendEventCallbackV2 mSendEventCallback = mock(ISendEventCallbackV2.class); - - @AfterClass - public static void closeKeyboard() { - // Try to close the keyboard if open. - try { - Espresso.closeSoftKeyboard(); - } catch (RuntimeException ex) { - // Do nothing. - } - } - - @Test - public void testView_click_on_EditText_opens_keyboard() { - RootConfig rootConfig = RootConfig.create("EditText Test", "1.0"); - - onView(withId(com.amazon.apl.android.test.R.id.apl)) - .perform(inflate(DOC, "", "", "{}", APLOptions.builder().build(), rootConfig)) - .check(hasRootContext()); - onView(withComponent(mTestContext.getRootContext().findComponentById("myEditText"))) - .perform(click()); - - assertTrue(isKeyboardOpen()); - } - - @Test - public void testView_click_on_disallowed_EditText_does_not_open_keyboard() { - RootConfig rootConfig = RootConfig.create("EditText Test", "1.0"); - rootConfig.set(RootProperty.kDisallowEditText, Boolean.TRUE); - - onView(withId(com.amazon.apl.android.test.R.id.apl)) - .perform(inflate(DOC, "", "", "{}", APLOptions.builder().build(), rootConfig)) - .check(hasRootContext()); - onView(withComponent(mTestContext.getRootContext().findComponentById("myEditText"))) - .perform(click()); - - assertFalse(isKeyboardOpen()); - } - - @Test - public void testView_disallowed_EditText_occupies_some_space() { - RootConfig rootConfig = RootConfig.create("EditText Test", "1.0"); - onView(withId(com.amazon.apl.android.test.R.id.apl)) - .perform(inflate(DOC, "", "", "{}", APLOptions.builder().build(), rootConfig)) - .check(hasRootContext()); - float expectedHeight = mTestContext.getRootContext().findComponentById("myEditText").getBounds().getHeight(); - float expectedSiblingTop = mTestContext.getRootContext().findComponentById("myPlainText").getBounds().getTop(); - - onView(withId(com.amazon.apl.android.test.R.id.apl)) - .perform(finish(mAplController)) - .check(isFinished()); - - rootConfig.set(RootProperty.kDisallowEditText, Boolean.TRUE); - onView(withId(com.amazon.apl.android.test.R.id.apl)) - .perform(inflate(DOC, "", "", "{}", APLOptions.builder().build(), rootConfig)) - .check(hasRootContext()); - float actualHeight = mTestContext.getRootContext().findComponentById("myEditText").getBounds().getHeight(); - float actualSiblingTop = mTestContext.getRootContext().findComponentById("myPlainText").getBounds().getTop(); - - assertEquals(expectedHeight, actualHeight, 0.1); - assertEquals(expectedSiblingTop, actualSiblingTop, 0.1); - assertEquals(expectedHeight, expectedSiblingTop, 0.1); - } -} diff --git a/apl/src/androidTest/java/com/amazon/apl/android/document/APLControllerTest.java b/apl/src/androidTest/java/com/amazon/apl/android/document/APLControllerTest.java index 05b62e6a..f359983e 100644 --- a/apl/src/androidTest/java/com/amazon/apl/android/document/APLControllerTest.java +++ b/apl/src/androidTest/java/com/amazon/apl/android/document/APLControllerTest.java @@ -18,6 +18,7 @@ import com.amazon.apl.android.IDocumentLifecycleListener; import com.amazon.apl.android.RootConfig; import com.amazon.apl.android.RootContext; +import com.amazon.apl.android.configuration.ConfigurationChange; import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.views.APLAbsoluteLayout; @@ -31,6 +32,7 @@ import java.util.concurrent.TimeUnit; import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertNotNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.eq; @@ -113,7 +115,7 @@ public void testAPLController_renderWithAutoSize() throws InterruptedException { @Override public void onDocumentRender(@NonNull RootContext rootContext) { assertTrue(rootContext.getTopComponent() instanceof Frame); - assertTrue(rootContext.isAutoSize()); + assertTrue(rootContext.isAutoSizeLayoutPending()); assertEquals(aplLayout.getLayoutParams().width, rootContext.getAutoSizedWidth()); assertEquals(aplLayout.getLayoutParams().height, rootContext.getAutoSizedHeight()); renderLatch.countDown(); @@ -134,6 +136,51 @@ public void onDocumentRender(@NonNull RootContext rootContext) { assertTrue(captor.getValue() > 0); }); - assertTrue(renderLatch.await(1, TimeUnit.SECONDS)); + assertTrue(renderLatch.await(5, TimeUnit.SECONDS)); + } + + @UiThreadTest + @Test + public void testAPLController_renderWithAutoSizeConfigurationChange() throws InterruptedException { + final CountDownLatch latch = new CountDownLatch(1); + activityRule.getScenario().onActivity(activity -> { + APLLayout aplLayout = activity.findViewById(com.amazon.apl.android.test.R.id.apl); + aplLayout.getPresenter().addDocumentLifecycleListener(new IDocumentLifecycleListener() { + @Override + public void onDocumentRender(@NonNull RootContext rootContext) { + assertTrue(rootContext.getTopComponent() instanceof Frame); + assertFalse(rootContext.isAutoSizeLayoutPending()); + + ConfigurationChange configurationChange = aplLayout.createConfigurationChange(). + minWidth(300).maxWidth(2000).minHeight(300).maxHeight(4000). + build(); + try { + aplLayout.handleConfigurationChange(configurationChange); + //layoutmanager.layout() is trigered after pending components are cleared, hence we call on tick + rootContext.onTick(400); + assertTrue(rootContext.isAutoSizeLayoutPending()); + assertEquals(aplLayout.getLayoutParams().width, rootContext.getAutoSizedWidth()); + assertEquals(aplLayout.getLayoutParams().height, rootContext.getAutoSizedHeight()); + latch.countDown(); + } catch (APLController.APLException e) { + throw new RuntimeException(e); + } + } + }); + + IAPLController aplController = new APLController.Builder() + .aplDocument(SIMPLE_DOC) + .aplOptions(APLOptions.builder().telemetryProvider(mockTelemetry).build()) + .aplLayout(aplLayout) + .rootConfig(RootConfig.create()) + .render(); + + assertNotNull(aplController); + verify(mockTelemetry).createMetricId(ITelemetryProvider.APL_DOMAIN, LIBRARY_INITIALIZATION_TIME, ITelemetryProvider.Type.TIMER); + ArgumentCaptor captor = ArgumentCaptor.forClass(Long.class); + verify(mockTelemetry).reportTimer(eq(1), eq(TimeUnit.MILLISECONDS), captor.capture()); + assertTrue(captor.getValue() > 0); + }); + assertTrue(latch.await(1, TimeUnit.SECONDS)); } } diff --git a/apl/src/androidTest/java/com/amazon/apl/android/document/AbstractDocUnitTest.java b/apl/src/androidTest/java/com/amazon/apl/android/document/AbstractDocUnitTest.java index 92c12161..c6902f91 100644 --- a/apl/src/androidTest/java/com/amazon/apl/android/document/AbstractDocUnitTest.java +++ b/apl/src/androidTest/java/com/amazon/apl/android/document/AbstractDocUnitTest.java @@ -64,6 +64,7 @@ public T get() { // Load the APL library. static { APLController.initializeAPL(InstrumentationRegistry.getInstrumentation().getContext()); + APLController.waitForInitializeAPLToComplete(null); } protected long mTime = 0L; diff --git a/apl/src/androidTest/java/com/amazon/apl/android/document/ViewhostTest.java b/apl/src/androidTest/java/com/amazon/apl/android/document/ViewhostTest.java new file mode 100644 index 00000000..2af1d313 --- /dev/null +++ b/apl/src/androidTest/java/com/amazon/apl/android/document/ViewhostTest.java @@ -0,0 +1,147 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.android.document; + +import static org.junit.Assert.assertNotNull; +import static org.junit.Assert.assertTrue; + +import com.amazon.apl.android.APLLayout; +import com.amazon.apl.android.DocumentSession; +import com.amazon.apl.android.Frame; +import com.amazon.apl.android.views.APLAbsoluteLayout; +import com.amazon.apl.viewhost.DocumentHandle; +import com.amazon.apl.viewhost.PreparedDocument; +import com.amazon.apl.viewhost.Viewhost; +import com.amazon.apl.viewhost.config.DocumentOptions; +import com.amazon.apl.viewhost.config.ViewhostConfig; +import com.amazon.apl.viewhost.internal.DocumentHandleImpl; +import com.amazon.apl.viewhost.internal.DocumentState; +import com.amazon.apl.viewhost.internal.DocumentStateChangeListener; +import com.amazon.apl.viewhost.primitives.JsonStringDecodable; +import com.amazon.apl.viewhost.request.FinishDocumentRequest; +import com.amazon.apl.viewhost.request.PrepareDocumentRequest; +import com.amazon.apl.viewhost.request.RenderDocumentRequest; + +import org.junit.Test; + +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; + +// This test requires a view to be displayed thus must be done via androidTest. +public class ViewhostTest extends AbstractDocViewTest { + + private static final String SIMPLE_DOC = "{" + + " \"type\": \"APL\"," + + " \"version\": \"1.0\"," + + " \"mainTemplate\": {" + + " \"item\":" + + " {" + + " \"type\": \"Frame\"" + + " }" + + " }" + + "}"; + + @Test + public void testViewhost_prepareAndRender() throws InterruptedException { + CountDownLatch displayedLatch = new CountDownLatch(1); + CountDownLatch inflatedLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(1); + final DocumentHandle[] mHandle = new DocumentHandle[1]; + + activityRule.getScenario().onActivity(activity -> { + APLLayout aplLayout = activity.findViewById(com.amazon.apl.android.test.R.id.apl); + DocumentOptions documentOptions = DocumentOptions.builder().build(); + ViewhostConfig viewhostConfig = ViewhostConfig.builder() + .defaultDocumentOptions(documentOptions) + .build(); + Viewhost mViewhost = Viewhost.create(viewhostConfig); + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(documentOptions) + .build(); + mViewhost.bind(aplLayout); + + mViewhost.registerStateChangeListener(new DocumentStateChangeListener() { + @Override + public void onDocumentStateChanged(DocumentState state, DocumentHandle handle) { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + assertTrue(((DocumentHandleImpl) handle).getRootContext().getTopComponent() instanceof Frame); + } + if (state == DocumentState.DISPLAYED) { + displayedLatch.countDown(); + assertTrue(aplLayout.getChildAt(0) instanceof APLAbsoluteLayout); + } + if (state == DocumentState.FINISHED) { + finishLatch.countDown(); + } + } + }); + // Prepare and render + PreparedDocument preparedDocument = mViewhost.prepare(request); + mHandle[0] = mViewhost.render(preparedDocument); + assertNotNull(mHandle[0]); + }); + assertTrue(inflatedLatch.await(5, TimeUnit.SECONDS)); + assertTrue(displayedLatch.await(5, TimeUnit.SECONDS)); + + // Finish + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder().build(); + boolean result = mHandle[0].finish(finishRequest); + assertTrue(finishLatch.await(1, TimeUnit.SECONDS)); + assertTrue(result); + } + + @Test + public void testViewhost_render() throws InterruptedException { + CountDownLatch displayedLatch = new CountDownLatch(1); + CountDownLatch inflatedLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(1); + final DocumentHandle[] mHandle = new DocumentHandle[1]; + + activityRule.getScenario().onActivity(activity -> { + APLLayout aplLayout = activity.findViewById(com.amazon.apl.android.test.R.id.apl); + DocumentOptions documentOptions = DocumentOptions.builder().build(); + ViewhostConfig viewhostConfig = ViewhostConfig.builder() + .defaultDocumentOptions(documentOptions) + .build(); + Viewhost mViewhost = Viewhost.create(viewhostConfig); + RenderDocumentRequest request = RenderDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(documentOptions) + .build(); + mViewhost.bind(aplLayout); + + mViewhost.registerStateChangeListener(new DocumentStateChangeListener() { + @Override + public void onDocumentStateChanged(DocumentState state, DocumentHandle handle) { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + assertTrue(((DocumentHandleImpl) handle).getRootContext().getTopComponent() instanceof Frame); + } + if (state == DocumentState.DISPLAYED) { + displayedLatch.countDown(); + assertTrue(aplLayout.getChildAt(0) instanceof APLAbsoluteLayout); + } + if (state == DocumentState.FINISHED) { + finishLatch.countDown(); + } + } + }); + mHandle[0] = mViewhost.render(request); + assertNotNull(mHandle[0]); + }); + assertTrue(inflatedLatch.await(5, TimeUnit.SECONDS)); + assertTrue(displayedLatch.await(5, TimeUnit.SECONDS)); + // Finish + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder().build(); + boolean result = mHandle[0].finish(finishRequest); + assertTrue(finishLatch.await(1, TimeUnit.SECONDS)); + assertTrue(result); + } +} diff --git a/apl/src/main/cpp/include/jnisession.h b/apl/src/main/cpp/include/jnisession.h new file mode 100644 index 00000000..f39ec84d --- /dev/null +++ b/apl/src/main/cpp/include/jnisession.h @@ -0,0 +1,28 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + */ + +#ifndef APLVIEWHOSTANDROID_JNISESSION_H +#define APLVIEWHOSTANDROID_JNISESSION_H + + +#ifdef __cplusplus +extern "C" { +#endif +/** +* Initialize and cache java class and method handles for callback to the rendering layer. +*/ +jboolean +jnisession_OnLoad(JavaVM *vm, void *reserved __attribute__((__unused__))); + +/** + * Release the class and method cache. + */ +void +jnisession_OnUnload(JavaVM *vm, void *reserved __attribute__((__unused__))); + +#ifdef __cplusplus +} +#endif + +#endif //APLVIEWHOSTANDROID_JNISESSION_H \ No newline at end of file diff --git a/apl/src/main/cpp/jniapl.cpp b/apl/src/main/cpp/jniapl.cpp index a809685c..52796189 100644 --- a/apl/src/main/cpp/jniapl.cpp +++ b/apl/src/main/cpp/jniapl.cpp @@ -22,6 +22,14 @@ #include "jniscaling.h" #include "jnitextmeasurecallback.h" #include "jniextensionmediator.h" +#include "jnisession.h" + +#ifdef INCLUDE_ALEXAEXT +#include "jniextensionexecutor.h" +#include "jniextensionproxy.h" +#include "jniextensionregistrar.h" +#include "jniextensionresource.h" +#endif #ifdef __ANDROID__ #include "loggingbridge.h" @@ -63,13 +71,26 @@ namespace apl { jboolean mediaplayerLoaded = mediaplayer_OnLoad(vm, reserved); jboolean documentmanagerLoaded = documentmanager_OnLoad(vm, reserved); jboolean mediaplayerFactoryLoaded = mediaplayerfactory_OnLoad(vm, reserved); + jboolean jnisessionLoaded = jnisession_OnLoad(vm, reserved); + +#ifdef INCLUDE_ALEXAEXT + jboolean extensionExecutorLoaded = extensionexecutor_OnLoad(vm, reserved); + jboolean extensionProxyLoaded = extensionproxy_OnLoad(vm, reserved); + jboolean extensionProviderLoaded = extensionprovider_OnLoad(vm, reserved); + jboolean extensionResourceProviderLoaded = extensionresource_OnLoad(vm, reserved); + + if (!extensionProxyLoaded || !extensionProviderLoaded || !extensionResourceProviderLoaded + || !extensionExecutorLoaded) { + return JNI_ERR; + } +#endif if (!driverLoaded || !contentLoaded || !rootconfigLoaded || !complexpropertyLoaded || !eventLoaded || !actionLoaded || !graphicLoaded || !jniutilLoaded || !jniscalingLoaded || !textmeasureLoaded || !localExtensionMediatorLoaded || !audioFactoryLoaded || !audioPlayerLoaded || !localExtensionMediatorLoaded || !mediaplayerLoaded || !mediaplayerFactoryLoaded - || !documentmanagerLoaded) { + || !documentmanagerLoaded || !jnisessionLoaded) { return JNI_ERR; } @@ -99,6 +120,14 @@ namespace apl { mediaplayer_OnUnload(vm, reserved); mediaplayerfactory_OnUnload(vm, reserved); documentmanager_OnUnload(vm, reserved); + jnisession_OnUnload(vm, reserved); +#ifdef INCLUDE_ALEXAEXT + extensionexecutor_OnUnload(vm, reserved); + extensionproxy_OnUnload(vm, reserved); + extensionprovider_OnUnload(vm, reserved); + extensionresource_OnUnload(vm, reserved); +#endif + } } //namespace jni diff --git a/apl/src/main/cpp/jnicomplexproperty.cpp b/apl/src/main/cpp/jnicomplexproperty.cpp index 514ceeb1..57364af9 100644 --- a/apl/src/main/cpp/jnicomplexproperty.cpp +++ b/apl/src/main/cpp/jnicomplexproperty.cpp @@ -725,6 +725,33 @@ namespace apl { return env->NewStringUTF(action->getLabel().c_str()); } + JNIEXPORT jfloat JNICALL + Java_com_amazon_apl_android_primitive_AccessibilityAdjustableRange_nGetMinValue(JNIEnv *env, jclass clazz, + jlong handle, + jint propertyId) { + auto value = getLookup(handle)->getObject(static_cast(propertyId), handle); + auto minValue = value.get("minValue").asNumber(); + return static_cast(minValue); + } + + JNIEXPORT jfloat JNICALL + Java_com_amazon_apl_android_primitive_AccessibilityAdjustableRange_nGetMaxValue(JNIEnv *env, jclass clazz, + jlong handle, + jint propertyId) { + auto value = getLookup(handle)->getObject(static_cast(propertyId), handle); + auto maxValue = value.get("maxValue").asNumber(); + return static_cast(maxValue); + } + + JNIEXPORT jfloat JNICALL + Java_com_amazon_apl_android_primitive_AccessibilityAdjustableRange_nGetCurrentValue(JNIEnv *env, jclass clazz, + jlong handle, + jint propertyId) { + auto value = getLookup(handle)->getObject(static_cast(propertyId), handle); + auto currentValue = value.get("currentValue").asNumber(); + return static_cast(currentValue); + } + JNIEXPORT jint JNICALL Java_com_amazon_apl_android_primitive_GraphicFilters_nGetGraphicFilterTypeAt(JNIEnv *env, jclass clazz, jlong handle, jint propertyId, jint index) { diff --git a/apl/src/main/cpp/jnicomponent.cpp b/apl/src/main/cpp/jnicomponent.cpp index 74c5e81e..b3b00f8a 100644 --- a/apl/src/main/cpp/jnicomponent.cpp +++ b/apl/src/main/cpp/jnicomponent.cpp @@ -198,6 +198,41 @@ namespace apl { return static_cast(c->isCharacterValid(character)); } + JNIEXPORT jfloat JNICALL + Java_com_amazon_apl_android_Component_nGetCalculatedWidth(JNIEnv *env, jclass clazz, + jlong componentHandle) { + auto component = get(componentHandle); + auto calculatedWidth = component->getCalculated(apl::kPropertyBounds).get().getWidth(); + + return calculatedWidth; + } + + JNIEXPORT jfloat JNICALL + Java_com_amazon_apl_android_Component_nGetCalculatedHeight(JNIEnv *env, jclass clazz, + jlong componentHandle) { + auto component = get(componentHandle); + auto calculatedHeight = component->getCalculated(apl::kPropertyBounds).get().getHeight(); + + return calculatedHeight; + } + + JNIEXPORT jfloatArray JNICALL + Java_com_amazon_apl_android_Component_nGetGlobalPointCoordinates(JNIEnv *env, jclass clazz, + jlong componentHandle, + jfloat pointA, + jfloat pointB) { + auto component = get(componentHandle); + auto point = component->localToGlobal({pointA, pointB}); + + float buffer[2] = { float (point.getX()), + float (point.getY())}; + + jfloatArray pointArray = env->NewFloatArray(2); + env->SetFloatArrayRegion(pointArray, 0, 2, buffer); + + return pointArray; + } + #ifdef __cplusplus } #endif diff --git a/apl/src/main/cpp/jnievent.cpp b/apl/src/main/cpp/jnievent.cpp index f26e7fd1..fee7fe88 100644 --- a/apl/src/main/cpp/jnievent.cpp +++ b/apl/src/main/cpp/jnievent.cpp @@ -45,6 +45,17 @@ namespace apl { return JNI_FALSE; } + Event::setUserDataReleaseCallback([](void* userData){ + JNIEnv* jniEnv; + JAVA_VM->GetEnv(reinterpret_cast(&jniEnv), JNI_VERSION_1_6); + if (!jniEnv) { + return; + } + if (userData) { + jniEnv->DeleteWeakGlobalRef((jobject) userData); + } + }); + return JNI_TRUE; } @@ -70,6 +81,7 @@ namespace apl { } jobject weak = env->NewWeakGlobalRef(instance); + event->setUserData(weak); event->getActionRef().addTerminateCallback([weak](const std::shared_ptr&) { // May be called from a different thread than nInit necessitating a call to get this thread's JNIEnv JNIEnv* jniEnv; @@ -83,6 +95,7 @@ namespace apl { return; } jniEnv->CallVoidMethod(local, ACTION_ON_TERMINATE); + jniEnv->DeleteLocalRef(local); }); } diff --git a/apl/src/main/cpp/jnirootconfig.cpp b/apl/src/main/cpp/jnirootconfig.cpp index ac8715e9..b348f3a5 100644 --- a/apl/src/main/cpp/jnirootconfig.cpp +++ b/apl/src/main/cpp/jnirootconfig.cpp @@ -298,9 +298,9 @@ namespace apl { Java_com_amazon_apl_android_RootConfig_nLiveData(JNIEnv *env, jclass clazz, jlong nativeHandle, jstring name, jlong liveDataHandle) { auto rc = get(nativeHandle); - auto liveArray = get(liveDataHandle); + auto liveObject = get(liveDataHandle); - rc->liveData(getStdString(env, name), liveArray); + rc->liveData(getStdString(env, name), liveObject); } diff --git a/apl/src/main/cpp/jnirootcontext.cpp b/apl/src/main/cpp/jnirootcontext.cpp index fb25a9e1..026375f4 100644 --- a/apl/src/main/cpp/jnirootcontext.cpp +++ b/apl/src/main/cpp/jnirootcontext.cpp @@ -885,6 +885,13 @@ namespace apl { env->ReleaseStringUTFChars(url_, url); } + JNIEXPORT jlong JNICALL + Java_com_amazon_apl_android_RootContext_nGetDocumentContext(JNIEnv *env, jobject thiz, jlong nativeHandle) { + auto rc = get(nativeHandle); + auto documentContext = rc->topDocument(); + return createHandle(documentContext); + } + /** * Notifies core when a media load has failed */ @@ -904,6 +911,91 @@ namespace apl { env->ReleaseStringUTFChars(error_, error); } + JNIEXPORT jstring JNICALL + Java_com_amazon_apl_android_RootContext_nDocumentCommandRequest(JNIEnv *env, + jclass clazz, + jlong handle, + jstring method_, + jstring params_) { + auto rc = get(handle); + + // Get method + const char* methodString = env->GetStringUTFChars(method_, nullptr); + std::string method(methodString); + env->ReleaseStringUTFChars(method_, methodString); + + // Re-parse command parameters + const char* paramsString = env->GetStringUTFChars(params_, nullptr); + auto paramsJson = rapidjson::Document(); + paramsJson.Parse(paramsString); + env->ReleaseStringUTFChars(params_, paramsString); + apl::Object params = apl::Object(std::move(paramsJson)); + + // Result is always an object + rapidjson::Document document(rapidjson::kObjectType); + rapidjson::Value result(rapidjson::kObjectType); + + if (method == "Document.getMainPackage") { + auto package = rc->topDocument()->content()->getDocument(); + rapidjson::Document value(&document.GetAllocator()); + value.CopyFrom(package->json(), value.GetAllocator()); + result.AddMember("name", "_main", document.GetAllocator()); + result.AddMember("value", value, document.GetAllocator()); + } else if (method == "Document.getPackageList") { + rapidjson::Value packages(rapidjson::kArrayType); + std::vector packages_ = rc->topDocument()->content()->getLoadedPackageNames(); + for (auto package : packages_) { + rapidjson::Value name; + name.SetString(package.c_str(), package.length(), document.GetAllocator()); + packages.PushBack(name, document.GetAllocator()); + } + result.AddMember("packages", packages, document.GetAllocator()); + } else if (method == "Document.getPackage") { + if (params.has("name")) { + std::string name = params.get("name").asString(); + rapidjson::Value packageName; + packageName.SetString(name.c_str(), document.GetAllocator()); + auto package = rc->topDocument()->content()->getPackage(name); + if (package != nullptr) { + rapidjson::Document value(&document.GetAllocator()); + value.CopyFrom(package->json(), value.GetAllocator()); + result.AddMember("name", packageName, document.GetAllocator()); + result.AddMember("value", value, document.GetAllocator()); + } + } + } else if (method == "Document.getVisualContext") { + rapidjson::Value value = rc->serializeVisualContext(document.GetAllocator()); + result.AddMember("value", value, document.GetAllocator()); + } else if (method == "Document.getDOM") { + if (params.has("extended")) { + bool extended = params.get("extended").asBoolean(); + rapidjson::Value value = rc->serializeDOM(extended, document.GetAllocator()); + result.AddMember("value", value, document.GetAllocator()); + } + } else if (method == "Document.getSceneGraph") { + // Not supported yet + } else if (method == "Document.getRootContext") { + rapidjson::Value value = rc->serializeContext(document.GetAllocator()); + result.AddMember("value", value, document.GetAllocator()); + } else if (method == "Document.getContext") { + if (params.has("componentId") && params.has("depth")) { + std::string componentId = params.get("componentId").asString(); + auto item = rc->findByUniqueId(componentId); + if (item) { + rapidjson::Value value = item->serializeContext(0, document.GetAllocator()); + result.AddMember("value", value, document.GetAllocator()); + } + } + } + + // Convert JSON to a string + rapidjson::StringBuffer buffer; + rapidjson::Writer writer(buffer); + result.Accept(writer); + std::u16string u16 = converter.from_bytes(buffer.GetString()); + return env->NewString(reinterpret_cast(u16.c_str()), u16.length()); + } + #pragma clang diagnostic pop #ifdef __cplusplus diff --git a/apl/src/main/cpp/jnisession.cpp b/apl/src/main/cpp/jnisession.cpp index 7f8764bc..b8b15525 100644 --- a/apl/src/main/cpp/jnisession.cpp +++ b/apl/src/main/cpp/jnisession.cpp @@ -8,6 +8,7 @@ #include "apl/apl.h" #include "jniutil.h" +#include "jnisession.h" namespace apl { namespace jni { @@ -15,16 +16,102 @@ namespace apl { #ifdef __cplusplus extern "C" { #endif +#pragma clang diagnostic push +#pragma clang diagnostic ignored "-Wunused-parameter" + static jclass SESSION_CLASS; + static jmethodID SESSION_WRITE; + static JavaVM *JAVA_VM; + + jboolean jnisession_OnLoad(JavaVM *vm, void *reserved) { + LOG(apl::LogLevel::kDebug) << "Loading View Host session JNI environment."; + + JAVA_VM = vm; + JNIEnv *env; + if (vm->GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { + return JNI_FALSE; + } + SESSION_CLASS = reinterpret_cast(env->NewGlobalRef( + env->FindClass("com/amazon/apl/android/Session"))); + SESSION_WRITE = env->GetMethodID(SESSION_CLASS, "write", + "(Lcom/amazon/apl/android/Session$LogEntryLevel;Lcom/amazon/apl/android/Session$LogEntrySource;Ljava/lang/String;[Ljava/lang/Object;)V"); + return JNI_TRUE; + } + + void jnisession_OnUnload(JavaVM *vm, void *reserved) { + LOG(apl::LogLevel::kDebug) << "Unloading View Host Session JNI environment."; + apl::LoggerFactory::instance().reset(); + + JNIEnv *env; + if (vm->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + LOG(apl::LogLevel::kError) << "Environment failure, cannot proceed"; + return; + } + + env->DeleteGlobalRef(SESSION_CLASS); + } std::atomic_bool isDebuggingEnabled(false); class AndroidSession : public Session { + private: + jobject mInstance; + public: + AndroidSession() : mInstance(nullptr) {} + + void setInstance(jobject instance) { + JNIEnv *env; + if (JAVA_VM->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + // environment failure, can't proceed. + return; + } + + if (mInstance != nullptr && !env->IsSameObject(mInstance, nullptr)) { + env->DeleteWeakGlobalRef(mInstance); + } + mInstance = env->NewWeakGlobalRef(instance); + } + + ~AndroidSession() override { + JNIEnv *env; + if (JAVA_VM->GetEnv((void **)&env, JNI_VERSION_1_6) != JNI_OK) { + return; + } + + if (mInstance != nullptr) { + env->DeleteWeakGlobalRef(mInstance); + mInstance = nullptr; + } + } + void write(const char *filename, const char *func, const char *value) override { LoggerFactory::instance().getLogger(LogLevel::kWarn, filename, func).session( *this).log("%s", value); + + if (!isDebuggingEnabled.load()) { + return; + } + + JNIEnv *env; + if (JAVA_VM->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + // environment failure, can't proceed. + LOG(apl::LogLevel::kError) + << "Attempted to write a session log with no env."; + return; + } + std::string message = std::string("[") + std::string(filename) + std::string(":") + std::string(func) + std::string("] ") + std::string(value); + jobject javaLevel = convertToJavaLogLevel(env, LogLevel::kInfo); + jobject javaSource = convertToJavaLogSource(env, "session"); + jstring javaMessage = env->NewStringUTF(message.c_str()); + + env->CallVoidMethod(mInstance, SESSION_WRITE, javaLevel, javaSource, javaMessage, nullptr); + + env->DeleteLocalRef(javaLevel); + env->DeleteLocalRef(javaSource); + env->DeleteLocalRef(javaMessage); } + //Report a log message resulting from execution of the Log command. void write(LogCommandMessage &&message) override { if (!isDebuggingEnabled.load()) { return; @@ -39,26 +126,114 @@ namespace apl { // Add arguments array if it's not empty if (!message.arguments.empty()) { writer.Key("arguments"); - rapidjson::Value serializedArg = message.arguments.serialize(serializer.GetAllocator()); + rapidjson::Value serializedArg = message.arguments.serialize( + serializer.GetAllocator()); serializedArg.Accept(writer); // Serialize into the writer directly } // Add origin object if it's not empty if (!message.origin.empty()) { writer.Key("origin"); - rapidjson::Value originSerialized = message.origin.serialize(serializer.GetAllocator()); + rapidjson::Value originSerialized = message.origin.serialize( + serializer.GetAllocator()); originSerialized.Accept(writer); } writer.EndObject(); std::string result = buffer.GetString(); - LoggerFactory::instance().getLogger(message.level, "Log", "Command").session(*this).log("%s %s", - message.text.c_str(), - result.c_str()); + LoggerFactory::instance().getLogger(message.level, "Log", "Command").session( + *this).log("%s %s", + message.text.c_str(), + result.c_str()); + + JNIEnv *env; + if (JAVA_VM->GetEnv((void **) &env, JNI_VERSION_1_6) != JNI_OK) { + // environment failure, can't proceed. + LOG(apl::LogLevel::kError) + << "Attempted to execute a log command with no env."; + return; + } + + jobject javaLevel = convertToJavaLogLevel(env, message.level); + jobject javaSource = convertToJavaLogSource(env, "command"); + jstring javaMessage = env->NewStringUTF(message.text.c_str()); + Object arguments = message.arguments; + + size_t size = arguments.size(); + jobjectArray javaArray = env->NewObjectArray( + static_cast(size), + env->FindClass("java/lang/Object"), + nullptr); + + for (size_t i = 0; i < size; ++i) { + apl::Object element = arguments.at(static_cast(i)); + jobject javaObject = getJObject(env, element); + env->SetObjectArrayElement(javaArray, static_cast(i), javaObject); + env->DeleteLocalRef(javaObject); + } + + env->CallVoidMethod(mInstance, SESSION_WRITE, javaLevel, javaSource, javaMessage, javaArray); + + env->DeleteLocalRef(javaLevel); + env->DeleteLocalRef(javaSource); + env->DeleteLocalRef(javaMessage); + + } + + // Utility function to convert C++ LogEntryLevel to Java LogEntryLevel + jobject convertToJavaLogLevel(JNIEnv *env, LogLevel level) { + jclass enumClass = env->FindClass("com/amazon/apl/android/Session$LogEntryLevel"); + jfieldID fieldID = nullptr; + + switch (level) { + case LogLevel::kNone: + fieldID = env->GetStaticFieldID(enumClass, "NONE", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + case LogLevel::kTrace: + fieldID = env->GetStaticFieldID(enumClass, "TRACE", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + case LogLevel::kDebug: + fieldID = env->GetStaticFieldID(enumClass, "DEBUG", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + case LogLevel::kInfo: + fieldID = env->GetStaticFieldID(enumClass, "INFO", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + case LogLevel::kWarn: + fieldID = env->GetStaticFieldID(enumClass, "WARN", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + case LogLevel::kError: + fieldID = env->GetStaticFieldID(enumClass, "ERROR", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + case LogLevel::kCritical: + fieldID = env->GetStaticFieldID(enumClass, "CRITICAL", "Lcom/amazon/apl/android/Session$LogEntryLevel;"); + break; + default: + break; + } + + return env->GetStaticObjectField(enumClass, fieldID); } - }; + // Utility function to convert C++ LogEntrySource to Java LogEntrySource + jobject convertToJavaLogSource(JNIEnv *env, const std::string &sourceString) { + jclass enumClass = env->FindClass("com/amazon/apl/android/Session$LogEntrySource"); + jfieldID fieldID = nullptr; + // Convert the source string to uppercase for case-insensitive comparison + std::string upperSourceString = sourceString; + std::transform(upperSourceString.begin(), upperSourceString.end(), upperSourceString.begin(), ::toupper); + + if (upperSourceString == "SESSION") { + fieldID = env->GetStaticFieldID(enumClass, "SESSION", "Lcom/amazon/apl/android/Session$LogEntrySource;"); + } else if (upperSourceString == "VIEW") { + fieldID = env->GetStaticFieldID(enumClass, "VIEW", "Lcom/amazon/apl/android/Session$LogEntrySource;"); + } else if (upperSourceString == "COMMAND") { + fieldID = env->GetStaticFieldID(enumClass, "COMMAND", "Lcom/amazon/apl/android/Session$LogEntrySource;"); + } + + return env->GetStaticObjectField(enumClass, fieldID); + } + }; JNIEXPORT void JNICALL Java_com_amazon_apl_android_Session_nSetDebuggingEnabled(JNIEnv *env, jclass clazz, @@ -67,8 +242,9 @@ namespace apl { } JNIEXPORT jlong JNICALL - Java_com_amazon_apl_android_Session_nCreate(JNIEnv *env, jclass clazz) { + Java_com_amazon_apl_android_Session_nCreate(JNIEnv *env, jobject instance) { auto session = std::make_shared(); + session->setInstance(instance); return createHandle(session); } @@ -78,6 +254,8 @@ namespace apl { return env->NewStringUTF(session->getLogId().c_str()); } +#pragma clang diagnostic pop + #ifdef __cplusplus } #endif diff --git a/apl/src/main/cpp/jniutil.cpp b/apl/src/main/cpp/jniutil.cpp index 18503632..d22f7707 100644 --- a/apl/src/main/cpp/jniutil.cpp +++ b/apl/src/main/cpp/jniutil.cpp @@ -11,8 +11,15 @@ namespace apl { namespace jni { - static std::wstring_convert, char16_t> converter; +#ifdef VERSION_NAME + /** + * Version string embedded for version information in .rodata . + */ + const char* versionString = "APLJNI Library is " VERSION_NAME " version."; +#endif + + static std::wstring_convert, char16_t> converter; /** * Convert a Java string into a std::string in UTF-8 encoding * @param env The Java environment diff --git a/apl/src/main/java/com/amazon/apl/android/APLAccessibilityDelegate.java b/apl/src/main/java/com/amazon/apl/android/APLAccessibilityDelegate.java index 356d3d75..d13a9f2a 100644 --- a/apl/src/main/java/com/amazon/apl/android/APLAccessibilityDelegate.java +++ b/apl/src/main/java/com/amazon/apl/android/APLAccessibilityDelegate.java @@ -15,6 +15,7 @@ import com.amazon.apl.android.primitive.AccessibilityActions; import com.amazon.apl.enums.PropertyKey; +import com.amazon.apl.enums.Role; import com.amazon.apl.enums.UpdateType; import java.util.HashMap; @@ -30,6 +31,7 @@ public class APLAccessibilityDelegate extends Accessibility private static final int ACTION_BASE_ID = 0x3f000000; private static final Map STANDARD_ACTION_MAP; // apl actionName -> android actionId + private static final Map ADJUSTABLE_STANDARD_ACTION_MAP; private static int sCustomActionCount = ACTION_BASE_ID; @@ -45,6 +47,14 @@ public class APLAccessibilityDelegate extends Accessibility STANDARD_ACTION_MAP.put("scrollbackward", AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); STANDARD_ACTION_MAP.put("scrollforward", AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); STANDARD_ACTION_MAP.put("swipeaway", R.id.action_swipe_away); + + ADJUSTABLE_STANDARD_ACTION_MAP = new HashMap<>(); + ADJUSTABLE_STANDARD_ACTION_MAP.put("activate", AccessibilityNodeInfoCompat.ACTION_CLICK); + ADJUSTABLE_STANDARD_ACTION_MAP.put("doubletap", R.id.action_double_tap); + ADJUSTABLE_STANDARD_ACTION_MAP.put("longpress", AccessibilityNodeInfoCompat.ACTION_LONG_CLICK); + ADJUSTABLE_STANDARD_ACTION_MAP.put("decrement", AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD); + ADJUSTABLE_STANDARD_ACTION_MAP.put("increment", AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD); + ADJUSTABLE_STANDARD_ACTION_MAP.put("swipeaway", R.id.action_swipe_away); } protected APLAccessibilityDelegate(C c, Context context) { @@ -64,6 +74,7 @@ public void onInitializeAccessibilityNodeInfo(View host, AccessibilityNodeInfoCo setActions(info); setResourceId(info); setText(info); + setRangeInfo(info); } @Override @@ -84,6 +95,11 @@ void resetCustomActionCount() { private void setActions(AccessibilityNodeInfoCompat info) { AccessibilityActions actions = mComponent.getAccessibilityActions(); Set actionSet = new HashSet<>(); + Map actionMap = STANDARD_ACTION_MAP; + + if (mComponent.getRole() == Role.kRoleAdjustable) { + actionMap = ADJUSTABLE_STANDARD_ACTION_MAP; + } for (AccessibilityActions.AccessibilityAction action : actions) { // ignore repeated action names. Consider only the first one. @@ -91,8 +107,8 @@ private void setActions(AccessibilityNodeInfoCompat info) { continue; int actionId; - if (STANDARD_ACTION_MAP.containsKey(action.name())) { - actionId = STANDARD_ACTION_MAP.get(action.name()); + if (actionMap.containsKey(action.name())) { + actionId = actionMap.get(action.name()); } else { actionId = sCustomActionCount++; } @@ -236,4 +252,18 @@ private void setText(AccessibilityNodeInfoCompat nodeInfo) { nodeInfo.setText(mComponent.getProperties().getString(PropertyKey.kPropertyText)); } } + + private void setRangeInfo(AccessibilityNodeInfoCompat nodeInfo) { + if (mComponent.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue) && + (mComponent.getAccessibilityAdjustableValue() != null && !mComponent.getAccessibilityAdjustableValue().isEmpty())) + return; + + if (mComponent.getRole() == Role.kRoleAdjustable && mComponent.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableRange)) { + nodeInfo.setRangeInfo(AccessibilityNodeInfoCompat.RangeInfoCompat.obtain( + AccessibilityNodeInfoCompat.RangeInfoCompat.RANGE_TYPE_FLOAT, + mComponent.getAccessibilityAdjustableRange().minValue(), + mComponent.getAccessibilityAdjustableRange().maxValue(), + mComponent.getAccessibilityAdjustableRange().currentValue())); + } + } } \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/android/APLController.java b/apl/src/main/java/com/amazon/apl/android/APLController.java index 0bd2abba..385c7fff 100644 --- a/apl/src/main/java/com/amazon/apl/android/APLController.java +++ b/apl/src/main/java/com/amazon/apl/android/APLController.java @@ -32,6 +32,7 @@ import com.amazon.apl.android.thread.Threading; import com.amazon.apl.android.utils.APLTrace; import com.amazon.apl.android.utils.TracePoint; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.DisplayState; import com.amazon.apl.viewhost.config.EmbeddedDocumentFactory; import com.amazon.common.NativeBinding; @@ -85,7 +86,17 @@ private static final class Load implements Callable { public Boolean call() { nativeLibraryLoadingStartTime = SystemClock.elapsedRealtime(); System.loadLibrary("common-jni"); - System.loadLibrary("discovery-jni"); + + // In size minimized variant, JNI code of discovery module, which really is JNI code for + // extension registration and proxy, is merged into apl-jni and JNI shared library + // for discovery is absent in discovery aar. This is controlled by BuildConfig build type. + // Since we have two valid variant combinations for our customers viz + // (apl-release, discovery-standard-release) and (aplMinSized-release, discovery-standard-releaseMinSized); + // we can use build type of apl library to decide to load discovery JNI or not. + if(!BuildConfig.BUILD_TYPE.equals("releaseMinSized")) { + System.loadLibrary("discovery-jni"); + } + System.loadLibrary("apl-jni"); nativeLibraryLoadingEndTime = SystemClock.elapsedRealtime(); return true; @@ -190,11 +201,13 @@ public APLController(RootContext rootContext, Content content) { @VisibleForTesting interface IContentCreator { - Content create(String aplDocument, APLOptions options, Content.CallbackV2 callbackV2, RootConfig rootConfig); + Content create(String aplDocument, APLOptions options, Content.CallbackV2 callbackV2, RootConfig rootConfig, IDTNetworkRequestHandler dtNetworkRequestHandler); } - private void initializeDocumentManager(@NonNull final RootConfig rootConfig, @NonNull EmbeddedDocumentFactory embeddedDocumentFactory) { - rootConfig.setDocumentManager(embeddedDocumentFactory, mMainHandler); + private void initializeDocumentManager(@NonNull final RootConfig rootConfig, + @NonNull EmbeddedDocumentFactory embeddedDocumentFactory, + @NonNull ITelemetryProvider telemetryProvider) { + rootConfig.setDocumentManager(embeddedDocumentFactory, mMainHandler, telemetryProvider); } /** @@ -220,8 +233,15 @@ static APLController renderDocument( @NonNull final InflationErrorCallback errorCallback, final boolean disableAsyncInflate, @NonNull final DocumentSession documentSession) { + Session aplSession = rootConfig.getSession(); + aplSession.setAPLListener(aplLayout.getDTView()); + aplLayout.setAPLSession(aplSession); + aplSession.write(Session.LogEntryLevel.INFO, Session.LogEntrySource.VIEW, "----- Rendering Document -----"); + final ExtensionRegistrar registrar = rootConfig.getExtensionProvider(); final ExtensionMediator mediator = (registrar != null) ? ExtensionMediator.create(registrar, documentSession) : null; + final UserPerceivedFatalReporter upfReporter = new UserPerceivedFatalReporter(options.getUserPerceivedFatalCallback()); + if (mediator != null) { rootConfig.extensionMediator(mediator); } @@ -229,17 +249,18 @@ static APLController renderDocument( aplLayout.setAgentName(rootConfig); final APLController aplController = new APLController(contentCreator); + final ITelemetryProvider telemetryProvider = options.getTelemetryProvider(); if(options.getEmbeddedDocumentFactory() != null) { - aplController.initializeDocumentManager(rootConfig, options.getEmbeddedDocumentFactory()); + aplController.initializeDocumentManager(rootConfig, options.getEmbeddedDocumentFactory(), telemetryProvider); } - if (!isInitialized(options.getTelemetryProvider())) { + if (!isInitialized(telemetryProvider)) { fail(options.getTelemetryProvider(), ITelemetryProvider.RENDER_DOCUMENT, Type.TIMER); aplController.onDocumentFinish(); errorCallback.onError(new APLException("The APLController must be initialized.", new IllegalStateException())); + upfReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.APL_INITIALIZATION_FAILURE); } - final ITelemetryProvider telemetryProvider = options.getTelemetryProvider(); aplController.mRenderDocumentTimer = telemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, TAG + "." + ITelemetryProvider.RENDER_DOCUMENT, Type.TIMER); long seed = SystemClock.elapsedRealtime() - startTime; telemetryProvider.startTimer(aplController.mRenderDocumentTimer, TimeUnit.MILLISECONDS, seed); @@ -275,7 +296,7 @@ public void onComplete(Content content) { final Runnable runnable = () -> { try { - final RootContext rootContext = RootContext.create(viewportMetrics, content, rootConfig, options, presenter); + final RootContext rootContext = RootContext.create(viewportMetrics, content, rootConfig, options, presenter, upfReporter); if (aplController.mIsFinished.get()) { Log.i(TAG, "Finished while creating RootContext. Aborting"); @@ -284,7 +305,8 @@ public void onComplete(Content content) { presenter.onDocumentRender(rootContext); } catch (Exception e) { - handleRenderingError(e, aplController, telemetryProvider, errorCallback); + upfReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.ROOT_CONTEXT_CREATION_FAILURE); + handleRenderingError(e, aplController, telemetryProvider, errorCallback, aplSession); } }; @@ -323,7 +345,7 @@ public Runnable onSuccess() { final Runnable runnable = () -> { try { - final RootContext rootContext = RootContext.create(viewportMetrics, content, rootConfig, options, presenter); + final RootContext rootContext = RootContext.create(viewportMetrics, content, rootConfig, options, presenter, upfReporter); if (aplController.mIsFinished.get()) { Log.i(TAG, "Finished while creating RootContext. Aborting"); @@ -332,7 +354,8 @@ public Runnable onSuccess() { presenter.onDocumentRender(rootContext); } catch (Exception e) { - handleRenderingError(e, aplController, telemetryProvider, errorCallback); + upfReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.ROOT_CONTEXT_CREATION_FAILURE); + handleRenderingError(e, aplController, telemetryProvider, errorCallback, aplSession); } }; @@ -347,12 +370,17 @@ public Runnable onSuccess() { @Override public Runnable onFailure() { - return () -> handleRenderingError( - new APLException("Required extensions failed to load.", new IllegalStateException()), - aplController, - telemetryProvider, - errorCallback - ); + return () -> { + telemetryProvider.fail(extensionRegistrationMetric); + upfReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE); + handleRenderingError( + new APLException("Required extensions failed to load.", new IllegalStateException()), + aplController, + telemetryProvider, + errorCallback, + aplSession + ); + }; } } ); @@ -361,7 +389,8 @@ public Runnable onFailure() { @Override public void onError(Exception e) { - handleRenderingError(e, aplController, telemetryProvider, errorCallback); + upfReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.CONTENT_CREATION_FAILURE); + handleRenderingError(e, aplController, telemetryProvider, errorCallback, aplSession); } @Override @@ -377,7 +406,7 @@ public void onPackageLoaded(Content content) { mediator.registerImageFilters(registrar, content, rootConfig); } } - }, rootConfig); + }, rootConfig, aplLayout.getDTView().getDTNetworkRequestHandler()); if (content != null) { aplController.mContent = content; aplLayout.addMetricsReadyListener(viewportMetrics -> { @@ -408,10 +437,12 @@ public void onPackageLoaded(Content content) { return aplController; } - private static void handleRenderingError(Exception e, APLController aplController, ITelemetryProvider telemetryProvider, InflationErrorCallback errorCallback) { + private static void handleRenderingError(Exception e, APLController aplController, ITelemetryProvider telemetryProvider, + InflationErrorCallback errorCallback, Session aplSession) { telemetryProvider.fail(aplController.mRenderDocumentTimer); aplController.onDocumentFinish(); errorCallback.onError(e); + aplSession.write(Session.LogEntryLevel.ERROR, Session.LogEntrySource.VIEW, "Document Failed: " + e.getMessage()); } /** @@ -753,6 +784,12 @@ private void finishDocumentInternal(RootContext rootContext) { if (mediator != null) { mediator.enable(false); } + ExtensionRegistrar registrar = rootContext.getRootConfig().getExtensionProvider(); + if (registrar != null) { + //closing on V1 connections since V1 connections are created per document + // however V2 connections are shared and cannot be closed upfront. + registrar.closeAllRemoteV1Connections(); + } } private void executeOnMyThread(final Consumer rootContextFunction) { diff --git a/apl/src/main/java/com/amazon/apl/android/APLLayout.java b/apl/src/main/java/com/amazon/apl/android/APLLayout.java index a06f2686..e3b60062 100644 --- a/apl/src/main/java/com/amazon/apl/android/APLLayout.java +++ b/apl/src/main/java/com/amazon/apl/android/APLLayout.java @@ -5,13 +5,12 @@ package com.amazon.apl.android; -import android.content.BroadcastReceiver; import android.content.Context; -import android.content.Intent; -import android.content.IntentFilter; import android.content.res.TypedArray; import android.graphics.Canvas; import android.graphics.Color; +import android.graphics.Paint; +import android.graphics.Path; import android.graphics.Shader; import android.graphics.drawable.ColorDrawable; import android.graphics.drawable.Drawable; @@ -63,6 +62,12 @@ import com.amazon.apl.android.utils.TransformUtils; import com.amazon.apl.android.views.APLAbsoluteLayout; import com.amazon.apl.android.views.APLImageView; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.enums.ViewState; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.RequestStatus; +import com.amazon.apl.devtools.views.IAPLView; import com.amazon.apl.enums.ComponentType; import com.amazon.apl.enums.FocusDirection; import com.amazon.apl.enums.PropertyKey; @@ -70,9 +75,12 @@ import com.amazon.apl.enums.ScreenShape; import com.amazon.apl.enums.UpdateType; import com.amazon.apl.enums.ViewportMode; +import com.amazon.apl.viewhost.internal.DocumentState; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; import java.util.ArrayList; -import java.util.Calendar; import java.util.Collections; import java.util.HashMap; import java.util.HashSet; @@ -88,6 +96,8 @@ import static com.amazon.apl.android.providers.ITelemetryProvider.APL_DOMAIN; import static com.amazon.apl.android.providers.ITelemetryProvider.Type.COUNTER; import static com.amazon.apl.android.providers.ITelemetryProvider.Type.TIMER; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_HIDE_HIGHLIGHT; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_HIGHLIGHT_COMPONENT; /** * A FrameLayout that is specified by a set of APL documents for layout, style, and data. @@ -96,7 +106,7 @@ * styled, properties are set, and then added to this ViewGroup. */ @UiThread -public class APLLayout extends FrameLayout implements AccessibilityManager.AccessibilityStateChangeListener { +public class APLLayout extends FrameLayout implements AccessibilityManager.AccessibilityStateChangeListener, IAPLView { private static final String TAG = "APLLayout"; private static final boolean DEBUG = BuildConfig.DEBUG; @@ -106,6 +116,12 @@ public class APLLayout extends FrameLayout implements AccessibilityManager.Acces @Nullable private RootContext mRootContext; + // The apl dev tools + private static final String DT_VIEW_NAME = "main"; + + private ViewTypeTarget mDTView; + private Session mAPLSession; + // Latest Configuration Change @Nullable private ConfigurationChange mLatestConfigChange; @@ -179,16 +195,6 @@ enum Theme { private AccessibilityManager mAccessibilityManager; private boolean mIsAccessibilityActive = false; private boolean mHandleConfigurationChangeOnSizeChanged = false; - private final BroadcastReceiver mTimezoneReceiver = new BroadcastReceiver() { - @Override - public void onReceive(Context context, Intent intent) { - if (mRootContext != null) { - Calendar now = Calendar.getInstance(); - long offset = now.get(Calendar.ZONE_OFFSET) + now.get(Calendar.DST_OFFSET); - mRootContext.setLocalTimeAdjustment(offset); - } - } - }; /** * Trace points for a given agent name. @@ -197,6 +203,12 @@ public void onReceive(Context context, Intent intent) { private boolean mIsReinflating = false; + /** + * Used for highlighting a document component. + */ + private final Canvas mCanvas = new Canvas(); + private final Path mPath = new Path(); + public APLLayout(@NonNull Context context) { this(context, true); } @@ -224,6 +236,14 @@ public APLLayout(@NonNull Context context, AttributeSet attrs, int defStyle) { init(context, null, checkInitialization); } + /** + * To be used only for testing. + * @param presenter + */ + public void setAplViewPresenterForTesting(IAPLViewPresenter presenter) { + mAplViewPresenter = presenter; + } + /** * Initialize the layout. * @@ -300,6 +320,26 @@ private void init(@NonNull final Context context, @Nullable final AttributeSet a new ColorDrawable(Color.TRANSPARENT) }); mBackgroundDrawable.setId(1, DOCUMENT_BACKGROUND_LAYER_ID); + + initializeDeveloperTools(); + } + + private void initializeDeveloperTools() { + mDTView = new ViewTypeTarget(DT_VIEW_NAME); + mDTView.setView(this); + } + + public void onNetworkRequestWillBeSent(int requestId, double timestamp, + String documentURL, String type) { + mDTView.onNetworkRequestWillBeSent(requestId, timestamp, documentURL, type); + } + + public void onNetworkLoadingFailed(int requestId, double timestamp) { + mDTView.onNetworkLoadingFailed(requestId, timestamp); + } + + public void onNetworkLoadingFinished(int requestId, double timestamp, int encodedDataLength) { + mDTView.onNetworkLoadingFinished(requestId, timestamp, encodedDataLength); } @Override @@ -307,8 +347,9 @@ protected void onAttachedToWindow() { Log.i(TAG, "super.onAttachedToWindow() called"); super.onAttachedToWindow(); Log.i(TAG, "super.onAttachedToWindow() completed successfully"); - registerTimeZoneReceiver(); + registerAccessibilityListener(); + mDTView.registerToCatalog(); } @Override @@ -316,45 +357,126 @@ protected void onDetachedFromWindow() { Log.i(TAG, "super.onDetachedFromWindow() called"); super.onDetachedFromWindow(); Log.i(TAG, "super.onDetachedFromWindow() completed successfully"); - unregisterTimeZoneReceiver(); mAccessibilityManager.removeAccessibilityStateChangeListener(this); + mDTView.unregisterToCatalog(); + } + + @VisibleForTesting + void setAccessibilityManager(AccessibilityManager accessibilityManager) { + mAccessibilityManager = accessibilityManager; + } + + private void registerAccessibilityListener() { + onAccessibilityStateChanged(mAccessibilityManager.isEnabled()); + mAccessibilityManager.addAccessibilityStateChangeListener(this); } - private void unregisterTimeZoneReceiver() { - Log.i(TAG, "Unregistering mTimezoneReceiver from context: " + mTimezoneReceiver.toString()); + @Override + public void startFrameMetricsRecording(int id, IDTCallback callback) { + if (mRootContext == null) { + DTError error = DTError.NO_DOCUMENT_RENDERED; + mDTView.post(() -> callback.execute(RequestStatus.failed(id, error))); + return; + } + + mRootContext.startFrameMetricsRecording(); + mDTView.post(() -> callback.execute(RequestStatus.successful())); + } + + @Override + public void stopFrameMetricsRecording(int id, IDTCallback> callback) { + if (mRootContext == null) { + Log.w(TAG, "No root context: "); + callback.execute(RequestStatus.failed(id, DTError.NO_DOCUMENT_RENDERED)); + return; + } + try { - getContext().unregisterReceiver(mTimezoneReceiver); - } catch (final Exception e) { - Log.wtf(TAG, "Error while unregistering mTimezoneReceiver", e); + List frameMetrics = mRootContext.stopFrameMetricsRecording(); + mDTView.post(() -> callback.execute(frameMetrics, RequestStatus.successful())); + } catch (JSONException e) { + mDTView.post(() -> callback.execute(RequestStatus.failed(id, DTError.METHOD_FAILURE))); } - Log.i(TAG, "Unregistered mTimezoneReceiver successfully"); } - private void registerTimeZoneReceiver() { - Log.i(TAG, "registerTimeZoneReceiver() called"); - // Update current time if we're being attached. - if (mRootContext != null) { - Calendar now = Calendar.getInstance(); - long offset = now.get(Calendar.ZONE_OFFSET) + now.get(Calendar.DST_OFFSET); - mRootContext.setLocalTimeAdjustment(offset); + @Override + public void documentCommandRequest(int id, String method, JSONObject params, IDTCallback callback) { + if (mRootContext == null) { + Log.w(TAG, "No root context: " + method); + mDTView.post(() -> callback.execute(RequestStatus.failed(id, DTError.NO_DOCUMENT_RENDERED))); + return; + } + + if (method.equals(DOCUMENT_HIGHLIGHT_COMPONENT.toString())) { + try { + String componentId = params.getString("componentId"); + if (!componentId.isEmpty()) { + Component component = mRootContext.getOrInflateComponentWithUniqueId(componentId); + drawPathFromPoints(component.getPathCoordinates()); + } else { + clearDrawing(); + } + mDTView.post(() -> callback.execute(RequestStatus.successful())); + } catch (JSONException e) { + Log.e(TAG, "Could not parse json for params: " + params); + mDTView.post(() -> callback.execute(RequestStatus.failed(id, DTError.METHOD_FAILURE))); + } + } else if (method.equals(DOCUMENT_HIDE_HIGHLIGHT.toString())) { + clearDrawing(); + mDTView.post(() -> callback.execute(RequestStatus.successful())); + } else { + String result = mRootContext.documentCommandRequest(method, params.toString()); + mDTView.post(() -> callback.execute(result, RequestStatus.successful())); } - Log.i(TAG, "Adding timezone actions on filter"); - IntentFilter filter = new IntentFilter(); - filter.addAction(Intent.ACTION_TIMEZONE_CHANGED); - filter.addAction(Intent.ACTION_TIME_CHANGED); - Log.i(TAG, "Registering mTimezoneReceiver on context: " + mTimezoneReceiver.toString()); - getContext().registerReceiver(mTimezoneReceiver, filter); - Log.i(TAG, "Registered mTimezoneReceiver successfully"); } - @VisibleForTesting - void setAccessibilityManager(AccessibilityManager accessibilityManager) { - mAccessibilityManager = accessibilityManager; + private void drawPathFromPoints(ArrayList componentPoints) { + IMetricsTransform metricsTransform = mRootContext.getMetricsTransform(); + + float[] p1 = componentPoints.get(0); + float[] p2 = componentPoints.get(1); + float[] p3 = componentPoints.get(2); + float[] p4 = componentPoints.get(3); + + // Create a path from 4 points. + mPath.moveTo(metricsTransform.toViewhost(p1[0]), + metricsTransform.toViewhost(p1[1])); + mPath.lineTo(metricsTransform.toViewhost((p2[0])), + metricsTransform.toViewhost(p2[1])); + mPath.lineTo(metricsTransform.toViewhost(p3[0]), + metricsTransform.toViewhost(p3[1])); + mPath.lineTo(metricsTransform.toViewhost(p4[0]), + metricsTransform.toViewhost(p4[1])); + mPath.lineTo(metricsTransform.toViewhost(p1[0]), + metricsTransform.toViewhost(p1[1])); + mPath.close(); + + this.dispatchDraw(mCanvas); } - private void registerAccessibilityListener() { - onAccessibilityStateChanged(mAccessibilityManager.isEnabled()); - mAccessibilityManager.addAccessibilityStateChangeListener(this); + private void clearDrawing() { + mPath.reset(); + } + + /** + * @return the {@link ViewTypeTarget} instance corresponding to the view. + */ + public ViewTypeTarget getDTView() { + return mDTView; + } + + /** + * Sets the APLSession. + * @param aplSession The {@link Session} associated with the document being rendered. + */ + public void setAPLSession(Session aplSession) { + mAPLSession = aplSession; + } + + private void writeAPLSessionInfoLog(String message) { + if (mAPLSession != null) { + mAPLSession.write(Session.LogEntryLevel.INFO, Session.LogEntrySource.VIEW, message); + } } /** @@ -429,14 +551,15 @@ void onDocumentRender(@NonNull RootContext rootContext) { // Update the view inflateViews(); + mDTView.setAPLOptions(mRootContext.getOptions()); } void inflateViews() { mViewsNeedLayout.set(true); - if (mRootContext.isAutoSize()) { + if (mRootContext.isAutoSizeLayoutPending()) { resize(); } - if (Looper.myLooper() == Looper.getMainLooper() && !mRootContext.isAutoSize()) { + if (Looper.myLooper() == Looper.getMainLooper() && !mRootContext.isAutoSizeLayoutPending()) { invalidate(); requestLayout(); } else { @@ -454,6 +577,7 @@ private void resize() { params.height = mRootContext.getAutoSizedHeight(); params.width = mRootContext.getAutoSizedWidth(); this.setLayoutParams(params); + writeAPLSessionInfoLog("Resizing to size= " + params.width + "x" + params.height); } /** @@ -488,7 +612,7 @@ public IAPLViewPresenter getPresenter() { * Add a callback for when metrics are ready. * @param metricsConsumer function to execute when the view is measured */ - void addMetricsReadyListener(Consumer metricsConsumer) { + public void addMetricsReadyListener(Consumer metricsConsumer) { if (mIsMeasureValid && !isLayoutRequested()) { if (Thread.currentThread() == Looper.getMainLooper().getThread()) { metricsConsumer.accept(mAplViewPresenter.getOrCreateViewportMetrics()); @@ -529,6 +653,16 @@ private ViewportMetrics createMetrics() { */ boolean mIsMeasureValid = false; + /** + * Method to update the DTView with the latest {@link ViewState} change. + * + * @param viewState The new {@link ViewState}. + */ + public void changeViewState(ViewState viewState) { + double currentTimeStamp = System.currentTimeMillis() / 1000D; + mDTView.post(() -> mDTView.onViewStateChange(viewState, currentTimeStamp)); + } + /** * Listens to changes in APL RootContext and updates the view hierarchy. */ @@ -626,6 +760,13 @@ public void applyAllProperties(Component component, View view) { Log.e(TAG, "adapter is null"); return; } + + // Since the ImageViewAdapter is a singleton. And we can create multiple APLLayout, + // then we need to update the DTNetworkRequestHandler before loading the image to + // know from which View is coming from. + if (adapter instanceof ImageViewAdapter) { + ((ImageViewAdapter) adapter).setDTNetworkRequestHandler(mDTView.getDTNetworkRequestHandler()); + } adapter.applyAllProperties(component, view); view.invalidate(); } @@ -640,6 +781,12 @@ public void requestLayout(Component component) { Log.e(TAG, "adapter is null"); return; } + // Since the ImageViewAdapter is a singleton. And we can create multiple APLLayout, + // then we need to update the DTNetworkRequestHandler before loading the image to + // know from which View is coming from. + if (adapter instanceof ImageViewAdapter) { + ((ImageViewAdapter) adapter).setDTNetworkRequestHandler(mDTView.getDTNetworkRequestHandler()); + } adapter.requestLayout(component, view); view.invalidate(); } @@ -689,13 +836,20 @@ public boolean onKeyPress(@NonNull final KeyEvent event) { public void preDocumentRender() { mIsRenderStartTimeSet = true; mRenderStartTime = SystemClock.elapsedRealtime(); + changeViewState(ViewState.LOADED); + writeAPLSessionInfoLog("Document Loaded"); } @Override public void onDocumentRender(RootContext rootContext) { // TODO We should consider an api that handles this for our clients. For now this is an // error scenario. - if (mRootContext != null) { + /** + * This code block makes sure that the previous document is finished before rendering a new one. This condition is applicable in legacy flow. + * However, for the unified APIs we have a usecase of restore document where the document is not finished before adding to the backstack + * Hence adding this check to not be executed for any call to render a document using Viewhost.java + **/ + if (mRootContext != null && mRootContext.getDocumentHandle() == null) { String message = "Trying to render a document prior to finishing old document!"; if (BuildConfig.DEBUG) { throw new AssertionError(message); @@ -724,11 +878,14 @@ public void onDocumentRender(RootContext rootContext) { } rootContext.notifyContext(); + changeViewState(ViewState.READY); + writeAPLSessionInfoLog("Document Ready"); } public void reinflate() { mIsReinflating = true; APLLayout.this.inflateViews(); + writeAPLSessionInfoLog("Document Reinflated"); } @Override @@ -742,6 +899,7 @@ public void onDocumentPaused() { for (IDocumentLifecycleListener documentLifecycleListener : mDocumentLifecycleListeners) { documentLifecycleListener.onDocumentPaused(); } + writeAPLSessionInfoLog("Document Paused"); } @Override @@ -749,6 +907,7 @@ public void onDocumentResumed() { for (IDocumentLifecycleListener documentLifecycleListener : mDocumentLifecycleListeners) { documentLifecycleListener.onDocumentResumed(); } + writeAPLSessionInfoLog("Document Resumed"); } @Override @@ -769,6 +928,12 @@ public void onDocumentDisplayed() { for (IDocumentLifecycleListener documentLifecycleListener : mDocumentLifecycleListeners) { documentLifecycleListener.onDocumentDisplayed(documentDisplayedTime); } + if (mRootContext != null && mRootContext.getDocumentHandle() != null) { + mRootContext.getDocumentHandle().setDocumentState(DocumentState.DISPLAYED); + } + + changeViewState(ViewState.INFLATED); + writeAPLSessionInfoLog("Document Inflated"); } @Override @@ -833,6 +998,9 @@ public void onDocumentFinish() { documentLifecycleListener.onDocumentFinish(); } mDocumentLifecycleListeners.clear(); + + changeViewState(ViewState.EMPTY); + mDTView.cleanup(); } @Override @@ -941,6 +1109,11 @@ public void onClick(View view) { } } + @Override + public boolean isHardwareAccelerationForVectorGraphicsEnabled() { + return mRootContext != null ? mRootContext.getRenderingContext().isHardwareAccelerationForVectorGraphicsEnabled() : false; + } + @Override public void onChildViewAdded(View parent, View child) { // no-op @@ -1117,6 +1290,9 @@ protected void onLayout(boolean changed, int l, int t, int r, int b) { protected void dispatchDraw(Canvas canvas) { try { super.dispatchDraw(canvas); + if (!mPath.isEmpty()){ + drawPathOnCanvas(canvas); + } } finally { if (mViewsNeedDisplay.getAndSet(false)) { mAplViewPresenter.onDocumentDisplayed(); @@ -1124,6 +1300,20 @@ protected void dispatchDraw(Canvas canvas) { } } + private void drawPathOnCanvas(Canvas canvas) { + invalidate(); // invalidate current canvas drawing cache. + Paint paint = new Paint(); + paint.setColor(Color.YELLOW); + paint.setStyle(Paint.Style.STROKE); + paint.setStrokeWidth(20); + canvas.drawPath(mPath, paint); + + paint.setColor(Color.GRAY); + paint.setStyle(Paint.Style.FILL); + paint.setAlpha(150); + canvas.drawPath(mPath, paint); + } + @NonNull @Override protected FrameLayout.LayoutParams generateDefaultLayoutParams() { @@ -1303,7 +1493,7 @@ public void setHandleConfigurationChangeOnSizeChanged(boolean handleConfiguratio @Override protected void onSizeChanged(int w, int h, int oldw, int oldh) { super.onSizeChanged(w, h, oldw, oldh); - if (mHandleConfigurationChangeOnSizeChanged && mRootContext != null && !mRootContext.isAutoSize()) { + if (mHandleConfigurationChangeOnSizeChanged && mRootContext != null && !mRootContext.isAutoSizeLayoutPending()) { Log.i(TAG, "Handle configuration change onSizeChanged"); try { handleConfigurationChange( diff --git a/apl/src/main/java/com/amazon/apl/android/APLOptions.java b/apl/src/main/java/com/amazon/apl/android/APLOptions.java index 3cbec8ca..251a2ea1 100644 --- a/apl/src/main/java/com/amazon/apl/android/APLOptions.java +++ b/apl/src/main/java/com/amazon/apl/android/APLOptions.java @@ -28,15 +28,18 @@ import com.amazon.apl.android.dependencies.IScreenLockListener; import com.amazon.apl.android.dependencies.ISendEventCallback; import com.amazon.apl.android.dependencies.ISendEventCallbackV2; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; import com.amazon.apl.android.dependencies.IViewportSizeUpdateCallback; import com.amazon.apl.android.dependencies.IVisualContextListener; import com.amazon.apl.android.dependencies.impl.DefaultUriSchemeValidator; +import com.amazon.apl.android.dependencies.impl.NoOpUserPerceivedFatalCallback; import com.amazon.apl.android.extension.IExtensionRegistration; import com.amazon.apl.android.media.RuntimeMediaPlayerFactory; import com.amazon.apl.android.providers.AbstractMediaPlayerProvider; import com.amazon.apl.android.providers.IDataRetriever; import com.amazon.apl.android.providers.IDataRetrieverProvider; import com.amazon.apl.android.providers.IImageLoaderProvider; +import com.amazon.apl.android.providers.ILocalTimeOffsetProvider; import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.providers.ITtsPlayerProvider; import com.amazon.apl.android.providers.impl.GlideImageLoaderProvider; @@ -102,6 +105,9 @@ public abstract class APLOptions { @Nullable public abstract IImageProcessor getImageProcessor(); + @Nullable + public abstract ILocalTimeOffsetProvider getLocalTimeOffsetProvider(); + // Callbacks public abstract IOnAplFinishCallback getOnAplFinishCallback(); public abstract IOpenUrlCallback getOpenUrlCallback(); @@ -144,6 +150,8 @@ public abstract class APLOptions { public abstract IClockProvider getAplClockProvider(); + public abstract IUserPerceivedFatalCallback getUserPerceivedFatalCallback(); + /** * @return options that are {@link IDocumentLifecycleListener}s. */ @@ -189,7 +197,8 @@ public void registerExtensions(@NonNull Content content, @NonNull RootConfig roo .contentDataRetriever((request, successCallback, failureCallback) -> failureCallback.onFailure(request, "Content datasources not implemented.")) .avgRetriever((request, successCallback, failureCallback) -> failureCallback.onFailure(request, "AVG source not implemented.")) .embeddedDocumentFactory(new NoOpEmbeddedDocumentFactory()) - .viewportSizeUpdateCallback((width, height) ->{}); + .viewportSizeUpdateCallback((width, height) ->{}) + .userPerceivedFatalCallback(new NoOpUserPerceivedFatalCallback()); } @AutoValue.Builder @@ -234,6 +243,13 @@ public abstract static class Builder { */ public abstract Builder imageProcessor(IImageProcessor provider); + /** + * Required for the local time to transition with timezones or daylight savings. + * @param provider the local time offset provider. + * @return this builder + */ + public abstract Builder localTimeOffsetProvider(ILocalTimeOffsetProvider provider); + /** * Required to support FinishEvents. * Defaults to no-op @@ -410,6 +426,14 @@ public Builder sendEventCallback(ISendEventCallback callback) { */ public abstract Builder viewhost(Viewhost viewhost); + /** + * Callback for reporting UPF events to Runtime. + * + * @param userPerceivedFatalCallback a callback for UPF + * @return this builder + */ + public abstract Builder userPerceivedFatalCallback(IUserPerceivedFatalCallback userPerceivedFatalCallback); + /** * Builds the options for this document. * @return the {@link APLOptions} diff --git a/apl/src/main/java/com/amazon/apl/android/Component.java b/apl/src/main/java/com/amazon/apl/android/Component.java index 2685dedb..e3dcac6d 100644 --- a/apl/src/main/java/com/amazon/apl/android/Component.java +++ b/apl/src/main/java/com/amazon/apl/android/Component.java @@ -15,6 +15,7 @@ import android.util.Log; import com.amazon.apl.android.primitive.AccessibilityActions; +import com.amazon.apl.android.primitive.AccessibilityAdjustableRange; import com.amazon.apl.android.primitive.Dimension; import com.amazon.apl.android.primitive.Rect; import com.amazon.apl.android.providers.ITelemetryProvider; @@ -308,6 +309,14 @@ public final String getAccessibilityLabel() { return mProperties.getString(PropertyKey.kPropertyAccessibilityLabel); } + /** + * @return Voice-over will read this string along with accessibility label when the user selects this component + */ + @Nullable + public final String getAccessibilityAdjustableValue() { + return mProperties.getString(PropertyKey.kPropertyAccessibilityAdjustableValue); + } + /** * @return Programmatic equivalents for complex touch interactions */ @@ -316,6 +325,14 @@ public final AccessibilityActions getAccessibilityActions() { return mProperties.getAccessibilityActions(PropertyKey.kPropertyAccessibilityActions); } + /** + * @return Provides range information to accessibility node for Voice-over. + */ + @Nullable + public final AccessibilityAdjustableRange getAccessibilityAdjustableRange() { + return mProperties.getAccessibilityAdjustableRange(PropertyKey.kPropertyAccessibilityAdjustableRange); + } + /** * @return If true, this component has the checked state set. */ @@ -571,6 +588,42 @@ public boolean hasProperty(PropertyKey propertyKey) { return mProperties.hasProperty(propertyKey); } + /** + * Used to get the component path coordinates. + * For Document Command Request Highlight only. + * + * @return The path coordinates containing 4 points. + */ + public ArrayList getPathCoordinates() { + float calculatedWidth = getCalculatedWidth(); + float calculatedHeight = getCalculatedHeight(); + + float[] p1 = getGlobalPointCoordinates(0, 0); + float[] p2 = getGlobalPointCoordinates(calculatedWidth, 0); + float[] p3 = getGlobalPointCoordinates(calculatedWidth,calculatedHeight); + float[] p4 = getGlobalPointCoordinates(0, calculatedHeight); + + ArrayList pathCoordinates = new ArrayList<>(); + pathCoordinates.add(p1); + pathCoordinates.add(p2); + pathCoordinates.add(p3); + pathCoordinates.add(p4); + + return pathCoordinates; + } + + private float getCalculatedWidth() { + return nGetCalculatedWidth(getNativeHandle()); + } + + private float getCalculatedHeight() { + return nGetCalculatedHeight(getNativeHandle()); + } + + private float[] getGlobalPointCoordinates(float pointA, float pointB) { + return nGetGlobalPointCoordinates(getNativeHandle(), pointA, pointB); + } + @Deprecated @VisibleForTesting public RootContext getRootContext() { @@ -611,6 +664,12 @@ public RootContext getRootContext() { private static native boolean nCheckDirty(long nativeHandle); + private static native float nGetCalculatedWidth(long nativeHandle); + + private static native float nGetCalculatedHeight(long nativeHandle); + + private static native float[] nGetGlobalPointCoordinates(long nativeHandle, float pointA, float pointB); + public String toString() { return "{type: " + getComponentType() + ", uid: " + getComponentId() + ", id: " + getId() + ", parent: " + getParentId() + "}"; } diff --git a/apl/src/main/java/com/amazon/apl/android/Content.java b/apl/src/main/java/com/amazon/apl/android/Content.java index 14c93585..716f84e4 100644 --- a/apl/src/main/java/com/amazon/apl/android/Content.java +++ b/apl/src/main/java/com/amazon/apl/android/Content.java @@ -9,6 +9,7 @@ import android.os.Handler; import android.os.Looper; import android.os.SystemClock; +import android.text.TextUtils; import android.util.Log; import androidx.annotation.NonNull; @@ -22,6 +23,8 @@ import com.amazon.apl.android.providers.impl.NoOpTelemetryProvider; import com.amazon.apl.android.scaling.ViewportMetrics; import com.amazon.apl.android.utils.ColorUtils; +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.common.BoundObject; import com.amazon.apl.enums.GradientType; import com.google.auto.value.AutoValue; @@ -70,6 +73,9 @@ public final class Content extends BoundObject { @NonNull private final Handler mMainHandler; + @Nullable + private final IDTNetworkRequestHandler mDTNetworkRequestHandler; + private IPackageLoader mPackageLoader; private IContentDataRetriever mDataRetriever; private CallbackV2 mCallback; @@ -97,7 +103,8 @@ public ContentException(String message) { private Content(@NonNull ITelemetryProvider telemetryProvider, @Nullable IPackageLoader packageLoader, @Nullable IContentDataRetriever dataRetriever, - long entryTime) { + long entryTime, + @Nullable IDTNetworkRequestHandler dtNetworkRequest) { // private constructor mImportRequests = Collections.newSetFromMap(new ConcurrentHashMap<>()); mParameters = Collections.newSetFromMap(new ConcurrentHashMap<>()); @@ -112,6 +119,7 @@ private Content(@NonNull ITelemetryProvider telemetryProvider, mDataRetriever = dataRetriever; setPackageLoader(packageLoader); mMainHandler = new Handler(Looper.getMainLooper()); + mDTNetworkRequestHandler = dtNetworkRequest; } /** @@ -182,7 +190,7 @@ public void onPackageLoaded(Content content) { * Construct the working Content object from a document that contains the apl 'mainTemplate'. * Also registers a callback to receive Content ImportRequests and data parameters. * - * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session)} instead to use the package loader from APLOptions + * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session, IDTNetworkRequestHandler)} instead to use the package loader from APLOptions * * @param mainTemplate The APL document containing the 'mainTemplate' tag. * @param callback A callback for Package and Data requests. @@ -192,14 +200,14 @@ public void onPackageLoaded(Content content) { @Deprecated public static Content create(@NonNull final String mainTemplate, final Callback callback) throws ContentException { long entryTime = SystemClock.elapsedRealtimeNanos(); // Must remain first for accurate telemetry! - return createContent(mainTemplate, null, callback, null, entryTime, null, new Session()); + return createContent(mainTemplate, null, callback, null, entryTime, null, new Session(), null); } /** * Construct the working Content object from a document that contains the apl 'mainTemplate'. * Also registers a callback to receive Content ImportRequests and data parameters. * - * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session)} instead to use the package loader from APLOptions + * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session, IDTNetworkRequestHandler)} instead to use the package loader from APLOptions * * @param mainTemplate The APL document containing the 'mainTemplate' tag. * @param aplOptions The APL options. @@ -210,13 +218,13 @@ public static Content create(@NonNull final String mainTemplate, final Callback public static Content create(@NonNull final String mainTemplate, @NonNull final APLOptions aplOptions, final Callback callback) throws ContentException { long entryTime = SystemClock.elapsedRealtimeNanos(); // Must remain first for accurate telemetry! Objects.requireNonNull(aplOptions); - return createContent(mainTemplate, aplOptions, callback, null, entryTime, null, new Session()); + return createContent(mainTemplate, aplOptions, callback, null, entryTime, null, new Session(), null); } /** * Construct the working Content object from a document that contains the apl 'mainTemplate'. * - * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session)} instead to use the package loader from APLOptions + * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session, IDTNetworkRequestHandler)} instead to use the package loader from APLOptions * * @param mainTemplate The main document. * @return A Content object based on the mainTemplate document. @@ -225,13 +233,13 @@ public static Content create(@NonNull final String mainTemplate, @NonNull final @NonNull public static Content create(final String mainTemplate) throws ContentException { long entryTime = SystemClock.elapsedRealtimeNanos(); // Must remain first for accurate telemetry! - return createContent(mainTemplate, null, null, null, entryTime, null, new Session()); + return createContent(mainTemplate, null, null, null, entryTime, null, new Session(), null); } /** * Construct the working Content object from a document that contains the apl 'mainTemplate'. * - * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session)} instead to use the package loader from APLOptions + * @deprecated use {@link #create(String, APLOptions, CallbackV2, Session, IDTNetworkRequestHandler)} instead to use the package loader from APLOptions * * @param mainTemplate The main document. * @param aplOptions The APL options. @@ -242,23 +250,24 @@ public static Content create(final String mainTemplate) throws ContentException public static Content create(final String mainTemplate, @NonNull final APLOptions aplOptions) throws ContentException { long entryTime = SystemClock.elapsedRealtimeNanos(); // Must remain first for accurate telemetry! Objects.requireNonNull(aplOptions); - return createContent(mainTemplate, aplOptions, null, null, entryTime, null, new Session()); + return createContent(mainTemplate, aplOptions, null, null, entryTime, null, new Session(), null); } /** * Construct the working Content object from a document that contains the apl 'mainTemplate'. * - * @param mainTemplate The main document. - * @param aplOptions The APL Options - * @param callback The callback for handling Content requests. - * @param session Experience logging session. - * @return A Content object if the maintemplate is valid, otherwise null. + * @param mainTemplate The main document. + * @param aplOptions The APL Options + * @param callback The callback for handling Content requests. + * @param session Experience logging session. + * @param dtNetworkRequest The IDTNetworkRequestHandler for notifying Dev Tools Clients of network events. + * @return A Content object if the maintemplate is valid, otherwise null. */ @Nullable - public static Content create(final String mainTemplate, @NonNull final APLOptions aplOptions, @NonNull final CallbackV2 callback, Session session) { + public static Content create(final String mainTemplate, @NonNull final APLOptions aplOptions, @NonNull final CallbackV2 callback, Session session, IDTNetworkRequestHandler dtNetworkRequest) { long entryTime = SystemClock.elapsedRealtimeNanos(); try { - return createContent(mainTemplate, aplOptions, null, callback, entryTime, null, session); + return createContent(mainTemplate, aplOptions, null, callback, entryTime, null, session, dtNetworkRequest); } catch (ContentException e) { callback.onError(e); return null; @@ -268,17 +277,18 @@ public static Content create(final String mainTemplate, @NonNull final APLOption /** * Construct the working Content object from a document that contains the apl 'mainTemplate'. * - * @param mainTemplate The main document. - * @param aplOptions The APL Options - * @param callback The callback for handling Content requests. - * @param rootConfig RootConfig - * @return A Content object if the maintemplate is valid, otherwise null. + * @param mainTemplate The main document. + * @param aplOptions The APL Options + * @param callback The callback for handling Content requests. + * @param rootConfig RootConfig + * @param dtNetworkRequest The IDTNetworkRequestHandler for notifying Dev Tools Clients of network events. + * @return A Content object if the maintemplate is valid, otherwise null. */ @Nullable - public static Content create(final String mainTemplate, @NonNull final APLOptions aplOptions, @NonNull final CallbackV2 callback, RootConfig rootConfig) { + public static Content create(final String mainTemplate, @NonNull final APLOptions aplOptions, @NonNull final CallbackV2 callback, RootConfig rootConfig, IDTNetworkRequestHandler dtNetworkRequest) { long entryTime = SystemClock.elapsedRealtimeNanos(); try { - return createContent(mainTemplate, aplOptions, null, callback, entryTime, rootConfig,(rootConfig != null && rootConfig.getSession() != null) ? rootConfig.getSession() : new Session()); + return createContent(mainTemplate, aplOptions, null, callback, entryTime, rootConfig,(rootConfig != null && rootConfig.getSession() != null) ? rootConfig.getSession() : new Session(), dtNetworkRequest); } catch (ContentException e) { callback.onError(e); return null; @@ -291,13 +301,14 @@ private static Content createContent(@NonNull final String mainTemplate, @Nullable final CallbackV2 callbackV2, long entryTime, final RootConfig rootConfig, - final Session session) throws ContentException { + final Session session, + final IDTNetworkRequestHandler dtNetworkRequestHandler) throws ContentException { if (mainTemplate.length() == 0) { throw new ContentException("Invalid document length."); } Content content = null; try { - content = new Content(getTelemetryProvider(aplOptions), getPackageLoader(aplOptions), getDataRetriever(aplOptions), entryTime); + content = new Content(getTelemetryProvider(aplOptions), getPackageLoader(aplOptions), getDataRetriever(aplOptions), entryTime, dtNetworkRequestHandler); content.setCallbacks(callbackV2, callback); content.importDocument(mainTemplate, rootConfig, session); } catch (Exception e) { @@ -447,6 +458,15 @@ private void notifyCallback(@SuppressWarnings("SameParameterValue") boolean noti if (notifyPackages) { for (ImportRequest importRequest : getRequestedPackages()) { final long startTime = SystemClock.elapsedRealtimeNanos(); + if (mDTNetworkRequestHandler != null) { + String source = importRequest.getSource(); + if (TextUtils.isEmpty(source)) { + source = IDTNetworkRequestHandler.getDefaultPackageUrl(importRequest.getPackageName(), importRequest.getVersion()); + } + if (IDTNetworkRequestHandler.isUrlRequest(source)) { + mDTNetworkRequestHandler.requestWillBeSent(importRequest.getRequestId(), SystemClock.elapsedRealtimeNanos(), source, DTNetworkRequestType.PACKAGE); + } + } mPackageLoader.fetch(importRequest, (ImportRequest request, APLJSONData result) -> this.handleImportSuccess(request, result, startTime), (ImportRequest request, String message) -> { @@ -455,6 +475,10 @@ private void notifyCallback(@SuppressWarnings("SameParameterValue") boolean noti request, duration, message)); + final String source = request.getSource(); + if (mDTNetworkRequestHandler != null && IDTNetworkRequestHandler.isUrlRequest(source)) { + mDTNetworkRequestHandler.loadingFailed(request.getRequestId(), SystemClock.elapsedRealtimeNanos()); + } }); } @@ -488,6 +512,11 @@ private void handleImportSuccess(ImportRequest request, APLJSONData result, fina Log.i(TAG, String.format("Package '%s' took %d milliseconds to download.", request.getPackageName(), duration)); + + final String source = request.getSource(); + if (mDTNetworkRequestHandler != null && IDTNetworkRequestHandler.isUrlRequest(source)) { + mDTNetworkRequestHandler.loadingFinished(request.getRequestId(), SystemClock.elapsedRealtimeNanos(), result.getSize()); + } invokeOnMyThread(() -> addPackage(request, result)); } @@ -517,7 +546,7 @@ private void tryAddPackage(ImportRequest request, APLJSONData result) { @SuppressWarnings("unused") private void coreRequestPackage(long nativeHandle, String source, String name, String version) { mTelemetryProvider.incrementCount(cContentImportRequests); - mImportRequests.add(new ImportRequest(nativeHandle, source, name, version)); + mImportRequests.add(new ImportRequest(nativeHandle, source, name, version, IDTNetworkRequestHandler.IdGenerator.generateId())); } /** @@ -615,14 +644,16 @@ public static class ImportRequest extends BoundObject { final public String version; private final ImportRef mImportRef; + private final int mRequestId; - ImportRequest(long nativeHandle, String source, String packageName, String version) { + ImportRequest(long nativeHandle, String source, String packageName, String version, int requestId) { bind(nativeHandle); // TODO replace these fields with native call to bound object this.source = source; this.packageName = packageName; this.version = version; mImportRef = ImportRef.create(packageName, version); + mRequestId = requestId; } @@ -644,6 +675,10 @@ public String getSource() { public ImportRef getImportRef() { return mImportRef; } + + public int getRequestId() { + return mRequestId; + } } /** diff --git a/apl/src/main/java/com/amazon/apl/android/DocumentSession.java b/apl/src/main/java/com/amazon/apl/android/DocumentSession.java index a5d33e47..fe05e8c6 100644 --- a/apl/src/main/java/com/amazon/apl/android/DocumentSession.java +++ b/apl/src/main/java/com/amazon/apl/android/DocumentSession.java @@ -5,6 +5,9 @@ package com.amazon.apl.android; +import android.os.Handler; + +import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.common.BoundObject; import java.util.ArrayList; @@ -22,6 +25,10 @@ public interface ISessionEndedCallback { private List mCallbacks; private IAPLController mController; + private DocumentHandle mHandle; + + private Handler mCoreWorker; + public static DocumentSession create() { return new DocumentSession(); } @@ -30,6 +37,10 @@ public void bind(IAPLController controller) { mController = controller; } + public void bind(DocumentHandle handle) { + mHandle = handle; + } + private DocumentSession() { mCallbacks = new ArrayList<>(); final long handle = nCreate(); @@ -44,7 +55,7 @@ public boolean hasEnded() { return nHasEnded(getNativeHandle()); } - void onSessionEnded(ISessionEndedCallback callback) { + public void onSessionEnded(ISessionEndedCallback callback) { mCallbacks.add(callback); } @@ -55,11 +66,21 @@ public void end() { mController.executeOnCoreThread(() -> nEnd(getNativeHandle())); } + if (mHandle != null && mCoreWorker != null) { + mCoreWorker.post( () -> { + nEnd(getNativeHandle()); + }); + } + for (ISessionEndedCallback callback : mCallbacks) { callback.onEnded(this); } } + public void setCoreWorker(Handler handler) { + mCoreWorker = handler; + } + private native long nCreate(); private native String nGetId(long sessionHandler_); private native boolean nHasEnded(long sessionHandler_); diff --git a/apl/src/main/java/com/amazon/apl/android/ExtensionMediator.java b/apl/src/main/java/com/amazon/apl/android/ExtensionMediator.java index 4d332f9c..1524feed 100644 --- a/apl/src/main/java/com/amazon/apl/android/ExtensionMediator.java +++ b/apl/src/main/java/com/amazon/apl/android/ExtensionMediator.java @@ -153,7 +153,7 @@ public void enable(boolean enabled) { * Invoked by a viewhost when the session associated with this mediator (if it has been * previously set) has ended. */ - void onSessionEnded() { + public void onSessionEnded() { nOnSessionEnded(getNativeHandle()); } diff --git a/apl/src/main/java/com/amazon/apl/android/IAPLViewPresenter.java b/apl/src/main/java/com/amazon/apl/android/IAPLViewPresenter.java index 16e34aa7..810a4160 100644 --- a/apl/src/main/java/com/amazon/apl/android/IAPLViewPresenter.java +++ b/apl/src/main/java/com/amazon/apl/android/IAPLViewPresenter.java @@ -260,4 +260,6 @@ public interface IAPLViewPresenter extends View.OnClickListener, IDocumentLifecy @Nullable APLTrace getAPLTrace(); + + boolean isHardwareAccelerationForVectorGraphicsEnabled(); } diff --git a/apl/src/main/java/com/amazon/apl/android/LiveArray.java b/apl/src/main/java/com/amazon/apl/android/LiveArray.java index 34c42fd4..cfa3ea50 100644 --- a/apl/src/main/java/com/amazon/apl/android/LiveArray.java +++ b/apl/src/main/java/com/amazon/apl/android/LiveArray.java @@ -9,8 +9,6 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.amazon.common.BoundObject; - import java.util.AbstractList; import java.util.ArrayList; import java.util.Collection; @@ -22,12 +20,12 @@ /** * Wrapper class for the APL Core LiveArray. - * + *

* To use a LiveArray, register the array with {@link RootConfig#liveData(String, LiveArray)}. * Make changes to the LiveArray from your UX thread. Changes in the LiveArray will * propagate through the APL data-binding context and show up on the screen. */ -public class LiveArray extends BoundObject implements List { +public class LiveArray extends LiveData implements List { /** * Store a copy of the array that is accessible to Java. A copy of this * array is stored in attached C++ object. The main purpose of the backing @@ -38,7 +36,7 @@ public class LiveArray extends BoundObject implements List { /** * Exception class for unrecoverable exceptions - * + *

* For most list overrides we try to perform the operation on the backing array * first. The backing array will throw standard List exceptions for all common * errors. Once the backing array has been modified we update the native APL object. @@ -349,7 +347,7 @@ public void add(Object o) { * Remove a range of objects from the array. * @param position The starting position at which to remove objects. * @param count The number of objects to remove. - * @return True if the position was valid and at least one object was removed. + * @throws LiveArrayException if the position was invalid */ public void removeRange(int position, int count) { if (position < 0 || count <= 0 || position + count > mBackingArray.size()) @@ -366,7 +364,7 @@ public void removeRange(int position, int count) { * Update a range of objects to new values. The position must fall within [0,size-array.length] * @param position The position to update. * @param collection An array of objects to update - * @return True if the position was valid and at least one object was updated. + * @throws LiveArrayException if the position was invalid */ public void setRange(int position, @NonNull Collection collection) { Object[] array = collection.toArray(); @@ -403,6 +401,26 @@ public Object innerGet(int position) { return nAt(getNativeHandle(), position); } + @Override + public boolean applyUpdates(List operations) { + try { + for (LiveArray.Update operation : operations) { + if (operation.getType().equals("insert")) { + add(operation.getIndex(), operation.getValue()); + } else if (operation.getType().equals("remove")) { + remove(operation.getIndex()); + } else if (operation.getType().equals("update")) { + set(operation.getIndex(), operation.getValue()); + } else + return false; + } + } catch (LiveArrayException ex) { + return false; + } + + return true; + } + private static native long nCreate(); private static native void nClear(long nativeHandle); private static native int nSize(long nativeHandle); diff --git a/apl/src/main/java/com/amazon/apl/android/LiveData.java b/apl/src/main/java/com/amazon/apl/android/LiveData.java new file mode 100644 index 00000000..b129377b --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/LiveData.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.android; + +import com.amazon.common.BoundObject; + +import java.util.List; + +import lombok.AllArgsConstructor; +import lombok.Getter; + +abstract public class LiveData extends BoundObject { + @AllArgsConstructor + @Getter + public static class Update { + private String type; + private int index; + private String key; + private Object value; + } + + public abstract boolean applyUpdates(List operations); +} diff --git a/apl/src/main/java/com/amazon/apl/android/LiveMap.java b/apl/src/main/java/com/amazon/apl/android/LiveMap.java index 9a4e72c7..2a10b8b6 100644 --- a/apl/src/main/java/com/amazon/apl/android/LiveMap.java +++ b/apl/src/main/java/com/amazon/apl/android/LiveMap.java @@ -9,38 +9,37 @@ import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; -import com.amazon.common.BoundObject; - import java.util.AbstractCollection; import java.util.AbstractSet; import java.util.Collection; import java.util.HashMap; import java.util.Iterator; +import java.util.List; import java.util.Map; import java.util.Objects; import java.util.Set; /** * Class supporting use of live maps in the top level data-binding context. - * + *

* LiveMaps are created and modified by Viewhost Runtimes. For example: - * + *

* // Before creating the root context: * LiveMap myMap = LiveMap.create(); * rootConfig.liveData("MyLiveMap", myMap); - * + *

* // After the root context has been created: * myMap.put("MyValue", "Changed string object"); - * + *

* Inside of the APL document the LiveMap may be used normally in data-binding * contexts. For example: - * + *

* { * "type": "Text", * "text": "The live object is currently '${MyLiveMap.MyValue}'" * } */ -public class LiveMap extends BoundObject implements Map { +public class LiveMap extends LiveData implements Map { private final Map mBackingMap = new HashMap<>(); /** @@ -53,7 +52,7 @@ static public LiveMap create() { /** * Construct a live map initialized by an Object map. - * + *

* Items are copied from the map. * * @param map The object map @@ -344,6 +343,25 @@ public Object innerGet(String key) { return nGet(getNativeHandle(), key); } + @Override + public boolean applyUpdates(List operations) { + try { + for (LiveMap.Update operation : operations) { + if (operation.getType().equals("set")) { + put(operation.getKey(), operation.getValue()); + } else if (operation.getType().equals("remove")) { + remove(operation.getKey()); + } else { + return false; + } + } + } catch (Exception ex) { + return false; + } + + return true; + } + private static native long nCreate(); private static native int nSize(long nativeHandle); private static native boolean nEmpty(long nativeHandle); diff --git a/apl/src/main/java/com/amazon/apl/android/NoOpComponent.java b/apl/src/main/java/com/amazon/apl/android/NoOpComponent.java deleted file mode 100644 index 04954f35..00000000 --- a/apl/src/main/java/com/amazon/apl/android/NoOpComponent.java +++ /dev/null @@ -1,21 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.android; - -import androidx.annotation.NonNull; - -/** - * No-op component that takes up space but otherwise does nothing - */ -public class NoOpComponent extends Component { - /** - * NoOp constructor - * {@inheritDoc} - */ - NoOpComponent(long nativeHandle, String componentId, @NonNull RenderingContext renderingContext) { - super(nativeHandle, componentId, renderingContext); - } -} diff --git a/apl/src/main/java/com/amazon/apl/android/PropertyMap.java b/apl/src/main/java/com/amazon/apl/android/PropertyMap.java index 8c23f172..5af8dbbf 100644 --- a/apl/src/main/java/com/amazon/apl/android/PropertyMap.java +++ b/apl/src/main/java/com/amazon/apl/android/PropertyMap.java @@ -11,6 +11,7 @@ import androidx.annotation.Nullable; import com.amazon.apl.android.primitive.AccessibilityActions; +import com.amazon.apl.android.primitive.AccessibilityAdjustableRange; import com.amazon.apl.android.primitive.BoundMediaSources; import com.amazon.apl.android.primitive.Dimension; import com.amazon.apl.android.primitive.Filters; @@ -200,6 +201,10 @@ public final AccessibilityActions getAccessibilityActions(K property) { return AccessibilityActions.create(getMapOwner(), property); } + public final AccessibilityAdjustableRange getAccessibilityAdjustableRange(K property) { + return AccessibilityAdjustableRange.create(getMapOwner(), property); + } + public final GraphicFilters getGraphicFilters(K property) { return GraphicFilters.create(getMapOwner(), property); } diff --git a/apl/src/main/java/com/amazon/apl/android/RenderingContext.java b/apl/src/main/java/com/amazon/apl/android/RenderingContext.java index 27cb64ae..5b9dabe8 100644 --- a/apl/src/main/java/com/amazon/apl/android/RenderingContext.java +++ b/apl/src/main/java/com/amazon/apl/android/RenderingContext.java @@ -54,6 +54,7 @@ public class RenderingContext { private final IExtensionEventCallback extensionEventCallback; private final APLTrace aplTrace; private final boolean mediaPlayerV2Enabled; + private final boolean isHardwareAccelerationForVectorGraphicsEnabled; private RenderingContext( int docVersion, @@ -71,7 +72,8 @@ private RenderingContext( IContentRetriever avgRetriever, IExtensionEventCallback extensionEventCallback, APLTrace aplTrace, - boolean mediaPlayerV2Enabled) { + boolean mediaPlayerV2Enabled, + boolean isHardwareAccelerationForVectorGraphicsEnabled) { this.docVersion = docVersion; this.metricsTransform = metricsTransform; this.textLayoutFactory = textLayoutFactory; @@ -88,6 +90,7 @@ private RenderingContext( this.extensionEventCallback = extensionEventCallback; this.aplTrace = aplTrace; this.mediaPlayerV2Enabled = mediaPlayerV2Enabled; + this.isHardwareAccelerationForVectorGraphicsEnabled = isHardwareAccelerationForVectorGraphicsEnabled; mShadowCache = new ShadowCache(); this.mPathCache = new WeakCache<>(); } @@ -166,6 +169,10 @@ public IContentRetriever getAvgRetriever() { public boolean isMediaPlayerV2Enabled() { return mediaPlayerV2Enabled; } + public boolean isHardwareAccelerationForVectorGraphicsEnabled() { + return isHardwareAccelerationForVectorGraphicsEnabled; + } + // Defaults are no-ops public static Builder builder() { return new Builder() @@ -200,6 +207,7 @@ public static final class Builder { private IExtensionEventCallback extensionEventCallback; private APLTrace aplTrace; private boolean isMediaPlayerV2Enabled; + private boolean isHardwareAccelerationForVectorGraphicsEnabled; Builder() { } @@ -285,6 +293,11 @@ public RenderingContext.Builder isMediaPlayerV2Enabled(boolean isMediaPlayerV2En return this; } + public RenderingContext.Builder isHardwareAccelerationForVectorGraphicsEnabled(boolean isHardwareAccelerationForVectorGraphicsEnabled) { + this.isHardwareAccelerationForVectorGraphicsEnabled = isHardwareAccelerationForVectorGraphicsEnabled; + return this; + } + public RenderingContext build() { return new RenderingContext( this.docVersion, @@ -302,7 +315,8 @@ public RenderingContext build() { this.avgRetriever, this.extensionEventCallback, this.aplTrace, - this.isMediaPlayerV2Enabled); + this.isMediaPlayerV2Enabled, + this.isHardwareAccelerationForVectorGraphicsEnabled); } } } diff --git a/apl/src/main/java/com/amazon/apl/android/RootConfig.java b/apl/src/main/java/com/amazon/apl/android/RootConfig.java index 0ee408ee..2d658794 100644 --- a/apl/src/main/java/com/amazon/apl/android/RootConfig.java +++ b/apl/src/main/java/com/amazon/apl/android/RootConfig.java @@ -17,6 +17,7 @@ import com.amazon.apl.android.audio.IAudioPlayerFactory; import com.amazon.apl.android.media.MediaPlayerFactoryProxy; import com.amazon.apl.android.media.RuntimeMediaPlayerFactory; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.utils.AccessibilitySettingsUtil; import com.amazon.apl.viewhost.config.EmbeddedDocumentFactory; import com.amazon.apl.viewhost.internal.DocumentManager; @@ -264,28 +265,15 @@ public RootConfig localTimeAdjustment(long adjustment) { } /** - * Sets a {@link LiveArray} to the top level context. + * Sets a {@link LiveData} to the top level context. * * @param name the name of the LiveArray. - * @param liveArray the data + * @param liveData the data * @return this for chaining */ @NonNull - public RootConfig liveData(@NonNull String name, @NonNull LiveArray liveArray) { - nLiveData(getNativeHandle(), name, liveArray.getNativeHandle()); - return this; - } - - /** - * Sets a {@link LiveMap} to the top level context. - * - * @param name the name of the LiveMap. - * @param liveMap the data - * @return this for chaining - */ - @NonNull - public RootConfig liveData(@NonNull String name, @NonNull LiveMap liveMap) { - nLiveData(getNativeHandle(), name, liveMap.getNativeHandle()); + public RootConfig liveData(@NonNull String name, @NonNull LiveData liveData) { + nLiveData(getNativeHandle(), name, liveData.getNativeHandle()); return this; } @@ -559,10 +547,10 @@ ExtensionMediator getExtensionMediator() { } /** - * Set document logging session. Hidden for now. + * Set document logging session. * @return This object for chaining */ - private RootConfig session(Session session) { + public RootConfig session(Session session) { mSession = session; nSession(getNativeHandle(), session.getNativeHandle()); return this; @@ -650,8 +638,9 @@ public RootConfig audioPlayerFactory(@NonNull IAudioPlayerFactory audioPlayerFac return this; } - public RootConfig setDocumentManager(@NonNull EmbeddedDocumentFactory embeddedDocumentFactory, @NonNull Handler handler) { - mDocumentManager = new DocumentManager(embeddedDocumentFactory, handler); + public RootConfig setDocumentManager(@NonNull EmbeddedDocumentFactory embeddedDocumentFactory, + @NonNull Handler handler, @NonNull ITelemetryProvider telemetryProvider) { + mDocumentManager = new DocumentManager(embeddedDocumentFactory, handler, telemetryProvider); nSetDocumentManager(getNativeHandle(), mDocumentManager.getNativeHandle()); return this; } diff --git a/apl/src/main/java/com/amazon/apl/android/RootContext.java b/apl/src/main/java/com/amazon/apl/android/RootContext.java index 2b268534..30063a12 100644 --- a/apl/src/main/java/com/amazon/apl/android/RootContext.java +++ b/apl/src/main/java/com/amazon/apl/android/RootContext.java @@ -5,6 +5,11 @@ package com.amazon.apl.android; +import static com.amazon.apl.android.providers.ITelemetryProvider.APL_DOMAIN; +import static com.amazon.apl.android.providers.ITelemetryProvider.Type.COUNTER; +import static com.amazon.apl.android.providers.ITelemetryProvider.Type.TIMER; +import static com.amazon.apl.enums.EventType.valueOf; + import android.os.Looper; import android.os.SystemClock; import android.text.TextUtils; @@ -16,10 +21,13 @@ import androidx.annotation.UiThread; import androidx.annotation.VisibleForTesting; +import com.amazon.alexaext.ExtensionRegistrar; import com.amazon.apl.android.bitmap.PooledBitmapFactory; import com.amazon.apl.android.configuration.ConfigurationChange; import com.amazon.apl.android.dependencies.IExtensionEventCallback; import com.amazon.apl.android.dependencies.IExtensionImageFilterCallback; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.dependencies.impl.NoOpUserPerceivedFatalCallback; import com.amazon.apl.android.events.ControlMediaEvent; import com.amazon.apl.android.events.DataSourceFetchEvent; import com.amazon.apl.android.events.ExtensionEvent; @@ -39,12 +47,14 @@ import com.amazon.apl.android.events.SpeakEvent; import com.amazon.apl.android.primitive.Rect; import com.amazon.apl.android.providers.AbstractMediaPlayerProvider; +import com.amazon.apl.android.providers.ILocalTimeOffsetProvider; import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.scaling.MetricsTransform; import com.amazon.apl.android.scaling.Scaling; import com.amazon.apl.android.scaling.ViewportMetrics; import com.amazon.apl.android.touch.Pointer; import com.amazon.apl.android.utils.APLTrace; +import com.amazon.apl.android.utils.FrameStat; import com.amazon.apl.android.utils.JNIUtils; import com.amazon.apl.android.utils.TracePoint; import com.amazon.apl.enums.ComponentType; @@ -55,6 +65,8 @@ import com.amazon.apl.enums.PropertyKey; import com.amazon.apl.enums.RootProperty; import com.amazon.apl.viewhost.Viewhost; +import com.amazon.apl.viewhost.internal.DocumentContext; +import com.amazon.apl.viewhost.internal.DocumentHandleImpl; import com.amazon.apl.viewhost.internal.ViewhostImpl; import com.amazon.common.BoundObject; @@ -62,6 +74,9 @@ import org.json.JSONException; import org.json.JSONObject; +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Calendar; import java.util.EnumMap; import java.util.HashSet; import java.util.LinkedHashMap; @@ -74,17 +89,12 @@ import java.util.concurrent.ConcurrentLinkedQueue; import java.util.concurrent.atomic.AtomicBoolean; -import static com.amazon.apl.android.providers.ITelemetryProvider.APL_DOMAIN; -import static com.amazon.apl.android.providers.ITelemetryProvider.Type.COUNTER; -import static com.amazon.apl.android.providers.ITelemetryProvider.Type.TIMER; -import static com.amazon.apl.enums.EventType.valueOf; - /** * APL RootContext is responsible for communication via JNI with APL core. It marshals the tasks of * layout, an component construction. */ -public class RootContext extends BoundObject implements IClock.IClockCallback { +public class RootContext extends BoundObject implements IClock.IClockCallback, ILocalTimeOffsetProvider.LocalTimeOffsetChangedListener { private static final String TAG = "RootContext"; private static final boolean DEBUG = false; @@ -96,6 +106,8 @@ public class RootContext extends BoundObject implements IClock.IClockCallback { private final APLOptions mOptions; @NonNull private final ITelemetryProvider mTelemetryProvider; + @NonNull + private UserPerceivedFatalReporter mUserPerceivedFatalReporter; // DoFrame metrics private static final String METRIC_DO_FRAME_FAIL = TAG + ".doFrame.fail"; @@ -164,14 +176,26 @@ public class RootContext extends BoundObject implements IClock.IClockCallback { private final IClock mAplClock; + private DocumentHandleImpl mDocumentHandle; + /** * We need to hold a reference to Content to ensure that it gets cleaned up *after* the RootContext. * Cleaning up *before* the RootContext can cause some resources C++ needs to be deleted prematurely. */ private final Content mContent; - //boolean to check if auto size is triggered. - private boolean mAutoSize; + /** + * This boolean can be set to true in 3 different situations. + * 1. When a render document request comes with APLLayout that has min/max width/height then it triggers the recalculation + * of width and height in layoutmanager.layoutComponent, hence resulting in setting new viewport size: + * https://tiny.amazon.com/tnp3mwwm/codeamazpackAPLCblob743eaplc + * 2. When a there is a config change request that comes with a new min/max width/height then it results in layout.resize() which + * further triggers layoutmanager.layoutComponent in the same way as referred in point 1 + * 3. When there is a set value command that changes the width/height property of a component it triggers ayout.resize() which + * further triggers layoutmanager.layoutComponent in the same way as referred in point 1 + * boolean to check if auto size is triggered. + */ + private boolean mIsAutoSizeLayoutPending; //Height after core auto sizes private int mAutoSizedHeight; @@ -179,23 +203,29 @@ public class RootContext extends BoundObject implements IClock.IClockCallback { //Width after core auto sizes private int mAutoSizedWidth; + private boolean mShouldRecordFrameMetrics; + + private List mFrameStats = new ArrayList<>(); /** * Construct a new RootContext object. * - * @param metrics the viewport metrics - * @param content the apl document - * @param rootConfig the environment configuration - * @param options the apl options - * @param viewPresenter the view presenter + * @param metrics the viewport metrics + * @param content the apl document + * @param rootConfig the environment configuration + * @param options the apl options + * @param viewPresenter the view presenter + * @param userPerceivedFatalReporter the user perceived fatal reporter */ private RootContext(@NonNull ViewportMetrics metrics, @NonNull Content content, @NonNull RootConfig rootConfig, @NonNull APLOptions options, - @NonNull IAPLViewPresenter viewPresenter) { + @NonNull IAPLViewPresenter viewPresenter, + @NonNull UserPerceivedFatalReporter userPerceivedFatalReporter) { mAplTrace = viewPresenter.getAPLTrace(); try (APLTrace.AutoTrace autoTrace = mAplTrace.startAutoTrace(TracePoint.ROOT_CONTEXT_CREATE)) { mOptions = options; + mUserPerceivedFatalReporter = userPerceivedFatalReporter; mAplClock = options.getAplClockProvider().create(this); mContent = content; mTelemetryProvider = mOptions.getTelemetryProvider(); @@ -203,7 +233,7 @@ private RootContext(@NonNull ViewportMetrics metrics, mRootConfig = rootConfig; mMetricsTransform = MetricsTransform.create(metrics); mRenderingContext = buildRenderingContext(APLVersionCodes.getVersionCode(content.getAPLVersion()), - options, rootConfig, mMetricsTransform); + options, rootConfig, mMetricsTransform, content); mTextMeasureCallback = TextMeasureCallback.factory().create(mMetricsTransform, new TextMeasure(mRenderingContext)); mAgentName = (String) rootConfig.getProperty(RootProperty.kAgentName); preBindInit(); @@ -211,23 +241,8 @@ private RootContext(@NonNull ViewportMetrics metrics, mTextMeasureCallback.onRootContextCreated(); bind(nativeHandle); inflate(); - autoSize(nativeHandle); - } - } - - /** - * Sets the variables with respect to auto size post inflation. - * @param rootContextHandle root context handle. - */ - private void autoSize(long rootContextHandle) { - int newHeight = Math.round(mMetricsTransform.toViewhost(viewportHeight(rootContextHandle))); - int newWidth = Math.round(mMetricsTransform.toViewhost(viewportWidth(rootContextHandle))); - setAutoSize(newWidth, newHeight); - if (mAutoSize) { - Log.i(TAG, String.format("Sending new height: %d and new width: %d in the callback", newHeight, newWidth)); - mAutoSizedHeight = newHeight; - mAutoSizedWidth = newWidth; - getOptions().getViewportSizeUpdateCallback().onViewportSizeUpdate(mAutoSizedWidth, mAutoSizedHeight); + checkIfAutoSizeNeeded(mMetricsTransform.getScaledViewhostWidth(), mMetricsTransform.getScaledViewhostHeight()); + mDocumentContext = new DocumentContext(nGetDocumentContext(getNativeHandle())); } } @@ -243,10 +258,12 @@ private RootContext(@NonNull APLOptions options, @NonNull MetricsTransform metricsTransform, @NonNull RootConfig rootConfig, @NonNull Content content, - long nativeHandle) { + long nativeHandle, + @NonNull UserPerceivedFatalReporter userPerceivedFatalReporter) { mAplTrace = viewPresenter.getAPLTrace(); try (APLTrace.AutoTrace autoTrace = mAplTrace.startAutoTrace(TracePoint.ROOT_CONTEXT_CREATE)) { mOptions = options; + mUserPerceivedFatalReporter = userPerceivedFatalReporter; mAplClock = options.getAplClockProvider().create(this); mTelemetryProvider = mOptions.getTelemetryProvider(); mMetricsTransform = metricsTransform; @@ -255,18 +272,35 @@ private RootContext(@NonNull APLOptions options, mContent = content; bind(nativeHandle); mRenderingContext = buildRenderingContext(APLVersionCodes.getVersionCode(nGetVersionCode(nativeHandle)), - options, rootConfig, metricsTransform); + options, rootConfig, metricsTransform, content); // rebind to the previously configured text measure. mTextMeasureCallback = TextMeasureCallback.factory().create(mRootConfig, mMetricsTransform, new TextMeasure(mRenderingContext)); mTextMeasureCallback.onRootContextCreated(); mAgentName = (String) rootConfig.getProperty(RootProperty.kAgentName); preBindInit(); inflate(); - autoSize(nativeHandle); + checkIfAutoSizeNeeded(mMetricsTransform.getScaledViewhostWidth(), mMetricsTransform.getScaledViewhostHeight()); + mDocumentContext = new DocumentContext(nGetDocumentContext(getNativeHandle())); + } + } + + /** + * Sets the variables with respect to auto size post inflation. + */ + private void checkIfAutoSizeNeeded(int oldWidth, int oldHeight) { + int newHeight = Math.round(mMetricsTransform.toViewhost(viewportHeight(this.getNativeHandle()))); + int newWidth = Math.round(mMetricsTransform.toViewhost(viewportWidth(this.getNativeHandle()))); + mIsAutoSizeLayoutPending = (oldHeight != newHeight) || (oldWidth != newWidth); + if (mIsAutoSizeLayoutPending) { + Log.i(TAG, String.format("old height: %d, old width: %d, new height: %d, new width: %d, hence autoSize: %s", + oldHeight, oldWidth, newHeight, newWidth, mIsAutoSizeLayoutPending)); + mAutoSizedWidth = newWidth; + mAutoSizedHeight = newHeight; + getOptions().getViewportSizeUpdateCallback().onViewportSizeUpdate(newHeight, newWidth); } } - private RenderingContext buildRenderingContext(int docVersion, APLOptions options, RootConfig config,MetricsTransform metricsTransform) { + private RenderingContext buildRenderingContext(int docVersion, APLOptions options, RootConfig config, MetricsTransform metricsTransform, Content content) { // TODO this is adapting legacy framework to new, deprecate the old ExtensionMediator extensionMediator = config.getExtensionMediator(); @@ -292,7 +326,8 @@ private RenderingContext buildRenderingContext(int docVersion, APLOptions option .extensionImageFilterCallback(ifCB) .extensionEventCallback(eeCB) .aplTrace(mAplTrace) - .isMediaPlayerV2Enabled(config.isMediaPlayerV2Enabled()); + .isMediaPlayerV2Enabled(config.isMediaPlayerV2Enabled()) + .isHardwareAccelerationForVectorGraphicsEnabled(content.optSetting("-experimentalHardwareAccelerationForAndroid", false)); if (extensionMediator != null) { ctxBuilder.extensionResourceProvider(extensionMediator.extensionResourceProvider); @@ -309,8 +344,8 @@ private AbstractMediaPlayerProvider getMediaPlayerProvider(APLOptions options) { /** * Creates a RootContext from metrics, content and root config information. * - * @param metrics the viewport metrics - * @param rootConfig the root config + * @param metrics the viewport metrics + * @param rootConfig the root config * @param textMeasureCallback the callback for text measurement * @return a non-zero handle if created, 0 if failed. */ @@ -363,25 +398,13 @@ private long createHandle(ViewportMetrics metrics, RootConfig rootConfig, TextMe */ private native float viewportWidth(long nativeHandle); - /** - * Sets auto size flag. - * @return - */ - private void setAutoSize(int newWidth, int newHeight) { - int height = mMetricsTransform.getScaledViewhostHeight(); - int width = mMetricsTransform.getScaledViewhostWidth(); - boolean autoSize = (height != newHeight) || (width != newWidth); - Log.i(TAG, String.format("old height: %d, old width: %d, new height: %d, new width: %d, hence autoSize: %s", - height, width, newHeight, newWidth, autoSize)); - mAutoSize = autoSize; - } - /** * Returns the value for auto size flag. + * * @return boolean */ - public boolean isAutoSize() { - return mAutoSize; + public boolean isAutoSizeLayoutPending() { + return mIsAutoSizeLayoutPending; } /** @@ -399,6 +422,11 @@ public int getAutoSizedHeight() { public int getAutoSizedWidth() { return mAutoSizedWidth; } + + /** + * Document context i.e. topDocument + */ + private DocumentContext mDocumentContext; /** * Initializes member variables and metrics. */ @@ -422,6 +450,25 @@ public static RootContext create( @NonNull final ViewportMetrics metrics, @NonNull final Content content, @NonNull final RootConfig rootConfig, @NonNull final APLOptions options, @NonNull IAPLViewPresenter presenter) throws IllegalArgumentException, IllegalStateException { + return create(metrics, content, rootConfig, options, presenter, + new UserPerceivedFatalReporter(new NoOpUserPerceivedFatalCallback())); + } + + /** + * Request to create a RootContext with UserPerceivedFatalReporter. + * + * @param metrics APL display metrics. + * @param content APL document content. + * @param rootConfig APL root config. + * @param options APL options. + * @param presenter APL presenter. + * @param userPerceivedFatalReporter APL UPF Reporter. + * @return a RootContext. + */ + public static RootContext create( + @NonNull final ViewportMetrics metrics, @NonNull final Content content, @NonNull final RootConfig rootConfig, + @NonNull final APLOptions options, @NonNull IAPLViewPresenter presenter, UserPerceivedFatalReporter userPerceivedFatalReporter) + throws IllegalArgumentException, IllegalStateException { if (metrics == null) { throw new IllegalArgumentException("Metrics must not be null"); } else if (content == null) { @@ -436,10 +483,24 @@ public static RootContext create( throw new IllegalArgumentException("APLPresenter must not be null"); } presenter.preDocumentRender(); - RootContext rootContext = new RootContext(metrics, content, rootConfig, options, presenter); + RootContext rootContext = new RootContext(metrics, content, rootConfig, options, presenter, userPerceivedFatalReporter); if (!rootContext.isBound()) { throw new IllegalStateException("Could not create RootContext"); } + + ILocalTimeOffsetProvider timeProvider = options.getLocalTimeOffsetProvider(); + if (timeProvider == null) { // Backwards compatible if no provider is set. + Log.i(TAG, "Updating local time adjustment from system"); + Calendar now = Calendar.getInstance(); + long offset = now.get(Calendar.ZONE_OFFSET) + now.get(Calendar.DST_OFFSET); + rootContext.setLocalTimeAdjustment(offset); + } else { + Log.i(TAG, "Updating local time adjustment from provider."); + long offset = timeProvider.getCurrentOffset(); + rootContext.setLocalTimeAdjustment(offset); + timeProvider.addListener(new WeakReference<>(rootContext)); + } + return rootContext; } @@ -494,7 +555,11 @@ public Runnable onSuccess() { @Override public Runnable onFailure() { - return () -> Log.e(TAG, "extension loading failed after reinflation"); + return () -> { + mUserPerceivedFatalReporter.reportFatal( + UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE); + Log.e(TAG, "extension loading failed after reinflation"); + }; } }); @@ -503,6 +568,8 @@ public Runnable onFailure() { @Override public void onError(Exception e) { + mUserPerceivedFatalReporter.reportFatal( + UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE); Log.e(TAG, "Error occured during content resolution: " + e.getMessage()); } }); @@ -546,14 +613,31 @@ private void clearComponentAndViewsResources() { public static RootContext createFromCachedDocumentState(@NonNull final DocumentState documentState, @NonNull IAPLViewPresenter viewPresenter) { // Recording the mRenderStartTime when we are trying to restore the document from cache. viewPresenter.preDocumentRender(); - return new RootContext(documentState.getOptions(), viewPresenter, documentState.getMetricsTransform(), documentState.getRootConfig(), documentState.getContent(), documentState.getNativeHandle()); + return new RootContext(documentState.getOptions(), viewPresenter, documentState.getMetricsTransform(), documentState.getRootConfig(), documentState.getContent(), documentState.getNativeHandle(), new UserPerceivedFatalReporter()); + } + + /** + * Creates a document from Cached state. + * @param viewPresenter + * @param metricsTransform + * @param options + * @param rootConfig + * @param content + * @param rootContextNativeHandle + * @return RootContext + */ + public static RootContext createFromCache(@NonNull IAPLViewPresenter viewPresenter, @NonNull MetricsTransform metricsTransform, @NonNull APLOptions options, @NonNull RootConfig rootConfig, @NonNull Content content, long rootContextNativeHandle) { + + // Recording the mRenderStartTime when we are trying to restore the document from cache. + viewPresenter.preDocumentRender(); + return new RootContext(options, viewPresenter, metricsTransform, rootConfig, content, rootContextNativeHandle, new UserPerceivedFatalReporter()); } public RenderingContext getRenderingContext() { return mRenderingContext; } - MetricsTransform getMetricsTransform() { + public MetricsTransform getMetricsTransform() { return mMetricsTransform; } @@ -603,11 +687,28 @@ public void finishDocument() { Viewhost viewhost = mOptions.getViewhost(); if (viewhost instanceof ViewhostImpl) { ViewhostImpl viewhostImpl = (ViewhostImpl)viewhost; - viewhostImpl.notifyRootContextFinished(); + if (mDocumentHandle != null) { + //Triggered from unified document flow and it will finish only the document which is mapped to this rootContext. + //The one usecase which still needs to be handled is to finish all embedded documents within a document that got rendered from Viewhost.java + //Not finishing the embedded docs can lead to a situation where the code still exposes a handle to perform actions on embedded docs, however the main document is terminated. + mDocumentHandle.setDocumentState(com.amazon.apl.viewhost.internal.DocumentState.FINISHED); + } else { + //triggered from legacy flow. It will finish all embedded documents defined for the document. + // It will end up finishing all the INFLATED document handles created by the viewhost instance. + viewhostImpl.notifyRootContextFinished(); + } } } } + public void setDocumentHandle(DocumentHandleImpl handle) { + mDocumentHandle = handle; + } + + public DocumentHandleImpl getDocumentHandle() { + return mDocumentHandle; + } + @VisibleForTesting(otherwise = VisibleForTesting.PACKAGE_PRIVATE) public void pauseDocument() { if (!mIsFinished.get() && mIsResumed.get()) { @@ -766,6 +867,12 @@ private void buildComponent(String componentId, long nativeHandle, int typeId) { } } + @Override + public void localTimeOffsetChanged(long newOffset) { + // Post to the frameloop. + post(() -> setLocalTimeAdjustment(newOffset)); + } + /** * Set the local time adjustment. This is the number of milliseconds added to the UTC * time that gives the correct local time including DST. @@ -831,6 +938,22 @@ public boolean updateDataSource(@NonNull final String type, @NonNull final Strin } + /** + * For the GC to close all V1 connections. + */ + @Override + public void finalize() { + try { + super.finalize(); + ExtensionRegistrar registrar = getRootConfig() != null ? getRootConfig().getExtensionProvider() : null; + if (registrar != null) { + registrar.closeAllRemoteV1Connections(); + } + } catch (Throwable e) { + Log.e(TAG, "GC Clean up of root context failed with an exception: " + e); + } + } + /** * Get a view associated with a component. Creates it if it does not exist. Updates properties. * @@ -1161,7 +1284,18 @@ private void coreFrameUpdate(long time) { mAplTrace.endTrace(); mAplTrace.startTrace(TracePoint.ROOT_CONTEXT_CLEAR_PENDING); + + int oldHeight = Math.round(mMetricsTransform.toViewhost(viewportHeight(this.getNativeHandle()))); + int oldWidth = Math.round(mMetricsTransform.toViewhost(viewportWidth(this.getNativeHandle()))); + nClearPending(nativeHandle); + //clear pending triggers layoutmanager.layout if there is a pending component, hence causing viewport changes if applicable + checkIfAutoSizeNeeded(oldWidth, oldHeight); + if (isAutoSizeLayoutPending()) { + Log.i(TAG, "Performing pending auto-size layout"); + mViewPresenter.reinflate(); + } + mAplTrace.endTrace(); mAplTrace.startTrace(TracePoint.ROOT_CONTEXT_HANDLE_DIRTY_PROPERTIES); @@ -1205,10 +1339,25 @@ private void checkDataSourceErrors(final long nativeHandle) { //check for errors in embedded docs ViewhostImpl viewhost = (ViewhostImpl)mOptions.getViewhost(); if (viewhost != null) { - viewhost.checkAndReportDataSourceErrors(); + viewhost.checkAndReportDataSourceErrors(errors); } } + public void startFrameMetricsRecording() { + mShouldRecordFrameMetrics = true; + } + + public List stopFrameMetricsRecording() throws JSONException { + mShouldRecordFrameMetrics = false; + List result = new ArrayList<>(); + for (FrameStat frameStat : mFrameStats) { + result.add(frameStat.toJSON()); + } + mFrameStats.clear(); + return result; + } + + /** * Called when a new display frame is being rendered. * See {@link IClock.IClockCallback#onTick(long)} @@ -1243,10 +1392,14 @@ public void onTick(long frameTimeNanos) { final long doFrameTime = end - frameTimeNanos; if (doFrameTime > TARGET_DO_FRAME_TIME && mTelemetryProvider != null) { - if(!mIsFinished.get()){ + if (!mIsFinished.get()) { mTelemetryProvider.incrementCount(cDropFrame); } } + if (mShouldRecordFrameMetrics) { + FrameStat pair = new FrameStat(frameTimeNanos, end); + mFrameStats.add(pair); + } } catch (Exception e) { // mTelemetryProvider may be null if the document has been finished. if (mTelemetryProvider != null) { @@ -1632,6 +1785,14 @@ public String getAgentName() { return mAgentName; } + public DocumentContext getDocumentContext() { + return mDocumentContext; + } + + public String documentCommandRequest(String method, String params) { + return nDocumentCommandRequest(getNativeHandle(), method, params); + } + // Native methods private static native boolean nIsDirty(long nativeHandle); @@ -1714,4 +1875,8 @@ private static native long nInvokeExtensionEventHandler(long nativeHandle, private static native void nMediaLoaded(long nativeHandle, String url); private static native void nMediaLoadFailed(long nativeHandle, String url, int errorCode, String error); + + private native long nGetDocumentContext(long nativeHandle); + + private static native String nDocumentCommandRequest(long nativeHandle, String method, String params); } diff --git a/apl/src/main/java/com/amazon/apl/android/RuntimeConfig.java b/apl/src/main/java/com/amazon/apl/android/RuntimeConfig.java index b652deb1..0c034c90 100644 --- a/apl/src/main/java/com/amazon/apl/android/RuntimeConfig.java +++ b/apl/src/main/java/com/amazon/apl/android/RuntimeConfig.java @@ -27,6 +27,8 @@ public abstract class RuntimeConfig { public abstract boolean isPreloadingFontsEnabled(); + public abstract boolean isEnableHardwareAccelerationForAVG(); + public abstract IBitmapPool getBitmapPool(); public abstract IBitmapCache getBitmapCache(); @@ -54,6 +56,7 @@ public static Builder builder() { return new AutoValue_RuntimeConfig.Builder() .fontResolver(new CompatFontResolver()) .preloadingFontsEnabled(true) + .enableHardwareAccelerationForAVG(false) .bitmapCache(bitmapCache) .bitmapPool(bitmapPool) .embeddedFontResolverEnabled(true) @@ -76,6 +79,24 @@ public abstract static class Builder { */ public abstract Builder preloadingFontsEnabled(boolean shouldPreloadFonts); + /** + * Defaults to false + * The reason for defaulting to false is that some runtimes are constrained to use Android + * FrameBuilder rendering pipeline, which has an architectural deficiency leading to + * significant quality degradation of graphic Paths at high scale values. + * + * Runtimes can opt-in for better fluidity with AVG animations by setting this to true, + * if they can use Skia (OpenGL) pipeline. + * + * https://developer.android.com/topic/performance/hardware-accel#scaling + * + * @param acceleratedAVGRendering specifies whether the viewhost should + * use HW Accelerated Canvas directly for + * rendering AVG + * @return this builder + */ + public abstract Builder enableHardwareAccelerationForAVG(boolean acceleratedAVGRendering); + /** * Bitmap pool to use for creating new Bitmap objects. * diff --git a/apl/src/main/java/com/amazon/apl/android/Session.java b/apl/src/main/java/com/amazon/apl/android/Session.java index 9fedb0ac..54607d04 100644 --- a/apl/src/main/java/com/amazon/apl/android/Session.java +++ b/apl/src/main/java/com/amazon/apl/android/Session.java @@ -5,31 +5,47 @@ package com.amazon.apl.android; +import com.amazon.apl.android.dependencies.IAPLSessionListener; import com.amazon.common.BoundObject; +import java.util.ArrayList; +import java.util.Collections; +import java.util.List; + /** * Document logging session. Limited at the moment, but ultimately provides a possibility to provide * logs that could be sent to the experience developer (parsing/execution errors/etc). */ public class Session extends BoundObject { - /** - * Creates a default Session. - */ + private final List mLogList; + private IAPLSessionListener mListener; + public Session() { bind(nCreate()); + mLogList = Collections.synchronizedList(new ArrayList<>()); + } + + public enum LogEntryLevel { + NONE, TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL + } + + public enum LogEntrySource { + SESSION, VIEW, COMMAND } /** * @return APL instance log ID. May be used to uniquely identify logs emitted by APL engine instance. */ - public String getLogId() { return nGetLogId(getNativeHandle()); } + public String getLogId() { + return nGetLogId(getNativeHandle()); + } /** * Enable publishing sensitive session information to the device logs. For example, * user-generated output from the Log command, which could contain arbitrary information from * the APL context. This is a security liability and should not be enabled in production builds * of apps unless explicitly intended. - * + *

* Note: * - This setting apples to all of the application's view hosts instances. * - This is not affected by the state of the debuggable flag in the application's manifest. @@ -38,7 +54,71 @@ public static void setSensitiveLoggingEnabled(boolean enabled) { nSetDebuggingEnabled(enabled); } - private static native long nCreate(); + public void setAPLListener(IAPLSessionListener listener) { + mListener = listener; + reportExistingLogs(); + } + + private void reportExistingLogs() { + for (LogInfo logInfo : mLogList) { + mListener.write(logInfo.getLevel(), logInfo.getSource(), logInfo.getMessage(), logInfo.getArguments()); + } + } + + + private native long nCreate(); + private static native String nGetLogId(long _handle); + private static native void nSetDebuggingEnabled(boolean enabled); + + public void write(LogEntryLevel level, LogEntrySource source, String message) { + write(level, source, message, null); + } + + /** + * Override to provide special handling of session-related log entries. + * + * @param level The log entry's severity level + * @param source The log entry's source + * @param message The log entry's message + * @param arguments Any additional arguments associated with log entry + */ + public void write(LogEntryLevel level, LogEntrySource source, String message, Object[] arguments){ + if (mListener != null) { + mListener.write(level, source, message, arguments); + } + mLogList.add(new LogInfo(level, source, message, arguments)); + } + + private static class LogInfo { + private final LogEntryLevel mLevel; + private final LogEntrySource mSource; + private final String mMessage; + private final Object[] mArguments; + + LogInfo(LogEntryLevel level, LogEntrySource source, String message, Object[] arguments) { + mLevel = level; + mSource = source; + mMessage = message; + mArguments = arguments; + } + + public LogEntryLevel getLevel() { + return mLevel; + } + + public LogEntrySource getSource() { + return mSource; + } + + public String getMessage() { + return mMessage; + } + + public Object[] getArguments() { + return mArguments; + } + } + } diff --git a/apl/src/main/java/com/amazon/apl/android/UserPerceivedFatalReporter.java b/apl/src/main/java/com/amazon/apl/android/UserPerceivedFatalReporter.java new file mode 100644 index 00000000..6fd02fd8 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/UserPerceivedFatalReporter.java @@ -0,0 +1,77 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.android; + +import android.util.Log; + +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.dependencies.impl.NoOpUserPerceivedFatalCallback; + +import java.util.concurrent.atomic.AtomicBoolean; + +/** + * UserPerceivedFatalReporter. Controls the lifecycle of UPF reporting and + * ensure only one success or failure is reported for a given interaction. + */ +public class UserPerceivedFatalReporter { + + private static final String TAG = "APLController"; + private IUserPerceivedFatalCallback mUserPerceivedFatalCallback; + private final AtomicBoolean mIsUpfAlreadyReported = new AtomicBoolean(false); + + public UserPerceivedFatalReporter() { + this(new NoOpUserPerceivedFatalCallback()); + } + + public UserPerceivedFatalReporter(IUserPerceivedFatalCallback userPerceivedFatalCallback) { + this.mUserPerceivedFatalCallback = userPerceivedFatalCallback; + mIsUpfAlreadyReported.set(false); + } + + /** + * To be called when an interaction is successful. + */ + public void reportSuccess() { + if (!mIsUpfAlreadyReported.get()) { + mUserPerceivedFatalCallback.onSuccess(); + mIsUpfAlreadyReported.set(true); + } else { + Log.w(TAG, "Attempt to report UPF success ignored, since UPF status has already been reported for this interaction."); + } + } + + /** + * To be called when user perceives UPF and report it to runtime. + * @param fatalError UpfConstant. + */ + public void reportFatal(UpfReason fatalError) { + if (!mIsUpfAlreadyReported.get()) { + mUserPerceivedFatalCallback.onFatalError(fatalError.toString()); + mIsUpfAlreadyReported.set(true); + } else { + Log.w(TAG, "Attempt to report UPF fatal ignored, since UPF status has already been reported for this interaction."); + } + } + + public enum UpfReason { + BACK_EXTENSION_FAILURE("APL_FATAL.BackExtensionFailed"), + APL_INITIALIZATION_FAILURE("APL_FATAL.AplInitializationFailed"), + REQUIRED_EXTENSION_LOADING_FAILURE("APL_FATAL.RequiredExtensionLoadingFailed"), + ROOT_CONTEXT_CREATION_FAILURE("APL_FATAL.RootContextCreationFailed"), + CONTENT_CREATION_FAILURE("APL_FATAL.ContentCreationFailed"), + CONTENT_RESOLUTION_FAILURE("APL_FATAL.ContentResolutionFailed"); + + private final String mUpfError; + + UpfReason(String upfError) { + mUpfError = upfError; + } + + @Override + public String toString() { + return mUpfError; + } + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/android/VectorGraphic.java b/apl/src/main/java/com/amazon/apl/android/VectorGraphic.java index d6a2e675..84c71634 100644 --- a/apl/src/main/java/com/amazon/apl/android/VectorGraphic.java +++ b/apl/src/main/java/com/amazon/apl/android/VectorGraphic.java @@ -56,7 +56,6 @@ public final UrlRequests.UrlRequest getSourceRequest() { UrlRequests requests = mProperties.getUrlRequests(PropertyKey.kPropertySource); if (requests.size() == 0) return null; return requests.at(0); - } public void updateGraphic(String content) { diff --git a/apl/src/main/java/com/amazon/apl/android/component/ComponentViewAdapter.java b/apl/src/main/java/com/amazon/apl/android/component/ComponentViewAdapter.java index 2d409f60..e1a96a1a 100644 --- a/apl/src/main/java/com/amazon/apl/android/component/ComponentViewAdapter.java +++ b/apl/src/main/java/com/amazon/apl/android/component/ComponentViewAdapter.java @@ -8,6 +8,7 @@ import android.content.Context; import android.text.TextUtils; import android.util.Log; +import android.view.accessibility.AccessibilityEvent; import android.view.View; import android.view.ViewGroup; import android.view.ViewParent; @@ -27,6 +28,7 @@ import com.amazon.apl.android.utils.AccessibilitySettingsUtil; import com.amazon.apl.android.utils.TracePoint; import com.amazon.apl.enums.PropertyKey; +import com.amazon.apl.enums.Role; import java.util.HashMap; import java.util.HashSet; @@ -52,6 +54,8 @@ public abstract class ComponentViewAdapter mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyOpacity, this::applyAlpha); mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyDisplay, this::applyDisplay); mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyAccessibilityLabel, this::applyAccessibility); + mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyAccessibilityAdjustableValue, this::applyAccessibility); + mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyAccessibilityAdjustableRange, this::applyAccessibilityAdjustableRange); mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyDisabled, this::applyDisabled); mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyTransform, this::applyTransform); mDynamicPropertyFunctionMap.put(PropertyKey.kPropertyBounds, this::requestLayout); @@ -172,15 +176,36 @@ private void applyAccessibility(C component, V view) { if (!ViewCompat.hasAccessibilityDelegate(view)) { ViewCompat.setAccessibilityDelegate(view, APLAccessibilityDelegate.create(component, view.getContext())); } + StringBuilder accessibility = new StringBuilder(); + String accessibilityLabel = component.getAccessibilityLabel(); + if (accessibilityLabel != null) + accessibility.append(accessibilityLabel); - String accessibility = component.getAccessibilityLabel(); - if (accessibility != null) { - view.setContentDescription(accessibility); + if (checkAccessibilityAdjustableValue(component)) { + String accessibilityAdjustableValue = component.getAccessibilityAdjustableValue(); + accessibility.append(" ").append(accessibilityAdjustableValue); } + view.setContentDescription(accessibility.toString()); + applyFocusability(component, view); } + /** + * Send Accessibility event to AccessibilityDelegate to update properties. + * @param component the component + * @param view the view + */ + private void applyAccessibilityAdjustableRange(C component, V view) { + if (checkAccessibilityAdjustableValue(component)) + return; + if (!ViewCompat.hasAccessibilityDelegate(view)) { + ViewCompat.setAccessibilityDelegate(view, APLAccessibilityDelegate.create(component, view.getContext())); + } else { + ViewCompat.getAccessibilityDelegate(view).sendAccessibilityEvent(view, AccessibilityEvent.CONTENT_CHANGE_TYPE_CONTENT_DESCRIPTION); + } + } + /** * Apply the accessibility focus to the view. * @param component the component @@ -190,12 +215,24 @@ public void applyFocusability(C component, V view) { boolean focusable = component.isFocusable() && !component.isDisabled(); boolean focusableInTouchMode = component.isFocusableInTouchMode(); // TODO this setting should likely be put in APLAccessibilityDelegate - boolean focusableForAccessibility = (!TextUtils.isEmpty(component.getAccessibilityLabel()) && sAccessibilitySettingsUtil.isScreenReaderEnabled(view.getContext())); + boolean accessibilityAdjustableValue = checkAccessibilityAdjustableValue(component); + boolean accessibilityContentDescription = !TextUtils.isEmpty(component.getAccessibilityLabel()) || accessibilityAdjustableValue; + boolean focusableForAccessibility = (accessibilityContentDescription && sAccessibilitySettingsUtil.isScreenReaderEnabled(view.getContext())); view.setFocusable(focusable || focusableForAccessibility); view.setFocusableInTouchMode(focusableInTouchMode || focusableForAccessibility); } + /** + * check for accessibilityAdjustableValue property. + * @param component the component + * @return true if accessibilityAdjustableValue is present and not empty, else false + */ + private boolean checkAccessibilityAdjustableValue(C component) { + return component.getRole() == Role.kRoleAdjustable && component.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue) && + component.getAccessibilityAdjustableValue() != null && !component.getAccessibilityAdjustableValue().isEmpty(); + } + /** * Apply disabled state to the view. * @param component the component diff --git a/apl/src/main/java/com/amazon/apl/android/component/ImageViewAdapter.java b/apl/src/main/java/com/amazon/apl/android/component/ImageViewAdapter.java index 3e472e36..03f485bf 100644 --- a/apl/src/main/java/com/amazon/apl/android/component/ImageViewAdapter.java +++ b/apl/src/main/java/com/amazon/apl/android/component/ImageViewAdapter.java @@ -30,6 +30,7 @@ import com.amazon.apl.android.utils.LazyImageLoader; import com.amazon.apl.android.utils.TracePoint; import com.amazon.apl.android.views.APLImageView; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.ImageScale; import com.amazon.apl.enums.PropertyKey; @@ -42,6 +43,8 @@ public class ImageViewAdapter extends ComponentViewAdapter public static final String METRIC_COUNTER_BITMAP_CACHE_HIT = "ImageBitmapCacheHit"; public static final String METRIC_COUNTER_BITMAP_CACHE_MISS = "ImageBitmapCacheMiss"; + private IDTNetworkRequestHandler mDTNetworkRequestHandler; + private ImageViewAdapter() { super(); putPropertyFunction(PropertyKey.kPropertyOverlayGradient, this::applyOverlayGradient); @@ -156,8 +159,12 @@ private void initImageLoading(Image image, @NonNull APLImageView view) { Log.d(TAG, "Loading image: " + image.getSourceUrls() + " into " + image); view.setLoadDeferred(false); telemetryProvider.incrementCount(metricBitmapCacheMissCounter); - LazyImageLoader.initImageLoad(this, image, view); + LazyImageLoader.initImageLoad(this, image, view, mDTNetworkRequestHandler); trace.endTrace(); + + if (mDTNetworkRequestHandler == null) { + Log.d(TAG, "DTNetworkRequestHandler is null, no network requests events will be tracked"); + } } /** @@ -240,6 +247,10 @@ private void createAndSetDrawableToImageView(@NonNull APLImageView view, @NonNul view.setLayoutRequestsEnabled(true); } + public void setDTNetworkRequestHandler(IDTNetworkRequestHandler dtNetworkRequestHandler) { + mDTNetworkRequestHandler = dtNetworkRequestHandler; + } + /** * Function to execute on result of Bitmap processing. */ diff --git a/apl/src/main/java/com/amazon/apl/android/component/NoOpViewAdapter.java b/apl/src/main/java/com/amazon/apl/android/component/NoOpViewAdapter.java deleted file mode 100644 index 5c9e3b73..00000000 --- a/apl/src/main/java/com/amazon/apl/android/component/NoOpViewAdapter.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.android.component; - -import android.content.Context; - -import com.amazon.apl.android.IAPLViewPresenter; -import com.amazon.apl.android.NoOpComponent; -import com.amazon.apl.android.views.APLAbsoluteLayout; - -/** - * No-op adapter that wires basic component properties to a basic layout - */ -public class NoOpViewAdapter extends ComponentViewAdapter { - private static NoOpViewAdapter INSTANCE; - - public static NoOpViewAdapter getInstance() { - if (INSTANCE == null) { - INSTANCE = new NoOpViewAdapter(); - } - return INSTANCE; - } - - NoOpViewAdapter() { - super(); - } - - @Override - public APLAbsoluteLayout createView(Context context, IAPLViewPresenter presenter) { - return new APLAbsoluteLayout(context, presenter); - } - - @Override - void applyPadding(NoOpComponent component, APLAbsoluteLayout layout) { - } -} diff --git a/apl/src/main/java/com/amazon/apl/android/dependencies/IAPLSessionListener.java b/apl/src/main/java/com/amazon/apl/android/dependencies/IAPLSessionListener.java new file mode 100644 index 00000000..db359386 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/dependencies/IAPLSessionListener.java @@ -0,0 +1,20 @@ +package com.amazon.apl.android.dependencies; + +import androidx.annotation.Nullable; +import com.amazon.apl.android.Session; + +/** + * Interface for handling Log Events from the Log Command and Dev Tools Log Domain. + */ +public interface IAPLSessionListener { + + /** + * This method will get called, when there is a log to write. + * + * @param level The specific {@link Session.LogEntryLevel}; NONE, TRACE, DEBUG, INFO, WARN, ERROR, CRITICAL. + * @param source The specific {@link Session.LogEntrySource} from where the log is being emitted from. + * @param message A {@link String} with the message for the log event. + * @param arguments Any additional arguments that may be attached to the log. + */ + void write(Session.LogEntryLevel level, Session.LogEntrySource source, String message, @Nullable Object[] arguments); +} diff --git a/apl/src/main/java/com/amazon/apl/android/dependencies/IUserPerceivedFatalCallback.java b/apl/src/main/java/com/amazon/apl/android/dependencies/IUserPerceivedFatalCallback.java new file mode 100644 index 00000000..2e0e1752 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/dependencies/IUserPerceivedFatalCallback.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.android.dependencies; + +/** + * Callback provided by the runtime to report on the success or failure of an interaction. + * An example of an interaction is rendering an APL document. + * The view host guarantees that for the interaction, either `onSuccess` or `onFatalError` will be called exactly once. + */ +public interface IUserPerceivedFatalCallback { + + /** + * To be called when user perceives UPF and report it to runtime. + * @param error send error. As per UPF Contract, it should be less than 100 characters. + */ + void onFatalError(String error); + + /** + * To be called when an interaction is successful. + */ + void onSuccess(); +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/android/dependencies/impl/MediaPlayer.java b/apl/src/main/java/com/amazon/apl/android/dependencies/impl/MediaPlayer.java index 4953e16e..7a3146fd 100644 --- a/apl/src/main/java/com/amazon/apl/android/dependencies/impl/MediaPlayer.java +++ b/apl/src/main/java/com/amazon/apl/android/dependencies/impl/MediaPlayer.java @@ -6,6 +6,8 @@ package com.amazon.apl.android.dependencies.impl; import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; import android.graphics.Matrix; import android.graphics.SurfaceTexture; import android.media.AudioAttributes; @@ -76,6 +78,7 @@ private enum Action { .setContentType(AudioAttributes.CONTENT_TYPE_MOVIE) .setUsage(AudioAttributes.USAGE_MEDIA) .build(); + private static final String ANDROID_ASSET_PATH = "/android_asset/"; private MediaSources mSources; @Nullable @@ -535,8 +538,12 @@ private void preparePlayer() throws IllegalStateException, IOException { MediaSource mediaSource = mSources.at(mCurrentTrackIndex); Map headers = mediaSource.headers(); setCompletionListener(mediaSource); + Uri uri = Uri.parse(mediaSource.url()); if (headers.size() > 0) { - mMediaPlayer.setDataSource(mContext, Uri.parse(mediaSource.url()), mediaSource.headers()); + mMediaPlayer.setDataSource(mContext, uri, mediaSource.headers()); + } else if ("file".equalsIgnoreCase(uri.getScheme()) + && uri.getPath() != null && uri.getPath().startsWith(ANDROID_ASSET_PATH)) { + setMediaSourceFromAsset(uri); } else { mMediaPlayer.setDataSource(mediaSource.url()); } @@ -549,6 +556,14 @@ private void preparePlayer() throws IllegalStateException, IOException { } } + private void setMediaSourceFromAsset(Uri uri) throws IOException { + AssetManager assetManager = mContext.getAssets(); + String assetPath = uri.getPath().substring(ANDROID_ASSET_PATH.length()); + try (AssetFileDescriptor fd = assetManager.openFd(assetPath)) { + mMediaPlayer.setDataSource(fd.getFileDescriptor(), fd.getStartOffset(), fd.getLength()); + } + } + private void playInternal(boolean shouldNotifyState) throws IllegalStateException { requestAudioFocus(); mMediaPlayer.start(); diff --git a/apl/src/main/java/com/amazon/apl/android/dependencies/impl/NoOpUserPerceivedFatalCallback.java b/apl/src/main/java/com/amazon/apl/android/dependencies/impl/NoOpUserPerceivedFatalCallback.java new file mode 100644 index 00000000..4ffc9b26 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/dependencies/impl/NoOpUserPerceivedFatalCallback.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.android.dependencies.impl; + +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; + +/** + * Implementation of {@link IUserPerceivedFatalCallback}. + */ + +public class NoOpUserPerceivedFatalCallback implements IUserPerceivedFatalCallback { + @Override + public void onFatalError(String error) { + // no-op + } + + @Override + public void onSuccess() { + // no-op + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/APLVectorGraphicView.java b/apl/src/main/java/com/amazon/apl/android/graphic/APLVectorGraphicView.java index f367081a..288a52a0 100644 --- a/apl/src/main/java/com/amazon/apl/android/graphic/APLVectorGraphicView.java +++ b/apl/src/main/java/com/amazon/apl/android/graphic/APLVectorGraphicView.java @@ -9,11 +9,13 @@ import android.graphics.Bitmap; import android.graphics.Canvas; import android.graphics.Matrix; +import android.graphics.Paint; import android.graphics.drawable.Drawable; import androidx.annotation.NonNull; import androidx.annotation.Nullable; import androidx.annotation.VisibleForTesting; +import android.os.Build; import android.util.Log; import android.widget.ImageView; @@ -45,6 +47,11 @@ public class APLVectorGraphicView extends ImageView { */ public APLVectorGraphicView(@NonNull Context context, @NonNull IAPLViewPresenter presenter) { super(context); + if (presenter.isHardwareAccelerationForVectorGraphicsEnabled()){ + // Required for drawing view alpha accurately when drawing on hardware accelerated canvas + Paint paint = new Paint(Paint.ANTI_ALIAS_FLAG); + setLayerType(LAYER_TYPE_HARDWARE, paint); + } mBitmapFactory = presenter.getBitmapFactory(); } diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/AlexaVectorDrawable.java b/apl/src/main/java/com/amazon/apl/android/graphic/AlexaVectorDrawable.java index 2ca9e9d4..c9615d5f 100644 --- a/apl/src/main/java/com/amazon/apl/android/graphic/AlexaVectorDrawable.java +++ b/apl/src/main/java/com/amazon/apl/android/graphic/AlexaVectorDrawable.java @@ -62,15 +62,16 @@ public class AlexaVectorDrawable extends Drawable { private VectorGraphicScale mScale; - private float mViewportWidth, mViewportHeight; + // Controls if hardware acceleration should be used + // Derived from the experimental flag "-experimentalHardwareAccelerationForAndroid" + private boolean mIsHardwareAccelerationEnabled = false; private final float EPSILON = 0.001f; private AlexaVectorDrawable(GraphicContainerElement element) { mVectorState = new VectorDrawableCompatState(element); RenderingContext rc = mVectorState.mPathRenderer.getRootGroup().getRenderingContext(); - mViewportWidth = rc.getMetricsTransform().toViewhost(mVectorState.mPathRenderer.getRootGroup().getViewportWidthActual()); - mViewportHeight = rc.getMetricsTransform().toViewhost(mVectorState.mPathRenderer.getRootGroup().getViewportHeightActual()); + mIsHardwareAccelerationEnabled = rc.isHardwareAccelerationForVectorGraphicsEnabled(); } @VisibleForTesting @@ -81,8 +82,7 @@ private AlexaVectorDrawable(GraphicContainerElement element) { GraphicContainerElement e = mVectorState.mPathRenderer.getRootGroup(); if(e != null) { RenderingContext rc = e.getRenderingContext(); - mViewportWidth = rc.getMetricsTransform().toViewhost(e.getViewportWidthActual()); - mViewportHeight = rc.getMetricsTransform().toViewhost(e.getViewportHeightActual()); + mIsHardwareAccelerationEnabled = rc.isHardwareAccelerationForVectorGraphicsEnabled(); } } @@ -180,40 +180,18 @@ public void draw(@NonNull Canvas canvas) { // we offset to (0, 0); mTmpBounds.offsetTo(0, 0); - // Draw directly into the canvas and enable hardware acceleration for better fluidity for the following cases: - // 1. For cases with proportional scaling (Scale Types: best-fit, best-fill and None) and where scale_x = scale_y in scaling Matrix with no Skew and no drop Shadow Filter - // Draw into a bimap backed canvas for the following cases: - // 1. For cases with non-uniform scaling (scale_x and scale_y are non-equal) / (Scale Type: fill) OR - // 2. For cases where there is a drop Shadow Filter Present OR - // 3. For cases where there is skew present - // 4. For cases where there is grow/shrink/stretch present - boolean useHardwareAcceleration = (mScale == null || mScale != VectorGraphicScale.kVectorGraphicScaleFill) - && !mVectorState.mPathRenderer.getRootGroup().doesMapContainFilters() - && !mVectorState.mPathRenderer.getRootGroup().doesMapContainsSkew() - && !mVectorState.mPathRenderer.getRootGroup().doesMapContainNonUniformScaling() - && doesUniformScaling(scaledWidth, scaledHeight); - - if (useHardwareAcceleration) { + if (mIsHardwareAccelerationEnabled) { + Log.d(TAG, "Using hardware acceleration for AVG rendering"); canvas.clipRect(mTmpBounds); mVectorState.drawAVGToCanvas(canvas, (int) scaledWidth, (int) scaledHeight,true); } else { + Log.d(TAG, "Using software acceleration for AVG rendering"); mVectorState.createOrEraseCachedBitmap((int) scaledWidth, (int) scaledHeight); mVectorState.drawCachedBitmapWithRootAlpha(canvas, colorFilter, mTmpBounds); } canvas.restoreToCount(saveCount); } - //For cases where there is grow/shrink/stretch present - private boolean doesUniformScaling(float scaledWidth, float scaledHeight) { - float mScaledWidth = scaledWidth / mViewportWidth; - float mScaledHeight = scaledHeight / mViewportHeight; - // Check if mScaledWidth is not equal to mScaledHeight - if (Math.abs(mScaledWidth - mScaledHeight) > EPSILON) { - return false; - } - return true; - } - @Override public int getAlpha() { return mVectorState.mPathRenderer.getRootAlpha(); @@ -429,8 +407,8 @@ private void drawAVGToCachedBitmap() { setDirty(false); } - void drawAVGToCanvas(Canvas canvas, int width, int height, boolean uniformScaling) { - mPathRenderer.draw(canvas, width, height, mBitmapFactory, uniformScaling); + void drawAVGToCanvas(Canvas canvas, int width, int height, boolean useHardwareAcceleration) { + mPathRenderer.draw(canvas, width, height, mBitmapFactory, useHardwareAcceleration); setDirty(false); } diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElement.java b/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElement.java index e54087b2..7ae7e175 100644 --- a/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElement.java +++ b/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElement.java @@ -182,66 +182,6 @@ boolean containsFilters() { return mFilters.size() > 0; } - /** - * Checks whether this AVG object has any skew associated with it - * - * @return true if yes, otherwise false - */ - boolean containsSkew() { - if (mProperties.hasProperty(kGraphicPropertyTransform)) { - Matrix transformMatrix = mProperties.getTransform(kGraphicPropertyTransform); - float[] matrixValues = new float[9]; - transformMatrix.getValues(matrixValues); - - float skewX = matrixValues[Matrix.MSKEW_X]; - float skewY = matrixValues[Matrix.MSKEW_Y]; - - if (skewX != 0 || skewY != 0) { - return true; - } - return false; - } - return false; - } - - /** - * Checks whether this AVG object has any non-uniform scaling - * - * @return true if yes, otherwise false - */ - boolean containsNonUniformScaling() { - if (mProperties.hasProperty(kGraphicPropertyTransform)) { - Matrix transformMatrix = mProperties.getTransform(kGraphicPropertyTransform); - float[] matrixValues = new float[9]; - transformMatrix.getValues(matrixValues); - - float scaleX = matrixValues[Matrix.MSCALE_X]; - float scaleY = matrixValues[Matrix.MSCALE_Y]; - - if (scaleX != scaleY) { - return true; - } - return false; - } - return false; - } - - /** - * Checks whether the hierarchy of this group contains a filter - * @return true if the hierarchy contains a filter, false otherwise - */ - boolean doesMapContainFilters() { - return mGraphicElementMap.doesMapContainFilters(); - } - - boolean doesMapContainsSkew(){ - return mGraphicElementMap.doesMapContainSkew(); - } - - boolean doesMapContainNonUniformScaling(){ - return mGraphicElementMap.doesMapContainNonUniformScaling(); - } - /** * Update cached properties when Graphic is marked dirty. */ diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElementMap.java b/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElementMap.java index 8ebda64c..425509a8 100644 --- a/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElementMap.java +++ b/apl/src/main/java/com/amazon/apl/android/graphic/GraphicElementMap.java @@ -17,23 +17,6 @@ class GraphicElementMap { private final SparseArray mUniqueIdToGraphicElementMap = new SparseArray<>(); private GraphicContainerElement mRoot; - /** - * Keeps track of whether an AVG object has a filter - */ - private boolean mContainsFilters; - - private boolean mContainsSkew; - - private boolean mContainsNonUniformScaling; - - boolean doesMapContainFilters() { - return mContainsFilters; - } - - boolean doesMapContainSkew(){ return mContainsSkew;} - - boolean doesMapContainNonUniformScaling(){ return mContainsNonUniformScaling;} - /** * Puts a {@link GraphicElement} in the map of unique ids to GraphicElements. * @@ -41,18 +24,6 @@ boolean doesMapContainFilters() { */ void put(@NonNull GraphicElement element) { mUniqueIdToGraphicElementMap.put(element.getUniqueId(), element); - // The following boolean is set when any element in the hierarchy contains a filter - if (!mContainsFilters) { - mContainsFilters = element.containsFilters(); - } - - if(!mContainsSkew){ - mContainsSkew = element.containsSkew(); - } - - if(!mContainsNonUniformScaling){ - mContainsNonUniformScaling = element.containsNonUniformScaling(); - } } /** diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/GraphicPathElement.java b/apl/src/main/java/com/amazon/apl/android/graphic/GraphicPathElement.java index 0996f403..61995c57 100644 --- a/apl/src/main/java/com/amazon/apl/android/graphic/GraphicPathElement.java +++ b/apl/src/main/java/com/amazon/apl/android/graphic/GraphicPathElement.java @@ -218,4 +218,4 @@ private void applyPath() { getRenderingContext().getPathCache().put(pathData, new WeakReference<>(mPath)); } } -} \ No newline at end of file +} diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/PathRenderer.java b/apl/src/main/java/com/amazon/apl/android/graphic/PathRenderer.java index 2d3262fc..1532352b 100644 --- a/apl/src/main/java/com/amazon/apl/android/graphic/PathRenderer.java +++ b/apl/src/main/java/com/amazon/apl/android/graphic/PathRenderer.java @@ -131,11 +131,11 @@ GraphicContainerElement getRootGroup() { * @param w the width of the avg * @param h the height of the avg */ - void draw(@NonNull Canvas canvas, int w, int h, @NonNull IBitmapFactory bitmapFactory, boolean uniformScaling) { + void draw(@NonNull Canvas canvas, int w, int h, @NonNull IBitmapFactory bitmapFactory, boolean useHardwareAcceleration) { mScaledWidth = w / mViewportWidth; mScaledHeight = h / mViewportHeight; // Traverse the tree in pre-order to draw. - drawGraphicElement(mScaledWidth, mScaledHeight, IDENTITY_MATRIX, mRootGroup, canvas, (float)mRootAlpha / 255, bitmapFactory, uniformScaling); + drawGraphicElement(mScaledWidth, mScaledHeight, IDENTITY_MATRIX, mRootGroup, canvas, (float)mRootAlpha / 255, bitmapFactory, useHardwareAcceleration); } @Nullable @@ -175,7 +175,7 @@ private static void drawGroupTree(final float xScale, final float yScale, @NonNull final Canvas canvas, final float currentOpacity, @NonNull final IBitmapFactory bitmapFactory, - boolean uniformScaling) { + boolean useHardwareAcceleration) { // Calculate current group's matrix by preConcat the parent's and // and the current one on the top of the stack. @@ -198,7 +198,7 @@ private static void drawGroupTree(final float xScale, final float yScale, // Draw the group tree in the same order as the AVG tree. for (GraphicElement child : currentGroup.getChildren()) { - drawGraphicElement(xScale, yScale, currentGroup.getStackedMatrix(), child, canvas, stackedOpacity, bitmapFactory, uniformScaling); + drawGraphicElement(xScale, yScale, currentGroup.getStackedMatrix(), child, canvas, stackedOpacity, bitmapFactory, useHardwareAcceleration); } canvas.restore(); @@ -210,7 +210,7 @@ private static void drawGraphicElement(final float xScale, final float yScale, @NonNull final Canvas parentCanvas, final float currentOpacity, @NonNull final IBitmapFactory bitmapFactory, - boolean uniformScaling) { + boolean useHardwareAcceleration) { Bitmap bitmap = getFilterBitmap(parentCanvas.getWidth(), parentCanvas.getHeight(), graphicElement, bitmapFactory); Canvas canvas = parentCanvas; @@ -223,10 +223,10 @@ private static void drawGraphicElement(final float xScale, final float yScale, if (graphicElement instanceof GraphicGroupElement) { GraphicGroupElement graphicGroupElement = (GraphicGroupElement) graphicElement; drawGroupTree(xScale, yScale, currentTransform, - graphicGroupElement, canvas, currentOpacity, bitmapFactory, uniformScaling); + graphicGroupElement, canvas, currentOpacity, bitmapFactory, useHardwareAcceleration); } else if (graphicElement instanceof GraphicPathElement) { GraphicPathElement graphicPathElement = (GraphicPathElement) graphicElement; - drawPath(xScale, yScale, currentTransform, graphicPathElement, canvas, currentOpacity, uniformScaling); + drawPath(xScale, yScale, currentTransform, graphicPathElement, canvas, currentOpacity, useHardwareAcceleration); } else if (graphicElement instanceof GraphicTextElement) { GraphicTextElement graphicTextElement = (GraphicTextElement) graphicElement; drawText(xScale, yScale, currentTransform, graphicTextElement, canvas, currentOpacity); @@ -281,7 +281,7 @@ private static void drawPath(final float xScale, final float yScale, @NonNull final GraphicPathElement pathElement, @NonNull final Canvas canvas, final float stackedOpacity, - boolean uniformScaling) { + boolean useHardwareAcceleration) { Matrix scaledTransform = scaleMatrix(currentTransform, xScale, yScale); final float matrixScale = getMatrixScale(currentTransform); @@ -291,8 +291,8 @@ private static void drawPath(final float xScale, final float yScale, } Path currentPath; - // if the scaling is uniform scale the paths for hardware rendering - if (uniformScaling) { + // Scale the paths for hardware rendering + if (useHardwareAcceleration) { // create a new Path because we still want to reference the original unscaled Path currentPath = new Path(pathElement.getPath()); currentPath.transform(scaledTransform); @@ -306,7 +306,7 @@ private static void drawPath(final float xScale, final float yScale, Paint fillPaint = pathElement.getFillPaint(stackedOpacity); Shader fillShader = fillPaint.getShader(); - if (uniformScaling && fillShader != null) { + if (useHardwareAcceleration && fillShader != null) { if (pathElement.getProperties().isGradient(GraphicPropertyKey.kGraphicPropertyFill)) { if (pathElement.getGradient(GraphicPropertyKey.kGraphicPropertyFill).getType() == GradientType.RADIAL) { // Gradient is radial so we obtain a new Shader, same is not needed for Linear gradient @@ -324,7 +324,7 @@ private static void drawPath(final float xScale, final float yScale, Paint strokePaint = pathElement.getStrokePaint(stackedOpacity); Shader strokeShader = strokePaint.getShader(); - if (uniformScaling && strokeShader != null) { + if (useHardwareAcceleration && strokeShader != null) { if (pathElement.getProperties().isGradient(GraphicPropertyKey.kGraphicPropertyStroke)) { if (pathElement.getGradient(GraphicPropertyKey.kGraphicPropertyStroke).getType() == GradientType.RADIAL) { // Gradient is radial so we obtain a new Shader, same is not needed for Linear gradient @@ -336,8 +336,8 @@ private static void drawPath(final float xScale, final float yScale, strokeShader.setLocalMatrix(strokeTransform); } - // If the scaling is uniform, then the stroke properties need to be adjusted for hardware acceleration - if (uniformScaling) { + // Adjust stroke properties for hardware acceleration + if (useHardwareAcceleration) { float scaleFactor = getMatrixScale(scaledTransform); // Adjust stroke width float scaledStrokeWidth = pathElement.getStrokeWidth() * scaleFactor; diff --git a/apl/src/main/java/com/amazon/apl/android/graphic/RenderableGraphicElement.java b/apl/src/main/java/com/amazon/apl/android/graphic/RenderableGraphicElement.java deleted file mode 100644 index 6f51b9f8..00000000 --- a/apl/src/main/java/com/amazon/apl/android/graphic/RenderableGraphicElement.java +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ -package com.amazon.apl.android.graphic; - -import android.graphics.Color; -import android.graphics.Matrix; -import android.graphics.Paint; - -import com.amazon.apl.android.PropertyMap; - -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyFill; -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyFillOpacity; -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyFillTransform; -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyStroke; -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyStrokeOpacity; -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyStrokeTransform; -import static com.amazon.apl.enums.GraphicPropertyKey.kGraphicPropertyStrokeWidth; - -/** - * Common interface for graphic elements which can be stroked/filled. - */ -interface RenderableGraphicElement { - - /** - * The properties for the RenderableGraphicElement. - */ - PropertyMap getProperties(); - - /** - * The fill pattern. Only applicable when the fill type is Pattern. - */ - GraphicPattern getFillGraphicPattern(); - - /** - * The stroke pattern. Only applicable when the stroke type is Pattern. - */ - GraphicPattern getStrokeGraphicPattern(); - - /** - * The opacity of the element's fill. - */ - default float getFillOpacity() { - return getProperties().getFloat(kGraphicPropertyFillOpacity); - } - - /** - * Transformation applied against the fill. Only applicable if fill type is Gradient or Pattern. - */ - default Matrix getFillTransform() { - return getProperties().getTransform(kGraphicPropertyFillTransform); - } - - /** - * The color of the element's fill. Only applicable when the fill type is Color. - */ - default int getFillColor() { - return getProperties().getColor(kGraphicPropertyFill); - } - - /** - * The opacity of the element's stroke. - */ - default float getStrokeOpacity() { - return getProperties().getFloat(kGraphicPropertyStrokeOpacity); - } - - /** - * Transformation applied against the stroke. Only applicable if stroke type is Gradient or - * Pattern. - */ - default Matrix getStrokeTransform() { - return getProperties().getTransform(kGraphicPropertyStrokeTransform); - } - - /** - * The color of the element's stroke. Only applicable when the stroke type is Color. - */ - default int getStrokeColor() { - return getProperties().getColor(kGraphicPropertyStroke); - } - - /** - * The width of the stroke. - */ - default float getStrokeWidth() { - return getProperties().getFloat(kGraphicPropertyStrokeWidth); - } - - - default Paint getFillPaint() { - Paint fillPaint = new Paint(Paint.ANTI_ALIAS_FLAG); - fillPaint.setStyle(Paint.Style.FILL); - if (getProperties().isGraphicPattern(kGraphicPropertyFill)) { - fillPaint.setShader(PathRenderer.createPattern(getFillTransform(), getFillGraphicPattern())); - fillPaint.setAlpha((int)(255 * getFillOpacity())); - } else if (getProperties().isColor(kGraphicPropertyFill)) { - fillPaint.setColor(applyAlpha(getFillColor(), getFillOpacity())); - } else { - // gradient shader is applied during draw call. - fillPaint.setAlpha((int)(255 * getFillOpacity())); - } - return fillPaint; - } - - default Paint getStrokePaint() { - Paint strokePaint = new Paint(Paint.ANTI_ALIAS_FLAG); - strokePaint.setStyle(Paint.Style.STROKE); - strokePaint.setStrokeWidth(getStrokeWidth()); - if (getProperties().isGraphicPattern(kGraphicPropertyStroke)) { - strokePaint.setShader(PathRenderer.createPattern(getStrokeTransform(), - getStrokeGraphicPattern())); - strokePaint.setAlpha((int)(255 * getStrokeOpacity())); - } else if (getProperties().isColor(kGraphicPropertyStroke)) { - strokePaint.setColor(applyAlpha(getStrokeColor(), getStrokeOpacity())); - } else { - // gradient shader is applied during draw call. - strokePaint.setAlpha((int)(255 * getStrokeOpacity())); - } - return strokePaint; - } - - static int applyAlpha(final int color, final float alpha) { - int alphaBytes = Color.alpha(color); - int colorWithAlphaApplied = color & 0x00FFFFFF; - colorWithAlphaApplied |= ((int) (alphaBytes * alpha)) << 24; - return colorWithAlphaApplied; - } -} diff --git a/apl/src/main/java/com/amazon/apl/android/primitive/AccessibilityAdjustableRange.java b/apl/src/main/java/com/amazon/apl/android/primitive/AccessibilityAdjustableRange.java new file mode 100644 index 00000000..0c6da761 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/primitive/AccessibilityAdjustableRange.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.android.primitive; + +import com.amazon.common.BoundObject; +import com.amazon.apl.enums.APLEnum; +import com.google.auto.value.AutoValue; + +/** + * AccessibilityAdjustableRange Property + * Provide values as rangeInfo to accessibility Node to expose to screen reader + */ +@AutoValue +public abstract class AccessibilityAdjustableRange { + public abstract float minValue(); + public abstract float maxValue(); + public abstract float currentValue(); + + public static AccessibilityAdjustableRange create(BoundObject boundObject, APLEnum propertyKey) { + return new AutoValue_AccessibilityAdjustableRange( + nGetMinValue(boundObject.getNativeHandle(), propertyKey.getIndex()), + nGetMaxValue(boundObject.getNativeHandle(), propertyKey.getIndex()), + nGetCurrentValue(boundObject.getNativeHandle(), propertyKey.getIndex()) + ); + } + + public static AccessibilityAdjustableRange create(float minValue, float maxValue, float currentValue) { + return new AutoValue_AccessibilityAdjustableRange(minValue, maxValue, currentValue); + } + + private static native float nGetMinValue(long componentHandle, int componentPropertyKey); + private static native float nGetMaxValue(long componentHandle, int componentPropertyKey); + private static native float nGetCurrentValue(long componentHandle, int componentPropertyKey); + private static native Object nGetAdjustableValue(long componentHandle, int componentPropertyKey); +} diff --git a/apl/src/main/java/com/amazon/apl/android/providers/ILocalTimeOffsetProvider.java b/apl/src/main/java/com/amazon/apl/android/providers/ILocalTimeOffsetProvider.java new file mode 100644 index 00000000..7db55f83 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/providers/ILocalTimeOffsetProvider.java @@ -0,0 +1,18 @@ +package com.amazon.apl.android.providers; + +import java.lang.ref.WeakReference; + +/** + * Provider that will call subscribed listeners with the new local time offset if it changes due changes + * in the default timezone (this includes daylight saving changes). + */ +public interface ILocalTimeOffsetProvider { + + void addListener(WeakReference listener); + + long getCurrentOffset(); + + interface LocalTimeOffsetChangedListener { + void localTimeOffsetChanged(long newOffset); + } +} diff --git a/apl/src/main/java/com/amazon/apl/android/providers/impl/BroadcastLocalTimeOffsetProvider.java b/apl/src/main/java/com/amazon/apl/android/providers/impl/BroadcastLocalTimeOffsetProvider.java new file mode 100644 index 00000000..39f82bf1 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/providers/impl/BroadcastLocalTimeOffsetProvider.java @@ -0,0 +1,80 @@ +package com.amazon.apl.android.providers.impl; + +import android.content.BroadcastReceiver; +import android.content.Context; +import android.content.Intent; +import android.content.IntentFilter; +import android.util.Log; + +import com.amazon.apl.android.providers.ILocalTimeOffsetProvider; + +import java.lang.ref.WeakReference; +import java.util.ArrayList; +import java.util.Calendar; +import java.util.ListIterator; +import java.util.TimeZone; + +/** + * This BroadcastLocalTimeOffsetProvider works by determining timezone and DST changes via + * monitoring changes in the local time when Intent.ACTION_TIME_TICK, + * Intent.ACTION_TIMEZONE_CHANGED, or ACTION_TIME_CHANGED is broadcast from the OS. + */ +public class BroadcastLocalTimeOffsetProvider extends BroadcastReceiver implements ILocalTimeOffsetProvider { + private final String TAG = BroadcastLocalTimeOffsetProvider.class.getSimpleName(); + + private long mCurrentOffset; + private final ArrayList> mListeners = new ArrayList<>(); + + public BroadcastLocalTimeOffsetProvider(Context context) { + mCurrentOffset = getCurrentOffset(); + + IntentFilter intentFilter = new IntentFilter(); + + intentFilter.addAction(Intent.ACTION_TIMEZONE_CHANGED); + intentFilter.addAction(Intent.ACTION_TIME_CHANGED); + intentFilter.addAction(Intent.ACTION_TIME_TICK); + + context.registerReceiver(this, intentFilter); + } + + @Override + public void addListener(WeakReference listener) { + synchronized (mListeners) { + mListeners.add(listener); + } + } + + @Override + public long getCurrentOffset() { + // Using TimeZone is the best way to calculate this but the TimeZone object was only + // added in API 24. + //return TimeZone.getDefault().getOffset(System.currentTimeMillis()); + Calendar now = Calendar.getInstance(); + return now.get(Calendar.ZONE_OFFSET) + now.get(Calendar.DST_OFFSET); + } + + @Override + public void onReceive(Context context, Intent intent) { + long offset = getCurrentOffset(); + + if (mCurrentOffset != offset) { + Log.i(TAG, "Time offset change detected, notifying all listeners."); + mCurrentOffset = offset; + + synchronized (mListeners) { + ListIterator> iter = mListeners.listIterator(); + while (iter.hasNext()) { + WeakReference weakListener = iter.next(); + + LocalTimeOffsetChangedListener listener = weakListener.get(); + + if (listener == null) { + iter.remove(); + } else { + listener.localTimeOffsetChanged(mCurrentOffset); + } + } + } + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/android/providers/impl/LoggingTelemetryProvider.java b/apl/src/main/java/com/amazon/apl/android/providers/impl/LoggingTelemetryProvider.java index f9bcfe08..cbad4284 100644 --- a/apl/src/main/java/com/amazon/apl/android/providers/impl/LoggingTelemetryProvider.java +++ b/apl/src/main/java/com/amazon/apl/android/providers/impl/LoggingTelemetryProvider.java @@ -11,6 +11,7 @@ import androidx.annotation.VisibleForTesting; import com.amazon.apl.android.providers.ITelemetryProvider; +import com.amazon.apl.android.utils.MetricInfo; import java.util.ArrayList; import java.util.Collections; @@ -246,14 +247,15 @@ public Metric getMetric(int id) { } /** - * A synchronized method that returns a one-time copy of the metrics + * Method that returns all timer related metrics. The value of each metric should be in Milliseconds. * - * @return one-time copy of the mMetrics. + * @return A Thread safe List of performance metrics. */ - public synchronized List getMetricsCopy() { - List copyOfMetrics = Collections.synchronizedList(new ArrayList<>()); + public synchronized List getPerformanceMetrics() { + List copyOfMetrics = Collections.synchronizedList(new ArrayList<>()); for (Metric metric: mMetrics){ - copyOfMetrics.add(metric); + MetricInfo metricInfo = new MetricInfo(metric.metricName, TimeUnit.MILLISECONDS.convert(metric.totalTime, TimeUnit.NANOSECONDS)); + copyOfMetrics.add(metricInfo); } return copyOfMetrics; } diff --git a/apl/src/main/java/com/amazon/apl/android/utils/FrameStat.java b/apl/src/main/java/com/amazon/apl/android/utils/FrameStat.java new file mode 100644 index 00000000..6be450ca --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/utils/FrameStat.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.android.utils; + +import org.json.JSONException; +import org.json.JSONObject; + +/** + * Collection of frame begin and end timestamps + */ +public class FrameStat { + // Timestamp in nanoseconds since a platform dependent arbitrary time base, marking the beginning of the frame loop. + long begin; + // Timestamp in nanoseconds since a platform dependent arbitrary time base, marking the ending of the frame loop. The time base is the same as the one used for begin timestamp. + long end; + + public FrameStat(long begin, long end) { + this.begin = begin; + this.end = end; + } + + public JSONObject toJSON() throws JSONException { + JSONObject jsonObject = new JSONObject(); + jsonObject.put("begin", begin); + jsonObject.put("end", end); + return jsonObject; + } +} diff --git a/apl/src/main/java/com/amazon/apl/android/utils/LazyImageLoader.java b/apl/src/main/java/com/amazon/apl/android/utils/LazyImageLoader.java index 8139e880..c1d45d61 100644 --- a/apl/src/main/java/com/amazon/apl/android/utils/LazyImageLoader.java +++ b/apl/src/main/java/com/amazon/apl/android/utils/LazyImageLoader.java @@ -6,6 +6,7 @@ package com.amazon.apl.android.utils; import android.graphics.Bitmap; +import android.os.SystemClock; import android.util.Log; import com.amazon.apl.android.Image; @@ -13,6 +14,8 @@ import com.amazon.apl.android.dependencies.IImageLoader; import com.amazon.apl.android.primitive.UrlRequests; import com.amazon.apl.android.views.APLImageView; +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.ImageScale; import java.util.ArrayList; @@ -22,19 +25,20 @@ public class LazyImageLoader { private static final String TAG = "LazyImageLoader"; + private static IDTNetworkRequestHandler mDTNetworkRequestHandler; /** * Starts the image load. * * @param view The view to display the image. */ - public static void initImageLoad(ImageViewAdapter adapter, Image image, APLImageView view) { + public static void initImageLoad(ImageViewAdapter adapter, Image image, APLImageView view, IDTNetworkRequestHandler dtNetworkRequestHandler) { if (view.getDrawable() != null) { view.setImageDrawable(null); // Clear the resources for this view if we're loading a new bitmap LazyImageLoader.clearImageResources(adapter, image, view); } - + mDTNetworkRequestHandler = dtNetworkRequestHandler; boolean needsScaling = (image.getScale() != ImageScale.kImageScaleNone); IImageLoader provider = image.getImageLoader(view.getContext()); @@ -83,7 +87,15 @@ private static class ImageLoad { public void load() { for (int i = 0; i < mSources.size(); i++) { final int index = i; - IImageLoader.LoadImageCallback2 callback = new LoadImageCallback(mImageView, (bitmap) -> { + final String url = mSources.get(index).url(); + int requestId = IDTNetworkRequestHandler.IdGenerator.generateId(); + // We should only pass a networkRequestHandler, when source is a URL request. + IDTNetworkRequestHandler networkRequestHandler = null; + if (mDTNetworkRequestHandler != null && IDTNetworkRequestHandler.isUrlRequest(url)) { + networkRequestHandler = mDTNetworkRequestHandler; + mDTNetworkRequestHandler.requestWillBeSent(requestId, SystemClock.elapsedRealtimeNanos(), url, DTNetworkRequestType.IMAGE); + } + IImageLoader.LoadImageCallback2 callback = new LoadImageCallback(mImageView, networkRequestHandler, requestId, (bitmap) -> { mBitmaps[index] = bitmap; boolean allLoaded = true; for (Bitmap loaded : mBitmaps) { @@ -121,17 +133,24 @@ public void load() { private static class LoadImageCallback implements IImageLoader.LoadImageCallback2 { private final APLImageView mImageView; private final BitmapAcceptor mResult; + private final int mRequestId; + private final IDTNetworkRequestHandler mDTNetworkRequestHandler; private boolean mHasReturned = false; - LoadImageCallback(APLImageView imageView, BitmapAcceptor result) { + LoadImageCallback(APLImageView imageView, IDTNetworkRequestHandler dtNetworkRequest, int requestId, BitmapAcceptor result) { mImageView = imageView; mResult = result; + mDTNetworkRequestHandler = dtNetworkRequest; + mRequestId = requestId; } @Override public synchronized void onSuccess(Bitmap bitmap, String source) { mImageView.getPresenter().mediaLoaded(source); setBitmap(bitmap); + if (mDTNetworkRequestHandler != null) { + mDTNetworkRequestHandler.loadingFinished(mRequestId, SystemClock.elapsedRealtimeNanos(), bitmap.getByteCount()); + } } @Override @@ -148,6 +167,9 @@ public synchronized void onError(Exception exception, int errorCode, String sour mImageView.getPresenter().mediaLoadFailed(source, errorCode, errorMessage); Log.e(TAG, "error loading image", exception); renderErrorBitmap(source); + if (mDTNetworkRequestHandler != null) { + mDTNetworkRequestHandler.loadingFailed(mRequestId, SystemClock.elapsedRealtimeNanos()); + } } private synchronized void renderErrorBitmap(String source) { diff --git a/apl/src/main/java/com/amazon/apl/android/utils/MetricInfo.java b/apl/src/main/java/com/amazon/apl/android/utils/MetricInfo.java new file mode 100644 index 00000000..dd007f94 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/android/utils/MetricInfo.java @@ -0,0 +1,27 @@ + /* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + + package com.amazon.apl.android.utils; + + /** + * Class to provide information about a metric. + */ + public class MetricInfo { + private final String mName; + private final long mValue; + + public MetricInfo(String name, long value) { + mName = name; + mValue = value; + } + + public String getName() { + return mName; + } + + public long getValue() { + return mValue; + } + } diff --git a/apl/src/main/java/com/amazon/apl/android/views/APLAbsoluteLayout.java b/apl/src/main/java/com/amazon/apl/android/views/APLAbsoluteLayout.java index 9e2e3931..3d7ddeec 100644 --- a/apl/src/main/java/com/amazon/apl/android/views/APLAbsoluteLayout.java +++ b/apl/src/main/java/com/amazon/apl/android/views/APLAbsoluteLayout.java @@ -103,7 +103,7 @@ protected void onMeasure(int widthMeasureSpec, int heightMeasureSpec) { int count = getChildCount(); for (int i = 0; i < count; i++) { View child = getChildAt(i); - if (child.getVisibility() != GONE) { + if (child.getVisibility() != GONE && child.getLayoutParams() != null) { ViewGroup.LayoutParams lp = child.getLayoutParams(); child.measure(MeasureSpec.makeMeasureSpec(lp.width, MeasureSpec.EXACTLY), MeasureSpec.makeMeasureSpec(lp.height, MeasureSpec.EXACTLY)); diff --git a/apl/src/main/java/com/amazon/apl/devtools/controllers/DTConnection.java b/apl/src/main/java/com/amazon/apl/devtools/controllers/DTConnection.java new file mode 100644 index 00000000..e3582b69 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/controllers/DTConnection.java @@ -0,0 +1,108 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.controllers; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.Event; +import com.amazon.apl.devtools.models.common.Request; +import com.amazon.apl.devtools.models.common.Response; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.error.ErrorResponse; +import com.amazon.apl.devtools.util.CommandRequestFactory; +import com.amazon.apl.devtools.util.RequestStatus; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.Collection; +import java.util.HashMap; +import java.util.Map; + +/** + * DTConnection is a wrapper for a WebSocket. + * DTConnection stores the Sessions created by a single web socket client. + * A DTConnection can have many Sessions. + */ +public class DTConnection { + private static final String TAG = DTConnection.class.getSimpleName(); + private final CommandRequestFactory mCommandRequestFactory; + private final Map mRegisteredSessionsMap = new HashMap<>(); + + public DTConnection(CommandRequestFactory commandRequestFactory) { + Log.i(TAG, "Wrapping web socket in a connection"); + mCommandRequestFactory = commandRequestFactory; + } + + public void registerSession(Session session) { + mRegisteredSessionsMap.put(session.getSessionId(), session); + Log.i(TAG, "registerSession: Connection has " + mRegisteredSessionsMap.size() + + " sessions"); + } + + public void unregisterSession(String sessionId) { + mRegisteredSessionsMap.remove(sessionId); + Log.i(TAG, "unregisterSession: Connection has " + mRegisteredSessionsMap.size() + + " sessions"); + } + + public boolean hasSession(String sessionId) { + return mRegisteredSessionsMap.containsKey(sessionId); + } + + public Session getSession(String sessionId) { + return mRegisteredSessionsMap.get(sessionId); + } + + protected void destroyRegisteredSessions() { + Collection sessions = mRegisteredSessionsMap.values(); + for (Session session : sessions) { + session.destroy(); + } + } + + public void handleMessage(String message) { + Log.i(TAG, "Message received by connection"); + try { + JSONObject obj = new JSONObject(message); + Request commandRequest = + mCommandRequestFactory.createCommandRequest(obj, this); + // Passing a callback to send a response after executing + commandRequest.execute(this::sendResponse); + } catch (DTException e) { + Log.e(TAG, "Error creating command request", e); + Response errorResponse = new ErrorResponse(e); + sendResponse(errorResponse); + } catch (JSONException e) { + Log.e(TAG, "Error deserializing command request", e); + // Sending error response with id of 0 because the invalid request might not have an id + Response errorResponse = new ErrorResponse(0, DTError.INVALID_COMMAND.getErrorCode(), + DTError.INVALID_COMMAND.getErrorMsg()); + sendResponse(errorResponse); + } + } + + private void sendResponse(TResponse response, RequestStatus requestStatus) { + if (requestStatus.getExecutionStatus() == RequestStatus.ExecutionStatus.FAILED) { + final DTError error = requestStatus.getError(); + Response errorResponse = new ErrorResponse(requestStatus.getId(), error.getErrorCode(), error.getErrorMsg()); + sendResponse(errorResponse); + return; + } + + sendResponse(response); + } + + protected void sendResponse(TResponse response) { + //no op + } + + public void sendEvent(TEvent event) { + //no op + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/controllers/DTServer.java b/apl/src/main/java/com/amazon/apl/devtools/controllers/DTServer.java new file mode 100644 index 00000000..790d6167 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/controllers/DTServer.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.controllers; + +/** + * Any APL runtime can use this class to start and stop the server in order to have the + * dev tools protocol enable/disable. + */ +public class DTServer { + + private static final String TAG = DTServer.class.getSimpleName(); + private static final int TIMEOUT_IN_SECONDS = 5; + private static DTServer sInstance; + + public DTServer() {} + + public static DTServer getInstance() { + if (sInstance == null) { + sInstance = new DTServer(); + } + return sInstance; + } + + /** + * Starts the server. + * + * @param portNumber The port number to start the server on. + */ + public void start(int portNumber) { + //no op + } + + /** + * Stops the server and does any necessary clean up. + */ + public void stop() { + // no op + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/controllers/DTServerData.java b/apl/src/main/java/com/amazon/apl/devtools/controllers/DTServerData.java new file mode 100644 index 00000000..38b2cffe --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/controllers/DTServerData.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.controllers; + +/** + * This class is to track the necessary data of the DTServer that we don't want to expose to any runtimes, + * like retry count and port number. + */ +public class DTServerData { + private static final int DEFAULT_PORT = 9092; + + /** + * The maximum of times to retry to start the server. + */ + public static final int MAX_RETRIES = 15; + private static DTServerData sInstance; + private int mPortNumer = DEFAULT_PORT; + private int mRetryCount = 0; + + private DTServerData() {} + + public static DTServerData getInstance() { + if (sInstance == null) { + sInstance = new DTServerData(); + } + return sInstance; + } + + public void setPortNumber(int portNumber) { + mPortNumer = portNumber; + } + + public void incrementRetryCount() { + mRetryCount++; + } + + public int getPortNumber() { + return mPortNumer; + } + + public int getRetryCount() { + return mRetryCount; + } + + public void reset() { + mRetryCount = 0; + mPortNumer = DEFAULT_PORT; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/enums/CommandMethod.java b/apl/src/main/java/com/amazon/apl/devtools/enums/CommandMethod.java new file mode 100644 index 00000000..c498d456 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/enums/CommandMethod.java @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.enums; + +import androidx.annotation.NonNull; + +public enum CommandMethod { + EMPTY(""), + TARGET_GET_TARGETS(CommandMethod.TARGET_GET_TARGETS_TEXT), + TARGET_ATTACH_TO_TARGET(CommandMethod.TARGET_ATTACH_TO_TARGET_TEXT), + VIEW_SET_DOCUMENT(CommandMethod.VIEW_SET_DOCUMENT_TEXT), + VIEW_CAPTURE_IMAGE(CommandMethod.VIEW_CAPTURE_IMAGE_TEXT), + VIEW_EXECUTE_COMMANDS(CommandMethod.VIEW_EXECUTE_COMMANDS_TEXT), + LIVE_DATA_UPDATE(CommandMethod.LIVE_DATA_UPDATE_TEXT), + PERFORMANCE_GET_METRICS(CommandMethod.PERFORMANCE_GET_METRICS_TEXT), + PERFORMANCE_ENABLE(CommandMethod.PERFORMANCE_ENABLE_TEXT), + PERFORMANCE_DISABLE(CommandMethod.PERFORMANCE_DISABLE_TEXT), + MEMORY_GET_MEMORY(CommandMethod.MEMORY_GET_MEMORY_TEXT), + FRAMEMETRICS_RECORD(CommandMethod.FRAMEMETRICS_RECORD_TEXT), + FRAMEMETRICS_STOP(CommandMethod.FRAMEMETRICS_STOP_TEXT), + LOG_ENABLE(CommandMethod.LOG_ENABLE_TEXT), + LOG_DISABLE(CommandMethod.LOG_DISABLE_TEXT), + LOG_CLEAR(CommandMethod.LOG_CLEAR_TEXT), + DOCUMENT_GET_MAIN_PACKAGE(CommandMethod.DOCUMENT_GET_MAIN_PACKAGE_TEXT), + DOCUMENT_GET_PACKAGE_LIST(CommandMethod.DOCUMENT_GET_PACKAGE_LIST_TEXT), + DOCUMENT_GET_PACKAGE(CommandMethod.DOCUMENT_GET_PACKAGE_TEXT), + DOCUMENT_GET_VISUAL_CONTEXT(CommandMethod.DOCUMENT_GET_VISUAL_CONTEXT_TEXT), + DOCUMENT_GET_DOM(CommandMethod.DOCUMENT_GET_DOM_TEXT), + DOCUMENT_GET_SCENE_GRAPH(CommandMethod.DOCUMENT_GET_SCENE_GRAPH_TEXT), + DOCUMENT_GET_ROOT_CONTEXT(CommandMethod.DOCUMENT_GET_ROOT_CONTEXT_TEXT), + DOCUMENT_GET_CONTEXT(CommandMethod.DOCUMENT_GET_CONTEXT_TEXT), + DOCUMENT_HIGHLIGHT_COMPONENT(CommandMethod.DOCUMENT_HIGHLIGHT_COMPONENT_TEXT), + DOCUMENT_HIDE_HIGHLIGHT(CommandMethod.DOCUMENT_HIDE_HIGHLIGHT_TEXT), + NETWORK_ENABLE(CommandMethod.NETWORK_ENABLE_TEXT), + NETWORK_DISABLE(CommandMethod.NETWORK_DISABLE_TEXT); + + private static final String TARGET_GET_TARGETS_TEXT = "Target.getTargets"; + private static final String TARGET_ATTACH_TO_TARGET_TEXT = "Target.attachToTarget"; + private static final String VIEW_SET_DOCUMENT_TEXT = "View.setDocument"; + private static final String VIEW_CAPTURE_IMAGE_TEXT = "View.captureImage"; + private static final String VIEW_EXECUTE_COMMANDS_TEXT = "View.executeCommands"; + private static final String LIVE_DATA_UPDATE_TEXT = "LiveData.update"; + private static final String PERFORMANCE_GET_METRICS_TEXT = "Performance.getMetrics"; + private static final String PERFORMANCE_ENABLE_TEXT = "Performance.enable"; + private static final String PERFORMANCE_DISABLE_TEXT = "Performance.disable"; + private static final String MEMORY_GET_MEMORY_TEXT = "Memory.getMemory"; + private static final String FRAMEMETRICS_RECORD_TEXT = "FrameMetrics.record"; + private static final String FRAMEMETRICS_STOP_TEXT = "FrameMetrics.stop"; + private static final String LOG_ENABLE_TEXT = "Log.enable"; + private static final String LOG_DISABLE_TEXT = "Log.disable"; + private static final String LOG_CLEAR_TEXT = "Log.clear"; + private static final String DOCUMENT_GET_MAIN_PACKAGE_TEXT = "Document.getMainPackage"; + private static final String DOCUMENT_GET_PACKAGE_LIST_TEXT = "Document.getPackageList"; + private static final String DOCUMENT_GET_PACKAGE_TEXT = "Document.getPackage"; + private static final String DOCUMENT_GET_VISUAL_CONTEXT_TEXT = "Document.getVisualContext"; + private static final String DOCUMENT_GET_DOM_TEXT = "Document.getDOM"; + private static final String DOCUMENT_GET_SCENE_GRAPH_TEXT = "Document.getSceneGraph"; + private static final String DOCUMENT_GET_ROOT_CONTEXT_TEXT = "Document.getRootContext"; + private static final String DOCUMENT_GET_CONTEXT_TEXT = "Document.getContext"; + private static final String DOCUMENT_HIGHLIGHT_COMPONENT_TEXT = "Document.highlightComponent"; + private static final String DOCUMENT_HIDE_HIGHLIGHT_TEXT = "Document.hideHighlight"; + private static final String NETWORK_ENABLE_TEXT = "Network.enable"; + private static final String NETWORK_DISABLE_TEXT = "Network.disable"; + + private final String mCommandMethodText; + + CommandMethod(String commandMethodText) { + mCommandMethodText = commandMethodText; + } + + @NonNull + @Override + public String toString() { + return mCommandMethodText; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/enums/DTError.java b/apl/src/main/java/com/amazon/apl/devtools/enums/DTError.java new file mode 100644 index 00000000..15d3a74a --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/enums/DTError.java @@ -0,0 +1,59 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.enums; + +public enum DTError { + // Command request is in a valid format, but the method specified has not been implemented + METHOD_NOT_IMPLEMENTED(1, "Method Not Implemented."), + + INVALID_SESSION_ID(2, "Invalid Session Id."), + + NO_PERFORMANCE_METRICS(3, "No Performance Metrics."), + + TARGET_ALREADY_ATTACHED(100, "Target Already Attached."), + + TARGET_NOT_ATTACHED(101, "Target Not Attached."), + + NO_SUCH_TARGET(102, "No Such Target."), + + LOG_ALREADY_ENABLED(200, "Log Already Enabled."), + + LOG_ALREADY_DISABLED(201, "Log Already Disabled."), + + // Invalid document provided in a command request, such as in setDocument and executeCommands + INVALID_DOCUMENT(300, "Invalid Document."), + + // Invalid command request format, usually caught and thrown from a JSONException + INVALID_COMMAND(301, "Invalid Command."), + + NETWORK_ALREADY_ENABLED(400, "Network Already Enabled."), + + NETWORK_ALREADY_DISABLED(401, "Network Already Disabled."), + + PERFORMANCE_ALREADY_ENABLED(500, "Performance Already Enabled."), + + PERFORMANCE_ALREADY_DISABLED(501, "Performance Already Disabled."), + + NO_DOCUMENT_RENDERED(502, "No Document Rendered."), + UNKNOWN_ERROR(503, "Unknown Error."), + METHOD_FAILURE(504, "Method Execution Failed."); + + private final int mErrorCode; + private final String mErrorMsg; + + DTError(int code, String errorMsg) { + mErrorCode = code; + mErrorMsg = errorMsg; + } + + public int getErrorCode() { + return mErrorCode; + } + + public String getErrorMsg() { + return mErrorMsg; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/enums/DTNetworkRequestType.java b/apl/src/main/java/com/amazon/apl/devtools/enums/DTNetworkRequestType.java new file mode 100644 index 00000000..5d0d9971 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/enums/DTNetworkRequestType.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.enums; + +/** + * The different DT Network type events that may occur. + */ +public enum DTNetworkRequestType { + PACKAGE("package"), + IMAGE("image"); + + private final String mType; + DTNetworkRequestType(String type) { + mType = type; + } + + public String toString() { + return mType; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/enums/EventMethod.java b/apl/src/main/java/com/amazon/apl/devtools/enums/EventMethod.java new file mode 100644 index 00000000..3b7cdb30 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/enums/EventMethod.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.enums; + +import androidx.annotation.NonNull; + +public enum EventMethod { + VIEW_STATE_CHANGE(EventMethod.VIEW_STATE_CHANGE_TEXT), + LOG_ENTRY_ADDED(EventMethod.LOG_ENTRY_ADDED_TEXT), + NETWORK_REQUEST_WILL_BE_SENT(EventMethod.NETWORK_REQUEST_WILL_BE_SENT_TEXT), + NETWORK_LOADING_FAILED(EventMethod.NETWORK_LOADING_FAILED_TEXT), + NETWORK_LOADING_FINISHED(EventMethod.NETWORK_LOADING_FINISHED_TEXT), + PERFORMANCE_METRIC(EventMethod.PERFORMANCE_METRIC_TEXT); + + private static final String VIEW_STATE_CHANGE_TEXT = "View.stateChange"; + private static final String LOG_ENTRY_ADDED_TEXT = "Log.entryAdded"; + private static final String NETWORK_REQUEST_WILL_BE_SENT_TEXT = "Network.requestWillBeSent"; + private static final String NETWORK_LOADING_FAILED_TEXT = "Network.loadingFailed"; + private static final String NETWORK_LOADING_FINISHED_TEXT = "Network.loadingFinished"; + private static final String PERFORMANCE_METRIC_TEXT = "Performance.metrics"; + private final String mEventMethodText; + + EventMethod(String eventMethodText) { + mEventMethodText = eventMethodText; + } + + @NonNull + @Override + public String toString() { + return mEventMethodText; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/enums/TargetType.java b/apl/src/main/java/com/amazon/apl/devtools/enums/TargetType.java new file mode 100644 index 00000000..31d1cf5e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/enums/TargetType.java @@ -0,0 +1,25 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.enums; + +import androidx.annotation.NonNull; + +public enum TargetType { + VIEW(TargetType.VIEW_TEXT); + + private static final String VIEW_TEXT = "view"; + private final String mTargetTypeText; + + TargetType(String targetTypeText) { + mTargetTypeText = targetTypeText; + } + + @NonNull + @Override + public String toString() { + return mTargetTypeText; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/enums/ViewState.java b/apl/src/main/java/com/amazon/apl/devtools/enums/ViewState.java new file mode 100644 index 00000000..e74d9cd6 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/enums/ViewState.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.enums; + +import androidx.annotation.NonNull; + +public enum ViewState { + // Initial state + EMPTY(ViewState.EMPTY_TEXT), + // When a target is attached or a view is displayed on the screen + INFLATED(ViewState.INFLATED_TEXT), + // When the view has been assigned a document + LOADED(ViewState.LOADED_TEXT), + // When the document is finished rendering and loading packages + READY(ViewState.READY_TEXT), + // When a view has some catastrophic error that prevents display, such as an invalid document + FAILED(ViewState.FAILED_TEXT); + + private static final String EMPTY_TEXT = "empty"; + private static final String INFLATED_TEXT = "inflated"; + private static final String LOADED_TEXT = "loaded"; + private static final String READY_TEXT = "ready"; + private static final String FAILED_TEXT = "failed"; + private final String mViewStateText; + + ViewState(String viewStateText) { + mViewStateText = viewStateText; + } + + @NonNull + @Override + public String toString() { + return mViewStateText; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/DocumentCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/DocumentCommandRequest.java new file mode 100644 index 00000000..d9b4ab78 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/DocumentCommandRequest.java @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.common.DocumentDomainResponse; +import com.amazon.apl.devtools.models.document.DocumentCommandRequestModel; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.RequestStatus; +import org.json.JSONException; +import org.json.JSONObject; + +public class DocumentCommandRequest extends DocumentCommandRequestModel implements ICommandValidator { + private static final String TAG = DocumentCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private final CommandMethod mCommandMethod; + private ViewTypeTarget mViewTypeTarget; + + public DocumentCommandRequest(CommandMethod commandMethod, + CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(commandMethod, obj); + mCommandMethod = commandMethod; + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + mCommandMethod + " command"); + mViewTypeTarget.documentCommandRequest(getId(), getStringMethod(), getParams(), (result, status) -> { + if (status.getExecutionStatus() == RequestStatus.ExecutionStatus.SUCCESSFUL) { + try { + if (result == null) { + callback.execute(new DocumentDomainResponse(getId(), getSessionId()), status); + } else { + callback.execute(new DocumentDomainResponse(getId(), getSessionId(), new JSONObject(result)), status); + } + } catch (JSONException e) { + Log.e(TAG, "Error creating json object for " + mCommandMethod + " for result: " + result); + callback.execute(RequestStatus.failed(getId(), DTError.METHOD_FAILURE)); + } + } + }); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + mCommandMethod + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session mSession = mConnection.getSession(getSessionId()); + mViewTypeTarget = (ViewTypeTarget) mSession.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/FrameMetricsRecordCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/FrameMetricsRecordCommandRequest.java new file mode 100644 index 00000000..b26c4a56 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/FrameMetricsRecordCommandRequest.java @@ -0,0 +1,46 @@ +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.frameMetrics.FrameMetricsRecordCommandRequestModel; +import com.amazon.apl.devtools.models.frameMetrics.FrameMetricsRecordCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import com.amazon.apl.devtools.util.IDTCallback; +import org.json.JSONException; +import org.json.JSONObject; + +public class FrameMetricsRecordCommandRequest extends FrameMetricsRecordCommandRequestModel implements ICommandValidator { + private static final String TAG = FrameMetricsRecordCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public FrameMetricsRecordCommandRequest(CommandRequestValidator commandRequestValidator, JSONObject obj, DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + CommandMethod.FRAMEMETRICS_RECORD+ " command"); + mViewTypeTarget.startFrameMetricsRecording(getId(), (result, requestStatus) -> + callback.execute(new FrameMetricsRecordCommandResponse(getId(), getSessionId()), requestStatus)); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.FRAMEMETRICS_RECORD + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/FrameMetricsStopCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/FrameMetricsStopCommandRequest.java new file mode 100644 index 00000000..d838de24 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/FrameMetricsStopCommandRequest.java @@ -0,0 +1,47 @@ +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.frameMetrics.FrameMetricsStopCommandRequestModel; +import com.amazon.apl.devtools.models.frameMetrics.FrameMetricsStopCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import com.amazon.apl.devtools.util.IDTCallback; +import org.json.JSONException; +import org.json.JSONObject; + +public class FrameMetricsStopCommandRequest extends FrameMetricsStopCommandRequestModel implements ICommandValidator { + private static final String TAG = FrameMetricsStopCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public FrameMetricsStopCommandRequest(CommandRequestValidator commandRequestValidator, JSONObject obj, DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + CommandMethod.FRAMEMETRICS_STOP+ " command"); + mViewTypeTarget.stopFrameMetricsRecording(getId(), (frameStatsList, requestStatus) -> { + callback.execute(new FrameMetricsStopCommandResponse(getId(), getSessionId(), frameStatsList), requestStatus); + }); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.FRAMEMETRICS_STOP + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/ICommandExecutor.java b/apl/src/main/java/com/amazon/apl/devtools/executers/ICommandExecutor.java new file mode 100644 index 00000000..b3738a2e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/ICommandExecutor.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import com.amazon.apl.devtools.models.common.Response; +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.RequestStatus; + +public interface ICommandExecutor { + default TResponse execute() { + return null; + } + + /** + * execute accepting a callback should be used when a value from another callback is needed in + * the web socket response. For example, see how status is used in an execute commands response. + * This method consumes the result of execute() by default, so that the caller is not forced to + * implement this method. + */ + default void execute(IDTCallback callback) { + callback.execute(execute(), RequestStatus.successful()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/ICommandValidator.java b/apl/src/main/java/com/amazon/apl/devtools/executers/ICommandValidator.java new file mode 100644 index 00000000..d3e87c9e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/ICommandValidator.java @@ -0,0 +1,12 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import com.amazon.apl.devtools.models.error.DTException; + +public interface ICommandValidator { + void validate() throws DTException; +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/LiveDataUpdateCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/LiveDataUpdateCommandRequest.java new file mode 100644 index 00000000..e0662f9f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/LiveDataUpdateCommandRequest.java @@ -0,0 +1,55 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.livedata.LiveDataUpdateCommandRequestModel; +import com.amazon.apl.devtools.models.livedata.LiveDataUpdateCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import com.amazon.apl.devtools.util.IDTCallback; +import org.json.JSONException; +import org.json.JSONObject; + +public final class LiveDataUpdateCommandRequest + extends LiveDataUpdateCommandRequestModel implements ICommandValidator { + private static final String TAG = LiveDataUpdateCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public LiveDataUpdateCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + CommandMethod.LIVE_DATA_UPDATE + " command"); + mViewTypeTarget.updateLiveData(getParams().getName(), getParams().getOperations(), (result, requestStatus) -> + callback.execute(new LiveDataUpdateCommandResponse(getId(), getSessionId(), result), requestStatus)); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.LIVE_DATA_UPDATE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + // TODO:: Validate target type before casting when more target types are added + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/LogClearCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/LogClearCommandRequest.java new file mode 100644 index 00000000..a75830e0 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/LogClearCommandRequest.java @@ -0,0 +1,48 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.LogDomainResponse; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.log.LogClearCommandRequestModel; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogClearCommandRequest extends LogClearCommandRequestModel implements ICommandValidator { + private static final String TAG = LogClearCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Session mSession; + + public LogClearCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public LogDomainResponse execute() { + mSession.clearLog(); + return new LogDomainResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.LOG_CLEAR + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + mCommandRequestValidator.validateLogEnabled(getId(), + getSessionId(), mConnection.getSession(getSessionId()).isLogEnabled()); + mSession = mConnection.getSession(getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/LogDisableCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/LogDisableCommandRequest.java new file mode 100644 index 00000000..c927461f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/LogDisableCommandRequest.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.LogDomainResponse; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.log.LogDisableCommandRequestModel; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogDisableCommandRequest extends LogDisableCommandRequestModel implements ICommandValidator { + private static final String TAG = LogDisableCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Session mSession; + + public LogDisableCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public LogDomainResponse execute() { + mSession.setLogEnabled(false); + return new LogDomainResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.LOG_DISABLE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + mSession = mConnection.getSession(getSessionId()); + mCommandRequestValidator.validateLogEnabled(getId(), getSessionId(), mSession.isLogEnabled()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/LogEnableCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/LogEnableCommandRequest.java new file mode 100644 index 00000000..2aff1036 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/LogEnableCommandRequest.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.LogDomainResponse; +import com.amazon.apl.devtools.models.log.LogEnableCommandRequestModel; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogEnableCommandRequest extends LogEnableCommandRequestModel implements ICommandValidator{ + private static final String TAG = LogEnableCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + + public LogEnableCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public LogDomainResponse execute() { + mConnection.getSession(getSessionId()).setLogEnabled(true); + return new LogDomainResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.LOG_ENABLE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/MemoryGetMemoryCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/MemoryGetMemoryCommandRequest.java new file mode 100644 index 00000000..60362c46 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/MemoryGetMemoryCommandRequest.java @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.os.Debug; +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.memory.MemoryGetMemoryCommandRequestModel; +import com.amazon.apl.devtools.models.memory.MemoryGetMemoryCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public class MemoryGetMemoryCommandRequest extends MemoryGetMemoryCommandRequestModel { + private static final String TAG = MemoryGetMemoryCommandRequest.class.getSimpleName(); + + public MemoryGetMemoryCommandRequest(JSONObject obj) + throws JSONException, DTException { + super(obj); + } + + @Override + public MemoryGetMemoryCommandResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.MEMORY_GET_MEMORY + " command"); + + Debug.MemoryInfo memoryInfo = new Debug.MemoryInfo(); + Debug.getMemoryInfo(memoryInfo); + + return new MemoryGetMemoryCommandResponse(getId(), memoryInfo); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/NetworkDisableCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/NetworkDisableCommandRequest.java new file mode 100644 index 00000000..3d802154 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/NetworkDisableCommandRequest.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.NetworkDomainResponse; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.network.NetworkDomainCommandRequestModel; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.RequestStatus; + +import org.json.JSONException; +import org.json.JSONObject; + +public class NetworkDisableCommandRequest extends NetworkDomainCommandRequestModel implements ICommandValidator { + private static final String TAG = NetworkDisableCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Session mSession; + + public NetworkDisableCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(CommandMethod.NETWORK_DISABLE, obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public NetworkDomainResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.NETWORK_DISABLE + " command"); + mSession.setNetworkEnabled(false); + return new NetworkDomainResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.NETWORK_DISABLE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + mSession = mConnection.getSession(getSessionId()); + mCommandRequestValidator.validateNetworkEnabled(getId(), getSessionId(), mSession.isNetworkEnabled()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/NetworkEnableCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/NetworkEnableCommandRequest.java new file mode 100644 index 00000000..f04febda --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/NetworkEnableCommandRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.NetworkDomainResponse; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.network.NetworkDomainCommandRequestModel; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.RequestStatus; + +import org.json.JSONException; +import org.json.JSONObject; + +public class NetworkEnableCommandRequest extends NetworkDomainCommandRequestModel implements ICommandValidator { + private static final String TAG = NetworkEnableCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Session mSession; + + public NetworkEnableCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(CommandMethod.NETWORK_ENABLE, obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public NetworkDomainResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.NETWORK_ENABLE + " command"); + mSession.setNetworkEnabled(true); + return new NetworkDomainResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.NETWORK_ENABLE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + mSession = mConnection.getSession(getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceDisableCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceDisableCommandRequest.java new file mode 100644 index 00000000..7d1735e4 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceDisableCommandRequest.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandResponse; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.performance.PerformanceDisableCommandRequestModel; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceDisableCommandRequest extends PerformanceDisableCommandRequestModel implements ICommandValidator { + private static final String TAG = PerformanceDisableCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Session mSession; + + public PerformanceDisableCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public PerformanceDomainCommandResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.PERFORMANCE_DISABLE + " command"); + // Disable the performance metric for this session + mSession.setPerformanceEnabled(false); + return new PerformanceDomainCommandResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.PERFORMANCE_DISABLE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + mSession = mConnection.getSession(getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceEnableCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceEnableCommandRequest.java new file mode 100644 index 00000000..d547290a --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceEnableCommandRequest.java @@ -0,0 +1,51 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandResponse; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.performance.PerformanceEnableCommandRequestModel; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceEnableCommandRequest extends PerformanceEnableCommandRequestModel implements ICommandValidator { + private static final String TAG = PerformanceEnableCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Session mSession; + + public PerformanceEnableCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public PerformanceDomainCommandResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.PERFORMANCE_ENABLE + " command"); + // Enable the performance metric for this session + mSession.setPerformanceEnabled(true); + return new PerformanceDomainCommandResponse(getId(), getSessionId()); + } + + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.PERFORMANCE_ENABLE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + mSession = mConnection.getSession(getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceGetMetricsCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceGetMetricsCommandRequest.java new file mode 100644 index 00000000..2ed4eefa --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/PerformanceGetMetricsCommandRequest.java @@ -0,0 +1,57 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.performance.PerformanceGetMetricsCommandRequestModel; +import com.amazon.apl.devtools.models.performance.PerformanceGetMetricsCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import com.amazon.apl.devtools.util.IDTCallback; +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceGetMetricsCommandRequest extends PerformanceGetMetricsCommandRequestModel implements ICommandValidator { + private static final String TAG = PerformanceGetMetricsCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public PerformanceGetMetricsCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + CommandMethod.PERFORMANCE_GET_METRICS + " command"); + mViewTypeTarget.getPerformanceMetrics(getId(), (performanceMetrics, requestStatus) -> + callback.execute(new PerformanceGetMetricsCommandResponse( + getId(), getSessionId(), performanceMetrics), requestStatus)); + } + + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.PERFORMANCE_GET_METRICS + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + boolean isPerformanceEnabled = session.isPerformanceEnabled(); + mCommandRequestValidator.validatePerformanceEnabled(getId(), getSessionId(), isPerformanceEnabled); + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/TargetAttachToTargetCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/TargetAttachToTargetCommandRequest.java new file mode 100644 index 00000000..490e7e6a --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/TargetAttachToTargetCommandRequest.java @@ -0,0 +1,62 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.Target; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.target.TargetAttachToTargetCommandRequestModel; +import com.amazon.apl.devtools.models.target.TargetAttachToTargetCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; +import com.amazon.apl.devtools.util.TargetCatalog; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class TargetAttachToTargetCommandRequest + extends TargetAttachToTargetCommandRequestModel implements ICommandValidator { + private static final String TAG = TargetAttachToTargetCommandRequest.class.getSimpleName(); + private final TargetCatalog mTargetCatalog; + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private Target mTarget; + + public TargetAttachToTargetCommandRequest(TargetCatalog targetCatalog, + CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) + throws JSONException, DTException { + super(obj); + mTargetCatalog = targetCatalog; + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + private String getTargetId() { + return getParams().getTargetId(); + } + + @Override + public TargetAttachToTargetCommandResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.TARGET_ATTACH_TO_TARGET + " command"); + Session session = new Session(mConnection, mTarget); + return new TargetAttachToTargetCommandResponse(getId(), session.getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.TARGET_ATTACH_TO_TARGET + " command"); + mCommandRequestValidator.validateBeforeGettingTargetFromTargetCatalog(getId(), + getTargetId()); + mTarget = mTargetCatalog.get(getTargetId()); + mCommandRequestValidator.validateBeforeCreatingSession(getId(), mConnection, mTarget); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/TargetGetTargetsCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/TargetGetTargetsCommandRequest.java new file mode 100644 index 00000000..206fbdde --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/TargetGetTargetsCommandRequest.java @@ -0,0 +1,33 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.target.TargetGetTargetsCommandRequestModel; +import com.amazon.apl.devtools.models.target.TargetGetTargetsCommandResponse; +import com.amazon.apl.devtools.util.TargetCatalog; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class TargetGetTargetsCommandRequest extends TargetGetTargetsCommandRequestModel { + private static final String TAG = TargetGetTargetsCommandRequest.class.getSimpleName(); + private final TargetCatalog mTargetCatalog; + + public TargetGetTargetsCommandRequest(TargetCatalog targetCatalog, JSONObject obj) + throws JSONException { + super(obj); + mTargetCatalog = targetCatalog; + } + + @Override + public TargetGetTargetsCommandResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.TARGET_GET_TARGETS + " command"); + return new TargetGetTargetsCommandResponse(getId(), mTargetCatalog.getAll()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/ViewCaptureImageCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/ViewCaptureImageCommandRequest.java new file mode 100644 index 00000000..3b501aea --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/ViewCaptureImageCommandRequest.java @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.graphics.Bitmap; +import android.util.Base64; +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.view.ViewCaptureImageCommandRequestModel; +import com.amazon.apl.devtools.models.view.ViewCaptureImageCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import com.amazon.apl.devtools.util.IDTCallback; +import org.json.JSONException; +import org.json.JSONObject; + +import java.io.ByteArrayOutputStream; + +public final class ViewCaptureImageCommandRequest + extends ViewCaptureImageCommandRequestModel implements ICommandValidator { + private static final String TAG = ViewCaptureImageCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public ViewCaptureImageCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + private byte[] compressBitmapToBytes(Bitmap bitmap, Bitmap.CompressFormat compressFormat, int quality) { + Log.i(TAG, "Compressing image to " + compressFormat.toString() + " format with " + + quality + " quality"); + ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream(); + bitmap.compress(compressFormat, quality, byteArrayOutputStream); + return byteArrayOutputStream.toByteArray(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + CommandMethod.VIEW_CAPTURE_IMAGE + " command"); + mViewTypeTarget.getCurrentBitmap((bitmap, requestStatus) -> { + // TODO:: Image compression type and quality is hardcoded, but this may change later + String imageCompressionType = "image/png"; + byte[] bytes = compressBitmapToBytes(bitmap, Bitmap.CompressFormat.PNG, 100); + + String encodedImageData = Base64.encodeToString(bytes, Base64.DEFAULT); + callback.execute(new ViewCaptureImageCommandResponse(getId(), getSessionId(), bitmap.getHeight(), + bitmap.getWidth(), imageCompressionType, encodedImageData), requestStatus); + }); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.VIEW_CAPTURE_IMAGE + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + // TODO:: Validate target type before casting when more target types are added + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/ViewExecuteCommandsCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/ViewExecuteCommandsCommandRequest.java new file mode 100644 index 00000000..5f17091b --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/ViewExecuteCommandsCommandRequest.java @@ -0,0 +1,60 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.view.ViewExecuteCommandsCommandRequestModel; +import com.amazon.apl.devtools.models.view.ViewExecuteCommandsCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import com.amazon.apl.devtools.util.IDTCallback; +import org.json.JSONException; +import org.json.JSONObject; + +public final class ViewExecuteCommandsCommandRequest + extends ViewExecuteCommandsCommandRequestModel implements ICommandValidator { + private static final String TAG = ViewExecuteCommandsCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public ViewExecuteCommandsCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public void execute(IDTCallback callback) { + Log.i(TAG, "Executing " + CommandMethod.VIEW_EXECUTE_COMMANDS + " command"); + + /* + * Passing a callback to consume the execute commands status in order to create a response. + * The response will be consumed by the caller of this execute method. + */ + mViewTypeTarget.executeCommands(getParams().getCommands(), (status, requestStatus) -> + callback.execute(new ViewExecuteCommandsCommandResponse(getId(), getSessionId(), status), requestStatus)); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.VIEW_EXECUTE_COMMANDS + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + // TODO:: Validate target type before casting when more target types are added + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/executers/ViewSetDocumentCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/executers/ViewSetDocumentCommandRequest.java new file mode 100644 index 00000000..a6f628e2 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/executers/ViewSetDocumentCommandRequest.java @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.executers; + +import android.util.Log; + +import com.amazon.apl.android.LiveArray; +import com.amazon.apl.android.LiveMap; +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.ViewTypeTarget; +import com.amazon.apl.devtools.models.error.DTException; +import com.amazon.apl.devtools.models.view.ViewSetDocumentCommandRequestModel; +import com.amazon.apl.devtools.models.view.ViewSetDocumentCommandResponse; +import com.amazon.apl.devtools.util.CommandRequestValidator; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; +import java.util.Map; + +public final class ViewSetDocumentCommandRequest + extends ViewSetDocumentCommandRequestModel implements ICommandValidator { + private static final String TAG = ViewSetDocumentCommandRequest.class.getSimpleName(); + private final CommandRequestValidator mCommandRequestValidator; + private final DTConnection mConnection; + private ViewTypeTarget mViewTypeTarget; + + public ViewSetDocumentCommandRequest(CommandRequestValidator commandRequestValidator, + JSONObject obj, + DTConnection connection) + throws JSONException, DTException { + super(obj); + mCommandRequestValidator = commandRequestValidator; + mConnection = connection; + validate(); + } + + @Override + public ViewSetDocumentCommandResponse execute() { + Log.i(TAG, "Executing " + CommandMethod.VIEW_SET_DOCUMENT + " command"); + mViewTypeTarget.post(() -> { + Params params = getParams(); + if (params != null) { + if (params.getConfiguration() != null && params.getConfiguration().mLiveArrays != null) { + for (Map.Entry> entry : params.getConfiguration().mLiveArrays.entrySet()) { + mViewTypeTarget.addLiveData(entry.getKey(), LiveArray.create(entry.getValue())); + } + } + if (params.getConfiguration() != null && params.getConfiguration().mLiveMaps != null) { + for (Map.Entry> entry : params.getConfiguration().mLiveMaps.entrySet()) { + mViewTypeTarget.addLiveData(entry.getKey(), LiveMap.create(entry.getValue())); + } + } + mViewTypeTarget.setDocument(params.getDocument(), params.getData()); + } + }); + return new ViewSetDocumentCommandResponse(getId(), getSessionId()); + } + + @Override + public void validate() throws DTException { + Log.i(TAG, "Validating " + CommandMethod.VIEW_SET_DOCUMENT + " command"); + mCommandRequestValidator.validateBeforeGettingSession(getId(), getSessionId(), mConnection); + Session session = mConnection.getSession(getSessionId()); + // TODO:: Validate target type before casting when more target types are added + mViewTypeTarget = (ViewTypeTarget) session.getTarget(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/RequestHeader.java b/apl/src/main/java/com/amazon/apl/devtools/models/RequestHeader.java new file mode 100644 index 00000000..78f581b5 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/RequestHeader.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models; + +import com.amazon.apl.devtools.models.common.Request; +import com.amazon.apl.devtools.util.CommandMethodUtil; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class RequestHeader extends Request { + public RequestHeader(CommandMethodUtil commandMethodUtil, JSONObject obj) throws JSONException { + super(commandMethodUtil.parseMethod(obj.getString("method")), obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/Session.java b/apl/src/main/java/com/amazon/apl/devtools/models/Session.java new file mode 100644 index 00000000..7034372e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/Session.java @@ -0,0 +1,88 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models; + +import static com.amazon.apl.android.Session.setSensitiveLoggingEnabled; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.models.common.Event; +import com.amazon.apl.devtools.models.common.SessionModel; +import com.amazon.apl.devtools.util.IdGenerator; + + +/** + * Session is a unique mapping between a DTConnection and a Target. + * Each DTConnection can only have 1 Session attached to a specific Target. + * For example, if there are 5 Targets, then a DTConnection can only have 5 Sessions, 1 per Target. + */ +public final class Session extends SessionModel { + private static final String TAG = Session.class.getSimpleName(); + private static final IdGenerator sessionIdGenerator = new IdGenerator(); + private final DTConnection mConnection; + private final Target mTarget; + private boolean isPerformanceEnabled; + private boolean isLogEnabled; + private boolean isNetworkEnabled; + + public Session(DTConnection connection, Target target) { + super(sessionIdGenerator.generateId("session")); + Log.i(TAG, "Creating session"); + mConnection = connection; + mTarget = target; + connection.registerSession(this); + target.registerSession(this); + } + + public DTConnection getConnection() { + return mConnection; + } + + public Target getTarget() { + return mTarget; + } + + + public void destroy() { + mConnection.unregisterSession(getSessionId()); + mTarget.unregisterSession(getSessionId()); + } + + public boolean isPerformanceEnabled() { + return isPerformanceEnabled; + } + + public void setPerformanceEnabled(boolean performanceEnabled) { + isPerformanceEnabled = performanceEnabled; + } + + public boolean isLogEnabled() { + return isLogEnabled; + } + + public void setLogEnabled(boolean isLogEnabled) { + setSensitiveLoggingEnabled(isLogEnabled); + this.isLogEnabled = isLogEnabled; + //TO DO: after Network.enable implemented, send old log entries after reconnect + } + + public boolean isNetworkEnabled() { + return isNetworkEnabled; + } + + public void setNetworkEnabled(boolean isNetworkEnabled) { + this.isNetworkEnabled = isNetworkEnabled; + } + + public void sendEvent(TEvent event) { + mConnection.sendEvent(event); + } + + public void clearLog() { + mTarget.clearLog(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/Target.java b/apl/src/main/java/com/amazon/apl/devtools/models/Target.java new file mode 100644 index 00000000..4e5a25e3 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/Target.java @@ -0,0 +1,74 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.TargetType; +import com.amazon.apl.devtools.models.common.TargetModel; +import com.amazon.apl.devtools.models.log.LogEntry; +import com.amazon.apl.devtools.util.DependencyContainer; +import com.amazon.apl.devtools.util.IdGenerator; +import com.amazon.apl.devtools.util.TargetCatalog; + +import java.util.Collection; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +/** + * Target is an object that can be uniquely identified and are the subject of command requests. + * There are different types of Targets. + */ +public abstract class Target extends TargetModel { + private static final String TAG = Target.class.getSimpleName(); + private static final IdGenerator targetIdGenerator = new IdGenerator(); + private final Map mRegisteredSessionsMap = new HashMap<>(); + private final TargetCatalog mTargetCatalog; + + protected Target(TargetType type, String name) { + super(targetIdGenerator.generateId("target"), type, name); + Log.i(TAG, "Creating target of type " + getType() + " with target id " + + getTargetId()); + mTargetCatalog = DependencyContainer.getInstance().getTargetCatalog(); + } + + public void registerToCatalog() { + mTargetCatalog.add(this); + } + + public void unregisterToCatalog() { + mTargetCatalog.remove(this); + } + + public void registerSession(Session session) { + mRegisteredSessionsMap.put(session.getSessionId(), session); + Log.i(TAG, "registerSession: Target with target id " + getTargetId() + + " is attached to " + mRegisteredSessionsMap.size() + " sessions"); + } + + public void unregisterSession(String sessionId) { + mRegisteredSessionsMap.remove(sessionId); + Log.i(TAG, "unregisterSession: Target with target id " + getTargetId() + + " is attached to " + mRegisteredSessionsMap.size() + " sessions"); + } + + public boolean hasSession(String sessionId) { + return mRegisteredSessionsMap.containsKey(sessionId); + } + + public Collection getRegisteredSessionIds() { + return mRegisteredSessionsMap.keySet(); + } + + public Collection getRegisteredSessions() { + return mRegisteredSessionsMap.values(); + } + + public abstract List getLogEntries(); + + public abstract void clearLog(); +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/ViewTypeTarget.java b/apl/src/main/java/com/amazon/apl/devtools/models/ViewTypeTarget.java new file mode 100644 index 00000000..3e3df0e0 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/ViewTypeTarget.java @@ -0,0 +1,229 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models; + +import android.graphics.Bitmap; +import android.os.Handler; + +import android.view.View; +import com.amazon.apl.android.APLOptions; +import com.amazon.apl.android.LiveData; +import com.amazon.apl.android.dependencies.IAPLSessionListener; +import com.amazon.apl.android.providers.ITelemetryProvider; +import com.amazon.apl.android.providers.impl.LoggingTelemetryProvider; +import com.amazon.apl.android.utils.MetricInfo; +import com.amazon.apl.developer.views.CaptureImageHelper; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.enums.TargetType; +import com.amazon.apl.devtools.enums.ViewState; +import com.amazon.apl.devtools.models.log.LogEntry; +import com.amazon.apl.devtools.models.log.LogEntryAddedEvent; +import com.amazon.apl.devtools.models.network.DTNetworkRequestHandler; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; +import com.amazon.apl.devtools.models.network.NetworkLoadingFailedEvent; +import com.amazon.apl.devtools.models.network.NetworkLoadingFinishedEvent; +import com.amazon.apl.devtools.models.network.NetworkRequestWillBeSentEvent; +import com.amazon.apl.devtools.models.performance.PerformanceMetricsEvent; +import com.amazon.apl.devtools.models.view.ExecuteCommandStatus; +import com.amazon.apl.devtools.models.view.ViewStateChangeEvent; +import com.amazon.apl.devtools.util.DependencyContainer; + +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.IdGenerator; +import com.amazon.apl.devtools.util.RequestStatus; +import com.amazon.apl.devtools.views.IAPLView; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +/** + * ViewTypeTarget is a Target that contains a single Android view. + */ +public final class ViewTypeTarget extends Target implements IAPLSessionListener { + private IAPLView mView; + private final Handler mHandler; + private final List mLogEntries = new ArrayList<>(); + private final IdGenerator mIdGenerator = new IdGenerator(); + private final IDTNetworkRequestHandler mDTNetworkRequestHandler; + private int mCurrentDocumentId = 0; + private ViewState mCurrentDocumentView = ViewState.EMPTY; + private APLOptions mAPLOptions; + + private final CaptureImageHelper mCaptureImageHelper = new CaptureImageHelper(); + + public ViewTypeTarget(String name) { + super(TargetType.VIEW, name ); + mHandler = DependencyContainer.getInstance().getTargetCatalog().getHandler(); + mDTNetworkRequestHandler = new DTNetworkRequestHandler(this); + } + + public void post(Runnable runnable) { + mHandler.post(runnable); + } + + public void setView(IAPLView aplViewLayout) { + mView = aplViewLayout; + } + + public void addLiveData(String name, LiveData data) { + mView.addLiveData(name, data); + } + + + @Override + public void registerSession(Session session) { + super.registerSession(session); + post(() -> onViewStateChange(mCurrentDocumentView, System.currentTimeMillis() / 1000D)); + } + + public void setDocument(String aplDocument, String aplDocumentData) { + mView.renderAPLDocument(aplDocument, aplDocumentData); + } + + public void setAPLOptions(APLOptions aplOptions) { + mAPLOptions = aplOptions; + } + + public void startFrameMetricsRecording(int id, IDTCallback callback) { + mView.post(() -> mView.startFrameMetricsRecording(id, callback)); + } + + public void stopFrameMetricsRecording(int id, IDTCallback> callback) { + mView.post(() -> mView.stopFrameMetricsRecording(id, callback)); + } + + public void getPerformanceMetrics(int id, IDTCallback> callback) { + if (mAPLOptions == null) { + callback.execute(RequestStatus.failed(id, DTError.NO_PERFORMANCE_METRICS)); + } + + ITelemetryProvider telemetryProvider = mAPLOptions.getTelemetryProvider(); + + // TODO: Update when the refactoring is completed. Jira:ELON-40529 + if (telemetryProvider instanceof LoggingTelemetryProvider) { + post(() -> { + List result = ((LoggingTelemetryProvider) telemetryProvider).getPerformanceMetrics(); + callback.execute(result, RequestStatus.successful()); + }); + } else { + post(() -> callback.execute(RequestStatus.failed(id, DTError.NO_PERFORMANCE_METRICS))); + } + } + + public void getCurrentBitmap(IDTCallback callback) { + post(() -> callback.execute(mCaptureImageHelper.captureImage((View) mView), RequestStatus.successful())); + } + + public void executeCommands(String commands, IDTCallback callback) { + mView.executeCommands(commands, callback); + } + + public void updateLiveData(String name, List operations, IDTCallback callback) { + mView.post(() -> mView.updateLiveData(name, operations, callback)); + } + + public void onViewStateChange(ViewState viewState, double timestamp) { + mCurrentDocumentView = viewState; + for (Session session : getRegisteredSessions()) { + session.sendEvent(new ViewStateChangeEvent(session.getSessionId(), viewState, + mCurrentDocumentId, timestamp)); + } + + if (mCurrentDocumentView == ViewState.READY) { + sendPerformanceMetricsEvent(); + } + } + + private void sendPerformanceMetricsEvent() { + post(() -> { + if (mAPLOptions != null && mAPLOptions.getTelemetryProvider() instanceof LoggingTelemetryProvider) { + List result = ((LoggingTelemetryProvider) mAPLOptions.getTelemetryProvider()).getPerformanceMetrics(); + for (Session session : getRegisteredSessions()) { + if (session.isPerformanceEnabled()) { + session.sendEvent(new PerformanceMetricsEvent(session.getSessionId(), result)); + } + } + } + }); + } + + public void onLogEntryAdded(com.amazon.apl.android.Session.LogEntryLevel level, com.amazon.apl.android.Session.LogEntrySource source, String messageText, double timestamp, Object[] arguments) { + LogEntry entry = new LogEntry(level, source, messageText, timestamp, arguments); + mLogEntries.add(entry); + for (Session session : getRegisteredSessions()) { + if (session.isLogEnabled()) { + session.sendEvent(new LogEntryAddedEvent(session.getSessionId(), level, source, messageText, + timestamp, arguments)); + } + } + } + + @Override + public void write(com.amazon.apl.android.Session.LogEntryLevel level, com.amazon.apl.android.Session.LogEntrySource source, String message, Object[] arguments) { + post(()-> onLogEntryAdded(level, source, message, System.currentTimeMillis() / 1000D, arguments)); + } + + public void onGenerateNewDocumentId() { + mCurrentDocumentId = mIdGenerator.generateId(); + } + + public int getCurrentDocumentId() { + return mCurrentDocumentId; + } + + public void setCurrentDocumentId(int documentId) { + mCurrentDocumentId = documentId; + } + + public void clearLog() { + mLogEntries.clear(); + } + + public List getLogEntries() { + return mLogEntries; + } + + public void cleanup() { + // Remove any remaining messages in the queue + mHandler.removeCallbacksAndMessages(null); + } + + public void documentCommandRequest(int id, String method, JSONObject params, IDTCallback callback) { + mView.documentCommandRequest(id, method, params, callback); + } + + public void onNetworkRequestWillBeSent(int requestId, double timestamp, + String documentURL, String type) { + for (Session session : getRegisteredSessions()) { + if (session.isNetworkEnabled()){ + session.sendEvent(new NetworkRequestWillBeSentEvent(session.getSessionId(), requestId, + timestamp, documentURL, type)); + } + } + } + + public void onNetworkLoadingFailed(int requestId, double timestamp) { + for (Session session : getRegisteredSessions()) { + if (session.isNetworkEnabled()) { + session.sendEvent(new NetworkLoadingFailedEvent(session.getSessionId(), requestId, timestamp)); + } + } + } + + public void onNetworkLoadingFinished(int requestId, double timestamp, int encodedDataLength) { + for (Session session : getRegisteredSessions()) { + if (session.isNetworkEnabled()) { + session.sendEvent(new NetworkLoadingFinishedEvent(session.getSessionId(), requestId, + timestamp, encodedDataLength)); + } + } + } + + public IDTNetworkRequestHandler getDTNetworkRequestHandler() { + return mDTNetworkRequestHandler; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/DocumentDomainRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/DocumentDomainRequest.java new file mode 100644 index 00000000..5cfea115 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/DocumentDomainRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; +import org.json.JSONException; +import org.json.JSONObject; + +public class DocumentDomainRequest extends Request { + private final String mSessionId; + + protected DocumentDomainRequest(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + } + + public String getSessionId() { + return mSessionId; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/DocumentDomainResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/DocumentDomainResponse.java new file mode 100644 index 00000000..b82d5eed --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/DocumentDomainResponse.java @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public class DocumentDomainResponse extends Response { + private final String mSessionId; + private final JSONObject mResult; + + public DocumentDomainResponse(int id, String sessionId) { + this(id, sessionId, null); + } + + public DocumentDomainResponse(int id, String sessionId, JSONObject result) { + super(id); + mSessionId = sessionId; + mResult = result; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + if (mResult == null) { + return super.toJSONObject().put("sessionId", getSessionId()); + } + return super.toJSONObject().put("sessionId", getSessionId()).put("result", mResult); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/Event.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/Event.java new file mode 100644 index 00000000..71e0212e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/Event.java @@ -0,0 +1,35 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.EventMethod; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class Event implements IResponseParser { + private final EventMethod mMethod; + private final String mSessionId; + + protected Event(EventMethod mMethod, String mSessionId) { + this.mMethod = mMethod; + this.mSessionId = mSessionId; + } + + public EventMethod getMethod() { + return mMethod; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return new JSONObject().put("method", getMethod().toString()) + .put("sessionId", getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/FrameMetricsDomainCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/FrameMetricsDomainCommandRequest.java new file mode 100644 index 00000000..c40c6320 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/FrameMetricsDomainCommandRequest.java @@ -0,0 +1,20 @@ +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class FrameMetricsDomainCommandRequest extends Request { + private final String mSessionId; + + protected FrameMetricsDomainCommandRequest(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + } + + public String getSessionId() { + return mSessionId; + } + +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/FrameMetricsDomainCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/FrameMetricsDomainCommandResponse.java new file mode 100644 index 00000000..a2e32812 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/FrameMetricsDomainCommandResponse.java @@ -0,0 +1,22 @@ +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class FrameMetricsDomainCommandResponse extends Response { + private final String mSessionId; + + protected FrameMetricsDomainCommandResponse(int id, String sessionId) { + super(id); + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject().put("sessionId", getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/IResponseParser.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/IResponseParser.java new file mode 100644 index 00000000..5ea8a1af --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/IResponseParser.java @@ -0,0 +1,17 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public interface IResponseParser { + JSONObject toJSONObject() throws JSONException; + + default String toJSONString() throws JSONException { + return toJSONObject().toString(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/LogDomainRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/LogDomainRequest.java new file mode 100644 index 00000000..7346941f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/LogDomainRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogDomainRequest extends Request { + private final String mSessionId; + + protected LogDomainRequest(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + } + + public String getSessionId() { + return mSessionId; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/LogDomainResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/LogDomainResponse.java new file mode 100644 index 00000000..00a6645e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/LogDomainResponse.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public class LogDomainResponse extends Response { + private final String mSessionId; + public LogDomainResponse(int id, String sessionId) { + super(id); + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject().put("sessionId", getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/NetworkDomainRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/NetworkDomainRequest.java new file mode 100644 index 00000000..9b5ec2ce --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/NetworkDomainRequest.java @@ -0,0 +1,23 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; +import org.json.JSONException; +import org.json.JSONObject; + +public class NetworkDomainRequest extends Request { + private final String mSessionId; + + protected NetworkDomainRequest(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + } + + public String getSessionId() { + return mSessionId; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/NetworkDomainResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/NetworkDomainResponse.java new file mode 100644 index 00000000..c80c14f2 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/NetworkDomainResponse.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public class NetworkDomainResponse extends Response { + private final String mSessionId; + + public NetworkDomainResponse(int id, String sessionId) { + super(id); + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject().put("sessionId", getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/PerformanceDomainCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/PerformanceDomainCommandRequest.java new file mode 100644 index 00000000..a58942c7 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/PerformanceDomainCommandRequest.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; + +import org.json.JSONException; +import org.json.JSONObject; + + +public abstract class PerformanceDomainCommandRequest extends Request { + private final String mSessionId; + + protected PerformanceDomainCommandRequest(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + } + + public String getSessionId() { + return mSessionId; + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/PerformanceDomainCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/PerformanceDomainCommandResponse.java new file mode 100644 index 00000000..2ad585d1 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/PerformanceDomainCommandResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceDomainCommandResponse extends Response { + private final String mSessionId; + + public PerformanceDomainCommandResponse(int id, String sessionId) { + super(id); + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject().put("sessionId", getSessionId()); + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/Request.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/Request.java new file mode 100644 index 00000000..f25c77db --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/Request.java @@ -0,0 +1,30 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.executers.ICommandExecutor; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class Request implements ICommandExecutor { + private final CommandMethod mMethod; + private final int mId; + + protected Request(CommandMethod method, JSONObject obj) throws JSONException { + mMethod = method; + mId = obj.getInt("id"); + } + + public CommandMethod getMethod() { + return mMethod; + } + + public int getId() { + return mId; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/Response.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/Response.java new file mode 100644 index 00000000..cafb2352 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/Response.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class Response implements IResponseParser { + private final int mId; + + protected Response(int id) { + mId = id; + } + + public int getId() { + return mId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return new JSONObject().put("id", getId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/SessionModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/SessionModel.java new file mode 100644 index 00000000..ded52304 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/SessionModel.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +public abstract class SessionModel { + private final String mSessionId; + + protected SessionModel(String sessionId) { + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetDomainCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetDomainCommandRequest.java new file mode 100644 index 00000000..f3881f58 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetDomainCommandRequest.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class TargetDomainCommandRequest extends Request { + protected TargetDomainCommandRequest(CommandMethod method, JSONObject obj) + throws JSONException { + super(method, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetDomainCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetDomainCommandResponse.java new file mode 100644 index 00000000..53700fef --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetDomainCommandResponse.java @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class TargetDomainCommandResponse extends Response { + protected TargetDomainCommandResponse(int id) { + super(id); + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetModel.java new file mode 100644 index 00000000..16df0051 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/TargetModel.java @@ -0,0 +1,36 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.TargetType; +import com.amazon.apl.devtools.util.DependencyContainer; +import com.amazon.apl.devtools.util.TargetCatalog; + +public abstract class TargetModel { + private final static String SEPERATOR = "."; + private final String mTargetId; + private final TargetType mType; + private final String mName; + + protected TargetModel(String targetId, TargetType type, String name) { + TargetCatalog catalog = DependencyContainer.getInstance().getTargetCatalog(); + mTargetId = targetId; + mType = type; + mName = name + SEPERATOR + catalog.getAll().size(); + } + + public String getTargetId() { + return mTargetId; + } + + public TargetType getType() { + return mType; + } + + public String getName() { + return mName; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/ViewDomainCommandRequest.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/ViewDomainCommandRequest.java new file mode 100644 index 00000000..f038c699 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/ViewDomainCommandRequest.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import com.amazon.apl.devtools.enums.CommandMethod; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class ViewDomainCommandRequest extends Request { + private final String mSessionId; + + protected ViewDomainCommandRequest(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + } + + public String getSessionId() { + return mSessionId; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/common/ViewDomainCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/common/ViewDomainCommandResponse.java new file mode 100644 index 00000000..45618ae9 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/common/ViewDomainCommandResponse.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.common; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class ViewDomainCommandResponse extends Response { + private final String mSessionId; + + protected ViewDomainCommandResponse(int id, String sessionId) { + super(id); + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject().put("sessionId", getSessionId()); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/document/DocumentCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/document/DocumentCommandRequestModel.java new file mode 100644 index 00000000..9baa193f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/document/DocumentCommandRequestModel.java @@ -0,0 +1,39 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.document; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.DocumentDomainRequest; +import com.amazon.apl.devtools.models.common.DocumentDomainResponse; +import org.json.JSONException; +import org.json.JSONObject; + +public class DocumentCommandRequestModel extends DocumentDomainRequest { + private final CommandMethod mMethod; + private final String mSessionId; + private final JSONObject mParams; + + protected DocumentCommandRequestModel(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mMethod = method; + mSessionId = obj.getString("sessionId"); + mParams = obj.has("params") && !obj.isNull("params") ? + obj.getJSONObject("params") : + new JSONObject(); + } + + public String getStringMethod() { + return mMethod.toString(); + } + + public String getSessionId() { + return mSessionId; + } + + public JSONObject getParams() { + return mParams; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/error/DTException.java b/apl/src/main/java/com/amazon/apl/devtools/models/error/DTException.java new file mode 100644 index 00000000..ce94e13b --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/error/DTException.java @@ -0,0 +1,31 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.error; + +public final class DTException extends Exception { + private final int mId; + private final int mCode; + + public DTException(int id, int code, String message) { + super(message); + mId = id; + mCode = code; + } + + public DTException(int id, int code, String message, Throwable cause) { + super(message, cause); + mId = id; + mCode = code; + } + + public int getId() { + return mId; + } + + public int getCode() { + return mCode; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/error/ErrorResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/error/ErrorResponse.java new file mode 100644 index 00000000..9396d0c6 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/error/ErrorResponse.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.error; + +import android.util.Log; + +import com.amazon.apl.devtools.models.common.Response; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class ErrorResponse extends Response { + private static final String TAG = ErrorResponse.class.getSimpleName(); + private final Error mError; + + public ErrorResponse(DTException e) { + super(e.getId()); + Log.i(TAG, "Creating error response from DTException"); + mError = new Error(e.getCode(), e.getMessage()); + } + + public ErrorResponse(int id, int code, String message) { + super(id); + Log.i(TAG, "Creating error response"); + mError = new Error(code, message); + } + + private static class Error { + private final int mCode; + private final String mMessage; + + private Error(int code, String message) { + mCode = code; + mMessage = message; + } + + public int getCode() { + return mCode; + } + + public String getMessage() { + return mMessage; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + return super.toJSONObject().put("error", new JSONObject() + .put("code", mError.mCode).put("message", mError.mMessage)); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsRecordCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsRecordCommandRequestModel.java new file mode 100644 index 00000000..00ef7e78 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsRecordCommandRequestModel.java @@ -0,0 +1,13 @@ +package com.amazon.apl.devtools.models.frameMetrics; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.FrameMetricsDomainCommandRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class FrameMetricsRecordCommandRequestModel extends FrameMetricsDomainCommandRequest { + protected FrameMetricsRecordCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.FRAMEMETRICS_RECORD, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsRecordCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsRecordCommandResponse.java new file mode 100644 index 00000000..038f6ec0 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsRecordCommandResponse.java @@ -0,0 +1,12 @@ +package com.amazon.apl.devtools.models.frameMetrics; + +import com.amazon.apl.devtools.models.common.FrameMetricsDomainCommandResponse; + +public class FrameMetricsRecordCommandResponse extends FrameMetricsDomainCommandResponse { + private static final String TAG = FrameMetricsRecordCommandResponse.class.getSimpleName(); + + public FrameMetricsRecordCommandResponse(int id, String sessionId) { + super(id, sessionId); + } + +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsStopCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsStopCommandRequestModel.java new file mode 100644 index 00000000..f3befe8a --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsStopCommandRequestModel.java @@ -0,0 +1,13 @@ +package com.amazon.apl.devtools.models.frameMetrics; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.FrameMetricsDomainCommandRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +public class FrameMetricsStopCommandRequestModel extends FrameMetricsDomainCommandRequest { + protected FrameMetricsStopCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.FRAMEMETRICS_STOP, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsStopCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsStopCommandResponse.java new file mode 100644 index 00000000..8d19dbd5 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsStopCommandResponse.java @@ -0,0 +1,39 @@ +package com.amazon.apl.devtools.models.frameMetrics; + +import com.amazon.apl.devtools.models.common.FrameMetricsDomainCommandResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +public class FrameMetricsStopCommandResponse extends FrameMetricsDomainCommandResponse { + private static final String TAG = FrameMetricsStopCommandResponse.class.getSimpleName(); + private List mFramestatsList; + + public FrameMetricsStopCommandResponse(int id, String sessionId, List framestatsList) { + super(id, sessionId); + mFramestatsList = framestatsList; + } + + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONObject result = new JSONObject(); + JSONObject value = new JSONObject(); + JSONArray framestatsArray = new JSONArray(); + + for (JSONObject framestatsObject : mFramestatsList) { + JSONObject framestatsItem = new JSONObject(); + framestatsItem.put("begin", framestatsObject.getLong("begin")); + framestatsItem.put("end", framestatsObject.getLong("end")); + framestatsArray.put(framestatsItem); + } + value.put("framestats", framestatsArray); + result.put("value", value); + return super.toJSONObject().put("result", result); + } + + +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/livedata/LiveDataUpdateCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/livedata/LiveDataUpdateCommandRequestModel.java new file mode 100644 index 00000000..21f0b8e6 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/livedata/LiveDataUpdateCommandRequestModel.java @@ -0,0 +1,68 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.livedata; + +import com.amazon.apl.android.LiveArray; +import com.amazon.apl.android.LiveData; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.common.ViewDomainCommandRequest; +import com.amazon.apl.devtools.models.error.DTException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.List; + +public abstract class LiveDataUpdateCommandRequestModel + extends ViewDomainCommandRequest { + private final Params mParams; + + protected LiveDataUpdateCommandRequestModel(JSONObject obj) throws JSONException, DTException { + super(CommandMethod.LIVE_DATA_UPDATE, obj); + try { + mParams = new Params(obj.getJSONObject("params")); + } catch (JSONException e) { + throw new DTException(getId(), DTError.INVALID_DOCUMENT.getErrorCode(), + "Invalid commands document", e); + } + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final String mName; + private final List mOperations = new ArrayList<>(); + + private Params(JSONObject params) throws JSONException { + mName = params.getString("name"); + JSONArray operations = params.getJSONArray("operations"); + for (int i = 0; i < operations.length(); i++) { + JSONObject operation = operations.getJSONObject(i); + mOperations.add( + new LiveData.Update( + operation.getString("type"), + operation.optInt("index", 0), + operation.optString("key", ""), + operation.get("value") + ) + ); + } + } + + public String getName() { + return mName; + } + + public List getOperations() { + return mOperations; + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/livedata/LiveDataUpdateCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/livedata/LiveDataUpdateCommandResponse.java new file mode 100644 index 00000000..a627480f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/livedata/LiveDataUpdateCommandResponse.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.livedata; + +import android.util.Log; + +import com.amazon.apl.devtools.models.common.ViewDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class LiveDataUpdateCommandResponse extends ViewDomainCommandResponse { + private static final String TAG = LiveDataUpdateCommandResponse.class.getSimpleName(); + + private final Result mResult; + + public LiveDataUpdateCommandResponse(int id, String sessionId, boolean status){ + super(id, sessionId); + mResult = new Result(status? "success" : "failure"); + } + + private static class Result { + private final String mStatus; + + public Result(String status) { + mStatus = status; + } + + public String getStatus() { + return mStatus; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing LiveDataUpdate response object"); + return super.toJSONObject(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/log/LogClearCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogClearCommandRequestModel.java new file mode 100644 index 00000000..e953f8d7 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogClearCommandRequestModel.java @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.LogDomainRequest; +import com.amazon.apl.devtools.models.common.LogDomainResponse; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogClearCommandRequestModel extends LogDomainRequest { + + protected LogClearCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.LOG_CLEAR, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/log/LogDisableCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogDisableCommandRequestModel.java new file mode 100644 index 00000000..a5ba0a69 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogDisableCommandRequestModel.java @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.LogDomainRequest; +import com.amazon.apl.devtools.models.common.LogDomainResponse; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogDisableCommandRequestModel extends LogDomainRequest { + + protected LogDisableCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.LOG_DISABLE, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEnableCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEnableCommandRequestModel.java new file mode 100644 index 00000000..974c4723 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEnableCommandRequestModel.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.LogDomainRequest; +import com.amazon.apl.devtools.models.common.LogDomainResponse; +import org.json.JSONException; +import org.json.JSONObject; + +public class LogEnableCommandRequestModel extends LogDomainRequest { + protected LogEnableCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.LOG_ENABLE, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEntry.java b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEntry.java new file mode 100644 index 00000000..c1bfdf47 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEntry.java @@ -0,0 +1,44 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.devtools.models.log; + +import com.amazon.apl.android.Session; + +public class LogEntry { + private final Session.LogEntryLevel level; + private final Session.LogEntrySource source; + private final String text; + private final double timestamp; + private final Object[] arguments; + + public LogEntry(Session.LogEntryLevel level, Session.LogEntrySource source, String text, double timestamp, Object[] arguments) { + this.level = level; + this.source = source; + this.text = text; + this.timestamp = timestamp; + this.arguments = arguments; + } + + public Session.LogEntryLevel getLevel() { + return level; + } + + public Session.LogEntrySource getSource() { + return source; + } + + public String getText() { + return text; + } + + public double getTimestamp() { + return timestamp; + } + + public Object[] getArguments() { + return arguments; + } +} + diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEntryAddedEvent.java b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEntryAddedEvent.java new file mode 100644 index 00000000..7138f1a8 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/log/LogEntryAddedEvent.java @@ -0,0 +1,119 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.log; + +import android.util.Log; + +import com.amazon.apl.android.Session; +import com.amazon.apl.devtools.enums.EventMethod; +import com.amazon.apl.devtools.models.common.Event; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.lang.reflect.Array; +import java.util.List; +import java.util.Locale; +import java.util.Map; + +public class LogEntryAddedEvent extends Event { + private static final String TAG = LogEntryAddedEvent.class.getSimpleName(); + private final LogEntryAddedEvent.Params mParams; + + public LogEntryAddedEvent(String mSessionId, Session.LogEntryLevel level, Session.LogEntrySource source, String messageText, double timestamp, Object[] arguments) { + super(EventMethod.LOG_ENTRY_ADDED, mSessionId); + mParams = new LogEntryAddedEvent.Params(level, messageText, source, timestamp, arguments); + } + + public LogEntryAddedEvent.Params getParams() { + return mParams; + } + + public static class Params { + private final Session.LogEntryLevel mLevel; + private final String mMessageText; + private final Session.LogEntrySource mSource; + private final double mTimestamp; + private final Object[] mArguments; + + public Params(Session.LogEntryLevel level, String messageText, Session.LogEntrySource source, double timestamp, Object[] arguments) { + mLevel = level; + mMessageText = messageText; + mSource = source; + mTimestamp = timestamp; + mArguments = arguments; + } + + public Session.LogEntryLevel getLevel() { + return mLevel; + } + + public String getMessageText() { + return mMessageText; + } + + public Session.LogEntrySource getSource() { + return mSource; + } + + public double getTimestamp() { + return mTimestamp; + } + + public Object[] getArguments() { + return mArguments; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + EventMethod.LOG_ENTRY_ADDED + " event object"); + JSONObject jsonParams = new JSONObject(); + JSONObject entryObject = new JSONObject() + .put("level", getParams().getLevel().toString().toLowerCase(Locale.ROOT)) + .put("text", getParams().getMessageText()) + .put("source", getParams().getSource().toString().toLowerCase(Locale.ROOT)) + .put("timestamp", getParams().getTimestamp()); + + if (getParams().getArguments() != null && getParams().getArguments().length > 0) { + entryObject.put("arguments", handleArgument(getParams().getArguments())); + } + jsonParams.put("entry", entryObject); + + return super.toJSONObject().put("params", jsonParams); + } + + private Object handleArgument(Object argument) throws JSONException { + if (argument instanceof Map) { + JSONObject jsonObject = new JSONObject(); + Map map = (Map) argument; + for (Map.Entry entry : map.entrySet()) { + jsonObject.put(entry.getKey().toString(), handleArgument(entry.getValue())); + } + return jsonObject; + } else if (argument instanceof List) { + JSONArray jsonArray = new JSONArray(); + List list = (List) argument; + for (Object item : list) { + jsonArray.put(handleArgument(item)); + } + return jsonArray; + } else if (argument.getClass().isArray()) { + JSONArray jsonArray = new JSONArray(); + int length = Array.getLength(argument); + for (int i = 0; i < length; i++) { + jsonArray.put(handleArgument(Array.get(argument, i))); + } + return jsonArray; + } else { + return argument; + } + } + +} + + diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandRequestModel.java new file mode 100644 index 00000000..07acfd9f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandRequestModel.java @@ -0,0 +1,18 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.memory; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.Request; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class MemoryGetMemoryCommandRequestModel extends Request { + protected MemoryGetMemoryCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.MEMORY_GET_MEMORY, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandResponse.java new file mode 100644 index 00000000..bc8f3c0e --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandResponse.java @@ -0,0 +1,64 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.memory; + +import android.os.Debug; +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.Response; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.HashMap; +import java.util.Map; + +public class MemoryGetMemoryCommandResponse extends Response { + private static final String TAG = MemoryGetMemoryCommandResponse.class.getSimpleName(); + + private Debug.MemoryInfo mMemoryInfo; + + public MemoryGetMemoryCommandResponse(int id, Debug.MemoryInfo memoryInfo) { + super(id); + mMemoryInfo = memoryInfo; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.MEMORY_GET_MEMORY + " response object"); + + // For a standard total, we're calculating Resident Set Size (RSS), which includes both + // private and shared, both clean and dirty. + final int total = mMemoryInfo.getTotalPrivateClean() + + mMemoryInfo.getTotalPrivateDirty() + + mMemoryInfo.getTotalSharedClean() + + mMemoryInfo.getTotalSharedDirty(); + + // In addition, we'll add platform-specific memory stats + Map stats = new HashMap(); + stats.put("dalvikPrivateDirty", mMemoryInfo.dalvikPrivateDirty); + stats.put("dalvikPss", mMemoryInfo.dalvikPss); + stats.put("dalvikSharedDirty", mMemoryInfo.dalvikSharedDirty); + stats.put("nativePrivateDirty", mMemoryInfo.nativePrivateDirty); + stats.put("nativePss", mMemoryInfo.nativePss); + stats.put("nativeSharedDirty", mMemoryInfo.nativeSharedDirty); + stats.put("otherPrivateDirty", mMemoryInfo.otherPrivateDirty); + stats.put("otherPss", mMemoryInfo.otherPss); + stats.put("otherSharedDirty", mMemoryInfo.otherSharedDirty); + + JSONArray statsArray = new JSONArray(); + for (Map.Entry stat : stats.entrySet()) { + statsArray.put(new JSONObject().put("name", stat.getKey()).put("value", stat.getValue())); + } + + JSONObject result = new JSONObject(); + result.put("total", total); + result.put("stats", statsArray); + return super.toJSONObject().put("result", result); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/network/DTNetworkRequestHandler.java b/apl/src/main/java/com/amazon/apl/devtools/models/network/DTNetworkRequestHandler.java new file mode 100644 index 00000000..9fd16cfe --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/network/DTNetworkRequestHandler.java @@ -0,0 +1,37 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import android.util.Log; +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.ViewTypeTarget; + +public class DTNetworkRequestHandler implements IDTNetworkRequestHandler { + private final static String TAG = DTNetworkRequestHandler.class.getSimpleName(); + private final ViewTypeTarget mTarget; + + public DTNetworkRequestHandler(ViewTypeTarget target) { + mTarget = target; + } + + @Override + public void requestWillBeSent(int requestId, double timestamp, String url, DTNetworkRequestType type) { + Log.d(TAG, "Event requestWillBeSent to " + mTarget.getName() + ". with request Id: " + requestId); + mTarget.post(() -> mTarget.onNetworkRequestWillBeSent(requestId, timestamp, url, type.toString())); + } + + @Override + public void loadingFailed(int requestId, double timestamp) { + Log.d(TAG, "Event loadingFailed to " + mTarget.getName() + ". with request Id: " + requestId); + mTarget.post(() -> mTarget.onNetworkLoadingFailed(requestId, timestamp)); + } + + @Override + public void loadingFinished(int requestId, double timestamp, int encodedDataLength) { + Log.d(TAG, "Event loadingFinished to " + mTarget.getName() + ". with request Id: " + requestId); + mTarget.post(() -> mTarget.onNetworkLoadingFinished(requestId, timestamp, encodedDataLength)); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/network/IDTNetworkRequestHandler.java b/apl/src/main/java/com/amazon/apl/devtools/models/network/IDTNetworkRequestHandler.java new file mode 100644 index 00000000..479767a8 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/network/IDTNetworkRequestHandler.java @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import android.net.Uri; +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.util.IdGenerator; +import java.util.Arrays; +import java.util.HashSet; +import java.util.Set; + +/** + * This dev tool interface is to listen to when there is a network call. + */ +public interface IDTNetworkRequestHandler { + + /** + * The DT Network Request Id Generator. + */ + IdGenerator IdGenerator = new IdGenerator(); + + /** + * The valid accepted URL schemes. + * null represents an HTTP URL without any scheme specified, which is supported in APL <= 1.1 + */ + Set ValidUrlScheme = new HashSet<>(Arrays.asList("https", "http", null)); + + /** + * When a network call is about to be made. + * + * @param requestId The specific requestId to the network request. + * @param timestamp The timestamp when the event happened. + * @param url The URL to where the network call is going to be made to. + * @param type The type of network request. + */ + void requestWillBeSent(int requestId, double timestamp, String url, DTNetworkRequestType type); + + /** + * When a network request fails. + * + * @param requestId The specific requestId to the network request. + * @param timestamp The timestamp when the event happened. + */ + void loadingFailed(int requestId, double timestamp); + + /** + * When a network request has successfully finished. + * + * @param requestId The specific requestId to the network request. + * @param timestamp The timestamp when the event happened. + * @param encodedDataLength The size of the data downloaded. + */ + void loadingFinished(int requestId, double timestamp, int encodedDataLength); + + + /** + * Checks if the provided path is a URL Request. + * + * @param path The path to check if it's a url request. + * @return true if the path is an url request, false otherwise. + */ + static boolean isUrlRequest(String path) { + if (path == null) { + return false; + } + + String scheme = Uri.parse(path).getScheme(); + if (scheme == null) { + return true; + } + return ValidUrlScheme.contains(scheme.toLowerCase()); + } + + /** + * When the source is empty for a package, we need to use the default url. + * + * @param packageName The package name we want to import + * @param version The version of the package. + * @return the default package url. + */ + static String getDefaultPackageUrl(String packageName, String version) { + final String cloudFrontLocationPrefix = "https://arl.assets.apl-alexa.com/packages/"; + final String cloudFrontLocationSuffix = "/document.json"; + + return cloudFrontLocationPrefix + packageName + "/" + version + cloudFrontLocationSuffix; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkDomainCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkDomainCommandRequestModel.java new file mode 100644 index 00000000..927cf3b7 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkDomainCommandRequestModel.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.NetworkDomainRequest; +import com.amazon.apl.devtools.models.common.NetworkDomainResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public class NetworkDomainCommandRequestModel extends NetworkDomainRequest { + private final String mSessionId; + private final JSONObject mParams; + + protected NetworkDomainCommandRequestModel(CommandMethod method, JSONObject obj) throws JSONException { + super(method, obj); + mSessionId = obj.getString("sessionId"); + mParams = obj.has("params") && !obj.isNull("params") ? + obj.getJSONObject("params") : + new JSONObject(); + } + + public String getSessionId() { + return mSessionId; + } + + public JSONObject getParams() { + return mParams; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkLoadingFailedEvent.java b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkLoadingFailedEvent.java new file mode 100644 index 00000000..ec7136f9 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkLoadingFailedEvent.java @@ -0,0 +1,54 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.EventMethod; +import com.amazon.apl.devtools.models.common.Event; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class NetworkLoadingFailedEvent extends Event { + private static final String TAG = NetworkLoadingFailedEvent.class.getSimpleName(); + private final Params mParams; + + public NetworkLoadingFailedEvent(String mSessionId, int requestId, double timestamp) { + super(EventMethod.NETWORK_LOADING_FAILED, mSessionId); + mParams = new Params(requestId, timestamp); + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final int mRequestId; + private final double mTimestamp; + + public Params(int requestId, double timestamp) { + mRequestId = requestId; + mTimestamp = timestamp; + } + + public int getRequestId() { + return mRequestId; + } + + public double getTimestamp() { + return mTimestamp; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + EventMethod.NETWORK_LOADING_FAILED + " event object"); + return super.toJSONObject().put("params", new JSONObject() + .put("requestId", getParams().getRequestId()) + .put("timestamp", getParams().getTimestamp())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkLoadingFinishedEvent.java b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkLoadingFinishedEvent.java new file mode 100644 index 00000000..bbddaaa3 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkLoadingFinishedEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.EventMethod; +import com.amazon.apl.devtools.models.common.Event; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class NetworkLoadingFinishedEvent extends Event { + private static final String TAG = NetworkLoadingFinishedEvent.class.getSimpleName(); + private final Params mParams; + + public NetworkLoadingFinishedEvent(String mSessionId, int requestId, double timestamp, + int encodedDataLength) { + super(EventMethod.NETWORK_LOADING_FINISHED, mSessionId); + mParams = new Params(requestId, timestamp, encodedDataLength); + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final int mRequestId; + private final double mTimestamp; + private final int mEncodedDataLength; + + public Params(int requestId, double timestamp, + int encodedDataLength) { + mRequestId = requestId; + mTimestamp = timestamp; + mEncodedDataLength = encodedDataLength; + } + + public int getRequestId() { + return mRequestId; + } + + public double getTimestamp() { + return mTimestamp; + } + + public int getEncodedDataLength() { + return mEncodedDataLength; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + EventMethod.NETWORK_LOADING_FINISHED + " event object"); + return super.toJSONObject().put("params", new JSONObject() + .put("encodedDataLength", getParams().getEncodedDataLength()) + .put("requestId", getParams().getRequestId()) + .put("timestamp", getParams().getTimestamp())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkRequestWillBeSentEvent.java b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkRequestWillBeSentEvent.java new file mode 100644 index 00000000..d5533328 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/network/NetworkRequestWillBeSentEvent.java @@ -0,0 +1,70 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.EventMethod; +import com.amazon.apl.devtools.models.common.Event; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class NetworkRequestWillBeSentEvent extends Event { + private static final String TAG = NetworkRequestWillBeSentEvent.class.getSimpleName(); + private final Params mParams; + + public NetworkRequestWillBeSentEvent(String mSessionId, int requestId, double timestamp, + String documentURL, String type) { + super(EventMethod.NETWORK_REQUEST_WILL_BE_SENT, mSessionId); + mParams = new Params(requestId, timestamp, documentURL, type); + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final int mRequestId; + private final double mTimestamp; + private final String mDocumentURL; + private final String mType; + + public Params(int requestId, double timestamp, + String documentURL, String type) { + mRequestId = requestId; + mTimestamp = timestamp; + mDocumentURL = documentURL; + mType = type; + } + + public int getRequestId() { + return mRequestId; + } + + public double getTimestamp() { + return mTimestamp; + } + + public String getDocumentURL() { + return mDocumentURL; + } + + public String getType() { + return mType; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + EventMethod.NETWORK_REQUEST_WILL_BE_SENT + " event object"); + return super.toJSONObject().put("params", new JSONObject() + .put("requestId", getParams().getRequestId()) + .put("timestamp", getParams().getTimestamp()) + .put("type", getParams().getType()) + .put("documentURL", getParams().getDocumentURL())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceDisableCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceDisableCommandRequestModel.java new file mode 100644 index 00000000..14caeaac --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceDisableCommandRequestModel.java @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.performance; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandRequest; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceDisableCommandRequestModel extends PerformanceDomainCommandRequest { + + protected PerformanceDisableCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.PERFORMANCE_DISABLE, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceEnableCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceEnableCommandRequestModel.java new file mode 100644 index 00000000..6b34b12d --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceEnableCommandRequestModel.java @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.performance; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandRequest; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceEnableCommandRequestModel extends PerformanceDomainCommandRequest { + + protected PerformanceEnableCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.PERFORMANCE_ENABLE, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceGetMetricsCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceGetMetricsCommandRequestModel.java new file mode 100644 index 00000000..ad5e6186 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceGetMetricsCommandRequestModel.java @@ -0,0 +1,21 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.performance; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandRequest; + +import org.json.JSONException; +import org.json.JSONObject; + + +public abstract class PerformanceGetMetricsCommandRequestModel + extends PerformanceDomainCommandRequest { + + protected PerformanceGetMetricsCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.PERFORMANCE_GET_METRICS, obj); + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceGetMetricsCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceGetMetricsCommandResponse.java new file mode 100644 index 00000000..a807236a --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceGetMetricsCommandResponse.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.performance; + +import android.util.Log; + +import com.amazon.apl.android.utils.MetricInfo; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.PerformanceDomainCommandResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.List; + +public class PerformanceGetMetricsCommandResponse extends PerformanceDomainCommandResponse { + private static final String TAG = PerformanceGetMetricsCommandResponse.class.getSimpleName(); + private final List mPerformanceMetrics; + + public PerformanceGetMetricsCommandResponse(int id, String sessionId, List performanceMetrics) { + super(id, sessionId); + mPerformanceMetrics = performanceMetrics; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.PERFORMANCE_GET_METRICS + " response object"); + // Parse collection of MetricInfo into a JSONArray + JSONArray metricInfoJSONArray = new JSONArray(); + for (MetricInfo metricInfo : mPerformanceMetrics) { + metricInfoJSONArray.put(new JSONObject().put("name", metricInfo.getName()) + .put("value", metricInfo.getValue())); + } + return super.toJSONObject().put("result", new JSONObject() + .put("metrics", metricInfoJSONArray)); + } + +} + + + + diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceMetricsEvent.java b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceMetricsEvent.java new file mode 100644 index 00000000..ea52636f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/performance/PerformanceMetricsEvent.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.performance; + +import com.amazon.apl.android.utils.MetricInfo; +import com.amazon.apl.devtools.enums.EventMethod; +import com.amazon.apl.devtools.models.common.Event; +import java.util.List; +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +public class PerformanceMetricsEvent extends Event { + private final List mPerformanceMetrics; + public PerformanceMetricsEvent(String mSessionId, List performanceMetrics) { + super(EventMethod.PERFORMANCE_METRIC, mSessionId); + mPerformanceMetrics = performanceMetrics; + } + + @Override + public JSONObject toJSONObject() throws JSONException { + JSONArray metricInfoJSONArray = new JSONArray(); + for (MetricInfo metricInfo : mPerformanceMetrics) { + metricInfoJSONArray.put(new JSONObject().put("name", metricInfo.getName()) + .put("value", metricInfo.getValue())); + } + return super.toJSONObject().put("params", new JSONObject().put("metrics", metricInfoJSONArray)); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetAttachToTargetCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetAttachToTargetCommandRequestModel.java new file mode 100644 index 00000000..ed47a15f --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetAttachToTargetCommandRequestModel.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.target; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.TargetDomainCommandRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class TargetAttachToTargetCommandRequestModel + extends TargetDomainCommandRequest { + private final Params mParams; + + protected TargetAttachToTargetCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.TARGET_ATTACH_TO_TARGET, obj); + mParams = new Params(obj.getJSONObject("params").getString("targetId")); + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final String mTargetId; + + private Params(String targetId) { + mTargetId = targetId; + } + + public String getTargetId() { + return mTargetId; + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetAttachToTargetCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetAttachToTargetCommandResponse.java new file mode 100644 index 00000000..8fa87bfd --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetAttachToTargetCommandResponse.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.target; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.TargetDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class TargetAttachToTargetCommandResponse extends TargetDomainCommandResponse { + private static final String TAG = TargetAttachToTargetCommandResponse.class.getSimpleName(); + private final Result mResult; + + public TargetAttachToTargetCommandResponse(int id, String sessionId) { + super(id); + mResult = new Result(sessionId); + } + + public Result getResult() { + return mResult; + } + + private static class Result { + private final String mSessionId; + + public Result(String sessionId) { + mSessionId = sessionId; + } + + public String getSessionId() { + return mSessionId; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.TARGET_ATTACH_TO_TARGET + " response object"); + return super.toJSONObject().put("result", new JSONObject() + .put("sessionId", getResult().getSessionId())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetGetTargetsCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetGetTargetsCommandRequestModel.java new file mode 100644 index 00000000..3d56bb8c --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetGetTargetsCommandRequestModel.java @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.target; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.TargetDomainCommandRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class TargetGetTargetsCommandRequestModel + extends TargetDomainCommandRequest { + protected TargetGetTargetsCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.TARGET_GET_TARGETS, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetGetTargetsCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetGetTargetsCommandResponse.java new file mode 100644 index 00000000..fa767df4 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/target/TargetGetTargetsCommandResponse.java @@ -0,0 +1,102 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.target; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.Target; +import com.amazon.apl.devtools.models.common.TargetDomainCommandResponse; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.Collection; + +public final class TargetGetTargetsCommandResponse extends TargetDomainCommandResponse { + private static final String TAG = TargetGetTargetsCommandResponse.class.getSimpleName(); + private final Result mResult; + + public TargetGetTargetsCommandResponse(int id, Collection targets) { + super(id); + mResult = new Result(targets); + } + + public Result getResult() { + return mResult; + } + + private static class Result { + private final Collection mTargetInfos; + + public Result(Collection targets) { + mTargetInfos = parseTargetInfos(targets); + } + + public Collection getTargetInfos() { + return mTargetInfos; + } + + /** + * parseTargetInfos parses a collection of Target into a collection of TargetInfo + */ + private Collection parseTargetInfos(Collection targets) { + Collection targetInfos = new ArrayList<>(); + for (Target target : targets) { + targetInfos.add(parseTargetInfo(target)); + } + return targetInfos; + } + + /** + * parseTargetInfo parses a single Target into a TargetInfo + */ + private TargetInfo parseTargetInfo(Target target) { + return new TargetInfo(target.getTargetId(), target.getType().toString(), + target.getName()); + } + + private static class TargetInfo { + private final String mTargetId; + private final String mType; + private final String mName; + + public TargetInfo(String targetId, String type, String name) { + mTargetId = targetId; + mType = type; + mName = name; + } + + public String getTargetId() { + return mTargetId; + } + + public String getType() { + return mType; + } + + public String getName() { + return mName; + } + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.TARGET_GET_TARGETS + " response object"); + // Parse collection of TargetInfo into a JSONArray + JSONArray targetInfoJSONArray = new JSONArray(); + for (Result.TargetInfo targetInfo : getResult().getTargetInfos()) { + targetInfoJSONArray.put(new JSONObject().put("targetId", targetInfo.getTargetId()) + .put("type", targetInfo.getType()) + .put("name", targetInfo.getName())); + } + return super.toJSONObject().put("result", new JSONObject() + .put("targetInfos", targetInfoJSONArray)); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ExecuteCommandStatus.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ExecuteCommandStatus.java new file mode 100644 index 00000000..ea5760f6 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ExecuteCommandStatus.java @@ -0,0 +1,26 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + + +import androidx.annotation.NonNull; + +public enum ExecuteCommandStatus { + COMPLETED("completed"), + TERMINATED("terminated"); + + private final String mStatus; + + ExecuteCommandStatus(String status) { + mStatus = status; + } + + @Override + @NonNull + public String toString() { + return mStatus; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewCaptureImageCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewCaptureImageCommandRequestModel.java new file mode 100644 index 00000000..1885c391 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewCaptureImageCommandRequestModel.java @@ -0,0 +1,20 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.ViewDomainCommandRequest; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class ViewCaptureImageCommandRequestModel + extends ViewDomainCommandRequest { + + protected ViewCaptureImageCommandRequestModel(JSONObject obj) throws JSONException { + super(CommandMethod.VIEW_CAPTURE_IMAGE, obj); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewCaptureImageCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewCaptureImageCommandResponse.java new file mode 100644 index 00000000..dd60e478 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewCaptureImageCommandResponse.java @@ -0,0 +1,69 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.ViewDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class ViewCaptureImageCommandResponse extends ViewDomainCommandResponse { + private static final String TAG = ViewCaptureImageCommandResponse.class.getSimpleName(); + private final Result mResult; + + public ViewCaptureImageCommandResponse(int id, String sessionId, int height, int width, + String type, String data) { + super(id, sessionId); + mResult = new Result(height, width, type, data); + } + + public Result getResult() { + return mResult; + } + + private static class Result { + private final int mHeight; + private final int mWidth; + private final String mType; + private final String mData; + + public Result(int height, int width, String type, String data) { + mHeight = height; + mWidth = width; + mType = type; + mData = data; + } + + public int getHeight() { + return mHeight; + } + + public int getWidth() { + return mWidth; + } + + public String getType() { + return mType; + } + + public String getData() { + return mData; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.VIEW_CAPTURE_IMAGE + " response object"); + return super.toJSONObject().put("result", new JSONObject() + .put("height", getResult().getHeight()) + .put("width", getResult().getWidth()) + .put("type", getResult().getType()) + .put("data", getResult().getData())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewExecuteCommandsCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewExecuteCommandsCommandRequestModel.java new file mode 100644 index 00000000..ab8685ef --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewExecuteCommandsCommandRequestModel.java @@ -0,0 +1,46 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.common.ViewDomainCommandRequest; +import com.amazon.apl.devtools.models.error.DTException; + +import org.json.JSONException; +import org.json.JSONObject; + +public abstract class ViewExecuteCommandsCommandRequestModel + extends ViewDomainCommandRequest { + private final Params mParams; + + protected ViewExecuteCommandsCommandRequestModel(JSONObject obj) throws JSONException, DTException { + super(CommandMethod.VIEW_EXECUTE_COMMANDS, obj); + try { + mParams = new Params(obj.getJSONObject("params").getJSONArray("commands") + .toString()); + } catch (JSONException e) { + throw new DTException(getId(), DTError.INVALID_DOCUMENT.getErrorCode(), + "Invalid commands document", e); + } + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final String mCommands; + + private Params(String commands) { + mCommands = commands; + } + + public String getCommands() { + return mCommands; + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewExecuteCommandsCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewExecuteCommandsCommandResponse.java new file mode 100644 index 00000000..2c257984 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewExecuteCommandsCommandResponse.java @@ -0,0 +1,47 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.ViewDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class ViewExecuteCommandsCommandResponse extends ViewDomainCommandResponse { + private static final String TAG = ViewExecuteCommandsCommandResponse.class.getSimpleName(); + private final Result mResult; + + public ViewExecuteCommandsCommandResponse(int id, String sessionId, ExecuteCommandStatus status) { + super(id, sessionId); + mResult = new Result(status.toString()); + } + + private Result getResult() { + return mResult; + } + + private static class Result { + private final String mStatus; + + public Result(String status) { + mStatus = status; + } + + public String getStatus() { + return mStatus; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.VIEW_EXECUTE_COMMANDS + " response object"); + return super.toJSONObject().put("result", new JSONObject() + .put("status", getResult().getStatus())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewSetDocumentCommandRequestModel.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewSetDocumentCommandRequestModel.java new file mode 100644 index 00000000..4b4b6f0a --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewSetDocumentCommandRequestModel.java @@ -0,0 +1,119 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.common.ViewDomainCommandRequest; +import com.amazon.apl.devtools.models.error.DTException; + +import org.json.JSONException; +import org.json.JSONObject; + +import java.util.ArrayList; +import java.util.HashMap; +import java.util.Iterator; +import java.util.Map; +import java.util.List; + +public abstract class ViewSetDocumentCommandRequestModel + extends ViewDomainCommandRequest { + private final Params mParams; + + public static class Configuration { + public final Map> mLiveArrays = new HashMap<>(); + public final Map> mLiveMaps = new HashMap<>(); + + public static Configuration create(JSONObject json) throws JSONException { + Configuration result = new Configuration(); + if (json.has("liveData")) { + JSONObject liveData = json.getJSONObject("liveData"); + for (Iterator it = liveData.keys(); it.hasNext(); ) { + String key = it.next(); + + if (liveData.optJSONArray(key) != null) { + List l = new ArrayList<>(); + for (int i = 0; i < liveData.getJSONArray(key).length(); i++) { + l.add(liveData.getJSONArray(key).get(i)); + } + result.mLiveArrays.put(key, l); + } else { + Map m = new HashMap<>(); + for (Iterator kiter = liveData.getJSONObject(key).keys(); kiter.hasNext(); ) { + String k = kiter.next(); + m.put(k, liveData.getJSONObject(key).get(k)); + } + result.mLiveMaps.put(key, m); + } + } + } + return result; + } + } + + protected ViewSetDocumentCommandRequestModel(JSONObject obj) throws JSONException, DTException { + super(CommandMethod.VIEW_SET_DOCUMENT, obj); + try { + JSONObject params = obj.getJSONObject("params"); + JSONObject document = params.getJSONObject("document"); + if (document.has("name") && document.getString("name").equals("RenderDocument")) { + JSONObject payload = document.getJSONObject("payload"); + mParams = new Params(payload.getJSONObject("document").toString(), + payload.getJSONObject("datasources").toString()); + } else { + mParams = new Params(document.toString()); + + JSONObject datasources = params.optJSONObject("data"); + if (datasources != null) mParams.data(datasources.toString()); + } + + if (params.has("configuration")) { + mParams.configuration(Configuration.create(params.getJSONObject("configuration"))); + } + } catch (JSONException e) { + throw new DTException(getId(), DTError.INVALID_DOCUMENT.getErrorCode(), + "Invalid APL document", e); + } + } + + public Params getParams() { + return mParams; + } + + public static class Params { + private final String mDocument; + private String mData; + private Configuration mConfiguration; + + private Params(String document) { + mDocument = document; + mData = null; + } + + private Params(String document, String data) { + mDocument = document; + mData = data; + } + + private void configuration(Configuration configuration) { + mConfiguration = configuration; + } + + private void data(String data) { + mData = data; + } + + public String getDocument() { + return mDocument; + } + + public String getData() { + return mData; + } + + public Configuration getConfiguration() { return mConfiguration; } + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewSetDocumentCommandResponse.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewSetDocumentCommandResponse.java new file mode 100644 index 00000000..23ac0834 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewSetDocumentCommandResponse.java @@ -0,0 +1,28 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.models.common.ViewDomainCommandResponse; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class ViewSetDocumentCommandResponse extends ViewDomainCommandResponse { + private static final String TAG = ViewSetDocumentCommandResponse.class.getSimpleName(); + + public ViewSetDocumentCommandResponse(int id, String sessionId) { + super(id, sessionId); + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + CommandMethod.VIEW_SET_DOCUMENT + " response object"); + return super.toJSONObject(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewStateChangeEvent.java b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewStateChangeEvent.java new file mode 100644 index 00000000..16cf9a88 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/models/view/ViewStateChangeEvent.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.view; + +import android.util.Log; + +import com.amazon.apl.devtools.enums.EventMethod; +import com.amazon.apl.devtools.enums.ViewState; +import com.amazon.apl.devtools.models.common.Event; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class ViewStateChangeEvent extends Event { + private static final String TAG = ViewStateChangeEvent.class.getSimpleName(); + private final Params mParams; + + public ViewStateChangeEvent(String mSessionId, ViewState state, int documentId, + double timestamp) { + super(EventMethod.VIEW_STATE_CHANGE, mSessionId); + mParams = new Params(state, documentId, timestamp); + } + + public Params getParams() { + return mParams; + } + + private static class Params { + private final ViewState mState; + private final int mDocumentId; + private final double mTimestamp; + + public Params(ViewState state, int documentId, double timestamp) { + mState = state; + mDocumentId = documentId; + mTimestamp = timestamp; + } + + public ViewState getState() { + return mState; + } + + public int getDocumentId() { + return mDocumentId; + } + + public double getTimestamp() { + return mTimestamp; + } + } + + @Override + public JSONObject toJSONObject() throws JSONException { + Log.i(TAG, "Serializing " + EventMethod.VIEW_STATE_CHANGE + " event object"); + return super.toJSONObject().put("params", new JSONObject() + .put("state", getParams().getState()) + .put("documentId", getParams().getDocumentId()) + .put("timestamp", getParams().getTimestamp())); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/CommandMethodUtil.java b/apl/src/main/java/com/amazon/apl/devtools/util/CommandMethodUtil.java new file mode 100644 index 00000000..80de82b4 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/CommandMethodUtil.java @@ -0,0 +1,19 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import com.amazon.apl.devtools.enums.CommandMethod; + +public final class CommandMethodUtil { + public CommandMethod parseMethod(String methodStr) { + for (CommandMethod commandMethod : CommandMethod.values()) { + if (commandMethod.toString().equals(methodStr)) { + return commandMethod; + } + } + return CommandMethod.EMPTY; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/CommandRequestFactory.java b/apl/src/main/java/com/amazon/apl/devtools/util/CommandRequestFactory.java new file mode 100644 index 00000000..5e7cbce0 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/CommandRequestFactory.java @@ -0,0 +1,134 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_CONTEXT; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_DOM; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_MAIN_PACKAGE; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_PACKAGE; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_PACKAGE_LIST; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_ROOT_CONTEXT; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_SCENE_GRAPH; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_GET_VISUAL_CONTEXT; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_HIDE_HIGHLIGHT; +import static com.amazon.apl.devtools.enums.CommandMethod.DOCUMENT_HIGHLIGHT_COMPONENT; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.executers.DocumentCommandRequest; +import com.amazon.apl.devtools.executers.FrameMetricsRecordCommandRequest; +import com.amazon.apl.devtools.executers.FrameMetricsStopCommandRequest; +import com.amazon.apl.devtools.executers.LiveDataUpdateCommandRequest; +import com.amazon.apl.devtools.executers.MemoryGetMemoryCommandRequest; +import com.amazon.apl.devtools.executers.NetworkDisableCommandRequest; +import com.amazon.apl.devtools.executers.NetworkEnableCommandRequest; +import com.amazon.apl.devtools.executers.PerformanceDisableCommandRequest; +import com.amazon.apl.devtools.executers.PerformanceEnableCommandRequest; +import com.amazon.apl.devtools.executers.PerformanceGetMetricsCommandRequest; +import com.amazon.apl.devtools.executers.TargetAttachToTargetCommandRequest; +import com.amazon.apl.devtools.executers.TargetGetTargetsCommandRequest; +import com.amazon.apl.devtools.executers.ViewCaptureImageCommandRequest; +import com.amazon.apl.devtools.executers.ViewExecuteCommandsCommandRequest; +import com.amazon.apl.devtools.executers.ViewSetDocumentCommandRequest; +import com.amazon.apl.devtools.executers.LogEnableCommandRequest; +import com.amazon.apl.devtools.executers.LogDisableCommandRequest; +import com.amazon.apl.devtools.executers.LogClearCommandRequest; +import com.amazon.apl.devtools.models.RequestHeader; +import com.amazon.apl.devtools.models.common.Request; +import com.amazon.apl.devtools.models.common.Response; +import com.amazon.apl.devtools.models.error.DTException; + +import org.json.JSONException; +import org.json.JSONObject; + +public final class CommandRequestFactory { + private static final String TAG = CommandRequestFactory.class.getSimpleName(); + private final TargetCatalog mTargetCatalog; + private final CommandMethodUtil mCommandMethodUtil; + private final CommandRequestValidator mCommandRequestValidator; + + public CommandRequestFactory(TargetCatalog targetCatalog, CommandMethodUtil commandMethodUtil, + CommandRequestValidator commandRequestValidator) { + mTargetCatalog = targetCatalog; + mCommandMethodUtil = commandMethodUtil; + mCommandRequestValidator = commandRequestValidator; + } + + // Using a wildcard because the response type associated with the returned request is unknown + public Request createCommandRequest(JSONObject obj, DTConnection connection) + throws JSONException, DTException { + Log.i(TAG, "Creating command request object"); + RequestHeader requestHeader = new RequestHeader(mCommandMethodUtil, obj); + switch (requestHeader.getMethod()) { + case TARGET_GET_TARGETS: + return new TargetGetTargetsCommandRequest(mTargetCatalog, obj); + case TARGET_ATTACH_TO_TARGET: + return new TargetAttachToTargetCommandRequest(mTargetCatalog, + mCommandRequestValidator, obj, connection); + case VIEW_SET_DOCUMENT: + return new ViewSetDocumentCommandRequest(mCommandRequestValidator, obj, connection); + case VIEW_CAPTURE_IMAGE: + return new ViewCaptureImageCommandRequest(mCommandRequestValidator, obj, + connection); + case VIEW_EXECUTE_COMMANDS: + return new ViewExecuteCommandsCommandRequest(mCommandRequestValidator, obj, + connection); + case LIVE_DATA_UPDATE: + return new LiveDataUpdateCommandRequest(mCommandRequestValidator, obj, + connection); + case PERFORMANCE_ENABLE: + return new PerformanceEnableCommandRequest(mCommandRequestValidator, obj, + connection); + case PERFORMANCE_DISABLE: + return new PerformanceDisableCommandRequest(mCommandRequestValidator, obj, + connection); + case PERFORMANCE_GET_METRICS: + return new PerformanceGetMetricsCommandRequest(mCommandRequestValidator, obj, + connection); + case MEMORY_GET_MEMORY: + return new MemoryGetMemoryCommandRequest(obj); + case FRAMEMETRICS_RECORD: + return new FrameMetricsRecordCommandRequest(mCommandRequestValidator, obj, connection); + case FRAMEMETRICS_STOP: + return new FrameMetricsStopCommandRequest(mCommandRequestValidator, obj, connection); + case LOG_ENABLE: + return new LogEnableCommandRequest(mCommandRequestValidator, obj, connection); + case LOG_DISABLE: + return new LogDisableCommandRequest(mCommandRequestValidator, obj, connection); + case LOG_CLEAR: + return new LogClearCommandRequest(mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_MAIN_PACKAGE: + return new DocumentCommandRequest(DOCUMENT_GET_MAIN_PACKAGE, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_PACKAGE_LIST: + return new DocumentCommandRequest(DOCUMENT_GET_PACKAGE_LIST, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_PACKAGE: + return new DocumentCommandRequest(DOCUMENT_GET_PACKAGE, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_VISUAL_CONTEXT: + return new DocumentCommandRequest(DOCUMENT_GET_VISUAL_CONTEXT, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_DOM: + return new DocumentCommandRequest(DOCUMENT_GET_DOM, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_SCENE_GRAPH: + return new DocumentCommandRequest(DOCUMENT_GET_SCENE_GRAPH, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_ROOT_CONTEXT: + return new DocumentCommandRequest(DOCUMENT_GET_ROOT_CONTEXT, mCommandRequestValidator, obj, connection); + case DOCUMENT_GET_CONTEXT: + return new DocumentCommandRequest(DOCUMENT_GET_CONTEXT, mCommandRequestValidator, obj, connection); + case DOCUMENT_HIGHLIGHT_COMPONENT: + return new DocumentCommandRequest(DOCUMENT_HIGHLIGHT_COMPONENT, mCommandRequestValidator, obj, connection); + case DOCUMENT_HIDE_HIGHLIGHT: + return new DocumentCommandRequest(DOCUMENT_HIDE_HIGHLIGHT, mCommandRequestValidator, obj, connection); + case NETWORK_ENABLE: + return new NetworkEnableCommandRequest(mCommandRequestValidator, obj, connection); + case NETWORK_DISABLE: + return new NetworkDisableCommandRequest(mCommandRequestValidator, obj, connection); + default: + throw new DTException(requestHeader.getId(), + DTError.METHOD_NOT_IMPLEMENTED.getErrorCode(), DTError.METHOD_NOT_IMPLEMENTED.getErrorMsg()); + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/CommandRequestValidator.java b/apl/src/main/java/com/amazon/apl/devtools/util/CommandRequestValidator.java new file mode 100644 index 00000000..f5659eba --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/CommandRequestValidator.java @@ -0,0 +1,76 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import android.util.Log; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.Target; +import com.amazon.apl.devtools.models.error.DTException; + +public final class CommandRequestValidator { + private static final String TAG = CommandRequestValidator.class.getSimpleName(); + private final TargetCatalog mTargetCatalog; + + public CommandRequestValidator(TargetCatalog targetCatalog) { + mTargetCatalog = targetCatalog; + } + + public void validateBeforeGettingTargetFromTargetCatalog(int id, String targetId) + throws DTException { + Log.i(TAG, "Validating target id " + targetId); + if (!mTargetCatalog.has(targetId)) { + throw new DTException(id, DTError.NO_SUCH_TARGET.getErrorCode(), "No such target"); + } + } + + public void validateBeforeCreatingSession(int id, DTConnection connection, Target target) + throws DTException { + Log.i(TAG, "Validating that target with target id " + target.getTargetId() + + " is not yet attached to a session belonging to this connection"); + for (String registeredSessionId : target.getRegisteredSessionIds()) { + if (connection.hasSession(registeredSessionId)) { + throw new DTException(id, DTError.TARGET_ALREADY_ATTACHED.getErrorCode(), + "Target is already attached"); + } + } + } + + public void validateBeforeGettingSession(int id, String sessionId, DTConnection connection) + throws DTException { + Log.i(TAG, "Validating session id " + sessionId); + if (!connection.hasSession(sessionId)) { + throw new DTException(id, DTError.INVALID_SESSION_ID.getErrorCode(), + "Invalid session id"); + } + } + + public void validatePerformanceEnabled(int id, String sessionId, Boolean isEnabled) + throws DTException { + Log.i(TAG, "Validating if performance metric is enabled " + sessionId); + if (!isEnabled) { + throw new DTException(id, DTError.PERFORMANCE_ALREADY_DISABLED.getErrorCode(), + "Metric is not enabled"); + } + } + + public void validateLogEnabled(int id, String sessionId, boolean isEnabled) throws DTException { + Log.i(TAG, "Validating if Log is enabled " + sessionId); + if (!isEnabled) { + throw new DTException(id, DTError.LOG_ALREADY_DISABLED.getErrorCode(), + "Log is not enabled"); + } + } + + public void validateNetworkEnabled(int id, String sessionId, boolean isEnabled) throws DTException { + Log.i(TAG, "Validating if Network is enabled " + sessionId); + if (!isEnabled) { + throw new DTException(id, DTError.NETWORK_ALREADY_DISABLED.getErrorCode(), + "Network is not enabled"); + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/DependencyContainer.java b/apl/src/main/java/com/amazon/apl/devtools/util/DependencyContainer.java new file mode 100644 index 00000000..a920cae7 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/DependencyContainer.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +public final class DependencyContainer { + private static DependencyContainer sInstance; + private final TargetCatalog mTargetCatalog; + private final CommandRequestFactory mCommandRequestFactory; + private final MultiViewUtil mMultiViewUtil; + + private DependencyContainer() { + mTargetCatalog = new TargetCatalog(); + CommandMethodUtil commandMethodUtil = new CommandMethodUtil(); + CommandRequestValidator commandRequestValidator = + new CommandRequestValidator(mTargetCatalog); + mCommandRequestFactory = new CommandRequestFactory(mTargetCatalog, commandMethodUtil, + commandRequestValidator); + mMultiViewUtil = new MultiViewUtil(); + } + + public static DependencyContainer getInstance() { + if (sInstance == null) { + sInstance = new DependencyContainer(); + } + return sInstance; + } + + public TargetCatalog getTargetCatalog() { + return mTargetCatalog; + } + + public CommandRequestFactory getCommandRequestFactory() { + return mCommandRequestFactory; + } + + public MultiViewUtil getMultiViewUtil() { + return mMultiViewUtil; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/IDTCallback.java b/apl/src/main/java/com/amazon/apl/devtools/util/IDTCallback.java new file mode 100644 index 00000000..9127e735 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/IDTCallback.java @@ -0,0 +1,14 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +public interface IDTCallback { + void execute(T t, RequestStatus requestStatus); + + default void execute(RequestStatus requestStatus) { + execute(null, requestStatus); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/IdGenerator.java b/apl/src/main/java/com/amazon/apl/devtools/util/IdGenerator.java new file mode 100644 index 00000000..df320255 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/IdGenerator.java @@ -0,0 +1,27 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +public final class IdGenerator { + private int mNextId = 100; + + public IdGenerator() { + } + + public IdGenerator(int startId) { + mNextId = startId; + } + + public int generateId() { + int id = mNextId; + mNextId++; + return id; + } + + public String generateId(String prefix) { + return prefix + generateId(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/MultiViewUtil.java b/apl/src/main/java/com/amazon/apl/devtools/util/MultiViewUtil.java new file mode 100644 index 00000000..1ded4582 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/MultiViewUtil.java @@ -0,0 +1,24 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +public final class MultiViewUtil { + public int computeNumberOfColumns(int numberOfViews) { + if (numberOfViews <= 1) { + // Return 1 column to avoid divide by zero error when result is used in a grid view + return 1; + } + return (int) Math.ceil(Math.sqrt(numberOfViews)); + } + + public int computeNumberOfRows(int numberOfViews, int numberOfColumns) { + if (numberOfViews <= 1 || numberOfColumns <= 0) { + // Return 1 row when the result of division will error or return a non-positive number + return 1; + } + return (int) Math.ceil((double) numberOfViews / numberOfColumns); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/RequestStatus.java b/apl/src/main/java/com/amazon/apl/devtools/util/RequestStatus.java new file mode 100644 index 00000000..1cae2567 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/RequestStatus.java @@ -0,0 +1,52 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import com.amazon.apl.devtools.enums.DTError; + +/** + * This class will track the execution status of any given request. + */ +public class RequestStatus { + private final ExecutionStatus mStatus; + private final int mId; + private final DTError mError; + + public enum ExecutionStatus { + SUCCESSFUL, + FAILED + } + + private RequestStatus(ExecutionStatus status) { + this(status, 0, DTError.UNKNOWN_ERROR); + } + + private RequestStatus(ExecutionStatus status, int id, DTError error) { + mStatus = status; + mId = id; + mError = error; + } + + public static RequestStatus successful() { + return new RequestStatus(ExecutionStatus.SUCCESSFUL); + } + + public static RequestStatus failed(int id, DTError error) { + return new RequestStatus(ExecutionStatus.FAILED, id, error); + } + + public ExecutionStatus getExecutionStatus() { + return mStatus; + } + + public int getId() { + return mId; + } + + public DTError getError() { + return mError; + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/util/TargetCatalog.java b/apl/src/main/java/com/amazon/apl/devtools/util/TargetCatalog.java new file mode 100644 index 00000000..95fb3023 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/util/TargetCatalog.java @@ -0,0 +1,81 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import android.os.Handler; +import android.os.HandlerThread; + +import com.amazon.apl.devtools.enums.TargetType; +import com.amazon.apl.devtools.models.Target; +import com.amazon.apl.devtools.models.ViewTypeTarget; + +import java.util.ArrayList; +import java.util.Collection; +import java.util.List; +import java.util.SortedMap; +import java.util.TreeMap; + +public final class TargetCatalog { + private final SortedMap mTargets = new TreeMap<>(); + private final Handler mHandler; + private final HandlerThread mHandlerThread; + + public TargetCatalog() { + // Create a background thread and associate a Handler with it + mHandlerThread = new HandlerThread("TargetCatalogThread"); + mHandlerThread.start(); + mHandler = new Handler(mHandlerThread.getLooper()); + } + + public void post(Runnable runnable) { + mHandler.post(runnable); + } + + public Handler getHandler() { + return mHandler; + } + + public Target add(Target target) { + return mTargets.put(target.getTargetId(), target); + } + + public Target remove(Target target) { + return mTargets.remove(target.getTargetId()); + } + + public boolean has(String targetId) { + return mTargets.containsKey(targetId); + } + + public Target get(String targetId) { + return mTargets.get(targetId); + } + + /** + * getAll returns an ordered collection of targets + */ + public Collection getAll() { + return mTargets.values(); + } + + /** + * getViewTypeTargets returns an ordered list of view type targets + */ + public List getViewTypeTargets() { + List viewTypeTargets = new ArrayList<>(); + for (Target target : getAll()) { + if (target.getType() == TargetType.VIEW) { + viewTypeTargets.add((ViewTypeTarget) target); + } + } + return viewTypeTargets; + } + + public void cleanup() { + mHandler.removeCallbacksAndMessages(null); + mHandlerThread.quit(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/devtools/views/CaptureImageHelper.java b/apl/src/main/java/com/amazon/apl/devtools/views/CaptureImageHelper.java new file mode 100644 index 00000000..72acd056 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/views/CaptureImageHelper.java @@ -0,0 +1,92 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.developer.views; + +import android.annotation.TargetApi; +import android.app.Activity; +import android.graphics.Bitmap; +import android.graphics.Canvas; +import android.graphics.Rect; +import android.os.Build; +import android.os.Handler; +import android.util.Log; +import android.view.PixelCopy; +import android.view.View; + +import androidx.annotation.NonNull; + +import java.util.concurrent.CompletableFuture; +import java.util.concurrent.ExecutionException; +import java.util.concurrent.TimeUnit; +import java.util.concurrent.TimeoutException; + +/** + * Class to capture screenshot of a view + * Uses different APIs to capture screenshot since the APIs were added at different SDK versions + * Refer https://developer.android.com/reference/android/view/View#getDrawingCache() + * Refer https://developer.android.com/reference/android/view/PixelCopy + */ +@TargetApi(24) +public class CaptureImageHelper { + private static final String TAG = "CaptureImageHelper"; + private final Bitmap mUnitBitmap = Bitmap.createBitmap(1, 1, Bitmap.Config.ARGB_8888); + + @NonNull + public Bitmap captureImage(View view) { + // Refer https://developer.android.com/reference/android/view/View#getDrawingCache() + // PixelCopy API is the recommended way and is only available on API levels >= 26 + if (android.os.Build.VERSION.SDK_INT >= Build.VERSION_CODES.P && view.getContext() instanceof Activity) { + return captureImage(view, (Activity) view.getContext()); + } else { + return captureImageLegacy(view); + } + } + + @TargetApi(26) + @NonNull + private Bitmap captureImage(View view, Activity activity) { + CompletableFuture completableFuture = new CompletableFuture<>(); + + Bitmap bitmap = Bitmap.createBitmap(view.getWidth(), view.getHeight(), Bitmap.Config.ARGB_8888); + int[] location = new int[2]; + view.getLocationInWindow(location); + + Rect rect = new Rect(location[0], location[1], location[0] + view.getWidth(), location[1] + view.getHeight()); + PixelCopy.OnPixelCopyFinishedListener listener = copyResult -> { + + if (copyResult == PixelCopy.SUCCESS) { + completableFuture.complete(bitmap); + } else { + completableFuture.complete(mUnitBitmap); + } + }; + + try { + view.post(() -> PixelCopy.request(activity.getWindow(), rect, bitmap, listener, new Handler())); + } catch (IllegalArgumentException e) { + Log.e(TAG, "Failed to capture screenshot because: ", e); + } + try { + return completableFuture.get(1000, TimeUnit.MILLISECONDS); + } catch (ExecutionException e) { + Log.e(TAG, "Failed to capture screenshot because: ", e); + return mUnitBitmap; + } catch (InterruptedException e) { + Log.e(TAG, "Failed to capture screenshot because: ", e); + return mUnitBitmap; + } catch (TimeoutException e) { + Log.e(TAG, "Failed to capture screenshot because: ", e); + return mUnitBitmap; + } + } + + @NonNull + private Bitmap captureImageLegacy(@NonNull View view) { + Bitmap screenshot = Bitmap.createBitmap(view.getMeasuredWidth(), view.getMeasuredHeight(), Bitmap.Config.ARGB_8888); + Canvas canvas = new Canvas(screenshot); + view.draw(canvas); + return screenshot; + } +} \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/devtools/views/IAPLView.java b/apl/src/main/java/com/amazon/apl/devtools/views/IAPLView.java new file mode 100644 index 00000000..00923cb4 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/devtools/views/IAPLView.java @@ -0,0 +1,87 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.views; + +import com.amazon.apl.android.LiveData; +import com.amazon.apl.devtools.enums.DTError; +import com.amazon.apl.devtools.models.view.ExecuteCommandStatus; +import com.amazon.apl.devtools.util.IDTCallback; +import com.amazon.apl.devtools.util.RequestStatus; +import org.json.JSONObject; + +import java.util.List; + +/** + * Interface which exposes various methods to provide the necessary functionality to the dev tools protocol. + */ +public interface IAPLView { + + /** + * Start recording frame metrics. + * + * @param id The request id. + * @param callback The callback to provide the correct response to the request. + */ + void startFrameMetricsRecording(int id, IDTCallback callback); + + /** + * Stops recording frame metrics. + * + * @param id The request id. + * @param callback The callback to provide the correct response to the request. + */ + void stopFrameMetricsRecording(int id, IDTCallback> callback); + + /** + * Handles the Document Domain Command Request from dev tools. + * + * @param method The type of dev tools document command. + * @param params The document command request's command parameters. + * @param callback The Document Domain response callback. + */ + void documentCommandRequest(int id, String method, JSONObject params, IDTCallback callback); + + /** + * Adds a runnable to be added to the message queue. + * + * @param runnable The {@link Runnable} to execute. + * @return true if the {@link Runnable} was successfully queued. False otherwise. + */ + boolean post(Runnable runnable); + + /** + * @param aplDocument The APL document that should be renderer. + * @param aplDocumentData The APL document's data. + */ + default void renderAPLDocument(String aplDocument, String aplDocumentData) { + // by default no op + } + + /** + * @param name The name of the LiveData Object + * @param liveData The LiveData to be added. + */ + default void addLiveData(String name, LiveData liveData) { + // by default no op + } + + /** + * @param name The name of the LiveData Object. + * @param operations The Specific operations to be executed. + * @param callback The callback to provide the correct response to the request. + */ + default void updateLiveData(String name, List operations, IDTCallback callback) { + callback.execute(RequestStatus.failed(0, DTError.METHOD_NOT_IMPLEMENTED)); + } + + /** + * @param commands The Command to be executed. + * @param callback The callback to provide the correct response to the request. + */ + default void executeCommands(String commands, IDTCallback callback) { + callback.execute(RequestStatus.failed(0, DTError.METHOD_NOT_IMPLEMENTED)); + } +} diff --git a/apl/src/main/java/com/amazon/apl/viewhost/DocumentHandle.java b/apl/src/main/java/com/amazon/apl/viewhost/DocumentHandle.java index a67f6a26..954cf17f 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/DocumentHandle.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/DocumentHandle.java @@ -101,6 +101,14 @@ public abstract class DocumentHandle extends UserDataHolder { */ public abstract boolean finish(FinishDocumentRequest request); + /** + * Return a setting from the main template. See https://developer.amazon.com/en-US/docs/alexa/alexa-presentation-language/apl-document.html#document_settings_property + * @param propertyName the property name + * @param defaultValue the fallback if not present + * @return the value if present otherwise the fallback. + */ + public abstract K getDocumentSetting(String propertyName, K defaultValue); + /** * Interface for returning the document's serialized visual context */ diff --git a/apl/src/main/java/com/amazon/apl/viewhost/Viewhost.java b/apl/src/main/java/com/amazon/apl/viewhost/Viewhost.java index e2702b8d..abcbd619 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/Viewhost.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/Viewhost.java @@ -4,13 +4,23 @@ */ package com.amazon.apl.viewhost; -import com.amazon.apl.android.RootConfig; +import androidx.annotation.NonNull; +import androidx.annotation.Nullable; + +import com.amazon.apl.android.APLLayout; +import com.amazon.apl.android.Action; +import com.amazon.apl.enums.DisplayState; import com.amazon.apl.viewhost.config.ViewhostConfig; +import com.amazon.apl.viewhost.internal.DocumentStateChangeListener; +import com.amazon.apl.viewhost.internal.SavedDocument; import com.amazon.apl.viewhost.internal.ViewhostImpl; import com.amazon.apl.viewhost.request.PrepareDocumentRequest; import com.amazon.apl.viewhost.request.RenderDocumentRequest; +import java.util.Map; + public abstract class Viewhost { + /** * Create an instance of Viewhost with a given configuration */ @@ -55,7 +65,77 @@ public static Viewhost create(ViewhostConfig config) { */ public abstract DocumentHandle render(PreparedDocument preparedDocument); - // TODO: Add APIs for view management (bind, unbind, isBound) - // TODO: Add APIs for platform events (updateDisplayState, cancelExecution, configurationChange) + /** + * Registers document state change listener. + * @param listener + */ + public abstract void registerStateChangeListener(DocumentStateChangeListener listener); + + /** + * Binds the Viewhost instance to the native view or throws IllegalStateException if a view is already bound. + * @param aplLayout native view + */ + public abstract void bind(APLLayout aplLayout); + + /** + * Unbinds the Viewhost instance. + */ + public abstract void unBind(); + + /** + * Returns true if a view is bound to the Viewhost instance, false otherwise. + * @return true/false + */ + public abstract boolean isBound(); + /** + * Updates the display state for this document. See https://developer.amazon.com/en-US/docs/alexa/alexa-presentation-language/apl-data-binding-evaluation.html#displaystate + * We will update the frame loop frequency according to the displayState. + * + * @param displayState the display state. + */ + public abstract void updateDisplayState(DisplayState displayState); + + /** + * Cancels the main sequencer. See https://developer.amazon.com/en-US/docs/alexa/alexa-presentation-language/apl-commands.html#command_sequencing + */ + public abstract void cancelExecution(); + + /** + * Restores the specified document (e.g. from the backstack). If a document is currently being rendered, it is + * replaced. + * + * @param document The document to restore. + * @return boolean Returns true when the request is valid and has been accepted to be restored. Returns false for an invalid request. + */ + public abstract boolean restoreDocument(SavedDocument document); + + /** + * Invoke an extension event handler. + * + * @param uri The URI of the custom document handler + * @param name The name of the handler to invoke + * @param data The data to associate with the handler + * @param fastMode If true, this handler will be invoked in fast mode + * @param callback the callback to receive an Action reference if the command doesn't resolve instantly. + */ + public abstract void invokeExtensionEventHandler(@NonNull String uri, @NonNull String name, + Map data, boolean fastMode, @Nullable ExtensionEventHandlerCallback callback); + + /** + * Callback for the Action associated with invoking an ExtensionEvent. + */ + public interface ExtensionEventHandlerCallback { + /** + * Called when the extension event was handled successfully + */ + void onComplete(); + + /** + * Called when the extension event handling was terminated before completion + */ + void onTerminated(); + } + + // TODO: Add API for platform events (configurationChange) // TODO: Add listeners for } \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/viewhost/config/DefaultEmbeddedDocumentFactory.java b/apl/src/main/java/com/amazon/apl/viewhost/config/DefaultEmbeddedDocumentFactory.java index 311d9be6..2811c618 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/config/DefaultEmbeddedDocumentFactory.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/config/DefaultEmbeddedDocumentFactory.java @@ -55,9 +55,14 @@ public void success(String response) { JSONObject message = new JSONObject(response); String document ,data = FIELD_EMPTY_FALLBACK_VALUE; if (message.has("name") && "RenderDocument".equals(message.getString("name"))) { - JSONObject payload = message.getJSONObject("payload"); - document = payload.optString(FIELD_DOCUMENT, FIELD_EMPTY_FALLBACK_VALUE); - data = payload.optString(FIELD_DATASOURCES, FIELD_EMPTY_FALLBACK_VALUE); + if (message.has("payload")) { + JSONObject payload = message.getJSONObject("payload"); + document = payload.optString(FIELD_DOCUMENT, FIELD_EMPTY_FALLBACK_VALUE); + data = payload.optString(FIELD_DATASOURCES, FIELD_EMPTY_FALLBACK_VALUE); + } else { + document = message.optString(FIELD_DOCUMENT, FIELD_EMPTY_FALLBACK_VALUE); + data = message.optString(FIELD_DATASOURCES, FIELD_EMPTY_FALLBACK_VALUE); + } } else { document = message.toString(); } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/config/DocumentOptions.java b/apl/src/main/java/com/amazon/apl/viewhost/config/DocumentOptions.java index 30d64dc5..cc6158c0 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/config/DocumentOptions.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/config/DocumentOptions.java @@ -7,11 +7,11 @@ import static com.amazon.apl.android.ExtensionMediator.IExtensionGrantRequestCallback; import com.amazon.alexaext.ExtensionRegistrar; -import com.amazon.apl.android.dependencies.IOpenUrlCallback; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.google.auto.value.AutoValue; import java.util.Map; -import java.util.Set; import javax.annotation.Nullable; @@ -33,6 +33,15 @@ public abstract class DocumentOptions { @Nullable public abstract Map getExtensionFlags(); + @Nullable + public abstract ITelemetryProvider getTelemetryProvider(); + + @Nullable + public abstract EmbeddedDocumentFactory getEmbeddedDocumentFactory(); + + @Nullable + public abstract IUserPerceivedFatalCallback getUserPerceivedFatalCallback(); + public static Builder builder() { return new AutoValue_DocumentOptions.Builder(); } @@ -42,6 +51,13 @@ public abstract static class Builder { public abstract Builder extensionGrantRequestCallback(IExtensionGrantRequestCallback callback); public abstract Builder extensionRegistrar(ExtensionRegistrar registrar); public abstract Builder extensionFlags(Map flags); + + public abstract Builder telemetryProvider(ITelemetryProvider telemetryProvider); + + public abstract Builder embeddedDocumentFactory(EmbeddedDocumentFactory embeddedDocumentFactory); + + public abstract Builder userPerceivedFatalCallback(IUserPerceivedFatalCallback userPerceivedFatalCallback); + public abstract DocumentOptions build(); } } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/config/ViewhostConfig.java b/apl/src/main/java/com/amazon/apl/viewhost/config/ViewhostConfig.java index bc46c383..54bf0049 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/config/ViewhostConfig.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/config/ViewhostConfig.java @@ -5,12 +5,16 @@ package com.amazon.apl.viewhost.config; import com.amazon.alexaext.ExtensionRegistrar; +import com.amazon.apl.android.audio.IAudioPlayerFactory; +import com.amazon.apl.android.media.RuntimeMediaPlayerFactory; +import com.amazon.apl.enums.RootProperty; import com.amazon.apl.viewhost.message.MessageHandler; import com.amazon.apl.android.dependencies.IContentRetriever; import com.amazon.apl.android.dependencies.IPackageLoader; import com.google.auto.value.AutoValue; import java.util.List; +import java.util.Map; import javax.annotation.Nullable; @@ -50,12 +54,6 @@ public abstract class ViewhostConfig { @Nullable public abstract ExtensionRegistrar getExtensionRegistrar(); - /** - * Defines how to resolve requests for embedded documents - */ - @Nullable - public abstract EmbeddedDocumentFactory getEmbeddedDocumentFactory(); - /** * Defines how to resolve packages */ @@ -68,6 +66,18 @@ public abstract class ViewhostConfig { @Nullable public abstract IContentRetriever getIContentRetriever(); + @Nullable + public abstract Map getRootProperties(); + + @Nullable + public abstract Map getEnvironmentProperties(); + + @Nullable + public abstract IAudioPlayerFactory getAudioPlayerFactory(); + + @Nullable + public abstract RuntimeMediaPlayerFactory getMediaPlayerFactory(); + public static Builder builder() { return new AutoValue_ViewhostConfig.Builder(); } @@ -78,9 +88,16 @@ public abstract static class Builder { public abstract Builder messageHandler(MessageHandler handler); public abstract Builder messageHandlers(List handler); public abstract Builder extensionRegistrar(ExtensionRegistrar registrar); - public abstract Builder embeddedDocumentFactory(EmbeddedDocumentFactory factory); public abstract Builder IPackageLoader(IPackageLoader packageLoader); public abstract Builder IContentRetriever(IContentRetriever contentRetriever); + + public abstract Builder audioPlayerFactory(IAudioPlayerFactory audioPlayerFactory); + + public abstract Builder mediaPlayerFactory(RuntimeMediaPlayerFactory mediaPlayerFactory); + + public abstract Builder rootProperties(Map map); + + public abstract Builder environmentProperties(Map map); public abstract ViewhostConfig build(); } } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/DTNetworkRequestManager.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/DTNetworkRequestManager.java new file mode 100644 index 00000000..44d8a475 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/DTNetworkRequestManager.java @@ -0,0 +1,136 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.viewhost.internal; + +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; +import com.amazon.apl.viewhost.request.PrepareDocumentRequest; +import java.util.ArrayList; +import java.util.List; + +/** + * This manager class is to correctly route the NetworkEvents to the DTNetworkRequestHandler + * because {@link ViewhostImpl#prepare(PrepareDocumentRequest)} does not guarantee that the APLLayout + * will be bind at that point before making the Network calls to import the packages. + */ +class DTNetworkRequestManager implements IDTNetworkRequestHandler { + private final List mNetworkRequests; + private IDTNetworkRequestHandler mDTNetworkRequestHandler; + private enum EventType { + REQUEST_WILL_BE_SENT, + LOADING_FINISHED, + LOADING_FAILED + } + + DTNetworkRequestManager() { + mNetworkRequests = new ArrayList<>(); + } + + + @Override + public void requestWillBeSent(int requestId, double timestamp, String url, DTNetworkRequestType type) { + if (mDTNetworkRequestHandler == null) { + NetworkRequestInfo networkRequestInfo = new NetworkRequestInfo(requestId, timestamp, EventType.REQUEST_WILL_BE_SENT).setUrl(url); + mNetworkRequests.add(networkRequestInfo); + return; + } + + mDTNetworkRequestHandler.requestWillBeSent(requestId, timestamp, url, type); + } + + @Override + public void loadingFailed(int requestId, double timestamp) { + if (mDTNetworkRequestHandler == null) { + mNetworkRequests.add(new NetworkRequestInfo(requestId, timestamp, EventType.LOADING_FAILED)); + return; + } + + mDTNetworkRequestHandler.loadingFailed(requestId, timestamp); + } + + @Override + public void loadingFinished(int requestId, double timestamp, int encodedDataLength) { + if (mDTNetworkRequestHandler == null) { + mNetworkRequests.add(new NetworkRequestInfo(requestId, timestamp, EventType.LOADING_FINISHED) + .setEncodedDataLength(encodedDataLength)); + return; + } + + mDTNetworkRequestHandler.loadingFinished(requestId, timestamp, encodedDataLength); + } + + public void bindDTNetworkRequest(IDTNetworkRequestHandler dtNetworkRequestHandler) { + mDTNetworkRequestHandler = dtNetworkRequestHandler; + reportAllNetworkEvents(); + } + + public void unbindDTNetworkRequest() { + mDTNetworkRequestHandler = null; + } + + private void reportAllNetworkEvents() { + for (NetworkRequestInfo networkRequestInfo : mNetworkRequests) { + int requestId = networkRequestInfo.getRequestId(); + double timestamp = networkRequestInfo.getTimestamp(); + String url = networkRequestInfo.getUrl(); + EventType eventType = networkRequestInfo.getEventType(); + + if (EventType.REQUEST_WILL_BE_SENT.equals(eventType)) { + mDTNetworkRequestHandler.requestWillBeSent(requestId, timestamp, url, DTNetworkRequestType.PACKAGE); + } else if (EventType.LOADING_FINISHED.equals(eventType)) { + mDTNetworkRequestHandler.loadingFinished(requestId, timestamp, networkRequestInfo.getEncodedDataLength()); + } else { + mDTNetworkRequestHandler.loadingFailed(requestId, timestamp); + } + } + mNetworkRequests.clear(); + } + + private static class NetworkRequestInfo { + private final int mRequestId; + private final double mTimestamp; + private final EventType mEventType; + private String mUrl; + private int mEncodedDataLength; + + NetworkRequestInfo(int requestId, double timestamp, EventType eventType) { + mRequestId = requestId; + mTimestamp = timestamp; + mEventType = eventType; + } + + public int getRequestId() { + return mRequestId; + } + + public double getTimestamp() { + return mTimestamp; + } + + public String getUrl() { + return mUrl; + } + + public EventType getEventType() { + return mEventType; + } + + public int getEncodedDataLength() { + return mEncodedDataLength; + } + + + public NetworkRequestInfo setUrl(String url) { + mUrl = url; + return this; + } + + public NetworkRequestInfo setEncodedDataLength(int encodedDataLength) { + mEncodedDataLength = encodedDataLength; + return this; + } + } +} diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/DataSourceError.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/DataSourceError.java deleted file mode 100644 index dc3ce782..00000000 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/DataSourceError.java +++ /dev/null @@ -1,20 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.viewhost.internal; - -import lombok.AllArgsConstructor; -import lombok.Getter; - -/** - * This class is a peer for DocumentError stuct in core. It defines errors corresponding to a document. - */ -@AllArgsConstructor -@Getter -public class DataSourceError { - private long documentContextId; - private Object error; - private boolean isTopDocument; -} diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentHandleImpl.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentHandleImpl.java index 66504799..e712f38d 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentHandleImpl.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentHandleImpl.java @@ -5,15 +5,26 @@ package com.amazon.apl.viewhost.internal; import android.os.Handler; +import android.os.SystemClock; +import android.text.TextUtils; import android.util.Log; +import androidx.annotation.NonNull; import androidx.annotation.Nullable; import com.amazon.apl.android.Action; import com.amazon.apl.android.Content; import com.amazon.apl.android.ExtensionMediator; +import com.amazon.apl.android.RootConfig; +import com.amazon.apl.android.RootContext; +import com.amazon.apl.android.Session; +import com.amazon.apl.android.UserPerceivedFatalReporter; +import com.amazon.apl.android.dependencies.impl.NoOpUserPerceivedFatalCallback; +import com.amazon.apl.android.providers.ITelemetryProvider; +import com.amazon.apl.android.providers.impl.NoOpTelemetryProvider; import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.config.DocumentOptions; +import com.amazon.apl.viewhost.config.ViewhostConfig; import com.amazon.apl.viewhost.primitives.Decodable; import com.amazon.apl.viewhost.primitives.JsonDecodable; import com.amazon.apl.viewhost.primitives.JsonStringDecodable; @@ -29,45 +40,90 @@ import java.lang.ref.WeakReference; import java.util.Collection; +import java.util.HashSet; import java.util.LinkedList; import java.util.Queue; import java.util.Set; +import java.util.UUID; +import java.util.concurrent.TimeUnit; /** * Internal implementation of the document handle */ -class DocumentHandleImpl extends DocumentHandle { +public class DocumentHandleImpl extends DocumentHandle { private static final String TAG = "DocumentHandleImpl"; - private Queue mExecuteCommandsRequestQueue= new LinkedList<>(); + private Queue mExecuteCommandsRequestQueue = new LinkedList<>(); private Handler mCoreWorker; - private Collection mDocumentStateChangeListeners; + private Set mDocumentStateChangeListeners; private Content mContent; private DocumentState mDocumentState; private ExtensionMediator mExtensionMediator; private DocumentOptions mDocumentOptions; + private ITelemetryProvider mTelemetryProvider = NoOpTelemetryProvider.getInstance(); + private String mToken; + /** * Retain a link to core's DocumentContext. It can be null while the document is being prepared * and the core document context hasn't been created yet. */ @Nullable private DocumentContext mDocumentContext; + + /** + * Root Context only for primary document. Null for embedded docs. + */ + @Nullable + private RootContext mRootContext; @Nullable private DocumentConfig mDocumentConfig; + @Nullable + private RootConfig mRootConfig; + @Nullable + private UserPerceivedFatalReporter mUserPerceivedFatalReporter; /** * Retain only a weak reference to the viewhost, since that is the parent object that creates * document handles. We want avoid circular references. */ private final WeakReference mViewhost; + private final long mDocumentCreationTime; + + private static final String RENDER_DOCUMENT_TAG = "Viewhost." + ITelemetryProvider.RENDER_DOCUMENT; + private Integer mRenderDocumentTimer = ITelemetryProvider.UNKNOWN_METRIC_ID; + private final String mUniqueID; + + private final Session mSession; DocumentHandleImpl(ViewhostImpl viewhost, Handler coreWorker) { mViewhost = new WeakReference(viewhost); mCoreWorker = coreWorker; mDocumentState = DocumentState.PENDING; - mDocumentStateChangeListeners = new LinkedList<>(); + mDocumentStateChangeListeners = new HashSet<>(); mContent = null; mExtensionMediator = null; mDocumentOptions = null; + mDocumentCreationTime = SystemClock.elapsedRealtime(); + mUniqueID = UUID.randomUUID().toString(); + mUserPerceivedFatalReporter = new UserPerceivedFatalReporter(new NoOpUserPerceivedFatalCallback()); + mSession = new Session(); + Log.d(TAG, "Document UniqueID is: " + mUniqueID); + } + + public Handler getCoreWorker() { + return mCoreWorker; + } + + public Session getSession() { + return mSession; + } + + /** + * Start time for prepare document call. + * + * @return + */ + public long getPrepareDocumentStartTime() { + return mDocumentCreationTime; } @Override @@ -77,12 +133,16 @@ public boolean isValid() { @Override public String getToken() { - return "token"; + return mToken; + } + + public void setToken(String token) { + mToken = token; } @Override public String getUniqueId() { - return "1234-ABCD"; + return mUniqueID; } @Override @@ -148,12 +208,12 @@ public boolean requestDataSourceContext(DataSourceContextCallback callback) { } @Nullable - public Content getContent(){ + public Content getContent() { return mContent; } @Nullable - public ExtensionMediator getExtensionMediator(){ + public ExtensionMediator getExtensionMediator() { return mExtensionMediator; } @@ -170,12 +230,19 @@ public void setDocumentOptions(DocumentOptions documentOptions) { mDocumentOptions = documentOptions; } + public void setUserPerceivedFatalReporter(UserPerceivedFatalReporter userPerceivedFatalReporter) { + mUserPerceivedFatalReporter = userPerceivedFatalReporter; + } + public UserPerceivedFatalReporter getUserPerceivedFatalReporter() { + return mUserPerceivedFatalReporter; + } + @Override public boolean executeCommands(ExecuteCommandsRequest request) { if (!isValid()) { return false; } - String commands = ((JsonStringDecodable)request.getCommands()).getString(); + String commands = ((JsonStringDecodable) request.getCommands()).getString(); Log.d(TAG, String.format("Running commands %s for host component", commands)); if (mViewhost.get() == null) { @@ -222,8 +289,8 @@ public boolean updateDataSource(UpdateDataSourceRequest request) { return false; } - if (getDocumentConfig() == null || getDocumentConfig().getNativeHandle() == 0) { - Log.e(TAG, "DocumentConfig handle 0 which means provider not defined in document options, hence ignoring the update data source request"); + if ((mRootContext == null ) && (getDocumentConfig() == null || getDocumentConfig().getNativeHandle() == 0)) { + Log.e(TAG, "RootContext null or DocumentConfig handle 0 which means provider not defined in document options, hence ignoring the update data source request"); return false; } @@ -231,9 +298,10 @@ public boolean updateDataSource(UpdateDataSourceRequest request) { UpdateDataSourceCallback callback = request.getCallback(); mCoreWorker.post(() -> { try { - String payload = ((JsonStringDecodable)request.getData()).getString(); + String payload = ((JsonStringDecodable) request.getData()).getString(); JSONObject jsonObject = new JSONObject(payload); - if (!jsonObject.has("type")) { + String type = jsonObject.has("type") ? jsonObject.getString("type") : request.getType(); + if (TextUtils.isEmpty(type)) { Log.e(TAG, "Data Source type not defined, hence update failed"); if (callback != null) { viewhost.publish(() -> { @@ -241,8 +309,7 @@ public boolean updateDataSource(UpdateDataSourceRequest request) { }); } } else { - String type = jsonObject.getString("type"); - boolean updated = nUpdateDataSource(type, payload, getDocumentConfig().getNativeHandle()); + boolean updated = mRootContext != null ? mRootContext.updateDataSource(type, payload) : nUpdateDataSource(type, payload, getDocumentConfig().getNativeHandle()); if (callback != null) { if (updated) { viewhost.publish(() -> { @@ -276,10 +343,11 @@ public synchronized boolean setUserData(Object data) { } public void setDocumentState(DocumentState state) { + Log.d(TAG, String.format("setDocumentState for handle: %s and new state: %s", this, state.toString())); mDocumentState = state; // update all the listeners with the new state for (DocumentStateChangeListener listener : mDocumentStateChangeListeners) { - listener.onDocumentStateChanged(state); + listener.onDocumentStateChanged(state, this); } final ViewhostImpl viewhost = mViewhost.get(); if (viewhost != null) { @@ -288,9 +356,11 @@ public void setDocumentState(DocumentState state) { } public void registerStateChangeListener(DocumentStateChangeListener listener) { - mDocumentStateChangeListeners.add(listener); - // the listener has no state set it to the state of the handler - listener.onDocumentStateChanged(mDocumentState); + if (!mDocumentStateChangeListeners.contains(listener)) { + mDocumentStateChangeListeners.add(listener); + // the listener has no state set it to the state of the handler + listener.onDocumentStateChanged(mDocumentState, this); + } } public void setContent(Content content) { @@ -337,6 +407,26 @@ public void setDocumentContext(DocumentContext documentContext) { pollRequests(); } + /** + * Will be used to set rootContext if a render document request is fulfilled via unified APIs. + * + * @param rootContext + */ + public void setRootContext(RootContext rootContext) { + mRootContext = rootContext; + mRootContext.setDocumentHandle(this); + DocumentContext documentContext = rootContext.getDocumentContext(); + setDocumentContext(documentContext); + } + + /** + * Set the telemetry provider for this instance + * @param telemetryProvider + */ + public void setTelemetryProvider(@NonNull ITelemetryProvider telemetryProvider) { + mTelemetryProvider = telemetryProvider; + } + /** * poll any pending execute command requests. */ @@ -355,15 +445,115 @@ public void setDocumentConfig(@Nullable DocumentConfig documentConfig) { this.mDocumentConfig = documentConfig; } + private boolean isTokenValid(String token) { + // If not specified, then we will not attempt to match the token + if (token == null) { + return true; + } + + // If specified, then the specified token must match + return token.equals(mToken); + } @Override public boolean finish(FinishDocumentRequest request) { - return false; + if (!isValid()) { + Log.w(TAG, "Could not finish, document is no longer valid."); + return false; + } + if (!isTokenValid(request.getToken())) { + Log.w(TAG, "Ignoring finish request due to mismatched tokens."); + return false; + } + mExecuteCommandsRequestQueue.clear(); + mCoreWorker.post(() -> { + if (mRootContext != null) { + try { + mRootContext.finishDocument(); + } catch (Exception e) { + Log.e(TAG, "Exception while fulfilling request and the exception is: " + e); + setDocumentState(DocumentState.ERROR); + } + } else { + //when the DocumentState is pending or prepared, but not yet rendered + setDocumentState(DocumentState.FINISHED); + } + if (mExtensionMediator != null) { + mExtensionMediator.enable(false); + } + }); + return true; + } + + void startRenderDocumentTimer(TimeUnit timeUnit, long initialElapsedTime) { + if (mRenderDocumentTimer != ITelemetryProvider.UNKNOWN_METRIC_ID) { + Log.w(TAG, "Attempting to start renderDocument but it is already running"); + return; + } + + mRenderDocumentTimer = mTelemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, RENDER_DOCUMENT_TAG, ITelemetryProvider.Type.TIMER); + mTelemetryProvider.startTimer(mRenderDocumentTimer, timeUnit, initialElapsedTime); + } + + void failRenderDocumentTimer() { + if (mRenderDocumentTimer == ITelemetryProvider.UNKNOWN_METRIC_ID) { + Log.w(TAG, "Attempting to fail renderDocument before starting"); + return; + } + + mTelemetryProvider.fail(mRenderDocumentTimer); + mRenderDocumentTimer = ITelemetryProvider.UNKNOWN_METRIC_ID; + } + + void stopRenderDocumentTimer() { + if (mRenderDocumentTimer == ITelemetryProvider.UNKNOWN_METRIC_ID) { + Log.w(TAG, "Attempting to stop renderDocument before starting"); + return; + } + + mTelemetryProvider.stopTimer(mRenderDocumentTimer); + mRenderDocumentTimer = ITelemetryProvider.UNKNOWN_METRIC_ID; + } + + @Override + public K getDocumentSetting(String propertyName, K defaultValue) { + if (mContent != null) { + return mContent.optSetting(propertyName, defaultValue); + } else { + Log.w(TAG, "Content is null, returning defaultValue"); + return defaultValue; + } + } + + @Override + public String toString() { + return "DocumentHandle{" + + "id=" + getUniqueId() + ", " + + "state=" + mDocumentState.toString() + ", " + + "token=" + getToken() + + "}"; + } + + public DocumentState getDocumentState() { + return mDocumentState; } public DocumentContext getDocumentContext() { return mDocumentContext; } + public RootContext getRootContext() { + return mRootContext; + } + + @Nullable + public RootConfig getRootConfig() { + return mRootConfig; + } + + public void setRootConfig(@Nullable RootConfig rootConfig) { + mRootConfig = rootConfig; + } + private static native boolean nUpdateDataSource(String type, String payload, long documentConfigNativeHandle); } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentManager.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentManager.java index bb42cd82..0334e46a 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentManager.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentManager.java @@ -10,6 +10,7 @@ import androidx.annotation.Keep; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.viewhost.config.EmbeddedDocumentFactory; import com.amazon.common.BoundObject; @@ -21,16 +22,24 @@ public class DocumentManager extends BoundObject { private static final String TAG = "DocumentManager"; private final EmbeddedDocumentFactory mEmbeddedDocumentFactory; private final Handler mHandler; + private final ITelemetryProvider mTelemetryProvider; - public DocumentManager(final EmbeddedDocumentFactory embeddedDocumentFactory, final Handler handler) { + private static final String EMBEDDED_DOCUMENT_COUNT = "embeddedDocumentCount"; + + private final int mEmbeddedDocumentCountMetric; + + public DocumentManager(final EmbeddedDocumentFactory embeddedDocumentFactory, final Handler handler, final ITelemetryProvider telemetryProvider) { mEmbeddedDocumentFactory = embeddedDocumentFactory; mHandler = handler; + mTelemetryProvider = telemetryProvider; + mEmbeddedDocumentCountMetric = mTelemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, EMBEDDED_DOCUMENT_COUNT, ITelemetryProvider.Type.COUNTER); long handle = nCreate(); bind(handle); } public void requestEmbeddedDocument(final EmbeddedDocumentRequestProxy embeddedDocumentRequestProxy) { - EmbeddedDocumentRequestImpl request = new EmbeddedDocumentRequestImpl(embeddedDocumentRequestProxy, mHandler); + mTelemetryProvider.incrementCount(mEmbeddedDocumentCountMetric); + EmbeddedDocumentRequestImpl request = new EmbeddedDocumentRequestImpl(embeddedDocumentRequestProxy, mHandler, mTelemetryProvider); String sourceUrl = embeddedDocumentRequestProxy.getRequestUrl(); if(TextUtils.isEmpty(sourceUrl)) { Log.e(TAG, "requestEmbeddedDocument: sourceUrl is null"); diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentState.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentState.java index 0c56e5a0..c38e055b 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentState.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentState.java @@ -4,12 +4,21 @@ */ package com.amazon.apl.viewhost.internal; -//https://aplspec.aka.corp.amazon.com/apl-view-host-1.0/latest/html/main.html#document-message-documentstatechanged a -enum DocumentState { +/** + * Enums to define the document state. + */ +public enum DocumentState { + //The first step in document creation when the document preparation is in progress. PENDING, + //The pre-inflation dependencies have been satisfied (imported packages, extensions loaded). PREPARED, + //The APL engine has inflated all of the components needed for the first frame. INFLATED, + //The APL engine has prepared a visual representations of components needed for the first frame and has handed off + // instructions to the platform for rendering. This corresponds to when the VUPL clock is stopped. DISPLAYED, + //All resources associated with a document have been released and no further interaction is possible. FINISHED, + //The document has resulted in a permanent failure. ERROR } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentStateChangeListener.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentStateChangeListener.java index d2bb2812..ae18d848 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentStateChangeListener.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/DocumentStateChangeListener.java @@ -4,6 +4,11 @@ */ package com.amazon.apl.viewhost.internal; -interface DocumentStateChangeListener { - void onDocumentStateChanged(DocumentState state); +import com.amazon.apl.viewhost.DocumentHandle; + +/** + * Listens to the document state change events. + */ +public interface DocumentStateChangeListener { + void onDocumentStateChanged(DocumentState state, DocumentHandle handle); } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestImpl.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestImpl.java index c19c7761..3e5b65c0 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestImpl.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestImpl.java @@ -5,12 +5,14 @@ package com.amazon.apl.viewhost.internal; import android.os.Handler; +import android.os.SystemClock; import android.util.Log; import androidx.annotation.VisibleForTesting; import com.amazon.apl.android.Content; import com.amazon.apl.android.ExtensionMediator; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.PreparedDocument; import com.amazon.apl.viewhost.config.DocumentOptions; @@ -19,6 +21,7 @@ import java.util.HashMap; import java.util.Map; +import java.util.concurrent.TimeUnit; public class EmbeddedDocumentRequestImpl implements EmbeddedDocumentFactory.EmbeddedDocumentRequest, DocumentStateChangeListener{ private static final String TAG = "EmbeddedDocumentRequestImpl"; @@ -28,15 +31,23 @@ public class EmbeddedDocumentRequestImpl implements EmbeddedDocumentFactory.Embe private final EmbeddedDocumentRequestProxy mEmbeddedDocumentRequestProxy; private final Handler mHandler; private DocumentHandleImpl mDocumentHandle; + private ITelemetryProvider mTelemetryProvider; + private static final String PREPARE_EMBEDDED_DOC_COUNT = "prepareEmbeddedDocCount"; + private static final String PREPARE_EMBEDDED_DOC_TIME = "prepareEmbeddedDocTime"; + private final int mPrepareEmbeddedDocCount; + private final int mPrepareEmbeddedDocTime; EmbeddedDocumentRequestImpl(EmbeddedDocumentRequestProxy embeddedDocumentRequestProxy, - Handler handler) { + Handler handler, ITelemetryProvider telemetryProvider) { mSource = EMPTY; mEmbeddedDocumentRequestProxy = embeddedDocumentRequestProxy; + mTelemetryProvider = telemetryProvider; mHandler = handler; mDocumentHandle = null; // We assume that documents are separate unless we're told otherwise mIsVisualContextConnected = false; + mPrepareEmbeddedDocCount = mTelemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, PREPARE_EMBEDDED_DOC_COUNT, ITelemetryProvider.Type.COUNTER); + mPrepareEmbeddedDocTime = mTelemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, PREPARE_EMBEDDED_DOC_TIME, ITelemetryProvider.Type.TIMER); } public void setSource(String source) { mSource = source; @@ -98,6 +109,10 @@ public void fail(String reason) { * @param documentConfigHandle */ private void handleEmbeddedDocumentRequestSuccess(Content content, long documentConfigHandle) { + long endTime = SystemClock.elapsedRealtime(); + long documentPreparationTime = endTime - mDocumentHandle.getPrepareDocumentStartTime(); + mTelemetryProvider.reportTimer(mPrepareEmbeddedDocTime, TimeUnit.MILLISECONDS, documentPreparationTime); + DocumentContext documentContext = mEmbeddedDocumentRequestProxy.success(content.getNativeHandle(), mIsVisualContextConnected, documentConfigHandle); @@ -110,6 +125,7 @@ private void handleEmbeddedDocumentRequestSuccess(Content content, long document } mDocumentHandle.setDocumentContext(documentContext); + mTelemetryProvider.incrementCount(mPrepareEmbeddedDocCount); } @@ -151,7 +167,7 @@ public Runnable onFailure() { } @Override - public void onDocumentStateChanged(DocumentState state) { + public void onDocumentStateChanged(DocumentState state, DocumentHandle handle) { // when the document state is prepared then content is done so call success if (state == DocumentState.PREPARED) { mHandler.post(() -> { @@ -174,7 +190,6 @@ public void onDocumentStateChanged(DocumentState state) { public void onComplete(Content content) { onContentRefreshComplete(content, mDocumentHandle.getDocumentOptions(), mediator, documentConfigHandle); } - @Override public void onError(Exception e) { Log.e(TAG, "Error occurred during content refresh: " + e.getMessage()); @@ -183,12 +198,14 @@ public void onError(Exception e) { } else { onContentRefreshComplete(content, mDocumentHandle.getDocumentOptions(), mediator, documentConfigHandle); } + } else { mEmbeddedDocumentRequestProxy.failure("DocumentHandle is null"); } }); } else if (state == DocumentState.ERROR) { mHandler.post(() -> mEmbeddedDocumentRequestProxy.failure("Content creation error, Document State set to Error")); + mTelemetryProvider.fail(mPrepareEmbeddedDocCount); } } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/InteractionTimerListener.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/InteractionTimerListener.java new file mode 100644 index 00000000..10306de5 --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/InteractionTimerListener.java @@ -0,0 +1,13 @@ +package com.amazon.apl.viewhost.internal; + +import com.amazon.apl.viewhost.DocumentHandle; + +/** + * Listener for any events related to pause or set timeout on the document. + */ +public interface InteractionTimerListener { + + void onPause(DocumentHandle handle); + + void onSetTimeout(DocumentHandle handle, int timeoutMS); +} diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/PreparedDocumentImpl.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/PreparedDocumentImpl.java index 6d5dea4c..ce4b2586 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/PreparedDocumentImpl.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/PreparedDocumentImpl.java @@ -7,10 +7,13 @@ import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.PreparedDocument; +import lombok.NonNull; + /** * Internal implementation of the prepared document */ class PreparedDocumentImpl extends PreparedDocument { + @NonNull private final DocumentHandleImpl mDocument; public PreparedDocumentImpl(DocumentHandleImpl document) { @@ -19,27 +22,27 @@ public PreparedDocumentImpl(DocumentHandleImpl document) { @Override public boolean isReady() { - return false; + return DocumentState.PREPARED.equals(mDocument.getDocumentState()); } @Override public boolean isValid() { - return false; + return getHandle().isValid(); } @Override public boolean hasToken() { - return false; + return getToken() != null; } @Override public String getToken() { - return null; + return getHandle().getToken(); } @Override public String getUniqueID() { - return null; + return getHandle().getUniqueId(); } @Override diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/SavedDocument.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/SavedDocument.java new file mode 100644 index 00000000..e167223b --- /dev/null +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/SavedDocument.java @@ -0,0 +1,32 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.viewhost.internal; + +import androidx.annotation.NonNull; + +import com.amazon.apl.enums.DisplayState; +import com.amazon.apl.viewhost.DocumentHandle; +import com.google.auto.value.AutoValue; + +/** + * Represents the state of a document when added to the backstack. + * + * Only exposed as an opaque type to clients. + */ +@AutoValue +public abstract class SavedDocument { + @NonNull + public abstract DocumentHandle getDocumentHandle(); + + public static Builder builder() { + return new AutoValue_SavedDocument.Builder(); + } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder documentHandle(DocumentHandle handle); + public abstract SavedDocument build(); + } +} diff --git a/apl/src/main/java/com/amazon/apl/viewhost/internal/ViewhostImpl.java b/apl/src/main/java/com/amazon/apl/viewhost/internal/ViewhostImpl.java index 86f33c6e..a78d1e2a 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/internal/ViewhostImpl.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/internal/ViewhostImpl.java @@ -6,24 +6,37 @@ import android.os.Handler; import android.os.Looper; +import android.os.SystemClock; import android.util.Log; import com.amazon.alexaext.ExtensionRegistrar; +import com.amazon.apl.android.APLLayout; import com.amazon.apl.android.APLOptions; +import com.amazon.apl.android.Action; import com.amazon.apl.android.Content; import com.amazon.apl.android.DocumentSession; import com.amazon.apl.android.Event; import com.amazon.apl.android.ExtensionMediator; import com.amazon.apl.android.ExtensionMediator.ILoadExtensionCallback; +import com.amazon.apl.android.IAPLViewPresenter; +import com.amazon.apl.android.RootConfig; +import com.amazon.apl.android.RootContext; import com.amazon.apl.android.Session; +import com.amazon.apl.android.UserPerceivedFatalReporter; import com.amazon.apl.android.dependencies.IDataSourceFetchCallback; import com.amazon.apl.android.dependencies.IPackageLoader; import com.amazon.apl.android.dependencies.ISendEventCallbackV2; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.dependencies.impl.NoOpUserPerceivedFatalCallback; import com.amazon.apl.android.events.DataSourceFetchEvent; import com.amazon.apl.android.events.OpenURLEvent; import com.amazon.apl.android.events.RefreshEvent; import com.amazon.apl.android.events.SendEvent; +import com.amazon.apl.android.providers.ITelemetryProvider; +import com.amazon.apl.android.providers.impl.NoOpTelemetryProvider; import com.amazon.apl.android.thread.Threading; +import com.amazon.apl.enums.DisplayState; +import com.amazon.apl.enums.RootProperty; import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.PreparedDocument; import com.amazon.apl.viewhost.Viewhost; @@ -40,6 +53,7 @@ import com.amazon.apl.viewhost.primitives.Decodable; import com.amazon.apl.viewhost.primitives.JsonDecodable; import com.amazon.apl.viewhost.primitives.JsonStringDecodable; +import com.amazon.apl.viewhost.request.FinishDocumentRequest; import com.amazon.apl.viewhost.request.PrepareDocumentRequest; import com.amazon.apl.viewhost.request.RenderDocumentRequest; @@ -47,24 +61,46 @@ import org.json.JSONObject; import java.lang.ref.WeakReference; +import java.util.Arrays; +import java.util.Collections; import java.util.HashMap; +import java.util.HashSet; import java.util.Iterator; +import java.util.Locale; import java.util.Map; +import java.util.Set; import java.util.concurrent.Executor; +import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; +import javax.annotation.Nullable; + import lombok.NonNull; /** * Internal implementation of the viewhost */ -public class ViewhostImpl extends Viewhost { +public class ViewhostImpl extends Viewhost implements DocumentStateChangeListener { private static final String TAG = "ViewhostImpl"; + private static final String FETCH_DATA_REQUEST = "FetchDataRequest"; + private static final String SEND_USER_EVENT_REQUEST = "SendUserEventRequest"; + private static final String OPEN_URL_REQUEST = "OpenURLRequest"; private final ViewhostConfig mConfig; + private final Executor mRuntimeInteractionWorker; private final Handler mCoreWorker; private final AtomicInteger mNextMessageId; private static final String EMPTY = ""; + private final DTNetworkRequestManager mDTNetworkRequestManager; + + private APLLayout mAplLayout; + + private Set mDocumentStateChangeListeners; + + private ITelemetryProvider mTelemetryProvider = NoOpTelemetryProvider.getInstance(); + + private static final String RENDER_DOCUMENT_COUNT_TAG = "Viewhost." + ITelemetryProvider.RENDER_DOCUMENT + "Count"; + private Integer mRenderDocumentCount; /** * Maintain a map of documents that are known to the new viewhost. This is needed for event @@ -72,12 +108,25 @@ public class ViewhostImpl extends Viewhost { */ private final Map> mDocumentMap; + @Nullable + private RootContext mRootContext; + + @Nullable + private DocumentHandle mTopDocument; + public ViewhostImpl(ViewhostConfig config, Executor runtimeInteractionWorker, Handler coreWorker) { mConfig = config; + if (mConfig.getDefaultDocumentOptions() != null && mConfig.getDefaultDocumentOptions().getTelemetryProvider() != null) { + mTelemetryProvider = mConfig.getDefaultDocumentOptions().getTelemetryProvider(); + } mDocumentMap = new HashMap<>(); mRuntimeInteractionWorker = runtimeInteractionWorker; mCoreWorker = coreWorker; mNextMessageId = new AtomicInteger(1); + mDocumentStateChangeListeners = new HashSet<>(); + mDTNetworkRequestManager = new DTNetworkRequestManager(); + + mRenderDocumentCount = mTelemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, RENDER_DOCUMENT_COUNT_TAG, ITelemetryProvider.Type.COUNTER); } public ViewhostImpl(ViewhostConfig config) { @@ -86,17 +135,37 @@ public ViewhostImpl(ViewhostConfig config) { @Override public PreparedDocument prepare(final PrepareDocumentRequest request) { + Log.d(TAG, "Prepare called with token: " + request.getToken()); // For first iteration of M1 milestone we are keeping the content creation in this block. May move it later String document = request.getDocument() != null ? ((JsonStringDecodable) request.getDocument()).getString() : EMPTY; APLOptions options = createAPLOptions(request); DocumentHandleImpl handle = new DocumentHandleImpl(this, mCoreWorker); + handle.setToken(request.getToken()); + DocumentOptions documentOptions = request.getDocumentOptions() != null ? request.getDocumentOptions() : mConfig.getDefaultDocumentOptions(); + if (documentOptions != null && documentOptions.getUserPerceivedFatalCallback() != null) { + handle.setUserPerceivedFatalReporter(new UserPerceivedFatalReporter(documentOptions.getUserPerceivedFatalCallback())); + } + DocumentSession session = request.getDocumentSession(); + session.setCoreWorker(mCoreWorker); + + // Set the telemetry provider for the DocumentHandle in the following order of preference: + // 1. DocumentOptions passed through PreparedDocumentRequest + // 2. DocumentOptions passed to the Viewhost config + if (request.getDocumentOptions() != null && request.getDocumentOptions().getTelemetryProvider() != null) { + handle.setTelemetryProvider(request.getDocumentOptions().getTelemetryProvider()); + } else { + DocumentOptions defaultOptions = (mConfig != null) ? mConfig.getDefaultDocumentOptions() : null; + + if (defaultOptions != null && defaultOptions.getTelemetryProvider() != null) { + handle.setTelemetryProvider(defaultOptions.getTelemetryProvider()); + } + } + Content.create(document, options, new Content.CallbackV2() { @Override public void onComplete(Content content) { - DocumentOptions documentOptions = request.getDocumentOptions() != null ? request.getDocumentOptions() : mConfig.getDefaultDocumentOptions(); - if (documentOptions != null) { handle.setDocumentOptions(documentOptions); } @@ -106,7 +175,7 @@ public void onComplete(Content content) { // 2. Viewhost Config final ExtensionRegistrar registrar = (documentOptions != null) ? documentOptions.getExtensionRegistrar() : mConfig.getExtensionRegistrar(); - final ExtensionMediator mediator = (registrar != null) ? ExtensionMediator.create(registrar, DocumentSession.create()) : null; + final ExtensionMediator mediator = (registrar != null) ? ExtensionMediator.create(registrar, session) : null; if (mediator != null) { handle.setExtensionMediator(mediator); } @@ -116,19 +185,30 @@ public void onComplete(Content content) { } else { handle.setContent(content); } + + session.bind(handle); + session.onSessionEnded(sessionEndedCallback -> { + mCoreWorker.post(() -> { + if (mediator != null) { + mediator.onSessionEnded(); + } + }); + }); } @Override public void onError(Exception e) { Log.i(TAG, e.toString()); + handle.getUserPerceivedFatalReporter().reportFatal(UserPerceivedFatalReporter.UpfReason.CONTENT_CREATION_FAILURE); handle.setDocumentState(DocumentState.ERROR); + writeAPLSessionLog(handle.getSession(), Session.LogEntryLevel.ERROR, "Document Failed: " + e.getMessage()); } @Override public void onPackageLoaded(Content content) { // do nothing when packages are loaded } - }, new Session()); + }, handle.getSession(), mDTNetworkRequestManager); return new PreparedDocumentImpl(handle); } @@ -146,12 +226,15 @@ private void loadExtensionAndSetContent(@NonNull ExtensionMediator mediator, @No Map flags = documentOptions.getExtensionFlags() != null ? documentOptions.getExtensionFlags() : new HashMap<>(); mediator.initializeExtensions(flags, content, documentOptions.getExtensionGrantRequestCallback()); + final int extensionRegistrationMetric = mTelemetryProvider.createMetricId(ITelemetryProvider.APL_DOMAIN, ITelemetryProvider.METRIC_TIMER_EXTENSION_REGISTRATION, ITelemetryProvider.Type.TIMER); + mTelemetryProvider.startTimer(extensionRegistrationMetric); mediator.loadExtensions( flags, content, new ILoadExtensionCallback() { @Override public Runnable onSuccess() { + mTelemetryProvider.stopTimer(extensionRegistrationMetric); if (isRefresh) { return () -> Log.i(TAG, "Content refresh successful"); } @@ -160,12 +243,23 @@ public Runnable onSuccess() { @Override public Runnable onFailure() { + mTelemetryProvider.fail(extensionRegistrationMetric); + handle.getUserPerceivedFatalReporter().reportFatal(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE); return () -> handle.setDocumentState(DocumentState.ERROR); } } ); } + private IUserPerceivedFatalCallback getUserPerceivedFatalCallback(final PrepareDocumentRequest request) { + final DocumentOptions documentOptions = request.getDocumentOptions(); + if (documentOptions != null && documentOptions.getUserPerceivedFatalCallback() != null) { + return documentOptions.getUserPerceivedFatalCallback(); + } + Log.d(TAG, "No UserPerceivedFatalCallback provided by the runtime to report UPF."); + return new NoOpUserPerceivedFatalCallback(); + } + private APLOptions createAPLOptions(final PrepareDocumentRequest request) { // Since Data is optional, if it is not included, this will be serialized into a null content in APLOptions, // and ignored later in the content creation process @@ -175,6 +269,7 @@ private APLOptions createAPLOptions(final PrepareDocumentRequest request) { IPackageLoader packageLoader = mConfig.getIPackageLoader() == null ? (importRequest, successCallback, failureCallback) -> failureCallback.onFailure(importRequest, "Content package loading not implemented.") : mConfig.getIPackageLoader(); APLOptions options = APLOptions.builder() .packageLoader(packageLoader) + .telemetryProvider(mTelemetryProvider) .contentDataRetriever((source, successCallback, failureCallback) -> { JSONObject currentData; try { @@ -198,15 +293,174 @@ private APLOptions createAPLOptions(final PrepareDocumentRequest request) { } @Override - public DocumentHandle render(RenderDocumentRequest request) { - return null; + public DocumentHandle render(final RenderDocumentRequest request) { + Log.d(TAG, "Render called using RenderDocumentRequest with token: " + request.getToken()); + final long entryTime = SystemClock.elapsedRealtime(); + PreparedDocument preparedDocument = prepare( + PrepareDocumentRequest.builder() + .token(request.getToken()) + .document(request.getDocument()) + .data(request.getData()) + .documentSession(request.getDocumentSession()) + .documentOptions(request.getDocumentOptions()) + .build() + ); + + return render(preparedDocument, SystemClock.elapsedRealtime() - entryTime); + } + + @Override + public DocumentHandle render(final PreparedDocument preparedDocument) { + Log.d(TAG, "Render called using PreparedDocument with token: " + preparedDocument.getToken()); + return render(preparedDocument, 0); + } + + DocumentHandle render(final PreparedDocument preparedDocument, final long initialElapsedTime) { + DocumentHandleImpl handle = preparedDocument.getHandle() instanceof DocumentHandleImpl ? + (DocumentHandleImpl) preparedDocument.getHandle() : null; + if (handle == null) { + Log.e(TAG, "Document handle is unexpectedly null, dropping request to render document"); + return null; + } + if (!handle.isValid()) { + Log.e(TAG, "Document not valid, dropping request to render document"); + return null; + } + writeAPLSessionLog(handle.getSession(), Session.LogEntryLevel.INFO, "----- Rendering Document -----"); + + handle.startRenderDocumentTimer(TimeUnit.MILLISECONDS, initialElapsedTime); + Log.i(TAG, "Rendering document with handle: " + handle); + + if ((DocumentState.DISPLAYED.equals(handle.getDocumentState()) || DocumentState.INFLATED.equals(handle.getDocumentState())) + && handle.getContent() != null) { + Log.d(TAG, "Reusing prepared document to render"); + handle.setDocumentState(DocumentState.PREPARED); + } + + for (DocumentStateChangeListener listener : mDocumentStateChangeListeners) { + handle.registerStateChangeListener(listener); + } + handle.registerStateChangeListener(this); + return handle; + } + + @Override + public void registerStateChangeListener(DocumentStateChangeListener listener) { + mDocumentStateChangeListeners.add(listener); + } + + @Override + public void bind(APLLayout aplLayout) { + if (mAplLayout != null) { + throw new IllegalStateException("There is already a view bound"); + } else { + mAplLayout = aplLayout; + if (mAplLayout != null) { + mDTNetworkRequestManager.bindDTNetworkRequest(aplLayout.getDTView().getDTNetworkRequestHandler()); + } + } + } + + @Override + public void unBind() { + mAplLayout = null; + mDTNetworkRequestManager.unbindDTNetworkRequest(); + } + + @Override + public boolean isBound() { + return mAplLayout != null; + } + + @Override + public void updateDisplayState(DisplayState displayState) { + Log.d(TAG, "Received updateDisplayState for handle: " + mTopDocument + " with display state: " + displayState); + RootContext rootContext = mRootContext; + if (rootContext != null) { + rootContext.updateDisplayState(displayState); + + if (displayState == DisplayState.kDisplayStateBackground || displayState == DisplayState.kDisplayStateForeground) { + rootContext.resumeDocument(); + } else { // If Hidden + rootContext.pauseDocument(); + } + } else { + Log.w(TAG, "Skipping DisplayState update due to no root context."); + } + } + + @Override + public void cancelExecution() { + Log.d(TAG, "Received cancelExecution for handle: " + mTopDocument); + if (mRootContext != null) { + mRootContext.cancelExecution(); + } else { + Log.w(TAG, "RootContext is null, skipping cancelExecution"); + } + } + + /** + * @param savedDocument The document to restore. + */ + @Override + public boolean restoreDocument(SavedDocument savedDocument) { + DocumentHandleImpl documentHandle = ((DocumentHandleImpl)savedDocument.getDocumentHandle()); + if (documentHandle == null) { + Log.e(TAG, "Failed to restore saved document due to null document handle."); + return false; + } + + Log.i(TAG, "Trying to restore document: " + documentHandle); + + if (!documentHandle.isValid()) { + Log.e(TAG, "Failed to restore saved document which is not valid."); + return false; + } + + if (mTopDocument != null && mTopDocument.equals(documentHandle) && mTopDocument.isValid()) { + Log.w(TAG, "Trying to restore the valid top document hence no action needed"); + return false; + } + + if (mTopDocument != null && mTopDocument.isValid()) { + mTopDocument.finish(FinishDocumentRequest.builder().build()); + } + + documentHandle.setDocumentState(DocumentState.PREPARED); + return true; } @Override - public DocumentHandle render(PreparedDocument preparedDocument) { - return null; + public void invokeExtensionEventHandler(@NonNull String uri, @NonNull String name, Map data, boolean fastMode, @Nullable ExtensionEventHandlerCallback callback) { + if (mRootContext != null) { + mCoreWorker.post(() -> { + Action action = mRootContext.invokeExtensionEventHandler(uri, name, data, fastMode); + if (callback != null) { + if (action != null) { + // Pending action, add callbacks + action.then(() -> { + publish(() -> callback.onComplete()); + }); + action.addTerminateCallback(() -> { + publish(() -> callback.onTerminated()); + }); + } else { + // Not waiting for an action, execute the success callback immediately + publish(() -> { + callback.onComplete(); + }); + } + } + }); + } else { + Log.w(TAG, "RootContext is null, skipping invokeExtensionEventHandler"); + } } + /** + * Internal method for invoking a Runnable on the runtime thread + * @param task + */ public void publish(Runnable task) { mRuntimeInteractionWorker.execute(task); } @@ -268,19 +522,23 @@ public boolean interceptEventIfNeeded(Event event) { return false; } - public void checkAndReportDataSourceErrors() { + public void checkAndReportDataSourceErrors(Object primaryDocumentErrors) { for (WeakReference weakDocumentHandle : mDocumentMap.values()) { if (null == weakDocumentHandle) { continue; } final DocumentHandleImpl document = weakDocumentHandle.get(); if (document != null) { - DocumentConfig documentConfig = document.getDocumentConfig();; - if (documentConfig == null && documentConfig.getNativeHandle() == 0) { - Log.e(TAG, "document config does not exist, hence ignoring the request"); + DocumentConfig documentConfig = document.getDocumentConfig(); + if (documentConfig != null && documentConfig.getNativeHandle() != 0) { + Object errors = nGetDataSourceErrors(document.getDocumentConfig().getNativeHandle()); + handleDataErrorRequest(document, errors); + } else if (document.getRootContext() != null && document.getRootContext().getNativeHandle() != 0) { + handleDataErrorRequest(document, primaryDocumentErrors); + } else { + Log.e(TAG, "Neither document config nor root context defined, hence ignoring the request"); } - Object errors = nGetDataSourceErrors(document.getDocumentConfig().getNativeHandle()); - handleDataErrorRequest(document, errors); + } } } @@ -303,7 +561,7 @@ public void handleDataErrorRequest(final DocumentHandle document, final Object d ActionMessage actionMessage = new ActionMessageImpl(mNextMessageId.getAndIncrement(), document, "ReportRuntimeErrorRequest", payload); - mRuntimeInteractionWorker.execute(() -> handler.handleAction(actionMessage)); + publish(() -> handler.handleAction(actionMessage)); } /** @@ -341,7 +599,7 @@ private void notifyVisualContextChanged(MessageHandler handler, DocumentHandle h } public void notifyDocumentStateChanged(DocumentHandle handle, DocumentState state) { - Log.d(TAG, String.format("notifyDocumentStateChanged for document state: %s", state.toString())); + Log.d(TAG, String.format("notifyDocumentStateChanged for document: %s with document state: %s", handle.toString(), state.toString())); try { MessageHandler messageHandler = mConfig.getMessageHandler(); if (messageHandler == null) { @@ -422,8 +680,8 @@ public void onDataSourceFetchRequest(String type, Map payload) { ActionMessage actionMessage = new ActionMessageImpl(mNextMessageId.getAndIncrement(), document, - "FetchDataRequest", messagePayload); - mRuntimeInteractionWorker.execute(() -> handler.handleAction(actionMessage)); + FETCH_DATA_REQUEST, messagePayload); + publish(() -> handler.handleAction(actionMessage)); } }); @@ -458,9 +716,11 @@ public void onSendEvent(Object[] args, ActionMessage actionMessage = new ActionMessageImpl(mNextMessageId.getAndIncrement(), document, - "SendUserEventRequest", payload); + SEND_USER_EVENT_REQUEST, payload); - mRuntimeInteractionWorker.execute(() -> handler.handleAction(actionMessage)); + publish(() -> handler.handleAction(actionMessage)); + writeAPLSessionLog(document.getSession(), Session.LogEntryLevel.INFO, String.format("%s Arguments: %s Components: %s Flags: %s Source: %s", + SEND_USER_EVENT_REQUEST, Arrays.toString(args), components.toString(), sources.toString(), flags.toString())); } }); @@ -491,6 +751,7 @@ public void onComplete(Content content) { @Override public void onError(Exception e) { Log.e(TAG, "Error occured during content resolution: " + e.getMessage()); + document.getUserPerceivedFatalReporter().reportFatal(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE); } }); } @@ -532,15 +793,149 @@ public void onFailure(String reason) { resultCallback.onResult(false); } }; - + int messageId = mNextMessageId.getAndIncrement(); ActionMessage actionMessage = new - ActionMessageImpl(mNextMessageId.getAndIncrement(), document, "OpenURLRequest", + ActionMessageImpl(messageId, document, OPEN_URL_REQUEST, new JsonDecodable(payload), listener); - mRuntimeInteractionWorker.execute(() -> handler.handleAction(actionMessage)); + publish(() -> handler.handleAction(actionMessage)); + writeAPLSessionLog(document.getSession(), Session.LogEntryLevel.INFO, String.format(Locale.US ,"%s source: %s requestId: %d", + OPEN_URL_REQUEST, url, messageId)); }); return true; } + + private void writeAPLSessionLog(Session session, Session.LogEntryLevel level, String message) { + if (session != null) { + session.write(level, Session.LogEntrySource.VIEW, message); + } else { + Log.d(TAG, "Session is null hence skipping session.write"); + } + } + private static native Object nGetDataSourceErrors(long nativeHandle); + + @Override + public void onDocumentStateChanged(DocumentState state, DocumentHandle handle) { + Log.i(TAG, String.format("onDocumentStateChanged triggered with state: %s and handle: %s ", + state.toString(), handle.toString())); + + DocumentHandleImpl documentHandle = (DocumentHandleImpl) handle; + IUserPerceivedFatalCallback userPerceivedFatalCallback; + final DocumentOptions documentOptions = documentHandle.getDocumentOptions(); + if (documentOptions != null && documentOptions.getUserPerceivedFatalCallback() != null) { + userPerceivedFatalCallback = documentOptions.getUserPerceivedFatalCallback(); + } else { + Log.d(TAG, "No UserPerceivedFatalCallback provided by the runtime to report UPF."); + userPerceivedFatalCallback = new NoOpUserPerceivedFatalCallback(); + } + + if (state == DocumentState.PREPARED) { + if (!isBound()) { + Log.i(TAG, "No view bound to Viewhost, hence request not fulfilled"); + return; + } + + APLOptions options = APLOptions.builder() + .telemetryProvider(mTelemetryProvider) + .userPerceivedFatalCallback(userPerceivedFatalCallback) + .viewhost(this).build(); + RootConfig rootConfig; + if (documentHandle.getRootConfig() == null) { + rootConfig = createRootConfig(); + documentHandle.setRootConfig(rootConfig); + } else { + rootConfig = documentHandle.getRootConfig(); + } + rootConfig.session(((DocumentHandleImpl) handle).getSession()); + mAplLayout.setAPLSession(documentHandle.getSession()); + documentHandle.getSession().setAPLListener(mAplLayout.getDTView()); + + if (documentHandle.getExtensionMediator() != null) { + rootConfig.extensionMediator(documentHandle.getExtensionMediator()); + } + + if (documentHandle.getDocumentOptions() != null && documentHandle.getDocumentOptions().getEmbeddedDocumentFactory() != null) { + rootConfig.setDocumentManager(documentHandle.getDocumentOptions().getEmbeddedDocumentFactory() , mCoreWorker, mTelemetryProvider); + } + + mAplLayout.setAgentName(rootConfig); + mAplLayout.addMetricsReadyListener(viewportMetrics -> { + IAPLViewPresenter presenter = mAplLayout.getPresenter(); + mCoreWorker.post(() -> { + try { + final RootContext rootContext; + if (documentHandle.getRootContext() != null) { + rootContext = RootContext.createFromCache(presenter, documentHandle.getRootContext().getMetricsTransform(), options, rootConfig, documentHandle.getContent(), documentHandle.getRootContext().getNativeHandle()); + if (presenter.getConfigurationChange() != null) { + rootContext.handleConfigurationChange(presenter.getConfigurationChange()); + } + } else { + rootContext = RootContext.create(viewportMetrics, documentHandle.getContent(), rootConfig, options, presenter, documentHandle.getUserPerceivedFatalReporter()); + } + presenter.onDocumentRender(rootContext); + documentHandle.setRootContext(rootContext); + setTopDocumentHandleAndRootContext(handle, rootContext); + } catch (Exception e) { + Log.e(TAG, "Exception occurred while fulfilling request and the exception is: " + e); + documentHandle.setDocumentState(DocumentState.ERROR); + } + }); + documentHandle.getContent().createDocumentBackground(viewportMetrics, rootConfig); + if (presenter != null) { + presenter.loadBackground(documentHandle.getContent().getDocumentBackground()); + } + }); + } else if (state == DocumentState.DISPLAYED) { + documentHandle.stopRenderDocumentTimer(); + mTelemetryProvider.incrementCount(mRenderDocumentCount); + } else if (state == DocumentState.ERROR) { + documentHandle.failRenderDocumentTimer(); + mTelemetryProvider.fail(mRenderDocumentCount); + Log.e(TAG, String.format("Document %s moved to error state", handle)); + } + } + + /** + * Set the document reference and rootContext for the top-level document + * + * @param rootContext + * @param documentHandle + */ + void setTopDocumentHandleAndRootContext(DocumentHandle documentHandle, RootContext rootContext) { + mTopDocument = documentHandle; + mRootContext = rootContext; + } + + /** + * Creates rootConfig from viewhostConfig + * @return + */ + private RootConfig createRootConfig() { + RootConfig config = RootConfig.create(); + + config.registerDataSource("dynamicIndexList"); + config.registerDataSource("dynamicTokenList"); + + Map rootProperties = mConfig.getRootProperties() == null ? Collections.emptyMap() : mConfig.getRootProperties(); + for(RootProperty key : rootProperties.keySet()) { + config.set(key, rootProperties.get(key)); + } + + Map environmentProperties = mConfig.getEnvironmentProperties() == null ? Collections.emptyMap() : mConfig.getEnvironmentProperties(); + for(String key: environmentProperties.keySet()) { + config.set(key, environmentProperties.get(key)); + } + + if (mConfig.getAudioPlayerFactory() != null) { + config.audioPlayerFactory(mConfig.getAudioPlayerFactory()); + } + + if (mConfig.getMediaPlayerFactory() != null) { + config.mediaPlayerFactory(mConfig.getMediaPlayerFactory()); + } + + return config; + } } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/request/RenderDocumentRequest.java b/apl/src/main/java/com/amazon/apl/viewhost/request/RenderDocumentRequest.java index 44452f96..7fe55206 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/request/RenderDocumentRequest.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/request/RenderDocumentRequest.java @@ -4,11 +4,62 @@ */ package com.amazon.apl.viewhost.request; +import com.amazon.apl.android.DocumentSession; import com.amazon.apl.viewhost.config.DocumentOptions; +import com.amazon.apl.viewhost.primitives.Decodable; +import com.google.auto.value.AutoValue; + +import javax.annotation.Nullable; /** * Represents a request to render an APL document from a document payload. */ +@AutoValue public abstract class RenderDocumentRequest { - // TODO: Implement for primary document rendering + /** + * The token to use for this request (optional). If provided, subsequent requests to manipulate + * the document (e.g. ExecuteCommandsRequest) must match the token provided here. In case of a + * mismatch, the request will be ignored. Tokens are treated as opaque values by the viewhost, + * they are only ever compared for equality. + */ + @Nullable + public abstract String getToken(); + + /** + * Document payload (required). The payload can be an APL document, or an APL payload wrapped in + * a typical Alexa Voice Service (AVS) envelope. If the document payload corresponds to an AVS + * envelope, this envelope will be used as a fall back data source in case no data source was + * provided with this request. + */ + public abstract Decodable getDocument(); + + /** + * The source for data (parameters) used by the APL document (optional). + */ + @Nullable + public abstract Decodable getData(); + + /** + * The document session to use when rendering this document (required). + */ + public abstract DocumentSession getDocumentSession(); + + /** + * The options to use for preparing and rendering the document (optional). These options + * override any default options provided at the time the viewhost was created. + */ + @Nullable + public abstract DocumentOptions getDocumentOptions(); + + public static Builder builder() { return new AutoValue_RenderDocumentRequest.Builder(); } + + @AutoValue.Builder + public abstract static class Builder { + public abstract Builder token(String token); + public abstract Builder document(Decodable document); + public abstract Builder data(Decodable data); + public abstract Builder documentSession(DocumentSession documentSession); + public abstract Builder documentOptions(DocumentOptions documentOptions); + public abstract RenderDocumentRequest build(); + } } diff --git a/apl/src/main/java/com/amazon/apl/viewhost/request/UpdateDataSourceRequest.java b/apl/src/main/java/com/amazon/apl/viewhost/request/UpdateDataSourceRequest.java index 24c530c5..dbb681a4 100644 --- a/apl/src/main/java/com/amazon/apl/viewhost/request/UpdateDataSourceRequest.java +++ b/apl/src/main/java/com/amazon/apl/viewhost/request/UpdateDataSourceRequest.java @@ -23,6 +23,12 @@ public abstract class UpdateDataSourceRequest { */ @Nullable public abstract String getToken(); + /** + * Get the update type such as dynamicIndexList, dynamicTokenList. + * @return + */ + @Nullable + public abstract String getType(); /** * Payload to update data source. */ @@ -42,6 +48,7 @@ public static UpdateDataSourceRequest.Builder builder() { @AutoValue.Builder public abstract static class Builder { public abstract Builder token(String token); + public abstract Builder type(String type); public abstract Builder data(Decodable data); public abstract Builder callback(UpdateDataSourceCallback callback); public abstract UpdateDataSourceRequest build(); diff --git a/apl/src/test/java/android/util/Log.java b/apl/src/test/java/android/util/Log.java new file mode 100644 index 00000000..1e14a435 --- /dev/null +++ b/apl/src/test/java/android/util/Log.java @@ -0,0 +1,38 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package android.util; + +// Reference: https://stackoverflow.com/questions/36787449/how-to-mock-method-e-in-log +public class Log { + /** + * Priority constant for the println method; use Log.i. + */ + public static final int INFO = 4; + public static int d(String tag, String msg) { + System.out.println("DEBUG: " + tag + ": " + msg); + return 0; + } + + public static int i(String tag, String msg) { + System.out.println("INFO: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg) { + System.out.println("WARN: " + tag + ": " + msg); + return 0; + } + + public static int w(String tag, String msg, Throwable e) { + System.out.println("WARN: " + tag + ": " + msg + "\nCaused by " + e.getMessage()); + return 0; + } + + public static int e(String tag, String msg) { + System.out.println("ERROR: " + tag + ": " + msg); + return 0; + } +} \ No newline at end of file diff --git a/apl/src/test/java/com/amazon/apl/android/APLAccessibilityDelegateTest.java b/apl/src/test/java/com/amazon/apl/android/APLAccessibilityDelegateTest.java index 9df7bf2a..5d495c42 100644 --- a/apl/src/test/java/com/amazon/apl/android/APLAccessibilityDelegateTest.java +++ b/apl/src/test/java/com/amazon/apl/android/APLAccessibilityDelegateTest.java @@ -13,6 +13,7 @@ import androidx.core.view.accessibility.AccessibilityNodeInfoCompat; import com.amazon.apl.android.primitive.AccessibilityActions; +import com.amazon.apl.android.primitive.AccessibilityAdjustableRange; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; import com.amazon.apl.enums.PropertyKey; import com.amazon.apl.enums.Role; @@ -41,6 +42,7 @@ public class APLAccessibilityDelegateTest extends ViewhostRobolectricTest { private static final String COMPONENT_ID = "test-component-id"; private static final String TEXT = "test text"; + private static final AccessibilityAdjustableRange ADJUSTABLE_RANGE = AccessibilityAdjustableRange.create(0, 10, 5); @Mock private View mView; @@ -110,6 +112,47 @@ public void test_nodeTextNotSetIfComponentTextNotPresent() { assertNull(mNode.getText()); } + @Test + public void test_nodeRangeInfoSetIfComponentAdjustable() { + configureRoleInNode(Role.kRoleAdjustable); + when(mComponent.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableRange)).thenReturn(true); + when(mComponent.getAccessibilityAdjustableRange()).thenReturn(ADJUSTABLE_RANGE); + + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, mNode); + + assertEquals(ADJUSTABLE_RANGE.minValue(), mNode.getRangeInfo().getMin(), 0.001); + assertEquals(ADJUSTABLE_RANGE.maxValue(), mNode.getRangeInfo().getMax(), 0.001); + assertEquals(ADJUSTABLE_RANGE.currentValue(), mNode.getRangeInfo().getCurrent(), 0.001); + } + + @Test + public void test_nodeRangeInfoNotSetIfComponentNotAdjustable() { + when(mComponent.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableRange)).thenReturn(true); + + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, mNode); + + assertNull(mNode.getRangeInfo()); + } + + @Test + public void test_nodeRangeInfoNotSetIfComponentAdjustableRangeNotPresent() { + configureRoleInNode(Role.kRoleAdjustable); + when(mComponent.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableRange)).thenReturn(false); + + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, mNode); + + assertNull(mNode.getRangeInfo()); + } + + @Test + public void test_nodeRangeInfoNotSetIfAdjustableValuePropertyPresent() { + when(mComponent.hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue)).thenReturn(true); + + mAccessibilityDelegate.onInitializeAccessibilityNodeInfo(mView, mNode); + + assertNull(mNode.getRangeInfo()); + } + @Test public void test_adjustableRole() { configureRoleInNode(Role.kRoleAdjustable); @@ -452,6 +495,26 @@ public void test_swipeAwayAction() { assertEquals("Play song", actualActionNode.getLabel()); } + @Test + public void test_incrementAction() { + configureRoleInNode(Role.kRoleAdjustable); + configureActionInNode("increment", "Increment value"); + + AccessibilityNodeInfoCompat.AccessibilityActionCompat actualActionNode = mNode.getActionList().get(0); + assertEquals(AccessibilityNodeInfoCompat.ACTION_SCROLL_FORWARD, actualActionNode.getId()); + assertEquals("Increment value", actualActionNode.getLabel()); + } + + @Test + public void test_decrementAction() { + configureRoleInNode(Role.kRoleAdjustable); + configureActionInNode("decrement", "Decrement value"); + + AccessibilityNodeInfoCompat.AccessibilityActionCompat actualActionNode = mNode.getActionList().get(0); + assertEquals(AccessibilityNodeInfoCompat.ACTION_SCROLL_BACKWARD, actualActionNode.getId()); + assertEquals("Decrement value", actualActionNode.getLabel()); + } + @Test public void test_customAction() { configureActionInNode("thumbsup", "Mark positively"); diff --git a/apl/src/test/java/com/amazon/apl/android/APLControllerTest.java b/apl/src/test/java/com/amazon/apl/android/APLControllerTest.java index dcc76a55..53c2b5cd 100644 --- a/apl/src/test/java/com/amazon/apl/android/APLControllerTest.java +++ b/apl/src/test/java/com/amazon/apl/android/APLControllerTest.java @@ -5,21 +5,28 @@ package com.amazon.apl.android; +import android.content.Context; import android.os.Looper; +import com.amazon.alexaext.ExtensionRegistrar; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.extension.IExtensionRegistration; import com.amazon.apl.android.providers.AbstractMediaPlayerProvider; + +import static com.amazon.apl.android.APLController.initializeAPL; import static com.amazon.apl.android.APLController.setLibraryFuture; -import static junit.framework.TestCase.assertNull; import androidx.test.platform.app.InstrumentationRegistry; import com.amazon.apl.android.bitmap.IBitmapPool; import com.amazon.apl.android.dependencies.IContentCompleteCallback; +import com.amazon.apl.devtools.models.ViewTypeTarget; import com.amazon.apl.android.font.CompatFontResolver; import com.amazon.apl.android.font.TypefaceResolver; import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.providers.ITtsPlayerProvider; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.DisplayState; import org.junit.Before; @@ -46,8 +53,11 @@ import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.doAnswer; +import static org.mockito.Mockito.doNothing; import static org.mockito.Mockito.inOrder; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; @@ -86,6 +96,19 @@ public class APLControllerTest extends ViewhostRobolectricTest { private DocumentSession mDocumentSession; @Mock private ITelemetryProvider mTelemetryProvider; + @Mock + private IUserPerceivedFatalCallback mUserPerceivedFatalCallback; + @Mock + private ViewTypeTarget mViewTypeTarget; + @Mock + private Session mAPLSession; + private Context mContext; + @Mock + private RuntimeConfig mRuntimeConfig; + @Mock + private IExtensionRegistration mExtensionRegistration; + @Mock + private ExtensionRegistrar mExtensionRegistrar; private APLController mController; @@ -93,10 +116,14 @@ public class APLControllerTest extends ViewhostRobolectricTest { public void setup() { mController = new APLController(mRootContext, mContent); when(mOptions.getTelemetryProvider()).thenReturn(mTelemetryProvider); + when(mOptions.getUserPerceivedFatalCallback()).thenReturn(mUserPerceivedFatalCallback); when(mRootContext.executeCommands(any())).thenReturn(mAction); when(mRootContext.getOptions()).thenReturn(mOptions); when(mRootContext.getRootConfig()).thenReturn(mRootConfig); + when(mRootConfig.getSession()).thenReturn(mAPLSession); when(mAplLayout.getPresenter()).thenReturn(mViewPresenter); + when(mAplLayout.getDTView()).thenReturn(mViewTypeTarget); + when(mOptions.getUserPerceivedFatalCallback()).thenReturn(mUserPerceivedFatalCallback); try { when(mLibraryFuture.get( any(long.class), @@ -227,7 +254,7 @@ public void testLifecycleEventsIgnoredAfterFinish() { mController.pauseDocument(); - verify(mRootContext).getRootConfig(); + verify(mRootContext, times(2)).getRootConfig(); verifyNoMoreInteractions(mRootContext); } @@ -255,7 +282,7 @@ public void testOnDocumentDisplayedStopTimer() { .rootConfig(mRootConfig) .aplDocument("{}") .aplOptions(aplOptions) - .contentCreator(((aplDocument, options, callbackV2, rootConfig) -> {callbackV2.onComplete(mContent); return mContent;})) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> {callbackV2.onComplete(mContent); return mContent;})) .documentSession(mDocumentSession) .render(); @@ -265,6 +292,145 @@ public void testOnDocumentDisplayedStopTimer() { verify(mTelemetryProvider).stopTimer(anyInt(), any(TimeUnit.class), anyLong()); } + @Test + public void testUPFCallback_whenDoesNotInitializeApl_ContentCreationError() { + // Setup + try { + when(mLibraryFuture.get( + any(long.class), + any(TimeUnit.class) + )).thenReturn(false); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (TimeoutException e) { + throw new RuntimeException(e); + } + when(mOptions.getContentCompleteCallback()).thenReturn(mContentCompleteCallback); + when(mOptions.getContentCompleteCallback()).thenReturn(mContentCompleteCallback); + when(mRootConfig.getExtensionProvider()).thenReturn(mExtensionRegistrar); + when(mOptions.getExtensionRegistration()).thenReturn(mExtensionRegistration); + doNothing().when(mExtensionRegistration).registerExtensions(any(), any()); + mockStatic(ExtensionMediator.class); + when(ExtensionMediator.create(any(), any())).thenReturn(mExtensionMediator); + doAnswer(invocation -> { + ExtensionMediator.ILoadExtensionCallback loadExtensionCallback = invocation.getArgument(2); + loadExtensionCallback.onSuccess().run(); + return null; + }).when(mExtensionMediator).loadExtensions(any(RootConfig.class), any(), any()); + setLibraryFuture(mLibraryFuture); + + // Test + APLController controller = (APLController)APLController.builder() + .aplLayout(mAplLayout) + .rootConfig(mRootConfig) + .aplDocument("{}") + .aplOptions(mOptions) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> {callbackV2.onComplete(mContent); return mContent;})) + .documentSession(mDocumentSession) + .render(); + + // Verify + verify(mUserPerceivedFatalCallback, times(1)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.APL_INITIALIZATION_FAILURE.toString())); + } + + @Test + public void testUPFCallback_whenDoesNotInitializeApl_ContentCreationComplete() { + // Setup + try { + when(mLibraryFuture.get( + any(long.class), + any(TimeUnit.class) + )).thenReturn(false); + } catch (ExecutionException e) { + throw new RuntimeException(e); + } catch (InterruptedException e) { + throw new RuntimeException(e); + } catch (TimeoutException e) { + throw new RuntimeException(e); + } + when(mOptions.getContentCompleteCallback()).thenReturn(mContentCompleteCallback); + when(mOptions.getContentCompleteCallback()).thenReturn(mContentCompleteCallback); + when(mRootConfig.getExtensionProvider()).thenReturn(mExtensionRegistrar); + when(mOptions.getExtensionRegistration()).thenReturn(mExtensionRegistration); + doNothing().when(mExtensionRegistration).registerExtensions(any(), any()); + mockStatic(ExtensionMediator.class); + when(ExtensionMediator.create(any(), any())).thenReturn(mExtensionMediator); + doAnswer(invocation -> { + ExtensionMediator.ILoadExtensionCallback loadExtensionCallback = invocation.getArgument(2); + loadExtensionCallback.onSuccess().run(); + return null; + }).when(mExtensionMediator).loadExtensions(any(RootConfig.class), any(), any()); + setLibraryFuture(mLibraryFuture); + + // Test + APLController controller = (APLController)APLController.builder() + .aplLayout(mAplLayout) + .rootConfig(mRootConfig) + .aplDocument("{}") + .aplOptions(mOptions) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> {callbackV2.onComplete(mContent); return mContent;})) + .documentSession(mDocumentSession) + .render(); + + // Verify + verify(mUserPerceivedFatalCallback, times(1)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.APL_INITIALIZATION_FAILURE.toString())); + } + + @Test + public void testUPFCallback_whenInitializeApl_ContentCreationError() { + // Setup + initializeAPL(mContext, mRuntimeConfig); + setLibraryFuture(mLibraryFuture); + + // Test + APLController controller = (APLController)APLController.builder() + .aplLayout(mAplLayout) + .rootConfig(mRootConfig) + .aplDocument("{}") + .aplOptions(mOptions) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> {callbackV2.onError(new Exception()); return null;})) + .documentSession(mDocumentSession) + .render(); + + // Verify + verify(mUserPerceivedFatalCallback, times(1)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.CONTENT_CREATION_FAILURE.toString())); + } + + @Test + public void testRequiredExtensionLoadingFailure_whenExtensionCallbackReportsFailure() { + // Setup + initializeAPL(mContext, mRuntimeConfig); + when(mOptions.getContentCompleteCallback()).thenReturn(mContentCompleteCallback); + when(mRootConfig.getExtensionProvider()).thenReturn(mExtensionRegistrar); + when(mOptions.getExtensionRegistration()).thenReturn(mExtensionRegistration); + doNothing().when(mExtensionRegistration).registerExtensions(any(), any()); + mockStatic(ExtensionMediator.class); + when(ExtensionMediator.create(any(), any())).thenReturn(mExtensionMediator); + doAnswer(invocation -> { + ExtensionMediator.ILoadExtensionCallback loadExtensionCallback = invocation.getArgument(2); + loadExtensionCallback.onFailure().run(); + + return null; + }).when(mExtensionMediator).loadExtensions(any(RootConfig.class), any(), any()); + setLibraryFuture(mLibraryFuture); + + // Test + APLController controller = (APLController)APLController.builder() + .aplLayout(mAplLayout) + .rootConfig(mRootConfig) + .aplDocument("{}") + .aplOptions(mOptions) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> {callbackV2.onComplete(mContent); return mContent;})) + .documentSession(mDocumentSession) + .disableAsyncInflate(true) + .render(); + + // Verify + verify(mUserPerceivedFatalCallback, times(1)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE.toString())); + } + @Test public void testDisplayStateChangesIgnoredAfterFinish() { mController.finishDocument(); @@ -272,7 +438,7 @@ public void testDisplayStateChangesIgnoredAfterFinish() { mController.updateDisplayState(DisplayState.kDisplayStateHidden); - verify(mRootContext).getRootConfig(); + verify(mRootContext, times(2)).getRootConfig(); verifyNoMoreInteractions(mRootContext); } @@ -284,7 +450,7 @@ public void testExposesUnderlyingDocVersion() { mController.finishDocument(); verify(mRootContext).finishDocument(); - verify(mRootContext).getRootConfig(); + verify(mRootContext, times(2)).getRootConfig(); mController.getDocVersion(); @@ -302,7 +468,7 @@ public void testRenderV2_addsDocumentLifecycleListeners() { .rootConfig(mRootConfig) .aplDocument("{}") .aplOptions(aplOptions) - .contentCreator(((aplDocument, options, callbackV2, rootConfig) -> mContent)) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> mContent)) .documentSession(mDocumentSession) .render(); @@ -326,7 +492,7 @@ public void testRenderV2_addsRootConfigLifecycleListeners() { .rootConfig(mRootConfig) .aplDocument("{}") .aplOptions(aplOptions) - .contentCreator(((aplDocument, options, callbackV2, rootConfig) -> mContent)) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> mContent)) .documentSession(mDocumentSession) .render(); @@ -358,7 +524,7 @@ public void testRenderV2_contentCompleteCallbackOnComplete() { .rootConfig(mRootConfig) .aplDocument("{}") .aplOptions(aplOptions) - .contentCreator(((aplDocument, options, callbackV2, rootConfig) -> {callbackV2.onComplete(mContent); return mContent;})) + .contentCreator(((aplDocument, options, callbackV2, rootConfig, dtNetworkRequestHandler) -> {callbackV2.onComplete(mContent); return mContent;})) .documentSession(mDocumentSession) .render(); diff --git a/apl/src/test/java/com/amazon/apl/android/APLOptionsTest.java b/apl/src/test/java/com/amazon/apl/android/APLOptionsTest.java index bdd49d3c..de7426bf 100644 --- a/apl/src/test/java/com/amazon/apl/android/APLOptionsTest.java +++ b/apl/src/test/java/com/amazon/apl/android/APLOptionsTest.java @@ -6,6 +6,7 @@ package com.amazon.apl.android; import com.amazon.apl.android.dependencies.impl.DefaultUriSchemeValidator; +import com.amazon.apl.android.dependencies.impl.NoOpUserPerceivedFatalCallback; import com.amazon.apl.android.providers.impl.GlideImageLoaderProvider; import com.amazon.apl.android.providers.impl.MediaPlayerProvider; import com.amazon.apl.android.providers.impl.NoOpTelemetryProvider; @@ -37,6 +38,7 @@ private void checkDefaults(APLOptions options) { assertTrue(options.getImageUriSchemeValidator() instanceof DefaultUriSchemeValidator); assertTrue(options.getTtsPlayerProvider() instanceof NoOpTtsPlayerProvider); assertTrue(options.getExtensionGrantRequestCallback().isExtensionGranted("")); + assertTrue(options.getUserPerceivedFatalCallback() instanceof NoOpUserPerceivedFatalCallback); } } diff --git a/apl/src/test/java/com/amazon/apl/android/ExtensionMediatorTest.java b/apl/src/test/java/com/amazon/apl/android/ExtensionMediatorTest.java index 0573ecd4..f2c25598 100644 --- a/apl/src/test/java/com/amazon/apl/android/ExtensionMediatorTest.java +++ b/apl/src/test/java/com/amazon/apl/android/ExtensionMediatorTest.java @@ -19,8 +19,8 @@ import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import org.junit.Before; -import org.junit.Ignore; import org.junit.Test; import org.mockito.Mock; import org.mockito.MockitoAnnotations; @@ -82,9 +82,10 @@ public class ExtensionMediatorTest extends ViewhostRobolectricTest { private ExtensionRegistrar mExtensionRegistrar; @Mock private IExtensionProvider mExtensionProvider; - @Mock private Session mSession; + @Mock + private IDTNetworkRequestHandler mDTNetworkRequestHandler; @Before public void setup() { @@ -133,7 +134,7 @@ public void testLoadExtensions_calls_onExtensionsLoadedCallback_only_once() thro @Test public void testInitializeExtensions_specific_grantedExtensions_loads_only_those_extensions() throws InterruptedException { - Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); RootConfig rootConfig = RootConfig.create(); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar().addProvider(mExtensionProvider); ExtensionMediator mediator = ExtensionMediator.create(extensionRegistrar, DocumentSession.create()); @@ -151,7 +152,7 @@ public void testInitializeExtensions_specific_grantedExtensions_loads_only_those @Test public void testInitializeExtensions_without_rootConfig_specific_grantedExtensions_loads_only_those_extensions() throws InterruptedException { - Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar().addProvider(mExtensionProvider); ExtensionMediator mediator = ExtensionMediator.create(extensionRegistrar, DocumentSession.create()); mediator.initializeExtensions(new HashMap<>(), content, (uri) -> { @@ -170,7 +171,7 @@ public void testInitializeExtensions_without_rootConfig_specific_grantedExtensio public void testExecutor() throws InterruptedException { // given RootConfig rootConfig = RootConfig.create(); - Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); TestLiveDataLocalExtension extension = spy(new TestLiveDataLocalExtension()); LegacyLocalExtensionProxy legacyLocalExtensionProxy = new LegacyLocalExtensionProxy(extension); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar().addProvider(mExtensionProvider); @@ -193,7 +194,7 @@ public void testExecutor() throws InterruptedException { @Test public void testExecutorWithoutRootConfig() throws InterruptedException { // given - Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); TestLiveDataLocalExtension extension = spy(new TestLiveDataLocalExtension()); LegacyLocalExtensionProxy legacyLocalExtensionProxy = new LegacyLocalExtensionProxy(extension); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar().addProvider(mExtensionProvider); @@ -214,7 +215,7 @@ public void testExecutorWithoutRootConfig() throws InterruptedException { } private ExtensionMediator loadExtensions() { - Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(mTestDoc, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); RootConfig rootConfig = RootConfig.create(); ExtensionMediator mediator = ExtensionMediator.create(mExtensionRegistrar, DocumentSession.create()); mediator.initializeExtensions(rootConfig, content, null); @@ -243,7 +244,7 @@ private ExtensionMediator loadExtensions() { @Test public void test_loadExtensions_requiredExtension_registeredAndGranted_callsOnSuccessCallback() throws InterruptedException { // given - Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar(); TestLiveDataLocalExtension extension = new TestLiveDataLocalExtension(); LegacyLocalExtensionProxy legacyLocalExtensionProxy = new LegacyLocalExtensionProxy(extension); @@ -262,7 +263,7 @@ public void test_loadExtensions_requiredExtension_registeredAndGranted_callsOnSu @Test public void test_loadExtensions_requiredExtension_notRegistered_callsOnFailCallback() throws InterruptedException { // given that the required extension is not registered in the ExtensionRegistrar - Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar(); ExtensionMediator mediator = ExtensionMediator.create(extensionRegistrar, DocumentSession.create()); // when callback grants this extension @@ -277,7 +278,7 @@ public void test_loadExtensions_requiredExtension_notRegistered_callsOnFailCallb @Test public void test_loadExtensions_requiredExtension_notGranted_callsOnFailCallback() throws InterruptedException { // given - Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar(); TestLiveDataLocalExtension extension = spy(new TestLiveDataLocalExtension()); LegacyLocalExtensionProxy legacyLocalExtensionProxy = new LegacyLocalExtensionProxy(extension); @@ -298,7 +299,7 @@ public void test_loadExtensions_requiredExtension_failsRegistration_callsOnFailC ProxyWithFailingRegistration legacyLocalExtensionProxy = new ProxyWithFailingRegistration(new TestLiveDataLocalExtension()); ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar().registerExtension(legacyLocalExtensionProxy); ExtensionMediator mediator = ExtensionMediator.create(extensionRegistrar, DocumentSession.create()); - Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession); + Content content = Content.create(requiredExtension, mOptions, mContentCallbackV2, mSession, mDTNetworkRequestHandler); // when mediator.initializeExtensions(new HashMap<>(), content, uri -> true); mediator.loadExtensions(new HashMap<>(), content, mLoadExtensionCallback); diff --git a/apl/src/test/java/com/amazon/apl/android/UserPerceivedFatalReporterTest.java b/apl/src/test/java/com/amazon/apl/android/UserPerceivedFatalReporterTest.java new file mode 100644 index 00000000..e85ede38 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/android/UserPerceivedFatalReporterTest.java @@ -0,0 +1,89 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.android; + +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +public class UserPerceivedFatalReporterTest extends ViewhostRobolectricTest { + + @Mock + private IUserPerceivedFatalCallback userPerceivedFatalCallback; + + private UserPerceivedFatalReporter mUserPerceivedFatalReporter; + + + @Before + public void setup() { + mUserPerceivedFatalReporter = new UserPerceivedFatalReporter(userPerceivedFatalCallback); + } + + @Test + public void testReportUpfOnlyOnceWhenMultipleFatalsReported() { + // Setup + + // Test + mUserPerceivedFatalReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE); + mUserPerceivedFatalReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE); + + // Verify + verify(userPerceivedFatalCallback, times(1)) + .onFatalError(eq(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE.toString())); + verify(userPerceivedFatalCallback, times(0)) + .onFatalError(eq(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE.toString())); + + } + + @Test + public void testReportUpfOnlyOnceWhenSuccessIsReportedBeforeFatal() { + // Setup + + // Test + mUserPerceivedFatalReporter.reportSuccess(); + mUserPerceivedFatalReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE); + + // Verify + verify(userPerceivedFatalCallback, times(1)).onSuccess(); + verify(userPerceivedFatalCallback, times(0)) + .onFatalError(eq(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE.toString())); + + } + + @Test + public void testReportUpfOnlyOnceWhenFatalIsReportedBeforeSuccess() { + // Setup + + // Test + mUserPerceivedFatalReporter.reportFatal(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE); + mUserPerceivedFatalReporter.reportSuccess(); + + // Verify + verify(userPerceivedFatalCallback, times(0)).onSuccess(); + verify(userPerceivedFatalCallback, times(1)) + .onFatalError(eq(UserPerceivedFatalReporter.UpfReason.CONTENT_RESOLUTION_FAILURE.toString())); + + } + + @Test + public void testReportUpfOnlyOnceWhenMultipleSuccessReported() { + // Setup + + // Test + mUserPerceivedFatalReporter.reportSuccess(); + mUserPerceivedFatalReporter.reportSuccess(); + + // Verify + verify(userPerceivedFatalCallback, times(1)).onSuccess(); + } +} \ No newline at end of file diff --git a/apl/src/test/java/com/amazon/apl/android/component/AbstractComponentViewAdapterTest.java b/apl/src/test/java/com/amazon/apl/android/component/AbstractComponentViewAdapterTest.java index e42db973..edd0d150 100644 --- a/apl/src/test/java/com/amazon/apl/android/component/AbstractComponentViewAdapterTest.java +++ b/apl/src/test/java/com/amazon/apl/android/component/AbstractComponentViewAdapterTest.java @@ -38,7 +38,6 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; -import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.eq; @@ -157,7 +156,7 @@ public void test_defaults() { assertEquals(View.VISIBLE, getView().getVisibility()); assertEquals(0f, getView().getZ(), 0.01); assertEquals(1f, getView().getAlpha(), 0.01); - assertNull(getView().getContentDescription()); + assertEquals("", getView().getContentDescription()); assertTrue(getView().isEnabled()); assertTrue(ViewCompat.hasAccessibilityDelegate(getView())); assertFalse(getView().hasOnClickListeners()); @@ -203,6 +202,69 @@ public void test_accessibility() { assertEquals(accessibilityString, getView().getContentDescription()); } + @Test + public void test_accessibility_adjustable_value() { + String accessibilityAdjustableValueString = "accessibilityAdjustableValue"; + when(component().getRole()).thenReturn(Role.kRoleAdjustable); + when(component().getAccessibilityLabel()).thenReturn(null); + when(component().hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue)).thenReturn(true); + when(component().getAccessibilityAdjustableValue()).thenReturn(accessibilityAdjustableValueString); + + applyAllProperties(); + + assertEquals(new StringBuilder().append(" ").append(accessibilityAdjustableValueString).toString(), getView().getContentDescription()); + } + + @Test + public void test_accessibility_label_and_adjustable_value() { + String accessibilityString = "accessibility"; + String accessibilityAdjustableValueString = "accessibilityAdjustableValue"; + when(component().getRole()).thenReturn(Role.kRoleAdjustable); + when(component().getAccessibilityLabel()).thenReturn(accessibilityString); + when(component().hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue)).thenReturn(true); + when(component().getAccessibilityAdjustableValue()).thenReturn(accessibilityAdjustableValueString); + + applyAllProperties(); + + assertEquals(String.join(" ", accessibilityString, accessibilityAdjustableValueString), getView().getContentDescription()); + } + + @Test + public void test_accessibility_label_and_empty_adjustable_value() { + String accessibilityString = "accessibility"; + when(component().getRole()).thenReturn(Role.kRoleAdjustable); + when(component().getAccessibilityLabel()).thenReturn(accessibilityString); + when(component().hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue)).thenReturn(true); + when(component().getAccessibilityAdjustableValue()).thenReturn(""); + + applyAllProperties(); + + assertEquals(accessibilityString, getView().getContentDescription()); + } + + @Test + public void test_accessibility_label_and_adjustable_value_not_present() { + String accessibilityString = "accessibility"; + when(component().getRole()).thenReturn(Role.kRoleAdjustable); + when(component().getAccessibilityLabel()).thenReturn(accessibilityString); + when(component().hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue)).thenReturn(false); + + applyAllProperties(); + + assertEquals(accessibilityString, getView().getContentDescription()); + } + + @Test + public void test_accessibility_label_and_adjustable_value_not_present_not_adjustable() { + String accessibilityString = "accessibility"; + when(component().getAccessibilityLabel()).thenReturn(accessibilityString); + when(component().hasProperty(PropertyKey.kPropertyAccessibilityAdjustableValue)).thenReturn(false); + + applyAllProperties(); + + assertEquals(accessibilityString, getView().getContentDescription()); + } + @Test public void test_screenReaderFocusability_noAccessibilityLabel() { String accessibilityString = ""; @@ -262,6 +324,7 @@ public void test_refresh_accessibility() { refreshProperties(PropertyKey.kPropertyAccessibilityLabel); verify(component(), atLeastOnce()).getAccessibilityLabel(); + verify(component(), atLeastOnce()).getRole(); verify(component()).isFocusable(); verify(component()).isFocusableInTouchMode(); verifyNoMoreInteractions(component()); @@ -276,6 +339,7 @@ public void test_refresh_disabled() { refreshProperties(PropertyKey.kPropertyDisabled); verify(component(), atLeastOnce()).isDisabled(); + verify(component(), atLeastOnce()).getRole(); verify(component()).isClickable(); verify(component()).isFocusable(); verify(component()).isFocusableInTouchMode(); diff --git a/apl/src/test/java/com/amazon/apl/android/component/EditTextViewAdapterTest.java b/apl/src/test/java/com/amazon/apl/android/component/EditTextViewAdapterTest.java index 56c0f9c1..04cab37f 100644 --- a/apl/src/test/java/com/amazon/apl/android/component/EditTextViewAdapterTest.java +++ b/apl/src/test/java/com/amazon/apl/android/component/EditTextViewAdapterTest.java @@ -366,6 +366,7 @@ public void test_refresh_accessibility() { verify(component(), atLeastOnce()).getAccessibilityLabel(); verify(component(), atLeastOnce()).isFocusable(); verify(component(), atLeastOnce()).isDisabled(); + verify(component(), atLeastOnce()).getRole(); verify(component()).isFocusableInTouchMode(); verifyNoMoreInteractions(component()); @@ -385,6 +386,7 @@ public void test_refresh_accessibility_disabled() { verify(component(), atLeastOnce()).getAccessibilityLabel(); verify(component(), atLeastOnce()).isFocusable(); verify(component(), atLeastOnce()).isDisabled(); + verify(component(), atLeastOnce()).getRole(); verify(component()).isFocusableInTouchMode(); verifyNoMoreInteractions(component()); diff --git a/apl/src/test/java/com/amazon/apl/android/component/ImageTest.java b/apl/src/test/java/com/amazon/apl/android/component/ImageTest.java index 37d9e57e..f45a7ce5 100644 --- a/apl/src/test/java/com/amazon/apl/android/component/ImageTest.java +++ b/apl/src/test/java/com/amazon/apl/android/component/ImageTest.java @@ -8,13 +8,6 @@ import android.content.Context; import android.graphics.Color; import android.graphics.RectF; -import android.net.Uri; -import android.os.Handler; -import android.os.Looper; -import android.util.Base64; - -import androidx.core.content.FileProvider; -import androidx.test.platform.app.InstrumentationRegistry; import com.amazon.apl.android.APLOptions; import com.amazon.apl.android.Component; @@ -24,9 +17,8 @@ import com.amazon.apl.android.primitive.Gradient; import com.amazon.apl.android.providers.IImageLoaderProvider; import com.amazon.apl.android.providers.ITelemetryProvider; -import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; -import com.amazon.apl.android.views.APLAbsoluteLayout; import com.amazon.apl.android.views.APLImageView; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.BlendMode; import com.amazon.apl.enums.ComponentType; import com.amazon.apl.enums.FilterType; @@ -39,16 +31,7 @@ import org.junit.Before; import org.junit.Test; -import org.robolectric.Robolectric; -import org.robolectric.annotation.Config; -import org.robolectric.shadows.ShadowContentResolver; -import org.robolectric.shadows.ShadowLog; - -import java.io.ByteArrayInputStream; -import java.io.File; -import java.io.FileOutputStream; -import java.io.IOException; -import java.net.URI; + import java.util.ArrayList; import java.util.Arrays; import java.util.HashMap; @@ -58,18 +41,14 @@ import static org.junit.Assert.assertArrayEquals; import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertNull; -import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; import static org.mockito.ArgumentMatchers.anyInt; -import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.argThat; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.never; -import static org.mockito.Mockito.spy; -import static org.mockito.Mockito.timeout; import static org.mockito.Mockito.verify; import static org.mockito.Mockito.when; -import static org.robolectric.Shadows.shadowOf; public class ImageTest extends AbstractComponentUnitTest { @@ -300,6 +279,7 @@ void testProperties_optionalExplicitValues(Image component) { public void testDependencies_invokeImageLoader() { IImageLoaderProvider provider = mock(IImageLoaderProvider.class); IImageLoader imageLoader = mock(IImageLoader.class); + IDTNetworkRequestHandler dtNetworkRequest = mock(IDTNetworkRequestHandler.class); when(imageLoader.withTelemetry(any(ITelemetryProvider.class))).thenReturn(imageLoader); when(provider.get(any(Context.class))).thenReturn(imageLoader); @@ -314,9 +294,11 @@ public void testDependencies_invokeImageLoader() { APLImageView view = (APLImageView) ComponentViewAdapterFactory.getAdapter(component).createView(mContext, mAPLPresenter); when(mAPLPresenter.findComponent(view)).thenReturn(component); ImageViewAdapter imageViewAdapter = (ImageViewAdapter)ComponentViewAdapterFactory.getAdapter(component); + imageViewAdapter.setDTNetworkRequestHandler(dtNetworkRequest); imageViewAdapter.applyAllProperties(component, view); verify(imageLoader).loadImage(argThat(load -> DUMMY_URI.equals(load.path()))); + verify(dtNetworkRequest).requestWillBeSent(anyInt(), anyDouble(), any(), any()); } @Test diff --git a/apl/src/test/java/com/amazon/apl/android/component/MultiChildViewAdapterTest.java b/apl/src/test/java/com/amazon/apl/android/component/MultiChildViewAdapterTest.java index a1ff7c6d..a06806b5 100644 --- a/apl/src/test/java/com/amazon/apl/android/component/MultiChildViewAdapterTest.java +++ b/apl/src/test/java/com/amazon/apl/android/component/MultiChildViewAdapterTest.java @@ -200,6 +200,7 @@ public void test_refresh_accessibility() { refreshProperties(PropertyKey.kPropertyAccessibilityLabel); verify(component(), atLeastOnce()).getAccessibilityLabel(); + verify(component(), atLeastOnce()).getRole(); verify(component()).isFocusable(); verify(component(), atLeastOnce()).isDisabled(); verify(component()).isFocusableInTouchMode(); @@ -219,6 +220,7 @@ public void test_refresh_accessibility_disabled() { refreshProperties(PropertyKey.kPropertyAccessibilityLabel); verify(component(), atLeastOnce()).getAccessibilityLabel(); + verify(component(), atLeastOnce()).getRole(); verify(component()).isFocusable(); verify(component(), atLeastOnce()).isDisabled(); verify(component()).isFocusableInTouchMode(); diff --git a/apl/src/test/java/com/amazon/apl/android/dependencies/impl/MediaPlayerTest.java b/apl/src/test/java/com/amazon/apl/android/dependencies/impl/MediaPlayerTest.java index ee508883..04fbb322 100644 --- a/apl/src/test/java/com/amazon/apl/android/dependencies/impl/MediaPlayerTest.java +++ b/apl/src/test/java/com/amazon/apl/android/dependencies/impl/MediaPlayerTest.java @@ -6,6 +6,8 @@ package com.amazon.apl.android.dependencies.impl; import android.content.Context; +import android.content.res.AssetFileDescriptor; +import android.content.res.AssetManager; import android.graphics.SurfaceTexture; import android.media.AudioManager; import android.media.MediaPlayer.OnPreparedListener; @@ -29,6 +31,7 @@ import org.mockito.MockitoAnnotations; import org.robolectric.Robolectric; import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; import org.robolectric.shadow.api.Shadow; import org.robolectric.shadows.ShadowAudioManager; import org.robolectric.shadows.ShadowLog; @@ -38,6 +41,8 @@ import org.robolectric.shadows.util.DataSource; import org.robolectric.util.Scheduler; +import java.io.FileDescriptor; +import java.io.IOException; import java.util.ArrayList; import java.util.Collections; import java.util.LinkedList; @@ -59,6 +64,7 @@ import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.mock; import static org.mockito.Mockito.spy; @@ -72,6 +78,7 @@ import androidx.test.core.app.ApplicationProvider; @RunWith(RobolectricTestRunner.class) +@Config(sdk = 22) public class MediaPlayerTest { private static final String TAG = "MediaPlayerTest"; private static final String VIDEO_URL = "dummy-url"; @@ -90,9 +97,15 @@ public class MediaPlayerTest { private PlaybackListener mListener; @Mock private Context mContext; + @Mock + private AssetManager mAssetManager; + @Mock + private AssetFileDescriptor mAssetFileDescriptor; + @Mock + private FileDescriptor mFileDescriptor; @Before - public void setup() { + public void setup() throws IOException { MockitoAnnotations.openMocks(this); ShadowLog.stream = System.out; mTextureView = new TextureView(ApplicationProvider.getApplicationContext()); @@ -103,16 +116,22 @@ public void setup() { // TODO Handle audio manager ShadowAudioManager mShadowAudioManager = shadowOf(mAudioManager); + when(mAssetFileDescriptor.getFileDescriptor()).thenReturn(mFileDescriptor); + when(mAssetManager.openFd(anyString())).thenReturn(mAssetFileDescriptor); + when(mContext.getAssets()).thenReturn(mAssetManager); + android.media.MediaPlayer mMediaPlayer = Shadow.newInstanceOf(android.media.MediaPlayer.class); mShadowMediaPlayer = shadowOf(mMediaPlayer); - MediaInfo mInfo = new MediaInfo(TRACK_TOTAL_DURATION_MS, PREPARATION_DELAY_MS); mAplMediaPlayer = new MediaPlayer(mMediaPlayer, mTextureView, mContext, mAudioManager); mAplMediaPlayer.setAudioTrack(AudioTrack.kAudioTrackForeground); mAplMediaPlayer.setVideoScale(VideoScale.kVideoScaleBestFit); mAplMediaPlayer.addMediaStateListener(mListener); + MediaInfo mInfo = new MediaInfo(TRACK_TOTAL_DURATION_MS, PREPARATION_DELAY_MS); DataSource ds = toDataSource(VIDEO_URL); ShadowMediaPlayer.addMediaInfo(ds, mInfo); + DataSource assetDs = toDataSource(mFileDescriptor, 0, 0); + ShadowMediaPlayer.addMediaInfo(assetDs, mInfo); mShadowMediaPlayer.doSetDataSource(ds); mScheduler = Robolectric.getForegroundThreadScheduler(); @@ -546,6 +565,23 @@ public void test_No_op() { checkState(State.END, RELEASED); } + @Test + public void testPlay_localAsset() throws IOException { + mMediaSources = mock(MediaSources.class); + MediaSources.MediaSource mediaSource = mock(MediaSources.MediaSource.class); + when(mediaSource.url()).thenReturn("file:///android_asset/sample.mp4"); + when(mediaSource.headers()).thenReturn(Collections.emptyMap()); + + when(mMediaSources.size()).thenReturn(1); + when(mMediaSources.at(anyInt())).thenReturn(mediaSource); + + initExpectedStatesForPlay(); + mAplMediaPlayer.setMediaSources(mMediaSources); + testPlayInternal(); + + verify(mAssetManager).openFd("sample.mp4"); + } + /** * Covers all the testPlay_XXXX scenarios defined in this class. */ diff --git a/apl/src/test/java/com/amazon/apl/android/document/ContentTest.java b/apl/src/test/java/com/amazon/apl/android/document/ContentTest.java index 06b304db..d5b4d68e 100644 --- a/apl/src/test/java/com/amazon/apl/android/document/ContentTest.java +++ b/apl/src/test/java/com/amazon/apl/android/document/ContentTest.java @@ -21,6 +21,8 @@ import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; import com.amazon.apl.android.scaling.ViewportMetrics; +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.GradientType; import com.amazon.apl.enums.ScreenShape; import com.amazon.apl.enums.ViewportMode; @@ -53,6 +55,8 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyLong; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; @@ -60,6 +64,7 @@ import static org.mockito.Mockito.reset; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; public class ContentTest extends ViewhostRobolectricTest { @@ -130,6 +135,23 @@ public class ContentTest extends ViewhostRobolectricTest { " }" + "}"; + private final String mTestImportDocWithCustomSource = "{" + + " \"type\": \"APL\"," + + " \"version\": \"1.0\"," + + " \"import\": [" + + " {\n" + + " \"name\": \"test-package2\"," + + " \"version\": \"1.0\"," + + " \"source\": \"file://sample_test\"" + + " }" + + " ]," + + " \"mainTemplate\": {" + + " \"item\": {" + + " \"type\": \"Text\"" + + " }" + + " }" + + "}"; + private final String mTestPackage2 = "{" + " \"type\": \"APL\"," + " \"version\": \"1.0\"," + @@ -257,6 +279,8 @@ public class ContentTest extends ViewhostRobolectricTest { IContentDataRetriever mDataRetriever; @Mock private Session mSession; + @Mock + private IDTNetworkRequestHandler mDTNetworkRequestHandler; @Before @@ -574,7 +598,7 @@ public void onComplete(Content content) { public void onError(Exception e) { Assert.fail(e.getCause().getMessage()); } - }, mSession); + }, mSession, mDTNetworkRequestHandler); assertNotNull("Content should not be null.", content); try { @@ -593,6 +617,85 @@ public void onError(Exception e) { verify(mPackageLoader, times(2)).fetch(any(), any(), any()); } + @Test + public void testDocImport_onFailureToImportPkg_failureCallbackIsTriggered() { + doAnswer(invocation -> { + ImportRequest request = invocation.getArgument(0); + IContentRetriever.FailureCallback failCallback = invocation.getArgument(2); + failCallback.onFailure(request, "ExpectedFailure"); + return null; + }).when(mPackageLoader).fetch(any(), any(), any()); + Content.create(mTestImportDoc, mAplOptions, new Content.CallbackV2() { + @Override + public void onComplete(Content content) { + super.onComplete(content); + } + + @Override + public void onError(Exception e) { + Assert.fail(e.getCause().getMessage()); + } + }, mSession, mDTNetworkRequestHandler); + + verify(mDTNetworkRequestHandler).requestWillBeSent(anyInt(), anyDouble(), any(), eq(DTNetworkRequestType.PACKAGE)); + verify(mDTNetworkRequestHandler).loadingFailed(anyInt(), anyDouble()); + } + + /** + * Test DTNetworkRequestHandler that the default URL source is used on a empty source. + */ + @Test + public void testDTNetworkRequestHandler_onPackageImportWithEmptySource_defaultSourceIsUsed() { + String expectedURL = "https://arl.assets.apl-alexa.com/packages/test-package2/1.0/document.json"; + doAnswer(invocation -> { + ImportRequest request = invocation.getArgument(0); + IContentRetriever.SuccessCallback successCallback = invocation.getArgument(1); + successCallback.onSuccess(request, APLJSONData.create(mTestPackage)); + return null; + }).when(mPackageLoader).fetch(any(), any(), any()); + Content.create(mTestImportDoc, mAplOptions, new Content.CallbackV2() { + @Override + public void onComplete(Content content) { + super.onComplete(content); + } + + @Override + public void onError(Exception e) { + Assert.fail(e.getCause().getMessage()); + } + }, mSession, mDTNetworkRequestHandler); + + verify(mDTNetworkRequestHandler).requestWillBeSent(anyInt(), anyDouble(), eq(expectedURL), eq(DTNetworkRequestType.PACKAGE)); + verify(mDTNetworkRequestHandler).loadingFinished(anyInt(), anyDouble(), anyInt()); + } + + /** + * Test DTNetworkRequestHandler that no events triggered when the source is a non-url source. + */ + @Test + public void testDTNetworkRequestHandler_onPackageImportWithNonUrlSource_noDTNetworkEventOccur() { + doAnswer(invocation -> { + ImportRequest request = invocation.getArgument(0); + IContentRetriever.SuccessCallback successCallback = invocation.getArgument(1); + successCallback.onSuccess(request, APLJSONData.create(mTestPackage)); + return null; + }).when(mPackageLoader).fetch(any(), any(), any()); + Content.create(mTestImportDocWithCustomSource, mAplOptions, new Content.CallbackV2() { + @Override + public void onComplete(Content content) { + super.onComplete(content); + } + + @Override + public void onError(Exception e) { + fail(e.getMessage()); + } + }, mSession, mDTNetworkRequestHandler); + + verify(mPackageLoader).fetch(any(), any(), any()); + verifyNoInteractions(mDTNetworkRequestHandler); + } + @Test public void testDocument_dataSources_callbackV2() { doAnswer(invocation -> { @@ -613,7 +716,7 @@ public void onComplete(Content content) { completeCalled.set(true); super.onComplete(content); } - }, mSession); + }, mSession, mDTNetworkRequestHandler); assertNotNull("Content should not be null.", content); InstrumentationRegistry.getInstrumentation().waitForIdleSync(); @@ -1200,7 +1303,7 @@ public void onPackageLoaded(Content content) { public void onError(Exception e) { Assert.fail(e.getCause().getMessage()); } - }, mSession); + }, mSession, mDTNetworkRequestHandler); assertNotNull("Content should not be null.", content); try { diff --git a/apl/src/test/java/com/amazon/apl/android/event/ReinflateEventTest.java b/apl/src/test/java/com/amazon/apl/android/event/ReinflateEventTest.java index 82101a81..ea606a85 100644 --- a/apl/src/test/java/com/amazon/apl/android/event/ReinflateEventTest.java +++ b/apl/src/test/java/com/amazon/apl/android/event/ReinflateEventTest.java @@ -26,6 +26,7 @@ import com.amazon.apl.android.providers.AbstractMediaPlayerProvider; import com.amazon.apl.android.scaling.ViewportMetrics; import com.amazon.apl.android.utils.APLTrace; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.amazon.apl.enums.ScreenShape; import com.amazon.apl.enums.ViewportMode; @@ -69,6 +70,8 @@ public class ReinflateEventTest extends AbstractDocUnitTest { private IPackageLoader mPackageLoader; @Mock private ExtensionMediator mMediator; + @Mock + private IDTNetworkRequestHandler mDTNetworkRequestHandler; private static final String REINFLATE_DOC = "{\n" + " \"type\": \"APL\",\n" + @@ -228,7 +231,7 @@ public void onComplete(Content content) { contentComplete.countDown(); } - }, mRootConfig); + }, mRootConfig, mDTNetworkRequestHandler); Assert.assertTrue(contentComplete.await(1, TimeUnit.SECONDS)); } @Test diff --git a/apl/src/test/java/com/amazon/apl/android/extension/ExtensionEventTest.java b/apl/src/test/java/com/amazon/apl/android/extension/ExtensionEventTest.java index 7d9b1935..ba8d83d8 100644 --- a/apl/src/test/java/com/amazon/apl/android/extension/ExtensionEventTest.java +++ b/apl/src/test/java/com/amazon/apl/android/extension/ExtensionEventTest.java @@ -801,13 +801,12 @@ public void testExtension_WithSimpleConfig() { */ @Test @SmallTest - public void testExtension_WithSimpleConfigAndFlags() { - testFlagsOfString(); - testFlagsOfMap(); - } - - private void testFlagsOfString() { + public void testExtension_WithFlagsOfString() { mRootConfig = mTestContext.buildRootConfig() + .registerExtension("_URIXdefault") + .registerExtensionEnvironment("_URIXbool", true) + .registerExtensionEnvironment("_URIXstring", "dog") + .registerExtensionEnvironment("_URIXnumber", 64) .registerExtensionFlags("_URIXdefault", "simpleFlagString"); loadDocument(WITH_CONFIG); @@ -816,13 +815,19 @@ private void testFlagsOfString() { } - private void testFlagsOfMap() { + @Test + @SmallTest + public void testExtension_WithFlagsOfMap() { Map map = new HashMap<>(); map.put("key1", 1); map.put("key2", true); map.put("key3", "three"); mRootConfig = mTestContext.buildRootConfig() + .registerExtension("_URIXdefault") + .registerExtensionEnvironment("_URIXbool", true) + .registerExtensionEnvironment("_URIXstring", "dog") + .registerExtensionEnvironment("_URIXnumber", 64) .registerExtensionFlags("_URIXdefault", map); loadDocument(WITH_CONFIG); diff --git a/apl/src/test/java/com/amazon/apl/android/font/CompatFontResolverParametrizedTest.java b/apl/src/test/java/com/amazon/apl/android/font/CompatFontResolverParametrizedTest.java deleted file mode 100644 index dc20715c..00000000 --- a/apl/src/test/java/com/amazon/apl/android/font/CompatFontResolverParametrizedTest.java +++ /dev/null @@ -1,70 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.android.font; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.junit.runner.RunWith; -import org.junit.runners.Parameterized; - -import java.util.Arrays; -import java.util.Collection; - -@RunWith(Parameterized.class) -public class CompatFontResolverParametrizedTest { - - private final String family; - private final int weight; - private final boolean italic; - private final String language; - private final String expectedResult; - - private CompatFontResolver mCompatFontResolver; - - @Parameterized.Parameters(name = "{index}: {0} {1} italic:{2} lang:{3} == {4}") - public static Collection fontFamilies() { - return Arrays.asList(new Object[][] { - { "Bookerly", 100, false, null, "Bookerly-Regular.ttf" }, - { "Bookerly", 100, false, "en-US", "Bookerly-Regular.ttf"}, - { "Bookerly", 100, false, "en-CA", "Bookerly-Regular.ttf"}, - { "bookerly", 100, true, null, "Bookerly-RegularItalic.ttf"}, - { "bookerly", 900, true, null, "Bookerly-BoldItalic.ttf"}, - { "amazon-ember-display", 900, true, null, "Amazon-Ember-BoldItalic.ttf"}, - { "Amazon Ember Display", 400, false, null, "AmazonEmberDisplay_Rg.ttf"}, - { "amazon_ember", 700, false, null, "AmazonEmberDisplay_Bd.ttf"}, - { "sans-serif", 400, false, "en-US", "AmazonEmberDisplay_Rg.ttf"}, - { "sans-serif", 400, false, "fr-FR", "AmazonEmberDisplay_Rg.ttf"}, - { "nonexistent", 123, true, "aa-ZZ", null}, - { "nonexistent", 0, false, null, null}, - }); - } - - public CompatFontResolverParametrizedTest(String family, int weight, boolean italic, String language, String expectedResult) { - this.family = family; - this.weight = weight; - this.italic = italic; - this.language = language; - this.expectedResult = expectedResult; - } - @Before - public void setUp() { - mCompatFontResolver = new CompatFontResolver(); - } - - @Test - public void test() { - FontKey.Builder builder = FontKey.build(family, weight).italic(italic); - if (language != null) { - builder.language(language); - } - FontKey key = builder.build(); - String fileName = mCompatFontResolver.getMatchingFontFileName(key); - - Assert.assertEquals(expectedResult, fileName); - - } -} diff --git a/apl/src/test/java/com/amazon/apl/android/font/CompatFontResolverTest.java b/apl/src/test/java/com/amazon/apl/android/font/CompatFontResolverTest.java deleted file mode 100644 index 163fcc49..00000000 --- a/apl/src/test/java/com/amazon/apl/android/font/CompatFontResolverTest.java +++ /dev/null @@ -1,66 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.android.font; - -import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; -import org.junit.Before; -import org.junit.Test; - -import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertNotNull; - -public class CompatFontResolverTest extends ViewhostRobolectricTest { - - private CompatFontResolver mCompatFontResolver; - - @Before - public void setUp() { - mCompatFontResolver = new CompatFontResolver(); - } - - @Test - public void testGetMatchingFontFilePath_WithBookerlyFamily() { - FontKey key1 = FontKey.build("bookerly", 100).italic(true).build(); - String fileName1 = mCompatFontResolver.getMatchingFontFileName(key1); - - - FontKey key2 = FontKey.build("Bookerly", 100).italic(true).build(); - String fileName2 = mCompatFontResolver.getMatchingFontFileName(key2); - - // Verify the path - assertEquals(fileName1, "Bookerly-RegularItalic.ttf"); - assertEquals(fileName2, "Bookerly-RegularItalic.ttf"); - } - - @Test - public void testGetMatchingFontFilePath_WithAmazonEmberFamily() { - FontKey key1 = FontKey.build("amazon-ember-display", 100).build(); - String fileName1 = mCompatFontResolver.getMatchingFontFileName(key1); - - - FontKey key2 = FontKey.build("amazon_ember", 100).build(); - String fileName2 = mCompatFontResolver.getMatchingFontFileName(key2); - - // Verify the path - assertEquals(fileName1, "AmazonEmberDisplay_Lt.ttf"); - assertEquals(fileName2, "AmazonEmberDisplay_Lt.ttf"); - } - - @Test - public void testGetMatchingFontFilePath_withLanguage_correctlyResolved() { - String[] fontFamilies = { "Bookerly", "Amazon Ember Display" }; - - for (String family: fontFamilies) { - FontKey keyWithLanguage = FontKey.build(family, 400).italic(true).language("en-US").build(); - FontKey keyWithoutLanguage = FontKey.build(family, 400).italic(true).build(); - String fileNameWithLanguage = mCompatFontResolver.getMatchingFontFileName(keyWithLanguage); - String fileNameWithoutLanguage = mCompatFontResolver.getMatchingFontFileName(keyWithoutLanguage); - - assertNotNull(fileNameWithLanguage); - assertEquals(fileNameWithLanguage, fileNameWithoutLanguage); - } - } -} diff --git a/apl/src/test/java/com/amazon/apl/android/font/TypefaceResolverText.java b/apl/src/test/java/com/amazon/apl/android/font/TypefaceResolverText.java deleted file mode 100644 index b5bd4e53..00000000 --- a/apl/src/test/java/com/amazon/apl/android/font/TypefaceResolverText.java +++ /dev/null @@ -1,54 +0,0 @@ -/* - * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -package com.amazon.apl.android.font; - -import android.graphics.Typeface; - -import com.amazon.apl.android.RuntimeConfig; -import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; - -import org.junit.Assert; -import org.junit.Before; -import org.junit.Test; -import org.mockito.Mock; - -public class TypefaceResolverText extends ViewhostRobolectricTest { - - private static final String DEFAULT_FONT_FAMILY = "bookerly"; - - @Mock - private IFontResolver fontResolver; - - @Before - public void resetTypefaceResolver() { - TypefaceResolver.getInstance().reset(); - } - - @Test - public void testDisabledEmbeddedFontLookup() { - RuntimeConfig runtimeConfig = RuntimeConfig.builder() - .fontResolver(fontResolver) - .embeddedFontResolverEnabled(false) - .build(); - TypefaceResolver.getInstance().initialize(getApplication(), runtimeConfig); - - Typeface face = TypefaceResolver.getInstance().getTypeface(DEFAULT_FONT_FAMILY, 100, false, "", false); - - // This should be a default constructed font. - Assert.assertEquals(Typeface.SANS_SERIF, face); - } - - @Test - public void testEmbeddedFontLookup() { - RuntimeConfig runtimeConfig = RuntimeConfig.builder() - .fontResolver(fontResolver) - .build(); - TypefaceResolver.getInstance().initialize(getApplication(), runtimeConfig); - - Typeface face = TypefaceResolver.getInstance().getTypeface(DEFAULT_FONT_FAMILY, 100, false, "", false); - Assert.assertNotEquals(Typeface.SANS_SERIF, face); - } -} diff --git a/apl/src/test/java/com/amazon/apl/android/graphic/APLVectorGraphicViewTest.java b/apl/src/test/java/com/amazon/apl/android/graphic/APLVectorGraphicViewTest.java index fc0936c9..6d1ed503 100644 --- a/apl/src/test/java/com/amazon/apl/android/graphic/APLVectorGraphicViewTest.java +++ b/apl/src/test/java/com/amazon/apl/android/graphic/APLVectorGraphicViewTest.java @@ -8,9 +8,7 @@ import static android.view.View.LAYER_TYPE_NONE; import android.graphics.Bitmap; -import android.graphics.BitmapFactory; import android.graphics.drawable.Drawable; -import android.os.Build; import android.util.Pair; import com.amazon.apl.android.IAPLViewPresenter; @@ -26,9 +24,9 @@ import org.junit.Test; import org.mockito.Mock; import org.mockito.Spy; -import org.robolectric.annotation.Config; import static junit.framework.Assert.assertEquals; +import static junit.framework.Assert.assertFalse; import static junit.framework.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; import static org.mockito.ArgumentMatchers.anyInt; @@ -61,6 +59,21 @@ public void setUp() { mAPLVectorGraphicView.setFrame(0, 0, VIEW_SIZE, VIEW_SIZE); mAPLVectorGraphicView.setPadding(VIEW_PADDING, VIEW_PADDING, VIEW_PADDING, VIEW_PADDING); } + + @Test + public void testHardwareAcceleration_disabled_withoutFlag() { + assertFalse(mMockPresenter.isHardwareAccelerationForVectorGraphicsEnabled()); + assertEquals(LAYER_TYPE_NONE, mAPLVectorGraphicView.getLayerType()); + } + + @Test + public void testHardwareAcceleration_enabled_withFlag() { + when(mMockPresenter.isHardwareAccelerationForVectorGraphicsEnabled()).thenReturn(true); + mAPLVectorGraphicView = spy(new APLVectorGraphicView(ViewhostRobolectricTest.getApplication().getApplicationContext(), mMockPresenter)); + assertTrue(mMockPresenter.isHardwareAccelerationForVectorGraphicsEnabled()); + assertEquals(LAYER_TYPE_HARDWARE, mAPLVectorGraphicView.getLayerType()); + } + @Test public void testOnDraw_intrinsicDrawableFits(){ when(mMockBitmapPool.get(anyInt(), anyInt(), any())).thenReturn(Bitmap.createBitmap(VIEW_INNER_SIZE, VIEW_INNER_SIZE, Bitmap.Config.ARGB_8888)); diff --git a/apl/src/test/java/com/amazon/apl/android/graphic/AlexaVectorDrawableTest.java b/apl/src/test/java/com/amazon/apl/android/graphic/AlexaVectorDrawableTest.java index ea7cfc85..7dcebadc 100644 --- a/apl/src/test/java/com/amazon/apl/android/graphic/AlexaVectorDrawableTest.java +++ b/apl/src/test/java/com/amazon/apl/android/graphic/AlexaVectorDrawableTest.java @@ -16,6 +16,7 @@ import android.graphics.Canvas; import android.graphics.Rect; +import com.amazon.apl.android.RenderingContext; import com.amazon.apl.android.bitmap.BitmapCreationException; import com.amazon.apl.android.bitmap.IBitmapFactory; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; @@ -71,71 +72,59 @@ public void test_updateDirtyGraphics_updatesViewport() { verify(mPathRenderer).applyBaseAndViewportDimensions(); } - @Test - public void test_uniform_scaling_draws_on_canvas(){ - VectorGraphicScale[] scales = {VectorGraphicScale.kVectorGraphicScaleBestFill, VectorGraphicScale.kVectorGraphicScaleNone, VectorGraphicScale.kVectorGraphicScaleBestFit}; - for (VectorGraphicScale scale : scales) { - when(mGraphicContainerElement.doesMapContainNonUniformScaling()).thenReturn(false); - mAlexaVectorDrawable.setScale(scale); - mAlexaVectorDrawable.setBounds(0, 0, 10, 10); - mAlexaVectorDrawable.draw(mockCanvas); - ArgumentCaptor captor = ArgumentCaptor.forClass(Rect.class); - verify(mockCanvas, atLeast(1)).clipRect(captor.capture()); - verify(mPathRenderer, atLeast(1)).draw(mockCanvas, 10, 10, mBitmapFactory, true); - Rect rect = captor.getValue(); - assertEquals(0, rect.left); - assertEquals(0, rect.top); - assertEquals(10, rect.right); - assertEquals(10, rect.bottom); - verifyNoInteractions(mBitmapFactory); - } - } - - public void test_fill_type_draws_on_bitmap() throws BitmapCreationException{ - VectorGraphicScale scale = VectorGraphicScale.kVectorGraphicScaleFill; + public void test_draw_hardwareAcceleration_unset_draws_on_bitmap() throws BitmapCreationException{ GraphicContainerElement graphicContainerElement = mock(GraphicContainerElement.class); PathRenderer pathRenderer = mock(PathRenderer.class); when(pathRenderer.getRootGroup()).thenReturn(graphicContainerElement); - mAlexaVectorDrawable.setScale(scale); mAlexaVectorDrawable.setBounds(0, 0, 10, 10); - mAlexaVectorDrawable.draw(mockCanvas); - verify(mBitmapFactory).createBitmap(10, 10); - } - @Test - public void test_non_uniform_scaling_draws_on_bitmap() throws BitmapCreationException{ - when(mGraphicContainerElement.doesMapContainNonUniformScaling()).thenReturn(true); - GraphicContainerElement graphicContainerElement = mock(GraphicContainerElement.class); - PathRenderer pathRenderer = mock(PathRenderer.class); - when(pathRenderer.getRootGroup()).thenReturn(graphicContainerElement); - mAlexaVectorDrawable.setBounds(0, 0, 10, 10); + // draw mAlexaVectorDrawable.draw(mockCanvas); // verify that bitmap was created instead of drawing straight into the canvas verify(mBitmapFactory).createBitmap(10, 10); } @Test - public void test_skew_draws_on_bitmap() throws BitmapCreationException{ - when(mGraphicContainerElement.doesMapContainsSkew()).thenReturn(true); + public void test_draw_hardwareAcceleration_disabled_draws_on_bitmap() throws BitmapCreationException{ + RenderingContext rc = RenderingContext.builder() + .isHardwareAccelerationForVectorGraphicsEnabled(false) + .build(); + when(mGraphicContainerElement.getRenderingContext()).thenReturn(rc); GraphicContainerElement graphicContainerElement = mock(GraphicContainerElement.class); PathRenderer pathRenderer = mock(PathRenderer.class); when(pathRenderer.getRootGroup()).thenReturn(graphicContainerElement); mAlexaVectorDrawable.setBounds(0, 0, 10, 10); + + // draw mAlexaVectorDrawable.draw(mockCanvas); // verify that bitmap was created instead of drawing straight into the canvas verify(mBitmapFactory).createBitmap(10, 10); } @Test - public void test_drop_shadow_present_draws_on_bitmap() throws BitmapCreationException{ - GraphicContainerElement graphicContainerElement = mock(GraphicContainerElement.class); - PathRenderer pathRenderer = mock(PathRenderer.class); - when(pathRenderer.getRootGroup()).thenReturn(graphicContainerElement); - when(mGraphicContainerElement.doesMapContainFilters()).thenReturn(true); + public void test_draw_hardwareAcceleration_enabled_draws_on_canvas() throws BitmapCreationException { + RenderingContext rc = RenderingContext.builder() + .isHardwareAccelerationForVectorGraphicsEnabled(true) + .build(); + when(mGraphicContainerElement.getRenderingContext()).thenReturn(rc); + when(mPathRenderer.getRootGroup()).thenReturn(mGraphicContainerElement); + when(mBitmapFactory.createBitmap(10, 10)).thenReturn(mBitmap); + mVectorState = new AlexaVectorDrawable.VectorDrawableCompatState(mPathRenderer, mBitmapFactory); + mAlexaVectorDrawable = new AlexaVectorDrawable(mVectorState); mAlexaVectorDrawable.setBounds(0, 0, 10, 10); + + // draw mAlexaVectorDrawable.draw(mockCanvas); - // verify that bitmap was created instead of drawing straight into the canvas - verify(mBitmapFactory).createBitmap(10, 10); + + ArgumentCaptor captor = ArgumentCaptor.forClass(Rect.class); + verify(mockCanvas, atLeast(1)).clipRect(captor.capture()); + verify(mPathRenderer, atLeast(1)).draw(mockCanvas, 10, 10, mBitmapFactory, true); + Rect rect = captor.getValue(); + assertEquals(0, rect.left); + assertEquals(0, rect.top); + assertEquals(10, rect.right); + assertEquals(10, rect.bottom); + verifyNoInteractions(mBitmapFactory); } } diff --git a/apl/src/test/java/com/amazon/apl/android/utils/LazyImageLoaderTest.java b/apl/src/test/java/com/amazon/apl/android/utils/LazyImageLoaderTest.java index b05613bb..e3d1cbce 100644 --- a/apl/src/test/java/com/amazon/apl/android/utils/LazyImageLoaderTest.java +++ b/apl/src/test/java/com/amazon/apl/android/utils/LazyImageLoaderTest.java @@ -14,6 +14,7 @@ import com.amazon.apl.android.primitive.UrlRequests; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; import com.amazon.apl.android.views.APLImageView; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; import com.bumptech.glide.load.engine.GlideException; import org.junit.Before; @@ -26,13 +27,15 @@ import java.util.List; import static org.junit.Assert.assertEquals; -import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; import static org.mockito.ArgumentMatchers.anyString; import static org.mockito.ArgumentMatchers.eq; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoInteractions; import static org.mockito.Mockito.when; public class LazyImageLoaderTest extends ViewhostRobolectricTest { @@ -46,6 +49,8 @@ public class LazyImageLoaderTest extends ViewhostRobolectricTest { private ImageViewAdapter mImageViewAdapter; @Mock private IImageLoader mImageLoader; + @Mock + private IDTNetworkRequestHandler mDTNetworkRequest; private APLImageView mImageView; @@ -73,10 +78,11 @@ public void initImageLoading_onSuccess() { }).when(mImageLoader).loadImage(any(IImageLoader.LoadImageParams.class)); // When - LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView); + LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView, mDTNetworkRequest); // Then verify(mockPresenter).mediaLoaded(mSource.url()); + verify(mDTNetworkRequest).loadingFinished(anyInt(), anyDouble(), anyInt()); } @Test @@ -100,7 +106,7 @@ public void initImageLoading_multipleSourcesLoadsBitmaps() { }).when(mImageLoader).loadImage(any(IImageLoader.LoadImageParams.class)); // When - LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView); + LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView, mDTNetworkRequest); // Then ArgumentCaptor> bitmapCaptor = ArgumentCaptor.forClass(List.class); @@ -122,7 +128,7 @@ public void initImageLoading_handlesMultipleCallbacks() { }).when(mImageLoader).loadImage(any(IImageLoader.LoadImageParams.class)); // When - LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView); + LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView, mDTNetworkRequest); // Then verify(mImageViewAdapter, times(1)).onImageLoad(any(), any()); @@ -141,11 +147,12 @@ public void initImageLoading_onError() { }).when(mImageLoader).loadImage(any(IImageLoader.LoadImageParams.class)); // When - LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView); + LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView, mDTNetworkRequest); // Then verify(mockPresenter).mediaLoadFailed(mSource.url(), errorCode, message); verify(mImageViewAdapter).onImageLoad(any(), any()); + verify(mDTNetworkRequest).loadingFailed(anyInt(), anyDouble()); } @Test @@ -159,9 +166,35 @@ public void initImageLoading_onError_nullException() { }).when(mImageLoader).loadImage(any(IImageLoader.LoadImageParams.class)); // When - LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView); + LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView, mDTNetworkRequest); // Then verify(mockPresenter).mediaLoadFailed(eq(mSource.url()), eq(errorCode), anyString()); + verify(mDTNetworkRequest).loadingFailed(anyInt(), anyDouble()); + } + + @Test + public void initImageLoading_onFileUrl_successfulLoadingWithNoDTNetworkInteraction() { + // Given + UrlRequests.UrlRequest source = UrlRequests.UrlRequest.builder() + .url("File:android_asset//test.png") + .headers(Collections.singletonMap("key", "value")) + .build(); + List sources = new ArrayList<>(); + sources.add(source); + when(mImage.getSourceRequests()).thenReturn(sources); + + doAnswer(invocation -> { + IImageLoader.LoadImageParams load = invocation.getArgument(0); + load.callback().onSuccess(BITMAP, load.path()); + return null; + }).when(mImageLoader).loadImage(any(IImageLoader.LoadImageParams.class)); + + // When + LazyImageLoader.initImageLoad(mImageViewAdapter, mImage, mImageView, mDTNetworkRequest); + + // Then + verify(mockPresenter).mediaLoaded(anyString()); + verifyNoInteractions(mDTNetworkRequest); } } diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/ViewTypeTargetTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/ViewTypeTargetTest.java new file mode 100644 index 00000000..b7c0d2b2 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/ViewTypeTargetTest.java @@ -0,0 +1,79 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ +package com.amazon.apl.devtools.models; + +import static org.junit.Assert.assertEquals; +import static org.mockito.Mockito.any; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.when; + +import com.amazon.apl.devtools.models.log.LogEntry; +import com.amazon.apl.devtools.models.log.LogEntryAddedEvent; + +import org.junit.Before; +import org.junit.Test; +import org.junit.runner.RunWith; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; +import org.robolectric.RobolectricTestRunner; +import org.robolectric.annotation.Config; + +import java.util.List; + +@RunWith(RobolectricTestRunner.class) +@Config(sdk = 32) +public class ViewTypeTargetTest { + @Mock + private Session mockSession; + private ViewTypeTarget viewTypeTarget; + + @Before + public void setUp() { + MockitoAnnotations.initMocks(this); + when(mockSession.isLogEnabled()).thenReturn(true); + viewTypeTarget = new ViewTypeTarget("mockName"); + viewTypeTarget.registerSession(mockSession); + } + + @Test + public void testOnLogEntryAdded() { + com.amazon.apl.android.Session.LogEntryLevel level = com.amazon.apl.android.Session.LogEntryLevel.INFO; + com.amazon.apl.android.Session.LogEntrySource source = com.amazon.apl.android.Session.LogEntrySource.COMMAND; + String messageText = "Test log message"; + double timestamp = 123456789.0; + Object[] arguments = {"arg1", "arg2"}; + + viewTypeTarget.onLogEntryAdded(level, source, messageText, timestamp, arguments); + + // Verify that the logEntries list is updated + List logEntries = viewTypeTarget.getLogEntries(); + assertEquals(1, logEntries.size()); + assertEquals(level, logEntries.get(0).getLevel()); + assertEquals(source, logEntries.get(0).getSource()); + assertEquals(messageText, logEntries.get(0).getText()); + assertEquals(timestamp, logEntries.get(0).getTimestamp(), 0.0); + + // Verify that the sendEvent method is called for the registered session + verify(mockSession, times(1)).sendEvent(any(LogEntryAddedEvent.class)); + } + + @Test + public void testClearLog() { + com.amazon.apl.android.Session.LogEntryLevel level = com.amazon.apl.android.Session.LogEntryLevel.INFO; + com.amazon.apl.android.Session.LogEntrySource source = com.amazon.apl.android.Session.LogEntrySource.COMMAND; + String messageText = "Test log message"; + double timestamp = 123456789.0; + Object[] arguments = {"arg1", "arg2"}; + LogEntry logEntry = new LogEntry(level, source, messageText, timestamp, arguments); + viewTypeTarget.getLogEntries().add(logEntry); + + viewTypeTarget.clearLog(); + + // Verify that the logEntries list is cleared + assertEquals(0, viewTypeTarget.getLogEntries().size()); + } +} + diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsCommandUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsCommandUnitTest.java new file mode 100644 index 00000000..c9aa9acc --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/frameMetrics/FrameMetricsCommandUnitTest.java @@ -0,0 +1,60 @@ +package com.amazon.apl.devtools.models.frameMetrics; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class FrameMetricsCommandUnitTest { + + @Test + public void testFrameMetricsRecord() { + try { + FrameMetricsRecordCommandResponse response = new FrameMetricsRecordCommandResponse(100, "Session100"); + JSONObject object = response.toJSONObject(); + assertTrue(object.has("id")); + assertEquals(object.getInt("id"), 100); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void testFrameMetricsStop() { + try { + List framestatsList = new ArrayList<>(); + JSONObject framestatsObject = new JSONObject(); + framestatsObject.put("begin", 100); + framestatsObject.put("end", 200); + framestatsList.add(framestatsObject); + FrameMetricsStopCommandResponse response = new FrameMetricsStopCommandResponse(1, "sessionId", framestatsList); + JSONObject object = response.toJSONObject(); + assertTrue(object.has("id")); + assertEquals(object.getInt("id"), 1); + assertTrue(object.has("result")); + JSONObject result = object.getJSONObject("result"); + assertTrue(result.has("value")); + JSONObject value = result.getJSONObject("value"); + assertTrue(value.has("framestats")); + JSONArray framestatsArray = value.getJSONArray("framestats"); + + JSONObject expectedFramestatsObject = new JSONObject(); + expectedFramestatsObject.put("begin", 100); + expectedFramestatsObject.put("end", 200); + JSONArray expectedFramestatsArray = new JSONArray(); + expectedFramestatsArray.put(expectedFramestatsObject); + + // Compare the expected JSONArray with the actual JSONArray + assertEquals(expectedFramestatsArray.toString(), framestatsArray.toString()); + + } catch (Exception e) { + fail(e.getMessage()); + } + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/log/LogEntryAddedEventUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/log/LogEntryAddedEventUnitTest.java new file mode 100644 index 00000000..9da6a496 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/log/LogEntryAddedEventUnitTest.java @@ -0,0 +1,81 @@ +package com.amazon.apl.devtools.models.log; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.apl.android.Session; +import com.amazon.apl.devtools.enums.EventMethod; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +import java.util.ArrayList; +import java.util.List; + +public class LogEntryAddedEventUnitTest { + + @Test + public void testLogEntryAddedEvent() { + String sessionId = "123"; + Session.LogEntryLevel level = Session.LogEntryLevel.INFO; + String messageText = "Test message"; + Session.LogEntrySource source = Session.LogEntrySource.VIEW; + double timestamp = 123456789.0; + Object[] arguments = new Object[]{1}; + + LogEntryAddedEvent logEntryAddedEvent = new LogEntryAddedEvent(sessionId, level, source, messageText, timestamp, arguments); + + assertEquals(EventMethod.LOG_ENTRY_ADDED, logEntryAddedEvent.getMethod()); + assertEquals(sessionId, logEntryAddedEvent.getSessionId()); + + LogEntryAddedEvent.Params params = logEntryAddedEvent.getParams(); + assertEquals(level, params.getLevel()); + assertEquals(messageText, params.getMessageText()); + assertEquals(source, params.getSource()); + assertEquals(timestamp, params.getTimestamp(), 0.0); + + Object[] actualArguments = params.getArguments(); + assertEquals(1, actualArguments.length); + assertEquals(1, actualArguments[0]); + } + + @Test + public void testLogEntryAddedEventToJSONObject() throws Exception { + String sessionId = "123"; + Session.LogEntryLevel level = Session.LogEntryLevel.INFO; + String messageText = "Test message"; + Session.LogEntrySource source = Session.LogEntrySource.VIEW; + double timestamp = 123456789.0; + Object[] arguments = new Object[]{1}; + + LogEntryAddedEvent logEntryAddedEvent = new LogEntryAddedEvent(sessionId, level, source, messageText, timestamp, arguments); + + JSONObject superJsonObject = mock(JSONObject.class); + when(superJsonObject.put(anyString(), any())).thenReturn(superJsonObject); + JSONObject jsonObject = logEntryAddedEvent.toJSONObject(); + + assertEquals(EventMethod.LOG_ENTRY_ADDED.toString(), jsonObject.getString("method")); + assertEquals(sessionId, jsonObject.getString("sessionId")); + + JSONObject paramsJson = jsonObject.getJSONObject("params"); + assertEquals(level.toString().toLowerCase(), paramsJson.getJSONObject("entry").getString("level")); + assertEquals(messageText, paramsJson.getJSONObject("entry").getString("text")); + assertEquals(source.toString().toLowerCase(), paramsJson.getJSONObject("entry").getString("source")); + assertEquals(timestamp, paramsJson.getJSONObject("entry").getDouble("timestamp"), 0.0); + + JSONArray argumentsArray = paramsJson.getJSONObject("entry").getJSONArray("arguments"); + List actualArguments = new ArrayList<>(); + for (int i = 0; i < argumentsArray.length(); i++) { + actualArguments.add(argumentsArray.get(i)); + } + + assertEquals(arguments.length, actualArguments.size()); + for (int i = 0; i < arguments.length; i++) { + assertEquals(arguments[i], actualArguments.get(i)); + } + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandResponseTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandResponseTest.java new file mode 100644 index 00000000..1648c424 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/memory/MemoryGetMemoryCommandResponseTest.java @@ -0,0 +1,42 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.memory; + +import static org.junit.Assert.assertEquals; +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.Mockito.mock; + +import android.os.Debug; + +import org.json.JSONArray; +import org.json.JSONObject; +import org.junit.Test; + +public class MemoryGetMemoryCommandResponseTest { + @Test + public void testCollectionOfProcessMemory() { + try { + Debug.MemoryInfo memoryInfo = mock(Debug.MemoryInfo.class); + MemoryGetMemoryCommandResponse response = new MemoryGetMemoryCommandResponse(100, memoryInfo); + + JSONObject object = response.toJSONObject(); + assertTrue(object.has("id")); + assertEquals(object.getInt("id"), 100); + + assertTrue(object.has("result")); + JSONObject result = object.getJSONObject("result"); + + assertTrue(result.has("total")); + assertTrue(result.has("stats")); + + JSONArray stats = result.getJSONArray("stats"); + assertEquals(stats.length(), 9); + } catch (Exception e) { + fail(e.getMessage()); + } + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/network/DTNetworkRequestHandlerUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/network/DTNetworkRequestHandlerUnitTest.java new file mode 100644 index 00000000..7e912a87 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/network/DTNetworkRequestHandlerUnitTest.java @@ -0,0 +1,84 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.ViewTypeTarget; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.ArgumentCaptor; +import org.mockito.Mock; + +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.times; +import static org.mockito.Mockito.verify; + +public class DTNetworkRequestHandlerUnitTest { + private static final String SAMPLE_URL = "https://sample.com"; + @Mock + private ViewTypeTarget mViewTypeTarget; + + private ArgumentCaptor mTargetArgumentCaptor; + private DTNetworkRequestHandler mDTNetworkRequestHandler; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + mTargetArgumentCaptor = ArgumentCaptor.forClass(Runnable.class); + mDTNetworkRequestHandler = new DTNetworkRequestHandler(mViewTypeTarget); + } + + @Test + public void requestWillBeSent_onJobSchedulesAndExecutes() { + // Given + int requestId = IDTNetworkRequestHandler.IdGenerator.generateId(); + + // When + mDTNetworkRequestHandler.requestWillBeSent(requestId, 0, SAMPLE_URL, DTNetworkRequestType.IMAGE); + + // Then + verify(mViewTypeTarget).post(mTargetArgumentCaptor.capture()); + Runnable job = mTargetArgumentCaptor.getValue(); + job.run(); + verify(mViewTypeTarget,times(1)) + .onNetworkRequestWillBeSent(eq(requestId), anyDouble(), eq(SAMPLE_URL), eq(DTNetworkRequestType.IMAGE.toString())); + } + + @Test + public void loadingFailed_onJobSchedulesAndExecutes() { + // Given + int requestId = IDTNetworkRequestHandler.IdGenerator.generateId(); + + // When + mDTNetworkRequestHandler.loadingFailed(requestId, 0); + + // Then + verify(mViewTypeTarget).post(mTargetArgumentCaptor.capture()); + Runnable job = mTargetArgumentCaptor.getValue(); + job.run(); + verify(mViewTypeTarget,times(1)).onNetworkLoadingFailed(eq(requestId), anyDouble()); + } + + @Test + public void loadingFinished_onJobSchedulesAndExecutes() { + // Given + int requestId = IDTNetworkRequestHandler.IdGenerator.generateId(); + + // When + mDTNetworkRequestHandler.loadingFinished(requestId, 0, 1000); + + // Then + verify(mViewTypeTarget).post(mTargetArgumentCaptor.capture()); + Runnable job = mTargetArgumentCaptor.getValue(); + job.run(); + verify(mViewTypeTarget,times(1)).onNetworkLoadingFinished(eq(requestId), anyDouble(), anyInt()); + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkLoadingFailedEventUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkLoadingFailedEventUnitTest.java new file mode 100644 index 00000000..495375b2 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkLoadingFailedEventUnitTest.java @@ -0,0 +1,63 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.apl.devtools.enums.EventMethod; + +import org.json.JSONObject; +import org.junit.Test; + +public class NetworkLoadingFailedEventUnitTest { + + @Test + public void testNetworkLoadingFailedEvent() { + String sessionId = "123"; + int requestId = 1; + double timestamp = 123456789.0; + + NetworkLoadingFailedEvent networkLoadingFailedEvent = new NetworkLoadingFailedEvent(sessionId, requestId, + timestamp); + + assertEquals(EventMethod.NETWORK_LOADING_FAILED, networkLoadingFailedEvent.getMethod()); + assertEquals(sessionId, networkLoadingFailedEvent.getSessionId()); + + NetworkLoadingFailedEvent.Params params = networkLoadingFailedEvent.getParams(); + + assertEquals(requestId, params.getRequestId()); + assertEquals(timestamp, params.getTimestamp(), 0.0); + } + + @Test + public void testNetworkLoadingFailedEventToJSONObject() throws Exception { + String sessionId = "123"; + int requestId = 1; + double timestamp = 123456789.0; + + NetworkLoadingFailedEvent networkLoadingFailedEvent = new NetworkLoadingFailedEvent(sessionId, requestId, + timestamp); + + assertEquals(EventMethod.NETWORK_LOADING_FAILED, networkLoadingFailedEvent.getMethod()); + assertEquals(sessionId, networkLoadingFailedEvent.getSessionId()); + + JSONObject superJsonObject = mock(JSONObject.class); + when(superJsonObject.put(anyString(), any())).thenReturn(superJsonObject); + JSONObject jsonObject = networkLoadingFailedEvent.toJSONObject(); + + assertEquals(EventMethod.NETWORK_LOADING_FAILED.toString(), jsonObject.getString("method")); + assertEquals(sessionId, jsonObject.getString("sessionId")); + + JSONObject paramsJson = jsonObject.getJSONObject("params"); + + assertEquals(requestId, paramsJson.getInt("requestId")); + assertEquals(timestamp, paramsJson.getDouble("timestamp"), 0.0); + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkLoadingFinishedEventUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkLoadingFinishedEventUnitTest.java new file mode 100644 index 00000000..cf5fc7ce --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkLoadingFinishedEventUnitTest.java @@ -0,0 +1,67 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.apl.devtools.enums.EventMethod; + +import org.json.JSONObject; +import org.junit.Test; + +public class NetworkLoadingFinishedEventUnitTest { + + @Test + public void testNetworkLoadingFinishedEvent() { + String sessionId = "123"; + int requestId = 1; + double timestamp = 123456789.0; + int encodedDataLength = 1; + + NetworkLoadingFinishedEvent networkLoadingFinishedEvent = new NetworkLoadingFinishedEvent(sessionId, requestId, + timestamp, encodedDataLength); + + assertEquals(EventMethod.NETWORK_LOADING_FINISHED, networkLoadingFinishedEvent.getMethod()); + assertEquals(sessionId, networkLoadingFinishedEvent.getSessionId()); + + NetworkLoadingFinishedEvent.Params params = networkLoadingFinishedEvent.getParams(); + + assertEquals(requestId, params.getRequestId()); + assertEquals(timestamp, params.getTimestamp(), 0.0); + assertEquals(encodedDataLength, params.getEncodedDataLength()); + } + + @Test + public void testNetworkLoadingFinishedEventToJSONObject() throws Exception { + String sessionId = "123"; + int requestId = 1; + double timestamp = 123456789.0; + int encodedDataLength = 1; + + NetworkLoadingFinishedEvent networkLoadingFinishedEvent = new NetworkLoadingFinishedEvent(sessionId, requestId, + timestamp, encodedDataLength); + + assertEquals(EventMethod.NETWORK_LOADING_FINISHED, networkLoadingFinishedEvent.getMethod()); + assertEquals(sessionId, networkLoadingFinishedEvent.getSessionId()); + + JSONObject superJsonObject = mock(JSONObject.class); + when(superJsonObject.put(anyString(), any())).thenReturn(superJsonObject); + JSONObject jsonObject = networkLoadingFinishedEvent.toJSONObject(); + + assertEquals(EventMethod.NETWORK_LOADING_FINISHED.toString(), jsonObject.getString("method")); + assertEquals(sessionId, jsonObject.getString("sessionId")); + + JSONObject paramsJson = jsonObject.getJSONObject("params"); + + assertEquals(requestId, paramsJson.getInt("requestId")); + assertEquals(timestamp, paramsJson.getDouble("timestamp"), 0.0); + assertEquals(encodedDataLength, paramsJson.getInt("encodedDataLength")); + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkRequestWillBeSentEventUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkRequestWillBeSentEventUnitTest.java new file mode 100644 index 00000000..8d777a8a --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/models/network/NetworkRequestWillBeSentEventUnitTest.java @@ -0,0 +1,71 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.models.network; + +import static org.junit.Assert.assertEquals; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.anyString; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.apl.devtools.enums.EventMethod; + +import org.json.JSONObject; +import org.junit.Test; + +public class NetworkRequestWillBeSentEventUnitTest { + + @Test + public void testNetworkRequestWillBeSentEvent() { + String sessionId = "123"; + int requestId = 1; + double timestamp = 123456789.0; + String documentURL = "testUrl"; + String type = "testType"; + + NetworkRequestWillBeSentEvent networkRequestWillBeSentEvent = new NetworkRequestWillBeSentEvent(sessionId, requestId, + timestamp, documentURL, type); + + assertEquals(EventMethod.NETWORK_REQUEST_WILL_BE_SENT, networkRequestWillBeSentEvent.getMethod()); + assertEquals(sessionId, networkRequestWillBeSentEvent.getSessionId()); + + NetworkRequestWillBeSentEvent.Params params = networkRequestWillBeSentEvent.getParams(); + + assertEquals(requestId, params.getRequestId()); + assertEquals(timestamp, params.getTimestamp(), 0.0); + assertEquals(documentURL, params.getDocumentURL()); + assertEquals(type, params.getType()); + } + + @Test + public void testNetworkRequestWillBeSentEventToJSONObject() throws Exception { + String sessionId = "123"; + int requestId = 1; + double timestamp = 123456789.0; + String documentURL = "testUrl"; + String type = "testType"; + + NetworkRequestWillBeSentEvent networkRequestWillBeSentEvent = new NetworkRequestWillBeSentEvent(sessionId, requestId, + timestamp, documentURL, type); + + assertEquals(EventMethod.NETWORK_REQUEST_WILL_BE_SENT, networkRequestWillBeSentEvent.getMethod()); + assertEquals(sessionId, networkRequestWillBeSentEvent.getSessionId()); + + JSONObject superJsonObject = mock(JSONObject.class); + when(superJsonObject.put(anyString(), any())).thenReturn(superJsonObject); + JSONObject jsonObject = networkRequestWillBeSentEvent.toJSONObject(); + + assertEquals(EventMethod.NETWORK_REQUEST_WILL_BE_SENT.toString(), jsonObject.getString("method")); + assertEquals(sessionId, jsonObject.getString("sessionId")); + + JSONObject paramsJson = jsonObject.getJSONObject("params"); + + assertEquals(requestId, paramsJson.getInt("requestId")); + assertEquals(timestamp, paramsJson.getDouble("timestamp"), 0.0); + assertEquals(documentURL, paramsJson.getString("documentURL")); + assertEquals(type, paramsJson.getString("type")); + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/util/CommandMethodUtilUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/util/CommandMethodUtilUnitTest.java new file mode 100644 index 00000000..2199eb79 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/util/CommandMethodUtilUnitTest.java @@ -0,0 +1,50 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import static org.junit.Assert.assertEquals; + +import com.amazon.apl.devtools.enums.CommandMethod; + +import org.junit.Before; +import org.junit.Test; + +public class CommandMethodUtilUnitTest { + private CommandMethodUtil mCommandMethodUtil; + + @Before + public void setup() { + mCommandMethodUtil = new CommandMethodUtil(); + } + + @Test + public void parseMethod_fromTargetAttachToTargetText_returnsCorrectly() { + CommandMethod expected = CommandMethod.TARGET_ATTACH_TO_TARGET; + CommandMethod actual = mCommandMethodUtil.parseMethod("Target.attachToTarget"); + assertEquals(expected, actual); + } + + @Test + public void parseMethod_fromViewSetDocumentText_returnsCorrectly() { + CommandMethod expected = CommandMethod.VIEW_SET_DOCUMENT; + CommandMethod actual = mCommandMethodUtil.parseMethod("View.setDocument"); + assertEquals(expected, actual); + } + + @Test + public void parseMethod_fromViewCaptureImageText_returnsCorrectly() { + CommandMethod expected = CommandMethod.VIEW_CAPTURE_IMAGE; + CommandMethod actual = mCommandMethodUtil.parseMethod("View.captureImage"); + assertEquals(expected, actual); + } + + @Test + public void parseMethod_fromUnknownText_returnsCorrectly() { + CommandMethod expected = CommandMethod.EMPTY; + CommandMethod actual = mCommandMethodUtil.parseMethod("Unknown text"); + assertEquals(expected, actual); + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/util/CommandRequestFactoryUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/util/CommandRequestFactoryUnitTest.java new file mode 100644 index 00000000..24f001ea --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/util/CommandRequestFactoryUnitTest.java @@ -0,0 +1,535 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.enums.CommandMethod; +import com.amazon.apl.devtools.executers.DocumentCommandRequest; +import com.amazon.apl.devtools.executers.MemoryGetMemoryCommandRequest; +import com.amazon.apl.devtools.executers.NetworkDisableCommandRequest; +import com.amazon.apl.devtools.executers.NetworkEnableCommandRequest; +import com.amazon.apl.devtools.executers.PerformanceDisableCommandRequest; +import com.amazon.apl.devtools.executers.PerformanceEnableCommandRequest; +import com.amazon.apl.devtools.executers.TargetAttachToTargetCommandRequest; +import com.amazon.apl.devtools.executers.TargetGetTargetsCommandRequest; +import com.amazon.apl.devtools.executers.ViewCaptureImageCommandRequest; +import com.amazon.apl.devtools.executers.ViewExecuteCommandsCommandRequest; +import com.amazon.apl.devtools.executers.ViewSetDocumentCommandRequest; +import com.amazon.apl.devtools.executers.LogClearCommandRequest; +import com.amazon.apl.devtools.executers.LogDisableCommandRequest; +import com.amazon.apl.devtools.executers.LogEnableCommandRequest; +import com.amazon.apl.devtools.models.Session; +import com.amazon.apl.devtools.models.Target; +import com.amazon.apl.devtools.models.common.Request; +import com.amazon.apl.devtools.models.common.Response; +import com.amazon.apl.devtools.models.error.DTException; + +import org.json.JSONArray; +import org.json.JSONException; +import org.json.JSONObject; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + + +public class CommandRequestFactoryUnitTest { + @Mock + private DTConnection mConnection; + + @Mock + private CommandMethodUtil mCommandMethodUtil; + + + private CommandRequestFactory mCommandRequestFactory; + + @Before + public void setup() { + mConnection = mock(DTConnection.class); + mCommandMethodUtil = mock(CommandMethodUtil.class); + TargetCatalog targetCatalog = mock(TargetCatalog.class); + CommandRequestValidator commandRequestValidator = mock(CommandRequestValidator.class); + Target target = mock(Target.class); + Session session = mock(Session.class); + + // These methods are called during validation while constructing a command request object + when(targetCatalog.has(any())).thenReturn(true); + when(targetCatalog.get(any())).thenReturn(target); + when(mConnection.getSession(any())).thenReturn(session); + + mCommandRequestFactory = new CommandRequestFactory(targetCatalog, mCommandMethodUtil, + commandRequestValidator); + } + + @Test + public void createCommandRequest_forTargetGetTargets_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.TARGET_GET_TARGETS.toString()) + .put("id", 100); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.TARGET_GET_TARGETS); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof TargetGetTargetsCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forTargetAttachToTarget_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.TARGET_ATTACH_TO_TARGET.toString()) + .put("id", 100) + .put("params", new JSONObject() + .put("targetId", "target100")); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.TARGET_ATTACH_TO_TARGET); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof TargetAttachToTargetCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forViewSetDocument_documentOnly_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.VIEW_SET_DOCUMENT.toString()) + .put("id", 100) + .put("sessionId", "session100") + .put("params", new JSONObject() + .put("document", new JSONObject())); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.VIEW_SET_DOCUMENT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof ViewSetDocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forViewSetDocument_documentAndDatasources_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.VIEW_SET_DOCUMENT.toString()) + .put("id", 100) + .put("sessionId", "session100") + .put("params", new JSONObject() + .put("document", new JSONObject() + .put("document", new JSONObject()) + .put("datasources", new JSONObject()))); + + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.VIEW_SET_DOCUMENT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof ViewSetDocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forViewSetDocument_renderDocumentDirective_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.VIEW_SET_DOCUMENT.toString()) + .put("id", 100) + .put("sessionId", "session100") + .put("params", new JSONObject() + .put("document", new JSONObject() + .put("name", "RenderDocument") + .put("payload", new JSONObject() + .put("document", new JSONObject()) + .put("datasources", new JSONObject())))); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.VIEW_SET_DOCUMENT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof ViewSetDocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forPerformanceEnable_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.PERFORMANCE_ENABLE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.PERFORMANCE_ENABLE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof PerformanceEnableCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forPerformanceDisable_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.PERFORMANCE_DISABLE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.PERFORMANCE_DISABLE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof PerformanceDisableCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forViewCaptureImage_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.VIEW_CAPTURE_IMAGE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.VIEW_CAPTURE_IMAGE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof ViewCaptureImageCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forViewExecuteCommands_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.VIEW_EXECUTE_COMMANDS.toString()) + .put("id", 100) + .put("sessionId", "session100") + .put("params", new JSONObject() + .put("commands", new JSONArray())); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.VIEW_EXECUTE_COMMANDS); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof ViewExecuteCommandsCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forLogEnable_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.LOG_ENABLE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.LOG_ENABLE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof LogEnableCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forLogDisable_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.LOG_DISABLE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.LOG_DISABLE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof LogDisableCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forLogClear_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.LOG_CLEAR.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.LOG_CLEAR); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof LogClearCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forBadJObject_throws() { + try { + JSONObject obj = new JSONObject(); + mCommandRequestFactory.createCommandRequest(obj, mConnection); + fail("CommandMethod should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof JSONException); + } + } + + @Test + public void createCommandRequest_forUnimplementedMethod_throws() { + try { + JSONObject obj = new JSONObject() + .put("method", "CommandMethod.Empty") + .put("id", 100); + when(mCommandMethodUtil.parseMethod(any())).thenReturn(CommandMethod.EMPTY); + mCommandRequestFactory.createCommandRequest(obj, mConnection); + fail("CommandMethod should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof DTException); + } + } + + @Test + public void createCommandRequest_forMemoryGetMemory_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.MEMORY_GET_MEMORY.toString()) + .put("id", 100); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.MEMORY_GET_MEMORY); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof MemoryGetMemoryCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetMainPackage_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_MAIN_PACKAGE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_MAIN_PACKAGE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetPackageList_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_PACKAGE_LIST.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_PACKAGE_LIST); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetPackage_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_PACKAGE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_PACKAGE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetVisualContext_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_VISUAL_CONTEXT.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_VISUAL_CONTEXT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetDom_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_DOM.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_DOM); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetSceneGraph_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_SCENE_GRAPH.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_SCENE_GRAPH); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetRootContext_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_ROOT_CONTEXT.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_ROOT_CONTEXT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentGetContext_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_GET_CONTEXT.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_GET_CONTEXT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentHighlightComponent_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_HIGHLIGHT_COMPONENT.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_HIGHLIGHT_COMPONENT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forDocumentHideHighlight_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.DOCUMENT_HIDE_HIGHLIGHT.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.DOCUMENT_HIDE_HIGHLIGHT); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof DocumentCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forNetworkEnable_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.NETWORK_ENABLE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.NETWORK_ENABLE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof NetworkEnableCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void createCommandRequest_forNetworkDisable_returnsCorrectRequestObject() { + try { + JSONObject obj = new JSONObject() + .put("method", CommandMethod.NETWORK_DISABLE.toString()) + .put("id", 100) + .put("sessionId", "session100"); + when(mCommandMethodUtil.parseMethod(any())).thenReturn( + CommandMethod.NETWORK_DISABLE); + Request request = mCommandRequestFactory.createCommandRequest(obj, + mConnection); + assertTrue(request instanceof NetworkDisableCommandRequest); + } catch (Exception e) { + fail(e.getMessage()); + } + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/util/CommandRequestValidatorUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/util/CommandRequestValidatorUnitTest.java new file mode 100644 index 00000000..0fc1a575 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/util/CommandRequestValidatorUnitTest.java @@ -0,0 +1,162 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; +import static org.mockito.ArgumentMatchers.any; +import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.when; + +import com.amazon.apl.devtools.controllers.DTConnection; +import com.amazon.apl.devtools.models.Target; +import com.amazon.apl.devtools.models.error.DTException; + +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; + +import java.util.ArrayList; +import java.util.Collection; + +public class CommandRequestValidatorUnitTest { + @Mock + private TargetCatalog mTargetCatalog; + + // Mock for validateBeforeCreatingSession + @Mock + private DTConnection mConnection; + + // Mock for validateBeforeCreatingSession and validateBeforeGettingSession + @Mock + private Target mTarget; + + + private CommandRequestValidator mCommandRequestValidator; + + @Before + public void setup() { + mTargetCatalog = mock(TargetCatalog.class); + + // Setup for validateBeforeCreatingSession + mConnection = mock(DTConnection.class); + mTarget = mock(Target.class); + Collection registeredSessionIds = new ArrayList<>(); + registeredSessionIds.add("target100"); + when(mTarget.getRegisteredSessionIds()).thenReturn(registeredSessionIds); + + mCommandRequestValidator = new CommandRequestValidator(mTargetCatalog); + } + + @Test + public void validateBeforeGettingTargetFromTargetCatalog_whenTargetExists_doesNotThrow() { + when(mTargetCatalog.has(any())).thenReturn(true); + try { + mCommandRequestValidator.validateBeforeGettingTargetFromTargetCatalog(100, + "target100"); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void validateBeforeGettingTargetFromTargetCatalog_whenTargetDoesNotExist_throws() { + when(mTargetCatalog.has(any())).thenReturn(false); + try { + mCommandRequestValidator.validateBeforeGettingTargetFromTargetCatalog(100, + "target100"); + fail("Method should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof DTException); + } + } + + @Test + public void validateBeforeCreatingSession_whenConnectionIsNotAttachedToTarget_doesNotThrow() { + when(mConnection.hasSession(any())).thenReturn(false); + try { + mCommandRequestValidator.validateBeforeCreatingSession(100, mConnection, mTarget); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void validateBeforeCreatingSession_whenConnectionIsAlreadyAttachedToTarget_throws() { + when(mConnection.hasSession(any())).thenReturn(true); + try { + mCommandRequestValidator.validateBeforeCreatingSession(100, mConnection, mTarget); + fail("Method should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof DTException); + } + } + + @Test + public void validateBeforeGettingSession_whenConnectionOwnsSession_doesNotThrow() { + when(mConnection.hasSession(any())).thenReturn(true); + try { + mCommandRequestValidator.validateBeforeGettingSession(100, "session100", + mConnection); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void validateBeforeGettingSession_whenConnectionDoesNotOwnSession_throws() { + when(mConnection.hasSession(any())).thenReturn(false); + try { + mCommandRequestValidator.validateBeforeGettingSession(100, "session100", + mConnection); + fail("Method should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof DTException); + } + } + + @Test + public void validatePerformanceEnabled_whenPerformanceEnabled_doesNotThrows() { + try { + mCommandRequestValidator.validatePerformanceEnabled(100, "session100", + true); + } catch (Exception e) { + fail(e.getMessage()); + } + } + + @Test + public void validatePerformanceEnabled_whenPerformanceDisabled_throws() { + try { + mCommandRequestValidator.validatePerformanceEnabled(100, "session100", + false); + fail("Method should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof DTException); + } + } + + @Test + public void validateLogIsEnabled_whenLogIsDisabled_throws() { + try { + mCommandRequestValidator.validateLogEnabled(100, "session100", + false); + fail("Method should throw exception"); + } catch (Exception e) { + assertTrue(e instanceof DTException); + } + } + + @Test + public void validateLogIsEnabled_whenLogIsEnabled_doesNotThrows() { + try { + mCommandRequestValidator.validateLogEnabled(100, "session100", + true); + } catch (Exception e) { + fail(e.getMessage()); + } + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/util/IdGeneratorUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/util/IdGeneratorUnitTest.java new file mode 100644 index 00000000..34771376 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/util/IdGeneratorUnitTest.java @@ -0,0 +1,34 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +public class IdGeneratorUnitTest { + private IdGenerator mIdGenerator; + + @Before + public void setup() { + mIdGenerator = new IdGenerator(); + } + + @Test + public void generateId_withoutPrefix_returnsCorrectly() { + int expected = 100; + int actual = mIdGenerator.generateId(); + assertEquals(expected, actual); + } + + @Test + public void generateId_withPrefix_returnsCorrectly() { + String expected = "session100"; + String actual = mIdGenerator.generateId("session"); + assertEquals(expected, actual); + } +} diff --git a/apl/src/test/java/com/amazon/apl/devtools/util/MultiViewUtilUnitTest.java b/apl/src/test/java/com/amazon/apl/devtools/util/MultiViewUtilUnitTest.java new file mode 100644 index 00000000..a3646af2 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/devtools/util/MultiViewUtilUnitTest.java @@ -0,0 +1,90 @@ +/* + * Copyright Amazon.com, Inc. or its affiliates. All Rights Reserved + * SPDX-License-Identifier: Apache-2.0 + */ + +package com.amazon.apl.devtools.util; + +import static org.junit.Assert.assertEquals; + +import org.junit.Before; +import org.junit.Test; + +public class MultiViewUtilUnitTest { + private MultiViewUtil mMultiViewUtil; + + @Before + public void setup() { + mMultiViewUtil = new MultiViewUtil(); + } + + @Test + public void computeNumberOfColumns_forNegativeNumberOfViews_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfColumns(-1); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfColumns_forZeroNumberOfViews_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfColumns(0); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfColumns_forOneView_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfColumns(1); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfColumns_forMultipleNumberOfViews_returnsCorrectly() { + int expected = 3; + int actual = mMultiViewUtil.computeNumberOfColumns(5); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfRows_withPerfectDivisors_returnsCorrectly() { + int expected = 2; + int actual = mMultiViewUtil.computeNumberOfRows(6, 3); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfRows_withImperfectDivisors_roundsUp() { + int expected = 3; + int actual = mMultiViewUtil.computeNumberOfRows(7, 3); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfRows_forNegativeNumberOfViews_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfRows(-1, 1); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfRows_forZeroNumberOfViews_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfRows(0, 1); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfRows_forOneView_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfRows(1, 1); + assertEquals(expected, actual); + } + + @Test + public void computeNumberOfRows_forZeroNumberOfColumns_returnsCorrectly() { + int expected = 1; + int actual = mMultiViewUtil.computeNumberOfRows(2, 0); + assertEquals(expected, actual); + } +} diff --git a/apl/src/test/java/com/amazon/apl/viewhost/config/EmbeddedDocumentFactoryTest.java b/apl/src/test/java/com/amazon/apl/viewhost/config/EmbeddedDocumentFactoryTest.java index e891d1e5..95225564 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/config/EmbeddedDocumentFactoryTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/config/EmbeddedDocumentFactoryTest.java @@ -47,6 +47,12 @@ public class EmbeddedDocumentFactoryTest extends ViewhostRobolectricTest { " \"datasources\": {}\n" + " }\n" + "}"; + private static final String RENDER_DOCUMENT_WITHOUT_PAYLOAD = "{\n" + + " \"name\": \"RenderDocument\",\n" + + " \"namespace\": \"Alexa.Presentation.APL\",\n" + + " \"document\": {},\n" + + " \"datasources\": {}\n" + + "}"; private static final String PAYLOAD = "{}"; private static final String NON_JSON_PAYLOAD = "non-json"; private static final String VALID_URL = "http://something.json"; @@ -78,6 +84,20 @@ public void testOnDocumentRequestedSuccessRenderDocumentDirectivePayload() { verify(mEmbeddedDocumentRequest).resolve(mPreparedDocument); } + @Test + public void testOnDocumentRequestedSuccessRenderDocumentDirectiveWithoutPayload() { + when(mEmbeddedDocumentRequest.getSource()).thenReturn(VALID_URL); + when(mViewhost.prepare(any(PrepareDocumentRequest.class))).thenReturn(mPreparedDocument); + doAnswer(invocation -> { + Callback callback = invocation.getArgument(1); + callback.success(RENDER_DOCUMENT_WITHOUT_PAYLOAD); + return null; + }).when(mDataRetriever).fetch(anyString(), any(Callback.class)); + + mDefaultEmbeddedDocumentFactory.onDocumentRequested(mEmbeddedDocumentRequest); + verify(mEmbeddedDocumentRequest).resolve(mPreparedDocument); + } + @Test public void testOnDocumentRequestedSuccessPayload() { when(mEmbeddedDocumentRequest.getSource()).thenReturn(VALID_URL); diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/DTNetworkRequestManagerTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/DTNetworkRequestManagerTest.java new file mode 100644 index 00000000..7ae35d76 --- /dev/null +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/DTNetworkRequestManagerTest.java @@ -0,0 +1,57 @@ +package com.amazon.apl.viewhost.internal; + +import com.amazon.apl.devtools.enums.DTNetworkRequestType; +import com.amazon.apl.devtools.models.network.IDTNetworkRequestHandler; +import org.junit.Before; +import org.junit.Test; +import org.mockito.Mock; +import org.mockito.MockitoAnnotations; + +import static org.mockito.ArgumentMatchers.anyDouble; +import static org.mockito.ArgumentMatchers.anyInt; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.verify; + +public class DTNetworkRequestManagerTest { + + @Mock + private IDTNetworkRequestHandler mDTNetworkRequestHandler; + + private DTNetworkRequestManager mDTNetworkRequestManager; + + @Before + public void setup() { + MockitoAnnotations.openMocks(this); + + mDTNetworkRequestManager = new DTNetworkRequestManager(); + } + + @Test + public void testBindDTNetworkRequest_AllStoredNetworkRequestEventAreReported() { + int requestId = IDTNetworkRequestHandler.IdGenerator.generateId(); + String testUrl = "testUrl"; + mDTNetworkRequestManager.requestWillBeSent(requestId,0, testUrl, DTNetworkRequestType.PACKAGE); + mDTNetworkRequestManager.loadingFinished(requestId, 0, 0); + mDTNetworkRequestManager.loadingFailed(requestId, 0); + + mDTNetworkRequestManager.bindDTNetworkRequest(mDTNetworkRequestHandler); + + verify(mDTNetworkRequestHandler).requestWillBeSent(eq(requestId), anyDouble(),eq(testUrl), eq(DTNetworkRequestType.PACKAGE)); + verify(mDTNetworkRequestHandler).loadingFinished(eq(requestId), anyDouble(), anyInt()); + verify(mDTNetworkRequestHandler).loadingFailed(eq(requestId), anyDouble()); + } + + @Test + public void testWhenAlreadyBindDTNetworkRequest_allNetworkEventsAreRerouted() { + mDTNetworkRequestManager.bindDTNetworkRequest(mDTNetworkRequestHandler); + int requestId = IDTNetworkRequestHandler.IdGenerator.generateId(); + String testUrl = "testUrl"; + mDTNetworkRequestManager.requestWillBeSent(requestId,0, testUrl, DTNetworkRequestType.IMAGE); + mDTNetworkRequestManager.loadingFinished(requestId, 0, 0); + mDTNetworkRequestManager.loadingFailed(requestId, 0); + + verify(mDTNetworkRequestHandler).requestWillBeSent(eq(requestId), anyDouble(),eq(testUrl), eq(DTNetworkRequestType.IMAGE)); + verify(mDTNetworkRequestHandler).loadingFinished(eq(requestId), anyDouble(), anyInt()); + verify(mDTNetworkRequestHandler).loadingFailed(eq(requestId), anyDouble()); + } +} diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentHandleTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentHandleTest.java index 3d6718bd..74e685bf 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentHandleTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentHandleTest.java @@ -8,6 +8,7 @@ import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import static org.junit.Assert.assertTrue; +import static org.junit.Assert.fail; import static org.mockito.ArgumentMatchers.any; import static org.mockito.Mockito.when; @@ -16,9 +17,9 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.amazon.apl.android.Action; +import com.amazon.apl.android.Content; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; import com.amazon.apl.viewhost.DocumentHandle; -import com.amazon.apl.viewhost.primitives.Decodable; import com.amazon.apl.viewhost.primitives.JsonStringDecodable; import com.amazon.apl.viewhost.request.ExecuteCommandsRequest; import com.amazon.apl.viewhost.request.ExecuteCommandsRequest.ExecuteCommandsCallback; @@ -29,6 +30,9 @@ import org.mockito.Mock; import org.mockito.MockitoAnnotations; +import java.util.HashMap; +import java.util.Map; + @RunWith(AndroidJUnit4.class) public class DocumentHandleTest extends ViewhostRobolectricTest { @@ -104,4 +108,59 @@ public void testUserDataHolder() { ((DocumentHandleImpl)mDocumentHandle).setDocumentState(DocumentState.ERROR); assertFalse(mDocumentHandle.setUserData(userData)); } + + @Test + public void testGetContentSetting_nullContent_returnsDefaultValue() { + DocumentHandleImpl handle = (DocumentHandleImpl) mDocumentHandle; + handle.setContent(null); + + String ret = handle.getDocumentSetting("my-property", "fallback"); + assertEquals("fallback", ret); + } + + private static String DOC_SETTINGS = "{" + + " \"type\": \"APL\"," + + " \"version\": \"1.0\"," + + " \"mainTemplate\": {" + + " \"item\": {" + + " \"type\": \"Frame\"," + + " \"backgroundColor\": \"orange\"" + + " }" + + " }," + + " \"settings\": {" + + " \"propertyA\": true," + + " \"propertyB\": 60000," + + " \"propertyC\": \"abc\"," + + " \"subSetting\": {" + + " \"propertyD\": 12.34" + + " }" + + " }" + + "}"; + @Test + public void testGetContentSetting_content_returnsExpectedValue() { + DocumentHandleImpl handle = (DocumentHandleImpl) mDocumentHandle; + try { + Content mContent = Content.create(DOC_SETTINGS); + handle.setContent(mContent); + } catch (Content.ContentException e) { + fail(e.getMessage()); + } + + // Properties existing in Content return expected values + boolean propertyA = handle.getDocumentSetting("propertyA", false); + int propertyB = handle.getDocumentSetting("propertyB", 3000); + String propertyC = handle.getDocumentSetting("propertyC", "def"); + Map subSetting = handle.getDocumentSetting("subSetting", new HashMap<>()); + double propertyD = subSetting.get("propertyD"); + + assertEquals(true, propertyA); + assertEquals(60000, propertyB); + assertEquals( "abc", propertyC); + assertEquals(12.34, propertyD, 0.001); + + // Properties not existing return default value + String propertyF = handle.getDocumentSetting("propertyF", "fallback"); + + assertEquals("fallback", propertyF); + } } diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentManagerTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentManagerTest.java index de74fa76..194e484c 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentManagerTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/DocumentManagerTest.java @@ -11,6 +11,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; import com.amazon.apl.viewhost.config.EmbeddedDocumentFactory; @@ -28,13 +29,15 @@ public class DocumentManagerTest extends ViewhostRobolectricTest { Handler mHandler; @Mock EmbeddedDocumentRequestProxy mEmbeddedDocumentRequestProxy; + @Mock + ITelemetryProvider mTelemetryProvider; DocumentManager documentManager; @Before public void setup() { MockitoAnnotations.openMocks(this); - documentManager = new DocumentManager(mEmbeddedDocumentFactory, mHandler); + documentManager = new DocumentManager(mEmbeddedDocumentFactory, mHandler, mTelemetryProvider); } @Test diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDataSourceContextTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDataSourceContextTest.java index 7531e67a..f69c8bde 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDataSourceContextTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDataSourceContextTest.java @@ -35,6 +35,7 @@ import com.amazon.apl.android.dependencies.IDataSourceErrorCallback; import com.amazon.apl.android.dependencies.IDataSourceFetchCallback; import com.amazon.apl.android.document.AbstractDocUnitTest; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.scaling.ViewportMetrics; import com.amazon.apl.android.utils.APLTrace; import com.amazon.apl.enums.ScreenShape; @@ -161,6 +162,8 @@ public class EmbeddedDataSourceContextTest extends AbstractDocUnitTest { private UpdateDataSourceCallback mCallback; @Mock private IDataSourceErrorCallback mDataSourceErrorCallback; + @Mock + private ITelemetryProvider mTelemetryProvider; @Before public void setup() throws JSONException { @@ -198,7 +201,7 @@ public void onDataSourceFetchRequest(String type, Map payload) { .viewhost(mViewhost) .build(); // TODO: Needed because AbstractDocUnitTest uses a deprecated version of renderDocument - mRootConfig.setDocumentManager(factory, mCoreWorker); + mRootConfig.setDocumentManager(factory, mCoreWorker, mTelemetryProvider); } @Test diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestTest.java index aae08b5c..41479ce1 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedDocumentRequestTest.java @@ -12,6 +12,7 @@ import androidx.test.ext.junit.runners.AndroidJUnit4; import com.amazon.apl.android.Content; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; import com.amazon.apl.viewhost.config.EmbeddedDocumentFactory.EmbeddedDocumentRequest; @@ -19,7 +20,6 @@ import org.junit.Test; import org.junit.runner.RunWith; import org.mockito.Mock; -import org.mockito.Mockito; @RunWith(AndroidJUnit4.class) public class EmbeddedDocumentRequestTest extends ViewhostRobolectricTest { @@ -34,10 +34,12 @@ public class EmbeddedDocumentRequestTest extends ViewhostRobolectricTest { DocumentHandleImpl mDocumentHandle; @Mock Content mContent; + @Mock + ITelemetryProvider mProvider; @Before public void setUp() { - embeddedDocumentRequest = new EmbeddedDocumentRequestImpl(mEmbeddedDocumentRequestProxy, mHandler); + embeddedDocumentRequest = new EmbeddedDocumentRequestImpl(mEmbeddedDocumentRequestProxy, mHandler, mProvider); when(mHandler.post(any(Runnable.class))).thenAnswer(invocation -> { Runnable task = invocation.getArgument(0); task.run(); @@ -60,7 +62,7 @@ public void testFail() { @Test public void testOnDocumentStateChangedToError() { - ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).onDocumentStateChanged(DocumentState.ERROR); + ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).onDocumentStateChanged(DocumentState.ERROR, mDocumentHandle); verify(mEmbeddedDocumentRequestProxy).failure(any()); } @@ -68,7 +70,7 @@ public void testOnDocumentStateChangedToError() { public void testOnDocumentStateChangedToPrepared() { ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).setDocumentHandle(mDocumentHandle); when(mDocumentHandle.getContent()).thenReturn(mContent); - ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).onDocumentStateChanged(DocumentState.PREPARED); + ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).onDocumentStateChanged(DocumentState.PREPARED, mDocumentHandle); verify(mEmbeddedDocumentRequestProxy).success(anyLong(), anyBoolean(), anyLong()); } @@ -79,7 +81,7 @@ public void testSuccessCallbackReturnsNullDocumentContext_setsTerminalDocumentSt when(mDocumentHandle.getContent()).thenReturn(mContent); when(mEmbeddedDocumentRequestProxy.success(anyLong(), anyBoolean(), anyLong())).thenReturn(null); // when - ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).onDocumentStateChanged(DocumentState.PREPARED); + ((EmbeddedDocumentRequestImpl)embeddedDocumentRequest).onDocumentStateChanged(DocumentState.PREPARED, mDocumentHandle); // then the DocumentHandle is in a terminal state assertEquals(mDocumentHandle.isValid(), false); } diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedVisualContextTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedVisualContextTest.java index 9a4121be..67852eb2 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedVisualContextTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/EmbeddedVisualContextTest.java @@ -30,6 +30,7 @@ import com.amazon.apl.android.RootConfig; import com.amazon.apl.android.dependencies.IVisualContextListener; import com.amazon.apl.android.document.AbstractDocUnitTest; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.PreparedDocument; import com.amazon.apl.viewhost.Viewhost; @@ -133,6 +134,8 @@ public class EmbeddedVisualContextTest extends AbstractDocUnitTest { HashMap mEmbeddedDocuments; private APLOptions mAplOptions; private String mGoodbyeCommands; + @Mock + private ITelemetryProvider mTelemetryProvider; @Before public void setup() throws JSONException { @@ -161,7 +164,7 @@ public void setup() throws JSONException { .viewhost(mViewhost) .build(); // TODO: Needed because AbstractDocUnitTest uses a deprecated version of renderDocument - mRootConfig.setDocumentManager(factory, mCoreWorker); + mRootConfig.setDocumentManager(factory, mCoreWorker, mTelemetryProvider); // Create a command array JSON string, which changes the "Hello, World" text. mGoodbyeCommands = new JSONArray().put(new JSONObject() diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/HostComponentTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/HostComponentTest.java index 1e17043a..82bc3a3d 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/HostComponentTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/HostComponentTest.java @@ -35,10 +35,12 @@ import com.amazon.apl.android.dependencies.IContentRetriever; import com.amazon.apl.android.dependencies.IMediaPlayer; import com.amazon.apl.android.dependencies.IPackageLoader; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; import com.amazon.apl.android.document.AbstractDocUnitTest; import com.amazon.apl.android.events.RefreshEvent; import com.amazon.apl.android.media.RuntimeMediaPlayerFactory; import com.amazon.apl.android.providers.AbstractMediaPlayerProvider; +import com.amazon.apl.android.providers.ITelemetryProvider; import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.PreparedDocument; import com.amazon.apl.viewhost.Viewhost; @@ -308,6 +310,8 @@ public class HostComponentTest extends AbstractDocUnitTest { IPackageLoader mPackageLoader; @Mock ExtensionMediator mMediator; + @Mock + private ITelemetryProvider mTelemetryProvider; private CapturingMessageHandler mMessageHandler; private Viewhost mViewhost; @@ -365,7 +369,7 @@ public IMediaPlayer createPlayer(Context context, View view) { .build(); mViewhost = Mockito.spy(new ViewhostImpl(config, mRuntimeInteractionWorker, mCoreWorker)); EmbeddedDocumentFactory factory = new EmbeddedDocumentFactoryTest(mViewhost); - mRootConfig.setDocumentManager(factory, mCoreWorker); + mRootConfig.setDocumentManager(factory, mCoreWorker, mTelemetryProvider); mOptions = APLOptions.builder() .embeddedDocumentFactory(factory) .viewhost(mViewhost) @@ -445,7 +449,7 @@ public void testDocumentStateChangedNotified() { public void testHostComponentReleased_beforeEmbeddedDocumentSuccess_setsEmbeddedDocumentToFinished() { // given ControlledTestEmbeddedFactory factory = new ControlledTestEmbeddedFactory(mViewhost); - mRootConfig.setDocumentManager(factory, mCoreWorker); + mRootConfig.setDocumentManager(factory, mCoreWorker, mTelemetryProvider); mOptions = APLOptions.builder() .embeddedDocumentFactory(factory) .viewhost(mViewhost) @@ -501,7 +505,7 @@ private void assertDocumentInflatedAndPreparedNotificationSent() { @Test public void testMultipleHostsMultipleExtensions(){ MultipleExtensionsDocumentFactoryTest factory = new MultipleExtensionsDocumentFactoryTest(mViewhost); - mRootConfig.setDocumentManager(factory, mCoreWorker); + mRootConfig.setDocumentManager(factory, mCoreWorker, mTelemetryProvider); mOptions = APLOptions.builder() .embeddedDocumentFactory(factory) .viewhost(mViewhost) @@ -517,7 +521,7 @@ public void testNullDocumentOptions() { .build(); mViewhost = new ViewhostImpl(config, mRuntimeInteractionWorker, mCoreWorker); EmbeddedDocumentFactory factory = new NullDocumentOptionsTest(mViewhost); - mRootConfig.setDocumentManager(factory, mCoreWorker); + mRootConfig.setDocumentManager(factory, mCoreWorker, mTelemetryProvider); mOptions = APLOptions.builder() .embeddedDocumentFactory(factory) .viewhost(mViewhost) @@ -681,6 +685,24 @@ public ExtensionRegistrar getExtensionRegistrar() { public Map getExtensionFlags() { return null; } + + @Nullable + @Override + public ITelemetryProvider getTelemetryProvider() { + return null; + } + + @Nullable + @Override + public EmbeddedDocumentFactory getEmbeddedDocumentFactory() { + return null; + } + + @Nullable + @Override + public IUserPerceivedFatalCallback getUserPerceivedFatalCallback() { + return null; + } }); mEmbeddedDocuments.put(request.getSource(), preparedDocument.getHandle()); diff --git a/apl/src/test/java/com/amazon/apl/viewhost/internal/ViewhostTest.java b/apl/src/test/java/com/amazon/apl/viewhost/internal/ViewhostTest.java index efed2d95..ca7ae1f0 100644 --- a/apl/src/test/java/com/amazon/apl/viewhost/internal/ViewhostTest.java +++ b/apl/src/test/java/com/amazon/apl/viewhost/internal/ViewhostTest.java @@ -13,59 +13,87 @@ import static org.junit.Assert.assertNull; import static org.junit.Assert.assertTrue; import static org.mockito.ArgumentMatchers.any; +import static org.mockito.ArgumentMatchers.eq; +import static org.mockito.Mockito.clearInvocations; import static org.mockito.Mockito.doAnswer; import static org.mockito.Mockito.mock; +import static org.mockito.Mockito.mockStatic; import static org.mockito.Mockito.never; +import static org.mockito.Mockito.spy; import static org.mockito.Mockito.times; import static org.mockito.Mockito.verify; +import static org.mockito.Mockito.verifyNoMoreInteractions; import static org.mockito.Mockito.when; import android.os.Handler; -import android.os.Looper; +import android.view.View; import androidx.test.ext.junit.runners.AndroidJUnit4; +import com.amazon.alexaext.ExtensionRegistrar; +import com.amazon.alexaext.IExtensionProvider; +import com.amazon.apl.android.APLLayout; +import com.amazon.apl.android.Action; +import com.amazon.apl.android.Content; import com.amazon.apl.android.DocumentSession; +import com.amazon.apl.android.ExtensionMediator; +import com.amazon.apl.android.IAPLViewPresenter; +import com.amazon.apl.android.RootContext; +import com.amazon.apl.android.Session; +import com.amazon.apl.android.UserPerceivedFatalReporter; import com.amazon.apl.android.dependencies.IDataSourceFetchCallback; import com.amazon.apl.android.dependencies.IOpenUrlCallback; import com.amazon.apl.android.dependencies.ISendEventCallbackV2; +import com.amazon.apl.android.dependencies.IUserPerceivedFatalCallback; +import com.amazon.apl.android.document.AbstractDocUnitTest; import com.amazon.apl.android.events.DataSourceFetchEvent; import com.amazon.apl.android.events.OpenURLEvent; import com.amazon.apl.android.events.PlayMediaEvent; import com.amazon.apl.android.events.SendEvent; -import com.amazon.apl.android.robolectric.ViewhostRobolectricTest; +import com.amazon.apl.android.scaling.ViewportMetrics; +import com.amazon.apl.android.shadow.ShadowBitmapRenderer; +import com.amazon.apl.android.utils.APLTrace; +import com.amazon.apl.enums.DisplayState; +import com.amazon.apl.enums.ScreenShape; +import com.amazon.apl.enums.ViewportMode; import com.amazon.apl.viewhost.DocumentHandle; import com.amazon.apl.viewhost.PreparedDocument; import com.amazon.apl.viewhost.Viewhost; import com.amazon.apl.viewhost.config.DocumentOptions; +import com.amazon.apl.viewhost.config.EmbeddedDocumentFactory; +import com.amazon.apl.viewhost.config.EmbeddedDocumentResponse; import com.amazon.apl.viewhost.config.ViewhostConfig; import com.amazon.apl.viewhost.example.ExampleDocumentFactory; +import com.amazon.apl.viewhost.internal.message.notification.DocumentStateChangedImpl; +import com.amazon.apl.viewhost.message.BaseMessage; import com.amazon.apl.viewhost.message.Message; -import com.amazon.apl.viewhost.message.MessageHandler; -import com.amazon.apl.viewhost.message.action.ActionMessage; import com.amazon.apl.viewhost.message.action.FetchDataRequest; import com.amazon.apl.viewhost.message.action.OpenURLRequest; -import com.amazon.apl.viewhost.message.action.ReportRuntimeErrorRequest; import com.amazon.apl.viewhost.message.action.SendUserEventRequest; import com.amazon.apl.viewhost.primitives.JsonStringDecodable; import com.amazon.apl.viewhost.request.ExecuteCommandsRequest; import com.amazon.apl.viewhost.request.FinishDocumentRequest; import com.amazon.apl.viewhost.request.PrepareDocumentRequest; import com.amazon.apl.viewhost.request.RenderDocumentRequest; +import com.amazon.apl.viewhost.request.UpdateDataSourceRequest; +import com.amazon.apl.viewhost.request.UpdateDataSourceRequest.UpdateDataSourceCallback; import com.amazon.apl.viewhost.utils.CapturingMessageHandler; import com.amazon.apl.viewhost.utils.ManualExecutor; -import org.json.JSONException; import org.junit.Before; import org.junit.Test; import org.junit.runner.RunWith; +import org.mockito.ArgumentCaptor; import org.mockito.Mock; +import org.robolectric.RuntimeEnvironment; import java.util.HashMap; import java.util.Map; +import java.util.concurrent.CountDownLatch; +import java.util.concurrent.TimeUnit; @RunWith(AndroidJUnit4.class) -public class ViewhostTest extends ViewhostRobolectricTest { +public class ViewhostTest extends AbstractDocUnitTest { @Mock private DocumentHandle mDocumentHandle; @Mock @@ -73,16 +101,151 @@ public class ViewhostTest extends ViewhostRobolectricTest { @Mock private DocumentContext mDocumentContext; + @Mock + private IAPLViewPresenter mViewPresenter; + private Viewhost mViewhost; + + private Viewhost mViewhost2; + + private Viewhost mViewhost3; private CapturingMessageHandler mMessageHandler; private ManualExecutor mRuntimeInteractionWorker; + @Mock + private IExtensionProvider mExtensionProvider; + @Mock + DocumentOptions mDocumentOptions; + @Mock + ExtensionMediator.IExtensionGrantRequestCallback mExtensionGrantRequestCallback; + @Mock + protected ShadowBitmapRenderer mockShadowRenderer; + @Mock + private UpdateDataSourceCallback mCallback; + + @Mock + private Handler mCoreWorker; + + @Mock + private RootContext mRootContext; + + @Mock + private Viewhost.ExtensionEventHandlerCallback mExtensionEventHandlerCallback; + + @Mock + private Action mAction; + + @Mock + private IUserPerceivedFatalCallback mUserPerceivedFatalCallback; + + private APLLayout mAplLayout; + + private static final String SIMPLE_DOC = "{" + + " \"type\": \"APL\"," + + " \"version\": \"2023.3\"," + + " \"mainTemplate\": {" + + " \"item\":" + + " {" + + " \"type\": \"Frame\"" + + " }" + + " }" + + "}"; + private static final String INVALID_DOC = "{" + + " \"type\": \"APL\"," + + " \"version\": \"2023.3\"," + + " \"mainTemplate\": " + + " \"item\":" + + " {" + + " \"type\": \"Frame\"" + + " }" + + " }" + + "}"; + private static final String SIMPLE_DOC_WITH_HOST_COMPONENT = "{\n" + + " \"type\": \"APL\",\n" + + " \"version\": \"2023.3\",\n" + + " \"mainTemplate\": {\n" + + " \"item\": {\n" + + " \"type\": \"Container\",\n" + + " \"items\": [\n" + + " \n" + + " {\n" + + " \"type\": \"Host\",\n" + + " \"source\": \"url\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " }\n" + + "}\n"; + private static final String SHOPPING_LIST_DOC = "{" + + " \"type\": \"APL\"," + + " \"version\": \"2023.2\"," + + " \"mainTemplate\": {" + + " \"parameters\": [" + + " \"shoppingListData\"" + + " ]," + + " \"items\": {" + + " \"type\": \"Sequence\"," + + " \"width\": \"100%\"," + + " \"height\": \"100%\"," + + " \"data\": \"${shoppingListData}\"," + + " \"items\": {" + + " \"type\": \"Text\"," + + " \"text\": \"${index + 1}. ${data.text}\"," + + " \"color\": \"white\"," + + " \"textAlign\": \"center\"," + + " \"textAlignVertical\": \"center\"" + + " }" + + " }" + + " }" + + "}"; + private static final String SHOPPING_LIST_DATA = "{\n" + + " \"shoppingListData\" : {\n" + + " \"type\": \"dynamicIndexList\",\n" + + " \"listId\": \"shoppingListA\",\n" + + " \"startIndex\": 0,\n" + + " \"minimumInclusiveIndex\": 0,\n" + + " \"maximumExclusiveIndex\": 100,\n" + + " \"items\": []\n" + + " }\n" + + "}"; + @Before public void setup() { + ViewportMetrics metrics = ViewportMetrics.builder() + .width(1280) + .height(720) + .dpi(160) + .shape(ScreenShape.RECTANGLE) + .theme("dark") + .mode(ViewportMode.kViewportModeHub) + .build(); + mMessageHandler = new CapturingMessageHandler(); mRuntimeInteractionWorker = new ManualExecutor(); - ViewhostConfig config = ViewhostConfig.builder().messageHandler(mMessageHandler).build(); - mViewhost = new ViewhostImpl(config, mRuntimeInteractionWorker, new Handler(Looper.getMainLooper())); + mAplLayout = spy(new APLLayout(RuntimeEnvironment.getApplication().getBaseContext(), null)); + mAplLayout.setAplViewPresenterForTesting(mViewPresenter); + ExtensionRegistrar extensionRegistrar = new ExtensionRegistrar().addProvider(mExtensionProvider); + when(mDocumentOptions.getExtensionGrantRequestCallback()).thenReturn(mExtensionGrantRequestCallback); + when(mDocumentOptions.getExtensionRegistrar()).thenReturn(extensionRegistrar); + when(mDocumentOptions.getUserPerceivedFatalCallback()).thenReturn(mUserPerceivedFatalCallback); + when(mAplLayout.getPresenter()).thenReturn(mViewPresenter); + when(mViewPresenter.getShadowRenderer()).thenReturn(mockShadowRenderer); + when(mViewPresenter.getAPLTrace()).thenReturn(mock(APLTrace.class)); + when(mViewPresenter.getOrCreateViewportMetrics()).thenReturn(metrics); + when(mCoreWorker.post(any(Runnable.class))).thenAnswer(invocation -> { + Runnable task = invocation.getArgument(0); + task.run(); + return null; + }); + // Create new viewhost for handling embedded documents + ViewhostConfig config = ViewhostConfig.builder() + .messageHandler(mMessageHandler) + .extensionRegistrar(extensionRegistrar) + .defaultDocumentOptions(mDocumentOptions) + .build(); + mViewhost = new ViewhostImpl(config, mRuntimeInteractionWorker, mCoreWorker); + mViewhost2 = new ViewhostImpl(config, mRuntimeInteractionWorker, mCoreWorker); + mViewhost3 = new ViewhostImpl(config, mRuntimeInteractionWorker, mCoreWorker); } @Test @@ -109,7 +272,7 @@ public void testPrepareDocument() { // Basic demonstration of a prepare document request, getting a document handle PrepareDocumentRequest request = PrepareDocumentRequest.builder() .token("mytoken") - .document(new JsonStringDecodable("document")) + .document(new JsonStringDecodable(SIMPLE_DOC)) .data(new JsonStringDecodable("data")) .documentSession(DocumentSession.create()) .documentOptions(DocumentOptions.builder().build()) @@ -117,23 +280,438 @@ public void testPrepareDocument() { PreparedDocument preparedDocument = mViewhost.prepare(request); DocumentHandle handle = preparedDocument.getHandle(); assertNotNull(handle); + assertEquals("mytoken", preparedDocument.getToken()); + assertTrue(preparedDocument.hasToken()); + assertTrue(preparedDocument.isValid()); + assertTrue(preparedDocument.isReady()); + assertEquals(handle.getUniqueId(), preparedDocument.getUniqueID()); - // Could be rendered at this point, if that pathway was implemented - assertNull(mViewhost.render(preparedDocument)); + assertNotNull(mViewhost.render(preparedDocument)); // Could finish document given a handle FinishDocumentRequest finishRequest = FinishDocumentRequest.builder() .token("mytoken") .build(); handle.finish(finishRequest); + } + @Test + public void testPrepareDocument_reportUpfFatal_onContentCreationFailed() throws Content.ContentException { + // Basic demonstration of a prepare document request, getting a document handle + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .token("mytoken") + .document(new JsonStringDecodable(SIMPLE_DOC)) + .data(new JsonStringDecodable("data")) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + when(mDocumentOptions.getUserPerceivedFatalCallback()).thenReturn(mUserPerceivedFatalCallback); + + mockStatic(Content.class); + Content.CallbackV2 callbackV2 = mock(Content.CallbackV2.class); + + doAnswer(invocation -> { + Exception e = new Exception(); + ((Content.CallbackV2) invocation.getMock()).onError(e); + return null; + }).when(callbackV2).onError(any()); + + when(Content.create(any(), any(), any(), any(Session.class), any())).then(invocation -> { + Content.CallbackV2 callback = invocation.getArgument(2); + callback.onError(new Exception()); + return null; + }); + PreparedDocument preparedDocument = mViewhost.prepare(request); + verify(mUserPerceivedFatalCallback, times(1)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.CONTENT_CREATION_FAILURE.toString())); + verify(mUserPerceivedFatalCallback, times(0)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE.toString())); } @Test - public void testRenderDocument() { - // Pathway not implemented - DocumentHandle handle = mViewhost.render(mock(RenderDocumentRequest.class)); - assertNull(handle); + public void testPrepareDocument_reportUpfFatal_onRequiredExtensionFailure() throws Content.ContentException { + ExtensionMediator extensionMediator = mock(ExtensionMediator.class); + ExtensionRegistrar extensionRegistrar = mock(ExtensionRegistrar.class); + ExtensionMediator.IExtensionGrantRequestCallback extensionGrantRequestCallback = mock(ExtensionMediator.IExtensionGrantRequestCallback.class); + Content content = mock(Content.class); + + // Basic demonstration of a prepare document request, getting a document handle + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .token("mytoken") + .document(new JsonStringDecodable(SIMPLE_DOC)) + .data(new JsonStringDecodable("data")) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + when(mDocumentOptions.getUserPerceivedFatalCallback()).thenReturn(mUserPerceivedFatalCallback); + when(mDocumentOptions.getExtensionRegistrar()).thenReturn(extensionRegistrar); + when(mDocumentOptions.getExtensionGrantRequestCallback()).thenReturn(extensionGrantRequestCallback); + + mockStatic(Content.class); + Content.CallbackV2 callbackV2 = mock(Content.CallbackV2.class); + + doAnswer(invocation -> { + ((Content.CallbackV2) invocation.getMock()).onComplete(content); + return content; + }).when(callbackV2).onComplete(any()); + + when(Content.create(any(), any(), any(), any(Session.class), any())).then(invocation -> { + Content.CallbackV2 callback = invocation.getArgument(2); + callback.onComplete(content); + return content; + }); + + mockStatic(ExtensionMediator.class); + when(ExtensionMediator.create(any(), any())).thenReturn(extensionMediator); + doAnswer(invocation -> { + ExtensionMediator.ILoadExtensionCallback loadExtensionCallback = invocation.getArgument(2); + loadExtensionCallback.onFailure().run(); + return null; + }).when(extensionMediator).loadExtensions((Map) any(), any(), any()); + + PreparedDocument preparedDocument = mViewhost.prepare(request); + + verify(mUserPerceivedFatalCallback, times(0)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.CONTENT_CREATION_FAILURE.toString())); + verify(mUserPerceivedFatalCallback, times(1)).onFatalError(eq(UserPerceivedFatalReporter.UpfReason.REQUIRED_EXTENSION_LOADING_FAILURE.toString())); + } + + @Test + public void testPreparedDocument_withoutRender_FinishRequest() { + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = preparedDocument.getHandle(); + assertNotNull(handle); + assertNull(((DocumentHandleImpl) handle).getRootContext()); + assertNull(((DocumentHandleImpl) handle).getDocumentContext()); + + //finish request + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder() + .build(); + boolean result = handle.finish(finishRequest); + assertTrue(result); + } + + @Test + public void testPreparedDocument_withoutRender_FinishRequest_InvalidToken() { + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .token("mytoken") + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = preparedDocument.getHandle(); + assertNotNull(handle); + assertNull(((DocumentHandleImpl) handle).getRootContext()); + assertNull(((DocumentHandleImpl) handle).getDocumentContext()); + + //finish request + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder() + .token("tokenMistake") + .build(); + boolean result = handle.finish(finishRequest); + //result should be false when tokens are not matched + assertFalse(result); + } + + @Test + public void testPreparedDocument_withoutRender_FinishRequest_noToken() { + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .token("mytoken") + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = preparedDocument.getHandle(); + assertNotNull(handle); + assertNull(((DocumentHandleImpl) handle).getRootContext()); + assertNull(((DocumentHandleImpl) handle).getDocumentContext()); + + //finish request + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder().build(); + boolean result = handle.finish(finishRequest); + //result should be true when finishRequest do not have any token specified + assertTrue(result); + } + + @Test + public void testPreparedDocument_Render_Finish() throws InterruptedException { + CountDownLatch inflatedLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(1); + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + } else if (state == DocumentState.FINISHED) { + finishLatch.countDown(); + } + }); + DocumentHandle handle = prepareAndRender(SIMPLE_DOC, ""); + assertTrue(inflatedLatch.await(5, TimeUnit.SECONDS)); + + //finish request + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder() + .build(); + boolean result = handle.finish(finishRequest); + assertTrue(result); + assertTrue(finishLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testUpdateDisplayState() throws InterruptedException { + CountDownLatch inflatedLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(1); + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + } else if (state == DocumentState.FINISHED) { + finishLatch.countDown(); + } + }); + DocumentHandleImpl handle = (DocumentHandleImpl)prepareAndRender(SIMPLE_DOC, ""); + inflatedLatch.await(5, TimeUnit.SECONDS); + handle.getRootContext().resumeDocument(); + clearInvocations(mViewPresenter); + + mViewhost.updateDisplayState(DisplayState.kDisplayStateHidden); + verify(mViewPresenter).onDocumentPaused(); + + mViewhost.updateDisplayState(DisplayState.kDisplayStateForeground); + verify(mViewPresenter).onDocumentResumed(); + + // Switching to background shouldn't pause when at 2024.1 and above. + mViewhost.updateDisplayState(DisplayState.kDisplayStateBackground); + verifyNoMoreInteractions(mViewPresenter); + } + + @Test + public void testRenderPreparedDocumentError() throws InterruptedException { + CountDownLatch preparedLatch = new CountDownLatch(1); + CountDownLatch errorLatch = new CountDownLatch(1); + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + if (!mViewhost.isBound()) { + mViewhost.bind(mAplLayout); + } + when(mAplLayout.getPresenter()).thenReturn(null); + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(DocumentOptions.builder().build()) + .build(); + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.PREPARED) { + preparedLatch.countDown(); + } else if (state == DocumentState.ERROR) { + errorLatch.countDown(); + } + }); + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = mViewhost.render(preparedDocument); + assertNotNull(handle); + assertTrue(preparedLatch.await(1, TimeUnit.SECONDS)); + assertTrue(errorLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testRenderPrepareDocument_viewUnbound_requestIgnored() { + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(DocumentOptions.builder().build()) + .build(); + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = mViewhost.render(preparedDocument); + assertNotNull(handle); + assertFalse(mViewhost.isBound()); + assertPrepared(handle); + } + + @Test + public void testRenderPreparedDocumentInflated() throws InterruptedException { + Map documentHandleMap = new HashMap<>(); + testRenderPreparedDocumentInflated(SIMPLE_DOC, documentHandleMap); + assertEquals(1, documentHandleMap.size()); + } + + @Test + public void testRenderPreparedDocumentWithEmbeddedDocsInflated() throws InterruptedException { + Map documentHandleMap = new HashMap<>(); + EmbeddedDocumentFactory factory = new EmbeddedDocumentFactoryTest(mViewhost, documentHandleMap); + when(mDocumentOptions.getEmbeddedDocumentFactory()).thenReturn(factory); + testRenderPreparedDocumentInflated(SIMPLE_DOC_WITH_HOST_COMPONENT, documentHandleMap); + assertEquals(2, documentHandleMap.size()); + } + + private void testRenderPreparedDocumentInflated(String document, Map documentHandleMap) throws InterruptedException { + CountDownLatch inflatedLatch = new CountDownLatch(1); + + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + documentHandleMap.put(handle.getUniqueId(), handle); + } + }); + prepareAndRender(document, ""); + assertTrue(inflatedLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testUpdateDataSourcePrimaryDocument() throws InterruptedException { + String type = "dynamicIndexList"; + String payload = "{\n" + + " \"listId\":\"shoppingListA\",\n" + + " \"listVersion\":1,\n" + + " \"operations\":[\n" + + " {\n" + + " \"type\":\"DeleteMultipleItems\",\n" + + " \"index\":0,\n" + + " \"count\":999\n" + + " },\n" + + " {\n" + + " \"type\":\"InsertMultipleItems\",\n" + + " \"index\":0,\n" + + " \"items\":[\n" + + " {\n" + + " \"primaryText\":\"Updated item 1\"\n" + + " },\n" + + " {\n" + + " \"primaryText\":\"Updated item 2\"\n" + + " },\n" + + " {\n" + + " \"primaryText\":\"Updated item 3\"\n" + + " },\n" + + " {\n" + + " \"primaryText\":\"Updated item 4\"\n" + + " },\n" + + " {\n" + + " \"primaryText\":\"Updated item 5\"\n" + + " }\n" + + " ]\n" + + " }\n" + + " ]\n" + + "}"; + UpdateDataSourceRequest updateDataSourceRequest = UpdateDataSourceRequest + .builder() + .data(new JsonStringDecodable(payload)) + .type(type) + .callback(mCallback) + .build(); + CountDownLatch updateDataSourceLatch = new CountDownLatch(1); + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.INFLATED) { + assertTrue(handle.updateDataSource(updateDataSourceRequest)); + updateDataSourceLatch.countDown(); + } + }); + prepareAndRender(SHOPPING_LIST_DOC, SHOPPING_LIST_DATA); + assertTrue(updateDataSourceLatch.await(5, TimeUnit.SECONDS)); + } + + @Test + public void testRenderDocument_callsPrepareAndRender() { + // This test verifies that the correct calls are made since render(RenderDocumentRequest) + // is essentially a chaining of a prepare(PrepareDocumentRequest) and render(PreparedDocument) + ViewhostImpl mockViewhost = spy((ViewhostImpl) mViewhost); + ArgumentCaptor captor = ArgumentCaptor.forClass(PrepareDocumentRequest.class); + + RenderDocumentRequest renderDocumentRequest = RenderDocumentRequest.builder() + .token("mytoken") + .document(new JsonStringDecodable(SIMPLE_DOC)) + .data(new JsonStringDecodable("data")) + .documentSession(DocumentSession.create()) + .documentOptions(DocumentOptions.builder().build()) + .build(); + mockViewhost.render(renderDocumentRequest); + + verify(mockViewhost).prepare(captor.capture()); + + PrepareDocumentRequest prepareDocumentRequest = captor.getValue(); + assertEquals(prepareDocumentRequest.getToken(), renderDocumentRequest.getToken()); + assertEquals(prepareDocumentRequest.getDocument(), renderDocumentRequest.getDocument()); + assertEquals(prepareDocumentRequest.getData(), renderDocumentRequest.getData()); + assertEquals(prepareDocumentRequest.getDocumentSession(), renderDocumentRequest.getDocumentSession()); + assertEquals(prepareDocumentRequest.getDocumentOptions(), renderDocumentRequest.getDocumentOptions()); + + verify(mockViewhost).render(any(PreparedDocument.class), any(Long.class)); + } + + @Test + public void testRenderDocument_viewhostNotBound_ignoresRendering() throws InterruptedException { + CountDownLatch preparedLatch = new CountDownLatch(1); + CountDownLatch errorLatch = new CountDownLatch(1); + + RenderDocumentRequest request = RenderDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(DocumentOptions.builder().build()) + .build(); + + // No Viewhost bound to layout, request should fail + mViewhost.bind(null); + + DocumentHandle handle = mViewhost.render(request); + assertNotNull(handle); + assertFalse(mViewhost.isBound()); + assertPrepared(handle); + } + + @Test + public void testRenderDocument_viewhostBound_inflatesAndFinishes() throws InterruptedException { + CountDownLatch inflatedLatch = new CountDownLatch(1); + CountDownLatch finishLatch = new CountDownLatch(1); + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + } else if (state == DocumentState.FINISHED) { + finishLatch.countDown(); + } + }); + + DocumentHandle handle = render(SIMPLE_DOC, ""); + assertTrue(inflatedLatch.await(5, TimeUnit.SECONDS)); + + // Finish + FinishDocumentRequest finishRequest = FinishDocumentRequest.builder() + .build(); + boolean result = handle.finish(finishRequest); + assertTrue(result); + assertTrue(finishLatch.await(1, TimeUnit.SECONDS)); + } + + @Test + public void testRenderDocument_inflated() throws InterruptedException { + Map documentHandleMap = new HashMap<>(); + testRenderDocument_inflated(SIMPLE_DOC, documentHandleMap); + assertEquals(1, documentHandleMap.size()); + } + + @Test + public void testRenderDocument_embeddedDoc_inflated() throws InterruptedException { + Map documentHandleMap = new HashMap<>(); + EmbeddedDocumentFactory factory = new EmbeddedDocumentFactoryTest(mViewhost, documentHandleMap); + when(mDocumentOptions.getEmbeddedDocumentFactory()).thenReturn(factory); + testRenderPreparedDocumentInflated(SIMPLE_DOC_WITH_HOST_COMPONENT, documentHandleMap); + assertEquals(2, documentHandleMap.size()); + } + + private void testRenderDocument_inflated(String document, Map documentHandleMap) throws InterruptedException { + CountDownLatch inflatedLatch = new CountDownLatch(1); + + mViewhost.registerStateChangeListener((state, handle) -> { + if (state == DocumentState.INFLATED) { + inflatedLatch.countDown(); + documentHandleMap.put(handle.getUniqueId(), handle); + } + }); + + render(document, ""); + assertTrue(inflatedLatch.await(5, TimeUnit.SECONDS)); } @Test @@ -159,8 +737,8 @@ public void testExampleMessage() { public void testAllowsUnrelatedEventsToProceedWithoutOverriding() { SendEvent event = mock(SendEvent.class); updateDocumentMap(222, mDocumentHandleImpl, mDocumentContext); - when(event.getDocumentContextId()).thenReturn((long)333); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + when(event.getDocumentContextId()).thenReturn((long) 333); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); verify(event, never()).overrideCallback(any(ISendEventCallbackV2.class)); } @@ -168,8 +746,8 @@ public void testAllowsUnrelatedEventsToProceedWithoutOverriding() { public void testUnrecognizedEventsPertainingToKnownDocuments() { PlayMediaEvent event = mock(PlayMediaEvent.class); updateDocumentMap(456, mDocumentHandleImpl, mDocumentContext); - when(event.getDocumentContextId()).thenReturn((long)456); - assertFalse(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + when(event.getDocumentContextId()).thenReturn((long) 456); + assertFalse(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); } @Test @@ -185,7 +763,7 @@ public void testInterceptSendEventIfNeeded() { return null; }).when(event).overrideCallback(any(ISendEventCallbackV2.class)); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); assertTrue(mRuntimeInteractionWorker.size() > 0); mRuntimeInteractionWorker.flush(); @@ -218,7 +796,7 @@ public void testInterceptDataSourceFetchEventIfNeeded() { return null; }).when(event).overrideCallback(any(IDataSourceFetchCallback.class)); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); // Second event DataSourceFetchEvent event2 = mock(DataSourceFetchEvent.class); @@ -229,7 +807,7 @@ public void testInterceptDataSourceFetchEventIfNeeded() { return null; }).when(event2).overrideCallback(any(IDataSourceFetchCallback.class)); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event2)); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event2)); assertTrue(mRuntimeInteractionWorker.size() > 0); mRuntimeInteractionWorker.flush(); @@ -271,7 +849,7 @@ public void testInterceptDataSourceFetchEventWithInvalidType() { return null; }).when(event).overrideCallback(any(IDataSourceFetchCallback.class)); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); assertEquals(0, mRuntimeInteractionWorker.size()); mRuntimeInteractionWorker.flush(); @@ -288,14 +866,14 @@ public void testInterceptOpenURLIfNeeded() { IOpenUrlCallback.IOpenUrlCallbackResult callbackResult = mock(IOpenUrlCallback.IOpenUrlCallbackResult.class); - + doAnswer(invocation -> { IOpenUrlCallback callback = invocation.getArgument(0); callback.onOpenUrl(source, callbackResult); return null; }).when(event).overrideCallback(any(IOpenUrlCallback.class)); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); assertTrue(mRuntimeInteractionWorker.size() > 0); mRuntimeInteractionWorker.flush(); @@ -329,7 +907,7 @@ public void testInterceptOpenURLIfNeededWithFailedCallback() { return null; }).when(event).overrideCallback(any(IOpenUrlCallback.class)); - assertTrue(((ViewhostImpl)mViewhost).interceptEventIfNeeded(event)); + assertTrue(((ViewhostImpl) mViewhost).interceptEventIfNeeded(event)); assertTrue(mRuntimeInteractionWorker.size() > 0); mRuntimeInteractionWorker.flush(); @@ -346,38 +924,451 @@ public void testInterceptOpenURLIfNeededWithFailedCallback() { verify(callbackResult, times(1)).onResult(false); } - @Test - public void testDataSourceError() throws JSONException { - - } - @Test public void testEventsDroppedWithoutMessageHandler() { ViewhostConfig config = ViewhostConfig.builder().build(); ViewhostImpl viewhost = new ViewhostImpl(config); when(mDocumentHandleImpl.getDocumentContext()).thenReturn(mDocumentContext); - when(mDocumentContext.getId()).thenReturn((long)123); + when(mDocumentContext.getId()).thenReturn((long) 123); viewhost.updateDocumentMap(mDocumentHandleImpl); SendEvent sendEvent = mock(SendEvent.class); - when(sendEvent.getDocumentContextId()).thenReturn((long)123); + when(sendEvent.getDocumentContextId()).thenReturn((long) 123); DataSourceFetchEvent dataSourceFetchEvent = mock(DataSourceFetchEvent.class); - when(dataSourceFetchEvent.getDocumentContextId()).thenReturn((long)123); + when(dataSourceFetchEvent.getDocumentContextId()).thenReturn((long) 123); OpenURLEvent openURLEvent = mock(OpenURLEvent.class); - when(openURLEvent.getDocumentContextId()).thenReturn((long)123); + when(openURLEvent.getDocumentContextId()).thenReturn((long) 123); assertFalse(viewhost.interceptEventIfNeeded(sendEvent)); assertFalse(viewhost.interceptEventIfNeeded(dataSourceFetchEvent)); assertFalse(viewhost.interceptEventIfNeeded(openURLEvent)); } + @Test + public void testCancelExecution() { + ((ViewhostImpl) mViewhost).setTopDocumentHandleAndRootContext(mDocumentHandleImpl, mRootContext); + mViewhost.cancelExecution(); + verify(mRootContext).cancelExecution(); + } + + + @Test + public void testInvokeExtensionEventHandler_nullRootContext() { + ((ViewhostImpl) mViewhost).setTopDocumentHandleAndRootContext(null, null); + + mViewhost.invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true, null); + + verify(mRootContext, times(0)).invokeExtensionEventHandler(any(String.class), any(String.class), any(Map.class), any(Boolean.class)); + assertTrue(mRuntimeInteractionWorker.size() == 0); + // No NPE, test passes + } + @Test + public void testInvokeExtensionEventHandler_nullCallback() { + ((ViewhostImpl) mViewhost).setTopDocumentHandleAndRootContext(mDocumentHandleImpl, mRootContext); + + mViewhost.invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true, null); + + verify(mRootContext).invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true); + assertTrue(mRuntimeInteractionWorker.size() == 0); + // No NPE, test passes + } + + @Test + public void testInvokeExtensionEventHandler_nullAction_callsOnComplete() { + ((ViewhostImpl) mViewhost).setTopDocumentHandleAndRootContext(mDocumentHandleImpl, mRootContext); + when(mRootContext.invokeExtensionEventHandler(any(String.class), any(String.class), any(Map.class), any(Boolean.class))).thenReturn(null); + + mViewhost.invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true, mExtensionEventHandlerCallback); + + verify(mRootContext).invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true); + assertTrue(mRuntimeInteractionWorker.size() == 1); + mRuntimeInteractionWorker.flush(); + verify(mExtensionEventHandlerCallback, times(1)).onComplete(); + verify(mExtensionEventHandlerCallback, times(0)).onTerminated(); + } + + @Test + public void testInvokeExtensionEventHandler_callback_action_addsCallbacksToAction() { + ((ViewhostImpl) mViewhost).setTopDocumentHandleAndRootContext(mDocumentHandleImpl, mRootContext); + when(mRootContext.invokeExtensionEventHandler(any(String.class), any(String.class), any(Map.class), any(Boolean.class))).thenReturn(mAction); + + mViewhost.invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true, mExtensionEventHandlerCallback); + + verify(mRootContext).invokeExtensionEventHandler("myextension:10", "MyExtension", new HashMap<>(), true); + + assertTrue(mRuntimeInteractionWorker.size() == 0); + + // callback.onComplete() is called when action completed successfully + ArgumentCaptor onCompleteRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mAction).then(onCompleteRunnable.capture()); + onCompleteRunnable.getValue().run(); + assertTrue(mRuntimeInteractionWorker.size() == 1); + mRuntimeInteractionWorker.flush(); + verify(mExtensionEventHandlerCallback, times(1)).onComplete(); + assertTrue(mRuntimeInteractionWorker.size() == 0); + + // callback.onTerminated() is called when action terminates + ArgumentCaptor onTerminateRunnable = ArgumentCaptor.forClass(Runnable.class); + verify(mAction).addTerminateCallback(onTerminateRunnable.capture()); + onTerminateRunnable.getValue().run(); + assertTrue(mRuntimeInteractionWorker.size() == 1); + mRuntimeInteractionWorker.flush(); + verify(mExtensionEventHandlerCallback, times(1)).onTerminated(); + assertTrue(mRuntimeInteractionWorker.size() == 0); + } + + @Test + public void testRestoreDocumentSuccess() throws InterruptedException { + DocumentHandle doc1 = prepareAndRender(SIMPLE_DOC, ""); + DocumentHandle doc2 = prepareAndRender(SIMPLE_DOC, ""); + assertTrue(mViewhost.restoreDocument(SavedDocument.builder().documentHandle(doc1).build())); + + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + int doc1InflationCount = 0; + while(!mMessageHandler.queue.isEmpty()) { + BaseMessage message = mMessageHandler.queue.poll(); + if (message instanceof DocumentStateChangedImpl + && DocumentState.INFLATED.toString().equals(((DocumentStateChangedImpl) message).getState()) + && doc1.getUniqueId().equals(message.getDocument().getUniqueId())) { + doc1InflationCount++; + } + } + assertEquals(2, doc1InflationCount); + } + + @Test + public void testRestoreDocumentInvalidDoc() throws InterruptedException { + DocumentHandle invalidDoc = mViewhost.prepare( PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(INVALID_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build()).getHandle(); + DocumentHandle doc2 = prepareAndRender(SIMPLE_DOC, ""); + assertFalse(mViewhost.restoreDocument(SavedDocument.builder().documentHandle(invalidDoc).build())); + + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + int invalidDocInflationCount = 0; + while(!mMessageHandler.queue.isEmpty()) { + BaseMessage message = mMessageHandler.queue.poll(); + if (message instanceof DocumentStateChangedImpl + && DocumentState.INFLATED.toString().equals(((DocumentStateChangedImpl) message).getState()) + && invalidDoc.getUniqueId().equals(message.getDocument().getUniqueId())) { + invalidDocInflationCount++; + } + } + assertEquals(0, invalidDocInflationCount); + } + + @Test + public void testReusePreparedDocument() { + PreparedDocument preparedDocument = mViewhost.prepare(PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build()); + DocumentHandleImpl documentHandle = (DocumentHandleImpl) preparedDocument.getHandle(); + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + if (!mViewhost.isBound()) { + mViewhost.bind(mAplLayout); + } + renderSuccess(preparedDocument); + assertEquals(DocumentState.INFLATED, documentHandle.getDocumentState()); + + //reuse finished prepared doc + renderSuccess(preparedDocument); + assertEquals(DocumentState.INFLATED, documentHandle.getDocumentState()); + + documentHandle.finish(FinishDocumentRequest.builder().build()); + finishSuccess(preparedDocument.getHandle()); + + assertEquals(DocumentState.FINISHED, documentHandle.getDocumentState()); + + //null response when document not valid + DocumentHandle handle = mViewhost.render(preparedDocument); + assertNull(handle); + } + + @Test + public void testMultiViewhost_usingThreeViewhosts_documentReuseSucess() { + //prepare using VH1 + PreparedDocument preparedDocument = mViewhost.prepare(PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build()); + DocumentHandleImpl handle = (DocumentHandleImpl) preparedDocument.getHandle(); + + //set apl layout + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + + //bind and render using VH2 + if (!mViewhost2.isBound()) { + mViewhost2.bind(mAplLayout); + } + mViewhost2.render(preparedDocument); + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + + assertNotNull(handle.getRootContext()); + assertNotNull(handle.getDocumentContext()); + + assertMessageReceived(DocumentState.INFLATED, handle.getUniqueId()); + + //bind and reuse using VH3 + if (!mViewhost3.isBound()) { + mViewhost3.bind(mAplLayout); + } + mViewhost3.render(preparedDocument); + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + + assertNotNull(handle.getRootContext()); + assertNotNull(handle.getDocumentContext()); + + assertMessageReceived(DocumentState.INFLATED, handle.getUniqueId()); + + } + + @Test + public void testMultiViewhost_usingTwoViewhosts_documentReuseSucess() { + //prepare using VH1 + PreparedDocument preparedDocument = mViewhost.prepare(PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build()); + DocumentHandleImpl handle = (DocumentHandleImpl) preparedDocument.getHandle(); + + //set apl layout + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + + //bind and render using VH2 + if (!mViewhost2.isBound()) { + mViewhost2.bind(mAplLayout); + } + mViewhost2.render(preparedDocument); + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + + assertNotNull(handle.getRootContext()); + assertNotNull(handle.getDocumentContext()); + + assertMessageReceived(DocumentState.INFLATED, handle.getUniqueId()); + + //reuse using VH2 again + + mViewhost2.render(preparedDocument); + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + + assertNotNull(handle.getRootContext()); + assertNotNull(handle.getDocumentContext()); + + assertMessageReceived(DocumentState.INFLATED, handle.getUniqueId()); + + } + + private void renderSuccess(PreparedDocument preparedDocument) { + mViewhost.render(preparedDocument); + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assertMessageReceived(DocumentState.INFLATED, preparedDocument.getHandle().getUniqueId()); + } + + private void finishSuccess(DocumentHandle documentHandle) { + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assertMessageReceived(DocumentState.FINISHED, documentHandle.getUniqueId()); + } + + private void assertMessageReceived(DocumentState state, String documentUniqueId) { + boolean stateFlag = false; + int stateCount = 0; + while(!mMessageHandler.queue.isEmpty()) { + BaseMessage message = mMessageHandler.queue.poll(); + if (message instanceof DocumentStateChangedImpl + && state.toString().equals(((DocumentStateChangedImpl) message).getState()) + && documentUniqueId.equals(message.getDocument().getUniqueId())) { + stateFlag = true; + stateCount++; + } + } + assertTrue(stateFlag); + assertEquals(1, stateCount); + } + + @Test + public void testIsBound() { + assertFalse(mViewhost.isBound()); + mViewhost.bind(mAplLayout); + assertTrue(mViewhost.isBound()); + } + + @Test + public void testMultiViewhost_renderPreparedDocumentSuccess() { + + //prepare using VH1 + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .data(new JsonStringDecodable("")) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = preparedDocument.getHandle(); + assertNotNull(handle); + //bind VH2 + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + if (!mViewhost2.isBound()) { + mViewhost2.bind(mAplLayout); + } + + //render using VH2 + mViewhost2.render(preparedDocument); + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + assert(mMessageHandler.queue.size() > 0); + + assertNotNull(((DocumentHandleImpl) handle).getRootContext()); + assertNotNull(((DocumentHandleImpl) handle).getDocumentContext()); + + int inflatedCount = 0; + while(!mMessageHandler.queue.isEmpty()) { + BaseMessage message = mMessageHandler.queue.poll(); + if (message instanceof DocumentStateChangedImpl + && DocumentState.INFLATED.toString().equals(((DocumentStateChangedImpl) message).getState()) + && handle.getUniqueId().equals(message.getDocument().getUniqueId())) { + inflatedCount++; + } + } + assertEquals(1, inflatedCount); + } + private void updateDocumentMap(long key, DocumentHandleImpl documentHandle, DocumentContext documentContext) { when(documentHandle.getDocumentContext()).thenReturn(documentContext); when(documentHandle.isValid()).thenReturn(true); when(documentContext.getId()).thenReturn(key); - ((ViewhostImpl)mViewhost).updateDocumentMap(documentHandle); + ((ViewhostImpl) mViewhost).updateDocumentMap(documentHandle); + } + + private class EmbeddedDocumentFactoryTest implements EmbeddedDocumentFactory { + private final Viewhost mViewhost; + private final Map mMap; + + EmbeddedDocumentFactoryTest(Viewhost viewhost, Map map) { + mViewhost = viewhost; + mMap = map; + } + + @Override + public void onDocumentRequested(EmbeddedDocumentRequest request) { + PrepareDocumentRequest prepareDocumentRequest = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(SIMPLE_DOC)) + .documentSession(DocumentSession.create()) + .build(); + + PreparedDocument preparedDocument = mViewhost.prepare(prepareDocumentRequest); + assertNotNull(preparedDocument.getHandle()); + + DocumentHandle handle = preparedDocument.getHandle(); + request.resolve(EmbeddedDocumentResponse.builder().preparedDocument(preparedDocument).visualContextAttached(false).build()); + + assertNotNull(((DocumentHandleImpl) handle).getDocumentContext()); + assertNull(((DocumentHandleImpl) handle).getRootContext()); + mMap.put(handle.getUniqueId(), handle); + } + } + + private DocumentHandle render(String document, String data) { + RenderDocumentRequest request = RenderDocumentRequest.builder() + .document(new JsonStringDecodable(document)) + .data(new JsonStringDecodable(data)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + + // Bind the Viewhost + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + mViewhost.bind(mAplLayout); + + DocumentHandle handle = mViewhost.render(request); + assertNotNull(handle); + assertNotNull(((DocumentHandleImpl) handle).getRootContext()); + assertNotNull(((DocumentHandleImpl) handle).getDocumentContext()); + return handle; + } + private DocumentHandle prepareAndRender(String document, String data) { + PrepareDocumentRequest request = PrepareDocumentRequest.builder() + .document(new JsonStringDecodable(document)) + .data(new JsonStringDecodable(data)) + .documentSession(DocumentSession.create()) + .documentOptions(mDocumentOptions) + .build(); + + mAplLayout.measure(View.MeasureSpec.makeMeasureSpec(640, View.MeasureSpec.EXACTLY), View.MeasureSpec.makeMeasureSpec(480, View.MeasureSpec.EXACTLY)); + mAplLayout.layout(0, 0, 640, 480); + if (!mViewhost.isBound()) { + mViewhost.bind(mAplLayout); + } + PreparedDocument preparedDocument = mViewhost.prepare(request); + DocumentHandle handle = mViewhost.render(preparedDocument); + assertNotNull(handle); + assertNotNull(((DocumentHandleImpl) handle).getRootContext()); + assertNotNull(((DocumentHandleImpl) handle).getDocumentContext()); + return handle; + } + + private void assertPrepared(DocumentHandle handle) { + assertTrue(mRuntimeInteractionWorker.size() > 0); + mRuntimeInteractionWorker.flush(); + + int prepareCount = 0; + int errorCount = 0; + int displayedCount = 0; + int inflatedCount = 0; + while(!mMessageHandler.queue.isEmpty()) { + BaseMessage message = mMessageHandler.queue.poll(); + if (message instanceof DocumentStateChangedImpl + && DocumentState.PREPARED.toString().equals(((DocumentStateChangedImpl) message).getState()) + && handle.getUniqueId().equals(message.getDocument().getUniqueId())) { + prepareCount++; + } + if (message instanceof DocumentStateChangedImpl + && DocumentState.ERROR.toString().equals(((DocumentStateChangedImpl) message).getState()) + && handle.getUniqueId().equals(message.getDocument().getUniqueId())) { + errorCount++; + } + if (message instanceof DocumentStateChangedImpl + && DocumentState.DISPLAYED.toString().equals(((DocumentStateChangedImpl) message).getState()) + && handle.getUniqueId().equals(message.getDocument().getUniqueId())) { + displayedCount++; + } + if (message instanceof DocumentStateChangedImpl + && DocumentState.INFLATED.toString().equals(((DocumentStateChangedImpl) message).getState()) + && handle.getUniqueId().equals(message.getDocument().getUniqueId())) { + inflatedCount++; + } + } + assertEquals(1, prepareCount); + assertEquals(0, errorCount); + assertEquals(0, displayedCount); + assertEquals(0, inflatedCount); } } diff --git a/build.gradle b/build.gradle index 71fcdaf0..b5a6ec78 100644 --- a/build.gradle +++ b/build.gradle @@ -5,7 +5,7 @@ // Top-level build file where you can add configuration options common to all sub-projects/modules. -def version = "2023.3"; +def version = "2024.1"; buildscript { repositories { @@ -13,8 +13,8 @@ buildscript { jcenter() } dependencies { - classpath 'com.android.tools.build:gradle:7.0.4' - classpath 'org.jacoco:org.jacoco.core:0.8.2' + 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 @@ -36,18 +36,6 @@ subprojects { jcenter() } - publishing { - repositories { - maven { - url System.env.CODEARTIFACT_URL - credentials { - username "aws" - password System.env.CODEARTIFACT_TOKEN - } - } - } - } - afterEvaluate {project -> if (project.hasProperty("android") && project.android.flavorDimensions.size == 0) { android { diff --git a/common/build.gradle b/common/build.gradle index f922f0eb..8e97ba66 100644 --- a/common/build.gradle +++ b/common/build.gradle @@ -7,13 +7,14 @@ apply plugin: 'com.android.library' apply plugin: 'maven-publish' android { - compileSdkVersion 28 + namespace 'com.amazon.common' + compileSdk 34 + buildToolsVersion = "33.0.0" ndkVersion "23.0.7599858" - buildToolsVersion "30.0.2" defaultConfig { minSdkVersion 22 - targetSdkVersion 28 + targetSdkVersion 34 versionCode 1 versionName "1.0" @@ -24,14 +25,13 @@ android { cppFlags "" targets "common-jni" } - ndk { - // Specifies the ABI configurations for the native libraries - // that Gradle will build and package with the APK. - abiFilters 'x86', 'armeabi-v7a', 'arm64-v8a' - } } } - + publishing { + multipleVariants { + allVariants() + } + } buildTypes { release { minifyEnabled false @@ -40,7 +40,7 @@ android { } externalNativeBuild { cmake { - version "3.10.2" + version "3.18.1" path "CMakeLists.txt" } @@ -53,26 +53,29 @@ android { dependencies { - implementation 'androidx.appcompat:appcompat:1.0.0' - implementation 'androidx.test.ext:junit:1.1.0' + implementation 'androidx.appcompat:appcompat:1.2.0' + androidTestImplementation 'androidx.test.ext:junit:1.1.0' testImplementation 'junit:junit:4.12' - androidTestImplementation 'androidx.test.espresso:espresso-core:3.1.1' + androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' } -afterEvaluate { - publishing { - publications { - release(MavenPublication) { - from components.release - pom { - description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + - ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION - } +publishing { + publications { + release(MavenPublication) { + pom { + description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + + ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION + } + afterEvaluate { + from components.default } } } } +task buildHostJNI(type: com.amazon.apl.android.CMakeTask) { +} + task release(dependsOn: ['build', 'publish']) { doLast { copy { diff --git a/common/src/main/AndroidManifest.xml b/common/src/main/AndroidManifest.xml index 3bbf44a3..a5918e68 100644 --- a/common/src/main/AndroidManifest.xml +++ b/common/src/main/AndroidManifest.xml @@ -1,5 +1,4 @@ - + \ No newline at end of file diff --git a/common/src/main/java/com/amazon/common/NativeBinding.java b/common/src/main/java/com/amazon/common/NativeBinding.java index 7791d365..57c8a095 100644 --- a/common/src/main/java/com/amazon/common/NativeBinding.java +++ b/common/src/main/java/com/amazon/common/NativeBinding.java @@ -6,6 +6,9 @@ package com.amazon.common; +import android.os.Handler; +import android.os.Looper; + import androidx.annotation.NonNull; import androidx.annotation.VisibleForTesting; @@ -30,6 +33,8 @@ public class NativeBinding extends PhantomReference { // more references to it. private static final Map> sReferenceMap = new ConcurrentHashMap<>(); + private static Handler mainThreadHandler = new Handler(Looper.getMainLooper()); + // The native object handle private final long mNativeHandle; @@ -97,7 +102,12 @@ private static void remove(NativeBinding binding) { bindingSet.remove(binding); if (bindingSet.isEmpty()) { sReferenceMap.remove(binding.mNativeHandle); - nUnbind(binding.mNativeHandle); + + // Post the unbinding to the main thread since we need core's cleanup to run on the + // same thread that invokes core. + mainThreadHandler.post(() -> { + nUnbind(binding.mNativeHandle); + }); } } diff --git a/commonTest/build.gradle b/commonTest/build.gradle index 28fccb30..2419ea26 100644 --- a/commonTest/build.gradle +++ b/commonTest/build.gradle @@ -3,11 +3,13 @@ plugins { } android { - compileSdkVersion 31 + namespace 'com.amazon.common.test' + compileSdk 34 + buildToolsVersion = "33.0.0" defaultConfig { minSdkVersion 22 - targetSdkVersion 31 + targetSdkVersion 34 versionCode 1 versionName "1.0" diff --git a/commonTest/src/main/AndroidManifest.xml b/commonTest/src/main/AndroidManifest.xml index babf7002..74b7379f 100644 --- a/commonTest/src/main/AndroidManifest.xml +++ b/commonTest/src/main/AndroidManifest.xml @@ -1,4 +1,3 @@ - + \ No newline at end of file diff --git a/commonTest/src/main/java/com/amazon/common/test/Asserts.java b/commonTest/src/main/java/com/amazon/common/test/Asserts.java index caca6b96..5443d04c 100644 --- a/commonTest/src/main/java/com/amazon/common/test/Asserts.java +++ b/commonTest/src/main/java/com/amazon/common/test/Asserts.java @@ -8,6 +8,7 @@ import java.util.concurrent.TimeUnit; import static junit.framework.TestCase.assertTrue; +import static org.junit.Assert.assertEquals; import static org.junit.Assert.assertFalse; import com.amazon.common.NativeBinding; @@ -50,6 +51,12 @@ protected void finalize() { System.runFinalization(); System.gc(); + // Any viewhost objects with a finalize will take an additional gc cycle to clean up. + // This is because having a finalize causes a Finalizer to be created on the first cycle + // before finally being cleaned on the second cycle. + System.runFinalization(); + System.gc(); + finalized = latch.await(1, TimeUnit.SECONDS); if (finalized) { break; diff --git a/commonTest/.gitignore b/coreengine/.gitignore similarity index 100% rename from commonTest/.gitignore rename to coreengine/.gitignore diff --git a/coreengine/CMakeLists.txt b/coreengine/CMakeLists.txt new file mode 100644 index 00000000..1f80d809 --- /dev/null +++ b/coreengine/CMakeLists.txt @@ -0,0 +1,163 @@ +# 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 Core location +if (NOT APL_CORE_DIR) + message(FATAL_ERROR "Please specify the location of APL Core") +endif () + +set(APL_PROJECT_DIR ${APL_CORE_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_CORE_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_CORE_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_CORE_DIR}/aplcore/include/apl/action/*.h + ${APL_CORE_DIR}/aplcore/include/apl/animation/*.h + ${APL_CORE_DIR}/aplcore/include/apl/audio/*.h + ${APL_CORE_DIR}/aplcore/include/apl/command/*.h + ${APL_CORE_DIR}/aplcore/include/apl/component/*.h + ${APL_CORE_DIR}/aplcore/include/apl/content/*.h + ${APL_CORE_DIR}/aplcore/include/apl/datagrammar/*.h + ${APL_CORE_DIR}/aplcore/include/apl/document/*.h + ${APL_CORE_DIR}/aplcore/include/apl/engine/*.h + ${APL_CORE_DIR}/aplcore/include/apl/graphic/*.h + ${APL_CORE_DIR}/aplcore/include/apl/media/*.h + ${APL_CORE_DIR}/aplcore/include/apl/primitives/*.h + ${APL_CORE_DIR}/aplcore/include/apl/time/*.h + ${APL_CORE_DIR}/aplcore/include/apl/utils/*.h + ${APL_CORE_DIR}/aplcore/include/apl/touch/*.h + ${APL_CORE_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/coreengine/build.gradle b/coreengine/build.gradle new file mode 100644 index 00000000..50171640 --- /dev/null +++ b/coreengine/build.gradle @@ -0,0 +1,119 @@ +/* + * 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' + +ext { + cmakeProjectPath = projectDir.absolutePath + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cmakeProjectPath = cmakeProjectPath.replace('\\', '/') + } + if (project.hasProperty('aplCoreDir')) { + aplCoreDirCmakeArg = "-DAPL_CORE_DIR=" + aplCoreDir + } else { + aplCoreDir = "${cmakeProjectPath}/../../APLCoreEngine" + aplCoreDirCmakeArg = "-DAPL_CORE_DIR=${aplCoreDir}" + } +} + +android { + namespace 'com.amazon.apl.coreengine' + compileSdk 33 + ndkVersion "23.0.7599858" + buildToolsVersion = "33.0.0" + + sourceSets { + // Encapsulates configurations for the main source set. + main { + // Changes the directory for Java sources. The default directory is + // 'src/main/java'. + java.srcDirs = [] + } + } + + 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 aplCoreDirCmakeArg + } + } + } + + buildTypes { + releaseWithSceneGraph { + minifyEnabled false + proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + matchingFallbacks = ['release'] + externalNativeBuild { + cmake { + arguments aplCoreDirCmakeArg, "-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" + } + } + 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('aplCoreDir')}/extensions/alexaext/include/" + } + rapidjson { + headers "src/main/cpp/rapidjson/include" + headerOnly true + } + } +} + +dependencies { +} + +task buildHostJNI(type: com.amazon.apl.android.CMakeTask) { + cmakeArgs aplCoreDirCmakeArg + makeTargets "apl", "alexaext" +} + +project.afterEvaluate { + // Dump configuration settings + println "APL Core Directory: " + aplCoreDirCmakeArg + println "Android SDK Directory: " + android.sdkDirectory.path + println "Android NDK Directory: " + android.ndkDirectory.path + + compileDebugJavaWithJavac.dependsOn externalNativeBuildDebug + compileReleaseJavaWithJavac.dependsOn externalNativeBuildRelease +} + +tasks.build.dependsOn(buildHostJNI) diff --git a/coreengine/src/main/AndroidManifest.xml b/coreengine/src/main/AndroidManifest.xml new file mode 100644 index 00000000..a5918e68 --- /dev/null +++ b/coreengine/src/main/AndroidManifest.xml @@ -0,0 +1,4 @@ + + + + \ No newline at end of file diff --git a/apl/src/main/java/com/amazon/apl/enums/.gitignore b/coreengine/src/main/cpp/alexaext/include/.gitignore similarity index 100% rename from apl/src/main/java/com/amazon/apl/enums/.gitignore rename to coreengine/src/main/cpp/alexaext/include/.gitignore diff --git a/coreengine/src/main/cpp/apl/include/.gitignore b/coreengine/src/main/cpp/apl/include/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/coreengine/src/main/cpp/apl/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/coreengine/src/main/cpp/aplconfig/include/.gitignore b/coreengine/src/main/cpp/aplconfig/include/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/coreengine/src/main/cpp/aplconfig/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/coreengine/src/main/cpp/aplsgconfig/include/.gitignore b/coreengine/src/main/cpp/aplsgconfig/include/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/coreengine/src/main/cpp/aplsgconfig/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/coreengine/src/main/cpp/rapidjson/include/.gitignore b/coreengine/src/main/cpp/rapidjson/include/.gitignore new file mode 100644 index 00000000..c96a04f0 --- /dev/null +++ b/coreengine/src/main/cpp/rapidjson/include/.gitignore @@ -0,0 +1,2 @@ +* +!.gitignore \ No newline at end of file diff --git a/coreengine/src/main/java/com/amazon/apl/enums/.gitignore b/coreengine/src/main/java/com/amazon/apl/enums/.gitignore new file mode 100644 index 00000000..2a9bbbda --- /dev/null +++ b/coreengine/src/main/java/com/amazon/apl/enums/.gitignore @@ -0,0 +1 @@ +*.java \ No newline at end of file diff --git a/discovery/.gitignore b/discovery/.gitignore deleted file mode 100644 index 796b96d1..00000000 --- a/discovery/.gitignore +++ /dev/null @@ -1 +0,0 @@ -/build diff --git a/discovery/CMakeLists.txt b/discovery/CMakeLists.txt index 9a2dc59a..d8f070b4 100644 --- a/discovery/CMakeLists.txt +++ b/discovery/CMakeLists.txt @@ -9,42 +9,25 @@ set(CMAKE_POLICY_DEFAULT_CMP0077 NEW) set(CMAKE_CXX_STANDARD 11) set(CMAKE_CXX_STANDARD_REQUIRED ON) set(CMAKE_POSITION_INDEPENDENT_CODE ON) -project (discovery-jni VERSION 1.0.0 LANGUAGES C CXX) - -# set APL Core location -if (NOT APL_CORE_DIR) - message(FATAL_ERROR "Please specify the location of APL Core") -endif () -set(APL_PROJECT_DIR ${APL_CORE_DIR}) - -# Tell core to compile alexa extensions. -set(ENABLE_ALEXAEXTENSIONS ON) -set(BUILD_ALEXAEXTENSIONS ON) set(ENABLE_PIC ON) +option(INCLUDE_ALEXAEXT "Link Alexa Extension JNI" ON) +project (discovery-jni VERSION 1.0.0 LANGUAGES C CXX) -FetchContent_Declare( - aplcore - SOURCE_DIR ${APL_CORE_DIR} -) -FetchContent_MakeAvailable(aplcore) # Creates and names a library, sets it as either STATIC # or SHARED, and provides the relative paths to its source code. # You can define multiple libraries, and CMake builds them for you. # Gradle automatically packages shared libraries with your APK. +if (INCLUDE_ALEXAEXT) + add_library( # Sets the name of the library. + discovery-jni -add_library( # Sets the name of the library. - discovery-jni - - # Sets the library as a shared library. - SHARED + # Sets the library as a shared library. + SHARED - # Provides a relative path to your source file(s). - src/main/cpp/jnidiscovery.cpp - src/main/cpp/jniextensionexecutor.cpp - src/main/cpp/jniextensionproxy.cpp - src/main/cpp/jniextensionregistrar.cpp - src/main/cpp/jniextensionresource.cpp) + # Provides a relative path to your source file(s). + src/main/cpp/jnidiscovery.cpp) +endif() # Searches for a specified prebuilt library and stores the path as a # variable. Because CMake includes system libraries in the search path by @@ -57,24 +40,58 @@ if (NOT ANDROID) find_package(JNI REQUIRED) include_directories(${JAVA_INCLUDE_PATH}) include_directories(${JAVA_INCLUDE_PATH2}) + + add_library(alexaext STATIC IMPORTED) + set_target_properties(alexaext + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/.cxx/cmake/debug/host/_deps/aplcore-build/extensions/alexaext/libalexaext.a" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../coreengine/src/main/cpp/alexaext/include" + ) + add_library(alexaextjni STATIC IMPORTED) + set_target_properties(alexaextjni + PROPERTIES + IMPORTED_LOCATION + "${CMAKE_CURRENT_SOURCE_DIR}/../alexaextjni/.cxx/cmake/debug/host/libalexaextjni.a" + INTERFACE_INCLUDE_DIRECTORIES "${CMAKE_CURRENT_SOURCE_DIR}/../alexaextjni/src/main/cpp/include" + ) + + add_library(rapidjson INTERFACE) + target_include_directories(rapidjson INTERFACE + # When we're building against RapidJSON, just use the include directory we discovered above + $ + ) + + target_link_libraries(discovery-jni alexaextjni rapidjson alexaext) else() - find_library( # Sets the name of the path variable. - log-lib + if (INCLUDE_ALEXAEXT) + find_library( # Sets the name of the path variable. + log-lib - # Specifies the name of the NDK library that - # you want CMake to locate. - log) + # Specifies the name of the NDK library that + # you want CMake to locate. + log) - # Specifies libraries CMake should link to your target library. You - # can link multiple libraries, such as libraries you define in this - # build script, prebuilt third-party libraries, or system libraries. + # Specifies libraries CMake should link to your target library. You + # can link multiple libraries, such as libraries you define in this + # build script, prebuilt third-party libraries, or system libraries. - target_link_libraries( # Specifies the target library. - discovery-jni + target_link_libraries( # Specifies the target library. + discovery-jni + + # Links the target library to the log library + # included in the NDK. + ${log-lib}) + + target_compile_definitions(discovery-jni PRIVATE INCLUDE_ALEXAEXT="${INCLUDE_ALEXAEXT}") + find_package(alexaextjni REQUIRED CONFIG) + target_link_libraries(discovery-jni alexaextjni::alexaextjni) + + find_package(coreengine REQUIRED CONFIG) + target_link_libraries(discovery-jni coreengine::alexaext coreengine::rapidjson) + + endif() - # Links the target library to the log library - # included in the NDK. - ${log-lib}) endif() # Specifies a path to native header files. @@ -82,5 +99,4 @@ include_directories(src/main/cpp/include) # Common lib includes include_directories(../common/src/main/cpp/include) - -target_link_libraries(discovery-jni alexaext) \ No newline at end of file +include_directories(../alexaextjni/src/main/cpp/include) \ No newline at end of file diff --git a/discovery/build.gradle b/discovery/build.gradle index 99387395..b9c985e1 100644 --- a/discovery/build.gradle +++ b/discovery/build.gradle @@ -3,111 +3,242 @@ * SPDX-License-Identifier: Apache-2.0 */ +import org.apache.tools.ant.taskdefs.condition.Os + apply plugin: 'com.android.library' apply plugin: 'maven-publish' ext { - aplAndroidCmakeArgs = " -DCMAKE_VERBOSE_MAKEFILE=ON" - aplCoreDirCmakeArg = "-DAPL_CORE_DIR=" + projectDir + "/../../apl-core-library" - if (project.hasProperty('aplCoreDir')) { - aplCoreDirCmakeArg = "-DAPL_CORE_DIR=" + aplCoreDir - } else { - + cmakeProjectPath = projectDir.absolutePath + if (Os.isFamily(Os.FAMILY_WINDOWS)) { + cmakeProjectPath = cmakeProjectPath.replace('\\', '/') } + aplAndroidCmakeArgs = "-DCMAKE_VERBOSE_MAKEFILE=ON" } android { - compileSdkVersion 28 + namespace 'com.amazon.alexa.android.extension.discovery' + compileSdk 34 + buildToolsVersion = "33.0.0" ndkVersion "23.0.7599858" - buildToolsVersion "30.0.2" - defaultConfig { minSdkVersion 22 - targetSdkVersion 28 versionCode 1 versionName "1.0" + targetSdkVersion 34 testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" consumerProguardFiles 'consumer-rules.pro' + externalNativeBuild { cmake { // Sets optional flags for the C++ compiler. - // Enables RTTI (RunTime Type Information) support and C++ exceptions. - cppFlags "-std=c++11", "-frtti", "-fexceptions", "-DBUILD_ALEXAEXTENSIONS=On", "-DALEXAEXTENSIONS=1" - // Build the APL Core JNI library (excludes all other targets) - targets "alexaext", "discovery-jni" + cppFlags "-std=c++11", "-fno-rtti", "-fno-exceptions" // Enable APL Core JNI build, and be verbose. - arguments aplCoreDirCmakeArg, aplAndroidCmakeArgs - } - ndk { - // Specifies the ABI configurations for the native libraries - // that Gradle will build and package with the APK. - abiFilters 'x86', 'armeabi-v7a', 'arm64-v8a' + arguments aplAndroidCmakeArgs } } } - + publishing { + multipleVariants { + allVariants() + } + } buildTypes { release { minifyEnabled false proguardFiles getDefaultProguardFile('proguard-android-optimize.txt'), 'proguard-rules.pro' + buildConfigField 'boolean', 'DEBUG_LOGGING', 'false' + } + debug { + buildConfigField 'boolean', 'DEBUG_LOGGING', 'true' } } externalNativeBuild { cmake { - version "3.10.2" + version "3.18.1" path "CMakeLists.txt" } } - compileOptions { - sourceCompatibility = 1.8 - targetCompatibility = 1.8 + buildFeatures { + aidl true + buildConfig true + prefab true } + testOptions { unitTests.returnDefaultValues = true } + // Define some custom properties to distinguish the flavors. These are the + // defaults, and they can be further customized in the productFlavors + // section. + productFlavors.all { flavor -> + // Allow flavor to identify itself as the original (legacy) one + flavor.ext.isLegacy = false + + // In general, we want our library to have compatibiltiy with Java 7 to + // maximize adoption by customers. However, on a per-flavor basis this + // can be overridden. + flavor.ext.javaSourceCompatibility = JavaVersion.VERSION_1_7 + } + + // Apply additional logic to the variants based on the custom properties + libraryVariants.all { variant -> + def flavor = variant.productFlavors[0] + + // Set the per-flavor Java source compatibility + def version = flavor.ext.javaSourceCompatibility + variant.javaCompileProvider.get().doFirst { + logger.info('Configuring variant ' + variant.getName() + ' with Java ' + version + ' compatibility') + sourceCompatibility = version + targetCompatibility = version + } + + // Rename the output of the flavor considered legacy according to the + // old convention (i.e. discovery-[release|debug].aar) for consistency. + if (flavor.ext.isLegacy) { + variant.outputs.all { output -> + if (outputFile != null && outputFileName.endsWith('.aar')) { + outputFileName = "${archivesBaseName}-${variant.buildType.name}.aar" + logger.info('Setting output of ' + variant.getName() + ' to be ' + outputFileName) + } + } + } + } + + sourceSets { + standardMinSized { + java.srcDir 'src/standard/java' + } + } + + // Define the flavors which produce library variants + flavorDimensions "discoveryLibraryVariant" + productFlavors { + // This flavor is the original library for Alexa extensions. It + // contains a client that is capable of connecting to both V1 and V2 + // extensions. It also has legacy V1 service interfaces. + standard { + dimension "discoveryLibraryVariant" + targetSdkVersion 31 + ext { + // This is the legacy flavor + isLegacy = true + + // For legacy reasons, this code relies on Java 8 features such + // as lambdas and default keyword on interfaces + javaSourceCompatibility = JavaVersion.VERSION_1_8 + } + externalNativeBuild { + cmake { + // Build the APL Core JNI library (excludes all other targets) + targets "discovery-jni" + } + } + } + + // This flavor is the same as standard flavor but omits the JNI code. The JNI code + // is moved to apl-jni in this flavor and should be used with releaseMinSized variant. + standardMinSized { + dimension "discoveryLibraryVariant" + targetSdkVersion 31 + ext { + // This is the legacy flavor + isLegacy = true + + // For legacy reasons, this code relies on Java 8 features such + // as lambdas and default keyword on interfaces + javaSourceCompatibility = JavaVersion.VERSION_1_8 + } + externalNativeBuild { + cmake { + arguments aplAndroidCmakeArgs, "-DINCLUDE_ALEXAEXT=OFF" + } + } + + } + + // This flavor is a standalone library for V2 extension services. + serviceV2 { + dimension "discoveryLibraryVariant" + targetSdkVersion 27 + externalNativeBuild { + cmake { + // Build the APL Core JNI library (excludes all other targets) + targets "discovery-jni" + } + } + } + } } dependencies { implementation fileTree(dir: 'libs', include: ['*.jar']) - implementation project(":common") + standardImplementation project(":common") + standardMinSizedImplementation project(":common") + + implementation project(':coreengine') + implementation project(':alexaextjni') - implementation 'androidx.appcompat:appcompat:1.0.0' + implementation 'androidx.appcompat:appcompat:1.2.0' - implementation 'com.android.support:support-annotations:28.0.0' + implementation 'androidx.annotation:annotation:1.0.0' testImplementation 'junit:junit:4.12' - androidTestImplementation 'com.android.support:support-annotations:28.0.0' - androidTestImplementation 'androidx.test.ext:junit:1.1.0' - androidTestImplementation 'androidx.test:runner:1.1.1' + androidTestImplementation 'androidx.test.ext:junit:1.1.3' + androidTestImplementation 'androidx.test:runner:1.4.0' androidTestImplementation 'androidx.test:rules:1.1.1' - androidTestImplementation 'org.mockito:mockito-core:2.25.0' + androidTestImplementation 'org.mockito:mockito-core:3.12.4' androidTestImplementation 'com.linkedin.dexmaker:dexmaker:2.25.0' androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.25.0' - + androidTestImplementation project(path: ':commonTest') + androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-instrumentation:2.9.1' + androidTestImplementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:2.9.1' } -afterEvaluate { - publishing { - publications { - release(MavenPublication) { - from components.release - pom { - description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + - ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION - } +publishing { + publications { + release(MavenPublication) { + pom { + description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + + ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION + } + afterEvaluate { + from components.default } } } } -task release(type: Copy, dependsOn: ['build', 'publish']) { - from 'build/outputs/aar' - into '../build/discovery' -} \ No newline at end of file +tasks.build.dependsOn(assembleAndroidTest) + +task buildHostJNI(type: com.amazon.apl.android.CMakeTask) { + cmakeArgs aplAndroidCmakeArgs + makeTargets "discovery-jni" + dependsOn ':alexaextjni:buildHostJNI' +} + +task release(dependsOn: ['build', 'publish']) { + doLast { + copy { + from 'build/outputs/aar' + into '../build/discovery' + } + + copy { + from 'build/outputs/apk/androidTest/standard/debug' + into '../build/discovery/androidTest' + } + + copy { + from 'build/outputs/apk/androidTest/serviceV2/debug' + into '../build/discovery/androidTest' + } + } +} diff --git a/discovery/docs/uml/extension-runtime-permission.uml b/discovery/docs/uml/extension-runtime-permission.uml new file mode 100644 index 00000000..e3c17934 --- /dev/null +++ b/discovery/docs/uml/extension-runtime-permission.uml @@ -0,0 +1,77 @@ +@startuml + +box Runtime #LightGreen + participant ExtensionProvider as ep +end box + +box Viewhost #LightBlue + participant ExtensionDiscovery as ed + participant ExtensionBinder as eb +end box + +box Android #LightCoral + participant PackageManager as pm + participant Context as co +end box + +box Extension + participant ExtensionService as es +end box + +note left of ep + +end note + +note right of es + +end note + +note right of es + +end note + +pm -> ed: onPackageAdded +ed -> ed: registerComponentInfo +ed -> pm: checkPermission("permission", "package") +alt Extension uses permission +note right of ed + ExtensionDiscovery checks + whether Extension has permission + to serve the runtime request + using checkPermission API +end note + pm -> ed: PERMISSION_GRANTED + ed -> ed: // Add to components cache +else + pm -> ed: PERMISSION_DENIED + ed -> ed: // Do not add to components cache +end + +ep -> ed: hasExtension +ed -> ep: true if extension is in components cache, \nfalse otherwise +ep -> eb: bind +eb -> co: bindService +alt Runtime uses permission + note right of pm + Android checks whether + runtime has permission + to connect to extension + using manifest + end note + co <-> es: create service and bind + co -> eb: // true if binding is successful + eb -> ep: true +else + co -> eb: SecurityException + eb -> ep: false +end + +@enduml diff --git a/discovery/src/androidTest/AndroidManifest.xml b/discovery/src/androidTest/AndroidManifest.xml index 04ed42d5..d215d7d3 100644 --- a/discovery/src/androidTest/AndroidManifest.xml +++ b/discovery/src/androidTest/AndroidManifest.xml @@ -4,8 +4,7 @@ --> + xmlns:tools="http://schemas.android.com/tools"> diff --git a/discovery/src/main/AndroidManifest.xml b/discovery/src/main/AndroidManifest.xml index 3aed0ee0..a65bbf68 100644 --- a/discovery/src/main/AndroidManifest.xml +++ b/discovery/src/main/AndroidManifest.xml @@ -4,8 +4,7 @@ ~ SPDX-License-Identifier: Apache-2.0 --> - + GetEnv(reinterpret_cast(&env), JNI_VERSION_1_6) != JNI_OK) { return -1; //JNI_ERR } + jboolean extensionExecutorLoaded = extensionexecutor_OnLoad(vm, reserved); jboolean extensionProxyLoaded = extensionproxy_OnLoad(vm, reserved); jboolean extensionProviderLoaded = extensionprovider_OnLoad(vm, reserved); diff --git a/discovery/src/standard/java/com/amazon/alexa/android/extension/discovery/ExtensionMultiplexClient.java b/discovery/src/standard/java/com/amazon/alexa/android/extension/discovery/ExtensionMultiplexClient.java index 3383c92f..2402c01d 100644 --- a/discovery/src/standard/java/com/amazon/alexa/android/extension/discovery/ExtensionMultiplexClient.java +++ b/discovery/src/standard/java/com/amazon/alexa/android/extension/discovery/ExtensionMultiplexClient.java @@ -517,6 +517,33 @@ public String getExtensionDefinition(final String uri) { return ExtensionDiscovery.getInstance(mContext.getContext()).getExtensionDefinition(uri); } + /** + * Disconnect an extension client. + * + * @param extensionURI The service identifier. + * @param callback Callback used in {@link #connect(String, ConnectionCallback)} + * @param message Reason for disconnect. + * @return True if the connection existed and disconnect was attempted. + */ + public boolean disconnectV1(final String extensionURI, final ConnectionCallback callback, + final String message) { + Log.i(TAG, String.format("Closing connection: %s, reason: %s", extensionURI, message)); + boolean result = false; + synchronized (mConnections) { + // find the connection and remove the callback + ClientConnection connection = mConnections.get(extensionURI); + if (null != connection && connection.mServiceV1 != null) { + Log.i(TAG, "Closing connection for uri: " + extensionURI); + result = connection.unregisterCallback(callback); + if (connection.mCallbacks.isEmpty()) { + clearConnection(connection); + } + } + } + return result; + } + + /** * Disconnect an extension client. * diff --git a/discovery/src/standard/java/com/amazon/alexaext/BaseRemoteProxyDelegate.java b/discovery/src/standard/java/com/amazon/alexaext/BaseRemoteProxyDelegate.java index 75cde384..9313fc48 100644 --- a/discovery/src/standard/java/com/amazon/alexaext/BaseRemoteProxyDelegate.java +++ b/discovery/src/standard/java/com/amazon/alexaext/BaseRemoteProxyDelegate.java @@ -98,6 +98,11 @@ void disconnect(@NonNull final String uri, final String message) { reset(); } + void disconnectV1(@NonNull final String uri, final String message) { + mMultiplexClient.disconnectV1(uri, this, message); + reset(); + } + @CallSuper synchronized boolean onRequestRegistration(@NonNull final ActivityDescriptor activity, final String request) { mCachedDescriptor = activity; @@ -132,6 +137,12 @@ synchronized void onRegisteredInternal(@NonNull final ActivityDescriptor activit return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + + try { mConnection.onRegistered(this, activity); } catch (RemoteException e) { @@ -163,6 +174,15 @@ boolean onMessageInternal(@NonNull final ActivityDescriptor activity, final Stri } void onUnregisteredInternal(@NonNull final ActivityDescriptor activity) { + + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + mCachedDescriptor = null; + mRegistered = false; + + return; + } + try { mConnection.onUnregistered(this, activity); } catch (RemoteException e) { @@ -179,6 +199,11 @@ void onSessionStartedInternal(@NonNull final SessionDescriptor session) { return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + try { mConnection.onSessionStarted(this, session); } catch (RemoteException e) { @@ -191,6 +216,11 @@ void onSessionEndedInternal(@NonNull final SessionDescriptor session) { return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + try { mConnection.onSessionEnded(this, session); } catch (RemoteException e) { @@ -203,6 +233,11 @@ void onForegroundInternal(@NonNull final ActivityDescriptor activity) { return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + try { mConnection.onForeground(this, activity); } catch (RemoteException e) { @@ -215,6 +250,11 @@ void onBackgroundInternal(@NonNull final ActivityDescriptor activity) { return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + try { mConnection.onBackground(this, activity); } catch (RemoteException e) { @@ -227,6 +267,11 @@ void onHiddenInternal(@NonNull final ActivityDescriptor activity) { return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + try { mConnection.onHidden(this, activity); } catch (RemoteException e) { @@ -264,6 +309,11 @@ synchronized void onResourceReadyInternal(@NonNull final ActivityDescriptor acti return; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return; + } + try { final SurfaceHolder surface = (SurfaceHolder) resourceHolder.getFacet(SurfaceHolder.class); mConnection.resourceAvailable(this, activity, surface.getSurface(), @@ -284,6 +334,11 @@ synchronized boolean sendMessage(@NonNull final ActivityDescriptor activity, fin return false; } + if(mConnection == null) { + Log.e(TAG, "mConnection should not be null while mConnected is true"); + return false; + } + try { mConnection.send(this, activity, message); return true; diff --git a/discovery/src/standard/java/com/amazon/alexaext/ExtensionRegistrar.java b/discovery/src/standard/java/com/amazon/alexaext/ExtensionRegistrar.java index bc1133e0..14b35e08 100644 --- a/discovery/src/standard/java/com/amazon/alexaext/ExtensionRegistrar.java +++ b/discovery/src/standard/java/com/amazon/alexaext/ExtensionRegistrar.java @@ -5,6 +5,8 @@ package com.amazon.alexaext; +import android.util.Log; + import androidx.annotation.NonNull; import com.amazon.common.BoundObject; @@ -19,6 +21,8 @@ * Interface that defines access to the extensions supported by the runtime. Effectively a collection of directly registered local (built-in) */ public class ExtensionRegistrar extends BoundObject { + + private final static String TAG = "ExtensionRegistrar"; private final Map mProxies; private final Set mProviders; @@ -50,6 +54,27 @@ public ExtensionRegistrar registerExtension(ExtensionProxy proxy) { return this; } + /** + * Explicitly closes all V1 open connections of RemoteExtensionProxy. + * This method will further call onConnectionClosed of the ExtensionMultiplexClient.ConnectionCallback. + */ + public void closeAllRemoteV1Connections(){ + Log.i(TAG, "Closing connections to all remote extensions"); + int count = 0; + for (ExtensionProxy extensionProxy: mProxies.values()) { + if (extensionProxy instanceof RemoteExtensionProxy) { + RemoteExtensionProxy proxy = (RemoteExtensionProxy)extensionProxy; + try { + proxy.disconnectV1(); + count++; + } catch (Throwable e) { + Log.e(TAG, "Exception caused while cleaning connections with cause: " + e); + } + } + } + Log.i(TAG, String.format("Closing connections to all remote extensions (count=%d)", count)); + } + /** * Identifies the presence of an extension. Called when a document has * requested an extension. This method returns true if an extension matching diff --git a/discovery/src/standard/java/com/amazon/alexaext/RemoteExtensionProxy.java b/discovery/src/standard/java/com/amazon/alexaext/RemoteExtensionProxy.java index 0bbced5d..7b176a47 100644 --- a/discovery/src/standard/java/com/amazon/alexaext/RemoteExtensionProxy.java +++ b/discovery/src/standard/java/com/amazon/alexaext/RemoteExtensionProxy.java @@ -67,6 +67,10 @@ protected void finalize() throws Throwable { mProxyDelegate.disconnect(getUri(), "Clean up."); } + protected void disconnectV1() { + mProxyDelegate.disconnectV1(getUri(), "Clean up V1 connections using disconnect."); + } + @Override protected boolean initialize(@NonNull final String uri) { return mProxyDelegate.onProxyInitialize(uri); diff --git a/github.patch b/github.patch deleted file mode 100644 index 2cb73bb4..00000000 --- a/github.patch +++ /dev/null @@ -1,286 +0,0 @@ -diff --git a/apl/build.gradle b/apl/build.gradle -index 2c57679..4a3450f 100644 ---- a/apl/build.gradle -+++ b/apl/build.gradle -@@ -1,42 +1,255 @@ - /* -- * Copyright 2019 Amazon.com, Inc. or its affiliates. All Rights Reserved. -+ * 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. -+import org.apache.tools.ant.taskdefs.condition.Os - --def version = "1.10.0"; -+apply plugin: 'com.android.library' -+apply plugin: 'jacoco' -+apply plugin: 'maven-publish' - --project.buildDir = "build" -+jacoco { -+ toolVersion = '0.8.2' -+} -+ -+tasks.withType(Test) { -+ jacoco.includeNoLocationClasses = true -+ jacoco.excludes = ['jdk.internal.*'] -+} - --buildscript { -+tasks.withType(Test) { -+ testLogging { -+ events "standardOut", "started", "passed", "skipped", "failed" -+ } - -- repositories { -- google() -- jcenter() -+ filter { -+ /** -+ * This filter can be used when you want to debug some failed unit test in local test run -+ * if you wish to run locally, you should use ./gradlew :apl:testDebugUnitTest in command line -+ * For example, uncomment the line below for running tests in a specific class -+ */ -+ //includeTestsMatching "com.amazon.apl.android.font.TypefaceResolverTest" - } -- dependencies { -- classpath 'com.android.tools.build:gradle:4.1.2' -- classpath 'org.jacoco:org.jacoco.core:0.8.2' -+} -+ -+task jacocoTestReport(type: JacocoReport, dependsOn: ['test']) { -+ def mainSrc = "$project.projectDir/src/main/java" -+ -+ def fileFilter = ['**/R.class', '**/R$*.class', '**/BuildConfig.*', '**/Manifest*.*', '**/*Test*.*', '**/AutoValue_*.*'] -+ def debugTree = fileTree(dir: "$project.buildDir/intermediates/javac/release/", excludes: fileFilter) -+ -+ sourceDirectories.from(files([mainSrc])) -+ classDirectories.from(files(debugTree)) -+ -+ executionData.from(fileTree(dir: "$buildDir", includes: [ -+ "jacoco/*.exec", -+ "outputs/code_coverage/debugAndroidTest/connected/*coverage.ec" -+ ])) -+ reports { -+ xml.enabled = true -+ html.enabled = true -+ } -+} - -- // NOTE: Do not place your application dependencies here; they belong -- // in the individual module build.gradle files -+ext { -+ cmakeProjectPath = projectDir.absolutePath -+ if (Os.isFamily(Os.FAMILY_WINDOWS)) { -+ cmakeProjectPath = cmakeProjectPath.replace('\\', '/') -+ } -+ aplAndroidCmakeArgs = "-DCMAKE_VERBOSE_MAKEFILE=ON" -+ aplCoreDirCmakeArg = "-DAPL_CORE_DIR=${cmakeProjectPath}/../../apl-core-library" -+ if (project.hasProperty('aplCoreDir')) { -+ aplCoreDirCmakeArg = "-DAPL_CORE_DIR=" + aplCoreDir - } - } - --allprojects { -- if (System.getenv("MAINLINE_BUILD")) { -- project.version = "${version}-SNAPSHOT" -- } else { -- project.version = "${version}." + (System.getenv("CODEBUILD_BUILD_NUMBER") ?: "0") -+android { -+ compileSdkVersion 31 -+ ndkVersion "23.0.7599858" -+ buildToolsVersion "30.0.2" -+ defaultConfig { -+ minSdkVersion 22 -+ targetSdkVersion 31 -+ versionCode 1 -+ versionName "1.0" -+ testInstrumentationRunner "androidx.test.runner.AndroidJUnitRunner" -+ testInstrumentationRunnerArguments clearPackageData: 'true' -+ renderscriptTargetApi 22 -+ 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", "apl-jni" -+ // Enable APL Core JNI build, and be verbose. -+ arguments aplCoreDirCmakeArg, aplAndroidCmakeArgs -+ } -+ } -+ } -+ compileOptions { -+ sourceCompatibility JavaVersion.VERSION_1_8 -+ targetCompatibility JavaVersion.VERSION_1_8 -+ } -+ buildTypes { -+ release { -+// minifyEnabled true -+// proguardFiles getDefaultProguardFile('proguard-android.txt'), 'proguard-rules.pro' -+ buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") -+ buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-core\"") -+ buildConfigField 'boolean', 'DEBUG_LOGGING', 'false' -+ } -+ debug { -+ testCoverageEnabled true -+ debuggable true -+ aplAndroidCmakeArgs += " -DDEBUG_MEMORY_USE=ON" -+ buildConfigField("long", "VERSION_CODE", "${defaultConfig.versionCode}") -+ buildConfigField("String", "VERSION_NAME", "\"${defaultConfig.versionName}-core\"") -+ buildConfigField 'boolean', 'DEBUG_LOGGING', 'true' -+ } - } -- project.group = "APLViewhostAndroid" -+ // Temporary fix until alpha10 - "More than one file was found with OS independent path 'META-INF/proguard/androidx-annotations.pro" -+ packagingOptions { -+ exclude 'META-INF/proguard/androidx-annotations.pro' -+ } -+ -+ externalNativeBuild { -+ cmake { -+ version "3.18.1" - -- repositories { -- google() -- jcenter() -+ // 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" -+ } -+ } -+ lintOptions { -+ // If set to true, turns off analysis progress reporting by lint. -+ quiet false -+ // if set to true (default), stops the build if errors are found. -+ abortOnError true -+ // if true, only report errors. -+ ignoreWarnings false -+ // flag code marked for unreleasable -+ fatal 'StopShip' -+ disable 'LongLogTag' -+ } -+ testOptions { -+ animationsDisabled true -+ -+ unitTests { -+ includeAndroidResources = true -+ } - } - } - --task clean(type: Delete) { -- delete rootProject.buildDir --} -\ No newline at end of file -+dependencies { -+ compileOnly 'org.projectlombok:lombok:1.18.28' -+ implementation fileTree(include: ['*.jar'], dir: 'libs') -+ implementation 'androidx.annotation:annotation:1.4.0' -+ implementation 'androidx.core:core:1.0.0' -+ implementation 'androidx.appcompat:appcompat:1.2.0' -+ implementation 'com.github.bumptech.glide:glide:4.11.0' -+ implementation project(':common') -+ implementation(project(':discovery')) { transitive = false } -+ testImplementation 'junit:junit:4.13.2' -+ testImplementation 'org.robolectric:robolectric:4.8.1' -+ testImplementation 'org.robolectric:shadows-httpclient:4.2' -+ testImplementation 'androidx.test:core:1.1.0' -+ testImplementation 'androidx.test.ext:junit:1.1.0' -+ testImplementation 'org.mockito:mockito-core:4.7.0' -+ testImplementation 'androidx.test:rules:1.4.0' -+ androidTestImplementation 'org.mockito:mockito-core:3.12.4' -+ androidTestImplementation 'androidx.test.ext:junit:1.1.0' -+ androidTestImplementation 'androidx.test:core:1.1.0' -+ androidTestImplementation 'androidx.annotation:annotation:1.4.0' -+ androidTestImplementation 'androidx.test.espresso:espresso-core:3.4.0' -+ androidTestImplementation 'androidx.test:runner:1.4.0' -+ androidTestImplementation 'androidx.test:rules:1.4.0' -+ androidTestImplementation 'androidx.test.uiautomator:uiautomator:2.2.0' -+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker:2.25.0' -+ androidTestImplementation 'com.linkedin.dexmaker:dexmaker-mockito:2.25.0' -+ androidTestImplementation project(":commonTest") -+ androidTestImplementation 'com.squareup.leakcanary:leakcanary-android-instrumentation:2.9.1' -+ androidTestImplementation 'com.squareup.leakcanary:leakcanary-object-watcher-android:2.9.1' -+ androidTestUtil 'androidx.test:orchestrator:1.1.1' -+ api "com.google.auto.value:auto-value-annotations:1.7" -+ api 'com.google.guava:guava:27.0.1-jre' -+ annotationProcessor "com.google.auto.value:auto-value:1.7" -+ annotationProcessor 'org.projectlombok:lombok:1.18.28' -+} -+ -+ -+tasks.whenTaskAdded { theTask -> -+ if (theTask.name.startsWith("test")) { -+ theTask.outputs.upToDateWhen { false } -+ } -+} -+ -+project.afterEvaluate { -+ // Dump configuration settings -+ println "APL CMake Args: " + aplAndroidCmakeArgs -+ println "APL Core Directory: " + aplCoreDirCmakeArg -+ println "Android SDK Directory: " + android.sdkDirectory.path -+ println "Android NDK Directory: " + android.ndkDirectory.path -+ -+ // enforce native tools build runs first for enum dependencies -+ compileDebugJavaWithJavac.dependsOn externalNativeBuildDebug -+ compileReleaseJavaWithJavac.dependsOn externalNativeBuildRelease -+ -+ javaPreCompileDebug.dependsOn externalNativeBuildDebug -+ -+ tasks.test.finalizedBy(jacocoTestReport) -+} -+ -+afterEvaluate { -+ publishing { -+ publications { -+ release(MavenPublication) { -+ from components.release -+ pom { -+ description = 'Commits: APLViewhostAndroid=' + System.env.CODEBUILD_RESOLVED_SOURCE_VERSION + -+ ',APLCoreEngine=' + System.env.CORE_SOURCE_VERSION -+ } -+ } -+ } -+ } -+} -+ -+tasks.build.dependsOn(assembleAndroidTest) -+ -+apply plugin: 'checkstyle' -+ -+checkstyle { -+ configDirectory = file("$project.projectDir/checkstyle") -+ ignoreFailures = false -+} -+ -+task checkstyle(type: Checkstyle, group: 'verification') { -+ source 'src' -+ include '**/*.java' -+ exclude '**/gen/**' -+ exclude '**/R.java' -+ classpath = files() -+} -+ -+task release(dependsOn: ['build', 'publish']) { -+ doLast { -+ copy { -+ from 'build/outputs/aar' -+ into '../build/apl' -+ } -+ -+ copy { -+ from 'build/reports' -+ into '../build/apl/reports/' -+ rename 'jacocoTestReport.xml', 'coverage.xml' -+ } -+ -+ copy { -+ from 'build/outputs/apk/androidTest/debug' -+ into '../build/apl/androidTest' -+ } -+ } -+} diff --git a/gradle-version b/gradle-version index be4cea5e..cf022018 100644 --- a/gradle-version +++ b/gradle-version @@ -1 +1 @@ -7.2 \ No newline at end of file +8.3 diff --git a/gradle.properties b/gradle.properties index d7f35de3..74ff54ab 100644 --- a/gradle.properties +++ b/gradle.properties @@ -17,4 +17,3 @@ org.gradle.jvmargs=-Xmx1536m # http://www.gradle.org/docs/current/userguide/multi_project_builds.html#sec:decoupled_projects # org.gradle.parallel=true android.useAndroidX=true -android.enableJetifier=true diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 756b8def..302b4891 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -8,4 +8,4 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists zipStoreBase=GRADLE_USER_HOME zipStorePath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-7.2-all.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-8.3-all.zip diff --git a/settings.gradle b/settings.gradle index ec4c02d6..4b8bd782 100644 --- a/settings.gradle +++ b/settings.gradle @@ -3,4 +3,6 @@ * SPDX-License-Identifier: Apache-2.0 */ -include ':apl', ':common', ':commonTest', ':discovery' \ No newline at end of file +include ':apl', ':common', ':commonTest', ':discovery' +include ':coreengine' +include ':alexaextjni' \ No newline at end of file