diff options
author | Assam Boudjelthia <assam.boudjelthia@qt.io> | 2021-05-10 16:16:37 +0300 |
---|---|---|
committer | Assam Boudjelthia <assam.boudjelthia@qt.io> | 2021-05-26 23:24:11 +0000 |
commit | 478ed8b71f289438ed664bf2676b270325c93bfc (patch) | |
tree | 0f698686f6c01b638f8f6b60da218289c06b30bb | |
parent | da30e402f38a434f856fa8670a8813c3cffe6440 (diff) |
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>
-rw-r--r-- | src/corelib/doc/src/external-resources.qdoc | 5 | ||||
-rw-r--r-- | src/corelib/kernel/qcoreapplication_platform.h | 21 | ||||
-rw-r--r-- | src/corelib/kernel/qjnihelpers.cpp | 3 | ||||
-rw-r--r-- | src/corelib/kernel/qjnihelpers_p.h | 2 | ||||
-rw-r--r-- | src/corelib/platform/android/qandroidnativeinterface.cpp | 149 | ||||
-rw-r--r-- | tests/auto/corelib/platform/android/tst_android.cpp | 112 |
6 files changed, 291 insertions, 1 deletions
diff --git a/src/corelib/doc/src/external-resources.qdoc b/src/corelib/doc/src/external-resources.qdoc index e8f1d3348d..62e6b7ea1d 100644 --- a/src/corelib/doc/src/external-resources.qdoc +++ b/src/corelib/doc/src/external-resources.qdoc @@ -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 +*/ diff --git a/src/corelib/kernel/qcoreapplication_platform.h b/src/corelib/kernel/qcoreapplication_platform.h index 6830e4d3ab..c65010faee 100644 --- a/src/corelib/kernel/qcoreapplication_platform.h +++ b/src/corelib/kernel/qcoreapplication_platform.h @@ -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 } diff --git a/src/corelib/kernel/qjnihelpers.cpp b/src/corelib/kernel/qjnihelpers.cpp index edb07b47cf..46143d4c2c 100644 --- a/src/corelib/kernel/qjnihelpers.cpp +++ b/src/corelib/kernel/qjnihelpers.cpp @@ -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"); diff --git a/src/corelib/kernel/qjnihelpers_p.h b/src/corelib/kernel/qjnihelpers_p.h index 90dbcd1cd8..07f5ff8e8a 100644 --- a/src/corelib/kernel/qjnihelpers_p.h +++ b/src/corelib/kernel/qjnihelpers_p.h @@ -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); diff --git a/src/corelib/platform/android/qandroidnativeinterface.cpp b/src/corelib/platform/android/qandroidnativeinterface.cpp index 296027340a..86723cc33b 100644 --- a/src/corelib/platform/android/qandroidnativeinterface.cpp +++ b/src/corelib/platform/android/qandroidnativeinterface.cpp @@ -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 diff --git a/tests/auto/corelib/platform/android/tst_android.cpp b/tests/auto/corelib/platform/android/tst_android.cpp index 3a00d9414b..710e5b2ecf 100644 --- a/tests/auto/corelib/platform/android/tst_android.cpp +++ b/tests/auto/corelib/platform/android/tst_android.cpp @@ -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" |