Add permission API backend for macOS and iOS

When submitting applications to the iOS and macOS AppStore the
application goes through static analysis, which will trigger on
uses of various privacy protected APIs, unless the application
has a corresponding usage description for the permission in the
Info.plist file. This applies even if the application never
requests the given permission, but just links to a Qt library
that has the offending symbols or library dependencies.

To ensure that the application does not have to add usage
descriptions to their Info.plist for permissions they never
plan to use we split up the various permission implementations
into small static libraries that register with the Qt plugin
mechanism as permission backends. We can then inspect the
application's Info.plist at configure time and only add the
relevant static permission libraries.

Furthermore, since some permissions can be checked without any
usage description, we allow the implementation to be split up
into two separate translation units. By putting the request in
its own translation unit we can selectively include it during
linking by telling the linker to look for a special symbol.
This is useful for libraries such as Qt Multimedia who would
like to check the current permission status, but without
needing to request any permission of its own.

Done-with: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
Change-Id: Ic2a43e1a0c45a91df6101020639f473ffd9454cc
Reviewed-by: Tor Arne Vestbø <tor.arne.vestbo@qt.io>
This commit is contained in:
Timur Pocheptsov 2022-05-10 15:02:43 +02:00
parent 1c6bf3e09e
commit f0a7d74e1d
26 changed files with 1286 additions and 5 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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

View File

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