diff options
author | Alexey Edelev <[email protected]> | 2025-05-21 17:33:09 +0200 |
---|---|---|
committer | Alexey Edelev <[email protected]> | 2025-07-07 21:20:38 +0200 |
commit | c33a769f5c1b1a5014ca724d110439c291709ff4 (patch) | |
tree | 70d8789f8d37d83ca109715e493c3837942d3599 | |
parent | c49ca53ecf05309c4e56684bc47d309cb13115e7 (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.cmake | 18 | ||||
-rw-r--r-- | cmake/QtBaseHelpers.cmake | 1 | ||||
-rw-r--r-- | cmake/QtPostProcessHelpers.cmake | 1 | ||||
-rw-r--r-- | src/corelib/CMakeLists.txt | 1 | ||||
-rw-r--r-- | src/corelib/Qt6AndroidGradleHelpers.cmake | 2 | ||||
-rw-r--r-- | src/corelib/Qt6AndroidMacros.cmake | 16 | ||||
-rw-r--r-- | src/corelib/Qt6AndroidPermissionHelpers.cmake | 68 | ||||
-rw-r--r-- | src/corelib/Qt6CoreConfigExtras.cmake.in | 2 | ||||
-rw-r--r-- | src/tools/androiddeployqt/main.cpp | 19 | ||||
-rw-r--r-- | tests/auto/other/android/CMakeLists.txt | 1 | ||||
-rw-r--r-- | tests/auto/other/android/deployment_settings/tst_android_deployment_settings.cpp | 9 | ||||
-rw-r--r-- | tests/auto/other/android/permissions/CMakeLists.txt | 23 | ||||
-rw-r--r-- | tests/auto/other/android/permissions/tst_android_permissions.cpp | 80 |
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" |