Permissions: respect context object's thread and life time
When a context object is provided, then callers expect that the functor or slot is executed in the thread of the context object. And if the context object has been destroyed by the time the permission response is received, the functor shouldn't be called at all. To implement this, we either have to plumb the call back through a signal/slot connection and benefit from QObject's infrastructure. This is not practical here, as we don't have an "engine QObject" that would emit a signal. Instead, we can create a QMetaCallEvent explicitly, following what we do in e.g. QHostInfo, and using a temporary QObject that handles the event to then call the functor. Add test coverage. Pick-to: 6.5 Change-Id: Id878e45b304857304165ab4a7c6aae76fbee46ce Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io> Reviewed-by: Qt CI Bot <qt_ci_bot@qt-project.org>
This commit is contained in:
parent
ef27cc126c
commit
27867de8ca
@ -2820,8 +2820,9 @@ Qt::PermissionStatus QCoreApplication::checkPermission(const QPermission &permis
|
|||||||
qApp->requestPermission(QCameraPermission{}, this, &CamerWidget::permissionUpdated);
|
qApp->requestPermission(QCameraPermission{}, this, &CamerWidget::permissionUpdated);
|
||||||
\endcode
|
\endcode
|
||||||
|
|
||||||
If \a context is destroyed before the request completes,
|
The \a functor will be called in the thread of the \a context object. If
|
||||||
the \a functor will not be called.
|
\a context is destroyed before the request completes, the \a functor will
|
||||||
|
not be called.
|
||||||
|
|
||||||
\include permissions.qdocinc requestPermission-postamble
|
\include permissions.qdocinc requestPermission-postamble
|
||||||
|
|
||||||
@ -2848,6 +2849,43 @@ void QCoreApplication::requestPermission(const QPermission &requestedPermission,
|
|||||||
|
|
||||||
Q_ASSERT(slotObj);
|
Q_ASSERT(slotObj);
|
||||||
|
|
||||||
|
// If we have a context object, then we dispatch the permission response
|
||||||
|
// asynchronously through a received object that lives in the same thread
|
||||||
|
// as the context object. Otherwise we call the functor synchronously when
|
||||||
|
// we get a response (which might still be asynchronous for the caller).
|
||||||
|
class PermissionReceiver : public QObject
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
PermissionReceiver(QtPrivate::QSlotObjectBase *slotObject, const QObject *context)
|
||||||
|
: slotObject(slotObject), context(context)
|
||||||
|
{}
|
||||||
|
protected:
|
||||||
|
bool event(QEvent *event) override {
|
||||||
|
if (event->type() == QEvent::MetaCall) {
|
||||||
|
auto metaCallEvent = static_cast<QMetaCallEvent *>(event);
|
||||||
|
if (metaCallEvent->id() == ushort(-1)) {
|
||||||
|
Q_ASSERT(slotObject);
|
||||||
|
// only execute if context object is still alive
|
||||||
|
if (context)
|
||||||
|
slotObject->call(const_cast<QObject*>(context.data()), metaCallEvent->args());
|
||||||
|
slotObject->destroyIfLastRef();
|
||||||
|
deleteLater();
|
||||||
|
|
||||||
|
return true;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return QObject::event(event);
|
||||||
|
}
|
||||||
|
private:
|
||||||
|
QtPrivate::QSlotObjectBase *slotObject;
|
||||||
|
QPointer<const QObject> context;
|
||||||
|
};
|
||||||
|
PermissionReceiver *receiver = nullptr;
|
||||||
|
if (context) {
|
||||||
|
receiver = new PermissionReceiver(slotObj, context);
|
||||||
|
receiver->moveToThread(context->thread());
|
||||||
|
}
|
||||||
|
|
||||||
QPermissions::Private::requestPermission(requestedPermission, [=](Qt::PermissionStatus status) {
|
QPermissions::Private::requestPermission(requestedPermission, [=](Qt::PermissionStatus status) {
|
||||||
Q_ASSERT_X(status != Qt::PermissionStatus::Undetermined, "QPermission",
|
Q_ASSERT_X(status != Qt::PermissionStatus::Undetermined, "QPermission",
|
||||||
"QCoreApplication::requestPermission() should never return Undetermined");
|
"QCoreApplication::requestPermission() should never return Undetermined");
|
||||||
@ -2858,10 +2896,27 @@ void QCoreApplication::requestPermission(const QPermission &requestedPermission,
|
|||||||
QPermission permission = requestedPermission;
|
QPermission permission = requestedPermission;
|
||||||
permission.m_status = status;
|
permission.m_status = status;
|
||||||
|
|
||||||
|
if (receiver) {
|
||||||
|
const int nargs = 2;
|
||||||
|
auto metaCallEvent = new QMetaCallEvent(slotObj, qApp, ushort(-1), nargs);
|
||||||
|
Q_CHECK_PTR(metaCallEvent);
|
||||||
|
void **args = metaCallEvent->args();
|
||||||
|
QMetaType *types = metaCallEvent->types();
|
||||||
|
const auto voidType = QMetaType::fromType<void>();
|
||||||
|
const auto permissionType = QMetaType::fromType<QPermission>();
|
||||||
|
types[0] = voidType;
|
||||||
|
types[1] = permissionType;
|
||||||
|
args[0] = nullptr;
|
||||||
|
args[1] = permissionType.create(&permission);
|
||||||
|
Q_CHECK_PTR(args[1]);
|
||||||
|
qApp->postEvent(receiver, metaCallEvent);
|
||||||
|
} else {
|
||||||
void *argv[] = { nullptr, &permission };
|
void *argv[] = { nullptr, &permission };
|
||||||
slotObj->call(const_cast<QObject*>(context), argv);
|
slotObj->call(const_cast<QObject*>(context), argv);
|
||||||
}
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!receiver)
|
||||||
slotObj->destroyIfLastRef();
|
slotObj->destroyIfLastRef();
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
@ -25,6 +25,10 @@ private Q_SLOTS:
|
|||||||
void converting_Bluetooth() const { return converting_impl<QBluetoothPermission>(); }
|
void converting_Bluetooth() const { return converting_impl<QBluetoothPermission>(); }
|
||||||
|
|
||||||
void conversionMaintainsState() const;
|
void conversionMaintainsState() const;
|
||||||
|
|
||||||
|
void functorWithContextInThread();
|
||||||
|
void receiverInThread();
|
||||||
|
void destroyedContextObject();
|
||||||
private:
|
private:
|
||||||
template <typename T>
|
template <typename T>
|
||||||
void converting_impl() const;
|
void converting_impl() const;
|
||||||
@ -141,5 +145,93 @@ void tst_QPermission::conversionMaintainsState() const
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
void tst_QPermission::functorWithContextInThread()
|
||||||
|
{
|
||||||
|
int argc = 0;
|
||||||
|
char *argv = nullptr;
|
||||||
|
QCoreApplication app(argc, &argv);
|
||||||
|
QThread::currentThread()->setObjectName("main thread");
|
||||||
|
QThread receiverThread;
|
||||||
|
receiverThread.setObjectName("receiverThread");
|
||||||
|
QObject receiver;
|
||||||
|
receiver.moveToThread(&receiverThread);
|
||||||
|
receiverThread.start();
|
||||||
|
auto guard = qScopeGuard([&receiverThread]{
|
||||||
|
receiverThread.quit();
|
||||||
|
QVERIFY(receiverThread.wait(1000));
|
||||||
|
});
|
||||||
|
|
||||||
|
DummyPermission dummy;
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*Could not find permission plugin for DummyPermission.*"));
|
||||||
|
#endif
|
||||||
|
QThread *permissionReceiverThread = nullptr;
|
||||||
|
qApp->requestPermission(dummy, &receiver, [&](const QPermission &permission){
|
||||||
|
auto dummy = permission.value<DummyPermission>();
|
||||||
|
QVERIFY(dummy);
|
||||||
|
permissionReceiverThread = QThread::currentThread();
|
||||||
|
});
|
||||||
|
QTRY_COMPARE(permissionReceiverThread, &receiverThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tst_QPermission::receiverInThread()
|
||||||
|
{
|
||||||
|
int argc = 0;
|
||||||
|
char *argv = nullptr;
|
||||||
|
QCoreApplication app(argc, &argv);
|
||||||
|
QThread::currentThread()->setObjectName("main thread");
|
||||||
|
QThread receiverThread;
|
||||||
|
receiverThread.setObjectName("receiverThread");
|
||||||
|
class Receiver : public QObject
|
||||||
|
{
|
||||||
|
public:
|
||||||
|
using QObject::QObject;
|
||||||
|
void handlePermission(const QPermission &permission)
|
||||||
|
{
|
||||||
|
auto dummy = permission.value<DummyPermission>();
|
||||||
|
QVERIFY(dummy);
|
||||||
|
permissionReceiverThread = QThread::currentThread();
|
||||||
|
}
|
||||||
|
|
||||||
|
QThread *permissionReceiverThread = nullptr;
|
||||||
|
} receiver;
|
||||||
|
receiver.moveToThread(&receiverThread);
|
||||||
|
receiverThread.start();
|
||||||
|
auto guard = qScopeGuard([&receiverThread]{
|
||||||
|
receiverThread.quit();
|
||||||
|
QVERIFY(receiverThread.wait(1000));
|
||||||
|
});
|
||||||
|
|
||||||
|
DummyPermission dummy;
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*Could not find permission plugin for DummyPermission.*"));
|
||||||
|
#endif
|
||||||
|
|
||||||
|
qApp->requestPermission(dummy, &receiver, &Receiver::handlePermission);
|
||||||
|
QTRY_COMPARE(receiver.permissionReceiverThread, &receiverThread);
|
||||||
|
}
|
||||||
|
|
||||||
|
void tst_QPermission::destroyedContextObject()
|
||||||
|
{
|
||||||
|
int argc = 0;
|
||||||
|
char *argv = nullptr;
|
||||||
|
QCoreApplication app(argc, &argv);
|
||||||
|
|
||||||
|
QObject *context = new QObject;
|
||||||
|
|
||||||
|
DummyPermission dummy;
|
||||||
|
#ifdef Q_OS_DARWIN
|
||||||
|
QTest::ignoreMessage(QtWarningMsg, QRegularExpression(".*Could not find permission plugin for DummyPermission.*"));
|
||||||
|
#endif
|
||||||
|
bool permissionReceived = false;
|
||||||
|
qApp->requestPermission(dummy, context, [&]{
|
||||||
|
permissionReceived = true;
|
||||||
|
});
|
||||||
|
QVERIFY2(!permissionReceived, "Permission received synchronously");
|
||||||
|
delete context;
|
||||||
|
QTest::qWait(100);
|
||||||
|
QVERIFY(!permissionReceived);
|
||||||
|
}
|
||||||
|
|
||||||
QTEST_APPLESS_MAIN(tst_QPermission)
|
QTEST_APPLESS_MAIN(tst_QPermission)
|
||||||
#include "tst_qpermission.moc"
|
#include "tst_qpermission.moc"
|
||||||
|
Loading…
Reference in New Issue
Block a user