summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAlexey Edelev <[email protected]>2025-05-21 17:33:09 +0200
committerAlexey Edelev <[email protected]>2025-07-07 21:20:38 +0200
commitc33a769f5c1b1a5014ca724d110439c291709ff4 (patch)
tree70d8789f8d37d83ca109715e493c3837942d3599
parentc49ca53ecf05309c4e56684bc47d309cb13115e7 (diff)
Rework android permission handling
Current QT_ANDROID_PERMISSIONS property format is inconvenient for use in the CMake generator expressions and mixes attribute syntax with CMake list syntax. This suggests the new format for the QT_ANDROID_PERMISSIONS property. Each element is encoded the following way: <android:name>\;<permission>\\\;<extra1>\;<value>\\\;<extra2>\;<value> Elements are separated using standard CMake semicolons. QT_ANDROID_PERMISSIONS is now transitive LINK property. This feature deprecates the '<permission' records in the Qt6<Module>-android-dependencies.xml files. If application links Qt Module that requires specific permissions, these permissions will be written to the application deployment-settings.json file. The 'permissions' record in the application deployment-settings.json file is changed too, the new format is following: "permissions": [{ "name": "permission", "extra1": "value", "extra2": "value" }] Comparing to the previous format each extra attribute is stored under a separate key in permission object. IMPORTANT: androiddeployqt has no backward compatibility with the old format. With QT_USE_ANDROID_MODERN_BUNDLE enabled permissions are written directly to the AndroidManifest.xml without androiddeployqt involved. Supply tests for the Android permissions, that reads the manifest-declared permissions in test using the Android PackageManager API. Change-Id: I691df33c70acc6c7139302b119edc791fef8d5ef Reviewed-by: Assam Boudjelthia <[email protected]>
-rw-r--r--cmake/QtAndroidHelpers.cmake18
-rw-r--r--cmake/QtBaseHelpers.cmake1
-rw-r--r--cmake/QtPostProcessHelpers.cmake1
-rw-r--r--src/corelib/CMakeLists.txt1
-rw-r--r--src/corelib/Qt6AndroidGradleHelpers.cmake2
-rw-r--r--src/corelib/Qt6AndroidMacros.cmake16
-rw-r--r--src/corelib/Qt6AndroidPermissionHelpers.cmake68
-rw-r--r--src/corelib/Qt6CoreConfigExtras.cmake.in2
-rw-r--r--src/tools/androiddeployqt/main.cpp19
-rw-r--r--tests/auto/other/android/CMakeLists.txt1
-rw-r--r--tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp9
-rw-r--r--tests/auto/other/android/permissions/CMakeLists.txt23
-rw-r--r--tests/auto/other/android/permissions/tst_android_permissions.cpp80
13 files changed, 225 insertions, 16 deletions
diff --git a/cmake/QtAndroidHelpers.cmake b/cmake/QtAndroidHelpers.cmake
index 64696fff6d4..41eaf55d8e2 100644
--- a/cmake/QtAndroidHelpers.cmake
+++ b/cmake/QtAndroidHelpers.cmake
@@ -473,3 +473,21 @@ 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()
diff --git a/cmake/QtBaseHelpers.cmake b/cmake/QtBaseHelpers.cmake
index c5db1039146..bdd80e6027d 100644
--- a/cmake/QtBaseHelpers.cmake
+++ b/cmake/QtBaseHelpers.cmake
@@ -238,6 +238,7 @@ macro(qt_internal_qtbase_build_repo)
if(ANDROID)
include(src/corelib/Qt6AndroidMacros.cmake)
include(src/corelib/Qt6AndroidGradleHelpers.cmake)
+ include(src/corelib/Qt6AndroidPermissionHelpers.cmake)
endif()
# Needed when building for WebAssembly.
diff --git a/cmake/QtPostProcessHelpers.cmake b/cmake/QtPostProcessHelpers.cmake
index 9f220f9d78b..3f1468a4304 100644
--- a/cmake/QtPostProcessHelpers.cmake
+++ b/cmake/QtPostProcessHelpers.cmake
@@ -790,6 +790,7 @@ 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})
endforeach()
endfunction()
diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt
index bdf0bee8166..c693c28743a 100644
--- a/src/corelib/CMakeLists.txt
+++ b/src/corelib/CMakeLists.txt
@@ -18,6 +18,7 @@ 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}AndroidGradleHelpers.cmake"
+ "${CMAKE_CURRENT_SOURCE_DIR}/${QT_CMAKE_EXPORT_NAMESPACE}AndroidPermissionHelpers.cmake"
)
endif()
if(WASM)
diff --git a/src/corelib/Qt6AndroidGradleHelpers.cmake b/src/corelib/Qt6AndroidGradleHelpers.cmake
index c238b6d981f..500946f0e9b 100644
--- a/src/corelib/Qt6AndroidGradleHelpers.cmake
+++ b/src/corelib/Qt6AndroidGradleHelpers.cmake
@@ -428,6 +428,8 @@ function(_qt_internal_android_generate_target_android_manifest target)
">"
)
+ _qt_internal_android_convert_permissions(APP_PERMISSIONS ${target} XML)
+
set(APP_ARGUMENTS "${QT_ANDROID_APPLICATION_ARGUMENTS}")
_qt_internal_configure_file(GENERATE OUTPUT "${out_file}.tmp"
diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake
index a1530fb9d18..df4932428b3 100644
--- a/src/corelib/Qt6AndroidMacros.cmake
+++ b/src/corelib/Qt6AndroidMacros.cmake
@@ -324,8 +324,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
@@ -426,27 +427,24 @@ function(_qt_internal_add_android_permission target)
message(FATAL_ERROR "NAME for adding Android permission cannot be empty (${target})")
endif()
- set(permission_entry "${arg_NAME}")
-
+ 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)")
+ 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}\' ")
+ string(APPEND permission_entry "\\\;${name}\;${value}")
math(EXPR index "${index} + 1")
endwhile()
- set(permission_entry "${permission_entry}\;${attributes}")
endif()
# Append the permission to the target's property
diff --git a/src/corelib/Qt6AndroidPermissionHelpers.cmake b/src/corelib/Qt6AndroidPermissionHelpers.cmake
new file mode 100644
index 00000000000..27409f06d06
--- /dev/null
+++ b/src/corelib/Qt6AndroidPermissionHelpers.cmake
@@ -0,0 +1,68 @@
+# 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()
diff --git a/src/corelib/Qt6CoreConfigExtras.cmake.in b/src/corelib/Qt6CoreConfigExtras.cmake.in
index 37155ac716b..857532650b5 100644
--- a/src/corelib/Qt6CoreConfigExtras.cmake.in
+++ b/src/corelib/Qt6CoreConfigExtras.cmake.in
@@ -31,6 +31,8 @@ _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]")
+
_qt_internal_create_global_android_targets()
_qt_internal_collect_default_android_abis()
if(__qt_Core_targets_file_included)
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/tests/auto/other/android/CMakeLists.txt b/tests/auto/other/android/CMakeLists.txt
index 92fcfa2d932..9c977441108 100644
--- a/tests/auto/other/android/CMakeLists.txt
+++ b/tests/auto/other/android/CMakeLists.txt
@@ -2,6 +2,7 @@
# 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)
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/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"