diff options
author | Juha Vuolle <[email protected]> | 2024-09-02 10:21:04 +0300 |
---|---|---|
committer | Juha Vuolle <[email protected]> | 2024-09-16 11:59:49 +0300 |
commit | c76c888556d21f93fc087b52e96a1b7f06465a2a (patch) | |
tree | aee53c1c1daa73cf87a74939b93d6e0cac6dddf2 | |
parent | 3af20bd8eb8c75017c5d6d138d7c42914ee5bee3 (diff) |
Introduce qt_add_android_permission CMake function
qt_add_android_permission function can be used to set
Android permissions on target executable. This allows
setting new permissions, or overriding permissions set
by Qt modules, without needing to supply a manual application
AndroidManifest.xml.
The change consists of:
- New public CMake function for setting the permissions
on the target + documentation
- Writing these application permissions into the deployment
settings json file
- Reading and handling these permissions at
androiddeployqt side
- Moving some pre-existing permission functionality from
QtAndroidHelpers.cmake to Qt6AndroidMacros.cmake
so that they can be reused also in the context
of application CMakeLists.txt processing
- Documentation update for Android permission handling
In future this same mechanism can be extended for Android
features.
[ChangeLog][CMake] Added qt_add_android_permission function
for setting Android permissions from application CMake
Fixes: QTBUG-128280
Change-Id: Ia22951fb435598be00b5da5eae11b9f35f704795
Reviewed-by: Assam Boudjelthia <[email protected]>
Reviewed-by: Alexey Edelev <[email protected]>
8 files changed, 211 insertions, 48 deletions
diff --git a/cmake/QtAndroidHelpers.cmake b/cmake/QtAndroidHelpers.cmake index 9e81e862058..81b5817967c 100644 --- a/cmake/QtAndroidHelpers.cmake +++ b/cmake/QtAndroidHelpers.cmake @@ -89,40 +89,7 @@ macro(qt_internal_setup_android_target_properties) endmacro() function(qt_internal_add_android_permission target) - cmake_parse_arguments(arg "" "NAME" "ATTRIBUTES" ${ARGN}) - - if(NOT target) - message(FATAL_ERROR "Target for adding Android permission cannot be empty (${arg_NAME})") - endif() - - 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 attributes must be name-value pairs (${arg_NAME})") - 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}") + _qt_internal_add_android_permission(${ARGV}) endfunction() @@ -233,9 +200,9 @@ function(qt_internal_android_dependencies_content target file_content_out) elseif(permission_len EQUAL 2) list(GET permission 0 name) list(GET permission 1 extras) - string(APPEND file_contents "<permission name=\"${name}\" extras=\'${extras}\'/>\n") + string(APPEND file_contents "<permission name=\"${name}\" extras=\"${extras}\"/>\n") else() - message(FATAL_ERROR "Unexpected permission: " "${permission}" "${permission_len}") + message(FATAL_ERROR "Invalid permission format: ${permission} ${permission_len}") endif() endforeach() endif() diff --git a/src/android/templates/doc/src/android-manifest-file-configuration.qdoc b/src/android/templates/doc/src/android-manifest-file-configuration.qdoc index 4cf03564ee2..51a4679fb72 100644 --- a/src/android/templates/doc/src/android-manifest-file-configuration.qdoc +++ b/src/android/templates/doc/src/android-manifest-file-configuration.qdoc @@ -258,10 +258,14 @@ Since Qt 6.9, it is possible to override the default permissions set by Qt modules. This is useful if you need to define the same permissions as used by a Qt module, but with additional or different attributes. -To achieve this, you can manually define these permissions in the Android -manifest file, along with the \c {<!-- %%INSERT_PERMISSIONS -->} placeholder. -Manually defined permissions take precedence over the same permissions added -by Qt modules, avoiding duplication. +There are two ways to achieve this. First way is to use +\l {qt_add_android_permission} CMake function in the application's +\c {CMakeLists.txt}. Permissions defined this way take precedence over +the same permissions defined by Qt modules, avoiding duplication. + +Second way is to manually define these permissions in the Android +manifest file. Permissions defined this way take precedence over permissions +set by Qt modules, or set with \l {qt_add_android_permission}. \section2 Style Extraction diff --git a/src/corelib/Qt6AndroidMacros.cmake b/src/corelib/Qt6AndroidMacros.cmake index a03fbd270dd..6d40f3ba5da 100644 --- a/src/corelib/Qt6AndroidMacros.cmake +++ b/src/corelib/Qt6AndroidMacros.cmake @@ -50,6 +50,43 @@ function(_qt_internal_add_tool_to_android_deployment_settings out_var tool json_ set(${out_var} "${${out_var}}" PARENT_SCOPE) endfunction() +# Generates a JSON array of permissions that the 'target' may have, +# returns an empty JSON array if no permissions were found. +function(_qt_internal_generate_android_permissions_json out_result target) + + set(${out_result} "[]" PARENT_SCOPE) + + if(NOT TARGET ${target}) + return() + endif() + + get_target_property(permissions ${target} QT_ANDROID_PERMISSIONS) + if(NOT permissions) + return() + endif() + + set(result "[") + set(json_objects "") + foreach(permission IN LISTS permissions) + # Check if the permission has also extra attributes in addition to the permission name + list(LENGTH permission permission_len) + if(permission_len EQUAL 1) + list(APPEND json_objects "{ \"name\": \"${permission}\" }") + elseif(permission_len EQUAL 2) + list(GET permission 0 name) + list(GET permission 1 extras) + list(APPEND json_objects "{ \"name\": \"${name}\", \"extras\": \"${extras}\" }") + else() + message(FATAL_ERROR "Invalid permission format: ${permission} ${permission_len}") + endif() + endforeach() + + # Join all JSON objects with a comma. This also avoids trailing commas JSON doesn't accept + string(JOIN ",\n " joined_json_objects ${json_objects}) + string(APPEND result "\n ${joined_json_objects}\n ]") + set(${out_result} "${result}" PARENT_SCOPE) +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 @@ -257,6 +294,9 @@ function(qt6_android_generate_deployment_settings target) __qt_internal_collect_plugin_library_files("${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") + # App binary string(APPEND file_contents " \"application-binary\": \"${target_output_name}\",\n") @@ -345,6 +385,54 @@ 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() + +if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) + function(qt_add_android_permission target) + qt6_add_android_permission(${ARGV}) + endfunction() +endif() + function(qt6_android_apply_arch_suffix target) get_target_property(called_from_qt_impl ${target} _qt_android_apply_arch_suffix_called_from_qt_impl) diff --git a/src/corelib/doc/snippets/cmake-macros/examples.cmake b/src/corelib/doc/snippets/cmake-macros/examples.cmake index 835d45bc089..db6bc5c6cd9 100644 --- a/src/corelib/doc/snippets/cmake-macros/examples.cmake +++ b/src/corelib/doc/snippets/cmake-macros/examples.cmake @@ -92,6 +92,21 @@ qt_android_generate_deployment_settings(myapp) qt_android_add_apk_target(myapp) #! [qt_android_deploy_basic] +#! [qt_add_android_permission] +qt_add_executable(myapp + // ... +) +qt_add_android_permission(myapp + NAME android.permission.BLUETOOTH_SCAN + ATTRIBUTES + minSdkVersion 31 + usesPermissionFlags neverForLocation +) +qt_add_android_permission(myapp + NAME android.permission.ACCESS_COARSE_LOCATION +) +#! [qt_add_android_permission] + #! [qt_finalize_project_manual] cmake_minimum_required(VERSIONS 3.16) diff --git a/src/corelib/doc/src/cmake/qt_add_android_permission.qdoc b/src/corelib/doc/src/cmake/qt_add_android_permission.qdoc new file mode 100644 index 00000000000..68fe6a8e5cc --- /dev/null +++ b/src/corelib/doc/src/cmake/qt_add_android_permission.qdoc @@ -0,0 +1,38 @@ +// Copyright (C) 2024 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +/*! +\page qt-add-android-permission.html +\ingroup cmake-commands-qtcore + +\title qt_add_android_permission +\keyword qt6_add_android_permission + +\summary {Adds an Android permission to the target executable.} + +\include cmake-find-package-core.qdocinc + +\cmakecommandsince 6.9 + +\section1 Synopsis + +\badcode +qt_add_android_permission(target NAME <permission-name> [ATTRIBUTES <name1> <value1> ...]) +\endcode + +\versionlessCMakeCommandsNote qt6_add_android_permission() + +\section1 Description + +The command adds an Android permission to the \c {target} executable. +This can be used to define additional permissions, or overriding +the default permissions set by Qt modules. + +For further information on defining Android permissions, +see \l {Qt Permissions and Features}. + +\section1 Example + +\snippet cmake-macros/examples.cmake qt_add_android_permission + +*/ diff --git a/src/tools/androiddeployqt/main.cpp b/src/tools/androiddeployqt/main.cpp index 5f8a8454b41..98324ab5b25 100644 --- a/src/tools/androiddeployqt/main.cpp +++ b/src/tools/androiddeployqt/main.cpp @@ -242,7 +242,8 @@ struct Options // Per package collected information // permissions 'name' => 'optional additional attributes' - QMap<QString, QString> permissions; + QMap<QString, QString> modulePermissions; + QMap<QString, QString> applicationPermissions; QStringList features; // Override qml import scanner path @@ -1457,6 +1458,21 @@ bool readInputFile(Options *options) } } + { + QJsonArray permissions = jsonObject.value("permissions"_L1).toArray(); + if (!permissions.isEmpty()) { + for (const QJsonValue &value : permissions) { + if (value.isObject()) { + QJsonObject permissionObj = value.toObject(); + QString name = permissionObj.value("name"_L1).toString(); + QString extras; + if (permissionObj.contains("extras"_L1)) + extras = permissionObj.value("extras"_L1).toString().trimmed(); + options->applicationPermissions.insert(name, extras); + } + } + } + } return true; } @@ -1896,14 +1912,23 @@ bool updateAndroidManifest(Options &options) QXmlStreamReader reader(&androidManifestXml); while (!reader.atEnd()) { reader.readNext(); - if (reader.isStartElement() && reader.name() == "uses-permission"_L1) - options.permissions.remove(QString(reader.attributes().value("android:name"_L1))); + if (reader.isStartElement() && reader.name() == "uses-permission"_L1) { + options.modulePermissions.remove( + QString(reader.attributes().value("android:name"_L1))); + options.applicationPermissions.remove( + QString(reader.attributes().value("android:name"_L1))); + } } androidManifestXml.close(); } + // Application may define permissions in its CMakeLists.txt, give them the priority + QMap<QString, QString> resolvedPermissions = options.modulePermissions; + for (auto [name, extras] : options.applicationPermissions.asKeyValueRange()) + resolvedPermissions.insert(name, extras); + QString permissions; - for (auto [name, extras] : options.permissions.asKeyValueRange()) + for (auto [name, extras] : resolvedPermissions.asKeyValueRange()) permissions += " <uses-permission android:name=\"%1\" %2 />\n"_L1.arg(name).arg(extras); replacements[QStringLiteral("<!-- %%INSERT_PERMISSIONS -->")] = permissions.trimmed(); @@ -2172,9 +2197,9 @@ bool readAndroidDependencyXml(Options *options, QString extras = reader.attributes().value("extras"_L1).toString(); // With duplicate permissions prioritize the one without any attributes, // as that is likely the most permissive - if (!options->permissions.contains(name) - || !options->permissions.value(name).isEmpty()) { - options->permissions.insert(name, extras); + if (!options->modulePermissions.contains(name) + || !options->modulePermissions.value(name).isEmpty()) { + options->modulePermissions.insert(name, extras); } } else if (reader.name() == "feature"_L1) { QString name = reader.attributes().value("name"_L1).toString(); diff --git a/tests/auto/other/android_deployment_settings/CMakeLists.txt b/tests/auto/other/android_deployment_settings/CMakeLists.txt index 613b1f29096..cd11c2c5e9d 100644 --- a/tests/auto/other/android_deployment_settings/CMakeLists.txt +++ b/tests/auto/other/android_deployment_settings/CMakeLists.txt @@ -18,6 +18,13 @@ function(tst_generate_android_deployment_setting target) qt6_android_generate_deployment_settings(${target}) endfunction() +function(tst_add_android_permissions target) + qt6_add_android_permission(${target} NAME PERMISSION_WITH_ATTRIBUTES + ATTRIBUTES + minSdkVersion 32 maxSdkVersion 34) + qt6_add_android_permission(${target} NAME PERMISSION_WITHOUT_ATTRIBUTES) +endfunction() + qt6_policy(SET QTP0002 NEW) set(target tst_android_deployment_settings_new) @@ -46,6 +53,7 @@ set_target_properties(${target} PROPERTIES # qt6_android_generate_deployment_settings QT_ANDROID_DEPLOYMENT_SETTINGS_FILE "custom_deployment_settings.json" ) +tst_add_android_permissions(${target}) tst_generate_android_deployment_setting(${target}) qt6_policy(SET QTP0002 OLD) @@ -67,6 +75,7 @@ set_target_properties(${target} PROPERTIES QT_ANDROID_PACKAGE_SOURCE_DIR "path\\to/source\\dir" QT_ANDROID_SYSTEM_LIBS_PREFIX "myLibPrefix" ) +tst_add_android_permissions(${target}) tst_generate_android_deployment_setting(${target}) get_target_property(new_settings 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 43797245259..1687ed9de92 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 @@ -1,6 +1,7 @@ // Copyright (C) 2023 The Qt Company Ltd. // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only +#include <QJsonArray> #include <QJsonDocument> #include <QJsonObject> #include <QFile> @@ -82,13 +83,29 @@ 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\"}]"; } void tst_android_deployment_settings::DeploymentSettings() { QFETCH(QString, key); QFETCH(QString, value); - QCOMPARE(jsonDoc[key].toString(), value); + QJsonValue keyValue = jsonDoc[key]; + if (keyValue.type() == QJsonValue::Type::String) { + QCOMPARE(keyValue.toString(), value); + } else if (keyValue.type() == QJsonValue::Type::Array) { + QJsonParseError parseError; + // For robustness (field order, whitespaces etc.) make comparison between QJsonDocuments + QJsonDocument expectedDoc = QJsonDocument::fromJson(value.toUtf8(), &parseError); + if (parseError.error != QJsonParseError::NoError) + qFatal("Failed to parse expected JSON array: %s", qPrintable(parseError.errorString())); + QCOMPARE(QJsonDocument(keyValue.toArray()), expectedDoc); + } else { + qFatal("Unhandled JSON type: %i", keyValue.type()); + } } void tst_android_deployment_settings::QtPaths_data() |