From f0a7d74e1dd2c1d802aa09d7b8c144599f4a54ce Mon Sep 17 00:00:00 2001 From: Timur Pocheptsov Date: Tue, 10 May 2022 15:02:43 +0200 Subject: [PATCH] Add permission API backend for macOS and iOS MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit When submitting applications to the iOS and macOS AppStore the application goes through static analysis, which will trigger on uses of various privacy protected APIs, unless the application has a corresponding usage description for the permission in the Info.plist file. This applies even if the application never requests the given permission, but just links to a Qt library that has the offending symbols or library dependencies. To ensure that the application does not have to add usage descriptions to their Info.plist for permissions they never plan to use we split up the various permission implementations into small static libraries that register with the Qt plugin mechanism as permission backends. We can then inspect the application's Info.plist at configure time and only add the relevant static permission libraries. Furthermore, since some permissions can be checked without any usage description, we allow the implementation to be split up into two separate translation units. By putting the request in its own translation unit we can selectively include it during linking by telling the linker to look for a special symbol. This is useful for libraries such as Qt Multimedia who would like to check the current permission status, but without needing to request any permission of its own. Done-with: Tor Arne Vestbø Change-Id: Ic2a43e1a0c45a91df6101020639f473ffd9454cc Reviewed-by: Tor Arne Vestbø --- cmake/QtFrameworkHelpers.cmake | 4 + cmake/QtModuleConfig.cmake.in | 4 +- cmake/QtPluginHelpers.cmake | 78 ++++++ examples/corelib/permissions/CMakeLists.txt | 10 + examples/corelib/permissions/Info.plist | 59 +++++ mkspecs/features/permissions.prf | 25 ++ mkspecs/features/qt.prf | 3 + src/corelib/CMakeLists.txt | 61 +++++ src/corelib/Qt6CoreConfigExtras.cmake.in | 9 + src/corelib/Qt6CoreMacros.cmake | 24 ++ src/corelib/configure.cmake | 2 +- src/corelib/kernel/qpermissions.cpp | 32 ++- src/corelib/kernel/qpermissions_darwin.mm | 88 +++++++ src/corelib/kernel/qpermissions_p.h | 16 +- .../darwin/qdarwinpermissionplugin.mm | 90 +++++++ .../qdarwinpermissionplugin_bluetooth.mm | 84 +++++++ .../qdarwinpermissionplugin_calendar.mm | 57 +++++ .../darwin/qdarwinpermissionplugin_camera.mm | 42 ++++ .../qdarwinpermissionplugin_contacts.mm | 58 +++++ .../qdarwinpermissionplugin_location.mm | 230 ++++++++++++++++++ .../qdarwinpermissionplugin_microphone.mm | 42 ++++ .../darwin/qdarwinpermissionplugin_p.h | 58 +++++ .../darwin/qdarwinpermissionplugin_p_p.h | 102 ++++++++ tests/manual/permissions/CMakeLists.txt | 49 ++++ tests/manual/permissions/Info.plist | 59 +++++ tests/manual/permissions/tst_qpermissions.cpp | 5 + 26 files changed, 1286 insertions(+), 5 deletions(-) create mode 100644 examples/corelib/permissions/Info.plist create mode 100644 mkspecs/features/permissions.prf create mode 100644 src/corelib/kernel/qpermissions_darwin.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_p.h create mode 100644 src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h create mode 100644 tests/manual/permissions/Info.plist diff --git a/cmake/QtFrameworkHelpers.cmake b/cmake/QtFrameworkHelpers.cmake index 3b4cb012230..e4e2a1373fc 100644 --- a/cmake/QtFrameworkHelpers.cmake +++ b/cmake/QtFrameworkHelpers.cmake @@ -31,6 +31,10 @@ macro(qt_find_apple_system_frameworks) qt_internal_find_apple_system_framework(FWWatchKit WatchKit) qt_internal_find_apple_system_framework(FWGameController GameController) qt_internal_find_apple_system_framework(FWCoreBluetooth CoreBluetooth) + qt_internal_find_apple_system_framework(FWAVFoundation AVFoundation) + qt_internal_find_apple_system_framework(FWContacts Contacts) + qt_internal_find_apple_system_framework(FWEventKit EventKit) + qt_internal_find_apple_system_framework(FWHealthKit HealthKit) endif() endmacro() diff --git a/cmake/QtModuleConfig.cmake.in b/cmake/QtModuleConfig.cmake.in index 8ea763d86e4..55402f50ca9 100644 --- a/cmake/QtModuleConfig.cmake.in +++ b/cmake/QtModuleConfig.cmake.in @@ -89,12 +89,12 @@ if (NOT QT_NO_CREATE_TARGETS AND @INSTALL_CMAKE_NAMESPACE@@target@_FOUND) endif() if (TARGET @QT_CMAKE_EXPORT_NAMESPACE@::@target@) + qt_make_features_available(@QT_CMAKE_EXPORT_NAMESPACE@::@target@) + foreach(extra_cmake_include @extra_cmake_includes@) include("${CMAKE_CURRENT_LIST_DIR}/${extra_cmake_include}") endforeach() - qt_make_features_available(@QT_CMAKE_EXPORT_NAMESPACE@::@target@) - if(EXISTS "${CMAKE_CURRENT_LIST_DIR}/@INSTALL_CMAKE_NAMESPACE@@target@Plugins.cmake") include("${CMAKE_CURRENT_LIST_DIR}/@INSTALL_CMAKE_NAMESPACE@@target@Plugins.cmake") endif() diff --git a/cmake/QtPluginHelpers.cmake b/cmake/QtPluginHelpers.cmake index aca5421221d..20bde6f310d 100644 --- a/cmake/QtPluginHelpers.cmake +++ b/cmake/QtPluginHelpers.cmake @@ -507,3 +507,81 @@ function(qt_internal_get_module_for_plugin target target_type out_var) endforeach() message(FATAL_ERROR "The plug-in '${target}' does not belong to any Qt module.") endfunction() + +function(qt_internal_add_darwin_permission_plugin permission) + string(TOLOWER "${permission}" permission_lower) + string(TOUPPER "${permission}" permission_upper) + set(permission_source_file "platform/darwin/qdarwinpermissionplugin_${permission_lower}.mm") + set(plugin_target "QDarwin${permission}PermissionPlugin") + set(plugin_name "qdarwin${permission_lower}permission") + qt_internal_add_plugin(${plugin_target} + STATIC # Force static, even in shared builds + OUTPUT_NAME ${plugin_name} + PLUGIN_TYPE permissions + DEFAULT_IF FALSE + SOURCES + ${permission_source_file} + DEFINES + QT_DARWIN_PERMISSION_PLUGIN=${permission} + LIBRARIES + Qt::Core + Qt::CorePrivate + ) + + # Disable PCH since CMake falls over on single .mm source targets + set_target_properties(${plugin_target} PROPERTIES + DISABLE_PRECOMPILE_HEADERS ON + ) + + # Generate plugin JSON file + set(content "{ \"Permissions\": [ \"Q${permission}Permission\" ] }") + get_target_property(plugin_build_dir "${plugin_target}" BINARY_DIR) + set(output_file "${plugin_build_dir}/${plugin_target}.json") + qt_configure_file(OUTPUT "${output_file}" CONTENT "${content}") + + # Associate required usage descriptions + set(usage_descriptions_property "_qt_info_plist_usage_descriptions") + set_target_properties(${plugin_target} PROPERTIES + ${usage_descriptions_property} "NS${permission}UsageDescription" + ) + set_property(TARGET ${plugin_target} APPEND PROPERTY + EXPORT_PROPERTIES ${usage_descriptions_property} + ) + set(usage_descriptions_genex "$, >") + set(extra_plugin_pri_content + "QT_PLUGIN.${plugin_name}.usage_descriptions = ${usage_descriptions_genex}" + ) + + # Support granular check and request implementations + set(separate_request_source_file + "${plugin_build_dir}/qdarwinpermissionplugin_${permission_lower}_request.mm") + set(separate_request_genex + "$>") + file(GENERATE OUTPUT "${separate_request_source_file}" CONTENT + " + #define BUILDING_PERMISSION_REQUEST 1 + #include \"${CMAKE_CURRENT_SOURCE_DIR}/${permission_source_file}\" + " + CONDITION "${separate_request_genex}" + ) + target_sources(${plugin_target} PRIVATE + "$<${separate_request_genex}:${separate_request_source_file}>" + ) + set_property(TARGET ${plugin_target} APPEND PROPERTY + EXPORT_PROPERTIES _qt_darwin_permissison_separate_request + ) + set(permission_request_symbol "_QDarwin${permission}PermissionRequest") + set(permission_request_flag "-Wl,-u,${permission_request_symbol}") + set(has_usage_description_property "_qt_has_${plugin_target}_usage_description") + set(has_usage_description_genex "$>") + target_link_options(${plugin_target} INTERFACE + "$<$:${permission_request_flag}>") + list(APPEND extra_plugin_pri_content + "QT_PLUGIN.${plugin_name}.request_flag = $<${separate_request_genex}:${permission_request_flag}>" + ) + + # Expose properties to qmake + set_property(TARGET ${plugin_target} PROPERTY + QT_PLUGIN_PRI_EXTRA_CONTENT ${extra_plugin_pri_content} + ) +endfunction() diff --git a/examples/corelib/permissions/CMakeLists.txt b/examples/corelib/permissions/CMakeLists.txt index bca93b679f1..5c9af5f0d96 100644 --- a/examples/corelib/permissions/CMakeLists.txt +++ b/examples/corelib/permissions/CMakeLists.txt @@ -15,6 +15,11 @@ qt_add_executable(permissions main.cpp ) +set_target_properties(permissions PROPERTIES + MACOSX_BUNDLE TRUE + MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist" +) + target_link_libraries(permissions PUBLIC Qt::Core Qt::Gui @@ -26,3 +31,8 @@ install(TARGETS permissions BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" ) + +if(APPLE AND NOT CMAKE_GENERATOR STREQUAL "Xcode") + add_custom_command(TARGET permissions + POST_BUILD COMMAND codesign -s - permissions.app) +endif() diff --git a/examples/corelib/permissions/Info.plist b/examples/corelib/permissions/Info.plist new file mode 100644 index 00000000000..dce43caf125 --- /dev/null +++ b/examples/corelib/permissions/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + + CFBundleDevelopmentRegion + English + + NSSupportsAutomaticGraphicsSwitching + + + NSBluetoothAlwaysUsageDescription + Testing BluetoothAlways + NSCalendarsUsageDescription + Testing Calendars + NSCameraUsageDescription + Testing Camera + NSContactsUsageDescription + Testing Contacts + NSHealthShareUsageDescription + Testing HealthShare + NSHealthUpdateUsageDescription + Testing HealthUpdate + NSLocationAlwaysAndWhenInUseUsageDescription + Testing LocationAlwaysAndWhenInUse + NSLocationAlwaysUsageDescription + Testing LocationAlways + NSLocationWhenInUseUsageDescription + Testing LocationWhenInUse + NSMicrophoneUsageDescription + Testing Microphone + + + diff --git a/mkspecs/features/permissions.prf b/mkspecs/features/permissions.prf new file mode 100644 index 00000000000..d80df6d01e5 --- /dev/null +++ b/mkspecs/features/permissions.prf @@ -0,0 +1,25 @@ +isEmpty(QMAKE_INFO_PLIST): \ + return() + +for(plugin, QT_PLUGINS) { + !equals(QT_PLUGIN.$${plugin}.TYPE, permissions): \ + next() + + usage_descriptions = $$eval(QT_PLUGIN.$${plugin}.usage_descriptions) + for(usage_description_key, usage_descriptions) { + usage_description = $$system("/usr/libexec/PlistBuddy" \ + "-c 'print $$usage_description_key' $$QMAKE_INFO_PLIST 2>/dev/null") + !isEmpty(usage_description): \ + break() + } + + isEmpty(usage_description): \ + next() + + request_flag = $$eval(QT_PLUGIN.$${plugin}.request_flag) + + QTPLUGIN += $$plugin + QMAKE_LFLAGS += $$request_flag + + QMAKE_INTERNAL_INCLUDED_FILES *= $$QMAKE_INFO_PLIST +} diff --git a/mkspecs/features/qt.prf b/mkspecs/features/qt.prf index 71b6679af33..d8a8627d83f 100644 --- a/mkspecs/features/qt.prf +++ b/mkspecs/features/qt.prf @@ -66,6 +66,9 @@ unix { } } +# Will automatically add plugins, so run first +contains(QT_CONFIG, permissions): load(permissions) + # qmake variables cannot contain dashes, so normalize the names first CLEAN_QT = $$replace(QT, -private$, _private) CLEAN_QT_PRIVATE = $$replace(QT_PRIVATE, -private$, _private) diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index 1703df32821..f9dc9f7f207 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1164,6 +1164,67 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_permissions kernel/qpermissions.cpp kernel/qpermissions.h kernel/qpermissions_p.h ) +if(QT_FEATURE_permissions AND APPLE) + qt_internal_extend_target(Core + SOURCES + kernel/qpermissions_darwin.mm + platform/darwin/qdarwinpermissionplugin.mm + PLUGIN_TYPES + permissions + ) + + foreach(permission Camera Microphone Bluetooth Contacts Calendar Location) + qt_internal_add_darwin_permission_plugin("${permission}") + endforeach() + + # Camera + qt_internal_extend_target(QDarwinCameraPermissionPlugin + LIBRARIES ${FWAVFoundation} + ) + set_property(TARGET QDarwinCameraPermissionPlugin PROPERTY + _qt_darwin_permissison_separate_request TRUE + ) + + # Microphone + qt_internal_extend_target(QDarwinMicrophonePermissionPlugin + LIBRARIES ${FWAVFoundation} + ) + set_property(TARGET QDarwinMicrophonePermissionPlugin PROPERTY + _qt_darwin_permissison_separate_request TRUE + ) + + # Bluetooth + qt_internal_extend_target(QDarwinBluetoothPermissionPlugin + LIBRARIES ${FWCoreBluetooth} + ) + set_property(TARGET QDarwinBluetoothPermissionPlugin PROPERTY + _qt_info_plist_usage_descriptions "NSBluetoothAlwaysUsageDescription" + ) + + # Contacts + qt_internal_extend_target(QDarwinContactsPermissionPlugin + LIBRARIES ${FWContacts} + ) + + # Calendar + qt_internal_extend_target(QDarwinCalendarPermissionPlugin + LIBRARIES ${FWEventKit} + ) + set_property(TARGET QDarwinCalendarPermissionPlugin PROPERTY + _qt_info_plist_usage_descriptions "NSCalendarsUsageDescription" + ) + + # Location + qt_internal_extend_target(QDarwinLocationPermissionPlugin + LIBRARIES ${FWCoreLocation} + ) + set_property(TARGET QDarwinLocationPermissionPlugin PROPERTY + _qt_info_plist_usage_descriptions + "NSLocationWhenInUseUsageDescription" + "NSLocationAlwaysUsageDescription" + ) +endif() + #### Keys ignored in scope 171:.:mimetypes:mimetypes/mimetypes.pri:QT_FEATURE_mimetype: # MIME_DATABASE = "mimetypes/mime/packages/freedesktop.org.xml" # OTHER_FILES = "$$MIME_DATABASE" diff --git a/src/corelib/Qt6CoreConfigExtras.cmake.in b/src/corelib/Qt6CoreConfigExtras.cmake.in index acbbf328935..16f2ea00683 100644 --- a/src/corelib/Qt6CoreConfigExtras.cmake.in +++ b/src/corelib/Qt6CoreConfigExtras.cmake.in @@ -50,6 +50,15 @@ if(ANDROID_PLATFORM) endif() endif() +if(QT_FEATURE_permissions AND APPLE) + if(NOT QT_NO_CREATE_TARGETS) + set_property(TARGET ${__qt_core_target} APPEND PROPERTY + INTERFACE_QT_EXECUTABLE_FINALIZERS + _qt_internal_darwin_permission_finalizer + ) + endif() +endif() + if(EMSCRIPTEN) set_property(GLOBAL PROPERTY TARGET_SUPPORTS_SHARED_LIBS TRUE) include("${CMAKE_CURRENT_LIST_DIR}/@QT_CMAKE_EXPORT_NAMESPACE@WasmMacros.cmake") diff --git a/src/corelib/Qt6CoreMacros.cmake b/src/corelib/Qt6CoreMacros.cmake index ba800d22dd7..19a75917a4a 100644 --- a/src/corelib/Qt6CoreMacros.cmake +++ b/src/corelib/Qt6CoreMacros.cmake @@ -712,6 +712,30 @@ function(qt6_finalize_target target) endif() endfunction() +function(_qt_internal_darwin_permission_finalizer target) + get_target_property(plist_file "${target}" MACOSX_BUNDLE_INFO_PLIST) + if(NOT plist_file) + return() + endif() + foreach(plugin_target IN LISTS QT_ALL_PLUGINS_FOUND_BY_FIND_PACKAGE_permissions) + set(versioned_plugin_target "${QT_CMAKE_EXPORT_NAMESPACE}::${plugin_target}") + get_target_property(usage_descriptions + ${versioned_plugin_target} + _qt_info_plist_usage_descriptions) + foreach(usage_description_key IN LISTS usage_descriptions) + execute_process(COMMAND "/usr/libexec/PlistBuddy" + -c "print ${usage_description_key}" "${plist_file}" + OUTPUT_VARIABLE usage_description + ERROR_VARIABLE plist_error) + if(usage_description AND NOT plist_error) + set_target_properties("${target}" + PROPERTIES "_qt_has_${plugin_target}_usage_description" TRUE) + qt6_import_plugins(${target} INCLUDE ${versioned_plugin_target}) + endif() + endforeach() + endforeach() +endfunction() + if(NOT QT_NO_CREATE_VERSIONLESS_FUNCTIONS) function(qt_add_executable) qt6_add_executable(${ARGV}) diff --git a/src/corelib/configure.cmake b/src/corelib/configure.cmake index 6e934957ebc..4c352ca99b0 100644 --- a/src/corelib/configure.cmake +++ b/src/corelib/configure.cmake @@ -972,7 +972,7 @@ qt_feature("permissions" PUBLIC SECTION "Utilities" LABEL "Application permissions" PURPOSE "Provides support for requesting user permission to access restricted data or APIs" - DISABLE ON + CONDITION APPLE ) qt_configure_add_summary_section(NAME "Qt Core") qt_configure_add_summary_entry(ARGS "backtrace") diff --git a/src/corelib/kernel/qpermissions.cpp b/src/corelib/kernel/qpermissions.cpp index f56b62284f4..be9717694a9 100644 --- a/src/corelib/kernel/qpermissions.cpp +++ b/src/corelib/kernel/qpermissions.cpp @@ -276,6 +276,10 @@ QMetaType QPermission::type() const \section1 Requirements \include permissions.qdocinc begin-usage-declarations + \row + \li Apple + \li \l{apple-usage-description}{Usage description} + \li \c NSCameraUsageDescription \include permissions.qdocinc end-usage-declarations \include permissions.qdocinc permission-metadata @@ -290,7 +294,10 @@ QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QCameraPermission) \section1 Requirements \include permissions.qdocinc begin-usage-declarations - + \row + \li Apple + \li \l{apple-usage-description}{Usage description} + \li \c NSMicrophoneUsageDescription \include permissions.qdocinc end-usage-declarations \include permissions.qdocinc permission-metadata @@ -305,6 +312,10 @@ QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QMicrophonePermission) \section1 Requirements \include permissions.qdocinc begin-usage-declarations + \row + \li Apple + \li \l{apple-usage-description}{Usage description} + \li \c NSBluetoothAlwaysUsageDescription \include permissions.qdocinc end-usage-declarations \include permissions.qdocinc permission-metadata @@ -324,6 +335,12 @@ QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QBluetoothPermission) \section1 Requirements \include permissions.qdocinc begin-usage-declarations + \row + \li Apple + \li \l{apple-usage-description}{Usage description} + \li \c NSLocationWhenInUseUsageDescription, and + \c NSLocationAlwaysUsageDescription if requesting + QLocationPermission::Always \include permissions.qdocinc end-usage-declarations \include permissions.qdocinc permission-metadata @@ -404,6 +421,10 @@ QLocationPermission::Availability QLocationPermission::availability() const \section1 Requirements \include permissions.qdocinc begin-usage-declarations + \row + \li Apple + \li \l{apple-usage-description}{Usage description} + \li \c NSContactsUsageDescription \include permissions.qdocinc end-usage-declarations \include permissions.qdocinc permission-metadata @@ -443,6 +464,10 @@ bool QContactsPermission::isReadOnly() const \section1 Requirements \include permissions.qdocinc begin-usage-declarations + \row + \li Apple + \li \l{apple-usage-description}{Usage description} + \li \c NSCalendarsUsageDescription \include permissions.qdocinc end-usage-declarations \include permissions.qdocinc permission-metadata @@ -472,6 +497,11 @@ bool QCalendarPermission::isReadOnly() const return d->isReadOnly; } +/*! + * \internal +*/ + +QPermissionPlugin::~QPermissionPlugin() = default; #ifndef QT_NO_DEBUG_STREAM QDebug operator<<(QDebug debug, const QPermission &permission) diff --git a/src/corelib/kernel/qpermissions_darwin.mm b/src/corelib/kernel/qpermissions_darwin.mm new file mode 100644 index 00000000000..ae2cb2c423c --- /dev/null +++ b/src/corelib/kernel/qpermissions_darwin.mm @@ -0,0 +1,88 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qpermissions.h" +#include "qpermissions_p.h" + +#include +#include +#include + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; + +namespace { + +Q_GLOBAL_STATIC_WITH_ARGS(QFactoryLoader, pluginLoader, + (QPermissionPluginInterface_iid, QLatin1String("/permissions"), Qt::CaseInsensitive)) + +QPermissionPlugin *permissionPlugin(const QPermission &permission) +{ + static QMutex mutex; + QMutexLocker locker(&mutex); + + const char *permissionType = permission.type().name(); + qCDebug(lcPermissions, "Looking for permission plugin for %s", permissionType); + + if (Q_UNLIKELY(!pluginLoader)) { + qCWarning(lcPermissions, "Cannot check or request permissions during application shutdown"); + return nullptr; + } + + auto metaDataList = pluginLoader()->metaData(); + for (int i = 0; i < metaDataList.size(); ++i) { + auto metaData = metaDataList.at(i).value(QtPluginMetaDataKeys::MetaData).toMap(); + auto permissions = metaData.value("Permissions"_L1).toArray(); + if (permissions.contains(QString::fromUtf8(permissionType))) { + auto className = metaDataList.at(i).value(QtPluginMetaDataKeys::ClassName).toString(); + qCDebug(lcPermissions) << "Found matching plugin" << qUtf8Printable(className); + auto *plugin = static_cast(pluginLoader()->instance(i)); + if (!plugin->parent()) { + // We want to re-parent the plugin to the factory loader, so that it's + // cleaned up properly. To do so we first need to move the plugin to the + // same thread as the factory loader, as the plugin might be instantiated + // on a secondary thread if triggered from a checkPermission call (which + // is allowed on any thread). + plugin->moveToThread(pluginLoader->thread()); + + // Also, as setParent will involve sending a ChildAdded event to the parent, + // we need to make the call on the same thread as the parent lives, as events + // are not allowed to be sent to an object owned by another thread. + QMetaObject::invokeMethod(plugin, [=] { + plugin->setParent(pluginLoader); + }); + } + return plugin; + } + } + + qCWarning(lcPermissions).nospace() << "Could not find permission plugin for " + << permission.type().name() << ". Please make sure you have included the " + << "required usage description in your Info.plist"; + + return nullptr; +} + +} // Unnamed namespace + +namespace QPermissions::Private +{ + Qt::PermissionStatus checkPermission(const QPermission &permission) + { + if (auto *plugin = permissionPlugin(permission)) + return plugin->checkPermission(permission); + else + return Qt::PermissionStatus::Denied; + } + + void requestPermission(const QPermission &permission, const QPermissions::Private::PermissionCallback &callback) + { + if (auto *plugin = permissionPlugin(permission)) + plugin->requestPermission(permission, callback); + else + callback(Qt::PermissionStatus::Denied); + } +} + +QT_END_NAMESPACE diff --git a/src/corelib/kernel/qpermissions_p.h b/src/corelib/kernel/qpermissions_p.h index fc1d948dce2..36f497f1988 100644 --- a/src/corelib/kernel/qpermissions_p.h +++ b/src/corelib/kernel/qpermissions_p.h @@ -9,6 +9,8 @@ #include #include +#include + #include QT_REQUIRE_CONFIG(permissions); @@ -26,7 +28,7 @@ QT_REQUIRE_CONFIG(permissions); QT_BEGIN_NAMESPACE -Q_DECLARE_LOGGING_CATEGORY(lcPermissions) +Q_DECLARE_EXPORTED_LOGGING_CATEGORY(lcPermissions, Q_CORE_EXPORT) namespace QPermissions::Private { @@ -36,6 +38,18 @@ namespace QPermissions::Private void requestPermission(const QPermission &permission, const PermissionCallback &callback); } +#define QPermissionPluginInterface_iid "org.qt-project.QPermissionPluginInterface.6.5" + +class Q_CORE_EXPORT QPermissionPlugin : public QObject +{ +public: + virtual ~QPermissionPlugin(); + + virtual Qt::PermissionStatus checkPermission(const QPermission &permission) = 0; + virtual void requestPermission(const QPermission &permission, + const QPermissions::Private::PermissionCallback &callback) = 0; +}; + QT_END_NAMESPACE #endif // QPERMISSIONS_P_H diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin.mm new file mode 100644 index 00000000000..5c527f396c8 --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin.mm @@ -0,0 +1,90 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p.h" + +QT_BEGIN_NAMESPACE + +QDarwinPermissionPlugin::QDarwinPermissionPlugin(QDarwinPermissionHandler *handler) + : QPermissionPlugin() + , m_handler(handler) +{ +} + +QDarwinPermissionPlugin::~QDarwinPermissionPlugin() +{ + [m_handler release]; +} + +Qt::PermissionStatus QDarwinPermissionPlugin::checkPermission(const QPermission &permission) +{ + return [m_handler checkPermission:permission]; +} + +void QDarwinPermissionPlugin::requestPermission(const QPermission &permission, const PermissionCallback &callback) +{ + if (!verifyUsageDescriptions(permission)) { + callback(Qt::PermissionStatus::Denied); + return; + } + + [m_handler requestPermission:permission withCallback:[=](Qt::PermissionStatus status) { + // In case the callback comes in on a secondary thread we need to marshal it + // back to the main thread. And if it doesn't, we still want to propagate it + // via an event, to avoid any GCD locks deadlocking the application on iOS + // if the user responds to the result by running a nested event loop. + // Luckily Qt::QueuedConnection gives us exactly what we need. + QMetaObject::invokeMethod(this, "permissionUpdated", Qt::QueuedConnection, + Q_ARG(Qt::PermissionStatus, status), Q_ARG(PermissionCallback, callback)); + }]; +} + +void QDarwinPermissionPlugin::permissionUpdated(Qt::PermissionStatus status, const PermissionCallback &callback) +{ + callback(status); +} + +bool QDarwinPermissionPlugin::verifyUsageDescriptions(const QPermission &permission) +{ + // FIXME: Look up the responsible process and inspect that, + // as that's what needs to have the usage descriptions. + // FIXME: Verify entitlements if the process is sandboxed. + auto *infoDictionary = NSBundle.mainBundle.infoDictionary; + for (auto description : [m_handler usageDescriptionsFor:permission]) { + if (!infoDictionary[description.toNSString()]) { + qCWarning(lcPermissions) << + "Requesting" << permission.type().name() << + "requires" << description << "in Info.plist"; + return false; + } + } + return true; +} + +QT_END_NAMESPACE + +QT_USE_NAMESPACE + +@implementation QDarwinPermissionHandler + +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + Q_UNREACHABLE(); // All handlers should at least provide a check +} + +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + Q_UNUSED(permission); + qCWarning(lcPermissions).nospace() << "Could not request " << permission.type().name() << ". " + << "Please make sure you have included the required usage description in your Info.plist"; + callback(Qt::PermissionStatus::Denied); +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + return {}; +} + +@end + +#include "moc_qdarwinpermissionplugin_p.cpp" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm new file mode 100644 index 00000000000..01fb638283e --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm @@ -0,0 +1,84 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p_p.h" + +#include + +#include + +@interface QDarwinBluetoothPermissionHandler () +@property (nonatomic, retain) CBCentralManager *manager; +@end + +@implementation QDarwinBluetoothPermissionHandler { + std::deque m_callbacks; +} + +- (instancetype)init +{ + if ((self = [super init])) + self.manager = nil; + + return self; +} + +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + Q_UNUSED(permission); + return [self currentStatus]; +} + +- (Qt::PermissionStatus)currentStatus +{ + switch (CBCentralManager.authorization) { + case CBManagerAuthorizationNotDetermined: + return Qt::PermissionStatus::Undetermined; + case CBManagerAuthorizationRestricted: + case CBManagerAuthorizationDenied: + return Qt::PermissionStatus::Denied; + case CBManagerAuthorizationAllowedAlways: + return Qt::PermissionStatus::Granted; + } + + Q_UNREACHABLE(); +} + +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + m_callbacks.push_back(callback); + if (!self.manager) { + self.manager = [[[CBCentralManager alloc] + initWithDelegate:self queue:dispatch_get_main_queue()] autorelease]; + } +} + +- (void)centralManagerDidUpdateState:(CBCentralManager *)manager +{ + Q_ASSERT(manager == self.manager); + Q_ASSERT(!m_callbacks.empty()); + + auto status = [self currentStatus]; + + for (auto callback : m_callbacks) + callback(status); + + m_callbacks = {}; + self.manager = nil; +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + Q_UNUSED(permission); +#ifdef Q_OS_MACOS + if (QOperatingSystemVersion::current() > QOperatingSystemVersion::MacOSBigSur) +#endif + { + return { "NSBluetoothAlwaysUsageDescription" }; + } + + return {}; +} +@end + +#include "moc_qdarwinpermissionplugin_p_p.cpp" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm new file mode 100644 index 00000000000..79a85ef3d2d --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm @@ -0,0 +1,57 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p_p.h" + +#include + +QT_DEFINE_PERMISSION_STATUS_CONVERTER(EKAuthorizationStatus); + +@interface QDarwinCalendarPermissionHandler () +@property (nonatomic, retain) EKEventStore *eventStore; +@end + +@implementation QDarwinCalendarPermissionHandler +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + Q_UNUSED(permission); + return [self currentStatus]; +} + +- (Qt::PermissionStatus)currentStatus +{ + const auto status = [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent]; + return nativeStatusToQtStatus(status); +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + Q_UNUSED(permission); + return { "NSCalendarsUsageDescription" }; +} + +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + if (!self.eventStore) { + // Note: Creating the EKEventStore results in warnings in the + // console about "An error occurred in the persistent store". + // This seems like a EventKit API bug. + self.eventStore = [[EKEventStore new] autorelease]; + } + + [self.eventStore requestAccessToEntityType:EKEntityTypeEvent + completion:^(BOOL granted, NSError * _Nullable error) { + Q_UNUSED(granted); // We use status instead + // Permission denied will result in an error, which we don't + // want to report/log, so we ignore the error and just report + // the status. + Q_UNUSED(error); + + callback([self currentStatus]); + } + ]; +} + +@end + +#include "moc_qdarwinpermissionplugin_p_p.cpp" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm new file mode 100644 index 00000000000..51c517d6f30 --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm @@ -0,0 +1,42 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p_p.h" + +#include + +QT_DEFINE_PERMISSION_STATUS_CONVERTER(AVAuthorizationStatus); + +#ifndef BUILDING_PERMISSION_REQUEST + +@implementation QDarwinCameraPermissionHandler +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + const auto status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + return nativeStatusToQtStatus(status); +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + Q_UNUSED(permission); + return { "NSCameraUsageDescription" }; +} +@end + +#include "moc_qdarwinpermissionplugin_p_p.cpp" + +#else // Building request + +@implementation QDarwinCameraPermissionHandler (Request) +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeVideo completionHandler:^(BOOL granted) + { + Q_UNUSED(granted); // We use status instead + const auto status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeVideo]; + callback(nativeStatusToQtStatus(status)); + }]; +} +@end + +#endif // BUILDING_PERMISSION_REQUEST diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm new file mode 100644 index 00000000000..3221b6dc1db --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm @@ -0,0 +1,58 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p_p.h" + +#include + +QT_DEFINE_PERMISSION_STATUS_CONVERTER(CNAuthorizationStatus); + +@interface QDarwinContactsPermissionHandler () +@property (nonatomic, retain) CNContactStore *contactStore; +@end + +@implementation QDarwinContactsPermissionHandler +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + Q_UNUSED(permission); + return [self currentStatus]; +} + +- (Qt::PermissionStatus)currentStatus +{ + const auto status = [CNContactStore authorizationStatusForEntityType:CNEntityTypeContacts]; + return nativeStatusToQtStatus(status); +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + Q_UNUSED(permission); + return { "NSContactsUsageDescription" }; +} + +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + if (!self.contactStore) { + // Note: Creating the CNContactStore results in warnings in the + // console about "Attempted to register account monitor for types + // client is not authorized to access", mentioning CardDAV, LDAP, + // and Exchange. This seems like a Contacts API bug. + self.contactStore = [[CNContactStore new] autorelease]; + } + + [self.contactStore requestAccessForEntityType:CNEntityTypeContacts + completionHandler:^(BOOL granted, NSError * _Nullable error) { + Q_UNUSED(granted); // We use status instead + // Permission denied will result in an error, which we don't + // want to report/log, so we ignore the error and just report + // the status. + Q_UNUSED(error); + + callback([self currentStatus]); + } + ]; +} + +@end + +#include "moc_qdarwinpermissionplugin_p_p.cpp" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm new file mode 100644 index 00000000000..5414e97cbfe --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm @@ -0,0 +1,230 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p_p.h" + +#include + +#include + +@interface QDarwinLocationPermissionHandler () +@property (nonatomic, retain) CLLocationManager *manager; +@end + +Q_LOGGING_CATEGORY(lcLocationPermission, "qt.permissions.location"); + +void warmUpLocationServices() +{ + // After creating a CLLocationManager the authorizationStatus + // will initially be kCLAuthorizationStatusNotDetermined. The + // status will then update to an actual status if the app was + // previously authorized/denied once the location services + // do some initial book-keeping in the background. By kicking + // off a CLLocationManager early on here, we ensure that by + // the time the user calls checkPermission the authorization + // status has been resolved. + qCDebug(lcLocationPermission) << "Warming up location services"; + [[CLLocationManager new] release]; +} + +Q_CONSTRUCTOR_FUNCTION(warmUpLocationServices); + +struct PermissionRequest +{ + QPermission permission; + PermissionCallback callback; +}; + +@implementation QDarwinLocationPermissionHandler { + std::deque m_requests; +} + +- (instancetype)init +{ + if ((self = [super init])) { + // The delegate callbacks will come in on the thread that + // the CLLocationManager is created on, and we want those + // to come in on the main thread, so we defer creation + // of the manger until requestPermission, where we know + // we are on the main thread. + self.manager = nil; + } + + return self; +} + +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + const auto locationPermission = permission.data(); + + auto status = [self authorizationStatus:locationPermission]; + if (status != Qt::PermissionStatus::Granted) + return status; + + return [self accuracyAuthorization:locationPermission]; +} + +- (Qt::PermissionStatus)authorizationStatus:(QLocationPermission)permission +{ + switch ([self authorizationStatus]) { + case kCLAuthorizationStatusRestricted: + case kCLAuthorizationStatusDenied: + return Qt::PermissionStatus::Denied; + case kCLAuthorizationStatusNotDetermined: + return Qt::PermissionStatus::Undetermined; + case kCLAuthorizationStatusAuthorizedAlways: + return Qt::PermissionStatus::Granted; +#ifdef Q_OS_IOS + case kCLAuthorizationStatusAuthorizedWhenInUse: + if (permission.availability() == QLocationPermission::WhenInUse) + return Qt::PermissionStatus::Granted; + else + return Qt::PermissionStatus::Denied; // FIXME: Verify +#endif + } + + Q_UNREACHABLE(); +} + +- (CLAuthorizationStatus)authorizationStatus +{ + if (self.manager) { + if (@available(macOS 11, iOS 14, *)) + return self.manager.authorizationStatus; + } + + return CLLocationManager.authorizationStatus; +} + +- (Qt::PermissionStatus)accuracyAuthorization:(QLocationPermission)permission +{ + auto status = CLAccuracyAuthorizationReducedAccuracy; + if (@available(macOS 11, iOS 14, *)) + status = self.manager.accuracyAuthorization; + + switch (status) { + case CLAccuracyAuthorizationFullAccuracy: + return Qt::PermissionStatus::Granted; + case CLAccuracyAuthorizationReducedAccuracy: + if (permission.accuracy() == QLocationPermission::Approximate) + return Qt::PermissionStatus::Granted; + else + return Qt::PermissionStatus::Denied; // FIXME: Verify + } + + Q_UNREACHABLE(); +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + QStringList usageDescriptions = { "NSLocationWhenInUseUsageDescription" }; + const auto locationPermission = permission.data(); + if (locationPermission.availability() == QLocationPermission::Always) + usageDescriptions << "NSLocationAlwaysUsageDescription"; + return usageDescriptions; +} + +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + const bool requestAlreadyInFlight = !m_requests.empty(); + + m_requests.push_back({ permission, callback }); + + if (requestAlreadyInFlight) { + qCDebug(lcLocationPermission).nospace() << "Already processing " + << m_requests.front().permission << ". Deferring request"; + } else { + [self requestQueuedPermission]; + } +} + +- (void)requestQueuedPermission +{ + Q_ASSERT(!m_requests.empty()); + const auto permission = m_requests.front().permission; + + qCDebug(lcLocationPermission) << "Requesting" << permission; + + if (!self.manager) { + self.manager = [[CLLocationManager new] autorelease]; + self.manager.delegate = self; + } + + const auto locationPermission = permission.data(); + switch (locationPermission.availability()) { + case QLocationPermission::WhenInUse: + // The documentation specifies that requestWhenInUseAuthorization can + // only be called when the current authorization status is undetermined. + switch ([self authorizationStatus]) { + case kCLAuthorizationStatusNotDetermined: + [self.manager requestWhenInUseAuthorization]; + break; + default: + [self deliverResult]; + } + break; + case QLocationPermission::Always: + // The documentation specifies that requestAlwaysAuthorization can only + // be called when the current authorization status is either undetermined, + // or authorized when in use. + switch ([self authorizationStatus]) { + case kCLAuthorizationStatusNotDetermined: +#ifdef Q_OS_IOS + case kCLAuthorizationStatusAuthorizedWhenInUse: +#endif + [self.manager requestAlwaysAuthorization]; + break; + default: + [self deliverResult]; + } + break; + } +} + +- (void)locationManager:(CLLocationManager *)manager didChangeAuthorizationStatus:(CLAuthorizationStatus)status +{ + qCDebug(lcLocationPermission) << "Processing authorization" + << "update with status" << status; + + if (m_requests.empty()) { + qCDebug(lcLocationPermission) << "No requests in flight. Ignoring."; + return; + } + + if (status == kCLAuthorizationStatusNotDetermined) { + // Initializing a CLLocationManager will result in an initial + // callback to the delegate even before we've requested any + // location permissions. Normally we would ignore this callback + // due to the request queue check above, but if this callback + // comes in after the application has requested a permission + // we don't want to report the undetermined status, but rather + // wait for the actual result to come in. + qCDebug(lcLocationPermission) << "Ignoring delegate callback" + << "with status kCLAuthorizationStatusNotDetermined"; + return; + } + + [self deliverResult]; +} + +- (void)deliverResult +{ + auto request = m_requests.front(); + m_requests.pop_front(); + + auto status = [self checkPermission:request.permission]; + qCDebug(lcLocationPermission) << "Result for" + << request.permission << "was" << status; + + request.callback(status); + + if (!m_requests.empty()) { + qCDebug(lcLocationPermission) << "Still have" + << m_requests.size() << "deferred request(s)"; + [self requestQueuedPermission]; + } +} + +@end + +#include "moc_qdarwinpermissionplugin_p_p.cpp" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm new file mode 100644 index 00000000000..5dc434309df --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm @@ -0,0 +1,42 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#include "qdarwinpermissionplugin_p_p.h" + +#include + +QT_DEFINE_PERMISSION_STATUS_CONVERTER(AVAuthorizationStatus); + +#ifndef BUILDING_PERMISSION_REQUEST + +@implementation QDarwinMicrophonePermissionHandler +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + const auto status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + return nativeStatusToQtStatus(status); +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ + Q_UNUSED(permission); + return { "NSMicrophoneUsageDescription" }; +} +@end + +#include "moc_qdarwinpermissionplugin_p_p.cpp" + +#else // Building request + +@implementation QDarwinMicrophonePermissionHandler (Request) +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback +{ + [AVCaptureDevice requestAccessForMediaType:AVMediaTypeAudio completionHandler:^(BOOL granted) + { + Q_UNUSED(granted); // We use status instead + const auto status = [AVCaptureDevice authorizationStatusForMediaType:AVMediaTypeAudio]; + callback(nativeStatusToQtStatus(status)); + }]; +} +@end + +#endif // BUILDING_PERMISSION_REQUEST diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_p.h b/src/corelib/platform/darwin/qdarwinpermissionplugin_p.h new file mode 100644 index 00000000000..03530133adc --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_p.h @@ -0,0 +1,58 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDARWINPERMISSIONPLUGIN_P_H +#define QDARWINPERMISSIONPLUGIN_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. This header file may change +// from version to version without notice, or even be removed. +// +// We mean it. +// + +#include +#include +#include + +#if defined(__OBJC__) +#include +#endif + +QT_USE_NAMESPACE + +using namespace QPermissions::Private; + +#if defined(__OBJC__) +Q_CORE_EXPORT +#endif +QT_DECLARE_NAMESPACED_OBJC_INTERFACE(QDarwinPermissionHandler, NSObject +- (Qt::PermissionStatus)checkPermission:(QPermission)permission; +- (void)requestPermission:(QPermission)permission withCallback:(PermissionCallback)callback; +- (QStringList)usageDescriptionsFor:(QPermission)permission; +) + +QT_BEGIN_NAMESPACE + +class Q_CORE_EXPORT QDarwinPermissionPlugin : public QPermissionPlugin +{ + Q_OBJECT +public: + QDarwinPermissionPlugin(QDarwinPermissionHandler *handler); + ~QDarwinPermissionPlugin(); + + Qt::PermissionStatus checkPermission(const QPermission &permission) override; + void requestPermission(const QPermission &permission, const PermissionCallback &callback) override; + +private: + Q_SLOT void permissionUpdated(Qt::PermissionStatus status, const PermissionCallback &callback); + bool verifyUsageDescriptions(const QPermission &permission); + QDarwinPermissionHandler *m_handler = nullptr; +}; + +QT_END_NAMESPACE + +#endif // QDARWINPERMISSIONPLUGIN_P_H diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h b/src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h new file mode 100644 index 00000000000..9e4bbe92de2 --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h @@ -0,0 +1,102 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only + +#ifndef QDARWINPERMISSIONPLUGIN_P_P_H +#define QDARWINPERMISSIONPLUGIN_P_P_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. This header file may change +// from version to version without notice, or even be removed. +// +// We mean it. +// + +#if !defined(QT_DARWIN_PERMISSION_PLUGIN) +#error "This header should only be included from permission plugins" +#endif + +#include +#include +#include + +#include "qdarwinpermissionplugin_p.h" + +using namespace QPermissions::Private; + +#ifndef QT_JOIN +#define QT_JOIN_IMPL(A, B) A ## B +#define QT_JOIN(A, B) QT_JOIN_IMPL(A, B) +#endif + +#define PERMISSION_PLUGIN_NAME(SUFFIX) \ + QT_JOIN(QT_JOIN(QT_JOIN( \ + QDarwin, QT_DARWIN_PERMISSION_PLUGIN), Permission), SUFFIX) + +#define PERMISSION_PLUGIN_CLASSNAME PERMISSION_PLUGIN_NAME(Plugin) +#define PERMISSION_PLUGIN_HANDLER PERMISSION_PLUGIN_NAME(Handler) + +QT_DECLARE_NAMESPACED_OBJC_INTERFACE( + PERMISSION_PLUGIN_HANDLER, + QDarwinPermissionHandler +) + +QT_BEGIN_NAMESPACE + +class Q_CORE_EXPORT PERMISSION_PLUGIN_CLASSNAME : public QDarwinPermissionPlugin +{ + Q_OBJECT + Q_PLUGIN_METADATA( + IID QPermissionPluginInterface_iid + FILE "QDarwin" QT_STRINGIFY(QT_DARWIN_PERMISSION_PLUGIN) "PermissionPlugin.json") +public: + PERMISSION_PLUGIN_CLASSNAME() + : QDarwinPermissionPlugin([[PERMISSION_PLUGIN_HANDLER alloc] init]) + {} +}; + +QT_END_NAMESPACE + +// Request +#if defined(BUILDING_PERMISSION_REQUEST) +extern "C" void PERMISSION_PLUGIN_NAME(Request)() {} +#endif + +// ------------------------------------------------------- + +namespace { +template +struct NativeStatusHelper; + +template +Qt::PermissionStatus nativeStatusToQtStatus(NativeStatus status) +{ + using Converter = NativeStatusHelper; + switch (status) { + case Converter::Authorized: + return Qt::PermissionStatus::Granted; + case Converter::Denied: + case Converter::Restricted: + return Qt::PermissionStatus::Denied; + case Converter::Undetermined: + return Qt::PermissionStatus::Undetermined; + } + Q_UNREACHABLE(); +} +} // namespace + +#define QT_DEFINE_PERMISSION_STATUS_CONVERTER(NativeStatus) \ +namespace { template<> \ +struct NativeStatusHelper \ +{\ + enum { \ + Authorized = NativeStatus##Authorized, \ + Denied = NativeStatus##Denied, \ + Restricted = NativeStatus##Restricted, \ + Undetermined = NativeStatus##NotDetermined \ + }; \ +}; } + +#endif // QDARWINPERMISSIONPLUGIN_P_P_H diff --git a/tests/manual/permissions/CMakeLists.txt b/tests/manual/permissions/CMakeLists.txt index 847d9a74115..50ec89665ff 100644 --- a/tests/manual/permissions/CMakeLists.txt +++ b/tests/manual/permissions/CMakeLists.txt @@ -5,3 +5,52 @@ qt_internal_add_test(tst_qpermissions LIBRARIES Qt::CorePrivate ) + +if (APPLE) + # Test an app bundle, but without any usage descriptions + + qt_internal_add_test(tst_qpermissions_app + SOURCES + tst_qpermissions.cpp + DEFINES + tst_QPermissions=tst_QPermissionsApp + LIBRARIES + Qt::CorePrivate + ) + + set_property(TARGET tst_qpermissions_app + PROPERTY MACOSX_BUNDLE TRUE) + set_property(TARGET tst_qpermissions_app + PROPERTY MACOSX_BUNDLE_GUI_IDENTIFIER "io.qt.dev.tst_permissions_app") + + # Test an app bundle with all the required usage descriptions + + qt_internal_add_test(tst_qpermissions_app_with_usage_descriptions + SOURCES + tst_qpermissions.cpp + DEFINES + tst_QPermissions=tst_QPermissionsAppWithUsageDescriptions + HAVE_USAGE_DESCRIPTION=1 + LIBRARIES + Qt::CorePrivate + Qt::Gui + ) + + set_property(TARGET tst_qpermissions_app_with_usage_descriptions + PROPERTY MACOSX_BUNDLE TRUE) + set_property(TARGET tst_qpermissions_app_with_usage_descriptions + PROPERTY MACOSX_BUNDLE_GUI_IDENTIFIER "io.qt.dev.tst_qpermissions_app_with_usage_descriptions") + set_property(TARGET tst_qpermissions_app_with_usage_descriptions + PROPERTY MACOSX_BUNDLE_INFO_PLIST "${CMAKE_CURRENT_SOURCE_DIR}/Info.plist") + + foreach(permission_plugin IN LISTS QT_ALL_PLUGINS_FOUND_BY_FIND_PACKAGE_permissions) + set(permission_plugin "${QT_CMAKE_EXPORT_NAMESPACE}::${permission_plugin}") + qt6_import_plugins(tst_qpermissions_app INCLUDE ${permission_plugin}) + qt6_import_plugins(tst_qpermissions_app_with_usage_descriptions INCLUDE ${permission_plugin}) + endforeach() + + if(NOT CMAKE_GENERATOR STREQUAL "Xcode") + add_custom_command(TARGET tst_qpermissions_app_with_usage_descriptions + POST_BUILD COMMAND codesign -s - tst_qpermissions_app_with_usage_descriptions.app) + endif() +endif() diff --git a/tests/manual/permissions/Info.plist b/tests/manual/permissions/Info.plist new file mode 100644 index 00000000000..dce43caf125 --- /dev/null +++ b/tests/manual/permissions/Info.plist @@ -0,0 +1,59 @@ + + + + + CFBundleInfoDictionaryVersion + 6.0 + CFBundlePackageType + APPL + + CFBundleName + ${MACOSX_BUNDLE_BUNDLE_NAME} + CFBundleIdentifier + ${MACOSX_BUNDLE_GUI_IDENTIFIER} + CFBundleExecutable + ${MACOSX_BUNDLE_EXECUTABLE_NAME} + + CFBundleVersion + ${MACOSX_BUNDLE_BUNDLE_VERSION} + CFBundleShortVersionString + ${MACOSX_BUNDLE_SHORT_VERSION_STRING} + + LSMinimumSystemVersion + ${CMAKE_OSX_DEPLOYMENT_TARGET} + + NSHumanReadableCopyright + ${MACOSX_BUNDLE_COPYRIGHT} + + CFBundleIconFile + ${MACOSX_BUNDLE_ICON_FILE} + + CFBundleDevelopmentRegion + English + + NSSupportsAutomaticGraphicsSwitching + + + NSBluetoothAlwaysUsageDescription + Testing BluetoothAlways + NSCalendarsUsageDescription + Testing Calendars + NSCameraUsageDescription + Testing Camera + NSContactsUsageDescription + Testing Contacts + NSHealthShareUsageDescription + Testing HealthShare + NSHealthUpdateUsageDescription + Testing HealthUpdate + NSLocationAlwaysAndWhenInUseUsageDescription + Testing LocationAlwaysAndWhenInUse + NSLocationAlwaysUsageDescription + Testing LocationAlways + NSLocationWhenInUseUsageDescription + Testing LocationWhenInUse + NSMicrophoneUsageDescription + Testing Microphone + + + diff --git a/tests/manual/permissions/tst_qpermissions.cpp b/tests/manual/permissions/tst_qpermissions.cpp index a97afe8487e..db8d968b5a8 100644 --- a/tests/manual/permissions/tst_qpermissions.cpp +++ b/tests/manual/permissions/tst_qpermissions.cpp @@ -9,6 +9,11 @@ #include #include +#if defined(Q_OS_MACOS) && defined(QT_BUILD_INTERNAL) +#include +Q_CONSTRUCTOR_FUNCTION(qt_mac_ensureResponsible); +#endif + class tst_QPermissions : public QObject { Q_OBJECT