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 <assam.boudjelthia@qt.io>
Reviewed-by:  Alexey Edelev <alexey.edelev@qt.io>
This commit is contained in:
Juha Vuolle 2024-09-02 10:21:04 +03:00
parent 3af20bd8eb
commit c76c888556
8 changed files with 211 additions and 48 deletions

View File

@ -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()

View File

@ -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

View File

@ -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)

View File

@ -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)

View File

@ -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
*/

View File

@ -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();

View File

@ -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

View File

@ -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()