diff options
author | Christian Strømme <[email protected]> | 2015-01-21 13:47:38 +0100 |
---|---|---|
committer | Christian Stromme <[email protected]> | 2015-02-09 16:35:47 +0000 |
commit | b8fc57ba4aa5b25ae31df0ec74c62730dd0fa9d0 (patch) | |
tree | cd7def072034c2d14b213cfd5c8b2269bee869c0 | |
parent | 011069957991ac884f6e5cbacc6391570c7091e8 (diff) |
Add runJavaScript() on Android and iOS.
This is a rough implementation on both Android and iOS and both comes
with certain limitations. On Android we require API level 19 or higher
and on iOS the runJavaScript function is not asynchronous.
Change-Id: Ia19782505e5e30d5517a4d627565e5432cc27bfa
Reviewed-by: Eskil Abrahamsen Blomfeldt <[email protected]>
-rw-r--r-- | src/imports/plugins.qmltypes | 9 | ||||
-rw-r--r-- | src/jar/src/org/qtproject/qt5/android/view/QtAndroidWebViewController.java | 35 | ||||
-rw-r--r-- | src/webview/qquickwebview.cpp | 75 | ||||
-rw-r--r-- | src/webview/qquickwebview.h | 7 | ||||
-rw-r--r-- | src/webview/qwebview.cpp | 9 | ||||
-rw-r--r-- | src/webview/qwebview_android.cpp | 53 | ||||
-rw-r--r-- | src/webview/qwebview_android_p.h | 4 | ||||
-rw-r--r-- | src/webview/qwebview_ios.mm | 8 | ||||
-rw-r--r-- | src/webview/qwebview_ios_p.h | 4 | ||||
-rw-r--r-- | src/webview/qwebview_p.h | 3 | ||||
-rw-r--r-- | src/webview/qwebviewinterface_p.h | 3 | ||||
-rw-r--r-- | tests/auto/webview/qwebview/qwebview.pro | 4 | ||||
-rw-r--r-- | tests/auto/webview/qwebview/tst_qwebview.cpp | 31 |
13 files changed, 244 insertions, 1 deletions
diff --git a/src/imports/plugins.qmltypes b/src/imports/plugins.qmltypes index 8acf6fd..a0a57bf 100644 --- a/src/imports/plugins.qmltypes +++ b/src/imports/plugins.qmltypes @@ -20,5 +20,14 @@ Module { Method { name: "goForward" } Method { name: "reload" } Method { name: "stop" } + Method { + name: "runJavaScript" + Parameter { name: "script"; type: "string" } + Parameter { name: "callback"; type: "QJValue" } + } + Method { + name: "runJavaScript" + Parameter { name: "script"; type: "string" } + } } } diff --git a/src/jar/src/org/qtproject/qt5/android/view/QtAndroidWebViewController.java b/src/jar/src/org/qtproject/qt5/android/view/QtAndroidWebViewController.java index dd3cab0..404fa3a 100644 --- a/src/jar/src/org/qtproject/qt5/android/view/QtAndroidWebViewController.java +++ b/src/jar/src/org/qtproject/qt5/android/view/QtAndroidWebViewController.java @@ -63,12 +63,16 @@ public class QtAndroidWebViewController private Method m_webViewOnPause = null; private Method m_webSettingsSetDisplayZoomControls = null; + // API 19 methods + private Method m_webViewEvaluateJavascript = null; + // Native callbacks private native void c_onPageFinished(long id, String url); private native void c_onPageStarted(long id, String url, Bitmap icon); private native void c_onProgressChanged(long id, int newProgress); private native void c_onReceivedIcon(long id, Bitmap icon); private native void c_onReceivedTitle(long id, String title); + private native void c_onRunJavaScriptResult(long id, long callbackId, String result); private class QtAndroidWebViewClient extends WebViewClient { @@ -136,6 +140,11 @@ public class QtAndroidWebViewController m_webViewOnResume = m_webView.getClass().getMethod("onResume"); m_webViewOnPause = m_webView.getClass().getMethod("onPause"); m_webSettingsSetDisplayZoomControls = webSettings.getClass().getMethod("setDisplayZoomControls", boolean.class); + if (Build.VERSION.SDK_INT > 18) { + m_webViewEvaluateJavascript = m_webView.getClass().getMethod("evaluateJavascript", + String.class, + ValueCallback.class); + } } catch (Exception e) { /* Do nothing */ e.printStackTrace(); } } @@ -300,6 +309,32 @@ public class QtAndroidWebViewController return progress[0]; } + public void runJavaScript(final String script, final long callbackId) + { + if (script == null) + return; + + if (Build.VERSION.SDK_INT < 19 || m_webViewEvaluateJavascript == null) + return; + + m_activity.runOnUiThread(new Runnable() { + @Override + public void run() { + try { + m_webViewEvaluateJavascript.invoke(m_webView, script, callbackId == -1 ? null : + new ValueCallback<String>() { + @Override + public void onReceiveValue(String result) { + c_onRunJavaScriptResult(m_id, callbackId, result); + } + }); + } catch (Exception e) { + e.printStackTrace(); + } + } + }); + } + public String getUrl() { final String[] url = {""}; diff --git a/src/webview/qquickwebview.cpp b/src/webview/qquickwebview.cpp index be51fb7..1803994 100644 --- a/src/webview/qquickwebview.cpp +++ b/src/webview/qquickwebview.cpp @@ -38,6 +38,38 @@ #include <QtQml/qqmlengine.h> #include <QtCore/qmutex.h> +namespace { + +class CallbackStorage +{ +public: + int insertCallback(const QJSValue &callback) + { + QMutexLocker locker(&m_mtx); + const int nextId = qMax(++m_counter, 0); + if (nextId == 0) + m_counter = 1; + + m_callbacks.insert(nextId, callback); + return nextId; + } + + QJSValue takeCallback(int callbackId) + { + QMutexLocker lock(&m_mtx); + return m_callbacks.take(callbackId); + } + +private: + QMutex m_mtx; + int m_counter; + QHash<int, QJSValue> m_callbacks; +}; + +} // namespace + +Q_GLOBAL_STATIC(CallbackStorage, callbacks) + /*! \qmltype WebView \inqmlmodule QtWebView @@ -64,6 +96,7 @@ QQuickWebView::QQuickWebView(QQuickItem *parent) connect(m_webView.data(), &QWebView::loadingChanged, this, &QQuickWebView::loadingChanged); connect(m_webView.data(), &QWebView::loadProgressChanged, this, &QQuickWebView::loadProgressChanged); connect(m_webView.data(), &QWebView::requestFocus, this, &QQuickWebView::onFocusRequest); + connect(m_webView.data(), &QWebView::javaScriptResult, this, &QQuickWebView::onRunJavaScriptResult); } QQuickWebView::~QQuickWebView() @@ -185,6 +218,48 @@ void QQuickWebView::stop() m_webView->stop(); } +/*! + \qmlmethod void QtWebView::WebView::runJavaScript(string script, variant callback) + + Runs the specified JavaScript. + In case a callback function is provided, it will be invoked after the script finished running. + + \badcode + runJavaScript("document.title", function(result) { console.log(result); }); + \endcode +*/ +void QQuickWebView::runJavaScript(const QString &script, const QJSValue &callback) +{ + const int callbackId = callback.isCallable() ? callbacks->insertCallback(callback) + : -1; + runJavaScriptPrivate(script, callbackId); +} + +void QQuickWebView::runJavaScriptPrivate(const QString &script, int callbackId) +{ + m_webView->runJavaScriptPrivate(script, callbackId); +} + +void QQuickWebView::onRunJavaScriptResult(int id, const QVariant &variant) +{ + if (id == -1) + return; + + QJSValue callback = callbacks->takeCallback(id); + if (callback.isUndefined()) + return; + + QQmlEngine *engine = qmlEngine(this); + if (engine == 0) { + qWarning() << "No JavaScript engine, unable to handle JavaScript callback!"; + return; + } + + QJSValueList args; + args.append(engine->toScriptValue(variant)); + callback.call(args); +} + void QQuickWebView::onFocusRequest(bool focus) { setFocus(focus); diff --git a/src/webview/qquickwebview.h b/src/webview/qquickwebview.h index aab13b1..c36cc9d 100644 --- a/src/webview/qquickwebview.h +++ b/src/webview/qquickwebview.h @@ -84,6 +84,8 @@ public Q_SLOTS: void goForward() Q_DECL_OVERRIDE; void reload() Q_DECL_OVERRIDE; void stop() Q_DECL_OVERRIDE; + void runJavaScript(const QString& script, + const QJSValue &callback = QJSValue()); Q_SIGNALS: void titleChanged(); @@ -91,7 +93,12 @@ Q_SIGNALS: void loadingChanged(); void loadProgressChanged(); +protected: + void runJavaScriptPrivate(const QString& script, + int callbackId) Q_DECL_OVERRIDE; + private Q_SLOTS: + void onRunJavaScriptResult(int id, const QVariant &variant); void onFocusRequest(bool focus); private: diff --git a/src/webview/qwebview.cpp b/src/webview/qwebview.cpp index 9d55331..1865c7e 100644 --- a/src/webview/qwebview.cpp +++ b/src/webview/qwebview.cpp @@ -48,6 +48,8 @@ QWebView::QWebView(QObject *p) connect(d, SIGNAL(loadingChanged()), this, SIGNAL(loadingChanged())); connect(d, SIGNAL(loadProgressChanged()), this, SIGNAL(loadProgressChanged())); connect(d, SIGNAL(requestFocus(bool)), this, SIGNAL(requestFocus(bool))); + connect(d, &QWebViewPrivate::javaScriptResult, + this, &QWebView::javaScriptResult); } QWebView::~QWebView() @@ -150,6 +152,13 @@ void QWebView::setFocus(bool focus) d->setFocus(focus); } +void QWebView::runJavaScriptPrivate(const QString &script, + int callbackId) +{ + Q_D(QWebView); + d->runJavaScriptPrivate(script, callbackId); +} + void QWebView::init() { Q_D(QWebView); diff --git a/src/webview/qwebview_android.cpp b/src/webview/qwebview_android.cpp index be87a56..1ff4271 100644 --- a/src/webview/qwebview_android.cpp +++ b/src/webview/qwebview_android.cpp @@ -45,6 +45,7 @@ #include <QtCore/qjsondocument.h> #include <QtCore/qjsonobject.h> #include <QtCore/qurl.h> +#include <QtCore/qdebug.h> QT_BEGIN_NAMESPACE @@ -152,6 +153,24 @@ void QAndroidWebViewPrivate::setVisibility(QWindow::Visibility visibility) m_window->setVisibility(visibility); } +void QAndroidWebViewPrivate::runJavaScriptPrivate(const QString &script, + int callbackId) +{ + if (QtAndroidPrivate::androidSdkVersion() < 19) { + qWarning() << "runJavaScript() requires API level 19 or higher."; + if (callbackId == -1) + return; + + // Emit signal here to remove the callback. + Q_EMIT javaScriptResult(callbackId, QVariant()); + } + + m_viewController.callMethod<void>("runJavaScript", + "(Ljava/lang/String;J)V", + static_cast<jstring>(QJNIObjectPrivate::fromString(script).object()), + callbackId); +} + void QAndroidWebViewPrivate::setVisible(bool visible) { m_window->setVisible(visible); @@ -195,6 +214,37 @@ void QAndroidWebViewPrivate::onApplicationStateChanged(Qt::ApplicationState stat QT_END_NAMESPACE +static void c_onRunJavaScriptResult(JNIEnv *env, + jobject thiz, + jlong id, + jlong callbackId, + jstring result) +{ + Q_UNUSED(env) + Q_UNUSED(thiz) + + const WebViews &wv = (*g_webViews); + QAndroidWebViewPrivate *wc = static_cast<QAndroidWebViewPrivate *>(wv[id]); + if (!wc) + return; + + const QString &resultString = QJNIObjectPrivate(result).toString(); + + // The result string is in JSON format, lets parse it to see what we got. + QJsonValue jsonValue; + const QByteArray &jsonData = "{ \"data\": " + resultString.toUtf8() + " }"; + QJsonParseError error; + const QJsonDocument &jsonDoc = QJsonDocument::fromJson(jsonData, &error); + if (error.error == QJsonParseError::NoError && jsonDoc.isObject()) { + const QJsonObject &object = jsonDoc.object(); + jsonValue = object.value(QStringLiteral("data")); + } + + Q_EMIT wc->javaScriptResult(int(callbackId), + jsonValue.isNull() ? resultString + : jsonValue.toVariant()); +} + static void c_onPageFinished(JNIEnv *env, jobject thiz, jlong id, @@ -315,7 +365,8 @@ JNIEXPORT jint JNI_OnLoad(JavaVM* vm, void* /*reserved*/) {"c_onPageStarted", "(JLjava/lang/String;Landroid/graphics/Bitmap;)V", reinterpret_cast<void *>(c_onPageStarted)}, {"c_onProgressChanged", "(JI)V", reinterpret_cast<void *>(c_onProgressChanged)}, {"c_onReceivedIcon", "(JLandroid/graphics/Bitmap;)V", reinterpret_cast<void *>(c_onReceivedIcon)}, - {"c_onReceivedTitle", "(JLjava/lang/String;)V", reinterpret_cast<void *>(c_onReceivedTitle)} + {"c_onReceivedTitle", "(JLjava/lang/String;)V", reinterpret_cast<void *>(c_onReceivedTitle)}, + {"c_onRunJavaScriptResult", "(JJLjava/lang/String;)V", reinterpret_cast<void *>(c_onRunJavaScriptResult)} }; const int nMethods = sizeof(methods) / sizeof(methods[0]); diff --git a/src/webview/qwebview_android_p.h b/src/webview/qwebview_android_p.h index 751b44f..45a0213 100644 --- a/src/webview/qwebview_android_p.h +++ b/src/webview/qwebview_android_p.h @@ -83,6 +83,10 @@ public Q_SLOTS: void reload() Q_DECL_OVERRIDE; void stop() Q_DECL_OVERRIDE; +protected: + void runJavaScriptPrivate(const QString& script, + int callbackId) Q_DECL_OVERRIDE; + private Q_SLOTS: void onApplicationStateChanged(Qt::ApplicationState state); diff --git a/src/webview/qwebview_ios.mm b/src/webview/qwebview_ios.mm index faa82c3..b9fdff9 100644 --- a/src/webview/qwebview_ios.mm +++ b/src/webview/qwebview_ios.mm @@ -283,4 +283,12 @@ void QIosWebViewPrivate::reload() [uiWebView reload]; } +void QIosWebViewPrivate::runJavaScriptPrivate(const QString &script, int callbackId) +{ + // ### TODO needs more async + NSString *result = [uiWebView stringByEvaluatingJavaScriptFromString:script.toNSString()]; + if (callbackId != -1) + Q_EMIT javaScriptResult(callbackId, QString::fromNSString(result)); +} + QT_END_NAMESPACE diff --git a/src/webview/qwebview_ios_p.h b/src/webview/qwebview_ios_p.h index 774d707..8302a68 100644 --- a/src/webview/qwebview_ios_p.h +++ b/src/webview/qwebview_ios_p.h @@ -86,6 +86,10 @@ public Q_SLOTS: void reload() Q_DECL_OVERRIDE; void stop() Q_DECL_OVERRIDE; +protected: + void runJavaScriptPrivate(const QString& script, + int callbackId) Q_DECL_OVERRIDE; + public: UIWebView *uiWebView; UIGestureRecognizer *m_recognizer; diff --git a/src/webview/qwebview_p.h b/src/webview/qwebview_p.h index 2a92620..578403a 100644 --- a/src/webview/qwebview_p.h +++ b/src/webview/qwebview_p.h @@ -93,10 +93,13 @@ Q_SIGNALS: void urlChanged(); void loadingChanged(); void loadProgressChanged(); + void javaScriptResult(int id, const QVariant &result); void requestFocus(bool focus); protected: void init(); + void runJavaScriptPrivate(const QString &script, + int callbackId) Q_DECL_OVERRIDE; private: friend class QQuickViewController; diff --git a/src/webview/qwebviewinterface_p.h b/src/webview/qwebviewinterface_p.h index 86d370c..ef02042 100644 --- a/src/webview/qwebviewinterface_p.h +++ b/src/webview/qwebviewinterface_p.h @@ -76,6 +76,9 @@ public: virtual void goForward() = 0; virtual void stop() = 0; virtual void reload() = 0; + + virtual void runJavaScriptPrivate(const QString &script, + int callbackId) = 0; }; #endif // QWEBVIEWINTERFACE_H diff --git a/tests/auto/webview/qwebview/qwebview.pro b/tests/auto/webview/qwebview/qwebview.pro index 3cfaca9..198bb21 100644 --- a/tests/auto/webview/qwebview/qwebview.pro +++ b/tests/auto/webview/qwebview/qwebview.pro @@ -2,6 +2,10 @@ CONFIG += testcase parallel_test TARGET = tst_qwebview osx:CONFIG -= app_bundle +!qtHaveModule(quick) { + DEFINES += QT_NO_QQUICKWEBVIEW_TESTS +} + !android: qtHaveModule(webengine) { QT += webengine webengine-private DEFINES += QT_WEBVIEW_WEBENGINE_BACKEND diff --git a/tests/auto/webview/qwebview/tst_qwebview.cpp b/tests/auto/webview/qwebview/tst_qwebview.cpp index e9f8d51..9702324 100644 --- a/tests/auto/webview/qwebview/tst_qwebview.cpp +++ b/tests/auto/webview/qwebview/tst_qwebview.cpp @@ -41,6 +41,11 @@ #include <QtCore/qfileinfo.h> #include <QtWebView/private/qwebview_p.h> #include <QtQml/qqmlengine.h> + +#ifndef QT_NO_QQUICKWEBVIEW_TESTS +#include <QtWebView/qquickwebview.h> +#endif // QT_NO_QQUICKWEBVIEW_TESTS + #ifdef QT_WEBVIEW_WEBENGINE_BACKEND #include <QtWebEngine> #endif // QT_WEBVIEW_WEBENGINE_BACKEND @@ -54,6 +59,7 @@ public: private slots: void initTestCase(); void load(); + void runJavaScript(); private: const QString m_cacheLocation; @@ -92,6 +98,31 @@ void tst_QWebView::load() QCOMPARE(view.url(), url); } +void tst_QWebView::runJavaScript() +{ +#ifndef QT_NO_QQUICKWEBVIEW_TESTS + const QString tstProperty = QString(QLatin1String("Qt.tst_data")); + const QString title = QString(QLatin1String("WebViewTitle")); + + QQuickWebView view; + QQmlEngine engine; + QQmlContext *rootContext = engine.rootContext(); + QQmlEngine::setContextForObject(&view, rootContext); + + QCOMPARE(view.loadProgress(), 0); + view.loadHtml(QString("<html><head><title>%1</title></head><body /></html>").arg(title)); + QTRY_COMPARE(view.loadProgress(), 100); + QTRY_VERIFY(!view.isLoading()); + QCOMPARE(view.title(), title); + QJSValue callback = engine.evaluate(QString("function(result) { %1 = result; }").arg(tstProperty)); + QVERIFY2(!callback.isError(), qPrintable(callback.toString())); + QVERIFY(!callback.isUndefined()); + QVERIFY(callback.isCallable()); + view.runJavaScript(QString(QLatin1String("document.title")), callback); + QTRY_COMPARE(engine.evaluate(tstProperty).toString(), title); +#endif // QT_NO_QQUICKWEBVIEW_TESTS +} + QTEST_MAIN(tst_QWebView) #include "tst_qwebview.moc" |