diff --git a/cmake/QtFrameworkHelpers.cmake b/cmake/QtFrameworkHelpers.cmake index 3b4cb01223..e4e2a1373f 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 8ea763d86e..55402f50ca 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 aca5421221..20bde6f310 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 bca93b679f..5c9af5f0d9 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 0000000000..dce43caf12 --- /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 0000000000..d80df6d01e --- /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 71b6679af3..d8a8627d83 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 1703df3282..f9dc9f7f20 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 acbbf32893..16f2ea0068 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 ba800d22dd..19a75917a4 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 6e934957eb..4c352ca99b 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 f56b62284f..be9717694a 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 0000000000..ae2cb2c423 --- /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 fc1d948dce..36f497f198 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 0000000000..5c527f396c --- /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 0000000000..01fb638283 --- /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 0000000000..79a85ef3d2 --- /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 0000000000..51c517d6f3 --- /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 0000000000..3221b6dc1d --- /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 0000000000..5414e97cbf --- /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 0000000000..5dc434309d --- /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 0000000000..03530133ad --- /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 0000000000..9e4bbe92de --- /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 847d9a7411..50ec89665f 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 0000000000..dce43caf12 --- /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 a97afe8487..db8d968b5a 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