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:
Volker Hilsheimer 2023-03-29 11:50:07 +02:00
parent ef27cc126c
commit 27867de8ca
2 changed files with 152 additions and 5 deletions

View File

@ -2820,8 +2820,9 @@ Qt::PermissionStatus QCoreApplication::checkPermission(const QPermission &permis
qApp->requestPermission(QCameraPermission{}, this, &CamerWidget::permissionUpdated);
\endcode
If \a context is destroyed before the request completes,
the \a functor will not be called.
The \a functor will be called in the thread of the \a context object. If
\a context is destroyed before the request completes, the \a functor will
not be called.
\include permissions.qdocinc requestPermission-postamble
@ -2848,6 +2849,43 @@ void QCoreApplication::requestPermission(const QPermission &requestedPermission,
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) {
Q_ASSERT_X(status != Qt::PermissionStatus::Undetermined, "QPermission",
"QCoreApplication::requestPermission() should never return Undetermined");
@ -2858,10 +2896,27 @@ void QCoreApplication::requestPermission(const QPermission &requestedPermission,
QPermission permission = requestedPermission;
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 };
slotObj->call(const_cast<QObject*>(context), argv);
}
}
if (!receiver)
slotObj->destroyIfLastRef();
});
}

View File

@ -25,6 +25,10 @@ private Q_SLOTS:
void converting_Bluetooth() const { return converting_impl<QBluetoothPermission>(); }
void conversionMaintainsState() const;
void functorWithContextInThread();
void receiverInThread();
void destroyedContextObject();
private:
template <typename T>
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)
#include "tst_qpermission.moc"