summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmake/FindBundletool.cmake48
-rw-r--r--cmake/QtAndroidHelpers.cmake30
-rw-r--r--cmake/QtBaseHelpers.cmake2
-rw-r--r--cmake/QtPlatformAndroid.cmake26
-rw-r--r--cmake/QtPostProcessHelpers.cmake2
-rw-r--r--cmake/QtTestHelpers.cmake7
-rw-r--r--configure.cmake3
-rw-r--r--doc/global/includes/cli-build-cmake.qdocinc2
-rw-r--r--src/android/templates/build.gradle2
-rw-r--r--src/android/templates_cmake/CMakeLists.txt24
-rw-r--r--src/android/templates_cmake/app/AndroidManifest.xml.in54
-rw-r--r--src/android/templates_cmake/app/build.gradle.in (renamed from src/android/templates_cmake/build.gradle.in)4
-rw-r--r--src/android/templates_cmake/app/gradle.properties.in1
-rw-r--r--src/android/templates_cmake/dynamic_feature/AndroidManifest.xml.in15
-rw-r--r--src/android/templates_cmake/dynamic_feature/build.gradle.in53
-rw-r--r--src/android/templates_cmake/settings.gradle.in1
-rw-r--r--src/corelib/CMakeLists.txt2
-rw-r--r--src/corelib/Qt6AndroidDynamicFeatureHelpers.cmake152
-rw-r--r--src/corelib/Qt6AndroidGradleHelpers.cmake424
-rw-r--r--src/corelib/Qt6AndroidMacros.cmake108
-rw-r--r--src/corelib/Qt6AndroidPermissionHelpers.cmake126
-rw-r--r--src/corelib/Qt6CoreConfigExtras.cmake.in3
-rw-r--r--src/corelib/Qt6CoreMacros.cmake3
-rw-r--r--src/corelib/animation/qabstractanimation.cpp1
-rw-r--r--src/corelib/animation/qabstractanimation.h1
-rw-r--r--src/corelib/animation/qabstractanimation_p.h1
-rw-r--r--src/corelib/animation/qanimationgroup.cpp1
-rw-r--r--src/corelib/animation/qanimationgroup.h1
-rw-r--r--src/corelib/animation/qanimationgroup_p.h1
-rw-r--r--src/corelib/animation/qparallelanimationgroup.cpp1
-rw-r--r--src/corelib/animation/qparallelanimationgroup.h1
-rw-r--r--src/corelib/animation/qparallelanimationgroup_p.h1
-rw-r--r--src/corelib/animation/qpauseanimation.cpp1
-rw-r--r--src/corelib/animation/qpauseanimation.h1
-rw-r--r--src/corelib/animation/qpropertyanimation.cpp1
-rw-r--r--src/corelib/animation/qpropertyanimation.h1
-rw-r--r--src/corelib/animation/qpropertyanimation_p.h1
-rw-r--r--src/corelib/animation/qsequentialanimationgroup.cpp1
-rw-r--r--src/corelib/animation/qsequentialanimationgroup.h1
-rw-r--r--src/corelib/animation/qsequentialanimationgroup_p.h1
-rw-r--r--src/corelib/animation/qvariantanimation.cpp1
-rw-r--r--src/corelib/animation/qvariantanimation.h1
-rw-r--r--src/corelib/animation/qvariantanimation_p.h1
-rw-r--r--src/corelib/thread/qmutex.cpp19
-rw-r--r--src/gui/kernel/qtestsupport_gui.cpp63
-rw-r--r--src/gui/kernel/qtestsupport_gui.h16
-rw-r--r--src/network/socket/qnativesocketengine_unix.cpp6
-rw-r--r--src/plugins/styles/modernwindows/qwindows11style.cpp5
-rw-r--r--src/tools/androiddeployqt/main.cpp19
-rw-r--r--src/tools/androidtestrunner/main.cpp155
-rw-r--r--src/widgets/kernel/qtestsupport_widgets.cpp63
-rw-r--r--src/widgets/kernel/qtestsupport_widgets.h13
-rw-r--r--src/widgets/widgets/qdockarealayout.cpp3
-rw-r--r--tests/auto/corelib/platform/android_legacy_packaging/testdata/build.gradle2
-rw-r--r--tests/auto/other/android/CMakeLists.txt8
-rw-r--r--tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp9
-rw-r--r--tests/auto/other/android/dynamic_feature/CMakeLists.txt33
-rw-r--r--tests/auto/other/android/dynamic_feature/feature/CMakeLists.txt8
-rw-r--r--tests/auto/other/android/dynamic_feature/feature/qtlogo.pngbin0 -> 1214 bytes
-rw-r--r--tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoader.java175
-rw-r--r--tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListener.java92
-rw-r--r--tests/auto/other/android/dynamic_feature/storeloader/java/src/main/java/org/qtproject/example/android_dynamic_feature/StoreLoaderListenerCallback.java26
-rw-r--r--tests/auto/other/android/dynamic_feature/storeloader/storeloader.cpp227
-rw-r--r--tests/auto/other/android/dynamic_feature/storeloader/storeloader.h65
-rw-r--r--tests/auto/other/android/dynamic_feature/tst_android_dynamic_feature.cpp37
-rw-r--r--tests/auto/other/android/package_source_dir/CMakeLists.txt54
-rw-r--r--tests/auto/other/android/package_source_dir/custom_android_manifest/AndroidManifest.xml51
-rw-r--r--tests/auto/other/android/package_source_dir/custom_android_manifest_bundle/app/AndroidManifest.xml51
-rw-r--r--tests/auto/other/android/package_source_dir/partial_template/app/AndroidManifest.xml51
-rw-r--r--tests/auto/other/android/package_source_dir/partial_template/app/build.gradle.in54
-rw-r--r--tests/auto/other/android/package_source_dir/tst_android_package_source_dir.cpp47
-rw-r--r--tests/auto/other/android/permissions/CMakeLists.txt23
-rw-r--r--tests/auto/other/android/permissions/tst_android_permissions.cpp80
73 files changed, 2359 insertions, 208 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 c5db1039146..911b6001564 100644
--- a/cmake/QtBaseHelpers.cmake
+++ b/cmake/QtBaseHelpers.cmake
@@ -237,7 +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 dfcdf12b6a7..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,19 +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"
)
if(QT_USE_ANDROID_MODERN_BUNDLE)
_qt_internal_android_get_target_deployment_dir(target_deployment_dir ${target})
- list(APPEND ${out_test_arguments} "--manifest" "${target_deployment_dir}/AndroidManifest.xml")
+ list(APPEND test_arguments
+ "--manifest" "${target_deployment_dir}/AndroidManifest.xml")
endif()
- set(${out_test_arguments} "${${out_test_arguments}}" PARENT_SCOPE)
+ 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/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
index 253626e3b53..546a4fc50f3 100644
--- a/src/android/templates_cmake/CMakeLists.txt
+++ b/src/android/templates_cmake/CMakeLists.txt
@@ -3,10 +3,20 @@
set(templates_files
"${CMAKE_CURRENT_SOURCE_DIR}/settings.gradle.in"
- "${CMAKE_CURRENT_SOURCE_DIR}/build.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}
@@ -15,10 +25,22 @@ add_custom_target(Qt6AndroidCMakeTemplates
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/build.gradle.in b/src/android/templates_cmake/app/build.gradle.in
index a76172594bd..917f371e46f 100644
--- a/src/android/templates_cmake/build.gradle.in
+++ b/src/android/templates_cmake/app/build.gradle.in
@@ -5,7 +5,7 @@ buildscript {
}
dependencies {
- classpath 'com.android.tools.build:gradle:8.6.0'
+ classpath 'com.android.tools.build:gradle:8.10.1'
}
}
@@ -44,8 +44,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/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/settings.gradle.in b/src/android/templates_cmake/settings.gradle.in
index 4aca55ffd6d..03954ae5f9f 100644
--- a/src/android/templates_cmake/settings.gradle.in
+++ b/src/android/templates_cmake/settings.gradle.in
@@ -15,3 +15,4 @@ dependencyResolutionManagement {
rootProject.name = "@ROOT_PROJECT_NAME@"
include(":app")
+@SUBPROJECTS@
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt
index bdf0bee8166..249b9b8c1ee 100644
--- a/src/corelib/CMakeLists.txt
+++ b/src/corelib/CMakeLists.txt
@@ -17,7 +17,9 @@ 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}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)
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
index 889438ffaad..09184c93b69 100644
--- a/src/corelib/Qt6AndroidGradleHelpers.cmake
+++ b/src/corelib/Qt6AndroidGradleHelpers.cmake
@@ -1,12 +1,61 @@
# 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_template_dir(template_directory)
- set(template_file "${template_directory}/${settings_gradle_filename}.in")
+ _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
@@ -16,16 +65,116 @@ function(_qt_internal_android_generate_bundle_settings_gradle target)
">"
)
- _qt_internal_android_get_target_android_build_dir(android_build_dir ${target})
- set(settings_gradle_file "${android_build_dir}/${settings_gradle_filename}")
+ 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.
@@ -42,23 +191,8 @@ function(_qt_internal_android_generate_target_build_gradle target)
"Please check your Android SDK installation.")
endif()
- _qt_internal_android_java_dir(android_java_dir)
- string(JOIN "\n " SOURCE_SETS
- "manifest.srcFile 'AndroidManifest.xml'"
- "java.srcDirs = ['${android_java_dir}/src', 'src', 'java']"
- "aidl.srcDirs = ['${android_java_dir}/src', 'src', 'aidl']"
- "res.srcDirs = ['${android_java_dir}/res', 'res']"
- "resources.srcDirs = ['resources']"
- "renderscript.srcDirs = ['src']"
- "assets.srcDirs = ['assets']"
- "jniLibs.srcDirs = ['libs']"
- )
-
- string(JOIN "\n " GRADLE_DEPENDENCIES
- "implementation fileTree(dir: 'libs', include: ['*.jar', '*.aar'])"
- "//noinspection GradleDependency"
- "implementation 'androidx.core:core:1.13.1'"
- )
+ _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")
@@ -76,21 +210,36 @@ function(_qt_internal_android_generate_target_build_gradle target)
"ndk.abiFilters = ['${target_abi_list}']"
)
- set(ANDROID_DEPLOYMENT_EXTRAS "")
+ 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_target_deployment_dir(target_deployment_dir ${target})
- set(build_gradle_filename "build.gradle")
- set(build_gradle_file "${target_deployment_dir}/${build_gradle_filename}")
- _qt_internal_android_template_dir(template_directory)
- _qt_internal_configure_file(GENERATE OUTPUT "${build_gradle_file}"
- INPUT "${template_directory}/${build_gradle_filename}.in")
+ _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.
@@ -99,21 +248,17 @@ function(_qt_internal_android_prepare_gradle_build 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})
- _qt_internal_android_generate_target_gradle_properties(${target})
-
- set(gradle_scripts
- "${android_build_dir}/gradle.properties"
- "${android_build_dir}/local.properties"
- "${android_build_dir}/settings.gradle"
- "${deployment_dir}/build.gradle"
- "${deployment_dir}/gradle.properties"
- )
- set_target_properties(${target} PROPERTIES _qt_android_deployment_files "${gradle_scripts}")
+ _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)
@@ -146,6 +291,15 @@ function(_qt_internal_android_add_gradle_build target type)
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}"
@@ -159,6 +313,7 @@ function(_qt_internal_android_add_gradle_build target type)
${gradle_scripts}
${target}_copy_gradle_files
${target}_android_deploy_aux
+ ${extra_deps}
WORKING_DIRECTORY
"${android_build_dir}"
VERBATIM
@@ -235,13 +390,120 @@ 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)
- set(gradle_properties "gradle.properties")
- _qt_internal_android_template_dir(template_directory)
- set(template_file "${template_directory}/${gradle_properties}.in")
- _qt_internal_android_get_target_deployment_dir(deployment_dir ${target})
+ cmake_parse_arguments(PARSE_ARGV 1 arg "" "DEPLOYMENT_DIR" "")
- _qt_internal_configure_file(CONFIGURE OUTPUT "${deployment_dir}/${gradle_properties}"
- INPUT "${template_file}")
+ 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
@@ -252,24 +514,88 @@ function(_qt_internal_android_generate_bundle_gradle_properties target)
set(gradle_properties_file_name "gradle.properties")
_qt_internal_android_get_target_android_build_dir(android_build_dir ${target})
- _qt_internal_android_template_dir(template_directory)
- set(template_file "${template_directory}/${gradle_properties_file_name}.in")
+ 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 "${android_build_dir}/${gradle_properties_file_name}"
+ 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 "${android_build_dir}/local.properties"
+ _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)
diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake
index 960ca73778b..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()
@@ -1653,12 +1639,16 @@ 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}")
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}")
@@ -1668,6 +1658,7 @@ function(_qt_internal_android_executable_finalizer target)
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
@@ -1760,7 +1752,7 @@ function(_qt_internal_android_get_target_deployment_dir out_deploy_dir target)
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}")
@@ -1800,8 +1792,6 @@ function(_qt_internal_android_add_aux_deployment target)
_qt_internal_android_get_target_deployment_dir(deployment_dir ${target})
endif()
- set(android_manifest "${deployment_dir}/AndroidManifest.xml")
-
cmake_policy(PUSH)
if(POLICY CMP0116)
# Without explicitly setting this policy to NEW, we get a warning
@@ -1822,7 +1812,11 @@ function(_qt_internal_android_add_aux_deployment target)
_qt_internal_android_get_use_terminal_for_deployment(uses_terminal)
- add_custom_command(OUTPUT "${android_manifest}"
+ # 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}"
@@ -1841,7 +1835,7 @@ function(_qt_internal_android_add_aux_deployment target)
set(arg_OUTPUT_TARGET_NAME ${target}_android_deploy_aux)
endif()
- add_custom_target(${arg_OUTPUT_TARGET_NAME} DEPENDS "${android_manifest}")
+ add_custom_target(${arg_OUTPUT_TARGET_NAME} DEPENDS "${libs_xml}")
cmake_policy(POP)
endfunction()
@@ -1901,6 +1895,23 @@ function(_qt_internal_android_template_dir out_var)
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)
@@ -1930,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 37155ac716b..8a88d558fa8 100644
--- a/src/corelib/Qt6CoreConfigExtras.cmake.in
+++ b/src/corelib/Qt6CoreConfigExtras.cmake.in
@@ -30,7 +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/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/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/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/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 4e888551445..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);
}
}
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/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/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
new file mode 100644
index 00000000000..b63f1384b11
--- /dev/null
+++ b/tests/auto/other/android/dynamic_feature/feature/qtlogo.png
Binary files differ
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"