diff options
95 files changed, 2905 insertions, 190 deletions
diff --git a/cmake/FindBundletool.cmake b/cmake/FindBundletool.cmake new file mode 100644 index 00000000000..f691b27da89 --- /dev/null +++ b/cmake/FindBundletool.cmake @@ -0,0 +1,48 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +#.rst: +# FindBundletool +# --------- +# +# Try to locate the android bundletool. +# If found, this will define the following variables: +# +# ``Bundletool_FOUND`` +# True if the Bundletool is found +# +# ``Bundletool_EXECUTABLE `` +# Path to the Bundletool executable +# +# If ``Bundletool_FOUND`` is TRUE, it will also define the following +# imported target: +# +# ``Bundletool::Bundletool`` +# The Bundletool executable + +if(DEFINED ENV{Bundletool_EXECUTABLE}) + if((NOT Bundletool_EXECUTABLE OR NOT EXISTS "${Bundletool_EXECUTABLE}") + AND EXISTS "$ENV{Bundletool_EXECUTABLE}") + set(_Bundletool_use_force FORCE) + else() + set(_Bundletool_use_force "") + endif() + set(Bundletool_EXECUTABLE "$ENV{Bundletool_EXECUTABLE}" CACHE FILEPATH + "Path to the 'bundletool' executable." ${_Bundletool_use_force}) + unset(_Bundletool_use_force) +endif() + +find_file(Bundletool_EXECUTABLE bundletool bundletool.jar) + +include(FindPackageHandleStandardArgs) +find_package_handle_standard_args(Bundletool DEFAULT_MSG Bundletool_EXECUTABLE) + +if(Bundletool_FOUND) + if(NOT TARGET Bundletool::Bundletool) + add_executable(Bundletool::Bundletool IMPORTED) + set_target_properties(Bundletool::Bundletool PROPERTIES + IMPORTED_LOCATION "${Bundletool_EXECUTABLE}") + endif() +endif() + +mark_as_advanced(Bundletool_EXECUTABLE) diff --git a/cmake/QtAndroidHelpers.cmake b/cmake/QtAndroidHelpers.cmake index 64696fff6d4..cb7d4716f12 100644 --- a/cmake/QtAndroidHelpers.cmake +++ b/cmake/QtAndroidHelpers.cmake @@ -473,3 +473,33 @@ function(qt_internal_create_source_jar) add_dependencies(install_android_source_jar_${module} ${jar_target}) add_dependencies(install_android_source_jars install_android_source_jar_${module}) endfunction() + +# The function stores Android permissions that are required by the module target. +# The stored INTERFACE_QT_ANDROID_PERMISSIONS is the transitive property. +function(qt_internal_android_add_interface_permissions target) + get_target_property(permissions ${target} QT_ANDROID_PERMISSIONS) + if(NOT permissions) + return() + endif() + + set(postprocessed_permissions "") + foreach(permission IN LISTS permissions) + # TODO: skip processing extras for now, add them back once internal API + # will cover adding extras using internal function. + list(APPEND postprocessed_permissions "name\;${permission}") + endforeach() + qt_internal_set_module_transitive_properties(${target} TYPE LINK PROPERTIES + INTERFACE_QT_ANDROID_PERMISSIONS "${postprocessed_permissions}") +endfunction() + +# The function stores Android features that are required by the module target. +# The stored INTERFACE_QT_ANDROID_FEATURES is the transitive property. +function(qt_internal_android_add_interface_features target) + get_target_property(features ${target} QT_ANDROID_FEATURES) + if(NOT features) + return() + endif() + + qt_internal_set_module_transitive_properties(${target} TYPE LINK PROPERTIES + INTERFACE_QT_ANDROID_FEATURES "${features}") +endfunction() diff --git a/cmake/QtBaseHelpers.cmake b/cmake/QtBaseHelpers.cmake index b3a4e8c7619..911b6001564 100644 --- a/cmake/QtBaseHelpers.cmake +++ b/cmake/QtBaseHelpers.cmake @@ -237,6 +237,9 @@ macro(qt_internal_qtbase_build_repo) # Needed when building qtbase for android. if(ANDROID) include(src/corelib/Qt6AndroidMacros.cmake) + include(src/corelib/Qt6AndroidDynamicFeatureHelpers.cmake) + include(src/corelib/Qt6AndroidGradleHelpers.cmake) + include(src/corelib/Qt6AndroidPermissionHelpers.cmake) endif() # Needed when building for WebAssembly. diff --git a/cmake/QtPlatformAndroid.cmake b/cmake/QtPlatformAndroid.cmake index b480b1c14a6..d56a889d114 100644 --- a/cmake/QtPlatformAndroid.cmake +++ b/cmake/QtPlatformAndroid.cmake @@ -30,6 +30,7 @@ include(UseJava) # Find JDK 8.0 find_package(Java 1.8 COMPONENTS Development REQUIRED) +find_package(Bundletool) # Ensure we are using the shared version of libc++ if(NOT ANDROID_STL STREQUAL c++_shared) @@ -117,13 +118,32 @@ function(qt_internal_android_test_runner_arguments target out_test_runner out_te set(deployment_tool "${host_bin_dir}/androiddeployqt") _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) - set(${out_test_arguments} + _qt_internal_android_get_platform_tools_path(platform_tools) + set(test_arguments "--path" "${android_build_dir}" - "--adb" "${ANDROID_SDK_ROOT}/platform-tools/adb" + "--adb" "${platform_tools}/adb" "--skip-install-root" - "--make" "\"${CMAKE_COMMAND}\" --build ${CMAKE_BINARY_DIR} --target ${target}_make_apk" - "--apk" "${android_build_dir}/${target}.apk" "--ndk-stack" "${ANDROID_NDK_ROOT}/ndk-stack" - PARENT_SCOPE ) + + if(QT_USE_ANDROID_MODERN_BUNDLE) + _qt_internal_android_get_target_deployment_dir(target_deployment_dir ${target}) + list(APPEND test_arguments + "--manifest" "${target_deployment_dir}/AndroidManifest.xml") + endif() + + if(EXISTS "${Bundletool_EXECUTABLE}" AND QT_USE_ANDROID_MODERN_BUNDLE) + list(APPEND test_arguments + "--make" "\"${CMAKE_COMMAND}\" --build ${CMAKE_BINARY_DIR} --target ${target}_make_aab" + "--aab" "${android_build_dir}/${target}.aab" + "--bundletool" "${Bundletool_EXECUTABLE}" + ) + else() + list(APPEND test_arguments + "--make" "\"${CMAKE_COMMAND}\" --build ${CMAKE_BINARY_DIR} --target ${target}_make_apk" + "--apk" "${android_build_dir}/${target}.apk" + ) + endif() + + set(${out_test_arguments} "${test_arguments}" PARENT_SCOPE) endfunction() diff --git a/cmake/QtPostProcessHelpers.cmake b/cmake/QtPostProcessHelpers.cmake index 9f220f9d78b..07fcdee268b 100644 --- a/cmake/QtPostProcessHelpers.cmake +++ b/cmake/QtPostProcessHelpers.cmake @@ -790,6 +790,8 @@ function(qt_modules_process_android_dependencies) qt_internal_get_qt_repo_known_modules(repo_known_modules) foreach (target ${repo_known_modules}) qt_internal_android_dependencies(${target}) + qt_internal_android_add_interface_permissions(${target}) + qt_internal_android_add_interface_features(${target}) endforeach() endfunction() diff --git a/cmake/QtTestHelpers.cmake b/cmake/QtTestHelpers.cmake index ad8a9d65c14..8a0666b0ceb 100644 --- a/cmake/QtTestHelpers.cmake +++ b/cmake/QtTestHelpers.cmake @@ -1176,6 +1176,13 @@ function(qt_internal_collect_command_environment out_path out_plugin_path) set(test_env_path "${test_env_path}${QT_PATH_SEPARATOR}${install_prefix}") endforeach() set(test_env_path "${test_env_path}${QT_PATH_SEPARATOR}$ENV{PATH}") + if(ANDROID) + # Add android platform tools to path. Required for the correct androidtestrunner work. + _qt_internal_android_get_platform_tools_path(platform_tools) + string(PREPEND test_env_path + "${platform_tools}" "${QT_PATH_SEPARATOR}") + endif() + string(REPLACE ";" "\;" test_env_path "${test_env_path}") set(${out_path} "${test_env_path}" PARENT_SCOPE) diff --git a/configure.cmake b/configure.cmake index 92fe4dba448..8d96ba7e1f0 100644 --- a/configure.cmake +++ b/configure.cmake @@ -1116,13 +1116,12 @@ qt_feature("network" PRIVATE ) qt_feature("printsupport" PRIVATE LABEL "Qt PrintSupport" - CONDITION QT_FEATURE_widgets AND NOT WASM + CONDITION QT_FEATURE_widgets SECTION "Module" PURPOSE "Provides the Qt PrintSupport module." ) qt_feature("sql" PRIVATE LABEL "Qt Sql" - CONDITION NOT WASM SECTION "Module" PURPOSE "Provides the Sql module." ) diff --git a/doc/global/includes/cli-build-cmake.qdocinc b/doc/global/includes/cli-build-cmake.qdocinc index b4c6ffccb4b..5f30e8632b7 100644 --- a/doc/global/includes/cli-build-cmake.qdocinc +++ b/doc/global/includes/cli-build-cmake.qdocinc @@ -28,7 +28,7 @@ \badcode \QtVersion md notepad-build cd notepad-build - C:\Qt\\1\msvc2019_64\bin\qt-cmake -GNinja C:\Examples\notepad + C:\Qt\\1\msvc2022_64\bin\qt-cmake -GNinja C:\Examples\notepad ninja \endcode diff --git a/src/3rdparty/gradle/gradle.properties b/src/3rdparty/gradle/gradle.properties index 4fe1674abd3..3472b396d96 100644 --- a/src/3rdparty/gradle/gradle.properties +++ b/src/3rdparty/gradle/gradle.properties @@ -16,3 +16,6 @@ org.gradle.parallel=true # Allow AndroidX usage android.useAndroidX=true + +# User-defined properties +@EXTRA_PROPERTIES@ diff --git a/src/android/CMakeLists.txt b/src/android/CMakeLists.txt index 7c99ce7264e..12fbca624b3 100644 --- a/src/android/CMakeLists.txt +++ b/src/android/CMakeLists.txt @@ -9,6 +9,7 @@ if (ANDROID) add_subdirectory(java) add_subdirectory(templates) add_subdirectory(templates_aar) + add_subdirectory(templates_cmake) endif() qt_internal_add_java_documentation(Global) diff --git a/src/android/REUSE.toml b/src/android/REUSE.toml index 7d5a22fd2f7..6d821d52904 100644 --- a/src/android/REUSE.toml +++ b/src/android/REUSE.toml @@ -1,7 +1,7 @@ version = 1 [[annotations]] -path = ["jar/build.gradle", "jar/settings.gradle", "templates/build.gradle"] +path = ["jar/build.gradle", "jar/settings.gradle", "templates/build.gradle", "templates_cmake/**"] precedence = "closest" comment = "double check" SPDX-FileCopyrightText = "Copyright (C) 2024 The Qt Company Ltd." diff --git a/src/android/templates/build.gradle b/src/android/templates/build.gradle index 6595cb5358f..b6c4797fde7 100644 --- a/src/android/templates/build.gradle +++ b/src/android/templates/build.gradle @@ -70,8 +70,8 @@ android { abortOnError = false } - // Do not compress Qt binary resources file aaptOptions { + // Do not compress Qt binary resources file noCompress 'rcc' } diff --git a/src/android/templates_cmake/CMakeLists.txt b/src/android/templates_cmake/CMakeLists.txt new file mode 100644 index 00000000000..546a4fc50f3 --- /dev/null +++ b/src/android/templates_cmake/CMakeLists.txt @@ -0,0 +1,46 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +set(templates_files + "${CMAKE_CURRENT_SOURCE_DIR}/settings.gradle.in" + "${CMAKE_CURRENT_SOURCE_DIR}/gradle.properties.in" +) + +set(app_template_files + "${CMAKE_CURRENT_SOURCE_DIR}/app/gradle.properties.in" + "${CMAKE_CURRENT_SOURCE_DIR}/app/build.gradle.in" + "${CMAKE_CURRENT_SOURCE_DIR}/app/AndroidManifest.xml.in" +) + +set(dynamic_feature_files + "${CMAKE_CURRENT_SOURCE_DIR}/dynamic_feature/AndroidManifest.xml.in" + "${CMAKE_CURRENT_SOURCE_DIR}/dynamic_feature/build.gradle.in" +) + +add_custom_target(Qt6AndroidCMakeTemplates + SOURCES + ${templates_files} +) + +qt_path_join(destination ${QT_INSTALL_DIR} ${INSTALL_DATADIR} "src/android/templates_cmake") + +qt_copy_or_install(FILES ${templates_files} DESTINATION "${destination}") +qt_copy_or_install(FILES ${app_template_files} DESTINATION "${destination}/app") +qt_copy_or_install(FILES ${dynamic_feature_files} DESTINATION "${destination}/dynamic_feature") + +if(NOT QT_WILL_INSTALL) + qt_internal_copy_at_build_time(TARGET Qt6AndroidCMakeTemplates + FILES ${templates_files} + DESTINATION ${destination} + ) + + qt_internal_copy_at_build_time(TARGET Qt6AndroidCMakeTemplates + FILES ${app_templates_files} + DESTINATION ${destination}/app + ) + + qt_internal_copy_at_build_time(TARGET Qt6AndroidCMakeTemplates + FILES ${dynamic_feature_files} + DESTINATION ${destination}/dynamic_feature + ) +endif() diff --git a/src/android/templates_cmake/app/AndroidManifest.xml.in b/src/android/templates_cmake/app/AndroidManifest.xml.in new file mode 100644 index 00000000000..61c33cc7657 --- /dev/null +++ b/src/android/templates_cmake/app/AndroidManifest.xml.in @@ -0,0 +1,54 @@ +<?xml version="1.0"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:dist="http://schemas.android.com/apk/distribution" + package="@APP_PACKAGE_NAME@" + android:installLocation="auto" + android:versionCode="@APP_VERSION_CODE@" + android:versionName="@APP_VERSION_NAME@"> + @APP_PERMISSIONS@ + @APP_FEATURES@ + <dist:module dist:instant="true"/> + <supports-screens + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" /> + <application + android:name="org.qtproject.qt.android.bindings.QtApplication" + android:hardwareAccelerated="true" + android:label="@APP_NAME@" + @APP_ICON@ + android:requestLegacyExternalStorage="true" + android:allowBackup="true" + android:fullBackupOnly="false"> + <activity + android:name="org.qtproject.qt.android.bindings.QtActivity" + android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" + android:launchMode="singleTop" + android:screenOrientation="unspecified" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <meta-data + android:name="android.app.lib_name" + android:value="@APP_LIB_NAME@" /> + + <meta-data + android:name="android.app.arguments" + android:value="@APP_ARGUMENTS@" /> + </activity> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.qtprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/qtprovider_paths"/> + </provider> + </application> +</manifest> diff --git a/src/android/templates_cmake/app/build.gradle.in b/src/android/templates_cmake/app/build.gradle.in new file mode 100644 index 00000000000..917f371e46f --- /dev/null +++ b/src/android/templates_cmake/app/build.gradle.in @@ -0,0 +1,53 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.10.1' + } +} + +apply plugin: '@GRADLE_PLUGIN_TYPE@' + +dependencies { + @GRADLE_DEPENDENCIES@ +} + +android { + namespace '@PACKAGE_NAME@' + compileSdkVersion '@ANDROID_COMPILE_SDK_VERSION@' + buildToolsVersion '@ANDROID_BUILD_TOOLS_VERSION@' + ndkVersion '@ANDROID_NDK_REVISION@' + + defaultConfig { + @DEFAULT_CONFIG_VALUES@ + } + + sourceSets { + main { +@SOURCE_SETS@ + } + } + + tasks.withType(JavaCompile) { + options.incremental = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + aaptOptions { + // Do not compress Qt binary resources file + noCompress 'rcc' + } + + @ANDROID_DEPLOYMENT_EXTRAS@ +} diff --git a/src/android/templates_cmake/app/gradle.properties.in b/src/android/templates_cmake/app/gradle.properties.in new file mode 100644 index 00000000000..1136551f3fa --- /dev/null +++ b/src/android/templates_cmake/app/gradle.properties.in @@ -0,0 +1 @@ +# Extend this template with the extra gradle properties applicable for Android app. diff --git a/src/android/templates_cmake/dynamic_feature/AndroidManifest.xml.in b/src/android/templates_cmake/dynamic_feature/AndroidManifest.xml.in new file mode 100644 index 00000000000..9945ec76f27 --- /dev/null +++ b/src/android/templates_cmake/dynamic_feature/AndroidManifest.xml.in @@ -0,0 +1,15 @@ +<?xml version="1.0" encoding="utf-8"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + xmlns:dist="http://schemas.android.com/apk/distribution" + split="@APP_TARGET@" + android:isFeatureSplit="true"> + <dist:module + dist:instant="true" + dist:title="@TITLE_VAR@"> + <dist:delivery> + <dist:on-demand /> + </dist:delivery> + <dist:fusing dist:include="false" /> + </dist:module> + <application android:hasCode="false"/> +</manifest> diff --git a/src/android/templates_cmake/dynamic_feature/build.gradle.in b/src/android/templates_cmake/dynamic_feature/build.gradle.in new file mode 100644 index 00000000000..917f371e46f --- /dev/null +++ b/src/android/templates_cmake/dynamic_feature/build.gradle.in @@ -0,0 +1,53 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.10.1' + } +} + +apply plugin: '@GRADLE_PLUGIN_TYPE@' + +dependencies { + @GRADLE_DEPENDENCIES@ +} + +android { + namespace '@PACKAGE_NAME@' + compileSdkVersion '@ANDROID_COMPILE_SDK_VERSION@' + buildToolsVersion '@ANDROID_BUILD_TOOLS_VERSION@' + ndkVersion '@ANDROID_NDK_REVISION@' + + defaultConfig { + @DEFAULT_CONFIG_VALUES@ + } + + sourceSets { + main { +@SOURCE_SETS@ + } + } + + tasks.withType(JavaCompile) { + options.incremental = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + aaptOptions { + // Do not compress Qt binary resources file + noCompress 'rcc' + } + + @ANDROID_DEPLOYMENT_EXTRAS@ +} diff --git a/src/android/templates_cmake/gradle.properties.in b/src/android/templates_cmake/gradle.properties.in new file mode 100644 index 00000000000..b751946190a --- /dev/null +++ b/src/android/templates_cmake/gradle.properties.in @@ -0,0 +1,12 @@ +# Project-wide Gradle settings. +# For more details on how to configure your build environment visit +# http://www.gradle.org/docs/current/userguide/build_environment.html +# Specifies the JVM arguments used for the daemon process. +# The setting is particularly useful for tweaking memory settings. +org.gradle.jvmargs=-Xmx2500m -XX:MaxMetaspaceSize=768m -XX:+HeapDumpOnOutOfMemoryError -Dfile.encoding=UTF-8 + +# Enable building projects in parallel +org.gradle.parallel=true + +# Allow AndroidX usage +android.useAndroidX=true diff --git a/src/android/templates_cmake/settings.gradle.in b/src/android/templates_cmake/settings.gradle.in new file mode 100644 index 00000000000..03954ae5f9f --- /dev/null +++ b/src/android/templates_cmake/settings.gradle.in @@ -0,0 +1,18 @@ +pluginManagement { + repositories { + google() + mavenCentral() + gradlePluginPortal() + } +} +dependencyResolutionManagement { + repositoriesMode.set(RepositoriesMode.FAIL_ON_PROJECT_REPOS) + repositories { + google() + mavenCentral() + } +} + +rootProject.name = "@ROOT_PROJECT_NAME@" +include(":app") +@SUBPROJECTS@ diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 4d83bbf794e..249b9b8c1ee 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -16,7 +16,11 @@ qt_internal_extend_sbom(WrapZLIB::WrapZLIB if(ANDROID) set(corelib_extra_cmake_files - "${CMAKE_CURRENT_SOURCE_DIR}/${QT_CMAKE_EXPORT_NAMESPACE}AndroidMacros.cmake") + "${CMAKE_CURRENT_SOURCE_DIR}/${QT_CMAKE_EXPORT_NAMESPACE}AndroidMacros.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/${QT_CMAKE_EXPORT_NAMESPACE}AndroidDynamicFeatureHelpers.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/${QT_CMAKE_EXPORT_NAMESPACE}AndroidGradleHelpers.cmake" + "${CMAKE_CURRENT_SOURCE_DIR}/${QT_CMAKE_EXPORT_NAMESPACE}AndroidPermissionHelpers.cmake" + ) endif() if(WASM) set(corelib_extra_cmake_files diff --git a/src/corelib/Qt6AndroidDynamicFeatureHelpers.cmake b/src/corelib/Qt6AndroidDynamicFeatureHelpers.cmake new file mode 100644 index 00000000000..e8a81bde1d7 --- /dev/null +++ b/src/corelib/Qt6AndroidDynamicFeatureHelpers.cmake @@ -0,0 +1,152 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause +# Collects the dynamic features for the target + +function(_qt_internal_android_add_dynamic_feature_deployment target) + get_target_property(dynamic_features ${target} _qt_android_dynamic_features) + if(NOT dynamic_features) + return() + endif() + + foreach(dynamic_feature IN LISTS dynamic_features) + get_target_property(is_imported ${dynamic_feature} IMPORTED) + if(is_imported) + message(FATAL_ERROR "Imported target ${dynamic_feature} can not be a" + " 'DYNAMIC_FEATURE'.") + endif() + + get_target_property(type ${dynamic_feature} TYPE) + if(NOT type STREQUAL "SHARED_LIBRARY") + message(FATAL_ERROR "Cannot make ${dynamic_feature} 'DYNAMIC_FEATURE'." + " The target must be the 'SHARED_LIBRARY'.") + endif() + + get_target_property(android_type ${dynamic_feature} _qt_android_target_type) + if(NOT android_type STREQUAL "" AND NOT android_type MATCHES "-NOTFOUND$" + AND NOT android_type STREQUAL "DYNAMIC_FEATURE") + message(FATAL_ERROR "Cannot make ${dynamic_feature} 'DYNAMIC_FEATURE'," + " it's already '${android_type}'. The target must be the plain 'SHARED_LIBRARY'.") + endif() + + # Mark the target as DYNAMIC_FEATURE, since we used it in this role once. + set_target_properties(${dynamic_feature} PROPERTIES _qt_android_target_type DYNAMIC_FEATURE) + _qt_internal_set_android_dynamic_feature_gradle_defaults(${dynamic_feature}) + + _qt_internal_android_get_dynamic_feature_deployment_dir(dynamic_feature_deployment_dir + ${target} ${dynamic_feature}) + _qt_internal_android_generate_target_build_gradle(${dynamic_feature} + DEPLOYMENT_DIR "${dynamic_feature_deployment_dir}") + _qt_internal_android_generate_dynamic_feature_manifest(${target} ${dynamic_feature}) + _qt_internal_android_copy_dynamic_feature(${target} ${dynamic_feature}) + endforeach() +endfunction() + +# Sets the default values of the gradle properties for the Android dynamic feature target. +function(_qt_internal_set_android_dynamic_feature_gradle_defaults target) + _qt_internal_android_java_dir(android_java_dir) + + # TODO: make androidx.core:core versionc configurable. + # Currently, it is hardcoded to 1.16.0. + set(implementation_dependencies "project(':app')" "'androidx.core:core:1.16.0'") + + set_target_properties(${target} PROPERTIES + _qt_android_gradle_java_source_dirs "src;java" + _qt_android_gradle_aidl_source_dirs "src;aidl" + _qt_android_gradle_res_source_dirs "res" + _qt_android_gradle_resources_source_dirs "resources" + _qt_android_gradle_renderscript_source_dirs "src" + _qt_android_gradle_assets_source_dirs "assets" + _qt_android_gradle_jniLibs_source_dirs "libs" + _qt_android_manifest "AndroidManifest.xml" + _qt_android_gradle_implementation_dependencies "${implementation_dependencies}" + ) +endfunction() + +# Copies the dynamic feature library to the respective gradle build tree. +function(_qt_internal_android_copy_dynamic_feature target dynamic_feature) + if(NOT TARGET ${dynamic_feature}) + message(FATAL_ERROR "${dynamic_feature} is not a target.") + endif() + + _qt_internal_android_get_dynamic_feature_deployment_dir(dynamic_feature_deployment_dir + ${target} ${dynamic_feature}) + + set(dynamic_feature_libs_dir "${dynamic_feature_deployment_dir}/libs/${CMAKE_ANDROID_ARCH_ABI}") + get_target_property(output_name ${dynamic_feature} OUTPUT_NAME) + if(NOT output_name) + get_target_property(suffix "${dynamic_feature}" SUFFIX) + set(output_name "lib${dynamic_feature}${suffix}") + endif() + set(output_file_path "${dynamic_feature_libs_dir}/${output_name}") + _qt_internal_copy_file_if_different_command(copy_command + "$<TARGET_FILE:${dynamic_feature}>" + "${output_file_path}" + ) + add_custom_command(OUTPUT ${output_file_path} + COMMAND ${copy_command} + DEPENDS ${dynamic_feature} + COMMENT "Copying ${dynamic_feature} dynamic feature to ${target} deployment directory" + ) + add_custom_target(${target}_deploy_dynamic_features DEPENDS "${output_file_path}") +endfunction() + +# Generates the feature name strings and copy them to the respective deployment directory. +function(_qt_internal_android_generate_dynamic_feature_names target) + get_target_property(dynamic_features ${target} _qt_android_dynamic_features) + if(NOT dynamic_features) + return() + endif() + + # Collect the titles + string(JOIN "\n" content + "<?xml version=\"1.0\" encoding=\"utf-8\"?>" + "<resources>" + ) + foreach(feature IN LISTS dynamic_features) + string(APPEND content "\n <string name=\"${feature}_title\">${feature}</string>") + endforeach() + string(APPEND content "\n</resources>") + + _qt_internal_android_get_target_deployment_dir(deployment_dir ${target}) + # TODO: androiddeployqt wipes the android build directory. Generate feature_names.xml target + # build dir and copy after androiddeployqt run. We should skip feature_names.xml copying when + # androiddeployqt is not involved into the deployment process anymore. + # + # set(output_file "${deployment_dir}/res/values/feature_names.xml") + set(output_file "$<TARGET_PROPERTY:${target},BINARY_DIR>/res/values/feature_names.xml") + _qt_internal_configure_file(GENERATE OUTPUT "${output_file}" CONTENT "${content}") + set(output_file_in_deployment_dir "${deployment_dir}/res/values/feature_names.xml") + add_custom_command(OUTPUT "${output_file_in_deployment_dir}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${output_file}" + "${output_file_in_deployment_dir}" + DEPENDS "${output_file}" ${target}_android_deploy_aux + VERBATIM + ) + add_custom_target(${target}_copy_feature_names + DEPENDS + "${output_file_in_deployment_dir}" + ) +endfunction() + +# Returns the dynamic feature deployment directory in the target build tree. +function(_qt_internal_android_get_dynamic_feature_deployment_dir out_var target dynamic_feature) + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + set(${out_var} "${android_build_dir}/${dynamic_feature}" PARENT_SCOPE) +endfunction() + +# Generates the AndroidManifest.xml file for the dynamic_feature. +function(_qt_internal_android_generate_dynamic_feature_manifest target dynamic_feature) + set(android_manifest_filename AndroidManifest.xml) + _qt_internal_android_get_dynamic_feature_deployment_dir(dynamic_feature_deployment_dir ${target} + ${dynamic_feature}) + + _qt_internal_android_get_template_path(template_file ${target} + "dynamic_feature/${android_manifest_filename}") + + set(APP_TARGET "${target}") + set(TITLE_VAR "@string/${dynamic_feature}_title") + + set(output_file "${dynamic_feature_deployment_dir}/AndroidManifest.xml") + _qt_internal_configure_file(CONFIGURE OUTPUT "${output_file}" INPUT "${template_file}") +endfunction() diff --git a/src/corelib/Qt6AndroidGradleHelpers.cmake b/src/corelib/Qt6AndroidGradleHelpers.cmake new file mode 100644 index 00000000000..09184c93b69 --- /dev/null +++ b/src/corelib/Qt6AndroidGradleHelpers.cmake @@ -0,0 +1,631 @@ +# Copyright (C) 2024 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Returns the path to the template file from either user defined template directory, or +# Qt default template directory. +function(_qt_internal_android_get_template_path out_var target template_name) + if(template_name STREQUAL "") + message(FATAL_ERROR "Template name is empty." + " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + endif() + + _qt_internal_android_template_dir(template_directory) + get_filename_component(template_directory "${template_directory}" ABSOLUTE) + + # The paths are ordered according to their priority, from highest to lowest. + set(possible_paths + "${template_directory}/${template_name}.in" + ) + + get_target_property(android_target_type ${target} _qt_android_target_type) + if(android_target_type STREQUAL "APPLICATION") + _qt_internal_android_get_package_source_dir(user_template_directory ${target}) + get_filename_component(user_template_directory "${user_template_directory}" ABSOLUTE) + + # Add user template with the higher priority + list(PREPEND possible_paths "${user_template_directory}/${template_name}.in") + endif() + + set(template_path "") + foreach(possible_path IN LISTS possible_paths) + if(EXISTS "${possible_path}") + set(template_path "${possible_path}") + break() + endif() + endforeach() + + if(template_path STREQUAL "") + message(FATAL_ERROR "'${template_name}' is not found." + " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + endif() + + set(${out_var} "${template_path}" PARENT_SCOPE) +endfunction() + +# Generates the settings.gradle file for the target. Writes the result to the target android build +# directory. +function(_qt_internal_android_generate_bundle_settings_gradle target) + set(settings_gradle_filename "settings.gradle") + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + set(settings_gradle_file "${android_build_dir}/${settings_gradle_filename}") + + # Skip generating the file if it's already provided by user. + get_target_property(deployment_files ${target} _qt_android_deployment_files) + if("${settings_gradle_file}" IN_LIST deployment_files) + return() + endif() + + _qt_internal_android_get_template_path(template_file ${target} "${settings_gradle_filename}") + + set(android_app_name "$<TARGET_PROPERTY:${target},QT_ANDROID_APP_NAME>") + string(JOIN "" ROOT_PROJECT_NAME + "$<IF:$<BOOL:${android_app_name}>," + "${android_app_name}," + "${target}" + ">" + ) + + set(target_dynamic_features "$<TARGET_PROPERTY:${target},_qt_android_dynamic_features>") + set(include_prefix "include(\":") + set(include_suffix "\")") + set(include_glue "${include_suffix}\n${include_prefix}") + string(JOIN "" SUBPROJECTS + "$<$<BOOL:${target_dynamic_features}>:" + "${include_prefix}" + "$<JOIN:${target_dynamic_features},${include_glue}>" + "${include_suffix}" + ">" + ) + + _qt_internal_configure_file(GENERATE OUTPUT ${settings_gradle_file} + INPUT "${template_file}") + set_property(TARGET ${target} APPEND PROPERTY _qt_android_deployment_files + "${settings_gradle_file}") +endfunction() + +# Generates the source sets for the target. +function(_qt_internal_android_get_gradle_source_sets out_var target) + set(known_types java aidl res resources renderscript assets jniLibs) + set(source_set "") + set(indent " ") + foreach(type IN LISTS known_types) + set(source_dirs + "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},_qt_android_gradle_${type}_source_dirs>>") + string(JOIN "" source_set + "${source_set}" + "$<$<BOOL:${source_dirs}>:" + "${indent}${type}.srcDirs = ['$<JOIN:${source_dirs},'$<COMMA> '>']\n" + ">" + ) + endforeach() + + set(manifest + "$<TARGET_PROPERTY:${target},_qt_android_manifest>") + string(JOIN "" source_set + "${source_set}" + "$<$<BOOL:${manifest}>:" + "${indent}manifest.srcFile '${manifest}'\n" + ">" + ) + set(${out_var} "${source_set}" PARENT_SCOPE) +endfunction() + +# Generates the gradle dependency list for the target. +function(_qt_internal_android_get_gradle_dependencies out_var target) + # Use dependencies from file tree by default + set(known_dependencies + "implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])") + foreach(dep_type implementation api) + string(JOIN "\n " dep_prefix + "\n //noinspection GradleDependency" + "${dep_type} " + ) + set(dep_postfix "") + set(dep_property "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},_qt_android_gradle_${dep_type}_dependencies>>") + string(JOIN "" known_dependencies + "${known_dependencies}" + "$<$<BOOL:${dep_property}>:" + "${dep_prefix}$<JOIN:${dep_property},${dep_postfix}${dep_prefix}>${dep_postfix}" + ">" + ) + endforeach() + set(${out_var} "${known_dependencies}" PARENT_SCOPE) +endfunction() + +# Sets the default values of the gradle properties for the Android executable target. +function(_qt_internal_set_android_application_gradle_defaults target) + _qt_internal_android_java_dir(android_java_dir) + + set(target_dynamic_features "$<TARGET_PROPERTY:${target},_qt_android_dynamic_features>") + string(JOIN "" implementation_dependencies + "$<$<BOOL:${target_dynamic_features}>:'com.google.android.play:feature-delivery:2.1.0'>" + ) + # TODO: make androidx.core:core version configurable. + # Currently, it is hardcoded to 1.16.0. + list(APPEND implementation_dependencies "'androidx.core:core:1.16.0'") + + set_target_properties(${target} PROPERTIES + _qt_android_gradle_java_source_dirs "${android_java_dir}/src;src;java" + _qt_android_gradle_aidl_source_dirs "${android_java_dir}/src;src;aidl" + _qt_android_gradle_res_source_dirs "${android_java_dir}/res;res" + _qt_android_gradle_resources_source_dirs "resources" + _qt_android_gradle_renderscript_source_dirs "src" + _qt_android_gradle_assets_source_dirs "assets" + _qt_android_gradle_jniLibs_source_dirs "libs" + _qt_android_manifest "AndroidManifest.xml" + _qt_android_gradle_implementation_dependencies "${implementation_dependencies}" + ) +endfunction() + +# Generates the build.gradle file for the target. Writes the result to the target app deployment +# directory. +function(_qt_internal_android_generate_target_build_gradle target) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "DEPLOYMENT_DIR" "") + + if(NOT arg_DEPLOYMENT_DIR) + message(FATAL_ERROR "DEPLOYMENT_DIR is not specified.") + endif() + + set(build_gradle_filename "build.gradle") + set(out_file "${arg_DEPLOYMENT_DIR}/${build_gradle_filename}") + + # Skip generating the file if it's already provided by user. + get_target_property(deployment_files ${target} _qt_android_deployment_files) + if("${out_file}" IN_LIST deployment_files) + return() + endif() + + # TODO: The current build.gradle.in templates hardcodes couple values that needs to be + # configurable in the future. For example the buildscript dependencies, or the use of + # androidx.core:core:1.13.1 and the dependency for all user applications. + + _qt_internal_android_get_gradle_property(PACKAGE_NAME ${target} + QT_ANDROID_PACKAGE_NAME "org.qtproject.example.$<MAKE_C_IDENTIFIER:${target}>") + + _qt_internal_android_get_target_sdk_build_tools_revision(ANDROID_BUILD_TOOLS_VERSION + ${target}) + + _qt_internal_detect_latest_android_platform(ANDROID_COMPILE_SDK_VERSION) + if(NOT ANDROID_COMPILE_SDK_VERSION) + message(FATAL_ERROR "Unable to detect the android platform in ${ANDROID_SDK_ROOT}. " + "Please check your Android SDK installation.") + endif() + + _qt_internal_android_get_gradle_source_sets(SOURCE_SETS ${target}) + _qt_internal_android_get_gradle_dependencies(GRADLE_DEPENDENCIES ${target}) + + _qt_internal_android_get_gradle_property(min_sdk_version ${target} + QT_ANDROID_MIN_SDK_VERSION "28") + + _qt_internal_android_get_gradle_property(target_sdk_version ${target} + QT_ANDROID_TARGET_SDK_VERSION "34") + + set(target_abis "$<TARGET_PROPERTY:${target},_qt_android_abis>") + set(target_abi_list "$<JOIN:${target_abis};${CMAKE_ANDROID_ARCH_ABI},'$<COMMA> '>") + + string(JOIN "\n " DEFAULT_CONFIG_VALUES + "resConfig 'en'" + "minSdkVersion ${min_sdk_version}" + "targetSdkVersion ${target_sdk_version}" + "ndk.abiFilters = ['${target_abi_list}']" + ) + + set(target_dynamic_features "$<TARGET_PROPERTY:${target},_qt_android_dynamic_features>") + set(include_prefix "\":") + set(include_suffix "\"") + set(include_glue "${include_suffix}$<COMMA>${include_prefix}") + string(APPEND ANDROID_DEPLOYMENT_EXTRAS + "$<$<BOOL:${target_dynamic_features}>:dynamicFeatures = [" + "${include_prefix}" + "$<JOIN:${target_dynamic_features},${include_glue}>" + "${include_suffix}]" + ">" + ) + + get_target_property(android_target_type ${target} _qt_android_target_type) + if(android_target_type STREQUAL "APPLICATION") + set(GRADLE_PLUGIN_TYPE "com.android.application") + set(template_subdir "app") + elseif(android_target_type STREQUAL "DYNAMIC_FEATURE") + set(GRADLE_PLUGIN_TYPE "com.android.dynamic-feature") + set(template_subdir "dynamic_feature") + else() + message(FATAL_ERROR "Unsupported target type for android bundle deployment ${target}") + endif() + + _qt_internal_android_get_template_path(template_file ${target} + "${template_subdir}/${build_gradle_filename}") + _qt_internal_configure_file(GENERATE + OUTPUT "${out_file}" + INPUT "${template_file}" + ) + set_property(TARGET ${target} APPEND PROPERTY _qt_android_deployment_files "${out_file}") +endfunction() + +# Prepares the artifacts for the gradle build of the target. +function(_qt_internal_android_prepare_gradle_build target) + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + _qt_internal_android_get_target_deployment_dir(deployment_dir ${target}) + + _qt_internal_android_copy_gradle_files(${target} "${android_build_dir}") + _qt_internal_android_copy_target_package_sources(${target}) + + _qt_internal_android_generate_bundle_gradle_properties(${target}) + _qt_internal_android_generate_bundle_settings_gradle(${target}) + _qt_internal_android_generate_bundle_local_properties(${target}) + _qt_internal_android_generate_target_build_gradle(${target} DEPLOYMENT_DIR "${deployment_dir}") + _qt_internal_android_generate_target_gradle_properties(${target} + DEPLOYMENT_DIR "${deployment_dir}") + _qt_internal_android_generate_target_android_manifest(${target} + DEPLOYMENT_DIR "${deployment_dir}") + + + _qt_internal_android_add_gradle_build(${target} apk) + _qt_internal_android_add_gradle_build(${target} aab) + + # Make global apk, aab, and aar targets depend on the respective targets. + _qt_internal_android_add_global_package_dependencies(${target}) + _qt_internal_create_global_apk_all_target_if_needed() +endfunction() + +# Adds the modern gradle build targets. +# These targets use the settings.gradle based build directory structure. +function(_qt_internal_android_add_gradle_build target type) + _qt_internal_android_get_deployment_type_option(android_deployment_type_option + "assembleRelease" "assembleDebug") + + _qt_internal_android_gradlew_name(gradlew_file_name) + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + set(gradlew "${android_build_dir}/${gradlew_file_name}") + + set(extra_args "") + if(type STREQUAL "aab") + set(extra_args "bundle") + endif() + + set(package_file_path "${android_build_dir}/${target}.${type}") + + _qt_internal_android_package_path(package_build_dir ${target} ${type}) + _qt_internal_android_get_deployment_type_option(deployment_type_suffix + "release" "debug") + set(package_build_file_path + "${package_build_dir}/${deployment_type_suffix}/app-${deployment_type_suffix}.${type}") + + set(extra_deps "") + if(TARGET ${target}_copy_feature_names) + list(APPEND extra_deps ${target}_copy_feature_names) + endif() + + if(TARGET ${target}_deploy_dynamic_features) + list(APPEND extra_deps ${target}_deploy_dynamic_features) + endif() + + set(gradle_scripts "$<TARGET_PROPERTY:${target},_qt_android_deployment_files>") + add_custom_command(OUTPUT "${package_file_path}" + BYPRODUCTS "${package_build_file_path}" + COMMAND + "${gradlew}" ${android_deployment_type_option} ${extra_args} + COMMAND + ${CMAKE_COMMAND} -E copy_if_different + "${package_build_file_path}" "${package_file_path}" + DEPENDS + ${target} + ${gradle_scripts} + ${target}_copy_gradle_files + ${target}_android_deploy_aux + ${extra_deps} + WORKING_DIRECTORY + "${android_build_dir}" + VERBATIM + ) + + add_custom_target(${target}_make_${type} DEPENDS "${package_file_path}") +endfunction() + +# Returns the path to the android executable package either apk or aab. +function(_qt_internal_android_package_path out_var target type) + set(supported_package_types apk aab) + if(NOT type IN_LIST supported_package_types) + message(FATAL_ERROR "Invalid package type, supported types: ${supported_package_types}") + endif() + + # aab packages are located in the bundle directory + if(type STREQUAL "aab") + set(type "bundle") + endif() + + _qt_internal_android_get_target_deployment_dir(deployment_dir ${target}) + + set(${out_var} "${deployment_dir}/build/outputs/${type}" PARENT_SCOPE) +endfunction() + +# Returns the path to the gradle build directory. +function(_qt_internal_android_gradle_template_dir out_var) + if(PROJECT_NAME STREQUAL "QtBase" OR QT_SUPERBUILD) + set(${out_var} "${QtBase_SOURCE_DIR}/src/3rdparty/gradle" PARENT_SCOPE) + else() + set(${out_var} "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_DATA}/src/3rdparty/gradle" PARENT_SCOPE) + endif() +endfunction() + +# Returns the path to the android java dir. +function(_qt_internal_android_java_dir out_var) + if(PROJECT_NAME STREQUAL "QtBase" OR QT_SUPERBUILD) + set(${out_var} "${QtBase_SOURCE_DIR}/src/android/java" PARENT_SCOPE) + else() + set(${out_var} "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_DATA}/src/android/java" PARENT_SCOPE) + endif() +endfunction() + +# Returns the platform-spefic name of the gradlew script. +function(_qt_internal_android_gradlew_name out_var) + if(CMAKE_HOST_WIN32) + set(gradlew_file_name "gradlew.bat") + else() + set(gradlew_file_name "gradlew") + endif() + + set(${out_var} "${gradlew_file_name}" PARENT_SCOPE) +endfunction() + +# Return the path to the gradlew script. +function(_qt_internal_android_gradlew_path out_var target) + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + set(${out_var} "${android_build_dir}/${gradlew_file_name}" PARENT_SCOPE) +endfunction() + +# Returns the generator expression for the gradle_property value. Defaults to the default_value +# argument. +function(_qt_internal_android_get_gradle_property out_var target target_property default_value) + set(target_property_genex "$<GENEX_EVAL:$<TARGET_PROPERTY:${target},${target_property}>>") + string(JOIN "" result + "$<IF:$<BOOL:${target_property_genex}>," + "${target_property_genex}," + "${default_value}" + ">" + ) + set(${out_var} "${result}" PARENT_SCOPE) +endfunction() + +# Generates gradle.properties for the specific target. Usually contains the +# target build type(executable, dynamic feature, library). +function(_qt_internal_android_generate_target_gradle_properties target) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "DEPLOYMENT_DIR" "") + + if(NOT arg_DEPLOYMENT_DIR) + message(FATAL_ERROR "DEPLOYMENT_DIR is not specified.") + endif() + + set(gradle_properties_file_name "gradle.properties") + set(out_file "${arg_DEPLOYMENT_DIR}/${gradle_properties_file_name}") + # Skip generating the file if it's already provided by user. + get_target_property(deployment_files ${target} _qt_android_deployment_files) + if("${out_file}" IN_LIST deployment_files) + return() + endif() + + _qt_internal_android_get_template_path(template_file ${target} + "app/${gradle_properties_file_name}") + _qt_internal_configure_file(CONFIGURE + OUTPUT "${out_file}" + INPUT "${template_file}" + ) + set_property(TARGET ${target} APPEND PROPERTY _qt_android_deployment_files "${out_file}") +endfunction() + +# Constucts generator expression that returns either target property or the default value +function(_qt_internal_android_get_manifest_property out_var target property default) + set(target_property "$<TARGET_PROPERTY:${target},${property}>") + string(JOIN "" out_genex + "$<IF:$<BOOL:${target_property}>," + "${target_property}," + "${default}" + ">" + ) + + set(${out_var} "${out_genex}" PARENT_SCOPE) +endfunction() + +# Generates the target AndroidManifest.xml +function(_qt_internal_android_generate_target_android_manifest target) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "DEPLOYMENT_DIR" "") + + if(NOT arg_DEPLOYMENT_DIR) + message(FATAL_ERROR "DEPLOYMENT_DIR is not specified.") + endif() + + set(android_manifest_filename "AndroidManifest.xml") + set(out_file "${arg_DEPLOYMENT_DIR}/${android_manifest_filename}") + + # Skip generating the file if it's already provided by user. + get_target_property(deployment_files ${target} _qt_android_deployment_files) + if("${out_file}" IN_LIST deployment_files) + return() + endif() + + _qt_internal_android_get_template_path(template_file ${target} + "app/${android_manifest_filename}") + set(temporary_file "${out_file}.tmp") + + # The file cannot be generated at cmake configure time, because androiddeployqt + # will override it at build time. We use this trick with temporary file to override + # it after the aux run of androiddeployqt. + add_custom_command(OUTPUT "${out_file}" + COMMAND ${CMAKE_COMMAND} -E copy_if_different + "${temporary_file}" + "${out_file}" + DEPENDS + "${template_file}" + "${temporary_file}" + ${target}_android_deploy_aux + ) + + _qt_internal_android_get_manifest_property(APP_PACKAGE_NAME ${target} + QT_ANDROID_PACKAGE_NAME "org.qtproject.example.$<MAKE_C_IDENTIFIER:${target}>") + _qt_internal_android_get_manifest_property(APP_NAME ${target} + QT_ANDROID_APP_NAME "${target}") + _qt_internal_android_get_manifest_property(APP_VERSION_CODE ${target} + QT_ANDROID_VERSION_CODE "1") + _qt_internal_android_get_manifest_property(APP_VERSION_NAME ${target} + QT_ANDROID_VERSION_NAME "1") + _qt_internal_android_get_manifest_property(APP_LIB_NAME ${target} OUTPUT_NAME "${target}") + + # For application icon we substitute the whole attribute definition, but not only value + # otherwise it leads to the Manifest processing issue. + set(target_property "$<TARGET_PROPERTY:${target},QT_ANDROID_APP_ICON>") + string(JOIN "" APP_ICON + "$<$<BOOL:${target_property}>:" + "android:icon=\"${target_property}\"" + ">" + ) + + file(READ "${template_file}" manifest_content) + string(REPLACE ">" "$<ANGLE-R>" manifest_content "${manifest_content}") + string(REPLACE ";" "$<SEMICOLON>" manifest_content "${manifest_content}") + string(REPLACE "," "$<COMMA>" manifest_content "${manifest_content}") + + _qt_internal_android_convert_permissions(APP_PERMISSIONS ${target} XML) + + set(feature_prefix "\n <uses-feature android:name=\"") + set(feature_suffix " \" android:required=\"false\" /$<ANGLE-R>") + set(feature_property "$<TARGET_PROPERTY:${target},QT_ANDROID_FEATURES>") + string(JOIN "" APP_FEATURES + "$<$<BOOL:${feature_property}>:" + "${feature_prefix}" + "$<JOIN:${feature_property},${feature_suffix},${feature_prefix}>" + "${feature_suffix}" + ">" + ) + + set(APP_ARGUMENTS "${QT_ANDROID_APPLICATION_ARGUMENTS}") + + _qt_internal_configure_file(GENERATE OUTPUT "${temporary_file}" + CONTENT "${manifest_content}") + + set_property(TARGET ${target} APPEND PROPERTY + _qt_android_deployment_files "${out_file}" "${temporary_file}") +endfunction() + +# Generates the top-level gradle.properties in the android-build directory +# The file contains the information about the versions of the android build +# tools, the list of supported ABIs. +function(_qt_internal_android_generate_bundle_gradle_properties target) + set(EXTRA_PROPERTIES "") + + set(gradle_properties_file_name "gradle.properties") + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + set(out_file "${android_build_dir}/${gradle_properties_file_name}") + + # Skip generating the file if it's already provided by user. + get_target_property(deployment_files ${target} _qt_android_deployment_files) + if("${out_file}" IN_LIST deployment_files) + return() + endif() + + _qt_internal_android_get_template_path(template_file ${target} "${gradle_properties_file_name}") + _qt_internal_configure_file(CONFIGURE + OUTPUT "${out_file}" + INPUT "${template_file}" + ) + set_property(TARGET ${target} APPEND PROPERTY _qt_android_deployment_files "${out_file}") +endfunction() + +# Generates the local.properties for gradle builds. Contains the path to the +# Android SDK root. +function(_qt_internal_android_generate_bundle_local_properties target) + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + set(out_file "${android_build_dir}/local.properties") + + # Skip generating the file if it's already provided by user. + get_target_property(deployment_files ${target} _qt_android_deployment_files) + if("${out_file}" IN_LIST deployment_files) + return() + endif() + + file(TO_CMAKE_PATH "${ANDROID_SDK_ROOT}" ANDROID_SDK_ROOT_NATIVE) + _qt_internal_configure_file(CONFIGURE OUTPUT "${out_file}" + CONTENT "sdk.dir=${ANDROID_SDK_ROOT_NATIVE}\n") +endfunction() + +# Copies the customized Android package sources to the Android build directory +function(_qt_internal_android_copy_target_package_sources target) + _qt_internal_android_get_package_source_dir(package_source_dir ${target}) + + if(NOT package_source_dir) + return() + endif() + get_filename_component(package_source_dir "${package_source_dir}" ABSOLUTE) + + # Collect deployment files from use-defined package source directory + file(GLOB_RECURSE package_files + LIST_DIRECTORIES false + RELATIVE "${package_source_dir}" + "${package_source_dir}/*" + ) + + # Do not copy files that we treat as CMake templates, having '.in' extention. + # + # TODO: If it ever will be an issue we may exclude only templates that are + # known by our build system. + list(FILTER package_files EXCLUDE REGEX ".+\\.in$") + + _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + list(TRANSFORM package_files PREPEND "${android_build_dir}/" OUTPUT_VARIABLE out_package_files) + list(TRANSFORM package_files PREPEND "${package_source_dir}/" OUTPUT_VARIABLE in_package_files) + + if(in_package_files) + # TODO: Add cmake < 3.26 support + if(CMAKE_VERSION VERSION_LESS 3.26) + message(FATAL_ERROR "The use of QT_ANDROID_PACKAGE_SOURCE_DIR property with + the QT_USE_ANDROID_MODERN_BUNDLE option enabled requires CMake version >= 3.26.") + endif() + set(copy_commands COMMAND "${CMAKE_COMMAND}" -E copy_directory_if_different + "${package_source_dir}" "${android_build_dir}") + else() + # We actually have nothing to deploy. + return() + endif() + + add_custom_command(OUTPUT ${out_package_files} + ${copy_commands} + DEPENDS + ${in_package_files} + VERBATIM + ) + + set_target_properties(${target} PROPERTIES _qt_android_deployment_files "${out_package_files}") +endfunction() + +# Copies gradle scripts to a build directory. +function(_qt_internal_android_copy_gradle_files target output_directory) + _qt_internal_android_gradlew_name(gradlew_file_name) + _qt_internal_android_gradle_template_dir(gradle_template_dir) + + set(gradlew_file_src "${gradle_template_dir}/${gradlew_file_name}") + set(gradlew_file_dst "${output_directory}/${gradlew_file_name}") + + add_custom_command(OUTPUT "${gradlew_file_dst}" + COMMAND + ${CMAKE_COMMAND} -E copy_if_different "${gradlew_file_src}" "${gradlew_file_dst}" + DEPENDS "${gradlew_file_src}" + COMMENT "Copying gradlew script for ${target}" + VERBATIM + ) + + # TODO: make a more precise directory copying + set(gradle_dir_src "${gradle_template_dir}/gradle") + set(gradle_dir_dst "${output_directory}/gradle") + add_custom_command(OUTPUT "${gradle_dir_dst}" + COMMAND + ${CMAKE_COMMAND} -E copy_directory "${gradle_dir_src}" "${gradle_dir_dst}" + DEPENDS "${gradle_dir_src}" + COMMENT "Copying gradle support files for ${target}" + VERBATIM + ) + + add_custom_target(${target}_copy_gradle_files + DEPENDS + "${gradlew_file_dst}" + "${gradle_dir_dst}" + ) +endfunction() diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index aa0e5bfed24..ed97a42c83f 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -120,6 +120,29 @@ function(_qt_internal_generate_android_permissions_json out_result target) set(${out_result} "${result}" PARENT_SCOPE) endfunction() +# Add the specific dynamic library as the dynamic feature for the Android application target. +function(qt6_add_android_dynamic_features target) + cmake_parse_arguments(PARSE_ARGV 1 arg "" "" "FEATURE_TARGETS") + if(NOT QT_USE_ANDROID_MODERN_BUNDLE) + message(FATAL_ERROR "qt6_add_android_dynamic_features is only supported with" + " 'QT_USE_ANDROID_MODERN_BUNDLE' enabled.") + endif() + if(NOT TARGET ${target}) + message(FATAL_ERROR "${target} is not a target. Cannot add the dynamic features.") + endif() + get_target_property(android_type ${target} _qt_android_target_type) + if(NOT android_type STREQUAL "APPLICATION") + message(FATAL_ERROR "${target} is not an android executable target." + " Cannot add the dynamic features.") + endif() + if(arg_FEATURE_TARGETS) + set_property(TARGET ${target} + APPEND PROPERTY _qt_android_dynamic_features ${arg_FEATURE_TARGETS}) + else() + message(WARNING "No dynamic features provided.") + endif() +endfunction() + # Generate the deployment settings json file for a cmake target. function(qt6_android_generate_deployment_settings target) # Information extracted from mkspecs/features/android/android_deployment_settings.prf @@ -324,8 +347,9 @@ function(qt6_android_generate_deployment_settings target) __qt_internal_collect_plugin_library_files_v2("${target}" "${plugin_targets}" plugin_targets) string(APPEND file_contents " \"android-deploy-plugins\":\"${plugin_targets}\",\n") - _qt_internal_generate_android_permissions_json(permissions_json_array "${target}") - string(APPEND file_contents " \"permissions\": ${permissions_json_array},\n") + + _qt_internal_android_convert_permissions(permissions_genex ${target} JSON) + string(APPEND file_contents " \"permissions\": ${permissions_genex},\n") # App binary string(APPEND file_contents @@ -415,44 +439,6 @@ if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) endfunction() endif() -function(_qt_internal_add_android_permission target) - if(NOT TARGET ${target}) - message(FATAL_ERROR "Empty or invalid target for adding Android permission: (${target})") - endif() - - cmake_parse_arguments(arg "" "NAME" "ATTRIBUTES" ${ARGN}) - - if(NOT arg_NAME) - message(FATAL_ERROR "NAME for adding Android permission cannot be empty (${target})") - endif() - - set(permission_entry "${arg_NAME}") - - if(arg_ATTRIBUTES) - # Permission with additional attributes - list(LENGTH arg_ATTRIBUTES attributes_len) - math(EXPR attributes_modulus "${attributes_len} % 2") - if(NOT (attributes_len GREATER 1 AND attributes_modulus EQUAL 0)) - message(FATAL_ERROR "Android permission: ${arg_NAME} attributes: ${arg_ATTRIBUTES} must" - " be name-value pairs (for example: minSdkVersion 30)") - endif() - # Combine name-value pairs - set(index 0) - set(attributes "") - while(index LESS attributes_len) - list(GET arg_ATTRIBUTES ${index} name) - math(EXPR index "${index} + 1") - list(GET arg_ATTRIBUTES ${index} value) - string(APPEND attributes "android:${name}=\'${value}\' ") - math(EXPR index "${index} + 1") - endwhile() - set(permission_entry "${permission_entry}\;${attributes}") - endif() - - # Append the permission to the target's property - set_property(TARGET ${target} APPEND PROPERTY QT_ANDROID_PERMISSIONS "${permission_entry}") -endfunction() - function(qt6_add_android_permission target) _qt_internal_add_android_permission(${ARGV}) endfunction() @@ -519,18 +505,6 @@ function(qt6_android_add_apk_target target) ">" ) - # Make global apk and aab targets depend on the current apk target. - if(TARGET aab) - add_dependencies(aab ${target}_make_aab) - endif() - if(TARGET aar) - add_dependencies(aar ${target}_make_aar) - endif() - if(TARGET apk) - add_dependencies(apk ${target}_make_apk) - _qt_internal_create_global_apk_all_target_if_needed() - endif() - _qt_internal_android_get_deployment_tool(deployment_tool) # No need to use genex for the BINARY_DIR since it's read-only. @@ -719,6 +693,10 @@ function(qt6_android_add_apk_target target) ) add_dependencies(${target}_make_aab ${target}_prepare_apk_dir) + # Make global apk, aab, and aar targets depend on the respective targets. + _qt_internal_android_add_global_package_dependencies(${target}) + _qt_internal_create_global_apk_all_target_if_needed() + if(QT_IS_ANDROID_MULTI_ABI_EXTERNAL_PROJECT) # When building per-ABI external projects we only need to copy ABI-specific libraries and # resources to the "main" ABI android build folder. @@ -1661,13 +1639,26 @@ endfunction() # module and is executed implicitly when configuring user projects. function(_qt_internal_android_executable_finalizer target) set_property(TARGET ${target} PROPERTY _qt_android_executable_finalizer_called TRUE) + set_property(TARGET ${target} PROPERTY _qt_android_in_finalizer "EXECUTABLE") _qt_internal_expose_android_package_source_dir_to_ide(${target}) _qt_internal_configure_android_multiabi_target("${target}") qt6_android_generate_deployment_settings("${target}") - qt6_android_add_apk_target("${target}") + if(QT_USE_ANDROID_MODERN_BUNDLE) + _qt_internal_android_generate_dynamic_feature_names("${target}") + _qt_internal_android_add_dynamic_feature_deployment("${target}") + + _qt_internal_android_prepare_gradle_build("${target}") + _qt_internal_android_add_aux_deployment("${target}") + + _qt_internal_collect_apk_dependencies_defer() + _qt_internal_collect_apk_imported_dependencies_defer("${target}") + else() + qt6_android_add_apk_target("${target}") + endif() _qt_internal_android_create_runner_wrapper("${target}") + set_property(TARGET ${target} PROPERTY _qt_android_in_finalizer "") endfunction() # Helper to add the android executable finalizer. @@ -1737,8 +1728,9 @@ function(_qt_internal_android_app_runner_arguments target out_runner_path out_ar set(${out_runner_path} "${runner_dir}/qt-android-runner.py" PARENT_SCOPE) _qt_internal_android_get_target_android_build_dir(android_build_dir ${target}) + _qt_internal_android_get_platform_tools_path(platform_tools) set(${out_arguments} - "--adb" "${ANDROID_SDK_ROOT}/platform-tools/adb" + "--adb" "${platform_tools}/adb" "--build-path" "${android_build_dir}" "--apk" "${android_build_dir}/${target}.apk" PARENT_SCOPE @@ -1754,8 +1746,13 @@ function(_qt_internal_android_get_target_android_build_dir out_build_dir target) endif() endfunction() +function(_qt_internal_android_get_target_deployment_dir out_deploy_dir target) + _qt_internal_android_get_target_android_build_dir(build_dir ${target}) + set(${out_deploy_dir} "${build_dir}/app" PARENT_SCOPE) +endfunction() + function(_qt_internal_expose_android_package_source_dir_to_ide target) - get_target_property(android_package_source_dir ${target} QT_ANDROID_PACKAGE_SOURCE_DIR) + _qt_internal_android_get_package_source_dir(android_package_source_dir ${target}) if(android_package_source_dir) get_target_property(target_source_dir ${target} SOURCE_DIR) if(NOT IS_ABSOLUTE "${android_package_source_dir}") @@ -1778,6 +1775,71 @@ function(_qt_internal_expose_android_package_source_dir_to_ide target) endif() endfunction() +function(_qt_internal_android_add_aux_deployment target) + cmake_parse_arguments(arg "" "OUTPUT_TARGET_NAME;DEPLOYMENT_DIRECTORY" "EXTRA_ARGS" ${ARGN}) + _qt_internal_validate_all_args_are_parsed(arg) + + string(JOIN "" deployment_file + "$<GENEX_EVAL:" + "$<TARGET_PROPERTY:${target},QT_ANDROID_DEPLOYMENT_SETTINGS_FILE>" + ">" + ) + + _qt_internal_android_get_deployment_tool(deployment_tool) + if(arg_DEPLOYMENT_DIRECTORY) + set(deployment_dir "${arg_DEPLOYMENT_DIRECTORY}") + else() + _qt_internal_android_get_target_deployment_dir(deployment_dir ${target}) + endif() + + cmake_policy(PUSH) + if(POLICY CMP0116) + # Without explicitly setting this policy to NEW, we get a warning + # even though we ensure there's actually no problem here. + # See https://gitlab.kitware.com/cmake/cmake/-/issues/21959 + cmake_policy(SET CMP0116 NEW) + set(relative_to_dir ${CMAKE_CURRENT_BINARY_DIR}) + else() + set(relative_to_dir ${CMAKE_BINARY_DIR}) + endif() + + set(target_file_copy_relative_path + "libs/${CMAKE_ANDROID_ARCH_ABI}/$<TARGET_FILE_NAME:${target}>") + _qt_internal_copy_file_if_different_command(copy_command + "$<TARGET_FILE:${target}>" + "${deployment_dir}/${target_file_copy_relative_path}" + ) + + _qt_internal_android_get_use_terminal_for_deployment(uses_terminal) + + # TODO: We use androiddeployqt to collect target depdenencies and produce the lib.xml file + # which autoloads the collected libraries. Should be done using GRE and transitive properties + # in the future. + set(libs_xml "${deployment_dir}/res/values/libs.xml") + add_custom_command(OUTPUT "${libs_xml}" + COMMAND ${copy_command} + COMMAND "${deployment_tool}" + --input "${deployment_file}" + --output "${deployment_dir}" + --builddir "${relative_to_dir}" + --aux-mode + ${arg_EXTRA_ARGS} + #TODO: Support signing + COMMENT "Deploying Android artifacts for ${target}" + DEPENDS "${target}" "${deployment_file}" + VERBATIM + ${uses_terminal} + ) + + if(NOT arg_OUTPUT_TARGET_NAME) + set(arg_OUTPUT_TARGET_NAME ${target}_android_deploy_aux) + endif() + + add_custom_target(${arg_OUTPUT_TARGET_NAME} DEPENDS "${libs_xml}") + + cmake_policy(POP) +endfunction() + # Enables the terminal usage for the add_custom_command calls when verbose deployment is enabled. function(_qt_internal_android_get_use_terminal_for_deployment out_var) if(QT_ENABLE_VERBOSE_DEPLOYMENT) @@ -1822,6 +1884,45 @@ function(_qt_internal_android_get_deployment_type_option out_var release_flag de endif() endfunction() +# Returns the path to the android template directory, that are used by CMake +# deployment procedures. +function(_qt_internal_android_template_dir out_var) + if(PROJECT_NAME STREQUAL "QtBase" OR QT_SUPERBUILD) + set(${out_var} "${QtBase_SOURCE_DIR}/src/android/templates_cmake" PARENT_SCOPE) + else() + set(${out_var} + "${QT6_INSTALL_PREFIX}/${QT6_INSTALL_DATA}/src/android/templates_cmake" PARENT_SCOPE) + endif() +endfunction() + +# Return the path to the target template directory if it's set for the target. +# Then this path is stored in the target QT_ANDROID_PACKAGE_SOURCE_DIR property +# and can only be effectively read in android finalizers. +function(_qt_internal_android_get_package_source_dir out_var target) + get_target_property(in_finalizer ${target} _qt_android_in_finalizer) + if(NOT in_finalizer) + message(FATAL_ERROR "The '_qt_internal_android_get_package_source_dir' function is" + " called outside the Android finalizer." + " This is the Qt issue, please report a bug at https://bugreports.qt.io.") + endif() + get_target_property(package_src_dir ${target} QT_ANDROID_PACKAGE_SOURCE_DIR) + if(NOT package_src_dir) + set(package_src_dir "") + endif() + set(${out_var} "${package_src_dir}" PARENT_SCOPE) +endfunction() + +# Add target_make_<apk|aab> as the depednecy for the respective global apk/aab +# target. +function(_qt_internal_android_add_global_package_dependencies target) + foreach(type apk aab aar) + # Make global apk and aab targets depend on the current apk target. + if(TARGET ${type} AND TARGET ${target}_make_${type}) + add_dependencies(${type} ${target}_make_${type}) + endif() + endforeach() +endfunction() + function(_qt_internal_android_get_target_abis out_abis target) get_target_property(target_abis ${target} QT_ANDROID_ABIS) if(target_abis) @@ -1840,5 +1941,10 @@ function(_qt_internal_android_get_target_abis out_abis target) set(${out_abis} "${android_abis}" PARENT_SCOPE) endfunction() +# Returns the path to the Android platform-tools(adb is located there). +function(_qt_internal_android_get_platform_tools_path out_var) + set(${out_var} "${ANDROID_SDK_ROOT}/platform-tools" PARENT_SCOPE) +endfunction() + set(QT_INTERNAL_ANDROID_TARGET_BUILD_DIR_SUPPORT ON CACHE INTERNAL "Indicates that Qt supports per-target Android build directories") diff --git a/src/corelib/Qt6AndroidPermissionHelpers.cmake b/src/corelib/Qt6AndroidPermissionHelpers.cmake new file mode 100644 index 00000000000..7f851e14667 --- /dev/null +++ b/src/corelib/Qt6AndroidPermissionHelpers.cmake @@ -0,0 +1,126 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +# Generates the generator expression that converts the 'target' +# QT_ANDROID_PERMISSIONS property to the specific 'type'. +# +# It's expected that each element in QT_ANDROID_PERMISSIONS list has specific +# format: +# <name>\;<permission>\\\;<extra1>\;<value>\\\;<extra2>\;<value> +# +# Synopsis +# _qt_internal_android_convert_permissions(out_var target <JSON|XML>) +# +# Arguments +# +# `out_var` +# The name of the variable where the resulting generator expression is +# stored. +# +# `target` +# The name of the target. +# +# `JSON` +# Generate JSON array known by androiddeployqt. +# +# `XML` +# Generate XML content compatible with AndroidManifest.xml. +function(_qt_internal_android_convert_permissions out_var target type) + set(permissions_property "$<TARGET_PROPERTY:${target},QT_ANDROID_PERMISSIONS>") + set(permissions_genex "$<$<BOOL:${permissions_property}>:") + if(type STREQUAL "JSON") + set(pref "{ \"") + set(post "\" }") + set(indent "\n ") + string(APPEND permissions_genex + "[${indent}$<JOIN:" + "$<JOIN:" + "${pref}$<JOIN:" + "${permissions_property}," + "${post}$<COMMA>${indent}${pref}" + ">${post}," + "\": \"" + ">," + "\"$<COMMA> \"" + ">\n ]" + ) + elseif(type STREQUAL "XML") + set(pref "<uses-permission\n android:") + set(post "' /$<ANGLE-R>\n") + string(APPEND permissions_genex + "$<JOIN:" + "$<JOIN:" + "${pref}$<JOIN:" + "${permissions_property}," + "${post}${pref}" + ">${post}\n," + "='" + ">," + "' android:" + ">" + ) + else() + message(FATAL_ERROR "Invalid type ${type}. Supported types: JSON, XML") + endif() + string(APPEND permissions_genex ">") + + set(${out_var} "${permissions_genex}" PARENT_SCOPE) +endfunction() + +# Add the specific Android permission to the target. The permission is stored +# in the QT_ANDROID_PERMISSIONS property(the property is not a public API) +# and has the following format: +# <name>\;<permission>\\\;<extra1>\;<value>\\\;<extra2>\;<value> +# +# Synopsis +# _qt_internal_add_android_permission(target NAME <permission> +# ATTRIBUTES <extra1> <value1> +# [<extra2> <value2>]... +# ) +# +# Arguments +# +# `target` +# The Android target. +# +# `NAME` +# The permission name. E.g. 'android.permission.CAMERA'. +# +# `ATTRIBUTES` +# Extra permission attribute key-value pairs. +# See https://developer.android.com/guide/topics/manifest/uses-permission-element +# for details. +function(_qt_internal_add_android_permission target) + if(NOT TARGET ${target}) + message(FATAL_ERROR "Empty or invalid target for adding Android permission: (${target})") + endif() + + cmake_parse_arguments(arg "" "NAME" "ATTRIBUTES" ${ARGN}) + + if(NOT arg_NAME) + message(FATAL_ERROR "NAME for adding Android permission cannot be empty (${target})") + endif() + + set(permission_entry "name\;${arg_NAME}") + if(arg_ATTRIBUTES) + # Permission with additional attributes + list(LENGTH arg_ATTRIBUTES attributes_len) + math(EXPR attributes_modulus "${attributes_len} % 2") + if(NOT (attributes_len GREATER 1 AND attributes_modulus EQUAL 0)) + message(FATAL_ERROR "Android permission: ${arg_NAME} attributes: ${arg_ATTRIBUTES}" + " must be name-value pairs (for example: minSdkVersion 30)") + endif() + # Combine name-value pairs + set(index 0) + while(index LESS attributes_len) + list(GET arg_ATTRIBUTES ${index} name) + math(EXPR index "${index} + 1") + list(GET arg_ATTRIBUTES ${index} value) + string(APPEND permission_entry "\\\;${name}\;${value}") + math(EXPR index "${index} + 1") + endwhile() + endif() + + # Append the permission to the target's property + set_property(TARGET ${target} APPEND PROPERTY QT_ANDROID_PERMISSIONS "${permission_entry}") +endfunction() diff --git a/src/corelib/Qt6CoreConfigExtras.cmake.in b/src/corelib/Qt6CoreConfigExtras.cmake.in index 15405197a61..8a88d558fa8 100644 --- a/src/corelib/Qt6CoreConfigExtras.cmake.in +++ b/src/corelib/Qt6CoreConfigExtras.cmake.in @@ -30,6 +30,10 @@ _qt_internal_setup_deploy_support() if(ANDROID_PLATFORM) include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]") + include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]") + include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]") + include("${CMAKE_CURRENT_LIST_DIR}/@[email protected]") + _qt_internal_create_global_android_targets() _qt_internal_collect_default_android_abis() if(__qt_Core_targets_file_included) diff --git a/src/corelib/Qt6CoreMacros.cmake b/src/corelib/Qt6CoreMacros.cmake index 7b522f99abb..5bc85ae7f7a 100644 --- a/src/corelib/Qt6CoreMacros.cmake +++ b/src/corelib/Qt6CoreMacros.cmake @@ -683,6 +683,9 @@ function(_qt_internal_create_executable target) ) qt6_android_apply_arch_suffix("${target}") + if(QT_USE_ANDROID_MODERN_BUNDLE) + _qt_internal_set_android_application_gradle_defaults(${target}) + endif() else() cmake_policy(PUSH) __qt_internal_set_cmp0156() diff --git a/src/corelib/animation/qabstractanimation.cpp b/src/corelib/animation/qabstractanimation.cpp index d74894e1e42..4388122b7b9 100644 --- a/src/corelib/animation/qabstractanimation.cpp +++ b/src/corelib/animation/qabstractanimation.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default /*! \class QAbstractAnimation diff --git a/src/corelib/animation/qabstractanimation.h b/src/corelib/animation/qabstractanimation.h index 69a30556a3a..b4b43e64a1d 100644 --- a/src/corelib/animation/qabstractanimation.h +++ b/src/corelib/animation/qabstractanimation.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QABSTRACTANIMATION_H #define QABSTRACTANIMATION_H diff --git a/src/corelib/animation/qabstractanimation_p.h b/src/corelib/animation/qabstractanimation_p.h index d6c245f36f0..51c635f1bed 100644 --- a/src/corelib/animation/qabstractanimation_p.h +++ b/src/corelib/animation/qabstractanimation_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QABSTRACTANIMATION_P_H #define QABSTRACTANIMATION_P_H diff --git a/src/corelib/animation/qanimationgroup.cpp b/src/corelib/animation/qanimationgroup.cpp index d2572a7462b..ae96069e86f 100644 --- a/src/corelib/animation/qanimationgroup.cpp +++ b/src/corelib/animation/qanimationgroup.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default /*! \class QAnimationGroup diff --git a/src/corelib/animation/qanimationgroup.h b/src/corelib/animation/qanimationgroup.h index 412e2d442ea..07d24ae1f73 100644 --- a/src/corelib/animation/qanimationgroup.h +++ b/src/corelib/animation/qanimationgroup.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QANIMATIONGROUP_H #define QANIMATIONGROUP_H diff --git a/src/corelib/animation/qanimationgroup_p.h b/src/corelib/animation/qanimationgroup_p.h index 334f780968a..a09bc6ebcc6 100644 --- a/src/corelib/animation/qanimationgroup_p.h +++ b/src/corelib/animation/qanimationgroup_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QANIMATIONGROUP_P_H #define QANIMATIONGROUP_P_H diff --git a/src/corelib/animation/qparallelanimationgroup.cpp b/src/corelib/animation/qparallelanimationgroup.cpp index 86e9417b595..0b43a73434f 100644 --- a/src/corelib/animation/qparallelanimationgroup.cpp +++ b/src/corelib/animation/qparallelanimationgroup.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default /*! \class QParallelAnimationGroup diff --git a/src/corelib/animation/qparallelanimationgroup.h b/src/corelib/animation/qparallelanimationgroup.h index 77bc6eabac7..9442f4f7355 100644 --- a/src/corelib/animation/qparallelanimationgroup.h +++ b/src/corelib/animation/qparallelanimationgroup.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QPARALLELANIMATIONGROUP_H #define QPARALLELANIMATIONGROUP_H diff --git a/src/corelib/animation/qparallelanimationgroup_p.h b/src/corelib/animation/qparallelanimationgroup_p.h index 62c53d36097..482b9555c7d 100644 --- a/src/corelib/animation/qparallelanimationgroup_p.h +++ b/src/corelib/animation/qparallelanimationgroup_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QPARALLELANIMATIONGROUP_P_H #define QPARALLELANIMATIONGROUP_P_H diff --git a/src/corelib/animation/qpauseanimation.cpp b/src/corelib/animation/qpauseanimation.cpp index 344b21946e3..74e22e2f053 100644 --- a/src/corelib/animation/qpauseanimation.cpp +++ b/src/corelib/animation/qpauseanimation.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default /*! \class QPauseAnimation diff --git a/src/corelib/animation/qpauseanimation.h b/src/corelib/animation/qpauseanimation.h index f661459f835..bf7863a171c 100644 --- a/src/corelib/animation/qpauseanimation.h +++ b/src/corelib/animation/qpauseanimation.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QPAUSEANIMATION_H #define QPAUSEANIMATION_H diff --git a/src/corelib/animation/qpropertyanimation.cpp b/src/corelib/animation/qpropertyanimation.cpp index 04f048af753..b4c6b6ff8a6 100644 --- a/src/corelib/animation/qpropertyanimation.cpp +++ b/src/corelib/animation/qpropertyanimation.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default /*! \class QPropertyAnimation diff --git a/src/corelib/animation/qpropertyanimation.h b/src/corelib/animation/qpropertyanimation.h index 038c202b8f3..590a6ddaf15 100644 --- a/src/corelib/animation/qpropertyanimation.h +++ b/src/corelib/animation/qpropertyanimation.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QPROPERTYANIMATION_H #define QPROPERTYANIMATION_H diff --git a/src/corelib/animation/qpropertyanimation_p.h b/src/corelib/animation/qpropertyanimation_p.h index ef5534cd9c3..c1918ae1bf8 100644 --- a/src/corelib/animation/qpropertyanimation_p.h +++ b/src/corelib/animation/qpropertyanimation_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QPROPERTYANIMATION_P_H #define QPROPERTYANIMATION_P_H diff --git a/src/corelib/animation/qsequentialanimationgroup.cpp b/src/corelib/animation/qsequentialanimationgroup.cpp index 260481dbef5..d11249ca7ed 100644 --- a/src/corelib/animation/qsequentialanimationgroup.cpp +++ b/src/corelib/animation/qsequentialanimationgroup.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default /*! \class QSequentialAnimationGroup diff --git a/src/corelib/animation/qsequentialanimationgroup.h b/src/corelib/animation/qsequentialanimationgroup.h index 6786078170d..b7f9c1b17a1 100644 --- a/src/corelib/animation/qsequentialanimationgroup.h +++ b/src/corelib/animation/qsequentialanimationgroup.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QSEQUENTIALANIMATIONGROUP_H #define QSEQUENTIALANIMATIONGROUP_H diff --git a/src/corelib/animation/qsequentialanimationgroup_p.h b/src/corelib/animation/qsequentialanimationgroup_p.h index cbdf204d0a6..131902b5aa5 100644 --- a/src/corelib/animation/qsequentialanimationgroup_p.h +++ b/src/corelib/animation/qsequentialanimationgroup_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QSEQUENTIALANIMATIONGROUP_P_H #define QSEQUENTIALANIMATIONGROUP_P_H diff --git a/src/corelib/animation/qvariantanimation.cpp b/src/corelib/animation/qvariantanimation.cpp index be5c09519e9..bd12d5dae95 100644 --- a/src/corelib/animation/qvariantanimation.cpp +++ b/src/corelib/animation/qvariantanimation.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #include "qvariantanimation.h" #include "qvariantanimation_p.h" diff --git a/src/corelib/animation/qvariantanimation.h b/src/corelib/animation/qvariantanimation.h index 4bdb9713578..172ee0d6090 100644 --- a/src/corelib/animation/qvariantanimation.h +++ b/src/corelib/animation/qvariantanimation.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QVARIANTANIMATION_H #define QVARIANTANIMATION_H diff --git a/src/corelib/animation/qvariantanimation_p.h b/src/corelib/animation/qvariantanimation_p.h index 0ac238a882b..5fdd9666dba 100644 --- a/src/corelib/animation/qvariantanimation_p.h +++ b/src/corelib/animation/qvariantanimation_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2016 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:default #ifndef QVARIANTANIMATION_P_H #define QVARIANTANIMATION_P_H diff --git a/src/corelib/io/qsavefile.cpp b/src/corelib/io/qsavefile.cpp index 91f168f20f6..a7d101dc124 100644 --- a/src/corelib/io/qsavefile.cpp +++ b/src/corelib/io/qsavefile.cpp @@ -1,5 +1,6 @@ // Copyright (C) 2012 David Faure <[email protected]> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:critical reason:guaranteed-behavior #include "qsavefile.h" diff --git a/src/corelib/io/qsavefile.h b/src/corelib/io/qsavefile.h index bf0a91bae74..5e8cffe7c38 100644 --- a/src/corelib/io/qsavefile.h +++ b/src/corelib/io/qsavefile.h @@ -1,5 +1,6 @@ // Copyright (C) 2012 David Faure <[email protected]> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:header-decls-only #ifndef QSAVEFILE_H #define QSAVEFILE_H diff --git a/src/corelib/io/qsavefile_p.h b/src/corelib/io/qsavefile_p.h index 50ecdad2daf..e1dcc0abe23 100644 --- a/src/corelib/io/qsavefile_p.h +++ b/src/corelib/io/qsavefile_p.h @@ -1,5 +1,6 @@ // Copyright (C) 2013 David Faure <[email protected]> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only +// Qt-Security score:significant reason:header-decls-only #ifndef QSAVEFILE_P_H #define QSAVEFILE_P_H diff --git a/src/corelib/thread/qmutex.cpp b/src/corelib/thread/qmutex.cpp index bd3b269c0b7..832d87332b0 100644 --- a/src/corelib/thread/qmutex.cpp +++ b/src/corelib/thread/qmutex.cpp @@ -120,8 +120,8 @@ void QBasicMutex::destroyInternal(void *ptr) Locks the mutex. If another thread has locked the mutex then this call will block until that thread has unlocked it. - Calling this function multiple times on the same mutex from the - same thread will cause a \e dead-lock. + If the mutex was already locked by the current thread, this call will + never return, causing a \e dead-lock. \sa unlock() */ @@ -140,9 +140,6 @@ void QBasicMutex::destroyInternal(void *ptr) If the lock was obtained, the mutex must be unlocked with unlock() before another thread can successfully lock it. - Calling this function multiple times on the same mutex from the - same thread will cause a \e dead-lock. - \sa lock(), unlock() */ @@ -157,9 +154,6 @@ void QBasicMutex::destroyInternal(void *ptr) If the lock was obtained, the mutex must be unlocked with unlock() before another thread can successfully lock it. - Calling this function multiple times on the same mutex from the - same thread will cause a \e dead-lock. - \sa lock(), unlock() */ @@ -172,9 +166,6 @@ void QBasicMutex::destroyInternal(void *ptr) If the lock was obtained, the mutex must be unlocked with unlock() before another thread can successfully lock it. - Calling this function multiple times on the same mutex from the - same thread will cause a \e dead-lock. - \sa lock(), unlock() */ @@ -202,9 +193,6 @@ void QBasicMutex::destroyInternal(void *ptr) If the lock was obtained, the mutex must be unlocked with unlock() before another thread can successfully lock it. - Calling this function multiple times on the same mutex from the - same thread will cause a \e dead-lock. - \sa lock(), unlock() */ @@ -222,9 +210,6 @@ void QBasicMutex::destroyInternal(void *ptr) If the lock was obtained, the mutex must be unlocked with unlock() before another thread can successfully lock it. - Calling this function multiple times on the same mutex from the - same thread will cause a \e dead-lock. - \sa lock(), unlock() */ diff --git a/src/gui/kernel/qscreen.cpp b/src/gui/kernel/qscreen.cpp index afe6de35305..caf4cb7fe5b 100644 --- a/src/gui/kernel/qscreen.cpp +++ b/src/gui/kernel/qscreen.cpp @@ -34,6 +34,12 @@ QT_BEGIN_NAMESPACE \note Both physical and logical DPI are expressed in device-independent dots. Multiply by QScreen::devicePixelRatio() to get device-dependent density. + To obtain a QScreen object, use QGuiApplication::primaryScreen() for the + primary screen, or QGuiApplication::screens() to get a list of all screens. + + \sa QGuiApplication::primaryScreen() + \sa QGuiApplication::screens() + \inmodule QtGui */ diff --git a/src/gui/kernel/qtestsupport_gui.cpp b/src/gui/kernel/qtestsupport_gui.cpp index ca62798ddfd..dfcea928fd8 100644 --- a/src/gui/kernel/qtestsupport_gui.cpp +++ b/src/gui/kernel/qtestsupport_gui.cpp @@ -24,8 +24,19 @@ QT_BEGIN_NAMESPACE /*! \since 5.0 + \overload - Returns \c true, if \a window is active within \a timeout milliseconds. Otherwise returns \c false. + The \a timeout is in milliseconds. +*/ +bool QTest::qWaitForWindowActive(QWindow *window, int timeout) +{ + return qWaitForWindowActive(window, QDeadlineTimer{timeout, Qt::TimerType::PreciseTimer}); +} + +/*! + \since 6.10 + + Returns \c true, if \a window is active within \a timeout. Otherwise returns \c false. The method is useful in tests that call QWindow::show() and rely on the window actually being active (i.e. being visible and having focus) before proceeding. @@ -38,7 +49,7 @@ QT_BEGIN_NAMESPACE \sa qWaitForWindowExposed(), qWaitForWindowFocused(), QWindow::isActive() */ -Q_GUI_EXPORT bool QTest::qWaitForWindowActive(QWindow *window, int timeout) +bool QTest::qWaitForWindowActive(QWindow *window, QDeadlineTimer timeout) { if (Q_UNLIKELY(!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation))) { qWarning() << "qWaitForWindowActive was called on a platform that doesn't support window" @@ -52,6 +63,17 @@ Q_GUI_EXPORT bool QTest::qWaitForWindowActive(QWindow *window, int timeout) } /*! + \since 6.10 + \overload + + This function uses the default timeout of 5 seconds. +*/ +bool QTest::qWaitForWindowActive(QWindow *window) +{ + return qWaitForWindowActive(window, Internal::defaultTryTimeout); +} + +/*! \since 6.7 Returns \c true, if \a window is the focus window within \a timeout. Otherwise returns \c false. @@ -73,9 +95,31 @@ Q_GUI_EXPORT bool QTest::qWaitForWindowFocused(QWindow *window, QDeadlineTimer t } /*! + \since 6.10 + \overload + + This function uses the default timeout of 5 seconds. +*/ +bool QTest::qWaitForWindowFocused(QWindow *window) +{ + return qWaitForWindowFocused(window, Internal::defaultTryTimeout); +} + +/*! \since 5.0 + \overload + + The \a timeout is in milliseconds. +*/ +bool QTest::qWaitForWindowExposed(QWindow *window, int timeout) +{ + return qWaitForWindowExposed(window, std::chrono::milliseconds(timeout)); +} - Returns \c true, if \a window is exposed within \a timeout milliseconds. Otherwise returns \c false. +/*! + \since 6.10 + + Returns \c true, if \a window is exposed within \a timeout. Otherwise returns \c false. The method is useful in tests that call QWindow::show() and rely on the window actually being being visible before proceeding. @@ -86,11 +130,22 @@ Q_GUI_EXPORT bool QTest::qWaitForWindowFocused(QWindow *window, QDeadlineTimer t \sa qWaitForWindowActive(), QWindow::isExposed() */ -Q_GUI_EXPORT bool QTest::qWaitForWindowExposed(QWindow *window, int timeout) +bool QTest::qWaitForWindowExposed(QWindow *window, QDeadlineTimer timeout) { return QTest::qWaitFor([&]() { return window->isExposed(); }, timeout); } +/*! + \since 6.10 + \overload + + This function uses the default timeout of 5 seconds. +*/ +bool QTest::qWaitForWindowExposed(QWindow *window) +{ + return qWaitForWindowExposed(window, Internal::defaultTryTimeout); +} + namespace QTest { QTouchEventSequence::~QTouchEventSequence() diff --git a/src/gui/kernel/qtestsupport_gui.h b/src/gui/kernel/qtestsupport_gui.h index 951d9df1c7c..39278c2f034 100644 --- a/src/gui/kernel/qtestsupport_gui.h +++ b/src/gui/kernel/qtestsupport_gui.h @@ -23,12 +23,16 @@ Q_GUI_EXPORT bool qt_handleTouchEventv2(QWindow *w, const QPointingDevice *devic namespace QTest { -[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowActive(QWindow *window, - int timeout = static_cast<int>(Internal::defaultTryTimeout.count())); -[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowFocused(QWindow *window, - QDeadlineTimer timeout = Internal::defaultTryTimeout); -[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowExposed(QWindow *window, - int timeout = static_cast<int>(Internal::defaultTryTimeout.count())); +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowActive(QWindow *window, int timeout); +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowActive(QWindow *window, QDeadlineTimer timeout); +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowActive(QWindow *window); + +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowFocused(QWindow *window, QDeadlineTimer timeout); +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowFocused(QWindow *window); + +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowExposed(QWindow *window, int timeout); +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowExposed(QWindow *window, QDeadlineTimer timeout); +[[nodiscard]] Q_GUI_EXPORT bool qWaitForWindowExposed(QWindow *window); Q_GUI_EXPORT QPointingDevice * createTouchDevice(QInputDevice::DeviceType devType = QInputDevice::DeviceType::TouchScreen, QInputDevice::Capabilities caps = QInputDevice::Capability::Position); diff --git a/src/gui/painting/qbackingstoredefaultcompositor.cpp b/src/gui/painting/qbackingstoredefaultcompositor.cpp index c1452ca7688..41ab7c97f8c 100644 --- a/src/gui/painting/qbackingstoredefaultcompositor.cpp +++ b/src/gui/painting/qbackingstoredefaultcompositor.cpp @@ -460,11 +460,20 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) + bool translucentBackground, + qreal sourceTransformFactor) { if (!rhi) return QPlatformBackingStore::FlushFailed; + // Note, the sourceTransformFactor is different from the sourceDevicePixelRatio, + // as the former may reflect the fact that the region and offset is pre-transformed, + // in which case we don't need to do a full transform here based on the source DPR. + // In the default case where no explicit source transform has been passed, we fall + // back to the source device pixel ratio. + if (!sourceTransformFactor) + sourceTransformFactor = sourceDevicePixelRatio; + Q_ASSERT(textures); // may be empty if there are no render-to-texture widgets at all, but null it cannot be if (!m_rhi) { @@ -508,7 +517,7 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo const QImage::Format format = QImage::toImageFormat(graphicsBuffer->format()); const QSize size = graphicsBuffer->size(); QImage wrapperImage(graphicsBuffer->data(), size.width(), size.height(), graphicsBuffer->bytesPerLine(), format); - toTexture(wrapperImage, rhi, resourceUpdates, scaledRegion(region, sourceDevicePixelRatio, offset), &flags); + toTexture(wrapperImage, rhi, resourceUpdates, scaledRegion(region, sourceTransformFactor, offset), &flags); gotTextureFromGraphicsBuffer = true; graphicsBuffer->unlock(); if (graphicsBuffer->origin() == QPlatformGraphicsBuffer::OriginBottomLeft) @@ -516,7 +525,7 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo } } if (!gotTextureFromGraphicsBuffer) - toTexture(backingStore, rhi, resourceUpdates, scaledRegion(region, sourceDevicePixelRatio, offset), &flags); + toTexture(backingStore, rhi, resourceUpdates, scaledRegion(region, sourceTransformFactor, offset), &flags); ensureResources(resourceUpdates, swapchain->renderPassDescriptor()); @@ -549,7 +558,7 @@ QPlatformBackingStore::FlushResult QBackingStoreDefaultCompositor::flush(QPlatfo // The backingstore is for the entire tlw. In case of native children, offset tells the position // relative to the tlw. The window rect is scaled by the source device pixel ratio to get // the source rect. - const QPoint sourceWindowOffset = scaledOffset(offset, sourceDevicePixelRatio); + const QPoint sourceWindowOffset = scaledOffset(offset, sourceTransformFactor); const QRect srcRect = toBottomLeftRect(sourceWindowRect.translated(sourceWindowOffset), m_texture->pixelSize().height()); const QMatrix3x3 source = sourceTransform(srcRect, m_texture->pixelSize(), origin); QMatrix4x4 target; // identity diff --git a/src/gui/painting/qbackingstoredefaultcompositor_p.h b/src/gui/painting/qbackingstoredefaultcompositor_p.h index c5a8ffd328e..cdc9d098099 100644 --- a/src/gui/painting/qbackingstoredefaultcompositor_p.h +++ b/src/gui/painting/qbackingstoredefaultcompositor_p.h @@ -41,7 +41,8 @@ public: const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground); + bool translucentBackground, + qreal sourceTransformFactor); private: enum UpdateUniformOption { diff --git a/src/gui/painting/qplatformbackingstore.cpp b/src/gui/painting/qplatformbackingstore.cpp index 21e89d67fd2..2acc5ef7c52 100644 --- a/src/gui/painting/qplatformbackingstore.cpp +++ b/src/gui/painting/qplatformbackingstore.cpp @@ -213,14 +213,15 @@ QPlatformBackingStore::FlushResult QPlatformBackingStore::rhiFlush(QWindow *wind const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) + bool translucentBackground, + qreal sourceTransformFactor) { auto &surfaceSupport = d_ptr->surfaceSupport[window->surfaceType()]; return surfaceSupport.compositor.flush(this, surfaceSupport.rhiSupport.rhi(), surfaceSupport.rhiSupport.swapChainForWindow(window), window, sourceDevicePixelRatio, region, offset, textures, - translucentBackground); + translucentBackground, sourceTransformFactor); } /*! diff --git a/src/gui/painting/qplatformbackingstore.h b/src/gui/painting/qplatformbackingstore.h index a6cb43b4e66..86035b98bea 100644 --- a/src/gui/painting/qplatformbackingstore.h +++ b/src/gui/painting/qplatformbackingstore.h @@ -151,7 +151,8 @@ public: const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground); + bool translucentBackground, + qreal sourceTransformFactor = 0); virtual QImage toImage() const; diff --git a/src/gui/painting/qrhibackingstore.cpp b/src/gui/painting/qrhibackingstore.cpp index d59cc2d83c5..3d9932e5ee2 100644 --- a/src/gui/painting/qrhibackingstore.cpp +++ b/src/gui/painting/qrhibackingstore.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qrhibackingstore_p.h" +#include "qpa/qplatformwindow.h" #include <private/qimage_p.h> QT_BEGIN_NAMESPACE @@ -43,10 +44,22 @@ void QRhiBackingStore::flush(QWindow *flushedWindow, const QRegion ®ion, cons createRhi(flushedWindow, rhiConfig); } + // The backing store operates on behalf of its window(), even if we're + // flushing a child window, so pull the source DPR from the window(). + const qreal sourceDevicePixelRatio = window()->devicePixelRatio(); + + // QBackingStore::flush will convert the region and offset from device independent + // pixels to native pixels before calling QPlatformBackingStore::flush, which means + // we can't pass on the window's DPR as the sourceTransformFactor, as that will include + // the Qt scale factor, which has already been applied. Instead we ask the platform + // window, which only reflect the remaining scale factor from the OS. + const qreal sourceTransformFactor = flushedWindow->handle()->devicePixelRatio(); + static QPlatformTextureList emptyTextureList; bool translucentBackground = m_image.hasAlphaChannel(); - rhiFlush(flushedWindow, flushedWindow->devicePixelRatio(), - region, offset, &emptyTextureList, translucentBackground); + rhiFlush(flushedWindow, sourceDevicePixelRatio, + region, offset, &emptyTextureList, translucentBackground, + sourceTransformFactor); } QImage::Format QRhiBackingStore::format() const diff --git a/src/network/socket/qnativesocketengine_unix.cpp b/src/network/socket/qnativesocketengine_unix.cpp index 6949eec9560..b78d2829704 100644 --- a/src/network/socket/qnativesocketengine_unix.cpp +++ b/src/network/socket/qnativesocketengine_unix.cpp @@ -822,6 +822,12 @@ qint64 QNativeSocketEnginePrivate::nativePendingDatagramSize() const recvResult = getsockopt(socketDescriptor, SOL_SOCKET, SO_NREAD, &value, &valuelen); if (recvResult != -1) recvResult = value; +#elif defined(Q_OS_VXWORKS) + // VxWorks: use ioctl(FIONREAD) to query the number of bytes available + int available = 0; + int ioctlResult = ::ioctl(socketDescriptor, FIONREAD, &available); + if (ioctlResult != -1) + recvResult = available; #else // We need to grow the buffer to fit the entire datagram. // We start at 1500 bytes (the MTU for Ethernet V2), which should catch diff --git a/src/opengl/qopenglcompositorbackingstore.cpp b/src/opengl/qopenglcompositorbackingstore.cpp index 20c86fb8adc..95dd1a562a6 100644 --- a/src/opengl/qopenglcompositorbackingstore.cpp +++ b/src/opengl/qopenglcompositorbackingstore.cpp @@ -163,7 +163,8 @@ QPlatformBackingStore::FlushResult QOpenGLCompositorBackingStore::rhiFlush(QWind const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) + bool translucentBackground, + qreal sourceTransformFactor) { // QOpenGLWidget/QQuickWidget content provided as textures. The raster content goes on top. @@ -171,6 +172,7 @@ QPlatformBackingStore::FlushResult QOpenGLCompositorBackingStore::rhiFlush(QWind Q_UNUSED(offset); Q_UNUSED(translucentBackground); Q_UNUSED(sourceDevicePixelRatio); + Q_UNUSED(sourceTransformFactor); m_rhi = rhi(window); Q_ASSERT(m_rhi); diff --git a/src/opengl/qopenglcompositorbackingstore_p.h b/src/opengl/qopenglcompositorbackingstore_p.h index 4512e2d589e..b50629000cf 100644 --- a/src/opengl/qopenglcompositorbackingstore_p.h +++ b/src/opengl/qopenglcompositorbackingstore_p.h @@ -48,7 +48,8 @@ public: const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) override; + bool translucentBackground, + qreal sourceTransformFactor = 0) override; const QPlatformTextureList *textures() const { return m_textures; } diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.h b/src/plugins/platforms/cocoa/qcocoabackingstore.h index 71b6015a54d..79aed15a1d0 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.h +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.h @@ -44,7 +44,8 @@ public: const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) override; + bool translucentBackground, + qreal sourceTransformFactor) override; QImage toImage() const override; QPlatformGraphicsBuffer *graphicsBuffer() const override; diff --git a/src/plugins/platforms/cocoa/qcocoabackingstore.mm b/src/plugins/platforms/cocoa/qcocoabackingstore.mm index 78d23b01dea..186aeaac44d 100644 --- a/src/plugins/platforms/cocoa/qcocoabackingstore.mm +++ b/src/plugins/platforms/cocoa/qcocoabackingstore.mm @@ -457,7 +457,8 @@ QPlatformBackingStore::FlushResult QCALayerBackingStore::rhiFlush(QWindow *windo const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) + bool translucentBackground, + qreal sourceTransformFactor) { if (!m_buffers.back()) { qCWarning(lcQpaBackingStore) << "Flush requested with no back buffer. Ignoring."; @@ -466,7 +467,8 @@ QPlatformBackingStore::FlushResult QCALayerBackingStore::rhiFlush(QWindow *windo finalizeBackBuffer(); - return QPlatformBackingStore::rhiFlush(window, sourceDevicePixelRatio, region, offset, textures, translucentBackground); + return QPlatformBackingStore::rhiFlush(window, sourceDevicePixelRatio, + region, offset, textures, translucentBackground, sourceTransformFactor); } QImage QCALayerBackingStore::toImage() const diff --git a/src/plugins/platforms/windows/qwindowswindow.cpp b/src/plugins/platforms/windows/qwindowswindow.cpp index 81a3f1d37b7..a4bb59cf42c 100644 --- a/src/plugins/platforms/windows/qwindowswindow.cpp +++ b/src/plugins/platforms/windows/qwindowswindow.cpp @@ -1191,6 +1191,11 @@ QMargins QWindowsGeometryHint::frame(const QWindow *w, const QRect &geometry, bool QWindowsGeometryHint::handleCalculateSize(const QWindow *window, const QMargins &customMargins, const MSG &msg, LRESULT *result) { + // Prevent adding any border for frameless window + if (msg.wParam && window->flags() & Qt::FramelessWindowHint) { + *result = 0; + return true; + } // Return 0 to remove the window's border const bool clientAreaExpanded = window->flags() & Qt::ExpandedClientAreaHint; if (msg.wParam && clientAreaExpanded) { diff --git a/src/plugins/platforms/xcb/qxcbbackingstore.cpp b/src/plugins/platforms/xcb/qxcbbackingstore.cpp index 8353fac6a92..fda47944d9d 100644 --- a/src/plugins/platforms/xcb/qxcbbackingstore.cpp +++ b/src/plugins/platforms/xcb/qxcbbackingstore.cpp @@ -870,7 +870,8 @@ QPlatformBackingStore::FlushResult QXcbBackingStore::rhiFlush(QWindow *window, const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) + bool translucentBackground, + qreal sourceTransformFactor) { if (!m_image || m_image->size().isEmpty()) return FlushFailed; @@ -878,7 +879,7 @@ QPlatformBackingStore::FlushResult QXcbBackingStore::rhiFlush(QWindow *window, m_image->flushScrolledRegion(true); auto result = QPlatformBackingStore::rhiFlush(window, sourceDevicePixelRatio, region, offset, - textures, translucentBackground); + textures, translucentBackground, sourceTransformFactor); if (result != FlushSuccess) return result; QXcbWindow *platformWindow = static_cast<QXcbWindow *>(window->handle()); diff --git a/src/plugins/platforms/xcb/qxcbbackingstore.h b/src/plugins/platforms/xcb/qxcbbackingstore.h index 674640780eb..5cda9e1ab79 100644 --- a/src/plugins/platforms/xcb/qxcbbackingstore.h +++ b/src/plugins/platforms/xcb/qxcbbackingstore.h @@ -27,7 +27,8 @@ public: const QRegion ®ion, const QPoint &offset, QPlatformTextureList *textures, - bool translucentBackground) override; + bool translucentBackground, + qreal sourceTransformFactor) override; QImage toImage() const override; QPlatformGraphicsBuffer *graphicsBuffer() const override; diff --git a/src/plugins/styles/modernwindows/qwindows11style.cpp b/src/plugins/styles/modernwindows/qwindows11style.cpp index 82e978862f8..e2e4fce357e 100644 --- a/src/plugins/styles/modernwindows/qwindows11style.cpp +++ b/src/plugins/styles/modernwindows/qwindows11style.cpp @@ -2212,8 +2212,9 @@ void QWindows11Style::polish(QWidget* widget) pal.setColor(scrollarea->viewport()->backgroundRole(), Qt::transparent); scrollarea->viewport()->setPalette(pal); scrollarea->viewport()->setProperty("_q_original_background_palette", originalPalette); - if (qobject_cast<QTableView *>(widget)) - widget->setAttribute(Qt::WA_Hover, true); + // QTreeView & QListView are already set in the base windowsvista style + if (auto table = qobject_cast<QTableView *>(widget)) + table->viewport()->setAttribute(Qt::WA_Hover, true); } } diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp index e93c3038fca..a27c52f5a99 100644 --- a/src/tools/androiddeployqt/main.cpp +++ b/src/tools/androiddeployqt/main.cpp @@ -1475,10 +1475,23 @@ bool readInputFile(Options *options) for (const QJsonValue &value : permissions) { if (value.isObject()) { QJsonObject permissionObj = value.toObject(); - QString name = permissionObj.value("name"_L1).toString(); + QString name; QString extras; - if (permissionObj.contains("extras"_L1)) - extras = permissionObj.value("extras"_L1).toString().trimmed(); + for (auto it = permissionObj.begin(); it != permissionObj.end(); ++it) { + if (it.key() == "name"_L1) { + name = it.value().toString(); + } else { + extras.append(" android:"_L1) + .append(it.key()) + .append("=\""_L1) + .append(it.value().toString()) + .append("\""_L1); + } + } + if (name.isEmpty()) { + fprintf(stderr, "Missing permission 'name' in permission specification"); + return false; + } options->applicationPermissions.insert(name, extras); } } @@ -1580,8 +1593,11 @@ bool copyAndroidTemplate(const Options &options) if (options.verbose) fprintf(stdout, "Copying Android package template.\n"); - if (!copyGradleTemplate(options)) - return false; + if (!options.auxMode) { + // Gradle is not configured and is not running in aux mode + if (!copyGradleTemplate(options)) + return false; + } if (!copyAndroidTemplate(options, "/src/android/templates"_L1)) return false; diff --git a/src/tools/androidtestrunner/main.cpp b/src/tools/androidtestrunner/main.cpp index eb4c84fbd75..564a332c091 100644 --- a/src/tools/androidtestrunner/main.cpp +++ b/src/tools/androidtestrunner/main.cpp @@ -40,6 +40,7 @@ struct Options QString buildPath; QString manifestPath; QString adbCommand{"adb"_L1}; + QString bundletoolPath; QString serial; QString makeCommand; QString package; @@ -48,7 +49,7 @@ struct Options QString stdoutFileName; QHash<QString, QString> outFiles; QStringList amStarttestArgs; - QString apkPath; + QString packagePath; QString ndkStackPath; QList<QStringList> preTestRunAdbCommands; bool showLogcatOutput = false; @@ -114,6 +115,24 @@ static bool execAdbCommand(const QStringList &args, QByteArray *output = nullptr return execCommand(g_options.adbCommand, argsWithSerial, output, verbose); } +static bool execBundletoolCommand(const QStringList &args, QByteArray *output = nullptr, + bool verbose = true) +{ + QString java("java"_L1); + QStringList argsFull = QStringList() << "-jar"_L1 << g_options.bundletoolPath << args; + return execCommand(java, argsFull, output, verbose); +} + +static void setPackagePath(const QString &path) +{ + if (!g_options.packagePath.isEmpty()) { + qCritical("Both --aab and --apk options provided. This is not supported."); + g_options.helpRequested = true; + return; + } + g_options.packagePath = path; +} + static bool execCommand(const QString &command, QByteArray *output = nullptr, bool verbose = true) { auto args = QProcess::splitCommand(command); @@ -133,6 +152,11 @@ static bool parseOptions() g_options.helpRequested = true; else g_options.adbCommand = arguments.at(++i); + } else if (argument.compare("--bundletool"_L1, Qt::CaseInsensitive) == 0) { + if (i + 1 == arguments.size()) + g_options.helpRequested = true; + else + g_options.bundletoolPath = arguments.at(++i); } else if (argument.compare("--path"_L1, Qt::CaseInsensitive) == 0) { if (i + 1 == arguments.size()) g_options.helpRequested = true; @@ -152,7 +176,12 @@ static bool parseOptions() if (i + 1 == arguments.size()) g_options.helpRequested = true; else - g_options.apkPath = arguments.at(++i); + setPackagePath(arguments.at(++i)); + } else if (argument.compare("--aab"_L1, Qt::CaseInsensitive) == 0) { + if (i + 1 == arguments.size()) + g_options.helpRequested = true; + else + setPackagePath(arguments.at(++i)); } else if (argument.compare("--activity"_L1, Qt::CaseInsensitive) == 0) { if (i + 1 == arguments.size()) g_options.helpRequested = true; @@ -200,7 +229,7 @@ static bool parseOptions() for (;i < arguments.size(); ++i) g_options.testArgsList << arguments.at(i); - if (g_options.helpRequested || g_options.buildPath.isEmpty() || g_options.apkPath.isEmpty()) + if (g_options.helpRequested || g_options.buildPath.isEmpty() || g_options.packagePath.isEmpty()) return false; g_options.serial = qEnvironmentVariable("ANDROID_SERIAL"); @@ -222,52 +251,57 @@ static bool parseOptions() static void printHelp() { - qWarning( "Syntax: %s <options> -- [TESTARGS] \n" - "\n" - " Runs a Qt for Android test on an emulator or a device. Specify a device\n" - " using the environment variables ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n" - " Returns the number of failed tests, -1 on test runner deployment related\n" - " failures or zero on success." - "\n" - " Mandatory arguments:\n" - " --path <path>: The path where androiddeployqt builds the android package.\n" - "\n" - " --make <make cmd>: make command to create an APK, for example:\n" - " \"cmake --build <build-dir> --target <target>_make_apk\".\n" - "\n" - " --apk <apk path>: The test apk path. The apk has to exist already, if it\n" - " does not exist the make command must be provided for building the apk.\n" - "\n" - " Optional arguments:\n" - " --adb <adb cmd>: The Android ADB command. If missing the one from\n" - " $PATH will be used.\n" - "\n" - " --activity <acitvity>: The Activity to run. If missing the first\n" - " activity from AndroidManifest.qml file will be used.\n" - "\n" - " --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n" - "\n" - " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n" - "\n" - " --show-logcat: Print Logcat output to stdout. If an ANR occurs during\n" - " the test run, logs from the system_server process are included.\n" - " This argument is implied if a test crashes.\n" - "\n" - " --ndk-stack: Path to ndk-stack tool that symbolizes crash stacktraces.\n" - " By default, ANDROID_NDK_ROOT env var is used to deduce the tool path.\n" - "\n" - " -- Arguments that will be passed to the test application.\n" - "\n" - " --verbose: Prints out information during processing.\n" - "\n" - " --pre-test-adb-command <command>: call the adb <command> after\n" - " installation and before the test run.\n" - "\n" - " --manifest <path>: Custom path to the AndroidManifest.xml.\n" - "\n" - " --help: Displays this information.\n", - qPrintable(QCoreApplication::arguments().at(0)) - ); + qWarning("Syntax: %s <options> -- [TESTARGS] \n" + "\n" + " Runs a Qt for Android test on an emulator or a device. Specify a device\n" + " using the environment variables ANDROID_SERIAL or ANDROID_DEVICE_SERIAL.\n" + " Returns the number of failed tests, -1 on test runner deployment related\n" + " failures or zero on success." + "\n" + " Mandatory arguments:\n" + " --path <path>: The path where androiddeployqt builds the android package.\n" + "\n" + " --make <make cmd>: make command to create an APK, for example:\n" + " \"cmake --build <build-dir> --target <target>_make_apk\".\n" + "\n" + " --apk <apk path>: The test apk path. The apk has to exist already, if it\n" + " does not exist the make command must be provided for building the apk.\n" + "\n" + " --aab <aab path>: The test aab path. The aab has to exist already, if it\n" + " does not exist the make command must be provided for building the aab.\n" + "\n" + " Optional arguments:\n" + " --adb <adb cmd>: The Android ADB command. If missing the one from\n" + " $PATH will be used.\n" + "\n" + " --activity <acitvity>: The Activity to run. If missing the first\n" + " activity from AndroidManifest.qml file will be used.\n" + "\n" + " --timeout <seconds>: Timeout to run the test. Default is 10 minutes.\n" + "\n" + " --skip-install-root: Do not append INSTALL_ROOT=... to the make command.\n" + "\n" + " --show-logcat: Print Logcat output to stdout. If an ANR occurs during\n" + " the test run, logs from the system_server process are included.\n" + " This argument is implied if a test crashes.\n" + "\n" + " --ndk-stack: Path to ndk-stack tool that symbolizes crash stacktraces.\n" + " By default, ANDROID_NDK_ROOT env var is used to deduce the tool path.\n" + "\n" + " -- Arguments that will be passed to the test application.\n" + "\n" + " --verbose: Prints out information during processing.\n" + "\n" + " --pre-test-adb-command <command>: call the adb <command> after\n" + " installation and before the test run.\n" + "\n" + " --manifest <path>: Custom path to the AndroidManifest.xml.\n" + "\n" + " --bundletool <bundletool path>: The path to Android bundletool.\n" + " See https://developer.android.com/tools/bundletool for details.\n" + "\n" + " --help: Displays this information.\n", + qPrintable(QCoreApplication::arguments().at(0))); } static QString packageNameFromAndroidManifest(const QString &androidManifestPath) @@ -850,10 +884,10 @@ int main(int argc, char *argv[]) return EXIT_ERROR; } - if (!QFile::exists(g_options.apkPath)) { + if (!QFile::exists(g_options.packagePath)) { qCritical("No apk \"%s\" found after running the make command. " "Check the provided path and the make command.", - qPrintable(g_options.apkPath)); + qPrintable(g_options.packagePath)); return EXIT_ERROR; } @@ -886,11 +920,22 @@ int main(int argc, char *argv[]) // do not install or run packages while another test is running testRunnerLock.acquire(); - const QStringList installArgs = { "install"_L1, "-r"_L1, "-g"_L1, g_options.apkPath }; - g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs, nullptr)); - if (!g_testInfo.isPackageInstalled) - return EXIT_ERROR; + if (g_options.packagePath.endsWith(".apk"_L1)) { + const QStringList installArgs = { "install"_L1, "-r"_L1, "-g"_L1, g_options.packagePath }; + g_testInfo.isPackageInstalled.store(execAdbCommand(installArgs, nullptr)); + if (!g_testInfo.isPackageInstalled) + return EXIT_ERROR; + } else if (g_options.packagePath.endsWith(".aab"_L1)) { + QFileInfo aab(g_options.packagePath); + const auto apksFilePath = aab.absoluteDir().absoluteFilePath(aab.baseName() + ".apks"_L1); + if (!execBundletoolCommand({ "build-apks"_L1, "--bundle"_L1, g_options.packagePath, + "--output"_L1, apksFilePath, "--local-testing"_L1, + "--overwrite"_L1 })) + return EXIT_ERROR; + if (!execBundletoolCommand({ "install-apks"_L1, "--apks"_L1, apksFilePath })) + return EXIT_ERROR; + } // Call additional adb command if set after installation and before starting the test for (const auto &command : g_options.preTestRunAdbCommands) { QByteArray output; diff --git a/src/widgets/kernel/qtestsupport_widgets.cpp b/src/widgets/kernel/qtestsupport_widgets.cpp index 5a7200e58aa..ce40ba8c6dd 100644 --- a/src/widgets/kernel/qtestsupport_widgets.cpp +++ b/src/widgets/kernel/qtestsupport_widgets.cpp @@ -31,6 +31,17 @@ static bool qWaitForWidgetWindow(QWidget *w, Predicate predicate, QDeadlineTimer /*! \since 5.0 + \overload + + The \a timeout is in milliseconds. +*/ +bool QTest::qWaitForWindowActive(QWidget *widget, int timeout) +{ + return qWaitForWindowActive(widget, QDeadlineTimer{timeout, Qt::TimerType::PreciseTimer}); +} + +/*! + \since 6.10 Returns \c true if \a widget is active within \a timeout milliseconds. Otherwise returns \c false. @@ -45,7 +56,7 @@ static bool qWaitForWidgetWindow(QWidget *w, Predicate predicate, QDeadlineTimer \sa qWaitForWindowExposed(), QWidget::isActiveWindow() */ -Q_WIDGETS_EXPORT bool QTest::qWaitForWindowActive(QWidget *widget, int timeout) +bool QTest::qWaitForWindowActive(QWidget *widget, QDeadlineTimer timeout) { if (Q_UNLIKELY(!QGuiApplicationPrivate::platformIntegration()->hasCapability(QPlatformIntegration::WindowActivation))) { qWarning() << "qWaitForWindowActive was called on a platform that doesn't support window" @@ -57,9 +68,19 @@ Q_WIDGETS_EXPORT bool QTest::qWaitForWindowActive(QWidget *widget, int timeout) } return qWaitForWidgetWindow(widget, [&](QWindow *window) { return window->isActive(); }, - QDeadlineTimer{timeout, Qt::TimerType::PreciseTimer}); + timeout); } +/*! + \since 6.10 + \overload + + This function uses the default timeout of 5 seconds. +*/ +bool QTest::qWaitForWindowActive(QWidget *widget) +{ + return qWaitForWindowActive(widget, Internal::defaultTryTimeout); +} /*! \since 6.7 @@ -86,7 +107,30 @@ Q_WIDGETS_EXPORT bool QTest::qWaitForWindowFocused(QWidget *widget, QDeadlineTim } /*! + \since 6.10 + \overload + + This function uses the default timeout of 5 seconds. +*/ +bool QTest::qWaitForWindowFocused(QWidget *widget) +{ + return qWaitForWindowFocused(widget, Internal::defaultTryTimeout); +} + +/*! \since 5.0 + \overload + + The \a timeout is in milliseconds. +*/ +bool QTest::qWaitForWindowExposed(QWidget *widget, int timeout) +{ + return qWaitForWindowExposed(widget, std::chrono::milliseconds(timeout)); +} + + +/*! + \since 6.10 Returns \c true if \a widget is exposed within \a timeout milliseconds. Otherwise returns \c false. @@ -99,11 +143,22 @@ Q_WIDGETS_EXPORT bool QTest::qWaitForWindowFocused(QWidget *widget, QDeadlineTim \sa qWaitForWindowActive(), QWidget::isVisible(), QWindow::isExposed() */ -Q_WIDGETS_EXPORT bool QTest::qWaitForWindowExposed(QWidget *widget, int timeout) +bool QTest::qWaitForWindowExposed(QWidget *widget, QDeadlineTimer timeout) { return qWaitForWidgetWindow(widget, [&](QWindow *window) { return window->isExposed(); }, - QDeadlineTimer{timeout, Qt::TimerType::PreciseTimer}); + timeout); +} + +/*! + \since 6.10 + \overload + + This function uses the default timeout of 5 seconds. +*/ +bool QTest::qWaitForWindowExposed(QWidget *widget) +{ + return qWaitForWindowExposed(widget, Internal::defaultTryTimeout); } namespace QTest { diff --git a/src/widgets/kernel/qtestsupport_widgets.h b/src/widgets/kernel/qtestsupport_widgets.h index b49e68db651..4b5e5ff7772 100644 --- a/src/widgets/kernel/qtestsupport_widgets.h +++ b/src/widgets/kernel/qtestsupport_widgets.h @@ -14,9 +14,16 @@ class QWidget; namespace QTest { -[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowActive(QWidget *widget, int timeout = 5000); -[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowFocused(QWidget *widget, QDeadlineTimer timeout = std::chrono::seconds{5}); -[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowExposed(QWidget *widget, int timeout = 5000); +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowActive(QWidget *widget, int timeout); +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowActive(QWidget *widget, QDeadlineTimer timeout); +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowActive(QWidget *widget); + +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowFocused(QWidget *widget, QDeadlineTimer timeout); +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowFocused(QWidget *widget); + +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowExposed(QWidget *widget, int timeout); +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowExposed(QWidget *widget, QDeadlineTimer timeout); +[[nodiscard]] Q_WIDGETS_EXPORT bool qWaitForWindowExposed(QWidget *widget); class Q_WIDGETS_EXPORT QTouchEventWidgetSequence : public QTouchEventSequence { diff --git a/src/widgets/widgets/qdockarealayout.cpp b/src/widgets/widgets/qdockarealayout.cpp index f5d92094f54..c1ce675d75a 100644 --- a/src/widgets/widgets/qdockarealayout.cpp +++ b/src/widgets/widgets/qdockarealayout.cpp @@ -3327,7 +3327,8 @@ int QDockAreaLayout::separatorMove(const QList<int> &separator, const QPoint &or { int delta = 0; const auto dockPosition = static_cast<QInternal::DockPosition>(separator.last()); - const bool isHorizontal = dockPosition == QInternal::LeftDock || dockPosition == QInternal::TopDock; + const bool isHorizontal = + dockPosition == QInternal::LeftDock || dockPosition == QInternal::RightDock; const bool isLeftOrTop = dockPosition == QInternal::LeftDock || dockPosition == QInternal::TopDock; const bool separatorIsWithinDock = separator.size() > 1; diff --git a/tests/auto/corelib/platform/CMakeLists.txt b/tests/auto/corelib/platform/CMakeLists.txt index 3a66ec2eae6..9810947e3c6 100644 --- a/tests/auto/corelib/platform/CMakeLists.txt +++ b/tests/auto/corelib/platform/CMakeLists.txt @@ -5,7 +5,9 @@ if(ANDROID) add_subdirectory(android) add_subdirectory(android_appless) add_subdirectory(androiditemmodel) - add_subdirectory(android_legacy_packaging) + if(NOT QT_USE_ANDROID_MODERN_BUNDLE) + add_subdirectory(android_legacy_packaging) + endif() endif() if(WIN32) add_subdirectory(windows) diff --git a/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle b/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle index cbdd833e2ae..1d796d9b6b9 100644 --- a/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle +++ b/tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle @@ -70,8 +70,8 @@ android { abortOnError = false } - // Do not compress Qt binary resources file aaptOptions { + // Do not compress Qt binary resources file noCompress 'rcc' } diff --git a/tests/auto/gui/text/qfont/BLACKLIST b/tests/auto/gui/text/qfont/BLACKLIST index 2d2440255ae..3f63b678bb5 100644 --- a/tests/auto/gui/text/qfont/BLACKLIST +++ b/tests/auto/gui/text/qfont/BLACKLIST @@ -1,12 +1,9 @@ [defaultFamily:cursive] centos b2qt -rhel [defaultFamily:fantasy] centos b2qt -rhel - # QTBUG-130738 [familyNameWithCommaQuote:weird] vxworks diff --git a/tests/auto/other/android/CMakeLists.txt b/tests/auto/other/android/CMakeLists.txt index bcbf5b657d7..0eb2d00b46a 100644 --- a/tests/auto/other/android/CMakeLists.txt +++ b/tests/auto/other/android/CMakeLists.txt @@ -2,3 +2,11 @@ # SPDX-License-Identifier: BSD-3-Clause add_subdirectory(deployment_settings) +add_subdirectory(permissions) + +if(QT_USE_TARGET_ANDROID_BUILD_DIR) + add_subdirectory(package_source_dir) + if(QT_USE_ANDROID_MODERN_BUNDLE) + add_subdirectory(dynamic_feature) + endif() +endif() diff --git a/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp b/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp index 1687ed9de92..571570e370f 100644 --- a/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp +++ b/tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp @@ -83,10 +83,11 @@ void tst_android_deployment_settings::DeploymentSettings_data() << "org.qtproject.android_deployment_settings_test"; QTest::newRow("android-app-name") << "android-app-name" << "Android Deployment Settings Test"; - QTest::newRow("permissions") << "permissions" - << "[{\"name\":\"PERMISSION_WITH_ATTRIBUTES\"," - "\"extras\":\"android:minSdkVersion='32' android:maxSdkVersion='34' \"}," - "{\"name\":\"PERMISSION_WITHOUT_ATTRIBUTES\"}]"; + QTest::newRow("permissions") + << "permissions" + << "[{\"maxSdkVersion\":\"34\",\"minSdkVersion\":\"32\",\"name\":\"PERMISSION_WITH_" + "ATTRIBUTES\"},{\"name\":\"PERMISSION_WITHOUT_ATTRIBUTES\"},{\"name\":\"android." + "permission.INTERNET\"},{\"name\":\"android.permission.WRITE_EXTERNAL_STORAGE\"}]"; } void tst_android_deployment_settings::DeploymentSettings() diff --git a/tests/auto/other/android/dynamic_feature/CMakeLists.txt b/tests/auto/other/android/dynamic_feature/CMakeLists.txt new file mode 100644 index 00000000000..b12c2af4dc7 --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/CMakeLists.txt @@ -0,0 +1,33 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +cmake_minimum_required(VERSION 3.16) + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_android_dynamic_feature LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +add_subdirectory(feature) + +qt_internal_add_test(tst_android_dynamic_feature + SOURCES + tst_android_dynamic_feature.cpp + storeloader/storeloader.h + storeloader/storeloader.cpp + INCLUDE_DIRECTORIES + storeloader + LIBRARIES + Qt6::Test + Qt6::Gui + Qt6::CorePrivate +) + +set_property(TARGET tst_android_dynamic_feature PROPERTY + QT_ANDROID_PACKAGE_NAME "org.qtproject.example.android_dynamic_feature") +set_property(TARGET tst_android_dynamic_feature APPEND PROPERTY + _qt_android_gradle_java_source_dirs "${CMAKE_CURRENT_SOURCE_DIR}/storeloader/java") + +qt6_add_android_dynamic_features(tst_android_dynamic_feature FEATURE_TARGETS + tst_android_dynamic_feature_resources) diff --git a/tests/auto/other/android/dynamic_feature/feature/CMakeLists.txt b/tests/auto/other/android/dynamic_feature/feature/CMakeLists.txt new file mode 100644 index 00000000000..bfe29cc0f5e --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/feature/CMakeLists.txt @@ -0,0 +1,8 @@ +cmake_minimum_required(VERSION 3.16) + +qt6_add_library(tst_android_dynamic_feature_resources SHARED) +qt6_add_resources(tst_android_dynamic_feature_resources "dynamic_resources" + PREFIX "/dynamic_resources" + FILES "qtlogo.png") + +target_link_libraries(tst_android_dynamic_feature_resources PRIVATE Qt6::Core) diff --git a/tests/auto/other/android/dynamic_feature/feature/qtlogo.png b/tests/auto/other/android/dynamic_feature/feature/qtlogo.png Binary files differnew file mode 100644 index 00000000000..b63f1384b11 --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/feature/qtlogo.png diff --git a/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoader.java b/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoader.java new file mode 100644 index 00000000000..5a72351c802 --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoader.java @@ -0,0 +1,175 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +package org.qtproject.example.android_dynamic_feature; + +import com.google.android.play.core.splitcompat.SplitCompat; +import com.google.android.play.core.splitinstall.SplitInstallManager; +import com.google.android.play.core.splitinstall.SplitInstallManagerFactory; +import com.google.android.play.core.splitinstall.SplitInstallRequest; +import com.google.android.play.core.splitinstall.SplitInstallHelper; +import com.google.android.play.core.splitinstall.testing.FakeSplitInstallManagerFactory; +import android.content.pm.PackageManager.NameNotFoundException; + +import java.util.Arrays; +import java.util.HashMap; +import java.io.File; +import android.util.Log; +import android.content.Context; +import android.os.Build; + +/** + * Android implementation of store loader. + */ +public class StoreLoader implements StoreLoaderListenerCallback +{ + private static final String TAG = "StoreLoader"; + private final SplitInstallManager m_splitInstallManager; + private HashMap<String, StoreLoaderListener> m_listeners = + new HashMap(); + private Context m_context; + private String m_moduleName; + + public static native void stateChangedNative(String callId, int state); + public static native void errorOccurredNative(String callId, int errorCode, + String errorMessage); + public static native void userConfirmationRequestedNative(String callId, int errorCode, + String errorMessage); + public static native void downloadProgressChangedNative(String callId, long bytes, long total); + public static native void finishedNative(String callId); + + public StoreLoader(Context context) { + m_context = context; + + SplitCompat.install(context); + + m_splitInstallManager = SplitInstallManagerFactory.create(context); + if (m_splitInstallManager == null) + Log.e(TAG,"Constructor: Did not get splitInstallManager"); + } + + /** + * Loads Feature Delivery module from a play store. + */ + public void installModuleFromStore(String moduleName, String callId) { + Log.d(TAG, "installModuleFromStore: " + moduleName + " " + callId); + m_moduleName = moduleName; + + registerListener(callId); + + SplitInstallRequest request = + SplitInstallRequest.newBuilder().addModule(m_moduleName).build(); + if (request == null) + Log.e(TAG, "Null request"); + + if (m_splitInstallManager == null) + Log.e(TAG, "Null m_splitInstallManager"); + + m_splitInstallManager.startInstall(request) + .addOnSuccessListener(sessionId -> { + Log.d(TAG, "Install start call succesfull"); + StoreLoaderListener listener = m_listeners.get(callId); + if (listener != null) { + if (!listener.isCancelled()) + listener.setSessionId(sessionId); + else + m_splitInstallManager.cancelInstall(sessionId); + } else { + Log.d(TAG, "Listener for callId '" + callId + "' is not found"); + } + }); + } + + public void onStateChanged(String callId, int state) { + stateChangedNative(callId, state); + if (state == StoreLoaderListenerCallback.LOADED || + state == StoreLoaderListenerCallback.CANCELED) { + finished(callId); + } + } + + public void onErrorOccurred(String callId, int errorCode, String errorMessage) { + onStateChanged(callId, ERROR); + + Log.e(TAG, "Error occurred " + errorCode + " " + errorMessage); + errorOccurredNative(callId, errorCode, errorMessage); + finished(callId); + } + + public void onUserConfirmationRequested(String callId, int errorCode, String errorMessage) { + Log.d(TAG, "Requires user confirmation " + errorCode); + userConfirmationRequestedNative(callId, errorCode, errorMessage); + } + + public void onDownloadProgressChanged(String callId, long bytes, long total) { + Log.d(TAG, "Downloading " + bytes + "/" + total); + downloadProgressChangedNative(callId, bytes, total); + } + + public void onLoadLibrary(String callId) { + Log.d(TAG, "Load library for the module " + m_moduleName); + + stateChangedNative(callId, StoreLoaderListenerCallback.LOADING); + // update context. + try { + m_context = m_context.createPackageContext(m_context.getPackageName(), 0); + } catch (NameNotFoundException ignored) { + Log.e(TAG, "Could not get package name"); + } + // install splitcompat for new context. + SplitCompat.install(m_context); + // try to load new library + boolean isLoaded = false; + for (String abi : Build.SUPPORTED_ABIS) { + String fullLibraryName = m_moduleName + "_" + abi; + try { + System.loadLibrary(fullLibraryName); + isLoaded = true; + break; + } catch (Exception e) { + Log.d(TAG, "Exception occurred when loading the library " + fullLibraryName + ":" + + e.getClass().getCanonicalName()); + } + } + + if (isLoaded) + onStateChanged(callId, StoreLoaderListenerCallback.LOADED); + else + onErrorOccurred(callId, -1, "Error loading library. Check logcat for details."); + + } + + public void cancelInstall(String callId) { + StoreLoaderListener listener = m_listeners.get(callId); + if (listener == null) { + Log.e(TAG, "The listener for callId " + callId + " is not found"); + return; + } + + if (listener.getSessionId() < 0) + listener.postponeCancel(); + else + m_splitInstallManager.cancelInstall(listener.getSessionId()); + } + + private void finished(String callId) { + unregisterListener(callId); + finishedNative(callId); + } + + private void registerListener(String callId) { + Log.d(TAG, "registerListener"); + StoreLoaderListener listener = new StoreLoaderListener(this, callId); + if (listener != null) { + m_listeners.put(callId, listener); + m_splitInstallManager.registerListener(listener); + } + } + + private void unregisterListener(String callId) { + Log.d(TAG, "unregisterListener"); + StoreLoaderListener listener = m_listeners.remove(callId); + if (listener != null) + m_splitInstallManager.unregisterListener(listener); + } +} diff --git a/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListener.java b/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListener.java new file mode 100644 index 00000000000..3e83a0fa7f9 --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListener.java @@ -0,0 +1,92 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +package org.qtproject.example.android_dynamic_feature; + +import com.google.android.play.core.splitinstall.SplitInstallStateUpdatedListener; +import com.google.android.play.core.splitinstall.SplitInstallSessionState; +import com.google.android.play.core.splitinstall.model.SplitInstallSessionStatus; + +import android.util.Log; + +public class StoreLoaderListener implements SplitInstallStateUpdatedListener { + + private final StoreLoaderListenerCallback m_callback; + private final String m_callId; + private int m_installSessionId = -1; + private static final String TAG = "StoreLoaderListener"; + private int m_currentStatus = SplitInstallSessionStatus.UNKNOWN; + private boolean m_postponeCancel = false; + + public StoreLoaderListener(StoreLoaderListenerCallback callback, String callId) { + m_callback = callback; + m_callId = callId; + } + + public void setSessionId(int sessionId) { m_installSessionId = sessionId; } + public int getSessionId() { return m_installSessionId; } + + public void postponeCancel() { m_postponeCancel = true; } + public boolean isCancelled() { return m_postponeCancel; } + + @Override + public void onStateUpdate(SplitInstallSessionState state) { + Log.d(TAG, + "onStateUpdate, status: " + state.status() + " session id: " + state.sessionId()); + if (state.sessionId() != m_installSessionId) { + // Not mine + return; + } + + switch (state.status()) { + case SplitInstallSessionStatus.DOWNLOADING: + Log.d(TAG, + "SplitInstallSessionState: DOWNLOADING " + state.bytesDownloaded() + "/" + + state.totalBytesToDownload()); + if (m_currentStatus != SplitInstallSessionStatus.DOWNLOADING) + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.DOWNLOADING); + + m_callback.onDownloadProgressChanged(m_callId, state.bytesDownloaded(), + state.totalBytesToDownload()); + break; + case SplitInstallSessionStatus.REQUIRES_USER_CONFIRMATION: + Log.d(TAG, "SplitInstallSessionState: REQUIRES_USER_CONFIRMATION"); + m_callback.onStateChanged(m_callId, + StoreLoaderListenerCallback.REQUIRES_USER_CONFIRMATION); + + m_callback.onUserConfirmationRequested(m_callId, state.errorCode(), "" /*noop for now*/); + break; + case SplitInstallSessionStatus.INSTALLED: + Log.d(TAG, "SplitInstallSessionState: INSTALLED"); + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.INSTALLED); + m_callback.onLoadLibrary(m_callId); + break; + case SplitInstallSessionStatus.INSTALLING: + Log.d(TAG, "SplitInstallSessionState: INSTALLING"); + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.INSTALLING); + break; + case SplitInstallSessionStatus.FAILED: + Log.d(TAG, + "SplitInstallSessionState: FAILED with error code: " + state.errorCode()); + m_callback.onErrorOccurred(m_callId, state.errorCode(), "" /*noop for now*/); + break; + case SplitInstallSessionStatus.PENDING: + Log.d(TAG, "SplitInstallSessionState: PENDING"); + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.PENDING); + break; + case SplitInstallSessionStatus.DOWNLOADED: + Log.d(TAG, "SplitInstallSessionState: DOWNLOADED"); + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.DOWNLOADED); + break; + case SplitInstallSessionStatus.CANCELED: + Log.d(TAG, "SplitInstallSessionState: CANCELED"); + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.CANCELED); + break; + case SplitInstallSessionStatus.CANCELING: + Log.d(TAG, "SplitInstallSessionState: CANCELING"); + m_callback.onStateChanged(m_callId, StoreLoaderListenerCallback.CANCELING); + break; + } + m_currentStatus = state.status(); + } +} diff --git a/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListenerCallback.java b/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListenerCallback.java new file mode 100644 index 00000000000..99c7327e328 --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListenerCallback.java @@ -0,0 +1,26 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +package org.qtproject.example.android_dynamic_feature; + +public interface StoreLoaderListenerCallback { + public static final int UNKNOWN = 0; // Unused but native part has and uses one. + public static final int INITIALIZED = 1; + public static final int PENDING = 2; + public static final int DOWNLOADING = 3; + public static final int DOWNLOADED = 4; + public static final int REQUIRES_USER_CONFIRMATION = 5; + public static final int CANCELING = 6; + public static final int CANCELED = 7; + public static final int INSTALLING = 8; + public static final int INSTALLED = 9; + public static final int LOADING = 10; + public static final int LOADED = 11; + public static final int ERROR = 12; + + public void onStateChanged(String callId, int state); + public void onErrorOccurred(String callId, int errorCode, String errorMessage); + public void onUserConfirmationRequested(String callId, int errorCode, String errorMessage); + public void onDownloadProgressChanged(String callId, long bytes, long total); + public void onLoadLibrary(String callId); +} diff --git a/tests/auto/other/android/dynamic_feature/storeloader/storeloader.cpp b/tests/auto/other/android/dynamic_feature/storeloader/storeloader.cpp new file mode 100644 index 00000000000..7abce59b79e --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/storeloader/storeloader.cpp @@ -0,0 +1,227 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include "storeloader.h" + +#include <QtCore/private/qobject_p.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qhash.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qlogging.h> +#include <QtCore/qmutex.h> +#include <QtCore/quuid.h> + +#include <jni.h> + +Q_DECLARE_JNI_CLASS(StoreLoader, + "org/qtproject/example/android_dynamic_feature/StoreLoader"); + +class StoreLoaderHandlerPrivate : public QObjectPrivate +{ + Q_DECLARE_PUBLIC(StoreLoaderHandler) +public: + void setState(StoreLoader::State state) + { + if (m_state == state) + return; + Q_Q(StoreLoaderHandler); + m_state = state; + q->stateChanged(m_state); + } + + const QString &callId() const & noexcept { return m_callId; } + +private: + StoreLoader::State m_state = StoreLoader::State::Unknown; + QString m_callId = QUuid::createUuid().toString(); +}; + +namespace QtPrivate { +QString asString(const jstring &s) +{ + return QJniObject(s).toString(); +} +} // namespace QtPrivate + +namespace { + +class StoreLoaderImpl +{ +public: + StoreLoaderImpl(); + bool registerNatives() const; + + void addHandler(StoreLoaderHandler *handler); + StoreLoaderHandler *findHandler(const jstring &callId); + void removeHandler(const jstring &callId); + + QtJniTypes::StoreLoader loader = nullptr; + +private: + QHash<QString, QPointer<StoreLoaderHandler>> m_handlers; + QMutex m_lock; +}; + +Q_GLOBAL_STATIC(StoreLoaderImpl, loaderInstance) + +void stateChangedNative(JNIEnv *, jobject, jstring callId, int state) +{ + qDebug("State changed %s.", qPrintable(callId)); + + if (auto *handler = loaderInstance->findHandler(callId)) + emit handler->stateChanged(static_cast<StoreLoader::State>(state)); +} + +void errorOccurredNative(JNIEnv *, jobject, jstring callId, int errorCode, jstring errorMessage) +{ + qDebug("Error occurred %s %d %s.", qPrintable(callId), errorCode, qPrintable(errorMessage)); + auto *handler = loaderInstance->findHandler(callId); + if (!handler) + return; + + emit handler->errorOccured(errorCode, QJniObject(errorMessage).toString()); +} + +void userConfirmationRequestedNative(JNIEnv *, jobject, jstring callId, int errorCode, + jstring errorMessage) +{ + qDebug("User confirmation requested %s %d %s.", qPrintable(callId), errorCode, + qPrintable(errorMessage)); + auto *handler = loaderInstance->findHandler(callId); + if (!handler) + return; + + emit handler->confirmationRequest(errorCode, QJniObject(errorMessage).toString()); +} + +void downloadProgressChangedNative(JNIEnv *, jobject, jstring callId, long bytes, long total) +{ + qDebug("Download progress changed %ld/%ld.", bytes, total); + auto *handler = loaderInstance->findHandler(callId); + if (!handler) + return; + + emit handler->downloadProgress(bytes, total); +} + +void finishedNative(JNIEnv *, jobject, jstring callId) +{ + auto *handler = loaderInstance->findHandler(callId); + if (!handler) + return; + + emit handler->finished(); +} + +} // namespace + +Q_DECLARE_JNI_NATIVE_METHOD(stateChangedNative) +Q_DECLARE_JNI_NATIVE_METHOD(errorOccurredNative) +Q_DECLARE_JNI_NATIVE_METHOD(userConfirmationRequestedNative) +Q_DECLARE_JNI_NATIVE_METHOD(downloadProgressChangedNative) +Q_DECLARE_JNI_NATIVE_METHOD(finishedNative) + +StoreLoaderImpl::StoreLoaderImpl() +{ + loader = QJniObject::construct<QtJniTypes::StoreLoader, QtJniTypes::Context>( + QNativeInterface::QAndroidApplication::context()); +} + +bool StoreLoaderImpl::registerNatives() const +{ + static bool result = [] { + return QtJniTypes::StoreLoader::registerNativeMethods({ + Q_JNI_NATIVE_METHOD(stateChangedNative), + Q_JNI_NATIVE_METHOD(errorOccurredNative), + Q_JNI_NATIVE_METHOD(userConfirmationRequestedNative), + Q_JNI_NATIVE_METHOD(downloadProgressChangedNative), + Q_JNI_NATIVE_METHOD(finishedNative), + }); + }(); + + if (!result) + qCritical("Unable to register native methods."); + + return result; +} + +void StoreLoaderImpl::addHandler(StoreLoaderHandler *handler) +{ + Q_ASSERT(handler); + + QMutexLocker lock(&m_lock); + const auto &callId = handler->callId(); + Q_ASSERT_X(m_handlers.constFind(callId) != m_handlers.constEnd(), "StoreLoaderImpl::addHandler", + qPrintable(QString("Handler with callId %1 already exists.").arg(callId))); + + m_handlers[callId] = QPointer(handler); +} + + +StoreLoaderHandler *StoreLoaderImpl::findHandler(const jstring &callId) +{ + QMutexLocker lock(&m_lock); + const auto it = m_handlers.constFind(QJniObject(callId).toString()); + if (it == m_handlers.constEnd()) { + qCritical("The handler for the call %s was not found.", qPrintable(callId)); + return nullptr; + } + + if (it.value().isNull()) { + qCritical("The handler for the call %s expired.", qPrintable(callId)); + m_handlers.erase(it); + } + + return it.value().get(); +} + +void StoreLoaderImpl::removeHandler(const jstring &callId) +{ + QMutexLocker lock(&m_lock); + m_handlers.remove(QJniObject(callId).toString()); +} + +std::unique_ptr<StoreLoaderHandler> +StoreLoader::loadModule(const QString &moduleName) +{ + if (moduleName.isEmpty()) + return {}; + + if (!loaderInstance->registerNatives()) + return {}; + + if (!loaderInstance->loader.isValid()) { + qCritical("StoreLoader not constructed"); + return {}; + } + + auto handlerPtr = std::make_unique<StoreLoaderHandler>( + nullptr, StoreLoaderHandler::PrivateConstructor{}); + loaderInstance->addHandler(handlerPtr.get()); + + const auto &callId = handlerPtr->callId(); + qDebug("Loading module %s, callId: %s.", qPrintable(moduleName), qPrintable(callId)); + + loaderInstance->loader.callMethod<void>("installModuleFromStore", moduleName, callId); + return handlerPtr; +} + +StoreLoaderHandler::StoreLoaderHandler(QObject *parent, PrivateConstructor) + : QObject(*new StoreLoaderHandlerPrivate(), parent) +{ +} + +StoreLoaderHandler::~StoreLoaderHandler() = default; + +const QString &StoreLoaderHandler::callId() const & noexcept +{ + Q_D(const StoreLoaderHandler); + return d->callId(); +} + +void StoreLoaderHandler::cancel() +{ + Q_D(StoreLoaderHandler); + loaderInstance->loader.callMethod<void>("cancelInstall", d->callId()); +} + +#include "moc_storeloader.cpp" diff --git a/tests/auto/other/android/dynamic_feature/storeloader/storeloader.h b/tests/auto/other/android/dynamic_feature/storeloader/storeloader.h new file mode 100644 index 00000000000..be4e103207e --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/storeloader/storeloader.h @@ -0,0 +1,65 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#ifndef STORELOADER_H +#define STORELOADER_H + +#include <QtCore/qtconfigmacros.h> +#include <QtCore/qobject.h> + +#include <memory> + +class StoreLoaderHandler; + +namespace StoreLoader { + Q_NAMESPACE +enum class State { + Unknown, + Initialized, + Pending, + Downloading, + Downloaded, + RequiresUserConfirmation, + Canceling, + Canceled, + Installing, + Installed, + Loading, + Loaded, + Error, +}; +Q_ENUM_NS(State) + +std::unique_ptr<StoreLoaderHandler> loadModule(const QString &moduleName); +}; // namespace StoreLoader + +class StoreLoaderHandlerPrivate; + +class StoreLoaderHandler : public QObject +{ + Q_OBJECT + QT_DEFINE_TAG_STRUCT(PrivateConstructor); + +public: + explicit StoreLoaderHandler(QObject *parent, PrivateConstructor); + ~StoreLoaderHandler() override; + + const QString &callId() const & noexcept; + + void cancel(); +signals: + void stateChanged(StoreLoader::State state); + void downloadProgress(qsizetype bytes, qsizetype total); + void errorOccured(int errorCode, const QString &errorString); + void confirmationRequest(int errorCode, const QString &errorString); + void finished(); + +private: + Q_DISABLE_COPY_MOVE(StoreLoaderHandler) + Q_DECLARE_PRIVATE(StoreLoaderHandler) + + friend std::unique_ptr<StoreLoaderHandler> + StoreLoader::loadModule(const QString &); +}; + +#endif // STORELOADER_H diff --git a/tests/auto/other/android/dynamic_feature/tst_android_dynamic_feature.cpp b/tests/auto/other/android/dynamic_feature/tst_android_dynamic_feature.cpp new file mode 100644 index 00000000000..739a645c5d5 --- /dev/null +++ b/tests/auto/other/android/dynamic_feature/tst_android_dynamic_feature.cpp @@ -0,0 +1,37 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <storeloader.h> + +#include <QtTest/QTest> +#include <QtTest/QSignalSpy> + +#include <QtCore/QVariant> + +using namespace Qt::Literals::StringLiterals; + +class QtDynamicFeatureTest : public QObject +{ + Q_OBJECT +public: + QtDynamicFeatureTest(){} + +private Q_SLOTS: + void loadResourcesFeature(); +}; + +void QtDynamicFeatureTest::loadResourcesFeature() +{ + QVERIFY(!QFile::exists(":/dynamic_resources/qtlogo.png")); + + auto handler = StoreLoader::loadModule("tst_android_dynamic_feature_resources"_L1); + + QSignalSpy spy(handler.get(), &StoreLoaderHandler::finished); + + QVERIFY(spy.wait(20000)); + QVERIFY(QFile::exists(":/dynamic_resources/qtlogo.png")); +} + +QTEST_MAIN(QtDynamicFeatureTest) + +#include "tst_android_dynamic_feature.moc" diff --git a/tests/auto/other/android/package_source_dir/CMakeLists.txt b/tests/auto/other/android/package_source_dir/CMakeLists.txt new file mode 100644 index 00000000000..360aa50d87d --- /dev/null +++ b/tests/auto/other/android/package_source_dir/CMakeLists.txt @@ -0,0 +1,54 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_android_package_source_dir LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +# Package name is generated and written to AndroidManifest.xml +qt_internal_add_test(tst_package_source_dir_not_defined + SOURCES + tst_android_package_source_dir.cpp + DEFINES + EXPECTED_APP_NAME="tst_package_source_dir_not_defined" +) + +# Package name from user-provided AndroidManifest.xml +qt_internal_add_test(tst_package_source_dir_custom_android_manifest + SOURCES + tst_android_package_source_dir.cpp + DEFINES + EXPECTED_APP_NAME="tst_package_source_dir_custom_android_manifest_my" +) + +if(QT_USE_ANDROID_MODERN_BUNDLE) + set_target_properties(tst_package_source_dir_custom_android_manifest + PROPERTIES + QT_ANDROID_PACKAGE_SOURCE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/custom_android_manifest_bundle" + ) +else() + set_target_properties(tst_package_source_dir_custom_android_manifest + PROPERTIES + QT_ANDROID_PACKAGE_SOURCE_DIR + "${CMAKE_CURRENT_SOURCE_DIR}/custom_android_manifest" + ) +endif() + +if(QT_USE_ANDROID_MODERN_BUNDLE) + # Partial gradle template is only supported by QT_USE_ANDROID_MODERN_BUNDLE + # + # Package name from user-provided gradle.build.in + qt_internal_add_test(tst_package_source_dir_partial_template + SOURCES + tst_android_package_source_dir.cpp + DEFINES + EXPECTED_APP_NAME="tst_package_source_dir_partial_template_my" + ) + set_target_properties(tst_package_source_dir_partial_template + PROPERTIES + QT_ANDROID_PACKAGE_SOURCE_DIR "${CMAKE_CURRENT_SOURCE_DIR}/partial_template" + ) +endif() diff --git a/tests/auto/other/android/package_source_dir/custom_android_manifest/AndroidManifest.xml b/tests/auto/other/android/package_source_dir/custom_android_manifest/AndroidManifest.xml new file mode 100644 index 00000000000..4f53295e979 --- /dev/null +++ b/tests/auto/other/android/package_source_dir/custom_android_manifest/AndroidManifest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.qtproject.example.tst_package_source_dir_custom_android_manifest" + android:installLocation="auto" + android:versionCode="1" + android:versionName="1.0"> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <supports-screens + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" /> + <application + android:name="org.qtproject.qt.android.bindings.QtApplication" + android:hardwareAccelerated="true" + android:label="tst_package_source_dir_custom_android_manifest_my" + android:requestLegacyExternalStorage="true" + android:allowBackup="true" + android:fullBackupOnly="false"> + <activity + android:name="org.qtproject.qt.android.bindings.QtActivity" + android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" + android:launchMode="singleTop" + android:screenOrientation="unspecified" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <meta-data + android:name="android.app.lib_name" + android:value="tst_package_source_dir_custom_android_manifest" /> + + <meta-data + android:name="android.app.arguments" + android:value="" /> + </activity> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.qtprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/qtprovider_paths"/> + </provider> + </application> +</manifest> diff --git a/tests/auto/other/android/package_source_dir/custom_android_manifest_bundle/app/AndroidManifest.xml b/tests/auto/other/android/package_source_dir/custom_android_manifest_bundle/app/AndroidManifest.xml new file mode 100644 index 00000000000..4f53295e979 --- /dev/null +++ b/tests/auto/other/android/package_source_dir/custom_android_manifest_bundle/app/AndroidManifest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.qtproject.example.tst_package_source_dir_custom_android_manifest" + android:installLocation="auto" + android:versionCode="1" + android:versionName="1.0"> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <supports-screens + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" /> + <application + android:name="org.qtproject.qt.android.bindings.QtApplication" + android:hardwareAccelerated="true" + android:label="tst_package_source_dir_custom_android_manifest_my" + android:requestLegacyExternalStorage="true" + android:allowBackup="true" + android:fullBackupOnly="false"> + <activity + android:name="org.qtproject.qt.android.bindings.QtActivity" + android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" + android:launchMode="singleTop" + android:screenOrientation="unspecified" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <meta-data + android:name="android.app.lib_name" + android:value="tst_package_source_dir_custom_android_manifest" /> + + <meta-data + android:name="android.app.arguments" + android:value="" /> + </activity> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.qtprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/qtprovider_paths"/> + </provider> + </application> +</manifest> diff --git a/tests/auto/other/android/package_source_dir/partial_template/app/AndroidManifest.xml b/tests/auto/other/android/package_source_dir/partial_template/app/AndroidManifest.xml new file mode 100644 index 00000000000..838d239e5f4 --- /dev/null +++ b/tests/auto/other/android/package_source_dir/partial_template/app/AndroidManifest.xml @@ -0,0 +1,51 @@ +<?xml version="1.0"?> +<manifest xmlns:android="http://schemas.android.com/apk/res/android" + package="org.qtproject.example.tst_package_source_dir_partial_template" + android:installLocation="auto" + android:versionCode="1" + android:versionName="1.0"> + <uses-permission android:name="android.permission.INTERNET" /> + <uses-permission android:name="android.permission.WRITE_EXTERNAL_STORAGE" /> + <supports-screens + android:anyDensity="true" + android:largeScreens="true" + android:normalScreens="true" + android:smallScreens="true" /> + <application + android:name="org.qtproject.qt.android.bindings.QtApplication" + android:hardwareAccelerated="true" + android:label="${appName}" + android:requestLegacyExternalStorage="true" + android:allowBackup="true" + android:fullBackupOnly="false"> + <activity + android:name="org.qtproject.qt.android.bindings.QtActivity" + android:configChanges="orientation|uiMode|screenLayout|screenSize|smallestScreenSize|layoutDirection|locale|fontScale|keyboard|keyboardHidden|navigation|mcc|mnc|density" + android:launchMode="singleTop" + android:screenOrientation="unspecified" + android:exported="true"> + <intent-filter> + <action android:name="android.intent.action.MAIN" /> + <category android:name="android.intent.category.LAUNCHER" /> + </intent-filter> + + <meta-data + android:name="android.app.lib_name" + android:value="tst_package_source_dir_partial_template" /> + + <meta-data + android:name="android.app.arguments" + android:value="" /> + </activity> + + <provider + android:name="androidx.core.content.FileProvider" + android:authorities="${applicationId}.qtprovider" + android:exported="false" + android:grantUriPermissions="true"> + <meta-data + android:name="android.support.FILE_PROVIDER_PATHS" + android:resource="@xml/qtprovider_paths"/> + </provider> + </application> +</manifest> diff --git a/tests/auto/other/android/package_source_dir/partial_template/app/build.gradle.in b/tests/auto/other/android/package_source_dir/partial_template/app/build.gradle.in new file mode 100644 index 00000000000..538f1ca6217 --- /dev/null +++ b/tests/auto/other/android/package_source_dir/partial_template/app/build.gradle.in @@ -0,0 +1,54 @@ +buildscript { + repositories { + google() + mavenCentral() + } + + dependencies { + classpath 'com.android.tools.build:gradle:8.6.0' + } +} + +apply plugin: '@GRADLE_PLUGIN_TYPE@' + +dependencies { + @GRADLE_DEPENDENCIES@ +} + +android { + namespace '@PACKAGE_NAME@' + compileSdkVersion '@ANDROID_COMPILE_SDK_VERSION@' + buildToolsVersion '@ANDROID_BUILD_TOOLS_VERSION@' + ndkVersion '@ANDROID_NDK_REVISION@' + + defaultConfig { + @DEFAULT_CONFIG_VALUES@ + manifestPlaceholders = [ appName:"tst_package_source_dir_partial_template_my" ] + } + + sourceSets { + main { +@SOURCE_SETS@ + } + } + + tasks.withType(JavaCompile) { + options.incremental = true + } + + compileOptions { + sourceCompatibility JavaVersion.VERSION_1_8 + targetCompatibility JavaVersion.VERSION_1_8 + } + + lintOptions { + abortOnError false + } + + aaptOptions { + // Do not compress Qt binary resources file + noCompress 'rcc' + } + + @ANDROID_DEPLOYMENT_EXTRAS@ +} diff --git a/tests/auto/other/android/package_source_dir/tst_android_package_source_dir.cpp b/tests/auto/other/android/package_source_dir/tst_android_package_source_dir.cpp new file mode 100644 index 00000000000..d74d388371f --- /dev/null +++ b/tests/auto/other/android/package_source_dir/tst_android_package_source_dir.cpp @@ -0,0 +1,47 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QCoreApplication> +#include <QJniObject> +#include <QTest> +#include <QDebug> + +using namespace QNativeInterface; + +Q_DECLARE_JNI_CLASS(ApplicationInfo, "android/content/pm/ApplicationInfo") +Q_DECLARE_JNI_CLASS(PackageManager, "android/content/pm/PackageManager") +Q_DECLARE_JNI_CLASS(CharSequence, "java/lang/CharSequence") + +class tst_android_package_source_dir : public QObject +{ + Q_OBJECT + +private slots: + void applicationName(); + +private: +}; + +void tst_android_package_source_dir::applicationName() +{ + QJniObject appCtx = QAndroidApplication::context(); + QVERIFY(appCtx.isValid()); + + const auto appInfo = appCtx.callMethod<QtJniTypes::ApplicationInfo>("getApplicationInfo"); + QVERIFY(appInfo.isValid()); + + const auto packageManager = appCtx.callMethod<QtJniTypes::PackageManager>("getPackageManager"); + QVERIFY(packageManager.isValid()); + + const auto appNameLabel = + appInfo.callMethod<QtJniTypes::CharSequence>("loadLabel", packageManager); + QVERIFY(appNameLabel.isValid()); + + const auto appName = appNameLabel.callMethod<jstring>("toString").toString(); + + QCOMPARE_EQ(appName, QString::fromLatin1(EXPECTED_APP_NAME)); +} + +QTEST_MAIN(tst_android_package_source_dir); + +#include "tst_android_package_source_dir.moc" diff --git a/tests/auto/other/android/permissions/CMakeLists.txt b/tests/auto/other/android/permissions/CMakeLists.txt new file mode 100644 index 00000000000..0424c674e1b --- /dev/null +++ b/tests/auto/other/android/permissions/CMakeLists.txt @@ -0,0 +1,23 @@ +# Copyright (C) 2025 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause + +if(NOT QT_BUILD_STANDALONE_TESTS AND NOT QT_BUILDING_QT) + cmake_minimum_required(VERSION 3.16) + project(tst_android_permissions LANGUAGES CXX) + find_package(Qt6BuildInternals REQUIRED COMPONENTS STANDALONE_TEST) +endif() + +qt_internal_add_test(tst_android_permissions + SOURCES + tst_android_permissions.cpp +) + +qt_add_android_permission(tst_android_permissions + NAME android.permission.ACCESS_COARSE_LOCATION +) + +qt_add_android_permission(tst_android_permissions + NAME android.permission.ACCESS_FINE_LOCATION + ATTRIBUTES + maxSdkVersion 31 +) diff --git a/tests/auto/other/android/permissions/tst_android_permissions.cpp b/tests/auto/other/android/permissions/tst_android_permissions.cpp new file mode 100644 index 00000000000..5d7a255d3ac --- /dev/null +++ b/tests/auto/other/android/permissions/tst_android_permissions.cpp @@ -0,0 +1,80 @@ +// Copyright (C) 2025 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only + +#include <QCoreApplication> +#include <QJniObject> +#include <QSet> +#include <QString> +#include <QTest> + +constexpr int GET_PERMISSIONS(0x00001000); + +using namespace QNativeInterface; +using namespace Qt::StringLiterals; + +Q_DECLARE_JNI_CLASS(PackageManager, "android/content/pm/PackageManager") +Q_DECLARE_JNI_CLASS(PackageInfo, "android/content/pm/PackageInfo") + +class tst_android_permissions : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase(); + void checkExpectedDefaults(); + void checkNonExisting(); + void checkNonDefaultPermissions(); + +private: + QJniArray<QString> m_requestedPermissions; +}; + +void tst_android_permissions::initTestCase() +{ + QJniObject appCtx = QAndroidApplication::context(); + QVERIFY(appCtx.isValid()); + + const auto packageName = appCtx.callMethod<QString>("getPackageName"); + const auto packageManager = appCtx.callMethod<QtJniTypes::PackageManager>("getPackageManager"); + QVERIFY(packageManager.isValid()); + + const auto packageInfo = QJniObject(packageManager.callMethod<QtJniTypes::PackageInfo>( + "getPackageInfo", packageName, jint(GET_PERMISSIONS))); + QVERIFY(packageInfo.isValid()); + + m_requestedPermissions = packageInfo.getField<QJniArray<QString>>("requestedPermissions"); + QVERIFY(m_requestedPermissions.isValid()); +} + +void tst_android_permissions::checkExpectedDefaults() +{ + QSet<QString> expectedDefaults{ { "android.permission.INTERNET"_L1 }, + { "android.permission.WRITE_EXTERNAL_STORAGE"_L1 }, + { "android.permission.READ_EXTERNAL_STORAGE"_L1 } }; + + for (const auto &permission : m_requestedPermissions) + expectedDefaults.remove(permission); + + QVERIFY(expectedDefaults.empty()); +} + +void tst_android_permissions::checkNonExisting() +{ + for (const auto &permission : m_requestedPermissions) + QCOMPARE_NE(permission, "android.permission.BLUETOOTH_SCAN"); +} + +void tst_android_permissions::checkNonDefaultPermissions() +{ + bool hasNonDefaultPermissions = false; + for (const auto &permission : m_requestedPermissions) { + if (permission == "android.permission.ACCESS_COARSE_LOCATION") + hasNonDefaultPermissions = true; + } + + QVERIFY(hasNonDefaultPermissions); +} + +QTEST_MAIN(tst_android_permissions); + +#include "tst_android_permissions.moc" |