Revision control
++ // Copyright (C) 2017 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
++ // Qt-Security score:significant reason:default
++
++ #include "qandroidwebview_p.h"
++ #include <QtWebView/qwebview.h>
++ #include <QtWebView/qwebviewloadinginfo.h>
++ #include <QtWebView/private/qwebviewfactory_p.h>
++
++ #include <QtCore/private/qjnihelpers_p.h>
++ #include <QtCore/qjniobject.h>
++
++ #include <QtCore/qabstracteventdispatcher.h>
++ #include <QtCore/qdebug.h>
++ #include <QtCore/qfile.h>
++ #include <QtCore/qfileinfo.h>
++ #include <QtCore/qjsondocument.h>
++ #include <QtCore/qjsonobject.h>
++ #include <QtCore/qmimedatabase.h>
++ #include <QtCore/qmimetype.h>
++ #include <QtCore/qset.h>
++ #include <QtCore/qthread.h>
++ #include <QtCore/qtimer.h>
++ #include <QtCore/qurl.h>
++
++ #include <QtGui/qguiapplication.h>
++
++ #include <android/bitmap.h>
++ #include <android/log.h>
++
++ QT_BEGIN_NAMESPACE
++
++ Q_DECLARE_JNI_CLASS(Bitmap, "android/graphics/Bitmap");
++
++ using namespace QtJniTypes;
++ using namespace Qt::StringLiterals;
++
++ QAndroidWebViewSettingsPrivate::QAndroidWebViewSettingsPrivate(
++ const WebViewController &viewController)
++ : m_viewController(viewController)
++ {
++ }
++
++ bool QAndroidWebViewSettingsPrivate::doTestAttribute(WebAttribute attribute) const
++ {
++ switch (attribute) {
++ case QWebViewSettings::WebAttribute::LocalStorageEnabled:
++ return localStorageEnabled();
++ case QWebViewSettings::WebAttribute::JavaScriptEnabled:
++ return javaScriptEnabled();
++ case QWebViewSettings::WebAttribute::AllowFileAccess:
++ return allowFileAccess();
++ case QWebViewSettings::WebAttribute::LocalContentCanAccessFileUrls:
++ return localContentCanAccessFileUrls();
++ }
++ return false;
++ }
++
++ void QAndroidWebViewSettingsPrivate::doSetAttribute(WebAttribute attribute, bool value)
++ {
++ switch (attribute) {
++ case QWebViewSettings::WebAttribute::LocalStorageEnabled:
++ setLocalStorageEnabled(value);
++ break;
++ case QWebViewSettings::WebAttribute::JavaScriptEnabled:
++ setJavaScriptEnabled(value);
++ break;
++ case QWebViewSettings::WebAttribute::AllowFileAccess:
++ setAllowFileAccess(value);
++ break;
++ case QWebViewSettings::WebAttribute::LocalContentCanAccessFileUrls:
++ setLocalContentCanAccessFileUrls(value);
++ break;
++ }
++ }
++
++ bool QAndroidWebViewSettingsPrivate::localStorageEnabled() const
++ {
++ return m_viewController.callMethod<jboolean>("isLocalStorageEnabled");
++ }
++
++ bool QAndroidWebViewSettingsPrivate::javaScriptEnabled() const
++ {
++ return m_viewController.callMethod<jboolean>("isJavaScriptEnabled");
++ }
++
++ bool QAndroidWebViewSettingsPrivate::localContentCanAccessFileUrls() const
++ {
++ return m_viewController.callMethod<jboolean>("isAllowFileAccessFromFileURLsEnabled");
++ }
++
++ bool QAndroidWebViewSettingsPrivate::allowFileAccess() const
++ {
++ return m_viewController.callMethod<jboolean>("isAllowFileAccessEnabled");
++ }
++
++ void QAndroidWebViewSettingsPrivate::setLocalContentCanAccessFileUrls(bool enabled)
++ {
++ m_viewController.callMethod<void>("setAllowFileAccessFromFileURLs", enabled);
++ }
++
++ void QAndroidWebViewSettingsPrivate::setJavaScriptEnabled(bool enabled)
++ {
++ m_viewController.callMethod<void>("setJavaScriptEnabled", enabled);
++ }
++
++ void QAndroidWebViewSettingsPrivate::setLocalStorageEnabled(bool enabled)
++ {
++ m_viewController.callMethod<void>("setLocalStorageEnabled", enabled);
++ }
++
++ void QAndroidWebViewSettingsPrivate::setAllowFileAccess(bool enabled)
++ {
++ m_viewController.callMethod<void>("setAllowFileAccess", enabled);
++ }
++
++ typedef QSet<QAndroidWebViewPrivate *> WebViews;
++ Q_GLOBAL_STATIC(WebViews, g_webViews)
++
++ QAndroidWebViewPrivate::QAndroidWebViewPrivate(QWebView *view)
++ : QWebViewPrivate(view),
++ m_callbackId(0),
++ m_window(nullptr),
++ m_viewController(nullptr),
++ m_webView(nullptr)
++ {
++ // QtAndroidWebViewController constructor blocks a qGuiThread until
++ // the WebView is created and configured in UI thread.
++ // That is why we cannot proceed until AndroidDeadlockProtector is locked
++ while (!QtAndroidPrivate::acquireAndroidDeadlockProtector()) {
++ auto eventDispatcher = QThread::currentThread()->eventDispatcher();
++ if (eventDispatcher)
++ eventDispatcher->processEvents(
++ QEventLoop::ExcludeUserInputEvents|QEventLoop::ExcludeSocketNotifiers);
++ }
++ m_viewController = WebViewController(QtAndroidPrivate::activity(), reinterpret_cast<jlong>(this));
++
++ QtAndroidPrivate::releaseAndroidDeadlockProtector();
++
++ m_webView = m_viewController.callMethod<WebView>("getWebView");
++ m_settings = new QAndroidWebViewSettingsPrivate(m_viewController);
++
++ m_window = QWindow::fromWinId(reinterpret_cast<WId>(m_webView.object()));
++ if (m_window) {
++ m_window->setParent(view);
++ connect(view, &QWindow::widthChanged, m_window, &QWindow::setWidth);
++ connect(view, &QWindow::heightChanged, m_window, &QWindow::setHeight);
++ connect(view, &QWindow::visibleChanged, m_window, &QWindow::setVisible);
++ }
++ g_webViews->insert(this);
++ connect(qApp, &QGuiApplication::applicationStateChanged,
++ this, &QAndroidWebViewPrivate::onApplicationStateChanged);
++ }
++
++ QAndroidWebViewPrivate::~QAndroidWebViewPrivate()
++ {
++ g_webViews->remove(this);
++ if (m_window != 0) {
++ m_window->setVisible(false);
++ m_window->setParent(0);
++ delete m_window;
++ }
++
++ m_viewController.callMethod<void>("destroy");
++ }
++
++ QString QAndroidWebViewPrivate::httpUserAgent() const
++ {
++ return m_viewController.callMethod<QString>("getUserAgent");
++ }
++
++ void QAndroidWebViewPrivate::setHttpUserAgent(const QString &userAgent)
++ {
++ m_viewController.callMethod<void>("setUserAgent", userAgent);
++ emit q_ptr->httpUserAgentStringChanged(userAgent);
++ }
++
++ QUrl QAndroidWebViewPrivate::url() const
++ {
++ return QUrl(m_viewController.callMethod<QString>("getUrl"));
++ }
++
++ void QAndroidWebViewPrivate::setUrl(const QUrl &url)
++ {
++ m_viewController.callMethod<void>("loadUrl", url.toString());
++ }
++
++ void QAndroidWebViewPrivate::loadHtml(const QString &html, const QUrl &baseUrl)
++ {
++ const QString mimeTypeString = u"text/html;charset=UTF-8"_s;
++ if (baseUrl.isEmpty() || baseUrl.scheme() == "data"_L1) {
++ const QString encoded = QUrl::toPercentEncoding(html);
++ m_viewController.callMethod<void>("loadData", encoded, mimeTypeString, jstring(nullptr));
++ } else {
++ // andorid webview in case of non data baseURL scheme will loaded 'html' into the WebView as
++ // a plain string meaning any url encoded entities in the string will not be decoded.
++ m_viewController.callMethod<void>("loadDataWithBaseURL", baseUrl.toString(), html,
++ mimeTypeString, jstring(nullptr),
++ baseUrl.isValid() ? baseUrl.toString() : QString());
++ }
++ }
++
++ bool QAndroidWebViewPrivate::canGoBack() const
++ {
++ return m_viewController.callMethod<bool>("canGoBack");
++ }
++
++ void QAndroidWebViewPrivate::goBack()
++ {
++ m_viewController.callMethod<void>("goBack");
++ }
++
++ bool QAndroidWebViewPrivate::canGoForward() const
++ {
++ return m_viewController.callMethod<bool>("canGoForward");
++ }
++
++ void QAndroidWebViewPrivate::goForward()
++ {
++ m_viewController.callMethod<void>("goForward");
++ }
++
++ void QAndroidWebViewPrivate::reload()
++ {
++ m_viewController.callMethod<void>("reload");
++ }
++
++ QString QAndroidWebViewPrivate::title() const
++ {
++ return m_viewController.callMethod<QString>("getTitle");
++ }
++
++ void QAndroidWebViewPrivate::runJavaScript(
++ const QString &script, const std::function<void(const QVariant &)> &resultCallback)
++ {
++ if (QtAndroidPrivate::androidSdkVersion() < 19) {
++ qWarning("runJavaScript() requires API level 19 or higher.");
++ if (resultCallback)
++ resultCallback(QVariant());
++ return;
++ }
++
++ m_callbacks.insert(m_callbackId, resultCallback);
++ m_viewController.callMethod<void>("runJavaScript", script, jlong(m_callbackId));
++ ++m_callbackId;
++ }
++
++ QWebViewSettingsPrivate *QAndroidWebViewPrivate::settings() const
++ {
++ return m_settings;
++ }
++
++ void QAndroidWebViewPrivate::setCookie(const QString &domain, const QString &name, const QString &value)
++ {
++ QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
++ const QString cookie = name + u'=' + value;
++ WebViewController::callStaticMethod<void>("setCookie", jlong(this), domain, cookie);
++ });
++ }
++
++ void QAndroidWebViewPrivate::deleteCookie(const QString &domain, const QString &name)
++ {
++ QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
++ const QString cookie = name.split(u'=').at(0) + u'=';
++ WebViewController::callStaticMethod<void>("removeCookie", jlong(this), domain, cookie);
++ });
++ }
++
++ void QAndroidWebViewPrivate::deleteAllCookies()
++ {
++ QNativeInterface::QAndroidApplication::runOnAndroidMainThread([=]() {
++ WebViewController::callStaticMethod<void>("removeCookies");
++ });
++ }
++
++ int QAndroidWebViewPrivate::loadProgress() const
++ {
++ return m_viewController.callMethod<int>("getProgress");
++ }
++
++ bool QAndroidWebViewPrivate::isLoading() const
++ {
++ return m_viewController.callMethod<bool>("isLoading");
++ }
++
++ void QAndroidWebViewPrivate::stop()
++ {
++ m_viewController.callMethod<void>("stopLoading");
++ }
++
++ void QAndroidWebViewPrivate::onApplicationStateChanged(Qt::ApplicationState state)
++ {
++ if (QtAndroidPrivate::androidSdkVersion() < 11)
++ return;
++
++ if (state == Qt::ApplicationActive)
++ m_viewController.callMethod<void>("onResume");
++ else
++ m_viewController.callMethod<void>("onPause");
++ }
++
++ void QAndroidWebViewPrivate::javaScriptResult(int id, const QVariant &result)
++ {
++ auto it = m_callbacks.find(id);
++ if (it != m_callbacks.end()) {
++ auto callback = it.value();
++ if (callback)
++ callback(result);
++ m_callbacks.erase(it);
++ }
++ }
++
++ bool QAndroidWebViewPrivate::loadQrcFile(QUrl filePath, QtJniTypes::QrcFileResponse &response, JNIEnv *env)
++ {
++ QFile file(u':' + filePath.path());
++ bool isOpen = file.exists() && file.size() != 0 && file.open(QIODeviceBase::ReadOnly);
++ if (!isOpen) {
++ return false;
++ }
++
++ QFileInfo fileInfo(file);
++ QMimeDatabase mimeDatabase;
++ QMimeType mimeType = mimeDatabase.mimeTypeForFile(fileInfo);
++ QString mimeTypeString = mimeType.name();
++
++ response.callMethod<void>("setMimeType", env->NewString(mimeTypeString.utf16(), mimeTypeString.size()));
++ QByteArray data = file.read(file.bytesAvailable());
++ jbyteArray jdata = env->NewByteArray(data.size());
++ env->SetByteArrayRegion(jdata, 0, data.size(), (jbyte *)data.data());
++ response.callMethod<void>("setData", jdata);
++
++ Q_ASSERT(file.atEnd());
++ file.close();
++
++ return true;
++ }
++
++ static void c_onRunJavaScriptResult(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jlong callbackId,
++ jstring result)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ const QString &resultString = QJniObject(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"));
++ }
++
++ wc->javaScriptResult(int(callbackId),
++ jsonValue.isNull() ? resultString : jsonValue.toVariant());
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onRunJavaScriptResult)
++
++ static void c_onPageFinished(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jstring url)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ QWebViewLoadingInfo loadingInfo(QWebViewFactory::LoadingInfo::create(
++ QUrl(QJniObject(url).toString()),
++ QWebViewLoadingInfo::LoadStatus::Succeeded,
++ QString()));
++ emit wc->q_ptr->loadingChanged(loadingInfo);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onPageFinished)
++
++ static void c_onPageStarted(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jstring url,
++ Bitmap icon)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ Q_UNUSED(icon);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ QUrl qurl(QJniObject(url).toString());
++ QWebViewLoadingInfo loadingInfo(QWebViewFactory::LoadingInfo::create(
++ qurl,
++ QWebViewLoadingInfo::LoadStatus::Started,
++ QString()));
++ emit wc->q_ptr->urlChanged(qurl);
++ emit wc->q_ptr->loadingChanged(loadingInfo);
++
++ // if (!icon)
++ // return;
++
++ // QImage image;
++ // if (favIcon(env, icon, &image))
++ // Q_EMIT wc->iconChanged(image);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onPageStarted)
++
++ static void c_onProgressChanged(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jint newProgress)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ emit wc->q_ptr->loadProgressChanged(newProgress);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onProgressChanged)
++
++ static void c_onReceivedIcon(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ Bitmap icon)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ Q_UNUSED(icon);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ if (!icon.isValid())
++ return;
++
++ // QImage image;
++ // if (favIcon(env, icon, &image))
++ // Q_EMIT wc->iconChanged(image);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onReceivedIcon)
++
++ static void c_onReceivedTitle(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jstring title)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++ const QString &qTitle = QJniObject(title).toString();
++ emit wc->q_ptr->titleChanged(qTitle);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onReceivedTitle)
++
++ static void c_onReceivedError(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jint errorCode,
++ jstring description,
++ jstring url)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++ Q_UNUSED(errorCode);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ QWebViewLoadingInfo loadingInfo(QWebViewFactory::LoadingInfo::create(
++ QUrl(QJniObject(url).toString()),
++ QWebViewLoadingInfo::LoadStatus::Failed,
++ QJniObject(description).toString()));
++ emit wc->q_ptr->loadingChanged(loadingInfo);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onReceivedError)
++
++ static bool c_onQrcNavigationRequest(JNIEnv *env,
++ jobject thiz,
++ jlong id,
++ jstring url,
++ QtJniTypes::QrcFileResponse response)
++ {
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return false;
++
++ return wc->loadQrcFile(QUrl(QJniObject(url).toString()), response, env);
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onQrcNavigationRequest)
++
++ static void c_onCookieAdded(JNIEnv *env,
++ jclass thiz,
++ jlong id,
++ jboolean result,
++ jstring domain,
++ jstring name)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ if (result)
++ emit wc->q_ptr->cookieAdded(QJniObject(domain).toString(), QJniObject(name).toString());
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onCookieAdded)
++
++ static void c_onCookieRemoved(JNIEnv *env,
++ jclass thiz,
++ jlong id,
++ jboolean result,
++ jstring domain,
++ jstring name)
++ {
++ Q_UNUSED(env);
++ Q_UNUSED(thiz);
++
++ Q_ASSERT(id);
++ QAndroidWebViewPrivate *wc = reinterpret_cast<QAndroidWebViewPrivate *>(id);
++ if (!g_webViews->contains(wc))
++ return;
++
++ if (result)
++ emit wc->q_ptr->cookieRemoved(QJniObject(domain).toString(), QJniObject(name).toString());
++ }
++ Q_DECLARE_JNI_NATIVE_METHOD(c_onCookieRemoved)
++ QT_END_NAMESPACE
++
++ extern "C" JNIEXPORT jint JNI_OnLoad(JavaVM* /* vm */, void* /*reserved*/)
++ {
++ static bool initialized = false;
++ if (initialized)
++ return JNI_VERSION_1_6;
++ initialized = true;
++
++ if (!WebViewController::registerNativeMethods({
++ Q_JNI_NATIVE_METHOD(c_onRunJavaScriptResult),
++ Q_JNI_NATIVE_METHOD(c_onPageFinished),
++ Q_JNI_NATIVE_METHOD(c_onPageStarted),
++ Q_JNI_NATIVE_METHOD(c_onProgressChanged),
++ Q_JNI_NATIVE_METHOD(c_onReceivedIcon),
++ Q_JNI_NATIVE_METHOD(c_onReceivedTitle),
++ Q_JNI_NATIVE_METHOD(c_onReceivedError),
++ Q_JNI_NATIVE_METHOD(c_onQrcNavigationRequest),
++ Q_JNI_NATIVE_METHOD(c_onCookieAdded),
++ Q_JNI_NATIVE_METHOD(c_onCookieRemoved),
++ })) {
++ qCritical("Failed to register native methods for WebViewController");
++ return JNI_ERR;
++ }
++
++ return JNI_VERSION_1_6;
++ }