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:
parent
1c6bf3e09e
commit
f0a7d74e1d
@ -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()
|
||||
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
@ -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()
|
||||
|
59
examples/corelib/permissions/Info.plist
Normal file
59
examples/corelib/permissions/Info.plist
Normal 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>
|
25
mkspecs/features/permissions.prf
Normal file
25
mkspecs/features/permissions.prf
Normal 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
|
||||
}
|
@ -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)
|
||||
|
@ -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"
|
||||
|
@ -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")
|
||||
|
@ -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})
|
||||
|
@ -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")
|
||||
|
@ -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)
|
||||
|
88
src/corelib/kernel/qpermissions_darwin.mm
Normal file
88
src/corelib/kernel/qpermissions_darwin.mm
Normal 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
|
@ -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
|
||||
|
90
src/corelib/platform/darwin/qdarwinpermissionplugin.mm
Normal file
90
src/corelib/platform/darwin/qdarwinpermissionplugin.mm
Normal 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"
|
@ -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"
|
@ -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"
|
@ -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
|
@ -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"
|
230
src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm
Normal file
230
src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm
Normal 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"
|
@ -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
|
58
src/corelib/platform/darwin/qdarwinpermissionplugin_p.h
Normal file
58
src/corelib/platform/darwin/qdarwinpermissionplugin_p.h
Normal 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
|
102
src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h
Normal file
102
src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h
Normal 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
|
@ -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()
|
||||
|
59
tests/manual/permissions/Info.plist
Normal file
59
tests/manual/permissions/Info.plist
Normal 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>
|
@ -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
|
||||
|
Loading…
Reference in New Issue
Block a user