summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
-rw-r--r--cmake/QtFrameworkHelpers.cmake4
-rw-r--r--cmake/QtModuleConfig.cmake.in4
-rw-r--r--cmake/QtPluginHelpers.cmake78
-rw-r--r--examples/corelib/permissions/CMakeLists.txt10
-rw-r--r--examples/corelib/permissions/Info.plist59
-rw-r--r--mkspecs/features/permissions.prf25
-rw-r--r--mkspecs/features/qt.prf3
-rw-r--r--src/corelib/CMakeLists.txt61
-rw-r--r--src/corelib/Qt6CoreConfigExtras.cmake.in9
-rw-r--r--src/corelib/Qt6CoreMacros.cmake24
-rw-r--r--src/corelib/configure.cmake2
-rw-r--r--src/corelib/kernel/qpermissions.cpp32
-rw-r--r--src/corelib/kernel/qpermissions_darwin.mm88
-rw-r--r--src/corelib/kernel/qpermissions_p.h16
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin.mm90
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm84
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm57
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm42
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm58
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm230
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm42
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_p.h58
-rw-r--r--src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h102
-rw-r--r--tests/manual/permissions/CMakeLists.txt49
-rw-r--r--tests/manual/permissions/Info.plist59
-rw-r--r--tests/manual/permissions/tst_qpermissions.cpp5
26 files changed, 1286 insertions, 5 deletions
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 "$<JOIN:$<TARGET_PROPERTY:${plugin_target},${usage_descriptions_property}>, >")
+ 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
+ "$<BOOL:$<TARGET_PROPERTY:${plugin_target},_qt_darwin_permissison_separate_request>>")
+ 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 "$<BOOL:$<TARGET_PROPERTY:${has_usage_description_property}>>")
+ target_link_options(${plugin_target} INTERFACE
+ "$<$<AND:${separate_request_genex},${has_usage_description_genex}>:${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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+
+ <key>CFBundleName</key>
+ <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
+ <key>CFBundleExecutable</key>
+ <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
+
+ <key>CFBundleVersion</key>
+ <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
+ <key>CFBundleShortVersionString</key>
+ <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
+
+ <key>LSMinimumSystemVersion</key>
+ <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
+
+ <key>NSHumanReadableCopyright</key>
+ <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
+
+ <key>CFBundleIconFile</key>
+ <string>${MACOSX_BUNDLE_ICON_FILE}</string>
+
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+
+ <key>NSSupportsAutomaticGraphicsSwitching</key>
+ <true/>
+
+ <key>NSBluetoothAlwaysUsageDescription</key>
+ <string>Testing BluetoothAlways</string>
+ <key>NSCalendarsUsageDescription</key>
+ <string>Testing Calendars</string>
+ <key>NSCameraUsageDescription</key>
+ <string>Testing Camera</string>
+ <key>NSContactsUsageDescription</key>
+ <string>Testing Contacts</string>
+ <key>NSHealthShareUsageDescription</key>
+ <string>Testing HealthShare</string>
+ <key>NSHealthUpdateUsageDescription</key>
+ <string>Testing HealthUpdate</string>
+ <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
+ <string>Testing LocationAlwaysAndWhenInUse</string>
+ <key>NSLocationAlwaysUsageDescription</key>
+ <string>Testing LocationAlways</string>
+ <key>NSLocationWhenInUseUsageDescription</key>
+ <string>Testing LocationWhenInUse</string>
+ <key>NSMicrophoneUsageDescription</key>
+ <string>Testing Microphone</string>
+
+</dict>
+</plist>
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 <QtCore/private/qfactoryloader_p.h>
+#include <QtCore/private/qcoreapplication_p.h>
+#include <QtCore/qcborarray.h>
+
+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<QPermissionPlugin*>(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 <private/qglobal_p.h>
#include <QtCore/qloggingcategory.h>
+#include <QtCore/QObject>
+
#include <functional>
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 <deque>
+
+#include <CoreBluetooth/CoreBluetooth.h>
+
+@interface QDarwinBluetoothPermissionHandler () <CBCentralManagerDelegate>
+@property (nonatomic, retain) CBCentralManager *manager;
+@end
+
+@implementation QDarwinBluetoothPermissionHandler {
+ std::deque<PermissionCallback> 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 <EventKit/EventKit.h>
+
+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 <AVFoundation/AVFoundation.h>
+
+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 <Contacts/Contacts.h>
+
+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 <deque>
+
+#include <CoreLocation/CoreLocation.h>
+
+@interface QDarwinLocationPermissionHandler () <CLLocationManagerDelegate>
+@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<PermissionRequest> 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<QLocationPermission>();
+
+ 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<QLocationPermission>();
+ 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<QLocationPermission>();
+ 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 <AVFoundation/AVFoundation.h>
+
+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 <QtCore/qnamespace.h>
+#include <QtCore/private/qpermissions_p.h>
+#include <QtCore/private/qcore_mac_p.h>
+
+#if defined(__OBJC__)
+#include <Foundation/NSObject.h>
+#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 <QtCore/qnamespace.h>
+#include <QtCore/private/qpermissions_p.h>
+#include <QtCore/private/qcore_mac_p.h>
+
+#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 <typename NativeStatus>
+struct NativeStatusHelper;
+
+template <typename NativeStatus>
+Qt::PermissionStatus nativeStatusToQtStatus(NativeStatus status)
+{
+ using Converter = NativeStatusHelper<NativeStatus>;
+ 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<NativeStatus> \
+{\
+ 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 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!DOCTYPE plist PUBLIC "-//Apple Computer//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd">
+<plist version="1.0">
+<dict>
+ <key>CFBundleInfoDictionaryVersion</key>
+ <string>6.0</string>
+ <key>CFBundlePackageType</key>
+ <string>APPL</string>
+
+ <key>CFBundleName</key>
+ <string>${MACOSX_BUNDLE_BUNDLE_NAME}</string>
+ <key>CFBundleIdentifier</key>
+ <string>${MACOSX_BUNDLE_GUI_IDENTIFIER}</string>
+ <key>CFBundleExecutable</key>
+ <string>${MACOSX_BUNDLE_EXECUTABLE_NAME}</string>
+
+ <key>CFBundleVersion</key>
+ <string>${MACOSX_BUNDLE_BUNDLE_VERSION}</string>
+ <key>CFBundleShortVersionString</key>
+ <string>${MACOSX_BUNDLE_SHORT_VERSION_STRING}</string>
+
+ <key>LSMinimumSystemVersion</key>
+ <string>${CMAKE_OSX_DEPLOYMENT_TARGET}</string>
+
+ <key>NSHumanReadableCopyright</key>
+ <string>${MACOSX_BUNDLE_COPYRIGHT}</string>
+
+ <key>CFBundleIconFile</key>
+ <string>${MACOSX_BUNDLE_ICON_FILE}</string>
+
+ <key>CFBundleDevelopmentRegion</key>
+ <string>English</string>
+
+ <key>NSSupportsAutomaticGraphicsSwitching</key>
+ <true/>
+
+ <key>NSBluetoothAlwaysUsageDescription</key>
+ <string>Testing BluetoothAlways</string>
+ <key>NSCalendarsUsageDescription</key>
+ <string>Testing Calendars</string>
+ <key>NSCameraUsageDescription</key>
+ <string>Testing Camera</string>
+ <key>NSContactsUsageDescription</key>
+ <string>Testing Contacts</string>
+ <key>NSHealthShareUsageDescription</key>
+ <string>Testing HealthShare</string>
+ <key>NSHealthUpdateUsageDescription</key>
+ <string>Testing HealthUpdate</string>
+ <key>NSLocationAlwaysAndWhenInUseUsageDescription</key>
+ <string>Testing LocationAlwaysAndWhenInUse</string>
+ <key>NSLocationAlwaysUsageDescription</key>
+ <string>Testing LocationAlways</string>
+ <key>NSLocationWhenInUseUsageDescription</key>
+ <string>Testing LocationWhenInUse</string>
+ <key>NSMicrophoneUsageDescription</key>
+ <string>Testing Microphone</string>
+
+</dict>
+</plist>
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 <QtCore/qwaitcondition.h>
#include <QtCore/qtimer.h>
+#if defined(Q_OS_MACOS) && defined(QT_BUILD_INTERNAL)
+#include <private/qcore_mac_p.h>
+Q_CONSTRUCTOR_FUNCTION(qt_mac_ensureResponsible);
+#endif
+
class tst_QPermissions : public QObject
{
Q_OBJECT