diff options
Diffstat (limited to 'src/corelib/platform')
19 files changed, 3766 insertions, 158 deletions
diff --git a/src/corelib/platform/android/qandroidextras.cpp b/src/corelib/platform/android/qandroidextras.cpp new file mode 100644 index 0000000000..aa0c3fd093 --- /dev/null +++ b/src/corelib/platform/android/qandroidextras.cpp @@ -0,0 +1,1228 @@ +// Copyright (C) 2021 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 "qandroidextras_p.h" + +#include <QtCore/qbuffer.h> +#include <QtCore/qdatastream.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qvariant.h> +#include <QtCore/qmutex.h> +#include <QtCore/qtimer.h> +#include <QtCore/qset.h> + +#if QT_CONFIG(future) +#include <QtCore/qpromise.h> +#endif + +QT_BEGIN_NAMESPACE + +class QAndroidParcelPrivate +{ +public: + QAndroidParcelPrivate(); + explicit QAndroidParcelPrivate(const QJniObject& parcel); + + void writeData(const QByteArray &data) const; + void writeBinder(const QAndroidBinder &binder) const; + void writeFileDescriptor(int fd) const; + + QByteArray readData() const; + int readFileDescriptor() const; + QAndroidBinder readBinder() const; + +private: + friend class QAndroidBinder; + friend class QAndroidParcel; + QJniObject handle; +}; + +struct FileDescriptor +{ + explicit FileDescriptor(int fd = -1) + : handle("java/io/FileDescriptor") + { + QJniEnvironment().checkAndClearExceptions(); + handle.setField("descriptor", fd); + } + + QJniObject handle; +}; + +QAndroidParcelPrivate::QAndroidParcelPrivate() + : handle(QJniObject::callStaticObjectMethod("android/os/Parcel","obtain", + "()Landroid/os/Parcel;").object()) +{} + +QAndroidParcelPrivate::QAndroidParcelPrivate(const QJniObject &parcel) + : handle(parcel) +{} + +void QAndroidParcelPrivate::writeData(const QByteArray &data) const +{ + if (data.isEmpty()) + return; + + QJniEnvironment().checkAndClearExceptions(); + QJniEnvironment env; + jbyteArray array = env->NewByteArray(data.size()); + env->SetByteArrayRegion(array, 0, data.length(), + reinterpret_cast<const jbyte*>(data.constData())); + handle.callMethod<void>("writeByteArray", "([B)V", array); + env->DeleteLocalRef(array); +} + +void QAndroidParcelPrivate::writeBinder(const QAndroidBinder &binder) const +{ + QJniEnvironment().checkAndClearExceptions(); + handle.callMethod<void>("writeStrongBinder", "(Landroid/os/IBinder;)V", + binder.handle().object()); +} + +void QAndroidParcelPrivate::writeFileDescriptor(int fd) const +{ + QJniEnvironment().checkAndClearExceptions(); + handle.callMethod<void>("writeFileDescriptor", "(Ljava/io/FileDescriptor;)V", + FileDescriptor(fd).handle.object()); +} + +QByteArray QAndroidParcelPrivate::readData() const +{ + QJniEnvironment().checkAndClearExceptions(); + auto array = handle.callObjectMethod("createByteArray", "()[B"); + QJniEnvironment env; + auto sz = env->GetArrayLength(jbyteArray(array.object())); + QByteArray res(sz, Qt::Initialization::Uninitialized); + env->GetByteArrayRegion(jbyteArray(array.object()), 0, sz, + reinterpret_cast<jbyte *>(res.data())); + return res; +} + +int QAndroidParcelPrivate::readFileDescriptor() const +{ + QJniEnvironment().checkAndClearExceptions(); + auto parcelFD = handle.callObjectMethod("readFileDescriptor", + "()Landroid/os/ParcelFileDescriptor;"); + if (parcelFD.isValid()) + return parcelFD.callMethod<jint>("getFd", "()I"); + return -1; +} + +QAndroidBinder QAndroidParcelPrivate::readBinder() const +{ + QJniEnvironment().checkAndClearExceptions(); + auto strongBinder = handle.callObjectMethod("readStrongBinder", "()Landroid/os/IBinder;"); + return QAndroidBinder(strongBinder.object()); +} + +/*! + \class QAndroidParcel + \inheaderfile QtCore/private/qandroidextras_p.h + \preliminary + \inmodule QtCorePrivate + \brief Wraps the most important methods of Android Parcel class. + + The QAndroidParcel is a convenience class that wraps the most important + \l {https://developer.android.com/reference/android/os/Parcel.html}{Android Parcel} + methods. + + \include qtcore.qdoc qtcoreprivate-usage + + \since 6.2 +*/ + +/*! + Creates a new object. + */ +QAndroidParcel::QAndroidParcel() + : d(new QAndroidParcelPrivate()) +{ +} + +/*! + Wraps the \a parcel object. + */ +QAndroidParcel::QAndroidParcel(const QJniObject& parcel) + : d(new QAndroidParcelPrivate(parcel)) +{ + +} + +QAndroidParcel::~QAndroidParcel() +{ +} + +/*! + Writes the provided \a data as a byte array + */ +void QAndroidParcel::writeData(const QByteArray &data) const +{ + d->writeData(data); +} + +/*! + Writes the provided \a value. The value is converted into a + QByteArray before is written. + */ +void QAndroidParcel::writeVariant(const QVariant &value) const +{ + QByteArray buff; + QDataStream stream(&buff, QIODevice::WriteOnly); + stream << value; + d->writeData(buff); +} + +/*! + Writes a \a binder object. This is useful for a client to + send to a server a binder which can be used by the server callback the client. + */ +void QAndroidParcel::writeBinder(const QAndroidBinder &binder) const +{ + d->writeBinder(binder); +} + +/*! + Writes the provided \a fd. + */ +void QAndroidParcel::writeFileDescriptor(int fd) const +{ + d->writeFileDescriptor(fd); +} + +/*! + Returns the data as a QByteArray + */ +QByteArray QAndroidParcel::readData() const +{ + return d->readData(); +} + +/*! + Returns the data as a QVariant + */ +QVariant QAndroidParcel::readVariant() const +{ + QDataStream stream(d->readData()); + QVariant res; + stream >> res; + return res; +} + +/*! + Returns the binder as a QAndroidBinder + */ +QAndroidBinder QAndroidParcel::readBinder() const +{ + return d->readBinder(); +} + +/*! + Returns the file descriptor + */ +int QAndroidParcel::readFileDescriptor() const +{ + return d->readFileDescriptor(); +} + +/*! + The return value is useful to call other Java API which are not covered by this wrapper + */ +QJniObject QAndroidParcel::handle() const +{ + return d->handle; +} + + + +/*! + \class QAndroidBinder + \inheaderfile QtCore/private/qandroidextras_p.h + \preliminary + \inmodule QtCorePrivate + \brief Wraps the most important methods of Android Binder class. + + The QAndroidBinder is a convenience class that wraps the most important + \l {https://developer.android.com/reference/android/os/Binder.html}{Android Binder} + methods. + + \include qtcore.qdoc qtcoreprivate-usage + + \since 6.2 +*/ + + +/*! + \enum QAndroidBinder::CallType + + This enum is used with \l QAndroidBinder::transact() to describe the mode in which the + IPC call is performed. + + \value Normal normal IPC, meaning that the caller waits the result from the callee + \value OneWay one-way IPC, meaning that the caller returns immediately, without waiting + for a result from the callee +*/ + + +class QAndroidBinderPrivate +{ +public: + explicit QAndroidBinderPrivate(QAndroidBinder *binder) + : handle("org/qtproject/qt/android/extras/QtAndroidBinder", "(J)V", jlong(binder)) + , m_isQtAndroidBinder(true) + { + QJniEnvironment().checkAndClearExceptions(); + } + + explicit QAndroidBinderPrivate(const QJniObject &binder) + : handle(binder), m_isQtAndroidBinder(false) {}; + void setDeleteListener(const std::function<void()> &func) { m_deleteListener = func; } + ~QAndroidBinderPrivate() + { + if (m_isQtAndroidBinder) { + QJniEnvironment().checkAndClearExceptions(); + handle.callMethod<void>("setId", "(J)V", jlong(0)); + if (m_deleteListener) + m_deleteListener(); + } + } + +private: + QJniObject handle; + std::function<void()> m_deleteListener; + bool m_isQtAndroidBinder; + friend class QAndroidBinder; +}; + +/*! + Creates a new object which can be used to perform IPC. + + \sa onTransact, transact + */ +QAndroidBinder::QAndroidBinder() + : d(new QAndroidBinderPrivate(this)) +{ +} + +/*! + Creates a new object from the \a binder Java object. + + \sa transact + */ +QAndroidBinder::QAndroidBinder(const QJniObject &binder) + : d(new QAndroidBinderPrivate(binder)) +{ +} + +QAndroidBinder::~QAndroidBinder() +{ +} + +/*! + Default implementation is a stub that returns false. + The user should override this method to get the transact data from the caller. + + The \a code is the action to perform. + The \a data is the marshaled data sent by the caller.\br + The \a reply is the marshaled data to be sent to the caller.\br + The \a flags are the additional operation flags.\br + + \warning This method is called from Binder's thread which is different + from the thread that this object was created. + + \sa transact + */ +bool QAndroidBinder::onTransact(int /*code*/, const QAndroidParcel &/*data*/, + const QAndroidParcel &/*reply*/, CallType /*flags*/) +{ + return false; +} + +/*! + Performs an IPC call + + The \a code is the action to perform. Should be between + \l {https://developer.android.com/reference/android/os/IBinder.html#FIRST_CALL_TRANSACTION} + {FIRST_CALL_TRANSACTION} and + \l {https://developer.android.com/reference/android/os/IBinder.html#LAST_CALL_TRANSACTION} + {LAST_CALL_TRANSACTION}.\br + The \a data is the marshaled data to send to the target.\br + The \a reply (if specified) is the marshaled data to be received from the target. + May be \b nullptr if you are not interested in the return value.\br + The \a flags are the additional operation flags.\br + + \return true on success + */ +bool QAndroidBinder::transact(int code, const QAndroidParcel &data, + QAndroidParcel *reply, CallType flags) const +{ + QJniEnvironment().checkAndClearExceptions(); + return d->handle.callMethod<jboolean>("transact", + "(ILandroid/os/Parcel;Landroid/os/Parcel;I)Z", + jint(code), data.d->handle.object(), + reply ? reply->d->handle.object() : nullptr, + jint(flags)); +} + +/*! + The return value is useful to call other Java API which are not covered by this wrapper + */ +QJniObject QAndroidBinder::handle() const +{ + return d->handle; +} + + + + +/*! + \class QAndroidServiceConnection + \inheaderfile QtCore/private/qandroidextras_p.h + \preliminary + \inmodule QtCorePrivate + \brief Wraps the most important methods of Android ServiceConnection class. + + The QAndroidServiceConnection is a convenience abstract class which wraps the + \l {https://developer.android.com/reference/android/content/ServiceConnection.html}{AndroidServiceConnection} + interface. + + It is useful when you perform a QtAndroidPrivate::bindService operation. + + \include qtcore.qdoc qtcoreprivate-usage + + \since 6.2 +*/ + +/*! + Creates a new object + */ +QAndroidServiceConnection::QAndroidServiceConnection() + : m_handle("org/qtproject/qt/android/extras/QtAndroidServiceConnection", "(J)V", jlong(this)) +{ +} + +/*! + Creates a new object from an existing \a serviceConnection. + + It's useful when you have your own Java implementation. + Of course onServiceConnected()/onServiceDisconnected() + will not be called anymore. + */ +QAndroidServiceConnection::QAndroidServiceConnection(const QJniObject &serviceConnection) + : m_handle(serviceConnection) +{ +} + +QAndroidServiceConnection::~QAndroidServiceConnection() +{ + m_handle.callMethod<void>("setId", "(J)V", jlong(this)); +} + +/*! + returns the underline QJniObject + */ +QJniObject QAndroidServiceConnection::handle() const +{ + return m_handle; +} + +/*! + \fn void QAndroidServiceConnection::onServiceConnected(const QString &name, const QAndroidBinder &serviceBinder) + + This notification is called when the client managed to connect to the service. + The \a name contains the server name, the \a serviceBinder is the binder that the client + uses to perform IPC operations. + + \warning This method is called from Binder's thread which is different + from the thread that this object was created. + + returns the underline QJniObject + */ + +/*! + \fn void QAndroidServiceConnection::onServiceDisconnected(const QString &name) + + Called when a connection to the Service has been lost. + The \a name parameter specifies which connectioen was lost. + + \warning This method is called from Binder's thread which is different + from the thread that this object was created. + + returns the underline QJniObject + */ + + +Q_CONSTINIT static QBasicAtomicInteger<uint> nextUniqueActivityRequestCode = Q_BASIC_ATOMIC_INITIALIZER(0); + +// Get a unique activity request code. +static int uniqueActivityRequestCode() +{ + constexpr uint ReservedForQtOffset = 0x1000; // Reserve all request codes under 0x1000 for Qt + + const uint requestCodeBase = nextUniqueActivityRequestCode.fetchAndAddRelaxed(1); + if (requestCodeBase == uint(INT_MAX) - ReservedForQtOffset) + qWarning("Unique activity request code has wrapped. Unexpected behavior may occur."); + + const int requestCode = static_cast<int>(requestCodeBase + ReservedForQtOffset); + return requestCode; +} + +class QAndroidActivityResultReceiverPrivate: public QtAndroidPrivate::ActivityResultListener +{ +public: + QAndroidActivityResultReceiver *q; + mutable QHash<int, int> localToGlobalRequestCode; + mutable QHash<int, int> globalToLocalRequestCode; + + int globalRequestCode(int localRequestCode) const + { + const auto oldSize = localToGlobalRequestCode.size(); + auto &e = localToGlobalRequestCode[localRequestCode]; + if (localToGlobalRequestCode.size() != oldSize) { + // new entry, populate: + int globalRequestCode = uniqueActivityRequestCode(); + e = globalRequestCode; + globalToLocalRequestCode[globalRequestCode] = localRequestCode; + } + return e; + } + + bool handleActivityResult(jint requestCode, jint resultCode, jobject data) + { + const auto it = std::as_const(globalToLocalRequestCode).find(requestCode); + if (it != globalToLocalRequestCode.cend()) { + q->handleActivityResult(*it, resultCode, QJniObject(data)); + return true; + } + + return false; + } + + static QAndroidActivityResultReceiverPrivate *get(QAndroidActivityResultReceiver *publicObject) + { + return publicObject->d.data(); + } +}; + +/*! + \class QAndroidActivityResultReceiver + \inheaderfile QtCore/private/qandroidextras_p.h + \preliminary + \inmodule QtCorePrivate + \since 6.2 + \brief Interface used for callbacks from onActivityResult() in the main Android activity. + + Create a subclass of this class to be notified of the results when using the + \c QtAndroidPrivate::startActivity() and \c QtAndroidPrivate::startIntentSender() APIs. + + \include qtcore.qdoc qtcoreprivate-usage + */ + +/*! + \internal +*/ +QAndroidActivityResultReceiver::QAndroidActivityResultReceiver() + : d(new QAndroidActivityResultReceiverPrivate) +{ + d->q = this; + QtAndroidPrivate::registerActivityResultListener(d.data()); +} + +/*! + \internal +*/ +QAndroidActivityResultReceiver::~QAndroidActivityResultReceiver() +{ + QtAndroidPrivate::unregisterActivityResultListener(d.data()); +} + +/*! + \fn void QAndroidActivityResultReceiver::handleActivityResult(int receiverRequestCode, int resultCode, const QJniObject &data) + + Reimplement this function to get activity results after starting an activity using + either QtAndroidPrivate::startActivity() or QtAndroidPrivate::startIntentSender(). + The \a receiverRequestCode is the request code unique to this receiver which was + originally passed to the startActivity() or startIntentSender() functions. The + \a resultCode is the result returned by the activity, and \a data is either null + or a Java object of the class android.content.Intent. Both the last to arguments + are identical to the arguments passed to onActivityResult(). +*/ + + + +class QAndroidServicePrivate : public QObject, public QtAndroidPrivate::OnBindListener +{ +public: + QAndroidServicePrivate(QAndroidService *service, + const std::function<QAndroidBinder*(const QAndroidIntent&)> &binder ={}) + : m_service(service) + , m_binder(binder) + { + QTimer::singleShot(0,this, [this]{ QtAndroidPrivate::setOnBindListener(this);}); + } + + ~QAndroidServicePrivate() + { + QMutexLocker lock(&m_bindersMutex); + while (!m_binders.empty()) { + auto it = m_binders.begin(); + lock.unlock(); + delete (*it); + lock.relock(); + } + } + + // OnBindListener interface + jobject onBind(jobject intent) override + { + auto qai = QAndroidIntent(intent); + auto binder = m_binder ? m_binder(qai) : m_service->onBind(qai); + if (binder) { + { + QMutexLocker lock(&m_bindersMutex); + binder->d->setDeleteListener([this, binder]{binderDestroied(binder);}); + m_binders.insert(binder); + } + return binder->handle().object(); + } + return nullptr; + } + +private: + void binderDestroied(QAndroidBinder* obj) + { + QMutexLocker lock(&m_bindersMutex); + m_binders.remove(obj); + } + +public: + QAndroidService *m_service = nullptr; + std::function<QAndroidBinder *(const QAndroidIntent &)> m_binder; + QMutex m_bindersMutex; + QSet<QAndroidBinder*> m_binders; +}; + +/*! + \class QAndroidService + \inheaderfile QtCore/private/qandroidextras_p.h + \preliminary + \inmodule QtCorePrivate + \brief Wraps the most important methods of Android Service class. + + The QAndroidService is a convenience class that wraps the most important + \l {https://developer.android.com/reference/android/app/Service.html}{Android Service} + methods. + + \include qtcore.qdoc qtcoreprivate-usage + + \since 6.2 +*/ + + +/*! + \fn QAndroidService::QAndroidService(int &argc, char **argv) + + Creates a new Android service, passing \a argc and \a argv as parameters. + + //! Parameter \a flags is omitted in the documentation. + + \sa QCoreApplication + */ +QAndroidService::QAndroidService(int &argc, char **argv, int flags) + : QCoreApplication (argc, argv, QtAndroidPrivate::acuqireServiceSetup(flags)) + , d(new QAndroidServicePrivate{this}) +{ +} + +/*! + \fn QAndroidService::QAndroidService(int &argc, char **argv, const std::function<QAndroidBinder *(const QAndroidIntent &)> &binder) + + Creates a new Android service, passing \a argc and \a argv as parameters. + + \a binder is used to create a \l {QAndroidBinder}{binder} when needed. + + //! Parameter \a flags is omitted in the documentation. + + \sa QCoreApplication + */ +QAndroidService::QAndroidService(int &argc, char **argv, + const std::function<QAndroidBinder*(const QAndroidIntent&)> &binder, + int flags) + : QCoreApplication (argc, argv, QtAndroidPrivate::acuqireServiceSetup(flags)) + , d(new QAndroidServicePrivate{this, binder}) +{ +} + +QAndroidService::~QAndroidService() +{} + +/*! + The user must override this method and to return a binder. + + The \a intent parameter contains all the caller information. + + The returned binder is used by the caller to perform IPC calls. + + \warning This method is called from Binder's thread which is different + from the thread that this object was created. + + \sa QAndroidBinder::onTransact, QAndroidBinder::transact + */ +QAndroidBinder* QAndroidService::onBind(const QAndroidIntent &/*intent*/) +{ + return nullptr; +} + +static jboolean onTransact(JNIEnv */*env*/, jclass /*cls*/, jlong id, jint code, jobject data, + jobject reply, jint flags) +{ + if (!id) + return false; + + return reinterpret_cast<QAndroidBinder*>(id)->onTransact( + code, QAndroidParcel(data), QAndroidParcel(reply), QAndroidBinder::CallType(flags)); +} + +static void onServiceConnected(JNIEnv */*env*/, jclass /*cls*/, jlong id, jstring name, + jobject service) +{ + if (!id) + return; + + return reinterpret_cast<QAndroidServiceConnection *>(id)->onServiceConnected( + QJniObject(name).toString(), QAndroidBinder(service)); +} + +static void onServiceDisconnected(JNIEnv */*env*/, jclass /*cls*/, jlong id, jstring name) +{ + if (!id) + return; + + return reinterpret_cast<QAndroidServiceConnection *>(id)->onServiceDisconnected( + QJniObject(name).toString()); +} + +bool QtAndroidPrivate::registerExtrasNatives(QJniEnvironment &env) +{ + static const JNINativeMethod methods[] = { + {"onTransact", "(JILandroid/os/Parcel;Landroid/os/Parcel;I)Z", (void *)onTransact}, + {"onServiceConnected", "(JLjava/lang/String;Landroid/os/IBinder;)V", (void *)onServiceConnected}, + {"onServiceDisconnected", "(JLjava/lang/String;)V", (void *)onServiceDisconnected} + }; + + return env.registerNativeMethods("org/qtproject/qt/android/extras/QtNative", methods, 3); +} + +/*! + \class QAndroidIntent + \inheaderfile QtCore/private/qandroidextras_p.h + \preliminary + \inmodule QtCorePrivate + \brief Wraps the most important methods of Android Intent class. + + The QAndroidIntent is a convenience class that wraps the most important + \l {https://developer.android.com/reference/android/content/Intent.html}{Android Intent} + methods. + + \include qtcore.qdoc qtcoreprivate-usage + + \since 6.2 +*/ + +/*! + Create a new intent + */ +QAndroidIntent::QAndroidIntent() + : m_handle("android.content.Intent", "()V") +{ + +} + +QAndroidIntent::~QAndroidIntent() +{} + +/*! + Wraps the provided \a intent java object. + */ +QAndroidIntent::QAndroidIntent(const QJniObject &intent) + : m_handle(intent) +{ +} + +/*! + Creates a new intent and sets the provided \a action. + */ +QAndroidIntent::QAndroidIntent(const QString &action) + : m_handle("android.content.Intent", "(Ljava/lang/String;)V", + QJniObject::fromString(action).object()) +{ + QJniEnvironment().checkAndClearExceptions(); +} + +/*! + Creates a new intent and sets the provided \a packageContext and the service \a className. + Example: + \code + auto serviceIntent = QAndroidIntent(QtAndroidPrivate::androidActivity().object(), "com.example.MyService"); + \endcode + + \sa QtAndroidPrivate::bindService + */ +QAndroidIntent::QAndroidIntent(const QJniObject &packageContext, const char *className) + : m_handle("android/content/Intent", "(Landroid/content/Context;Ljava/lang/Class;)V", + packageContext.object(), QJniEnvironment().findClass(className)) +{ + QJniEnvironment().checkAndClearExceptions(); +} + +/*! + Sets the \a key with the \a data in the Intent extras + */ +void QAndroidIntent::putExtra(const QString &key, const QByteArray &data) +{ + QJniEnvironment().checkAndClearExceptions(); + QJniEnvironment env; + jbyteArray array = env->NewByteArray(data.size()); + env->SetByteArrayRegion(array, 0, data.length(), + reinterpret_cast<const jbyte*>(data.constData())); + m_handle.callObjectMethod("putExtra", "(Ljava/lang/String;[B)Landroid/content/Intent;", + QJniObject::fromString(key).object(), array); + env->DeleteLocalRef(array); + QJniEnvironment().checkAndClearExceptions(); +} + +/*! + Returns the extra \a key data from the Intent extras + */ +QByteArray QAndroidIntent::extraBytes(const QString &key) +{ + QJniEnvironment().checkAndClearExceptions(); + auto array = m_handle.callObjectMethod("getByteArrayExtra", "(Ljava/lang/String;)[B", + QJniObject::fromString(key).object()); + if (!array.isValid() || !array.object()) + return QByteArray(); + QJniEnvironment env; + auto sz = env->GetArrayLength(jarray(array.object())); + QByteArray res(sz, Qt::Initialization::Uninitialized); + env->GetByteArrayRegion(jbyteArray(array.object()), 0, sz, + reinterpret_cast<jbyte *>(res.data())); + QJniEnvironment().checkAndClearExceptions(); + + return res; +} + +/*! + Sets the \a key with the \a value in the Intent extras. + */ +void QAndroidIntent::putExtra(const QString &key, const QVariant &value) +{ + QByteArray buff; + QDataStream stream(&buff, QIODevice::WriteOnly); + stream << value; + putExtra(key, buff); +} + +/*! + Returns the extra \a key data from the Intent extras as a QVariant + */ +QVariant QAndroidIntent::extraVariant(const QString &key) +{ + QDataStream stream(extraBytes(key)); + QVariant res; + stream >> res; + return res; +} + +/*! + The return value is useful to call other Java API which are not covered by this wrapper + */ +QJniObject QAndroidIntent::handle() const +{ + return m_handle; +} + + + +/*! + \namespace QtAndroidPrivate + \preliminary + \inmodule QtCorePrivate + \since 6.2 + \brief The QtAndroidPrivate namespace provides miscellaneous functions + to aid Android development. + \inheaderfile QtCore/private/qandroidextras_p.h + + \include qtcore.qdoc qtcoreprivate-usage +*/ + +/*! + \since 6.2 + \enum QtAndroidPrivate::BindFlag + + This enum is used with QtAndroidPrivate::bindService to describe the mode in which the + binding is performed. + + \value None No options. + \value AutoCreate Automatically creates the service as long as the binding exist. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_AUTO_CREATE} + {BIND_AUTO_CREATE} documentation for more details. + \value DebugUnbind Include debugging help for mismatched calls to unbind. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_DEBUG_UNBIND} + {BIND_DEBUG_UNBIND} documentation for more details. + \value NotForeground Don't allow this binding to raise the target service's process to the foreground scheduling priority. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_NOT_FOREGROUND} + {BIND_NOT_FOREGROUND} documentation for more details. + \value AboveClient Indicates that the client application binding to this service considers the service to be more important than the app itself. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_ABOVE_CLIENT} + {BIND_ABOVE_CLIENT} documentation for more details. + \value AllowOomManagement Allow the process hosting the bound service to go through its normal memory management. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_ALLOW_OOM_MANAGEMENT} + {BIND_ALLOW_OOM_MANAGEMENT} documentation for more details. + \value WaivePriority Don't impact the scheduling or memory management priority of the target service's hosting process. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_WAIVE_PRIORITY} + {BIND_WAIVE_PRIORITY} documentation for more details. + \value Important This service is assigned a higher priority so that it is available to the client when needed. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_IMPORTANT} + {BIND_IMPORTANT} documentation for more details. + \value AdjustWithActivity If binding from an activity, allow the target service's process importance to be raised based on whether the activity is visible to the user. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_ADJUST_WITH_ACTIVITY} + {BIND_ADJUST_WITH_ACTIVITY} documentation for more details. + \value ExternalService The service being bound is an isolated, external service. + See \l {https://developer.android.com/reference/android/content/Context.html#BIND_EXTERNAL_SERVICE} + {BIND_EXTERNAL_SERVICE} documentation for more details. +*/ + +/*! + \since 6.2 + + Starts the activity given by \a intent and provides the result asynchronously through the + \a resultReceiver if this is non-null. + + If \a resultReceiver is null, then the \c startActivity() method in the \c androidActivity() + will be called. Otherwise \c startActivityForResult() will be called. + + The \a receiverRequestCode is a request code unique to the \a resultReceiver, and will be + returned along with the result, making it possible to use the same receiver for more than + one intent. + + */ +void QtAndroidPrivate::startActivity(const QJniObject &intent, + int receiverRequestCode, + QAndroidActivityResultReceiver *resultReceiver) +{ + QJniObject activity = QtAndroidPrivate::activity(); + if (resultReceiver != 0) { + QAndroidActivityResultReceiverPrivate *resultReceiverD = + QAndroidActivityResultReceiverPrivate::get(resultReceiver); + activity.callMethod<void>("startActivityForResult", + "(Landroid/content/Intent;I)V", + intent.object<jobject>(), + resultReceiverD->globalRequestCode(receiverRequestCode)); + } else { + activity.callMethod<void>("startActivity", + "(Landroid/content/Intent;)V", + intent.object<jobject>()); + } +} + +/*! + \since 6.2 + + Starts the activity given by \a intent and provides the result asynchronously through the + \a resultReceiver if this is non-null. + + If \a resultReceiver is null, then the \c startActivity() method in the \c androidActivity() + will be called. Otherwise \c startActivityForResult() will be called. + + The \a receiverRequestCode is a request code unique to the \a resultReceiver, and will be + returned along with the result, making it possible to use the same receiver for more than + one intent. + + */ +void QtAndroidPrivate::startActivity(const QAndroidIntent &intent, + int receiverRequestCode, + QAndroidActivityResultReceiver *resultReceiver) +{ + startActivity(intent.handle(), receiverRequestCode, resultReceiver); +} + +/*! + \since 6.2 + + Starts the activity given by \a intent, using the request code \a receiverRequestCode, + and provides the result by calling \a callbackFunc. +*/ +void QtAndroidPrivate::startActivity(const QJniObject &intent, + int receiverRequestCode, + std::function<void(int, int, const QJniObject &data)> callbackFunc) +{ + QJniObject activity = QtAndroidPrivate::activity(); + QAndroidActivityCallbackResultReceiver::instance()->registerCallback(receiverRequestCode, + callbackFunc); + startActivity(intent, receiverRequestCode, QAndroidActivityCallbackResultReceiver::instance()); +} + +/*! + \since 6.2 + + Starts the activity given by \a intentSender and provides the result asynchronously through the + \a resultReceiver if this is non-null. + + If \a resultReceiver is null, then the \c startIntentSender() method in the \c androidActivity() + will be called. Otherwise \c startIntentSenderForResult() will be called. + + The \a receiverRequestCode is a request code unique to the \a resultReceiver, and will be + returned along with the result, making it possible to use the same receiver for more than + one intent. + +*/ +void QtAndroidPrivate::startIntentSender(const QJniObject &intentSender, + int receiverRequestCode, + QAndroidActivityResultReceiver *resultReceiver) +{ + QJniObject activity = QtAndroidPrivate::activity(); + if (resultReceiver != 0) { + QAndroidActivityResultReceiverPrivate *resultReceiverD = + QAndroidActivityResultReceiverPrivate::get(resultReceiver); + activity.callMethod<void>("startIntentSenderForResult", + "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V", + intentSender.object<jobject>(), + resultReceiverD->globalRequestCode(receiverRequestCode), + 0, // fillInIntent + 0, // flagsMask + 0, // flagsValues + 0); // extraFlags + } else { + activity.callMethod<void>("startIntentSender", + "(Landroid/content/IntentSender;Landroid/content/Intent;III)V", + intentSender.object<jobject>(), + 0, // fillInIntent + 0, // flagsMask + 0, // flagsValues + 0); // extraFlags + + } + +} + +/*! + \since 6.2 + \fn bool QtAndroidPrivate::bindService(const QAndroidIntent &serviceIntent, const QAndroidServiceConnection &serviceConnection, BindFlags flags = BindFlag::None) + + Binds the service given by \a serviceIntent, \a serviceConnection and \a flags. + The \a serviceIntent object identifies the service to connect to. + The \a serviceConnection is a listener that receives the information as the service + is started and stopped. + + \return true on success + + See \l {https://developer.android.com/reference/android/content/Context.html#bindService%28android.content.Intent,%20android.content.ServiceConnection,%20int%29} + {Android documentation} documentation for more details. + + \sa QAndroidIntent, QAndroidServiceConnection, BindFlag +*/ +bool QtAndroidPrivate::bindService(const QAndroidIntent &serviceIntent, + const QAndroidServiceConnection &serviceConnection, BindFlags flags) +{ + QJniEnvironment().checkAndClearExceptions(); + QJniObject contextObj = QtAndroidPrivate::context(); + return contextObj.callMethod<jboolean>( + "bindService", + "(Landroid/content/Intent;Landroid/content/ServiceConnection;I)Z", + serviceIntent.handle().object(), + serviceConnection.handle().object(), + jint(flags)); +} + +QAndroidActivityCallbackResultReceiver * QAndroidActivityCallbackResultReceiver::s_instance = nullptr; + +QAndroidActivityCallbackResultReceiver::QAndroidActivityCallbackResultReceiver() + : QAndroidActivityResultReceiver() + , callbackMap() +{ +} + +void QAndroidActivityCallbackResultReceiver::handleActivityResult(int receiverRequestCode, + int resultCode, + const QJniObject &intent) +{ + callbackMap[receiverRequestCode](receiverRequestCode, resultCode, intent); + callbackMap.remove(receiverRequestCode); +} + +QAndroidActivityCallbackResultReceiver * QAndroidActivityCallbackResultReceiver::instance() { + if (!s_instance) { + s_instance = new QAndroidActivityCallbackResultReceiver(); + } + return s_instance; +} + +void QAndroidActivityCallbackResultReceiver::registerCallback( + int receiverRequestCode, + std::function<void(int, int, const QJniObject &data)> callbackFunc) +{ + callbackMap.insert(receiverRequestCode, callbackFunc); +} + +// Permissions API + +static const char qtNativeClassName[] = "org/qtproject/qt/android/QtNative"; + +QtAndroidPrivate::PermissionResult resultFromAndroid(jint value) +{ + return value == 0 ? QtAndroidPrivate::Authorized : QtAndroidPrivate::Denied; +} + +using PendingPermissionRequestsHash + = QHash<int, QSharedPointer<QPromise<QtAndroidPrivate::PermissionResult>>>; +Q_GLOBAL_STATIC(PendingPermissionRequestsHash, g_pendingPermissionRequests); +Q_CONSTINIT static QBasicMutex g_pendingPermissionRequestsMutex; + +static int nextRequestCode() +{ + Q_CONSTINIT static QBasicAtomicInt counter = Q_BASIC_ATOMIC_INITIALIZER(0); + return counter.fetchAndAddRelaxed(1); +} + +/*! + \internal + + This function is called when the result of the permission request is available. + Once a permission is requested, the result is braodcast by the OS and listened + to by QtActivity which passes it to C++ through a native JNI method call. + */ +static void sendRequestPermissionsResult(JNIEnv *env, jobject *obj, jint requestCode, + jobjectArray permissions, jintArray grantResults) +{ + Q_UNUSED(obj); + + QMutexLocker locker(&g_pendingPermissionRequestsMutex); + auto it = g_pendingPermissionRequests->constFind(requestCode); + if (it == g_pendingPermissionRequests->constEnd()) { + qWarning() << "Found no valid pending permission request for request code" << requestCode; + return; + } + + auto request = *it; + g_pendingPermissionRequests->erase(it); + locker.unlock(); + + const int size = env->GetArrayLength(permissions); + std::unique_ptr<jint[]> results(new jint[size]); + env->GetIntArrayRegion(grantResults, 0, size, results.get()); + + for (int i = 0 ; i < size; ++i) { + QtAndroidPrivate::PermissionResult result = resultFromAndroid(results[i]); + request->addResult(result, i); + } + + QtAndroidPrivate::releaseAndroidDeadlockProtector(); + request->finish(); +} + +QFuture<QtAndroidPrivate::PermissionResult> +requestPermissionsInternal(const QStringList &permissions) +{ + // No mechanism to request permission for SDK version below 23, because + // permissions defined in the manifest are granted at install time. + if (QtAndroidPrivate::androidSdkVersion() < 23) { + QList<QtAndroidPrivate::PermissionResult> result; + result.reserve(permissions.size()); + // ### can we kick off all checkPermission()s, and whenAll() collect results? + for (const QString &permission : permissions) + result.push_back(QtAndroidPrivate::checkPermission(permission).result()); + return QtFuture::makeReadyRangeFuture(result); + } + + if (!QtAndroidPrivate::acquireAndroidDeadlockProtector()) + return QtFuture::makeReadyValueFuture(QtAndroidPrivate::Denied); + + QSharedPointer<QPromise<QtAndroidPrivate::PermissionResult>> promise; + promise.reset(new QPromise<QtAndroidPrivate::PermissionResult>()); + QFuture<QtAndroidPrivate::PermissionResult> future = promise->future(); + promise->start(); + + const int requestCode = nextRequestCode(); + QMutexLocker locker(&g_pendingPermissionRequestsMutex); + g_pendingPermissionRequests->insert(requestCode, promise); + locker.unlock(); + + QNativeInterface::QAndroidApplication::runOnAndroidMainThread([permissions, requestCode] { + QJniEnvironment env; + jclass clazz = env.findClass("java/lang/String"); + auto array = env->NewObjectArray(permissions.size(), clazz, nullptr); + int index = 0; + + for (auto &perm : permissions) + env->SetObjectArrayElement(array, index++, QJniObject::fromString(perm).object()); + + QJniObject(QtAndroidPrivate::activity()).callMethod<void>("requestPermissions", + "([Ljava/lang/String;I)V", + array, + requestCode); + env->DeleteLocalRef(array); + }); + + return future; +} + +/*! + \preliminary + Requests the \a permission and returns a QFuture representing the + result of the request. + + \since 6.2 + \sa checkPermission() +*/ +QFuture<QtAndroidPrivate::PermissionResult> +QtAndroidPrivate::requestPermission(const QString &permission) +{ + return requestPermissions({permission}); +} + +QFuture<QtAndroidPrivate::PermissionResult> +QtAndroidPrivate::requestPermissions(const QStringList &permissions) +{ + // avoid the uneccessary call and response to an empty permission string + if (permissions.isEmpty()) + return QtFuture::makeReadyValueFuture(QtAndroidPrivate::Denied); + return requestPermissionsInternal(permissions); +} + +/*! + \preliminary + Checks whether this process has the named \a permission and returns a QFuture + representing the result of the check. + + \since 6.2 + \sa requestPermission() +*/ +QFuture<QtAndroidPrivate::PermissionResult> +QtAndroidPrivate::checkPermission(const QString &permission) +{ + QtAndroidPrivate::PermissionResult result = Denied; + if (!permission.isEmpty()) { + auto res = QJniObject::callStaticMethod<jint>(qtNativeClassName, + "checkSelfPermission", + "(Ljava/lang/String;)I", + QJniObject::fromString(permission).object()); + result = resultFromAndroid(res); + } + return QtFuture::makeReadyValueFuture(result); +} + +bool QtAndroidPrivate::registerPermissionNatives(QJniEnvironment &env) +{ + if (QtAndroidPrivate::androidSdkVersion() < 23) + return true; + + const JNINativeMethod methods[] = { + {"sendRequestPermissionsResult", "(I[Ljava/lang/String;[I)V", + reinterpret_cast<void *>(sendRequestPermissionsResult) + }}; + + return env.registerNativeMethods(qtNativeClassName, methods, 1); +} + +QT_END_NAMESPACE + +#include "moc_qandroidextras_p.cpp" diff --git a/src/corelib/platform/android/qandroidextras_p.h b/src/corelib/platform/android/qandroidextras_p.h new file mode 100644 index 0000000000..efdc6cf74f --- /dev/null +++ b/src/corelib/platform/android/qandroidextras_p.h @@ -0,0 +1,246 @@ +// Copyright (C) 2016 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 QANDROIDEXTRAS_H +#define QANDROIDEXTRAS_H + +// +// 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. +// + +#include <jni.h> +#include <functional> + +#include <QtCore/private/qglobal_p.h> +#include <QtCore/qjniobject.h> +#include <QtCore/private/qjnihelpers_p.h> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qmap.h> + +#if QT_CONFIG(future) +#include <QtCore/qfuture.h> +#endif + +QT_BEGIN_NAMESPACE + +class QAndroidParcel; +class QAndroidBinderPrivate; +class QAndroidBinder; + +class Q_CORE_EXPORT QAndroidBinder +{ +public: + enum class CallType { + Normal = 0, + OneWay = 1 + }; + +public: + explicit QAndroidBinder(); + QAndroidBinder(const QJniObject &binder); + + virtual ~QAndroidBinder(); + + virtual bool onTransact(int code, const QAndroidParcel &data, + const QAndroidParcel &reply, CallType flags); + bool transact(int code, const QAndroidParcel &data, + QAndroidParcel *reply = nullptr, CallType flags = CallType::Normal) const; + + QJniObject handle() const; + +private: + friend class QAndroidBinderPrivate; + friend class QAndroidParcelPrivate; + friend class QAndroidServicePrivate; + QSharedPointer<QAndroidBinderPrivate> d; +}; + +class QAndroidParcelPrivate; + +class Q_CORE_EXPORT QAndroidParcel +{ +public: + QAndroidParcel(); + explicit QAndroidParcel(const QJniObject& parcel); + virtual ~QAndroidParcel(); + + void writeData(const QByteArray &data) const; + void writeVariant(const QVariant &value) const; + void writeBinder(const QAndroidBinder &binder) const; + void writeFileDescriptor(int fd) const; + + QByteArray readData() const; + QVariant readVariant() const; + QAndroidBinder readBinder() const; + int readFileDescriptor() const; + + QJniObject handle() const; + +private: + friend class QAndroidParcelPrivate; + friend class QAndroidBinder; + QSharedPointer<QAndroidParcelPrivate> d; +}; + +class QAndroidActivityResultReceiverPrivate; + +class Q_CORE_EXPORT QAndroidActivityResultReceiver +{ +public: + QAndroidActivityResultReceiver(); + virtual ~QAndroidActivityResultReceiver(); + virtual void handleActivityResult(int receiverRequestCode, int resultCode, + const QJniObject &data) = 0; + +private: + friend class QAndroidActivityResultReceiverPrivate; + Q_DISABLE_COPY(QAndroidActivityResultReceiver) + + QScopedPointer<QAndroidActivityResultReceiverPrivate> d; +}; + +class Q_CORE_EXPORT QAndroidServiceConnection +{ +public: + QAndroidServiceConnection(); + explicit QAndroidServiceConnection(const QJniObject &serviceConnection); + virtual ~QAndroidServiceConnection(); + + virtual void onServiceConnected(const QString &name, + const QAndroidBinder &serviceBinder) = 0; + virtual void onServiceDisconnected(const QString &name) = 0; + + QJniObject handle() const; +private: + Q_DISABLE_COPY(QAndroidServiceConnection) + QJniObject m_handle; +}; + +class Q_CORE_EXPORT QAndroidIntent +{ +public: + QAndroidIntent(); + virtual ~QAndroidIntent(); + explicit QAndroidIntent(const QJniObject &intent); + explicit QAndroidIntent(const QString &action); + explicit QAndroidIntent(const QJniObject &packageContext, const char *className); + + void putExtra(const QString &key, const QByteArray &data); + QByteArray extraBytes(const QString &key); + + void putExtra(const QString &key, const QVariant &value); + QVariant extraVariant(const QString &key); + + QJniObject handle() const; + +private: + QJniObject m_handle; +}; + +class QAndroidServicePrivate; + +class Q_CORE_EXPORT QAndroidService : public QCoreApplication +{ + Q_OBJECT + +public: + QAndroidService(int &argc, char **argv +#ifndef Q_QDOC + , int flags = ApplicationFlags +#endif + ); + QAndroidService(int &argc, char **argv, + const std::function<QAndroidBinder*(const QAndroidIntent &intent)> & binder +#ifndef Q_QDOC + , int flags = ApplicationFlags +#endif + ); + virtual ~QAndroidService(); + + virtual QAndroidBinder* onBind(const QAndroidIntent &intent); + +private: + friend class QAndroidServicePrivate; + Q_DISABLE_COPY(QAndroidService) + + QScopedPointer<QAndroidServicePrivate> d; +}; + +class QAndroidActivityCallbackResultReceiver: public QAndroidActivityResultReceiver +{ +public: + QAndroidActivityCallbackResultReceiver(); + void handleActivityResult(int receiverRequestCode, int resultCode, + const QJniObject &intent) override; + void registerCallback(int receiverRequestCode, + std::function<void(int, int, const QJniObject &)> callbackFunc); + + static QAndroidActivityCallbackResultReceiver *instance(); +private: + QMap<int, std::function<void(int, int, const QJniObject &data)>> callbackMap; + + static QAndroidActivityCallbackResultReceiver *s_instance; +}; + +namespace QtAndroidPrivate +{ + Q_CORE_EXPORT void startIntentSender(const QJniObject &intentSender, + int receiverRequestCode, + QAndroidActivityResultReceiver *resultReceiver = nullptr); + Q_CORE_EXPORT void startActivity(const QJniObject &intent, + int receiverRequestCode, + QAndroidActivityResultReceiver *resultReceiver = nullptr); + Q_CORE_EXPORT void startActivity(const QAndroidIntent &intent, + int receiverRequestCode, + QAndroidActivityResultReceiver *resultReceiver = nullptr); + Q_CORE_EXPORT void startActivity(const QJniObject &intent, + int receiverRequestCode, + std::function<void(int, int, const QJniObject &data)> + callbackFunc); + + enum class BindFlag { + None = 0x00000000, + AutoCreate = 0x00000001, + DebugUnbind = 0x00000002, + NotForeground = 0x00000004, + AboveClient = 0x00000008, + AllowOomManagement = 0x00000010, + WaivePriority = 0x00000020, + Important = 0x00000040, + AdjustWithActivity = 0x00000080, + ExternalService = -2147483648 // 0x80000000 + + }; + Q_DECLARE_FLAGS(BindFlags, BindFlag) + + Q_CORE_EXPORT bool bindService(const QAndroidIntent &serviceIntent, + const QAndroidServiceConnection &serviceConnection, + BindFlags flags = BindFlag::None); + +#if QT_CONFIG(future) + enum PermissionResult { + Undetermined, + Authorized, + Denied + }; + + Q_CORE_EXPORT QFuture<QtAndroidPrivate::PermissionResult> + requestPermission(const QString &permission); + QFuture<QtAndroidPrivate::PermissionResult> + requestPermissions(const QStringList &permissions); + Q_CORE_EXPORT QFuture<QtAndroidPrivate::PermissionResult> + checkPermission(const QString &permission); +#endif + +} + +QT_END_NAMESPACE + +#endif // QANDROIDEXTRAS_H diff --git a/src/corelib/platform/android/qandroidnativeinterface.cpp b/src/corelib/platform/android/qandroidnativeinterface.cpp index 74d21c5d4c..fc3a09c78b 100644 --- a/src/corelib/platform/android/qandroidnativeinterface.cpp +++ b/src/corelib/platform/android/qandroidnativeinterface.cpp @@ -1,47 +1,36 @@ -/**************************************************************************** -** -** Copyright (C) 2021 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ - -#include <QtCore/qcoreapplication.h> +// Copyright (C) 2021 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 <QtCore/qcoreapplication_platform.h> + +#include <QtCore/private/qnativeinterface_p.h> #include <QtCore/private/qjnihelpers_p.h> +#include <QtCore/qjniobject.h> +#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT) +#include <QtCore/qfuture.h> +#include <QtCore/qfuturewatcher.h> +#include <QtCore/qpromise.h> +#include <QtCore/qtimer.h> +#include <QtCore/qthreadpool.h> +#include <deque> +#include <memory> +#endif QT_BEGIN_NAMESPACE +#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT) +static const char qtNativeClassName[] = "org/qtproject/qt/android/QtNative"; + +struct PendingRunnable { + std::function<QVariant()> function; + std::shared_ptr<QPromise<QVariant>> promise; +}; + +using PendingRunnables = std::deque<PendingRunnable>; +Q_GLOBAL_STATIC(PendingRunnables, g_pendingRunnables); +Q_CONSTINIT static QBasicMutex g_pendingRunnablesMutex; +#endif + /*! \class QNativeInterface::QAndroidApplication \since 6.2 @@ -57,14 +46,14 @@ QT_BEGIN_NAMESPACE QT_DEFINE_NATIVE_INTERFACE(QAndroidApplication); /*! - \fn jobject QNativeInterface::QAndroidApplication::context() + \fn QJniObject QNativeInterface::QAndroidApplication::context() - Returns the Android context as a \c jobject. The context is an \c Activity + Returns the Android context as a \c QJniObject. The context is an \c Activity if the main activity object is valid. Otherwise, the context is a \c Service. \since 6.2 */ -jobject QNativeInterface::QAndroidApplication::context() +QtJniTypes::Context QNativeInterface::QAndroidApplication::context() { return QtAndroidPrivate::context(); } @@ -79,7 +68,176 @@ jobject QNativeInterface::QAndroidApplication::context() */ bool QNativeInterface::QAndroidApplication::isActivityContext() { - return QtAndroidPrivate::activity(); + return QtAndroidPrivate::activity().isValid(); +} + +/*! + \fn int QNativeInterface::QAndroidApplication::sdkVersion() + + Returns the Android SDK version. This is also known as the API level. + + \since 6.2 +*/ +int QNativeInterface::QAndroidApplication::sdkVersion() +{ + return QtAndroidPrivate::androidSdkVersion(); +} + +/*! + \fn void QNativeInterface::QAndroidApplication::hideSplashScreen(int duration) + + Hides the splash screen by using a fade effect for the given \a duration. + If \a duration is not provided (default is 0) the splash screen is hidden + immedetiately after the app starts. + + \since 6.2 +*/ +void QNativeInterface::QAndroidApplication::hideSplashScreen(int duration) +{ + QtAndroidPrivate::activity().callMethod<void>("hideSplashScreen", duration); +} + +/*! + Posts the function \a runnable to the Android thread. The function will be + queued and executed on the Android UI thread. If the call is made on the + Android UI thread \a runnable will be executed immediately. If the Android + app is paused or the main Activity is null, \c runnable is added to the + Android main thread's queue. + + This call returns a QFuture<QVariant> which allows doing both synchronous + and asynchronous calls, and can handle any return type. However, to get + a result back from the QFuture::result(), QVariant::value() should be used. + + If the \a runnable execution takes longer than the period of \a timeout, + the blocking calls \l QFuture::waitForFinished() and \l QFuture::result() + are ended once \a timeout has elapsed. However, if \a runnable has already + started execution, it won't be cancelled. + + The following example shows how to run an asynchronous call that expects + a return type: + + \code + auto task = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() { + QJniObject surfaceView; + if (!surfaceView.isValid()) + qDebug() << "SurfaceView object is not valid yet"; + + surfaceView = QJniObject("android/view/SurfaceView", + "(Landroid/content/Context;)V", + QNativeInterface::QAndroidApplication::context()); + + return QVariant::fromValue(surfaceView); + }).then([](QFuture<QVariant> future) { + auto surfaceView = future.result().value<QJniObject>(); + if (surfaceView.isValid()) + qDebug() << "Retrieved SurfaceView object is valid"; + }); + \endcode + + The following example shows how to run a synchronous call with a void + return type: + + \code + QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]() { + QJniObject activity = QNativeInterface::QAndroidApplication::context(); + // Hide system ui elements or go full screen + activity.callObjectMethod("getWindow", "()Landroid/view/Window;") + .callObjectMethod("getDecorView", "()Landroid/view/View;") + .callMethod<void>("setSystemUiVisibility", "(I)V", 0xffffffff); + }).waitForFinished(); + \endcode + + \note Becareful about the type of operations you do on the Android's main + thread, as any long operation can block the app's UI rendering and input + handling. If the function is expected to have long execution time, it's + also good to use a \l QDeadlineTimer in your \a runnable to manage + the execution and make sure it doesn't block the UI thread. Usually, + any operation longer than 5 seconds might block the app's UI. For more + information, see \l {Android: Keeping your app responsive}{Keeping your app responsive}. + + \since 6.2 +*/ +#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT) +QFuture<QVariant> QNativeInterface::QAndroidApplication::runOnAndroidMainThread( + const std::function<QVariant()> &runnable, + const QDeadlineTimer timeout) +{ + auto promise = std::make_shared<QPromise<QVariant>>(); + QFuture<QVariant> future = promise->future(); + promise->start(); + + if (!timeout.isForever()) { + QThreadPool::globalInstance()->start([=]() mutable { + QEventLoop loop; + QTimer::singleShot(timeout.remainingTime(), &loop, [&]() { + future.cancel(); + promise->finish(); + loop.quit(); + }); + + QFutureWatcher<QVariant> watcher; + QObject::connect(&watcher, &QFutureWatcher<QVariant>::finished, &loop, [&]() { + loop.quit(); + }); + QObject::connect(&watcher, &QFutureWatcher<QVariant>::canceled, &loop, [&]() { + loop.quit(); + }); + watcher.setFuture(future); + + // we're going to sleep, make sure we don't block + // QThreadPool::globalInstance(): + + QThreadPool::globalInstance()->releaseThread(); + const auto sg = qScopeGuard([] { + QThreadPool::globalInstance()->reserveThread(); + }); + loop.exec(); + }); + } + + QMutexLocker locker(&g_pendingRunnablesMutex); +#ifdef __cpp_aggregate_paren_init + g_pendingRunnables->emplace_back(runnable, std::move(promise)); +#else + g_pendingRunnables->push_back({runnable, std::move(promise)}); +#endif + locker.unlock(); + + QJniObject::callStaticMethod<void>(qtNativeClassName, + "runPendingCppRunnablesOnAndroidThread", + "()V"); + return future; +} + +// function called from Java from Android UI thread +static void runPendingCppRunnables(JNIEnv */*env*/, jobject /*obj*/) +{ + // run all posted runnables + for (;;) { + QMutexLocker locker(&g_pendingRunnablesMutex); + if (g_pendingRunnables->empty()) + break; + + PendingRunnable r = std::move(g_pendingRunnables->front()); + g_pendingRunnables->pop_front(); + locker.unlock(); + + // run the runnable outside the sync block! + if (!r.promise->isCanceled()) + r.promise->addResult(r.function()); + r.promise->finish(); + } +} +#endif + +bool QtAndroidPrivate::registerNativeInterfaceNatives(QJniEnvironment &env) +{ +#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT) + const JNINativeMethod methods = {"runPendingCppRunnables", "()V", (void *)runPendingCppRunnables}; + return env.registerNativeMethods(qtNativeClassName, &methods, 1); +#else + return true; +#endif } QT_END_NAMESPACE diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin.mm new file mode 100644 index 0000000000..5c527f396c --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin.mm @@ -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" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm new file mode 100644 index 0000000000..0cd375561f --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_bluetooth.mm @@ -0,0 +1,86 @@ +// 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 +{ + auto status = CBCentralManager.authorization; + switch (status) { + case CBManagerAuthorizationNotDetermined: + return Qt::PermissionStatus::Undetermined; + case CBManagerAuthorizationRestricted: + case CBManagerAuthorizationDenied: + return Qt::PermissionStatus::Denied; + case CBManagerAuthorizationAllowedAlways: + return Qt::PermissionStatus::Granted; + } + + qCWarning(lcPermissions) << "Unknown permission status" << status << "detected in" << self; + return Qt::PermissionStatus::Denied; +} + +- (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" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm new file mode 100644 index 0000000000..a3eddd6d8f --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_calendar.mm @@ -0,0 +1,71 @@ +// 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> + +@interface QDarwinCalendarPermissionHandler () +@property (nonatomic, retain) EKEventStore *eventStore; +@end + +@implementation QDarwinCalendarPermissionHandler +- (Qt::PermissionStatus)checkPermission:(QPermission)permission +{ + Q_UNUSED(permission); + return [self currentStatus]; +} + +- (Qt::PermissionStatus)currentStatus +{ + auto status = [EKEventStore authorizationStatusForEntityType:EKEntityTypeEvent]; + switch (status) { + case EKAuthorizationStatusNotDetermined: + return Qt::PermissionStatus::Undetermined; + case EKAuthorizationStatusRestricted: + case EKAuthorizationStatusDenied: + return Qt::PermissionStatus::Denied; + case EKAuthorizationStatusAuthorized: + return Qt::PermissionStatus::Granted; +#if QT_MACOS_IOS_PLATFORM_SDK_EQUAL_OR_ABOVE(140000, 170000) + case EKAuthorizationStatusWriteOnly: + // FIXME: Add WriteOnly AccessMode + return Qt::PermissionStatus::Denied; +#endif + } + + qCWarning(lcPermissions) << "Unknown permission status" << status << "detected in" << self; + return Qt::PermissionStatus::Denied; +} + +- (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" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm new file mode 100644 index 0000000000..51c517d6f3 --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_camera.mm @@ -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 diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm new file mode 100644 index 0000000000..3221b6dc1d --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_contacts.mm @@ -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" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm new file mode 100644 index 0000000000..1d32c0fcac --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_location.mm @@ -0,0 +1,263 @@ +// 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.value<QLocationPermission>(); + + auto status = [self authorizationStatus:locationPermission]; + if (status != Qt::PermissionStatus::Granted) + return status; + + return [self accuracyAuthorization:locationPermission]; +} + +- (Qt::PermissionStatus)authorizationStatus:(QLocationPermission)permission +{ + NSString *bundleIdentifier = NSBundle.mainBundle.bundleIdentifier; + if (!bundleIdentifier || !bundleIdentifier.length) { + qCWarning(lcLocationPermission) << "Missing bundle identifier" + << "in Info.plist. Can not use location permissions."; + return Qt::PermissionStatus::Denied; + } + +#if defined(Q_OS_VISIONOS) + if (permission.availability() == QLocationPermission::Always) + return Qt::PermissionStatus::Denied; +#endif + + auto status = [self authorizationStatus]; + switch (status) { + case kCLAuthorizationStatusRestricted: + case kCLAuthorizationStatusDenied: + return Qt::PermissionStatus::Denied; + case kCLAuthorizationStatusNotDetermined: + return Qt::PermissionStatus::Undetermined; +#if !defined(Q_OS_VISIONOS) + case kCLAuthorizationStatusAuthorizedAlways: + return Qt::PermissionStatus::Granted; +#endif +#if defined(Q_OS_IOS) || defined(Q_OS_VISIONOS) + case kCLAuthorizationStatusAuthorizedWhenInUse: + if (permission.availability() == QLocationPermission::Always) + return Qt::PermissionStatus::Denied; + return Qt::PermissionStatus::Granted; +#endif + } + + qCWarning(lcPermissions) << "Unknown permission status" << status << "detected in" << self; + return Qt::PermissionStatus::Denied; +} + +- (CLAuthorizationStatus)authorizationStatus +{ + if (self.manager) { + if (@available(macOS 11, iOS 14, *)) + return self.manager.authorizationStatus; + } + + return QT_IGNORE_DEPRECATIONS(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; + } + + qCWarning(lcPermissions) << "Unknown accuracy status" << status << "detected in" << self; + return Qt::PermissionStatus::Denied; +} + +- (QStringList)usageDescriptionsFor:(QPermission)permission +{ +#if defined(Q_OS_MACOS) + return { "NSLocationUsageDescription" }; +#else // iOS 11 and above + QStringList usageDescriptions = { "NSLocationWhenInUseUsageDescription" }; + const auto locationPermission = *permission.value<QLocationPermission>(); + if (locationPermission.availability() == QLocationPermission::Always) + usageDescriptions << "NSLocationAlwaysAndWhenInUseUsageDescription"; + return usageDescriptions; +#endif +} + +- (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.value<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: +#if defined(Q_OS_VISIONOS) + [self deliverResult]; // Not supported +#else + // 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: + [self.manager requestAlwaysAuthorization]; + break; +#ifdef Q_OS_IOS + case kCLAuthorizationStatusAuthorizedWhenInUse: + // Unfortunately when asking for AlwaysAuthorization when in + // the WhenInUse state, to "upgrade" the permission, the iOS + // location system will not give us a callback if the user + // denies the request, leaving us hanging without a way to + // respond to the permission request. + qCWarning(lcLocationPermission) << "QLocationPermission::WhenInUse" + << "can not be upgraded to QLocationPermission::Always on iOS." + << "Please request QLocationPermission::Always directly."; + Q_FALLTHROUGH(); +#endif + default: + [self deliverResult]; + } +#endif + 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" diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm b/src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm new file mode 100644 index 0000000000..5dc434309d --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_microphone.mm @@ -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 diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_p.h b/src/corelib/platform/darwin/qdarwinpermissionplugin_p.h new file mode 100644 index 0000000000..03530133ad --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_p.h @@ -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 diff --git a/src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h b/src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h new file mode 100644 index 0000000000..649af06507 --- /dev/null +++ b/src/corelib/platform/darwin/qdarwinpermissionplugin_p_p.h @@ -0,0 +1,104 @@ +// 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; + } + qCWarning(lcPermissions) << "Unknown permission status" << status << "detected in" + << QT_STRINGIFY(QT_DARWIN_PERMISSION_PLUGIN); + return Qt::PermissionStatus::Denied; +} +} // 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 diff --git a/src/corelib/platform/ios/PrivacyInfo.xcprivacy b/src/corelib/platform/ios/PrivacyInfo.xcprivacy new file mode 100644 index 0000000000..5f84a229a5 --- /dev/null +++ b/src/corelib/platform/ios/PrivacyInfo.xcprivacy @@ -0,0 +1,31 @@ +<?xml version="1.0" encoding="UTF-8"?> +<!DOCTYPE plist PUBLIC "-//Apple//DTD PLIST 1.0//EN" "http://www.apple.com/DTDs/PropertyList-1.0.dtd"> +<plist version="1.0"> +<dict> + <key>NSPrivacyTracking</key> + <false/> + <key>NSPrivacyCollectedDataTypes</key> + <array/> + <key>NSPrivacyTrackingDomains</key> + <array/> + <key>NSPrivacyAccessedAPITypes</key> + <array> + <dict> + <key>NSPrivacyAccessedAPIType</key> + <string>NSPrivacyAccessedAPICategoryFileTimestamp</string> + <key>NSPrivacyAccessedAPITypeReasons</key> + <array> + <string>0A2A.1</string> <!-- QFileInfo --> + </array> + </dict> + <dict> + <key>NSPrivacyAccessedAPIType</key> + <string>NSPrivacyAccessedAPICategoryDiskSpace</string> + <key>NSPrivacyAccessedAPITypeReasons</key> + <array> + <string>85F4.1</string> <!-- QStorageInfo --> + </array> + </dict> + </array> +</dict> +</plist> diff --git a/src/corelib/platform/wasm/qstdweb.cpp b/src/corelib/platform/wasm/qstdweb.cpp index 198ce897ca..75e76a6806 100644 --- a/src/corelib/platform/wasm/qstdweb.cpp +++ b/src/corelib/platform/wasm/qstdweb.cpp @@ -1,53 +1,384 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2019 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 "qstdweb_p.h" +#include <QtCore/qcoreapplication.h> +#include <QtCore/qfile.h> +#include <QtCore/qmimedata.h> + #include <emscripten/bind.h> +#include <emscripten/emscripten.h> +#include <emscripten/html5.h> +#include <emscripten/threading.h> + #include <cstdint> #include <iostream> +#include <unordered_map> + QT_BEGIN_NAMESPACE +using namespace Qt::Literals::StringLiterals; + namespace qstdweb { +static void usePotentialyUnusedSymbols() +{ + // Using this adds a reference on JSEvents and specialHTMLTargets which are always exported. + // This hack is needed as it is currently impossible to specify a dollar sign in + // target_link_options. The following is impossible: + // DEFAULT_LIBRARY_FUNCS_TO_INCLUDE=$JSEvents + // TODO(mikolajboc): QTBUG-108444, review this when cmake gets fixed. + // Volatile is to make this unoptimizable, so that the function is referenced, but is not + // called at runtime. + volatile bool doIt = false; + if (doIt) + emscripten_set_wheel_callback(EMSCRIPTEN_EVENT_TARGET_WINDOW, 0, 0, NULL); +} + +Q_CONSTRUCTOR_FUNCTION(usePotentialyUnusedSymbols) typedef double uint53_t; // see Number.MAX_SAFE_INTEGER +namespace { +// Reads file in chunks in order to avoid holding two copies in memory at the same time +struct ChunkedFileReader +{ +public: + static void read(File file, char *buffer, uint32_t offset, uint32_t end, + std::function<void()> onCompleted) + { + (new ChunkedFileReader(end, std::move(onCompleted), std::move(file))) + ->readNextChunk(offset, buffer); + } + +private: + ChunkedFileReader(uint32_t end, std::function<void()> onCompleted, File file) + : end(end), onCompleted(std::move(onCompleted)), file(std::move(file)) + { + } + + void readNextChunk(uint32_t chunkBegin, char *chunkBuffer) + { + // Copy current chunk from JS memory to Wasm memory + qstdweb::ArrayBuffer result = fileReader.result(); + qstdweb::Uint8Array(result).copyTo(chunkBuffer); + + // Read next chunk if not at buffer end + const uint32_t nextChunkBegin = std::min(chunkBegin + result.byteLength(), end); + if (nextChunkBegin == end) { + onCompleted(); + delete this; + return; + } + char *nextChunkBuffer = chunkBuffer + result.byteLength(); + fileReader.onLoad([this, nextChunkBegin, nextChunkBuffer](emscripten::val) { + readNextChunk(nextChunkBegin, nextChunkBuffer); + }); + const uint32_t nextChunkEnd = std::min(nextChunkBegin + chunkSize, end); + qstdweb::Blob blob = file.slice(nextChunkBegin, nextChunkEnd); + fileReader.readAsArrayBuffer(blob); + } + + static constexpr uint32_t chunkSize = 256 * 1024; + + qstdweb::FileReader fileReader; + uint32_t end; + std::function<void()> onCompleted; + File file; +}; + +enum class CallbackType { + Then, + Catch, + Finally, +}; + +void validateCallbacks(const PromiseCallbacks& callbacks) { + Q_ASSERT(!!callbacks.catchFunc || !!callbacks.finallyFunc || !!callbacks.thenFunc); +} + +using ThunkId = int; + +#define THUNK_NAME(type, i) callbackThunk##type##i + +// A resource pool for exported promise thunk functions. ThunkPool::poolSize sets of +// 3 promise thunks (then, catch, finally) are exported and can be used by promises +// in C++. To allocate a thunk, call allocateThunk. When a thunk is ready for use, +// a callback with allocation RAII object ThunkAllocation will be returned. Deleting +// the object frees the thunk and automatically makes any pending allocateThunk call +// run its callback with a free thunk slot. +class ThunkPool { +public: + static constexpr size_t poolSize = 4; + + // An allocation for a thunk function set. Following the RAII pattern, destruction of + // this objects frees a corresponding thunk pool entry. + // To actually make the thunks react to a js promise's callbacks, call bindToPromise. + class ThunkAllocation { + public: + ThunkAllocation(int thunkId, ThunkPool* pool) : m_thunkId(thunkId), m_pool(pool) {} + ~ThunkAllocation() { + m_pool->free(m_thunkId); + } + + // The id of the underlaying thunk set + int id() const { return m_thunkId; } + + // Binds the corresponding thunk set to the js promise 'target'. + void bindToPromise(emscripten::val target, const PromiseCallbacks& callbacks) { + using namespace emscripten; + + if (Q_LIKELY(callbacks.thenFunc)) { + target = target.call<val>( + "then", + emscripten::val::module_property(thunkName(CallbackType::Then, id()).data())); + } + if (callbacks.catchFunc) { + target = target.call<val>( + "catch", + emscripten::val::module_property(thunkName(CallbackType::Catch, id()).data())); + } + // Guarantee the invocation of at least one callback by always + // registering 'finally'. This is required by WebPromiseManager + // design + target = target.call<val>( + "finally", emscripten::val::module_property( + thunkName(CallbackType::Finally, id()).data())); + } + + private: + int m_thunkId; + ThunkPool* m_pool; + }; + + ThunkPool() { + std::iota(m_free.begin(), m_free.end(), 0); + } + + void setThunkCallback(std::function<void(int, CallbackType, emscripten::val)> callback) { + m_callback = std::move(callback); + } + + void allocateThunk(std::function<void(std::unique_ptr<ThunkAllocation>)> onAllocated) { + if (m_free.empty()) { + m_pendingAllocations.push_back(std::move(onAllocated)); + return; + } + + const int thunkId = m_free.back(); + m_free.pop_back(); + onAllocated(std::make_unique<ThunkAllocation>(thunkId, this)); + } + + static QByteArray thunkName(CallbackType type, size_t i) { + return QStringLiteral("promiseCallback%1%2").arg([type]() -> QString { + switch (type) { + case CallbackType::Then: + return QStringLiteral("Then"); + case CallbackType::Catch: + return QStringLiteral("Catch"); + case CallbackType::Finally: + return QStringLiteral("Finally"); + } + }()).arg(i).toLatin1(); + } + + static ThunkPool* get(); + +#define THUNK(i) \ + static void THUNK_NAME(Then, i)(emscripten::val result) \ + { \ + get()->onThunkCalled(i, CallbackType::Then, std::move(result)); \ + } \ + static void THUNK_NAME(Catch, i)(emscripten::val result) \ + { \ + get()->onThunkCalled(i, CallbackType::Catch, std::move(result)); \ + } \ + static void THUNK_NAME(Finally, i)() \ + { \ + get()->onThunkCalled(i, CallbackType::Finally, emscripten::val::undefined()); \ + } + + THUNK(0); + THUNK(1); + THUNK(2); + THUNK(3); + +#undef THUNK + +private: + void onThunkCalled(int index, CallbackType type, emscripten::val result) { + m_callback(index, type, std::move(result)); + } + + void free(int thunkId) { + if (m_pendingAllocations.empty()) { + // Return the thunk to the free pool + m_free.push_back(thunkId); + return; + } + + // Take the next enqueued allocation and reuse the thunk + auto allocation = m_pendingAllocations.back(); + m_pendingAllocations.pop_back(); + allocation(std::make_unique<ThunkAllocation>(thunkId, this)); + } + + std::function<void(int, CallbackType, emscripten::val)> m_callback; + + std::vector<int> m_free = std::vector<int>(poolSize); + std::vector<std::function<void(std::unique_ptr<ThunkAllocation>)>> m_pendingAllocations; +}; + +Q_GLOBAL_STATIC(ThunkPool, g_thunkPool) + +ThunkPool* ThunkPool::get() +{ + return g_thunkPool; +} + +#define CALLBACK_BINDING(i) \ + emscripten::function(ThunkPool::thunkName(CallbackType::Then, i).data(), \ + &ThunkPool::THUNK_NAME(Then, i)); \ + emscripten::function(ThunkPool::thunkName(CallbackType::Catch, i).data(), \ + &ThunkPool::THUNK_NAME(Catch, i)); \ + emscripten::function(ThunkPool::thunkName(CallbackType::Finally, i).data(), \ + &ThunkPool::THUNK_NAME(Finally, i)); + +EMSCRIPTEN_BINDINGS(qtThunkPool) { + CALLBACK_BINDING(0) + CALLBACK_BINDING(1) + CALLBACK_BINDING(2) + CALLBACK_BINDING(3) +} + +#undef CALLBACK_BINDING +#undef THUNK_NAME + +class WebPromiseManager +{ +public: + WebPromiseManager(); + ~WebPromiseManager(); + + WebPromiseManager(const WebPromiseManager& other) = delete; + WebPromiseManager(WebPromiseManager&& other) = delete; + WebPromiseManager& operator=(const WebPromiseManager& other) = delete; + WebPromiseManager& operator=(WebPromiseManager&& other) = delete; + + void adoptPromise(emscripten::val target, PromiseCallbacks callbacks); + + static WebPromiseManager* get(); + +private: + struct RegistryEntry { + PromiseCallbacks callbacks; + std::unique_ptr<ThunkPool::ThunkAllocation> allocation; + }; + + static std::optional<CallbackType> parseCallbackType(emscripten::val callbackType); + + void subscribeToJsPromiseCallbacks(int i, const PromiseCallbacks& callbacks, emscripten::val jsContextfulPromise); + void promiseThunkCallback(int i, CallbackType type, emscripten::val result); + + void registerPromise(std::unique_ptr<ThunkPool::ThunkAllocation> allocation, PromiseCallbacks promise); + void unregisterPromise(ThunkId context); + + std::array<RegistryEntry, ThunkPool::poolSize> m_promiseRegistry; +}; + +Q_GLOBAL_STATIC(WebPromiseManager, webPromiseManager) + +WebPromiseManager::WebPromiseManager() +{ + ThunkPool::get()->setThunkCallback(std::bind( + &WebPromiseManager::promiseThunkCallback, this, + std::placeholders::_1, std::placeholders::_2, std::placeholders::_3)); +} + +std::optional<CallbackType> +WebPromiseManager::parseCallbackType(emscripten::val callbackType) +{ + if (!callbackType.isString()) + return std::nullopt; + + const std::string data = callbackType.as<std::string>(); + if (data == "then") + return CallbackType::Then; + if (data == "catch") + return CallbackType::Catch; + if (data == "finally") + return CallbackType::Finally; + return std::nullopt; +} + +WebPromiseManager::~WebPromiseManager() = default; + +WebPromiseManager *WebPromiseManager::get() +{ + return webPromiseManager(); +} + +void WebPromiseManager::promiseThunkCallback(int context, CallbackType type, emscripten::val result) +{ + auto* promiseState = &m_promiseRegistry[context]; + + auto* callbacks = &promiseState->callbacks; + switch (type) { + case CallbackType::Then: + callbacks->thenFunc(result); + break; + case CallbackType::Catch: + callbacks->catchFunc(result); + break; + case CallbackType::Finally: + // Final callback may be empty, used solely for promise unregistration + if (callbacks->finallyFunc) { + callbacks->finallyFunc(); + } + unregisterPromise(context); + break; + } +} + +void WebPromiseManager::registerPromise( + std::unique_ptr<ThunkPool::ThunkAllocation> allocation, + PromiseCallbacks callbacks) +{ + const ThunkId id = allocation->id(); + m_promiseRegistry[id] = + RegistryEntry {std::move(callbacks), std::move(allocation)}; +} + +void WebPromiseManager::unregisterPromise(ThunkId context) +{ + m_promiseRegistry[context] = {}; +} + +void WebPromiseManager::adoptPromise(emscripten::val target, PromiseCallbacks callbacks) { + ThunkPool::get()->allocateThunk([=](std::unique_ptr<ThunkPool::ThunkAllocation> allocation) { + allocation->bindToPromise(std::move(target), callbacks); + registerPromise(std::move(allocation), std::move(callbacks)); + }); +} +#if defined(QT_STATIC) + +EM_JS(bool, jsHaveAsyncify, (), { return typeof Asyncify !== "undefined"; }); +EM_JS(bool, jsHaveJspi, (), + { return typeof Asyncify !== "undefined" && !!Asyncify.makeAsyncFunction && !!WebAssembly.Function; }); + +#else + +bool jsHaveAsyncify() { return false; } +bool jsHaveJspi() { return false; } + +#endif +} // namespace + +ArrayBuffer::ArrayBuffer(uint32_t size) +{ + m_arrayBuffer = emscripten::val::global("ArrayBuffer").new_(size); +} ArrayBuffer::ArrayBuffer(const emscripten::val &arrayBuffer) :m_arrayBuffer(arrayBuffer) @@ -63,23 +394,92 @@ uint32_t ArrayBuffer::byteLength() const return m_arrayBuffer["byteLength"].as<uint32_t>(); } +ArrayBuffer ArrayBuffer::slice(uint32_t begin, uint32_t end) const +{ + return ArrayBuffer(m_arrayBuffer.call<emscripten::val>("slice", begin, end)); +} + +emscripten::val ArrayBuffer::val() const +{ + return m_arrayBuffer; +} + Blob::Blob(const emscripten::val &blob) :m_blob(blob) { } +Blob Blob::fromArrayBuffer(const ArrayBuffer &arrayBuffer) +{ + auto array = emscripten::val::array(); + array.call<void>("push", arrayBuffer.val()); + return Blob(emscripten::val::global("Blob").new_(array)); +} + uint32_t Blob::size() const { return m_blob["size"].as<uint32_t>(); } +Blob Blob::copyFrom(const char *buffer, uint32_t size, std::string mimeType) +{ + Uint8Array contentCopy = Uint8Array::copyFrom(buffer, size); + + emscripten::val contentArray = emscripten::val::array(); + contentArray.call<void>("push", contentCopy.val()); + emscripten::val type = emscripten::val::object(); + type.set("type", std::move(mimeType)); + return Blob(emscripten::val::global("Blob").new_(contentArray, type)); +} + +// Copies content from the given buffer into a Blob object +Blob Blob::copyFrom(const char *buffer, uint32_t size) +{ + return copyFrom(buffer, size, "application/octet-stream"); +} + +Blob Blob::slice(uint32_t begin, uint32_t end) const +{ + return Blob(m_blob.call<emscripten::val>("slice", begin, end)); +} + +ArrayBuffer Blob::arrayBuffer_sync() const +{ + QEventLoop loop; + emscripten::val buffer; + qstdweb::Promise::make(m_blob, QStringLiteral("arrayBuffer"), { + .thenFunc = [&loop, &buffer](emscripten::val arrayBuffer) { + buffer = arrayBuffer; + loop.quit(); + } + }); + loop.exec(); + return ArrayBuffer(buffer); +} + +emscripten::val Blob::val() const +{ + return m_blob; +} + File::File(const emscripten::val &file) :m_file(file) { } +File::~File() = default; + +File::File(const File &other) = default; + +File::File(File &&other) = default; + +File &File::operator=(const File &other) = default; + +File &File::operator=(File &&other) = default; + + Blob File::slice(uint64_t begin, uint64_t end) const { return Blob(m_file.call<emscripten::val>("slice", uint53_t(begin), uint53_t(end))); @@ -95,6 +495,52 @@ uint64_t File::size() const return uint64_t(m_file["size"].as<uint53_t>()); } +std::string Blob::type() const +{ + return m_blob["type"].as<std::string>(); +} + +// Streams partial file content into the given buffer asynchronously. The completed +// callback is called on completion. +void File::stream(uint32_t offset, uint32_t length, char *buffer, + std::function<void()> completed) const +{ + ChunkedFileReader::read(*this, buffer, offset, offset + length, std::move(completed)); +} + +// Streams file content into the given buffer asynchronously. The completed +// callback is called on completion. +void File::stream(char *buffer, std::function<void()> completed) const +{ + stream(0, size(), buffer, std::move(completed)); +} + +std::string File::type() const +{ + return m_file["type"].as<std::string>(); +} + +emscripten::val File::val() const +{ + return m_file; +} + +FileUrlRegistration::FileUrlRegistration(File file) +{ + m_path = QString::fromStdString(emscripten::val::global("window")["URL"].call<std::string>( + "createObjectURL", file.file())); +} + +FileUrlRegistration::~FileUrlRegistration() +{ + emscripten::val::global("window")["URL"].call<void>("revokeObjectURL", + emscripten::val(m_path.toStdString())); +} + +FileUrlRegistration::FileUrlRegistration(FileUrlRegistration &&other) = default; + +FileUrlRegistration &FileUrlRegistration::operator=(FileUrlRegistration &&other) = default; + FileList::FileList(const emscripten::val &fileList) :m_fileList(fileList) { @@ -116,6 +562,11 @@ File FileList::operator[](int index) const return item(index); } +emscripten::val FileList::val() const +{ + return m_fileList; +} + ArrayBuffer FileReader::result() const { return ArrayBuffer(m_fileReader["result"]); @@ -126,19 +577,27 @@ void FileReader::readAsArrayBuffer(const Blob &blob) const m_fileReader.call<void>("readAsArrayBuffer", blob.m_blob); } -void FileReader::onLoad(const std::function<void ()> &onLoad) +void FileReader::onLoad(const std::function<void(emscripten::val)> &onLoad) { - m_onLoad.reset(new EventCallback(m_fileReader, "load", onLoad)); + m_onLoad.reset(); + m_onLoad = std::make_unique<EventCallback>(m_fileReader, "load", onLoad); } -void FileReader::onError(const std::function<void ()> &onError) +void FileReader::onError(const std::function<void(emscripten::val)> &onError) { - m_onError.reset(new EventCallback(m_fileReader, "error", onError)); + m_onError.reset(); + m_onError = std::make_unique<EventCallback>(m_fileReader, "error", onError); } -void FileReader::onAbort(const std::function<void ()> &onAbort) +void FileReader::onAbort(const std::function<void(emscripten::val)> &onAbort) { - m_onAbort.reset(new EventCallback(m_fileReader, "abort", onAbort)); + m_onAbort.reset(); + m_onAbort = std::make_unique<EventCallback>(m_fileReader, "abort", onAbort); +} + +emscripten::val FileReader::val() const +{ + return m_fileReader; } Uint8Array Uint8Array::heap() @@ -146,26 +605,37 @@ Uint8Array Uint8Array::heap() return Uint8Array(heap_()); } +// Constructs a Uint8Array which references the given emscripten::val, which must contain a JS Unit8Array Uint8Array::Uint8Array(const emscripten::val &uint8Array) : m_uint8Array(uint8Array) { } +// Constructs a Uint8Array which references an ArrayBuffer Uint8Array::Uint8Array(const ArrayBuffer &buffer) : m_uint8Array(Uint8Array::constructor_().new_(buffer.m_arrayBuffer)) { } +// Constructs a Uint8Array which references a view into an ArrayBuffer Uint8Array::Uint8Array(const ArrayBuffer &buffer, uint32_t offset, uint32_t length) : m_uint8Array(Uint8Array::constructor_().new_(buffer.m_arrayBuffer, offset, length)) { } -Uint8Array::Uint8Array(char *buffer, uint32_t size) -:m_uint8Array(Uint8Array::constructor_().new_(Uint8Array::heap().buffer().m_arrayBuffer, uint32_t(buffer), size)) +// Constructs a Uint8Array which references an area on the heap. +Uint8Array::Uint8Array(const char *buffer, uint32_t size) +:m_uint8Array(Uint8Array::constructor_().new_(Uint8Array::heap().buffer().m_arrayBuffer, uintptr_t(buffer), size)) +{ + +} + +// Constructs a Uint8Array which allocates and references a new ArrayBuffer with the given size. +Uint8Array::Uint8Array(uint32_t size) +: m_uint8Array(Uint8Array::constructor_().new_(size)) { } @@ -185,16 +655,56 @@ void Uint8Array::set(const Uint8Array &source) m_uint8Array.call<void>("set", source.m_uint8Array); // copies source content } +Uint8Array Uint8Array::subarray(uint32_t begin, uint32_t end) +{ + // Note: using uint64_t here errors with "Cannot convert a BigInt value to a number" + // (see JS BigInt and Number types). Use uint32_t for now. + return Uint8Array(m_uint8Array.call<emscripten::val>("subarray", begin, end)); +} + +// Copies the Uint8Array content to a destination on the heap void Uint8Array::copyTo(char *destination) const { Uint8Array(destination, length()).set(*this); } +// Copies the Uint8Array content to a destination QByteArray +QByteArray Uint8Array::copyToQByteArray() const +{ + if (length() > std::numeric_limits<qsizetype>::max()) + return QByteArray(); + + QByteArray destinationArray; + destinationArray.resize(length()); + copyTo(destinationArray.data()); + return destinationArray; +} + +// Copies the Uint8Array content to a destination on the heap void Uint8Array::copy(char *destination, const Uint8Array &source) { Uint8Array(destination, source.length()).set(source); } +// Copies content from a source on the heap to a new Uint8Array object +Uint8Array Uint8Array::copyFrom(const char *buffer, uint32_t size) +{ + Uint8Array contentCopy(size); + contentCopy.set(Uint8Array(buffer, size)); + return contentCopy; +} + +// Copies content from a QByteArray to a new Uint8Array object +Uint8Array Uint8Array::copyFrom(const QByteArray &buffer) +{ + return copyFrom(buffer.constData(), buffer.size()); +} + +emscripten::val Uint8Array::val() const +{ + return m_uint8Array; +} + emscripten::val Uint8Array::heap_() { return emscripten::val::module_property("HEAPU8"); @@ -205,30 +715,221 @@ emscripten::val Uint8Array::constructor_() return emscripten::val::global("Uint8Array"); } +class EventListener { +public: + EventListener(uintptr_t handler) + :m_handler(handler) + { + + } + + // Special function - addEventListender() allows adding an object with a + // handleEvent() function which eceives the event. + void handleEvent(emscripten::val event) { + auto handlerPtr = reinterpret_cast<std::function<void(emscripten::val)> *>(m_handler); + (*handlerPtr)(event); + } + + uintptr_t m_handler; +}; + // Registers a callback function for a named event on the given element. The event // name must be the name as returned by the Event.type property: e.g. "load", "error". -EventCallback::EventCallback(emscripten::val element, const std::string &name, const std::function<void ()> &fn) -:m_fn(fn) +EventCallback::~EventCallback() +{ + m_element.call<void>("removeEventListener", m_eventName, m_eventListener); +} + +EventCallback::EventCallback(emscripten::val element, const std::string &name, const std::function<void(emscripten::val)> &handler) + :m_element(element) + ,m_eventName(name) + ,m_handler(std::make_unique<std::function<void(emscripten::val)>>(handler)) +{ + uintptr_t handlerUint = reinterpret_cast<uintptr_t>(m_handler.get()); // FIXME: pass pointer directly instead + m_eventListener = emscripten::val::module_property("QtEventListener").new_(handlerUint); + m_element.call<void>("addEventListener", m_eventName, m_eventListener); +} + +EMSCRIPTEN_BINDINGS(qtStdwebCalback) { + emscripten::class_<EventListener>("QtEventListener") + .constructor<uintptr_t>() + .function("handleEvent", &EventListener::handleEvent); +} + +namespace Promise { + void adoptPromise(emscripten::val promiseObject, PromiseCallbacks callbacks) { + validateCallbacks(callbacks); + + WebPromiseManager::get()->adoptPromise( + std::move(promiseObject), std::move(callbacks)); + } + + void all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks) { + struct State { + std::map<int, emscripten::val> results; + int remainingThenCallbacks; + int remainingFinallyCallbacks; + }; + + validateCallbacks(callbacks); + + auto state = std::make_shared<State>(); + state->remainingThenCallbacks = state->remainingFinallyCallbacks = promises.size(); + + for (size_t i = 0; i < promises.size(); ++i) { + PromiseCallbacks individualPromiseCallback; + if (callbacks.thenFunc) { + individualPromiseCallback.thenFunc = [i, state, callbacks](emscripten::val partialResult) mutable { + state->results.emplace(i, std::move(partialResult)); + if (!--(state->remainingThenCallbacks)) { + std::vector<emscripten::val> transformed; + for (auto& data : state->results) { + transformed.push_back(std::move(data.second)); + } + callbacks.thenFunc(emscripten::val::array(std::move(transformed))); + } + }; + } + if (callbacks.catchFunc) { + individualPromiseCallback.catchFunc = [state, callbacks](emscripten::val error) mutable { + callbacks.catchFunc(error); + }; + } + individualPromiseCallback.finallyFunc = [state, callbacks]() mutable { + if (!--(state->remainingFinallyCallbacks)) { + if (callbacks.finallyFunc) + callbacks.finallyFunc(); + // Explicitly reset here for verbosity, this would have been done automatically with the + // destruction of the adopted promise in WebPromiseManager. + state.reset(); + } + }; + + adoptPromise(std::move(promises.at(i)), std::move(individualPromiseCallback)); + } + } +} + +// Asyncify and thread blocking: Normally, it's not possible to block the main +// thread, except if asyncify is enabled. Secondary threads can always block. +// +// haveAsyncify(): returns true if the main thread can block on QEventLoop::exec(), +// if either asyncify 1 or 2 (JSPI) is available. +// +// haveJspi(): returns true if asyncify 2 (JSPI) is available. +// +// canBlockCallingThread(): returns true if the calling thread can block on +// QEventLoop::exec(), using either asyncify or as a seconarday thread. +bool haveJspi() { - element.set(contextPropertyName(name).c_str(), emscripten::val(intptr_t(this))); - element.set((std::string("on") + name).c_str(), emscripten::val::module_property("qtStdWebEventCallbackActivate")); + static bool HaveJspi = jsHaveJspi(); + return HaveJspi; } -void EventCallback::activate(emscripten::val event) +bool haveAsyncify() { - emscripten::val target = event["target"]; - std::string eventName = event["type"].as<std::string>(); - EventCallback *that = reinterpret_cast<EventCallback *>(target[contextPropertyName(eventName).c_str()].as<intptr_t>()); - that->m_fn(); + static bool HaveAsyncify = jsHaveAsyncify() || haveJspi(); + return HaveAsyncify; } -std::string EventCallback::contextPropertyName(const std::string &eventName) +bool canBlockCallingThread() { - return std::string("data-qtEventCallbackContext") + eventName; + return haveAsyncify() || !emscripten_is_main_runtime_thread(); } -EMSCRIPTEN_BINDINGS(qtStdwebCalback) { - emscripten::function("qtStdWebEventCallbackActivate", &EventCallback::activate); +BlobIODevice::BlobIODevice(Blob blob) + : m_blob(blob) +{ + +} + +bool BlobIODevice::open(QIODevice::OpenMode mode) +{ + if (mode.testFlag(QIODevice::WriteOnly)) + return false; + return QIODevice::open(mode); +} + +bool BlobIODevice::isSequential() const +{ + return false; +} + +qint64 BlobIODevice::size() const +{ + return m_blob.size(); +} + +bool BlobIODevice::seek(qint64 pos) +{ + if (pos >= size()) + return false; + return QIODevice::seek(pos); +} + +qint64 BlobIODevice::readData(char *data, qint64 maxSize) +{ + uint64_t begin = QIODevice::pos(); + uint64_t end = std::min<uint64_t>(begin + maxSize, size()); + uint64_t size = end - begin; + if (size > 0) { + qstdweb::ArrayBuffer buffer = m_blob.slice(begin, end).arrayBuffer_sync(); + qstdweb::Uint8Array(buffer).copyTo(data); + } + return size; +} + +qint64 BlobIODevice::writeData(const char *, qint64) +{ + Q_UNREACHABLE(); +} + +Uint8ArrayIODevice::Uint8ArrayIODevice(Uint8Array array) + : m_array(array) +{ + +} + +bool Uint8ArrayIODevice::open(QIODevice::OpenMode mode) +{ + return QIODevice::open(mode); +} + +bool Uint8ArrayIODevice::isSequential() const +{ + return false; +} + +qint64 Uint8ArrayIODevice::size() const +{ + return m_array.length(); +} + +bool Uint8ArrayIODevice::seek(qint64 pos) +{ + if (pos >= size()) + return false; + return QIODevice::seek(pos); +} + +qint64 Uint8ArrayIODevice::readData(char *data, qint64 maxSize) +{ + uint64_t begin = QIODevice::pos(); + uint64_t end = std::min<uint64_t>(begin + maxSize, size()); + uint64_t size = end - begin; + if (size > 0) + m_array.subarray(begin, end).copyTo(data); + return size; +} + +qint64 Uint8ArrayIODevice::writeData(const char *data, qint64 maxSize) +{ + uint64_t begin = QIODevice::pos(); + uint64_t end = std::min<uint64_t>(begin + maxSize, size()); + uint64_t size = end - begin; + if (size > 0) + m_array.subarray(begin, end).set(Uint8Array(data, size)); + return size; } } // namespace qstdweb diff --git a/src/corelib/platform/wasm/qstdweb_p.h b/src/corelib/platform/wasm/qstdweb_p.h index 75c2ec34b1..a3b5bd5b6b 100644 --- a/src/corelib/platform/wasm/qstdweb_p.h +++ b/src/corelib/platform/wasm/qstdweb_p.h @@ -1,41 +1,5 @@ -/**************************************************************************** -** -** Copyright (C) 2019 The Qt Company Ltd. -** Contact: https://www.qt.io/licensing/ -** -** This file is part of the QtCore module of the Qt Toolkit. -** -** $QT_BEGIN_LICENSE:LGPL$ -** Commercial License Usage -** Licensees holding valid commercial Qt licenses may use this file in -** accordance with the commercial license agreement provided with the -** Software or, alternatively, in accordance with the terms contained in -** a written agreement between you and The Qt Company. For licensing terms -** and conditions see https://www.qt.io/terms-conditions. For further -** information use the contact form at https://www.qt.io/contact-us. -** -** GNU Lesser General Public License Usage -** Alternatively, this file may be used under the terms of the GNU Lesser -** General Public License version 3 as published by the Free Software -** Foundation and appearing in the file LICENSE.LGPL3 included in the -** packaging of this file. Please review the following information to -** ensure the GNU Lesser General Public License version 3 requirements -** will be met: https://www.gnu.org/licenses/lgpl-3.0.html. -** -** GNU General Public License Usage -** Alternatively, this file may be used under the terms of the GNU -** General Public License version 2.0 or (at your option) the GNU General -** Public license version 3 or any later version approved by the KDE Free -** Qt Foundation. The licenses are as published by the Free Software -** Foundation and appearing in the file LICENSE.GPL2 and LICENSE.GPL3 -** included in the packaging of this file. Please review the following -** information to ensure the GNU General Public License requirements will -** be met: https://www.gnu.org/licenses/gpl-2.0.html and -** https://www.gnu.org/licenses/gpl-3.0.html. -** -** $QT_END_LICENSE$ -** -****************************************************************************/ +// Copyright (C) 2021 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 QSTDWEB_P_H #define QSTDWEB_P_H @@ -51,17 +15,36 @@ // We mean it. // -#include <qglobal.h> +#include <private/qglobal_p.h> +#include <QtCore/qglobal.h> +#include "QtCore/qhash.h" +#include "QtCore/qiodevice.h" + #include <emscripten/val.h> + #include <cstdint> #include <functional> +#include <initializer_list> +#include <memory> +#include <string> +#include <utility> + +#if QT_CONFIG(thread) +#include <emscripten/proxying.h> +#include <emscripten/threading.h> +#endif // #if QT_CONFIG(thread) QT_BEGIN_NAMESPACE +class QMimeData; + namespace qstdweb { + extern const char makeContextfulPromiseFunctionName[]; // DOM API in C++, implemented using emscripten val.h and bind.h. - // This is private API and can be extened and changed as needed. + // This is private API and can be extended and changed as needed. + // The API mirrors that of the native API, with some extensions + // to ease usage from C++ code. class ArrayBuffer; class Blob; @@ -71,40 +54,84 @@ namespace qstdweb { class Uint8Array; class EventCallback; - class ArrayBuffer { + class Q_CORE_EXPORT ArrayBuffer { public: + explicit ArrayBuffer(uint32_t size); explicit ArrayBuffer(const emscripten::val &arrayBuffer); uint32_t byteLength() const; + ArrayBuffer slice(uint32_t begin, uint32_t end) const; + emscripten::val val() const; private: friend class Uint8Array; emscripten::val m_arrayBuffer = emscripten::val::undefined(); }; - class Blob { + class Q_CORE_EXPORT Blob { public: explicit Blob(const emscripten::val &blob); + static Blob fromArrayBuffer(const ArrayBuffer &arrayBuffer); uint32_t size() const; + static Blob copyFrom(const char *buffer, uint32_t size, std::string mimeType); + static Blob copyFrom(const char *buffer, uint32_t size); + Blob slice(uint32_t begin, uint32_t end) const; + ArrayBuffer arrayBuffer_sync() const; + emscripten::val val() const; + std::string type() const; private: friend class FileReader; emscripten::val m_blob = emscripten::val::undefined(); }; - class File { + class Q_CORE_EXPORT File { public: File() = default; explicit File(const emscripten::val &file); + ~File(); + + File(const File &other); + File(File &&other); + File &operator=(const File &other); + File &operator=(File &&other); Blob slice(uint64_t begin, uint64_t end) const; std::string name() const; uint64_t size() const; + std::string type() const; + void stream(uint32_t offset, uint32_t length, char *buffer, + std::function<void()> completed) const; + void stream(char *buffer, std::function<void()> completed) const; + emscripten::val val() const; + void fileUrlRegistration() const; + const QString &fileUrlPath() const { return m_urlPath; } + emscripten::val file() const { return m_file; } private: emscripten::val m_file = emscripten::val::undefined(); + QString m_urlPath; }; - class FileList { + class Q_CORE_EXPORT FileUrlRegistration + { + public: + explicit FileUrlRegistration(File file); + ~FileUrlRegistration(); + + FileUrlRegistration(const FileUrlRegistration &other) = delete; + FileUrlRegistration(FileUrlRegistration &&other); + FileUrlRegistration &operator=(const FileUrlRegistration &other) = delete; + FileUrlRegistration &operator=(FileUrlRegistration &&other); + + const QString &path() const { return m_path; } + + private: + QString m_path; + }; + + using FileUrlRegistrations = std::vector<std::unique_ptr<FileUrlRegistration>>; + + class Q_CORE_EXPORT FileList { public: FileList() = default; explicit FileList(const emscripten::val &fileList); @@ -112,19 +139,21 @@ namespace qstdweb { int length() const; File item(int index) const; File operator[](int index) const; + emscripten::val val() const; private: emscripten::val m_fileList = emscripten::val::undefined(); }; - class FileReader { + class Q_CORE_EXPORT FileReader { public: ArrayBuffer result() const; void readAsArrayBuffer(const Blob &blob) const; - void onLoad(const std::function<void ()> &onLoad); - void onError(const std::function<void ()> &onError); - void onAbort(const std::function<void ()> &onAbort); + void onLoad(const std::function<void(emscripten::val)> &onLoad); + void onError(const std::function<void(emscripten::val)> &onError); + void onAbort(const std::function<void(emscripten::val)> &onAbort); + emscripten::val val() const; private: emscripten::val m_fileReader = emscripten::val::global("FileReader").new_(); @@ -133,35 +162,170 @@ namespace qstdweb { std::unique_ptr<EventCallback> m_onAbort; }; - class Uint8Array { + class Q_CORE_EXPORT Uint8Array { public: static Uint8Array heap(); explicit Uint8Array(const emscripten::val &uint8Array); explicit Uint8Array(const ArrayBuffer &buffer); + explicit Uint8Array(uint32_t size); Uint8Array(const ArrayBuffer &buffer, uint32_t offset, uint32_t length); - Uint8Array(char *buffer, uint32_t size); + Uint8Array(const char *buffer, uint32_t size); ArrayBuffer buffer() const; uint32_t length() const; void set(const Uint8Array &source); + Uint8Array subarray(uint32_t begin, uint32_t end); void copyTo(char *destination) const; + QByteArray copyToQByteArray() const; + static void copy(char *destination, const Uint8Array &source); + static Uint8Array copyFrom(const char *buffer, uint32_t size); + static Uint8Array copyFrom(const QByteArray &buffer); + emscripten::val val() const; + private: static emscripten::val heap_(); static emscripten::val constructor_(); emscripten::val m_uint8Array = emscripten::val::undefined(); }; - class EventCallback + class Q_CORE_EXPORT EventCallback + { + public: + EventCallback() = default; + ~EventCallback(); + EventCallback(EventCallback const&) = delete; + EventCallback& operator=(EventCallback const&) = delete; + EventCallback(emscripten::val element, const std::string &name, + const std::function<void(emscripten::val)> &fn); + + private: + emscripten::val m_element = emscripten::val::undefined(); + std::string m_eventName; + std::unique_ptr<std::function<void(emscripten::val)>> m_handler; + emscripten::val m_eventListener = emscripten::val::undefined(); + }; + + struct PromiseCallbacks + { + std::function<void(emscripten::val)> thenFunc; + std::function<void(emscripten::val)> catchFunc; + std::function<void()> finallyFunc; + }; + + namespace Promise { + void Q_CORE_EXPORT adoptPromise(emscripten::val promise, PromiseCallbacks callbacks); + + template<typename... Args> + void make(emscripten::val target, + QString methodName, + PromiseCallbacks callbacks, + Args... args) + { + emscripten::val promiseObject = target.call<emscripten::val>( + methodName.toStdString().c_str(), std::forward<Args>(args)...); + if (promiseObject.isUndefined() || promiseObject["constructor"]["name"].as<std::string>() != "Promise") { + qFatal("This function did not return a promise"); + } + + adoptPromise(std::move(promiseObject), std::move(callbacks)); + } + + void Q_CORE_EXPORT all(std::vector<emscripten::val> promises, PromiseCallbacks callbacks); + }; + + template<class F> + decltype(auto) bindForever(F wrappedCallback) + { + return wrappedCallback; + } + + class Q_CORE_EXPORT BlobIODevice: public QIODevice { public: - EventCallback(emscripten::val element, const std::string &name, const std::function<void ()> &fn); - static void activate(emscripten::val event); + BlobIODevice(Blob blob); + bool open(QIODeviceBase::OpenMode mode) override; + bool isSequential() const override; + qint64 size() const override; + bool seek(qint64 pos) override; + + protected: + qint64 readData(char *data, qint64 maxSize) override; + qint64 writeData(const char *, qint64) override; + private: - static std::string contextPropertyName(const std::string &eventName); - std::function<void ()> m_fn; + Blob m_blob; }; + + class Uint8ArrayIODevice: public QIODevice + { + public: + Uint8ArrayIODevice(Uint8Array array); + bool open(QIODevice::OpenMode mode) override; + bool isSequential() const override; + qint64 size() const override; + bool seek(qint64 pos) override; + + protected: + qint64 readData(char *data, qint64 maxSize) override; + qint64 writeData(const char *data, qint64 size) override; + + private: + Uint8Array m_array; + }; + + inline emscripten::val window() + { + static emscripten::val savedWindow = emscripten::val::global("window"); + return savedWindow; + } + + bool haveAsyncify(); + bool Q_CORE_EXPORT haveJspi(); + bool canBlockCallingThread(); + + struct CancellationFlag + { + }; + +#if QT_CONFIG(thread) + template<class T> + T proxyCall(std::function<T()> task, emscripten::ProxyingQueue *queue) + { + T result; + queue->proxySync(emscripten_main_runtime_thread_id(), + [task, result = &result]() { *result = task(); }); + return result; + } + + template<> + inline void proxyCall<void>(std::function<void()> task, emscripten::ProxyingQueue *queue) + { + queue->proxySync(emscripten_main_runtime_thread_id(), task); + } + + template<class T> + T runTaskOnMainThread(std::function<T()> task, emscripten::ProxyingQueue *queue) + { + return emscripten_is_main_runtime_thread() ? task() : proxyCall<T>(std::move(task), queue); + } + + template<class T> + T runTaskOnMainThread(std::function<T()> task) + { + emscripten::ProxyingQueue singleUseQueue; + return runTaskOnMainThread<T>(task, &singleUseQueue); + } + +#else + template<class T> + T runTaskOnMainThread(std::function<T()> task) + { + return task(); + } +#endif // QT_CONFIG(thread) + } QT_END_NAMESPACE diff --git a/src/corelib/platform/windows/qcomobject_p.h b/src/corelib/platform/windows/qcomobject_p.h new file mode 100644 index 0000000000..8f27a18ff6 --- /dev/null +++ b/src/corelib/platform/windows/qcomobject_p.h @@ -0,0 +1,127 @@ +// Copyright (C) 2023 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 QCOMOBJECT_P_H +#define QCOMOBJECT_P_H + +// +// 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. +// + +#include <QtCore/private/qglobal_p.h> + +#if defined(Q_OS_WIN) || defined(Q_QDOC) + +# include <QtCore/qt_windows.h> + +QT_BEGIN_NAMESPACE + +namespace QtPrivate { + +template <typename... TInterfaces> +struct QComObjectTraits +{ + static constexpr bool isGuidOf(REFIID riid) noexcept + { + return ((riid == __uuidof(TInterfaces)) || ...); + } +}; + +} // namespace QtPrivate + +// NOTE: In order to be able to query the intermediate interface, i.e. the one you do not specify in +// QComObject interface list (TFirstInterface, TInterfaces) but that is a base for any of them +// (except IUnknown) you need to provide an explicit specialization of function +// QComObjectTraits<...>::isGuidOf for that type. For example, if you want to inherit interface +// IMFSampleGrabberSinkCallback which inherits IMFClockStateSink and you want to be able to query +// the latter one you need to provide this explicit specialization: +// +// class SinkCallback : public QComObject<IMFSampleGrabberSinkCallback> +// { +// ... +// }; +// +// namespace QtPrivate { +// +// template <> +// struct QComObjectTraits<IMFSampleGrabberSinkCallback> +// { +// static constexpr bool isGuidOf(REFIID riid) noexcept +// { +// return QComObjectTraits<IMFSampleGrabberSinkCallback, IMFClockStateSink>::isGuidOf(riid); +// } +// }; +// +// } + +template <typename TFirstInterface, typename... TAdditionalInterfaces> +class QComObject : public TFirstInterface, public TAdditionalInterfaces... +{ +public: + STDMETHODIMP QueryInterface(REFIID riid, void **ppvObject) override + { + if (!ppvObject) + return E_POINTER; + + if (riid == __uuidof(IUnknown)) { + *ppvObject = static_cast<IUnknown *>(static_cast<TFirstInterface *>(this)); + AddRef(); + + return S_OK; + } + + return tryQueryInterface<TFirstInterface, TAdditionalInterfaces...>(riid, ppvObject); + } + + STDMETHODIMP_(ULONG) AddRef() override { return ++m_referenceCount; } + + STDMETHODIMP_(ULONG) Release() override + { + const LONG referenceCount = --m_referenceCount; + if (referenceCount == 0) + delete this; + + return referenceCount; + } + +protected: + QComObject() = default; + + // Destructor is not public. Caller should call Release. + // Derived class should make its destructor private to force this behavior. + virtual ~QComObject() = default; + +private: + template <typename TInterface, typename... TRest> + HRESULT tryQueryInterface(REFIID riid, void **ppvObject) + { + if (QtPrivate::QComObjectTraits<TInterface>::isGuidOf(riid)) { + *ppvObject = static_cast<TInterface *>(this); + AddRef(); + + return S_OK; + } + + if constexpr (sizeof...(TRest) > 0) + return tryQueryInterface<TRest...>(riid, ppvObject); + + *ppvObject = nullptr; + + return E_NOINTERFACE; + } + + std::atomic<LONG> m_referenceCount = 1; +}; + +QT_END_NAMESPACE + +#endif // Q_OS_WIN + +#endif // QCOMOBJECT_P_H diff --git a/src/corelib/platform/windows/qfactorycacheregistration.cpp b/src/corelib/platform/windows/qfactorycacheregistration.cpp new file mode 100644 index 0000000000..6bd69c66d1 --- /dev/null +++ b/src/corelib/platform/windows/qfactorycacheregistration.cpp @@ -0,0 +1,53 @@ +// 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 "qfactorycacheregistration_p.h" + +#include <QtCore/QMutex> + +QT_BEGIN_NAMESPACE + +#ifdef QT_USE_FACTORY_CACHE_REGISTRATION + +static QBasicMutex registrationMutex; +static detail::QWinRTFactoryCacheRegistration *firstElement; + +detail::QWinRTFactoryCacheRegistration::QWinRTFactoryCacheRegistration( + QFunctionPointer clearFunction) + : m_clearFunction(clearFunction) +{ + QMutexLocker lock(®istrationMutex); + + // forward pointers + m_next = std::exchange(firstElement, this); + + // backward pointers + m_prevNext = &firstElement; + if (m_next) + m_next->m_prevNext = &m_next; +} + +detail::QWinRTFactoryCacheRegistration::~QWinRTFactoryCacheRegistration() +{ + QMutexLocker lock(®istrationMutex); + + *m_prevNext = m_next; + + if (m_next) + m_next->m_prevNext = m_prevNext; +} + +void detail::QWinRTFactoryCacheRegistration::clearAllCaches() +{ + QMutexLocker lock(®istrationMutex); + + detail::QWinRTFactoryCacheRegistration *element; + + for (element = firstElement; element != nullptr; element = element->m_next) { + element->m_clearFunction(); + } +} + +#endif + +QT_END_NAMESPACE diff --git a/src/corelib/platform/windows/qfactorycacheregistration_p.h b/src/corelib/platform/windows/qfactorycacheregistration_p.h new file mode 100644 index 0000000000..d0b19b995b --- /dev/null +++ b/src/corelib/platform/windows/qfactorycacheregistration_p.h @@ -0,0 +1,52 @@ +// 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 QFACTORYCACHEREGISTRATION_P_H +#define QFACTORYCACHEREGISTRATION_P_H + +// +// 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. +// + +#include <QtCore/qglobal.h> + +#if !defined(QT_BOOTSTRAPPED) && QT_CONFIG(cpp_winrt) +# define QT_USE_FACTORY_CACHE_REGISTRATION +#endif + +#ifdef QT_USE_FACTORY_CACHE_REGISTRATION + +#include "qt_winrtbase_p.h" + +QT_BEGIN_NAMESPACE + +namespace detail { + +class QWinRTFactoryCacheRegistration +{ +public: + Q_CORE_EXPORT explicit QWinRTFactoryCacheRegistration(QFunctionPointer clearFunction); + Q_CORE_EXPORT ~QWinRTFactoryCacheRegistration(); + Q_CORE_EXPORT static void clearAllCaches(); + + Q_DISABLE_COPY_MOVE(QWinRTFactoryCacheRegistration) +private: + QWinRTFactoryCacheRegistration **m_prevNext = nullptr; + QWinRTFactoryCacheRegistration *m_next = nullptr; + QFunctionPointer m_clearFunction; +}; + +inline QWinRTFactoryCacheRegistration reg([]() noexcept { winrt::clear_factory_cache(); }); +} + +QT_END_NAMESPACE + +#endif +#endif // QFACTORYCACHEREGISTRATION_P_H diff --git a/src/corelib/platform/windows/qt_winrtbase_p.h b/src/corelib/platform/windows/qt_winrtbase_p.h new file mode 100644 index 0000000000..fb7366f93d --- /dev/null +++ b/src/corelib/platform/windows/qt_winrtbase_p.h @@ -0,0 +1,34 @@ +// 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 QT_WINRTBASE_P_H +#define QT_WINRTBASE_P_H + +// +// 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. +// + +#include <QtCore/qglobal.h> + +#if QT_CONFIG(cpp_winrt) +# include <winrt/base.h> +# include <QtCore/private/qfactorycacheregistration_p.h> +// Workaround for Windows SDK bug. +// See https://github.com/microsoft/Windows.UI.Composition-Win32-Samples/issues/47 +namespace winrt::impl +{ + template <typename Async> + auto wait_for(Async const& async, Windows::Foundation::TimeSpan const& timeout); +} +// See https://learn.microsoft.com/en-us/windows/uwp/cpp-and-winrt-apis/faq#how-do-i-resolve-ambiguities-with-getcurrenttime-and-or-try- +// for more workarounds. +#endif // QT_CONFIG(cpp/winrt) + +#endif // QT_WINRTBASE_P_H |