diff --git a/examples/corelib/permissions/CMakeLists.txt b/examples/corelib/permissions/CMakeLists.txt new file mode 100644 index 00000000000..bca93b679f1 --- /dev/null +++ b/examples/corelib/permissions/CMakeLists.txt @@ -0,0 +1,28 @@ +cmake_minimum_required(VERSION 3.16) +project(permissions LANGUAGES CXX) + +set(CMAKE_AUTOMOC ON) + +if(NOT DEFINED INSTALL_EXAMPLESDIR) + set(INSTALL_EXAMPLESDIR "examples") +endif() + +set(INSTALL_EXAMPLEDIR "${INSTALL_EXAMPLESDIR}/corelib/permissions") + +find_package(Qt6 REQUIRED COMPONENTS Core Gui Widgets) + +qt_add_executable(permissions + main.cpp +) + +target_link_libraries(permissions PUBLIC + Qt::Core + Qt::Gui + Qt::Widgets +) + +install(TARGETS permissions + RUNTIME DESTINATION "${INSTALL_EXAMPLEDIR}" + BUNDLE DESTINATION "${INSTALL_EXAMPLEDIR}" + LIBRARY DESTINATION "${INSTALL_EXAMPLEDIR}" +) diff --git a/examples/corelib/permissions/main.cpp b/examples/corelib/permissions/main.cpp new file mode 100644 index 00000000000..29e26b0b59b --- /dev/null +++ b/examples/corelib/permissions/main.cpp @@ -0,0 +1,83 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR BSD-3-Clause + +#include +#include +#include +#include +#include +#include + +QT_REQUIRE_CONFIG(permissions); +#include + +class PermissionWidget : public QWidget +{ + Q_OBJECT +public: + explicit PermissionWidget(QWidget *parent = nullptr) : QWidget(parent) + { + QVBoxLayout *layout = new QVBoxLayout(this); + + static const QPermission permissions[] = { + QCameraPermission{}, + QMicrophonePermission{}, + QBluetoothPermission{}, + QContactsPermission{}, + QCalendarPermission{}, + QLocationPermission{} + }; + + for (auto permission : permissions) { + auto permissionName = QString::fromLatin1(permission.name()); + QPushButton *button = new QPushButton(permissionName.sliced(1, permissionName.length() - 11)); + connect(button, &QPushButton::clicked, this, &PermissionWidget::buttonClicked); + button->setProperty("permission", QVariant::fromValue(permission)); + layout->addWidget(button); + } + + QPalette pal = palette(); + pal.setBrush(QPalette::Window, QGradient(QGradient::HappyAcid)); + setPalette(pal); + } + +private: + void buttonClicked() + { + auto *button = static_cast(sender()); + + auto permission = button->property("permission").value(); + Q_ASSERT(permission.type().isValid()); + + switch (qApp->checkPermission(permission)) { + case Qt::PermissionStatus::Undetermined: + qApp->requestPermission(permission, this, + [this, button](const QPermission &permission) { + emit button->clicked(); // Try again + } + ); + return; + case Qt::PermissionStatus::Denied: + QMessageBox::warning(this, button->text(), + tr("Permission is needed to use %1. Please grant permission "\ + "to this application in the system settings.").arg(button->text())); + return; + case Qt::PermissionStatus::Granted: + break; // Proceed + } + + // All good, can use the feature + QMessageBox::information(this, button->text(), + tr("Accessing %1").arg(button->text())); + } +}; + +int main(int argc, char **argv) +{ + QApplication app(argc, argv); + PermissionWidget widget; + widget.show(); + return app.exec(); +} + +#include "main.moc" diff --git a/src/corelib/CMakeLists.txt b/src/corelib/CMakeLists.txt index f583d3180c8..1703df32821 100644 --- a/src/corelib/CMakeLists.txt +++ b/src/corelib/CMakeLists.txt @@ -1159,6 +1159,11 @@ qt_internal_extend_target(Core CONDITION QT_FEATURE_mimetype mimetypes/qmimetypeparser.cpp mimetypes/qmimetypeparser_p.h ) +qt_internal_extend_target(Core CONDITION QT_FEATURE_permissions + SOURCES + kernel/qpermissions.cpp kernel/qpermissions.h kernel/qpermissions_p.h +) + #### Keys ignored in scope 171:.:mimetypes:mimetypes/mimetypes.pri:QT_FEATURE_mimetype: # MIME_DATABASE = "mimetypes/mime/packages/freedesktop.org.xml" # OTHER_FILES = "$$MIME_DATABASE" diff --git a/src/corelib/configure.cmake b/src/corelib/configure.cmake index a8d6037a7d6..6e934957ebc 100644 --- a/src/corelib/configure.cmake +++ b/src/corelib/configure.cmake @@ -968,6 +968,12 @@ qt_feature("poll-exit-on-error" PUBLIC PURPOSE "Exit on error instead of just printing the error code and continue." ) qt_feature_definition("poll-exit-on-error" "QT_POLL_EXIT_ON_ERROR") +qt_feature("permissions" PUBLIC + SECTION "Utilities" + LABEL "Application permissions" + PURPOSE "Provides support for requesting user permission to access restricted data or APIs" + DISABLE ON +) qt_configure_add_summary_section(NAME "Qt Core") qt_configure_add_summary_entry(ARGS "backtrace") qt_configure_add_summary_entry(ARGS "doubleconversion") @@ -998,6 +1004,7 @@ qt_configure_add_summary_entry( ARGS "forkfd_pidfd" CONDITION LINUX ) +qt_configure_add_summary_entry(ARGS "permissions") qt_configure_end_summary_section() # end of "Qt Core" section qt_configure_add_report_entry( TYPE NOTE diff --git a/src/corelib/doc/include/QtCoreDoc b/src/corelib/doc/include/QtCoreDoc index 3dc7ce46e5f..f3c875e49ac 100644 --- a/src/corelib/doc/include/QtCoreDoc +++ b/src/corelib/doc/include/QtCoreDoc @@ -1,2 +1,3 @@ #include #include "../../platform/android/qandroidextras_p.h" +#include "../../kernel/qpermissions.h" diff --git a/src/corelib/doc/qtcore.qdocconf b/src/corelib/doc/qtcore.qdocconf index 19e1fb23e5d..f0a9fcc573d 100644 --- a/src/corelib/doc/qtcore.qdocconf +++ b/src/corelib/doc/qtcore.qdocconf @@ -34,7 +34,8 @@ headerdirs += .. sourcedirs += .. \ ../../tools/androiddeployqt \ - ../../android/templates + ../../android/templates \ + src/includes exampledirs += \ ../ \ diff --git a/src/corelib/doc/src/includes/permissions.qdocinc b/src/corelib/doc/src/includes/permissions.qdocinc new file mode 100644 index 00000000000..00bf848d37b --- /dev/null +++ b/src/corelib/doc/src/includes/permissions.qdocinc @@ -0,0 +1,51 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GFDL-1.3-no-invariants-only + +//! [requestPermission-functor] + When the request is ready, \a functor will be called as + \c {functor(const QPermission &permission)}, with + \c permission describing the result of the request. +//! [requestPermission-functor] + +//! [requestPermission-postamble] + If the user explicitly grants the application the requested \a permission, + or the \a permission is known to not require user authorization on the given + platform, the status will be Qt::PermissionStatus::Granted. + + If the user explicitly denies the application the requested \a permission, + or the \a permission is known to not be accessible or applicable to applications + on the given platform, the status will be Qt::PermissionStatus::Denied. + + The result of a request will never be Qt::PermissionStatus::Undetermined. + + \note Permissions can only be requested from the main thread. +//! [requestPermission-postamble] + +//! [permission-metadata] + \inmodule QtCore + \inheaderfile QPermissions + \ingroup permissions + \since 6.5 + \sa QPermission, + QCoreApplication::requestPermission(), + QCoreApplication::checkPermission(), + {Application Permissions} +//! [permission-metadata] + +//! [begin-usage-declarations] + To request this permission at runtime, the following platform + specific usage declarations have to be made at build time: + + \table + \header + \li Platform + \li Type + \li +//! [begin-usage-declarations] + +//! [end-usage-declarations] + \endtable + + Please see the individual usage declaration types for how + to add them to your project. +//! [end-usage-declarations] diff --git a/src/corelib/global/qconfig-bootstrapped.h b/src/corelib/global/qconfig-bootstrapped.h index 5d42facca37..ff5709f7af1 100644 --- a/src/corelib/global/qconfig-bootstrapped.h +++ b/src/corelib/global/qconfig-bootstrapped.h @@ -108,6 +108,7 @@ #define QT_FEATURE_commandlineparser 1 #define QT_FEATURE_settings -1 +#define QT_FEATURE_permissions -1 #define QT_NO_TEMPORARYFILE diff --git a/src/corelib/global/qnamespace.h b/src/corelib/global/qnamespace.h index 52a90999640..1bf5bb6169e 100644 --- a/src/corelib/global/qnamespace.h +++ b/src/corelib/global/qnamespace.h @@ -1708,6 +1708,12 @@ namespace Qt { PassThrough }; + enum class PermissionStatus { + Undetermined, + Granted, + Denied, + }; + // QTBUG-48701 enum ReturnByValueConstant { ReturnByValue }; // ### Qt 7: Remove me @@ -1803,6 +1809,7 @@ namespace Qt { Q_ENUM_NS(ChecksumType) Q_ENUM_NS(HighDpiScaleFactorRoundingPolicy) Q_ENUM_NS(TabFocusBehavior) + Q_ENUM_NS(PermissionStatus) #endif // Q_DOC } diff --git a/src/corelib/global/qnamespace.qdoc b/src/corelib/global/qnamespace.qdoc index 1a57c921996..5414369efec 100644 --- a/src/corelib/global/qnamespace.qdoc +++ b/src/corelib/global/qnamespace.qdoc @@ -3295,6 +3295,31 @@ \value PassThrough Don't round. */ +/*! + \enum Qt::PermissionStatus + + This enum describes the possible statuses of a permissions. + + \value Undetermined + The permission status is not yet known. Permission should be requested + via QCoreApplication::requestPermission() to determine the actual status. + This status will never be the result of requesting a permission. + + \value Granted + The user has explicitly granted the application the permission, + or the permission is known to not require user authorization on + the given platform. + + \value Denied + The user has explicitly denied the application the requested permission, + or the permission is known to not be accessible or applicable to applications + on the given platform. + + \since 6.5 + \sa QCoreApplication::requestPermission(), QCoreApplication::checkPermission(), + {Application Permissions} +*/ + /*! \enum Qt::ReturnByValueConstant \since 5.15 diff --git a/src/corelib/kernel/qcoreapplication.cpp b/src/corelib/kernel/qcoreapplication.cpp index 7fc90733d09..c178380e525 100644 --- a/src/corelib/kernel/qcoreapplication.cpp +++ b/src/corelib/kernel/qcoreapplication.cpp @@ -42,6 +42,10 @@ #include #include +#if QT_CONFIG(permissions) +#include +#endif + #ifndef QT_NO_QOBJECT #if defined(Q_OS_UNIX) # if defined(Q_OS_DARWIN) @@ -2659,6 +2663,125 @@ QString QCoreApplication::applicationVersion() return coreappdata() ? coreappdata()->applicationVersion : QString(); } +#if QT_CONFIG(permissions) || defined(Q_QDOC) + +/*! + Checks the status of the given \a permission + + If the result is Qt::PermissionStatus::Undetermined then permission should be + requested via requestPermission() to determine the user's intent. + + \since 6.5 + \sa requestPermission(), {Application Permissions} +*/ +Qt::PermissionStatus QCoreApplication::checkPermission(const QPermission &permission) +{ + return QPermissions::Private::checkPermission(permission); +} + +/*! + \fn template void QCoreApplication::requestPermission( + const QPermission &permission, Functor functor) + + Requests the given \a permission. + + \include permissions.qdocinc requestPermission-functor + + The \a functor can be a free-standing or static member function: + + \code + qApp->requestPermission(QCameraPermission{}, &permissionUpdated); + \endcode + + or a lambda: + + \code + qApp->requestPermission(QCameraPermission{}, [](const QPermission &permission) { + }); + \endcode + + \include permissions.qdocinc requestPermission-postamble + + \since 6.5 + \sa checkPermission(), {Application Permissions} +*/ + +/*! + \fn template void QCoreApplication::requestPermission( + const QPermission &permission, const QObject *context, + Functor functor) + + Requests the given \a permission, in the context of \a context. + + \include permissions.qdocinc requestPermission-functor + + The \a functor can be a free-standing or static member function: + + \code + qApp->requestPermission(QCameraPermission{}, context, &permissionUpdated); + \endcode + + a lambda: + + \code + qApp->requestPermission(QCameraPermission{}, context, [](const QPermission &permission) { + }); + \endcode + + or a slot in the \a context object: + + \code + qApp->requestPermission(QCameraPermission{}, this, &CamerWidget::permissionUpdated); + \endcode + + If \a context is destroyed before the request completes, + the \a functor will not be called. + + \include permissions.qdocinc requestPermission-postamble + + \since 6.5 + \overload + \sa checkPermission(), {Application Permissions} +*/ + +/*! + \internal + + Called by the various requestPermission overloads to perform the request. + + Calls the functor encapsulated in the \a slotObj in the given \a context + (which may be \c nullptr). +*/ +void QCoreApplication::requestPermission(const QPermission &requestedPermission, + QtPrivate::QSlotObjectBase *slotObj, const QObject *context) +{ + if (QThread::currentThread() != QCoreApplicationPrivate::mainThread()) { + qWarning(lcPermissions, "Permissions can only be requested from the GUI (main) thread"); + return; + } + + Q_ASSERT(slotObj); + + QPermissions::Private::requestPermission(requestedPermission, [=](Qt::PermissionStatus status) { + Q_ASSERT_X(status != Qt::PermissionStatus::Undetermined, "QPermission", + "QCoreApplication::requestPermission() should never return Undetermined"); + if (status == Qt::PermissionStatus::Undetermined) + status = Qt::PermissionStatus::Denied; + + if (QCoreApplication::self) { + QPermission permission = requestedPermission; + permission.m_status = status; + + void *argv[] = { nullptr, &permission }; + slotObj->call(const_cast(context), argv); + } + + slotObj->destroyIfLastRef(); + }); +} + +#endif // QT_CONFIG(permissions) + #if QT_CONFIG(library) Q_GLOBAL_STATIC(QRecursiveMutex, libraryPathMutex) diff --git a/src/corelib/kernel/qcoreapplication.h b/src/corelib/kernel/qcoreapplication.h index f31fd203b99..82580ceb34c 100644 --- a/src/corelib/kernel/qcoreapplication.h +++ b/src/corelib/kernel/qcoreapplication.h @@ -33,6 +33,10 @@ class QPostEventList; class QAbstractEventDispatcher; class QAbstractNativeEventFilter; +#if QT_CONFIG(permissions) || defined(Q_QDOC) +class QPermission; +#endif + #define qApp QCoreApplication::instance() class Q_CORE_EXPORT QCoreApplication @@ -107,6 +111,74 @@ public: static QString applicationFilePath(); static qint64 applicationPid() Q_DECL_CONST_FUNCTION; +#if QT_CONFIG(permissions) || defined(Q_QDOC) + Qt::PermissionStatus checkPermission(const QPermission &permission); + +# ifdef Q_QDOC + template + void requestPermission(const QPermission &permission, Functor functor); + template + void requestPermission(const QPermission &permission, const QObject *context, Functor functor); +# else + template // requestPermission to a QObject slot + void requestPermission(const QPermission &permission, + const typename QtPrivate::FunctionPointer::Object *receiver, Slot slot) + { + using CallbackSignature = QtPrivate::FunctionPointer; + using SlotSignature = QtPrivate::FunctionPointer; + + static_assert(int(SlotSignature::ArgumentCount) <= int(CallbackSignature::ArgumentCount), + "Slot requires more arguments than what can be provided."); + static_assert((QtPrivate::CheckCompatibleArguments::value), + "Slot arguments are not compatible (must be QPermission)"); + + auto slotObj = new QtPrivate::QSlotObject(slot); + requestPermission(permission, slotObj, receiver); + } + + // requestPermission to a functor or function pointer (with context) + template ::IsPointerToMemberFunction + && !std::is_same::value, bool> = true> + void requestPermission(const QPermission &permission, const QObject *context, Func func) + { + using CallbackSignature = QtPrivate::FunctionPointer; + constexpr int MatchingArgumentCount = QtPrivate::ComputeFunctorArgumentCount< + Func, CallbackSignature::Arguments>::Value; + + static_assert(MatchingArgumentCount == 0 + || MatchingArgumentCount == CallbackSignature::ArgumentCount, + "Functor arguments are not compatible (must be QPermission)"); + + QtPrivate::QSlotObjectBase *slotObj = nullptr; + if constexpr (MatchingArgumentCount == CallbackSignature::ArgumentCount) { + slotObj = new QtPrivate::QFunctorSlotObject(std::move(func)); + } else { + slotObj = new QtPrivate::QFunctorSlotObject::Value, void>(std::move(func)); + } + + requestPermission(permission, slotObj, context); + } + + // requestPermission to a functor or function pointer (without context) + template ::IsPointerToMemberFunction + && !std::is_same::value, bool> = true> + void requestPermission(const QPermission &permission, Func func) + { + requestPermission(permission, nullptr, std::move(func)); + } + +private: + void requestPermission(const QPermission &permission, + QtPrivate::QSlotObjectBase *slotObj, const QObject *context); +public: +# endif // Q_QDOC + +#endif // QT_CONFIG(permission) + #if QT_CONFIG(library) static void setLibraryPaths(const QStringList &); static QStringList libraryPaths(); diff --git a/src/corelib/kernel/qpermissions.cpp b/src/corelib/kernel/qpermissions.cpp new file mode 100644 index 00000000000..f56b62284f4 --- /dev/null +++ b/src/corelib/kernel/qpermissions.cpp @@ -0,0 +1,493 @@ +// 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 "qhashfunctions.h" + +#include +#include + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcPermissions, "qt.permissions", QtWarningMsg); + +/*! + \page permissions.html + \title Application Permissions + \brief Managing application permissions + + Many features of today's devices and operating systems can have + significant privacy, security, and performance implications if + misused. It's therefore increasingly common for platforms to + require explicit consent from the user before accessing these + features. + + The Qt permission APIs allow the application to check or request + permission for such features in a cross platform manner. + + \section1 Usage + + A feature that commonly requires user consent is access to the + microphone of the device. An application for recording voice + memos would perhaps look something like this initially: + + \code + void VoiceMemoWidget::onRecordingInitiated() + { + m_microphone->startRecording(); + } + \endcode + + To ensure this application works well on platforms that + require user consent for microphone access we would extend + it like this: + + \code + void VoiceMemoWidget::onRecordingInitiated() + { + #if QT_CONFIG(permissions) + QMicrophonePermission microphonePermission; + switch (qApp->checkPermission(microphonePermission)) { + case Qt::PermissionStatus::Undetermined: + qApp->requestPermission(microphonePermission, this + &VoiceMemoWidget::onRecordingInitiated); + return; + case Qt::PermissionStatus::Denied: + m_permissionInstructionsDialog->show(); + return; + case Qt::PermissionStatus::Granted: + break; // Proceed + } + #endif + m_microphone->startRecording(); + } + \endcode + + We first check if we already know the status of the microphone permission. + If we don't we initiate a permission request to determine the current + status, which will potentially ask the user for consent. We connect the + result of the request to the slot we're already in, so that we get another + chance at evaluating the permission status. + + Once the permission status is known, either because we had been granted or + denied permission at an earlier time, or after getting the result back from + the request we just initiated, we redirect the user to a dialog explaining + why we can not record voice memos at this time (if the permission was denied), + or proceed to using the microphone (if permission was granted). + + The use of the \c{QT_CONFIG(permissions)} macro ensures that the code + will work as before on platforms where permissions are not available. + + \section2 Declaring Permissions + + Some platforms require that the permissions you request are declared + up front at build time. + + \section3 Apple platforms + \target apple-usage-description + + Each permission you request must be accompanied by a so called + \e {usage description} string in the application's \c Info.plist + file, describing why the application needs to access the given + permission. For example: + + \badcode + NSMicrophoneUsageDescription + The microphone is used to record voice memos. + \endcode + + The relevant usage description keys are described in the documentation + for each permission type. + + \sa {Information Property List Files}. + + \section3 Android + \target android-uses-permission + + Each permission you request must be accompanied by a \c uses-permission + entry in the application's \c AndroidManifest.xml file. For example: + + \badcode + + + + \endcode + + The relevant permission names are described in the documentation + for each permission type. + + \sa {Qt Creator: Editing Manifest Files}. + + \section1 Available Permissions + + The following permissions types are available: + + \annotatedlist permissions + + \section1 Best Practices + + To ensure the best possible user experience for the end user we recommend + adopting the following best practices for managing application permissions: + + \list + + \li Request the minimal set of permissions needed. For example, if you only + need access to the microphone, do \e not request camera permission just in case. + Use the properties of individual permission types to limit the permission scope + even further, for example QContactsPermission::setReadOnly() to request read + only access. + + \li Request permissions in response to specific actions by the user. For example, + defer requesting microphone permission until the user presses the button to record + audio. Associating the permission request to a specific action gives the user a clearer + context of why the permission is needed. Do \e not request all needed permission on + startup. + + \li Present extra context and explanation if needed. Sometimes the action by the user + is not enough context. Consider presenting an explanation-dialog after the user has + initiated the action, but before requesting the permission, so the user is aware of + what's about to happen when the system permission dialog subsequently pops up. + + \li Be transparent and explicit about why permissions are needed. In explanation + dialogs and usage descriptions, be transparent about why the particular permission + is needed for your application to provide a specific feature, so users can make + informed decisions. + + \li Account for denied permissions. The permissions you request may be denied + for various reasons. You should always account for this situation, by gracefully + degrading the experience of your application, and presenting clear explanations + the user about the situation. + + \li Never request permissions from a library. The request of permissions should + be done as close as possible to the user, where the information needed to make + good decisions on the points above is available. Libraries can check permissions, + to ensure they have the prerequisites for doing their work, but if the permission + is undetermined or denied this should be reflected through the library's API, + so that the application in turn can request the necessary permissions. + + \endlist +*/ + + +/*! + \class QPermission + \inmodule QtCore + \inheaderfile QPermissions + \brief An opaque wrapper of a typed permission. + + The QPermission class is an opaque wrapper of a \l{typed permission}, + used when checking or requesting permissions. You do not need to construct + this type explicitly, as the type is automatically used when checking or + requesting permissions: + + \code + qApp->checkPermission(QCameraPermission{}); + \endcode + + When requesting permissions, the given functor will + be passed an instance of a QPermissions, which can be used + to check the result of the request: + + \code + qApp->requestPermission(QCameraPermission{}, [](const QPermission &permission) { + if (permission.status() == Qt::PermissionStatus:Granted) + takePhoto(); + }); + \endcode + + To inspect the properties of the original typed permission, + use the data() function: + + \code + QLocationPermission locationPermission; + locationPermission.setAccuracy(QLocationPermission::Precise); + qApp->requestPermission(locationPermission, this, &LocationWidget::permissionUpdated); + \endcode + + \code + void LocationWidget::permissionUpdated(const QPermission &permission) + { + if (permission.status() != Qt::PermissionStatus:Granted) + return; + auto locationPermission = permission.data(); + if (locationPermission.accuracy() != QLocationPerission::Precise) + return; + updatePreciseLocation(); + } + \endcode + + \target typed permission + \section2 Typed Permissions + + The following permissions are available: + + \annotatedlist permissions + + \sa {Application Permissions} +*/ + +/*! + \fn template QPermission::QPermission(const Type &type) + + Constructs a permission from the given \l{typed permission} \a type. + + You do not need to construct this type explicitly, as the type is automatically + used when checking or requesting permissions. +*/ + +/*! + \fn template Type QPermission::data() const + + Returns the \l{typed permission} of type \c Type. + + The type must match the type that was originally used to request + the permission. Use type() for dynamically choosing which typed + permission to request. +*/ + +/*! + Returns the status of the permission. +*/ +Qt::PermissionStatus QPermission::status() const +{ + return m_status; +} + +/*! + Returns the type of the permission. +*/ +QMetaType QPermission::type() const +{ + return m_data.metaType(); +} + +#define QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(ClassName) \ + ClassName::ClassName() : d(new ClassName##Private) {} \ + ClassName::ClassName(const ClassName &other) noexcept = default; \ + ClassName::ClassName(ClassName &&other) noexcept = default; \ + ClassName::~ClassName() noexcept = default; \ + ClassName &ClassName::operator=(const ClassName &other) noexcept = default; + +/*! + \class QCameraPermission + \brief Access the camera for taking pictures or videos. + + \section1 Requirements + + \include permissions.qdocinc begin-usage-declarations + \include permissions.qdocinc end-usage-declarations + + \include permissions.qdocinc permission-metadata +*/ +class QCameraPermissionPrivate : public QSharedData {}; +QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QCameraPermission) + +/*! + \class QMicrophonePermission + \brief Access the microphone for monitoring or recording sound. + + \section1 Requirements + + \include permissions.qdocinc begin-usage-declarations + + \include permissions.qdocinc end-usage-declarations + + \include permissions.qdocinc permission-metadata +*/ +class QMicrophonePermissionPrivate : public QSharedData {}; +QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QMicrophonePermission) + +/*! + \class QBluetoothPermission + \brief Access Bluetooth peripherals. + + \section1 Requirements + + \include permissions.qdocinc begin-usage-declarations + \include permissions.qdocinc end-usage-declarations + + \include permissions.qdocinc permission-metadata +*/ +class QBluetoothPermissionPrivate : public QSharedData {}; +QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QBluetoothPermission) + +/*! + \class QLocationPermission + \brief Access the user's location. + + By default the request is for approximate accuracy, + and only while the application is in use. Use + setAccuracy() and/or setAvailability() to override + the default. + + \section1 Requirements + + \include permissions.qdocinc begin-usage-declarations + \include permissions.qdocinc end-usage-declarations + + \include permissions.qdocinc permission-metadata +*/ +class QLocationPermissionPrivate : public QSharedData +{ +public: + using Accuracy = QLocationPermission::Accuracy; + Accuracy accuracy = Accuracy::Approximate; + + using Availability = QLocationPermission::Availability; + Availability availability = Availability::WhenInUse; +}; + +QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QLocationPermission) + +/*! + \enum QLocationPermission::Accuracy + + This enum is used to control the accuracy of the location data. + + \value Approximate An approximate location is requested. + \value Precise A precise location is requested. +*/ + +/*! + \enum QLocationPermission::Availability + + This enum is used to control the availability of the location data. + + \value WhenInUse The location is only available only when the + application is in use. + \value Always The location is available at all times, including when + the application is in the background. +*/ + +/*! + Sets the desired \a accuracy of the request. +*/ +void QLocationPermission::setAccuracy(Accuracy accuracy) +{ + d.detach(); + d->accuracy = accuracy; +} + +/*! + Returns the accuracy of the request. +*/ +QLocationPermission::Accuracy QLocationPermission::accuracy() const +{ + return d->accuracy; +} + +/*! + Sets the desired \a availability of the request. +*/ +void QLocationPermission::setAvailability(Availability availability) +{ + d.detach(); + d->availability = availability; +} + +/*! + Returns the availability of the request. +*/ +QLocationPermission::Availability QLocationPermission::availability() const +{ + return d->availability; +} + +/*! + \class QContactsPermission + \brief Access the user's contacts. + + By default the request is for both read and write access. + Use setReadOnly() to override the default. + + \section1 Requirements + + \include permissions.qdocinc begin-usage-declarations + \include permissions.qdocinc end-usage-declarations + + \include permissions.qdocinc permission-metadata +*/ +class QContactsPermissionPrivate : public QSharedData +{ +public: + bool isReadOnly = false; +}; + +QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QContactsPermission) + +/*! + Sets whether to \a enable read-only access to the contacts. +*/ +void QContactsPermission::setReadOnly(bool enable) +{ + d.detach(); + d->isReadOnly = enable; +} + +/*! + Returns whether the request is for read-only access to the contacts. +*/ +bool QContactsPermission::isReadOnly() const +{ + return d->isReadOnly; +} + +/*! + \class QCalendarPermission + \brief Access the user's calendar. + + By default the request is for both read and write access. + Use setReadOnly() to override the default. + + \section1 Requirements + + \include permissions.qdocinc begin-usage-declarations + \include permissions.qdocinc end-usage-declarations + + \include permissions.qdocinc permission-metadata +*/ +class QCalendarPermissionPrivate : public QSharedData +{ +public: + bool isReadOnly = false; +}; + +QT_DEFINE_PERMISSION_SPECIAL_FUNCTIONS(QCalendarPermission) + +/*! + Sets whether to \a enable read-only access to the calendar. +*/ +void QCalendarPermission::setReadOnly(bool enable) +{ + d.detach(); + d->isReadOnly = enable; +} + +/*! + Returns whether the request is for read-only access to the calendar. +*/ +bool QCalendarPermission::isReadOnly() const +{ + return d->isReadOnly; +} + + +#ifndef QT_NO_DEBUG_STREAM +QDebug operator<<(QDebug debug, const QPermission &permission) +{ + const auto verbosity = debug.verbosity(); + QDebugStateSaver saver(debug); + debug.nospace().setVerbosity(0); + if (verbosity >= QDebug::DefaultVerbosity) + debug << permission.type().name() << "("; + debug << permission.status(); + if (verbosity >= QDebug::DefaultVerbosity) + debug << ")"; + return debug; +} +#endif + +QT_END_NAMESPACE + +#include "moc_qpermissions.cpp" diff --git a/src/corelib/kernel/qpermissions.h b/src/corelib/kernel/qpermissions.h new file mode 100644 index 00000000000..94ca5fff5bf --- /dev/null +++ b/src/corelib/kernel/qpermissions.h @@ -0,0 +1,156 @@ +// 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 QPERMISSIONS_H +#define QPERMISSIONS_H + +#if 0 +#pragma qt_class(QPermissions) +#endif + +#include +#include +#include + +#include +#include +#include + +#if !defined(Q_QDOC) +QT_REQUIRE_CONFIG(permissions); +#endif + +QT_BEGIN_NAMESPACE + +#ifndef QT_NO_DEBUG_STREAM +class QDebug; +#endif + +struct QMetaObject; +class QCoreApplication; + +class Q_CORE_EXPORT QPermission +{ + Q_GADGET + + template + struct is_permission : public std::false_type {}; + + template + struct is_permission : public std::true_type {}; + +public: + explicit QPermission() = default; + +#ifdef Q_QDOC + template + QPermission(const Type &type); +#else + template ::value, bool> = true> + QPermission(const T &t) : m_data(QVariant::fromValue(t)) {} +#endif + + Qt::PermissionStatus status() const; + + QMetaType type() const; + +#ifdef Q_QDOC + template + Type data() const; +#else + template ::value, bool> = true> + T data() const + { + auto requestedType = QMetaType::fromType(); + if (type() != requestedType) { + qWarning() << "Can not convert from" << type().name() + << "to" << requestedType.name(); + return T{}; + } + return m_data.value(); + } +#endif + +#ifndef QT_NO_DEBUG_STREAM + friend Q_CORE_EXPORT QDebug operator<<(QDebug debug, const QPermission &); +#endif + +private: + Qt::PermissionStatus m_status = Qt::PermissionStatus::Undetermined; + QVariant m_data; + + friend class QCoreApplication; +}; + +#define QT_PERMISSION(ClassName) \ + Q_GADGET \ + using QtPermissionHelper = void; \ + friend class QPermission; \ +public: \ + ClassName(); \ + ClassName(const ClassName &other) noexcept; \ + ClassName(ClassName &&other) noexcept; \ + ~ClassName() noexcept; \ + ClassName &operator=(const ClassName &other) noexcept; \ + QT_MOVE_ASSIGNMENT_OPERATOR_IMPL_VIA_PURE_SWAP(ClassName) \ + void swap(ClassName &other) noexcept { d.swap(other.d); } \ +private: \ + QtPrivate::QExplicitlySharedDataPointerV2 d; + +class QLocationPermissionPrivate; +class Q_CORE_EXPORT QLocationPermission +{ + QT_PERMISSION(QLocationPermission) +public: + enum Accuracy { Approximate, Precise }; + Q_ENUM(Accuracy) + + void setAccuracy(Accuracy accuracy); + Accuracy accuracy() const; + + enum Availability { WhenInUse, Always }; + Q_ENUM(Availability) + + void setAvailability(Availability availability); + Availability availability() const; +}; +Q_DECLARE_SHARED(QLocationPermission); + +class QCalendarPermissionPrivate; +class Q_CORE_EXPORT QCalendarPermission +{ + QT_PERMISSION(QCalendarPermission) +public: + void setReadOnly(bool enable); + bool isReadOnly() const; +}; +Q_DECLARE_SHARED(QCalendarPermission); + +class QContactsPermissionPrivate; +class Q_CORE_EXPORT QContactsPermission +{ + QT_PERMISSION(QContactsPermission) +public: + void setReadOnly(bool enable); + bool isReadOnly() const; +}; +Q_DECLARE_SHARED(QContactsPermission); + +#define Q_DECLARE_MINIMAL_PERMISSION(ClassName) \ + class ClassName##Private; \ + class Q_CORE_EXPORT ClassName \ + { \ + QT_PERMISSION(ClassName) \ + }; \ + Q_DECLARE_SHARED(ClassName); + +Q_DECLARE_MINIMAL_PERMISSION(QCameraPermission); +Q_DECLARE_MINIMAL_PERMISSION(QMicrophonePermission); +Q_DECLARE_MINIMAL_PERMISSION(QBluetoothPermission); + +#undef QT_PERMISSION +#undef Q_DECLARE_MINIMAL_PERMISSION + +QT_END_NAMESPACE + +#endif // QPERMISSIONS_H diff --git a/src/corelib/kernel/qpermissions_p.h b/src/corelib/kernel/qpermissions_p.h new file mode 100644 index 00000000000..fc1d948dce2 --- /dev/null +++ b/src/corelib/kernel/qpermissions_p.h @@ -0,0 +1,41 @@ +// 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 QPERMISSIONS_P_H +#define QPERMISSIONS_P_H + +#include "qpermissions.h" + +#include +#include + +#include + +QT_REQUIRE_CONFIG(permissions); + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcPermissions) + +namespace QPermissions::Private +{ + using PermissionCallback = std::function; + + Qt::PermissionStatus checkPermission(const QPermission &permission); + void requestPermission(const QPermission &permission, const PermissionCallback &callback); +} + +QT_END_NAMESPACE + +#endif // QPERMISSIONS_P_H diff --git a/tests/manual/permissions/.gitignore b/tests/manual/permissions/.gitignore new file mode 100644 index 00000000000..bd59d3407ec --- /dev/null +++ b/tests/manual/permissions/.gitignore @@ -0,0 +1 @@ +tst_qpermissions diff --git a/tests/manual/permissions/CMakeLists.txt b/tests/manual/permissions/CMakeLists.txt new file mode 100644 index 00000000000..847d9a74115 --- /dev/null +++ b/tests/manual/permissions/CMakeLists.txt @@ -0,0 +1,7 @@ + +qt_internal_add_test(tst_qpermissions + SOURCES + tst_qpermissions.cpp + LIBRARIES + Qt::CorePrivate +) diff --git a/tests/manual/permissions/tst_qpermissions.cpp b/tests/manual/permissions/tst_qpermissions.cpp new file mode 100644 index 00000000000..a97afe8487e --- /dev/null +++ b/tests/manual/permissions/tst_qpermissions.cpp @@ -0,0 +1,111 @@ +// Copyright (C) 2022 The Qt Company Ltd. +// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR GPL-3.0-only WITH Qt-GPL-exception-1.0 + +#include + +#include +#include +#include +#include +#include + +class tst_QPermissions : public QObject +{ + Q_OBJECT + +private slots: + void initTestCase_data(); + + void checkPermission(); + void checkPermissionInNonMainThread(); + + void requestPermission(); + void requestPermissionInNonMainThread(); +}; + +void tst_QPermissions::initTestCase_data() +{ + QTest::addColumn("permission"); + + QTest::newRow("Camera") << QPermission(QCameraPermission{}); + QTest::newRow("Microphone") << QPermission(QMicrophonePermission{}); + QTest::newRow("Bluetooth") << QPermission(QBluetoothPermission{}); + QTest::newRow("Contacts") << QPermission(QContactsPermission{}); + QTest::newRow("Calendar") << QPermission(QCalendarPermission{}); + QTest::newRow("Location") << QPermission(QLocationPermission{}); +} + +void tst_QPermissions::checkPermission() +{ + QFETCH_GLOBAL(QPermission, permission); + qApp->checkPermission(permission); +} + +class Thread : public QThread +{ +public: + QMutex mutex; + QWaitCondition cond; + std::function function; + + void run() override + { + QMutexLocker locker(&mutex); + function(); + cond.wakeOne(); + } +}; + +void tst_QPermissions::checkPermissionInNonMainThread() +{ + QFETCH_GLOBAL(QPermission, permission); + + Thread thread; + thread.function = [=]{ + qApp->checkPermission(permission); + }; + + QVERIFY(!thread.isFinished()); + QMutexLocker locker(&thread.mutex); + thread.start(); + QVERIFY(!thread.isFinished()); + thread.cond.wait(locker.mutex()); + QVERIFY(thread.wait(1000)); + QVERIFY(thread.isFinished()); +} + +void tst_QPermissions::requestPermission() +{ + QFETCH_GLOBAL(QPermission, permission); + QTimer::singleShot(0, [=] { + qApp->requestPermission(permission, [=](auto result) { + qDebug() << result; + Q_ASSERT(QThread::currentThread() == thread()); + qApp->exit(); + }); + }); + qApp->exec(); +} + +void tst_QPermissions::requestPermissionInNonMainThread() +{ + QFETCH_GLOBAL(QPermission, permission); + + QTest::ignoreMessage(QtWarningMsg, "Permissions can only be requested from the GUI (main) thread"); + + Thread thread; + thread.function = [&]{ + qApp->requestPermission(permission, [&]() {}); + }; + + QVERIFY(!thread.isFinished()); + QMutexLocker locker(&thread.mutex); + thread.start(); + QVERIFY(!thread.isFinished()); + thread.cond.wait(locker.mutex()); + QVERIFY(thread.wait(1000)); + QVERIFY(thread.isFinished()); +} + +QTEST_MAIN(tst_QPermissions) +#include "tst_qpermissions.moc"