Android: Add runOnMainAndroidThread() under QNativeInterface
This replaces QtAndroidPrivate::runOnAndroidThread{Sync} calls. This also now allows passing std::function<> that can return values, and not only an std::function<void()>. This adds some tests for this calls as well. Fixes: QTBUG-90501 Change-Id: I138d2aae64be17347f7ff712d8a86edb49ea8350 Reviewed-by: Fabian Kosmale <fabian.kosmale@qt.io>
This commit is contained in:
parent
da30e402f3
commit
478ed8b71f
@ -105,3 +105,8 @@
|
||||
\externalpage https://doc.qt.io/qtcreator/creator-deploying-android.html#editing-manifest-files
|
||||
\title Qt Creator: Editing Manifest Files
|
||||
*/
|
||||
|
||||
/*!
|
||||
\externalpage https://developer.android.com/training/articles/perf-anr
|
||||
\title Android: Keeping your app responsive
|
||||
*/
|
||||
|
@ -44,6 +44,11 @@
|
||||
#include <QtCore/qnativeinterface.h>
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
|
||||
#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT)
|
||||
#include <QtCore/qfuture.h>
|
||||
#include <QtCore/qvariant.h>
|
||||
#endif
|
||||
|
||||
#if defined(Q_OS_ANDROID) && !defined(Q_OS_ANDROID_EMBEDDED)
|
||||
class _jobject;
|
||||
typedef _jobject* jobject;
|
||||
@ -61,6 +66,22 @@ struct Q_CORE_EXPORT QAndroidApplication
|
||||
static bool isActivityContext();
|
||||
static int sdkVersion();
|
||||
static void hideSplashScreen(int duration = 0);
|
||||
|
||||
#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT)
|
||||
static QFuture<QVariant> runOnAndroidMainThread(const std::function<QVariant()> &runnable,
|
||||
const QDeadlineTimer
|
||||
&timeout = QDeadlineTimer(-1));
|
||||
|
||||
template <class T>
|
||||
std::enable_if_t<std::is_invocable_v<T> && std::is_same_v<std::invoke_result_t<T>, void>,
|
||||
QFuture<void>> static runOnAndroidMainThread(const T &runnable,
|
||||
const QDeadlineTimer
|
||||
&timeout = QDeadlineTimer(-1))
|
||||
{
|
||||
std::function<QVariant()> func = [&](){ runnable(); return QVariant(); };
|
||||
return static_cast<QFuture<void>>(runOnAndroidMainThread(func, timeout));
|
||||
}
|
||||
#endif
|
||||
};
|
||||
#endif
|
||||
}
|
||||
|
@ -358,6 +358,9 @@ jint QtAndroidPrivate::initJNI(JavaVM *vm, JNIEnv *env)
|
||||
if (!registerPermissionNatives())
|
||||
return JNI_ERR;
|
||||
|
||||
if (!registerNativeInterfaceNatives())
|
||||
return JNI_ERR;
|
||||
|
||||
g_runPendingCppRunnablesMethodID = env->GetStaticMethodID(jQtNative,
|
||||
"runPendingCppRunnablesOnAndroidThread",
|
||||
"()V");
|
||||
|
@ -128,7 +128,9 @@ namespace QtAndroidPrivate
|
||||
Q_CORE_EXPORT PermissionsHash requestPermissionsSync(JNIEnv *env, const QStringList &permissions, int timeoutMs = INT_MAX);
|
||||
Q_CORE_EXPORT PermissionsResult checkPermission(const QString &permission);
|
||||
Q_CORE_EXPORT bool shouldShowRequestPermissionRationale(const QString &permission);
|
||||
|
||||
bool registerPermissionNatives();
|
||||
bool registerNativeInterfaceNatives();
|
||||
|
||||
Q_CORE_EXPORT void handleActivityResult(jint requestCode, jint resultCode, jobject data);
|
||||
Q_CORE_EXPORT void registerActivityResultListener(ActivityResultListener *listener);
|
||||
|
@ -37,12 +37,27 @@
|
||||
**
|
||||
****************************************************************************/
|
||||
|
||||
#include <QtCore/qcoreapplication.h>
|
||||
#include <QtCore/qcoreapplication_platform.h>
|
||||
|
||||
#include <QtCore/private/qjnihelpers_p.h>
|
||||
#include <QtCore/qjniobject.h>
|
||||
#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT)
|
||||
#include <QtConcurrent/QtConcurrent>
|
||||
#include <QtCore/qpromise.h>
|
||||
#include <deque>
|
||||
#endif
|
||||
|
||||
QT_BEGIN_NAMESPACE
|
||||
|
||||
#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT)
|
||||
static const char qtNativeClassName[] = "org/qtproject/qt/android/QtNative";
|
||||
|
||||
typedef std::pair<std::function<QVariant()>, QSharedPointer<QPromise<QVariant>>> RunnablePair;
|
||||
typedef std::deque<RunnablePair> PendingRunnables;
|
||||
Q_GLOBAL_STATIC(PendingRunnables, g_pendingRunnables);
|
||||
static QBasicMutex g_pendingRunnablesMutex;
|
||||
#endif
|
||||
|
||||
/*!
|
||||
\class QNativeInterface::QAndroidApplication
|
||||
\since 6.2
|
||||
@ -110,4 +125,136 @@ void QNativeInterface::QAndroidApplication::hideSplashScreen(int duration)
|
||||
"hideSplashScreen", "(I)V", 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)
|
||||
{
|
||||
QSharedPointer<QPromise<QVariant>> promise(new QPromise<QVariant>());
|
||||
QFuture<QVariant> future = promise->future();
|
||||
promise->start();
|
||||
|
||||
(void) QtConcurrent::run([=, &future]() {
|
||||
if (!timeout.isForever()) {
|
||||
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);
|
||||
loop.exec();
|
||||
}
|
||||
});
|
||||
|
||||
QMutexLocker locker(&g_pendingRunnablesMutex);
|
||||
g_pendingRunnables->push_back(std::pair(runnable, promise));
|
||||
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;
|
||||
|
||||
std::pair pair = std::move(g_pendingRunnables->front());
|
||||
g_pendingRunnables->pop_front();
|
||||
locker.unlock();
|
||||
|
||||
// run the runnable outside the sync block!
|
||||
auto promise = pair.second;
|
||||
if (!promise->isCanceled())
|
||||
promise->addResult(pair.first());
|
||||
promise->finish();
|
||||
}
|
||||
}
|
||||
#endif
|
||||
|
||||
bool QtAndroidPrivate::registerNativeInterfaceNatives()
|
||||
{
|
||||
#if QT_CONFIG(future) && !defined(QT_NO_QOBJECT)
|
||||
JNINativeMethod methods = {"runPendingCppRunnables", "()V", (void *)runPendingCppRunnables};
|
||||
return QJniEnvironment().registerNativeMethods(qtNativeClassName, &methods, 1);
|
||||
#else
|
||||
return true;
|
||||
#endif
|
||||
}
|
||||
|
||||
QT_END_NAMESPACE
|
||||
|
@ -40,6 +40,7 @@ private slots:
|
||||
void assetsNotWritable();
|
||||
void testAndroidSdkVersion();
|
||||
void testAndroidActivity();
|
||||
void testRunOnAndroidMainThread();
|
||||
};
|
||||
|
||||
void tst_Android::assetsRead()
|
||||
@ -77,6 +78,117 @@ void tst_Android::testAndroidActivity()
|
||||
QVERIFY(activity.callMethod<jboolean>("isTaskRoot"));
|
||||
}
|
||||
|
||||
void tst_Android::testRunOnAndroidMainThread()
|
||||
{
|
||||
// async void
|
||||
{
|
||||
int res = 0;
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{ res = 1; });
|
||||
QTRY_COMPARE(res, 1);
|
||||
}
|
||||
|
||||
// sync void
|
||||
{
|
||||
int res = 0;
|
||||
auto task = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
res = 1;
|
||||
});
|
||||
task.waitForFinished();
|
||||
QCOMPARE(res, 1);
|
||||
}
|
||||
|
||||
// sync return value
|
||||
{
|
||||
auto task = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]{
|
||||
return 1;
|
||||
});
|
||||
task.waitForFinished();
|
||||
QVERIFY(task.isResultReadyAt(0));
|
||||
QCOMPARE(task.result().value<int>(), 1);
|
||||
}
|
||||
|
||||
// nested calls
|
||||
{
|
||||
// nested async/async
|
||||
int res = 0;
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
res = 3;
|
||||
});
|
||||
});
|
||||
QTRY_COMPARE(res, 3);
|
||||
|
||||
// nested async/sync
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
res = 5;
|
||||
}).waitForFinished();
|
||||
});
|
||||
QTRY_COMPARE(res, 5);
|
||||
|
||||
// nested sync/sync
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
res = 4;
|
||||
}).waitForFinished();
|
||||
}).waitForFinished();
|
||||
QCOMPARE(res, 4);
|
||||
|
||||
|
||||
// nested sync/async
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
QNativeInterface::QAndroidApplication::runOnAndroidMainThread([&res]{
|
||||
res = 6;
|
||||
});
|
||||
}).waitForFinished();
|
||||
QCOMPARE(res, 6);
|
||||
}
|
||||
|
||||
// timeouts
|
||||
{
|
||||
auto task = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]{
|
||||
QThread::msleep(500);
|
||||
return 1;
|
||||
}, QDeadlineTimer(100));
|
||||
task.waitForFinished();
|
||||
QVERIFY(task.isCanceled());
|
||||
QVERIFY(task.isFinished());
|
||||
QVERIFY(!task.isResultReadyAt(0));
|
||||
|
||||
auto task2 = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]{
|
||||
return 2;
|
||||
}, QDeadlineTimer(0));
|
||||
task2.waitForFinished();
|
||||
QVERIFY(task2.isCanceled());
|
||||
QVERIFY(task2.isFinished());
|
||||
QVERIFY(!task2.isResultReadyAt(0));
|
||||
|
||||
QDeadlineTimer deadline(1000);
|
||||
auto task3 = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]{
|
||||
return 3;
|
||||
}, QDeadlineTimer(10000));
|
||||
task3.waitForFinished();
|
||||
QVERIFY(deadline.remainingTime() > 0);
|
||||
QVERIFY(task3.isFinished());
|
||||
QVERIFY(!task3.isCanceled());
|
||||
QVERIFY(task3.isResultReadyAt(0));
|
||||
QCOMPARE(task3.result().value<int>(), 3);
|
||||
}
|
||||
|
||||
// cancelled future
|
||||
{
|
||||
auto task = QNativeInterface::QAndroidApplication::runOnAndroidMainThread([]{
|
||||
QThread::msleep(2000);
|
||||
return 1;
|
||||
});
|
||||
task.cancel();
|
||||
QVERIFY(task.isCanceled());
|
||||
task.waitForFinished();
|
||||
QVERIFY(task.isFinished());
|
||||
QVERIFY(!task.isResultReadyAt(0));
|
||||
}
|
||||
}
|
||||
|
||||
QTEST_MAIN(tst_Android)
|
||||
#include "tst_android.moc"
|
||||
|
||||
|
Loading…
Reference in New Issue
Block a user