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