summaryrefslogtreecommitdiffstats
diff options
context:
space:
mode:
authorAssam Boudjelthia <assam.boudjelthia@qt.io>2021-05-10 16:16:37 +0300
committerAssam Boudjelthia <assam.boudjelthia@qt.io>2021-05-26 23:24:11 +0000
commit478ed8b71f289438ed664bf2676b270325c93bfc (patch)
tree0f698686f6c01b638f8f6b60da218289c06b30bb
parentda30e402f38a434f856fa8670a8813c3cffe6440 (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.qdoc5
-rw-r--r--src/corelib/kernel/qcoreapplication_platform.h21
-rw-r--r--src/corelib/kernel/qjnihelpers.cpp3
-rw-r--r--src/corelib/kernel/qjnihelpers_p.h2
-rw-r--r--src/corelib/platform/android/qandroidnativeinterface.cpp149
-rw-r--r--tests/auto/corelib/platform/android/tst_android.cpp112
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"