diff options
Diffstat (limited to 'src/plugins/platforms/android')
53 files changed, 3433 insertions, 1581 deletions
diff --git a/src/plugins/platforms/android/CMakeLists.txt b/src/plugins/platforms/android/CMakeLists.txt index 7db5556d7e..aaf62dd7e7 100644 --- a/src/plugins/platforms/android/CMakeLists.txt +++ b/src/plugins/platforms/android/CMakeLists.txt @@ -1,34 +1,33 @@ -# Generated from android.pro. +# Copyright (C) 2023 The Qt Company Ltd. +# SPDX-License-Identifier: BSD-3-Clause ##################################################################### ## QAndroidIntegrationPlugin Plugin: ##################################################################### -qt_find_package(EGL) # special case +qt_find_package(EGL) qt_internal_add_plugin(QAndroidIntegrationPlugin OUTPUT_NAME qtforandroid PLUGIN_TYPE platforms - DEFAULT_IF ${QT_QPA_DEFAULT_PLATFORM} MATCHES android # special case + DEFAULT_IF "android" IN_LIST QT_QPA_PLATFORMS SOURCES androidcontentfileengine.cpp androidcontentfileengine.h androiddeadlockprotector.h androidjniaccessibility.cpp androidjniaccessibility.h - androidjniclipboard.cpp androidjniclipboard.h androidjniinput.cpp androidjniinput.h androidjnimain.cpp androidjnimain.h androidjnimenu.cpp androidjnimenu.h - androidsurfaceclient.h main.cpp qandroidassetsfileenginehandler.cpp qandroidassetsfileenginehandler.h qandroideventdispatcher.cpp qandroideventdispatcher.h qandroidinputcontext.cpp qandroidinputcontext.h qandroidplatformaccessibility.cpp qandroidplatformaccessibility.h - qandroidplatformbackingstore.cpp qandroidplatformbackingstore.h qandroidplatformclipboard.cpp qandroidplatformclipboard.h qandroidplatformdialoghelpers.cpp qandroidplatformdialoghelpers.h qandroidplatformfiledialoghelper.cpp qandroidplatformfiledialoghelper.h qandroidplatformfontdatabase.cpp qandroidplatformfontdatabase.h qandroidplatformforeignwindow.cpp qandroidplatformforeignwindow.h + qandroidplatformiconengine.cpp qandroidplatformiconengine.h qandroidplatformintegration.cpp qandroidplatformintegration.h qandroidplatformmenu.cpp qandroidplatformmenu.h qandroidplatformmenubar.cpp qandroidplatformmenubar.h @@ -41,8 +40,20 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin qandroidplatformtheme.cpp qandroidplatformtheme.h qandroidplatformwindow.cpp qandroidplatformwindow.h qandroidsystemlocale.cpp qandroidsystemlocale.h - DEFINES - QT_USE_QSTRINGBUILDER + androidwindowembedding.cpp androidwindowembedding.h + androidbackendregister.cpp androidbackendregister.h + NO_UNITY_BUILD_SOURCES + # Conflicting symbols and macros with androidjnimain.cpp + # TODO: Unify the usage of FIND_AND_CHECK_CLASS, and similar + # macros. Q_JNI_FIND_AND_CHECK_CLASS in `qjnihelpers_p.h` + # seems to be doing most of the work already. + androidjnimenu.cpp + qandroidinputcontext.cpp + androidjniaccessibility.cpp + qandroidplatformdialoghelpers.cpp + # Conflicting JNI classes, and types + androidcontentfileengine.cpp + qandroidplatformintegration.cpp INCLUDE_DIRECTORIES ${CMAKE_CURRENT_SOURCE_DIR} ${QtBase_SOURCE_DIR}/src/3rdparty/android @@ -54,12 +65,9 @@ qt_internal_add_plugin(QAndroidIntegrationPlugin Qt::GuiPrivate android jnigraphics - EGL::EGL # special case + EGL::EGL ) -#### Keys ignored in scope 1:.:.:android.pro:<TRUE>: -# OTHER_FILES = "$$PWD/android.json" - ## Scopes: ##################################################################### @@ -77,4 +85,7 @@ qt_internal_extend_target(QAndroidIntegrationPlugin CONDITION QT_FEATURE_vulkan SOURCES qandroidplatformvulkaninstance.cpp qandroidplatformvulkaninstance.h qandroidplatformvulkanwindow.cpp qandroidplatformvulkanwindow.h + NO_UNITY_BUILD_SOURCES + # To avoid undefined symbols due to missing VK_USE_PLATFORM_ANDROID_KHR + qandroidplatformvulkaninstance.cpp qandroidplatformvulkanwindow.cpp ) diff --git a/src/plugins/platforms/android/androidbackendregister.cpp b/src/plugins/platforms/android/androidbackendregister.cpp new file mode 100644 index 0000000000..bfd86138aa --- /dev/null +++ b/src/plugins/platforms/android/androidbackendregister.cpp @@ -0,0 +1,52 @@ +// Copyright (C) 2024 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 + +#include "androidbackendregister.h" + +#include "androidjnimain.h" + +QT_BEGIN_NAMESPACE + +Q_LOGGING_CATEGORY(lcAndroidBackendRegister, "qt.qpa.androidbackendregister") + +Q_DECLARE_JNI_CLASS(BackendRegister, "org/qtproject/qt/android/BackendRegister"); + +bool AndroidBackendRegister::registerNatives() +{ + return QtJniTypes::BackendRegister::registerNativeMethods( + { Q_JNI_NATIVE_SCOPED_METHOD(registerBackend, AndroidBackendRegister), + Q_JNI_NATIVE_SCOPED_METHOD(unregisterBackend, AndroidBackendRegister) }); +} + +void AndroidBackendRegister::registerBackend(JNIEnv *, jclass, jclass interfaceClass, + jobject interface) +{ + if (AndroidBackendRegister *reg = QtAndroid::backendRegister()) { + const QJniObject classObject(static_cast<jobject>(interfaceClass)); + QString name = classObject.callMethod<jstring>("getName").toString(); + name.replace('.', '/'); + + QMutexLocker lock(®->m_registerMutex); + reg->m_register[name] = QJniObject(interface); + } else { + qCWarning(lcAndroidBackendRegister) + << "AndroidBackendRegister pointer is null, cannot register functionality"; + } +} + +void AndroidBackendRegister::unregisterBackend(JNIEnv *, jclass, jclass interfaceClass) +{ + if (AndroidBackendRegister *reg = QtAndroid::backendRegister()) { + const QJniObject classObject(static_cast<jobject>(interfaceClass)); + QString name = classObject.callMethod<jstring>("getName").toString(); + name.replace('.', '/'); + + QMutexLocker lock(®->m_registerMutex); + reg->m_register.remove(name); + } else { + qCWarning(lcAndroidBackendRegister) + << "AndroidBackendRegister pointer is null, cannot unregister functionality"; + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidbackendregister.h b/src/plugins/platforms/android/androidbackendregister.h new file mode 100644 index 0000000000..c186f7e107 --- /dev/null +++ b/src/plugins/platforms/android/androidbackendregister.h @@ -0,0 +1,67 @@ +// Copyright (C) 2024 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 + +#ifndef ANDROIDBACKENDREGISTER_H +#define ANDROIDBACKENDREGISTER_H + +#include <type_traits> + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> +#include <QtCore/qjniobject.h> + +#include <QtCore/qstring.h> +#include <QtCore/qmap.h> +#include <QtCore/qmutex.h> +#include <QtCore/qloggingcategory.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_LOGGING_CATEGORY(lcAndroidBackendRegister) + +template <typename T> +using ValidInterfaceType = std::enable_if_t<std::is_base_of_v<QtJniTypes::JObjectBase, T>, bool>; + +class AndroidBackendRegister +{ +public: + static bool registerNatives(); + + template <typename T, ValidInterfaceType<T> = true> + [[nodiscard]] T getInterface() + { + QMutexLocker lock(&m_registerMutex); + return m_register.value(QString(QtJniTypes::Traits<T>::className().data())); + } + + template <typename Object> + using IsObjectType = + typename std::disjunction<std::is_base_of<QJniObject, Object>, + std::is_base_of<QtJniTypes::JObjectBase, Object>>; + + template <typename Interface, typename Ret, typename... Args, + ValidInterfaceType<Interface> = true> + auto callInterface(const char *func, Args... args) + { + if (const auto obj = getInterface<Interface>(); obj.isValid()) + return obj.template callMethod<Ret, Args...>(func, std::forward<Args>(args)...); + + if constexpr (IsObjectType<Ret>::value) + return Ret(QJniObject()); + if constexpr (!std::is_same_v<Ret, void>) + return Ret{}; + } + +private: + QMutex m_registerMutex; + QMap<QString, QJniObject> m_register; + + static void registerBackend(JNIEnv *, jclass, jclass interfaceClass, jobject interface); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(registerBackend) + static void unregisterBackend(JNIEnv *, jclass, jclass interfaceClass); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(unregisterBackend) +}; + +QT_END_NAMESPACE + +#endif // ANDROIDBACKENDREGISTER_H diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 04862ccba6..5757f5d8cc 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -1,5 +1,5 @@ -// Copyright (C) 2019 Volker Krause <vkrause@kde.org> -// Copyright (C) 2021 The Qt Company Ltd. +// Copyright (C) 2019 Volker Krause <vkrause@kde.org> +// Copyright (C) 2022 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 #include "androidcontentfileengine.h" @@ -7,16 +7,37 @@ #include <QtCore/qcoreapplication.h> #include <QtCore/qjnienvironment.h> #include <QtCore/qjniobject.h> +#include <QtCore/qurl.h> +#include <QtCore/qdatetime.h> +#include <QtCore/qmimedatabase.h> -#include <QDebug> +QT_BEGIN_NAMESPACE using namespace QNativeInterface; using namespace Qt::StringLiterals; -AndroidContentFileEngine::AndroidContentFileEngine(const QString &f) - : m_file(f) +Q_DECLARE_JNI_CLASS(ContentResolverType, "android/content/ContentResolver"); +Q_DECLARE_JNI_CLASS(UriType, "android/net/Uri"); +Q_DECLARE_JNI_CLASS(Uri, "android/net/Uri"); +Q_DECLARE_JNI_CLASS(ParcelFileDescriptorType, "android/os/ParcelFileDescriptor"); +Q_DECLARE_JNI_CLASS(CursorType, "android/database/Cursor"); + +static QJniObject &contentResolverInstance() +{ + static QJniObject contentResolver; + if (!contentResolver.isValid()) { + contentResolver = QJniObject(QNativeInterface::QAndroidApplication::context()) + .callMethod<QtJniTypes::ContentResolverType>("getContentResolver"); + } + + return contentResolver; +} + +AndroidContentFileEngine::AndroidContentFileEngine(const QString &filename) + : m_initialFile(filename), + m_documentFile(DocumentFile::parseFromAnyUri(filename)) { - setFileName(f); + setFileName(filename); } bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, @@ -29,6 +50,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, } if (openMode & QFileDevice::WriteOnly) { openModeStr += u'w'; + if (!m_documentFile->exists()) { + if (QUrl(m_initialFile).path().startsWith("/tree/"_L1)) { + const int lastSeparatorIndex = m_initialFile.lastIndexOf('/'); + const QString fileName = m_initialFile.mid(lastSeparatorIndex + 1); + + QString mimeType; + const auto mimeTypes = QMimeDatabase().mimeTypesForFileName(fileName); + if (!mimeTypes.empty()) + mimeType = mimeTypes.first().name(); + else + mimeType = "application/octet-stream"; + + if (m_documentFile->parent()) { + auto createdFile = m_documentFile->parent()->createFile(mimeType, fileName); + if (createdFile) + m_documentFile = createdFile; + } + } else { + qWarning() << "open(): non-existent content URI with a document type provided"; + } + } } if (openMode & QFileDevice::Truncate) { openModeStr += u't'; @@ -36,21 +78,19 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, openModeStr += u'a'; } - m_pfd = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative", - "openParcelFdForContentUrl", - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", - QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object(), - QJniObject::fromString(openModeStr).object()); + m_pfd = contentResolverInstance().callMethod< + QtJniTypes::ParcelFileDescriptorType, QtJniTypes::UriType, jstring>( + "openFileDescriptor", + m_documentFile->uri().object(), + QJniObject::fromString(openModeStr).object<jstring>()); if (!m_pfd.isValid()) return false; - const auto fd = m_pfd.callMethod<jint>("getFd", "()I"); + const auto fd = m_pfd.callMethod<jint>("getFd"); if (fd < 0) { - m_pfd.callMethod<void>("close", "()V"); - m_pfd = QJniObject(); + closeNativeFileDescriptor(); return false; } @@ -59,47 +99,130 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, bool AndroidContentFileEngine::close() { + closeNativeFileDescriptor(); + return QFSFileEngine::close(); +} + +void AndroidContentFileEngine::closeNativeFileDescriptor() +{ if (m_pfd.isValid()) { - m_pfd.callMethod<void>("close", "()V"); + m_pfd.callMethod<void>("close"); m_pfd = QJniObject(); } - - return QFSFileEngine::close(); } qint64 AndroidContentFileEngine::size() const { - const jlong size = QJniObject::callStaticMethod<jlong>( - "org/qtproject/qt/android/QtNative", "getSize", - "(Landroid/content/Context;Ljava/lang/String;)J", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - return (qint64)size; + return m_documentFile->length(); +} + +bool AndroidContentFileEngine::remove() +{ + return m_documentFile->remove(); +} + +bool AndroidContentFileEngine::rename(const QString &newName) +{ + if (m_documentFile->rename(newName)) { + m_initialFile = m_documentFile->uri().toString(); + return true; + } + return false; +} + +bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories, + std::optional<QFileDevice::Permissions> permissions) const +{ + Q_UNUSED(permissions) + + QString tmp = dirName; + tmp.remove(m_initialFile); + + QStringList dirParts = tmp.split(u'/'); + dirParts.removeAll(""); + + if (dirParts.isEmpty()) + return false; + + auto createdDir = m_documentFile; + bool allDirsCreated = true; + for (const auto &dir : dirParts) { + // Find if the sub-dir already exists and then don't re-create it + bool subDirExists = false; + for (const DocumentFilePtr &subDir : m_documentFile->listFiles()) { + if (dir == subDir->name() && subDir->isDirectory()) { + createdDir = subDir; + subDirExists = true; + } + } + + if (!subDirExists) { + createdDir = createdDir->createDirectory(dir); + if (!createdDir) { + allDirsCreated = false; + break; + } + } + + if (!createParentDirectories) + break; + } + + return allDirsCreated; +} + +bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const +{ + if (recurseParentDirectories) + qWarning() << "rmpath(): Unsupported for Content URIs"; + + const QString dirFileName = QUrl(dirName).fileName(); + bool deleted = false; + for (const DocumentFilePtr &dir : m_documentFile->listFiles()) { + if (dirFileName == dir->name() && dir->isDirectory()) { + deleted = dir->remove(); + break; + } + } + + return deleted; +} + +QByteArray AndroidContentFileEngine::id() const +{ + return m_documentFile->id().toUtf8(); +} + +QDateTime AndroidContentFileEngine::fileTime(QFile::FileTime time) const +{ + switch (time) { + case QFile::FileModificationTime: + return m_documentFile->lastModified(); + break; + default: + break; + } + + return QDateTime(); } AndroidContentFileEngine::FileFlags AndroidContentFileEngine::fileFlags(FileFlags type) const { - FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag); FileFlags flags; - const bool isDir = QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - // If it is a directory then we know it exists so there is no reason to explicitly check - const bool exists = isDir ? true : QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkFileExists", - "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - if (!exists && !isDir) + if (!m_documentFile->exists()) + return flags; + + flags = ExistsFlag; + if (!m_documentFile->canRead()) return flags; - if (isDir) { - flags = DirectoryType | commonFlags; + + flags |= ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm; + + if (m_documentFile->isDirectory()) { + flags |= DirectoryType; } else { - flags = FileType | commonFlags; - const bool writable = QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkIfWritable", - "(Landroid/content/Context;Ljava/lang/String;)Z", QAndroidApplication::context(), - QJniObject::fromString(fileName(DefaultName)).object()); - if (writable) + flags |= FileType; + if (m_documentFile->canWrite()) flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm; } return type & flags; @@ -114,41 +237,39 @@ QString AndroidContentFileEngine::fileName(FileName f) const case DefaultName: case AbsoluteName: case CanonicalName: - return m_file; + return m_documentFile->uri().toString(); case BaseName: - { - const qsizetype pos = m_file.lastIndexOf(u'/'); - return m_file.mid(pos); - } + return m_documentFile->name(); default: - return QString(); + break; } -} -QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, const QStringList &filterNames) -{ - return new AndroidContentFileEngineIterator(filters, filterNames); + return QString(); } -QAbstractFileEngine::Iterator *AndroidContentFileEngine::endEntryList() +QAbstractFileEngine::IteratorUniquePtr +AndroidContentFileEngine::beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) { - return nullptr; + return std::make_unique<AndroidContentFileEngineIterator>(path, filters, filterNames); } AndroidContentFileEngineHandler::AndroidContentFileEngineHandler() = default; AndroidContentFileEngineHandler::~AndroidContentFileEngineHandler() = default; -QAbstractFileEngine* AndroidContentFileEngineHandler::create(const QString &fileName) const +std::unique_ptr<QAbstractFileEngine> +AndroidContentFileEngineHandler::create(const QString &fileName) const { - if (!fileName.startsWith("content"_L1)) - return nullptr; + if (fileName.startsWith("content"_L1)) + return std::make_unique<AndroidContentFileEngine>(fileName); + + return {}; - return new AndroidContentFileEngine(fileName); } -AndroidContentFileEngineIterator::AndroidContentFileEngineIterator(QDir::Filters filters, - const QStringList &filterNames) - : QAbstractFileEngineIterator(filters, filterNames) +AndroidContentFileEngineIterator::AndroidContentFileEngineIterator( + const QString &path, QDir::Filters filters, const QStringList &filterNames) + : QAbstractFileEngineIterator(path, filters, filterNames) { } @@ -156,47 +277,546 @@ AndroidContentFileEngineIterator::~AndroidContentFileEngineIterator() { } -QString AndroidContentFileEngineIterator::next() +bool AndroidContentFileEngineIterator::advance() { - if (!hasNext()) - return QString(); - ++m_index; - return currentFilePath(); -} + if (m_index == -1 && m_files.isEmpty()) { + const auto currentPath = path(); + if (currentPath.isEmpty()) + return false; -bool AndroidContentFileEngineIterator::hasNext() const -{ - if (m_index == -1) { - if (path().isEmpty()) + const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath); + if (iterDoc->isDirectory()) + for (const auto &doc : iterDoc->listFiles()) + m_files.append(doc); + if (m_files.isEmpty()) return false; - const bool isDir = QJniObject::callStaticMethod<jboolean>( - "org/qtproject/qt/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", - QAndroidApplication::context(), - QJniObject::fromString(path()).object()); - if (isDir) { - QJniObject objArray = QJniObject::callStaticObjectMethod("org/qtproject/qt/android/QtNative", - "listContentsFromTreeUri", - "(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;", - QAndroidApplication::context(), - QJniObject::fromString(path()).object()); - if (objArray.isValid()) { - QJniEnvironment env; - const jsize length = env->GetArrayLength(objArray.object<jarray>()); - for (int i = 0; i != length; ++i) { - m_entries << QJniObject(env->GetObjectArrayElement( - objArray.object<jobjectArray>(), i)).toString(); - } - } - } m_index = 0; + return true; + } + + if (m_index < m_files.size() - 1) { + ++m_index; + return true; } - return m_index < m_entries.size(); + + return false; } QString AndroidContentFileEngineIterator::currentFileName() const { - if (m_index <= 0 || m_index > m_entries.size()) + if (m_index < 0 || m_index > m_files.size()) + return QString(); + return m_files.at(m_index)->name(); +} + +QString AndroidContentFileEngineIterator::currentFilePath() const +{ + if (m_index < 0 || m_index > m_files.size()) return QString(); - return m_entries.at(m_index - 1); + return m_files.at(m_index)->uri().toString(); } + +// Start of Cursor + +class Cursor +{ +public: + explicit Cursor(const QJniObject &object) + : m_object{object} { } + + ~Cursor() + { + if (m_object.isValid()) + m_object.callMethod<void>("close"); + } + + enum Type { + FIELD_TYPE_NULL = 0x00000000, + FIELD_TYPE_INTEGER = 0x00000001, + FIELD_TYPE_FLOAT = 0x00000002, + FIELD_TYPE_STRING = 0x00000003, + FIELD_TYPE_BLOB = 0x00000004 + }; + + QVariant data(int columnIndex) const + { + int type = m_object.callMethod<jint>("getType", columnIndex); + switch (type) { + case FIELD_TYPE_NULL: + return {}; + case FIELD_TYPE_INTEGER: + return QVariant::fromValue(m_object.callMethod<jlong>("getLong", columnIndex)); + case FIELD_TYPE_FLOAT: + return QVariant::fromValue(m_object.callMethod<jdouble>("getDouble", columnIndex)); + case FIELD_TYPE_STRING: + return QVariant::fromValue(m_object.callMethod<jstring>("getString", + columnIndex).toString()); + case FIELD_TYPE_BLOB: { + auto blob = m_object.callMethod<jbyteArray>("getBlob", columnIndex); + QJniEnvironment env; + const auto blobArray = blob.object<jbyteArray>(); + const int size = env->GetArrayLength(blobArray); + const auto byteArray = env->GetByteArrayElements(blobArray, nullptr); + QByteArray data{reinterpret_cast<const char *>(byteArray), size}; + env->ReleaseByteArrayElements(blobArray, byteArray, 0); + return QVariant::fromValue(data); + } + } + return {}; + } + + static std::unique_ptr<Cursor> queryUri(const QJniObject &uri, + const QStringList &projection = {}, + const QString &selection = {}, + const QStringList &selectionArgs = {}, + const QString &sortOrder = {}) + { + auto cursor = contentResolverInstance().callMethod<QtJniTypes::CursorType>( + "query", + uri.object<QtJniTypes::UriType>(), + QJniArray(projection), + selection.isEmpty() ? nullptr : QJniObject::fromString(selection).object<jstring>(), + QJniArray(selectionArgs), + sortOrder.isEmpty() ? nullptr : QJniObject::fromString(sortOrder).object<jstring>()); + if (!cursor.isValid()) + return {}; + return std::make_unique<Cursor>(cursor); + } + + static QVariant queryColumn(const QJniObject &uri, const QString &column) + { + const auto query = queryUri(uri, {column}); + if (!query) + return {}; + + if (query->rowCount() != 1 || query->columnCount() != 1) + return {}; + query->moveToFirst(); + return query->data(0); + } + + bool isNull(int columnIndex) const + { + return m_object.callMethod<jboolean>("isNull", columnIndex); + } + + int columnCount() const { return m_object.callMethod<jint>("getColumnCount"); } + int rowCount() const { return m_object.callMethod<jint>("getCount"); } + int row() const { return m_object.callMethod<jint>("getPosition"); } + bool isFirst() const { return m_object.callMethod<jboolean>("isFirst"); } + bool isLast() const { return m_object.callMethod<jboolean>("isLast"); } + bool moveToFirst() { return m_object.callMethod<jboolean>("moveToFirst"); } + bool moveToLast() { return m_object.callMethod<jboolean>("moveToLast"); } + bool moveToNext() { return m_object.callMethod<jboolean>("moveToNext"); } + +private: + QJniObject m_object; +}; + +// End of Cursor + +// Start of DocumentsContract + +Q_DECLARE_JNI_CLASS(DocumentsContract, "android/provider/DocumentsContract"); + +/*! + * + * DocumentsContract Api. + * Check https://developer.android.com/reference/android/provider/DocumentsContract + * for more information. + * + * \note This does not implement all facilities of the native API. + * + */ +namespace DocumentsContract +{ + +namespace Document { +const QLatin1String COLUMN_DISPLAY_NAME("_display_name"); +const QLatin1String COLUMN_DOCUMENT_ID("document_id"); +const QLatin1String COLUMN_FLAGS("flags"); +const QLatin1String COLUMN_LAST_MODIFIED("last_modified"); +const QLatin1String COLUMN_MIME_TYPE("mime_type"); +const QLatin1String COLUMN_SIZE("_size"); + +constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008; +constexpr int FLAG_SUPPORTS_DELETE = 0x00000004; +constexpr int FLAG_SUPPORTS_MOVE = 0x00000100; +constexpr int FLAG_SUPPORTS_RENAME = 0x00000040; +constexpr int FLAG_SUPPORTS_WRITE = 0x00000002; +constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200; + +const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory"); +} // namespace Document + +QString documentId(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "getDocumentId", + uri.object()).toString(); +} + +QString treeDocumentId(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jstring, QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "getTreeDocumentId", + uri.object()).toString(); +} + +QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId) +{ + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "buildChildDocumentsUriUsingTree", + uri.object<QtJniTypes::UriType>(), + QJniObject::fromString(parentDocumentId).object<jstring>()); + +} + +QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId) +{ + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "buildDocumentUriUsingTree", + treeUri.object<QtJniTypes::UriType>(), + QJniObject::fromString(documentId).object<jstring>()); +} + +bool isDocumentUri(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jboolean>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "isDocumentUri", + QNativeInterface::QAndroidApplication::context(), + uri.object<QtJniTypes::UriType>()); +} + +bool isTreeUri(const QJniObject &uri) +{ + return QJniObject::callStaticMethod<jboolean>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "isTreeUri", + uri.object<QtJniTypes::UriType>()); +} + +QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType, + const QString &displayName) +{ + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "createDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + parentDocumentUri.object<QtJniTypes::UriType>(), + QJniObject::fromString(mimeType).object<jstring>(), + QJniObject::fromString(displayName).object<jstring>()); +} + +bool deleteDocument(const QJniObject &documentUri) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_DELETE)) + return {}; + + return QJniObject::callStaticMethod<jboolean>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "deleteDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + documentUri.object<QtJniTypes::UriType>()); +} + +QJniObject moveDocument(const QJniObject &sourceDocumentUri, + const QJniObject &sourceParentDocumentUri, + const QJniObject &targetParentDocumentUri) +{ + const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_MOVE)) + return {}; + + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "moveDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + sourceDocumentUri.object<QtJniTypes::UriType>(), + sourceParentDocumentUri.object<QtJniTypes::UriType>(), + targetParentDocumentUri.object<QtJniTypes::UriType>()); +} + +QJniObject renameDocument(const QJniObject &documentUri, const QString &displayName) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_RENAME)) + return {}; + + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(), + "renameDocument", + contentResolverInstance().object<QtJniTypes::ContentResolverType>(), + documentUri.object<QtJniTypes::UriType>(), + QJniObject::fromString(displayName).object<jstring>()); +} +} // End DocumentsContract namespace + +// Start of DocumentFile + +using namespace DocumentsContract; + +namespace { +class MakeableDocumentFile : public DocumentFile +{ +public: + MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {}) + : DocumentFile(uri, parent) + {} +}; +} + +DocumentFile::DocumentFile(const QJniObject &uri, + const DocumentFilePtr &parent) + : m_uri{uri} + , m_parent{parent} +{} + +QJniObject parseUri(const QString &uri) +{ + QString uriToParse = uri; + if (uriToParse.contains(' ')) + uriToParse.replace(' ', QUrl::toPercentEncoding(" ")); + + return QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::Uri>::className(), + "parse", + QJniObject::fromString(uriToParse).object<jstring>()); +} + +DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName) +{ + const QString encodedUri = QUrl(fileName).toEncoded(); + const QJniObject uri = parseUri(encodedUri); + + if (DocumentsContract::isDocumentUri(uri) || !DocumentsContract::isTreeUri(uri)) + return fromSingleUri(uri); + + const QString documentType = "/document/"_L1; + const QString treeType = "/tree/"_L1; + + const int treeIndex = encodedUri.indexOf(treeType); + const int documentIndex = encodedUri.indexOf(documentType); + const int index = fileName.lastIndexOf("/"); + + if (index <= treeIndex + treeType.size() || index <= documentIndex + documentType.size()) + return fromTreeUri(uri); + + const QString parentUrl = encodedUri.left(index); + DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl)); + + const QString baseName = encodedUri.mid(index); + const QString fileUrl = parentUrl + QUrl::toPercentEncoding(baseName); + + DocumentFilePtr docFile = std::make_shared<MakeableDocumentFile>(parseUri(fileUrl)); + if (parentDocFile && parentDocFile->isDirectory()) + docFile->m_parent = parentDocFile; + + return docFile; +} + +DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri) +{ + return std::make_shared<MakeableDocumentFile>(uri); +} + +DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri) +{ + QString docId; + if (isDocumentUri(treeUri)) + docId = documentId(treeUri); + else + docId = treeDocumentId(treeUri); + + return std::make_shared<MakeableDocumentFile>(buildDocumentUriUsingTree(treeUri, docId)); +} + +DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName) +{ + if (isDirectory()) { + return std::make_shared<MakeableDocumentFile>( + createDocument(m_uri, mimeType, displayName), + shared_from_this()); + } + return {}; +} + +DocumentFilePtr DocumentFile::createDirectory(const QString &displayName) +{ + if (isDirectory()) { + return std::make_shared<MakeableDocumentFile>( + createDocument(m_uri, Document::MIME_TYPE_DIR, displayName), + shared_from_this()); + } + return {}; +} + +const QJniObject &DocumentFile::uri() const +{ + return m_uri; +} + +const DocumentFilePtr &DocumentFile::parent() const +{ + return m_parent; +} + +QString DocumentFile::name() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString(); +} + +QString DocumentFile::id() const +{ + return DocumentsContract::documentId(uri()); +} + +QString DocumentFile::mimeType() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_MIME_TYPE).toString(); +} + +bool DocumentFile::isDirectory() const +{ + return mimeType() == Document::MIME_TYPE_DIR; +} + +bool DocumentFile::isFile() const +{ + const QString type = mimeType(); + return type != Document::MIME_TYPE_DIR && !type.isEmpty(); +} + +bool DocumentFile::isVirtual() const +{ + return isDocumentUri(m_uri) && (Cursor::queryColumn(m_uri, + Document::COLUMN_FLAGS).toInt() & Document::FLAG_VIRTUAL_DOCUMENT); +} + +QDateTime DocumentFile::lastModified() const +{ + const auto timeVariant = Cursor::queryColumn(m_uri, Document::COLUMN_LAST_MODIFIED); + if (timeVariant.isValid()) + return QDateTime::fromMSecsSinceEpoch(timeVariant.toLongLong()); + return {}; +} + +int64_t DocumentFile::length() const +{ + return Cursor::queryColumn(m_uri, Document::COLUMN_SIZE).toLongLong(); +} + +namespace { +constexpr int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001; +constexpr int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002; +} + +bool DocumentFile::canRead() const +{ + const auto context = QJniObject(QNativeInterface::QAndroidApplication::context()); + const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission", + m_uri.object<QtJniTypes::UriType>(), + FLAG_GRANT_READ_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + return !mimeType().isEmpty(); +} + +bool DocumentFile::canWrite() const +{ + const auto context = QJniObject(QNativeInterface::QAndroidApplication::context()); + const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission", + m_uri.object<QtJniTypes::UriType>(), + FLAG_GRANT_WRITE_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + const QString type = mimeType(); + if (type.isEmpty()) + return false; + + const int flags = Cursor::queryColumn(m_uri, Document::COLUMN_FLAGS).toInt(); + if (flags & Document::FLAG_SUPPORTS_DELETE) + return true; + + const bool supportsWrite = (flags & Document::FLAG_SUPPORTS_WRITE); + const bool isDir = (type == Document::MIME_TYPE_DIR); + const bool dirSupportsCreate = (isDir && (flags & Document::FLAG_DIR_SUPPORTS_CREATE)); + + return dirSupportsCreate || supportsWrite; +} + +bool DocumentFile::remove() +{ + return deleteDocument(m_uri); +} + +bool DocumentFile::exists() const +{ + return !name().isEmpty(); +} + +std::vector<DocumentFilePtr> DocumentFile::listFiles() +{ + std::vector<DocumentFilePtr> res; + const auto childrenUri = buildChildDocumentsUriUsingTree(m_uri, documentId(m_uri)); + const auto query = Cursor::queryUri(childrenUri, {Document::COLUMN_DOCUMENT_ID}); + if (!query) + return res; + + while (query->moveToNext()) { + const auto uri = buildDocumentUriUsingTree(m_uri, query->data(0).toString()); + res.push_back(std::make_shared<MakeableDocumentFile>(uri, shared_from_this())); + } + return res; +} + +bool DocumentFile::rename(const QString &newName) +{ + QJniObject uri; + if (newName.startsWith("content://"_L1)) { + auto lastSeparatorIndex = [](const QString &file) { + int posDecoded = file.lastIndexOf("/"); + int posEncoded = file.lastIndexOf(QUrl::toPercentEncoding("/")); + return posEncoded > posDecoded ? posEncoded : posDecoded; + }; + + // first try to see if the new file is under the same tree and thus used rename only + const QString parent = m_uri.toString().left(lastSeparatorIndex(m_uri.toString())); + if (newName.contains(parent)) { + QString displayName = newName.mid(lastSeparatorIndex(newName)); + if (displayName.startsWith('/')) + displayName.remove(0, 1); + else if (displayName.startsWith(QUrl::toPercentEncoding("/"))) + displayName.remove(0, 3); + + uri = renameDocument(m_uri, displayName); + } else { + // Move + QJniObject srcParentUri = fromTreeUri(parseUri(parent))->uri(); + const QString destParent = newName.left(lastSeparatorIndex(newName)); + QJniObject targetParentUri = fromTreeUri(parseUri(destParent))->uri(); + uri = moveDocument(m_uri, srcParentUri, targetParentUri); + } + } else { + uri = renameDocument(m_uri, newName); + } + + if (uri.isValid()) { + m_uri = uri; + return true; + } + + return false; +} + +QT_END_NAMESPACE + +// End of DocumentFile diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index e58c990c51..a5dd1b30f3 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -5,7 +5,13 @@ #define ANDROIDCONTENTFILEENGINE_H #include <private/qfsfileengine_p.h> + #include <QtCore/qjniobject.h> +#include <QtCore/qlist.h> + +QT_BEGIN_NAMESPACE + +using DocumentFilePtr = std::shared_ptr<class DocumentFile>; class AndroidContentFileEngine : public QFSFileEngine { @@ -14,35 +20,93 @@ public: bool open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) override; bool close() override; qint64 size() const override; + bool remove() override; + bool rename(const QString &newName) override; + bool mkdir(const QString &dirName, bool createParentDirectories, + std::optional<QFile::Permissions> permissions = std::nullopt) const override; + bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; + QByteArray id() const override; + bool caseSensitive() const override { return true; } + QDateTime fileTime(QFile::FileTime time) const override; FileFlags fileFlags(FileFlags type = FileInfoAll) const override; QString fileName(FileName file = DefaultName) const override; - QAbstractFileEngine::Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override; - QAbstractFileEngine::Iterator *endEntryList() override; + IteratorUniquePtr beginEntryList(const QString &path, QDir::Filters filters, + const QStringList &filterNames) override; + private: - QString m_file; - QJniObject m_pfd; + void closeNativeFileDescriptor(); + QString m_initialFile; + QJniObject m_pfd; + DocumentFilePtr m_documentFile; }; class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler { + Q_DISABLE_COPY_MOVE(AndroidContentFileEngineHandler) public: AndroidContentFileEngineHandler(); ~AndroidContentFileEngineHandler(); - QAbstractFileEngine *create(const QString &fileName) const override; + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; }; class AndroidContentFileEngineIterator : public QAbstractFileEngineIterator { public: - AndroidContentFileEngineIterator(QDir::Filters filters, const QStringList &filterNames); + AndroidContentFileEngineIterator(const QString &path, QDir::Filters filters, + const QStringList &filterNames); ~AndroidContentFileEngineIterator(); - QString next() override; - bool hasNext() const override; + + bool advance() override; + QString currentFileName() const override; + QString currentFilePath() const override; private: - mutable QStringList m_entries; - mutable int m_index = -1; + mutable QList<DocumentFilePtr> m_files; + mutable qsizetype m_index = -1; }; +/*! + * + * DocumentFile Api. + * Check https://developer.android.com/reference/androidx/documentfile/provider/DocumentFile + * for more information. + * + */ +class DocumentFile : public std::enable_shared_from_this<DocumentFile> +{ +public: + static DocumentFilePtr parseFromAnyUri(const QString &filename); + static DocumentFilePtr fromSingleUri(const QJniObject &uri); + static DocumentFilePtr fromTreeUri(const QJniObject &treeUri); + + DocumentFilePtr createFile(const QString &mimeType, const QString &displayName); + DocumentFilePtr createDirectory(const QString &displayName); + const QJniObject &uri() const; + const DocumentFilePtr &parent() const; + QString name() const; + QString id() const; + QString mimeType() const; + bool isDirectory() const; + bool isFile() const; + bool isVirtual() const; + QDateTime lastModified() const; + int64_t length() const; + bool canRead() const; + bool canWrite() const; + bool remove(); + bool exists() const; + std::vector<DocumentFilePtr> listFiles(); + bool rename(const QString &newName); + +protected: + DocumentFile(const QJniObject &uri, const std::shared_ptr<DocumentFile> &parent); + +protected: + QJniObject m_uri; + DocumentFilePtr m_parent; +}; + +QT_END_NAMESPACE + #endif // ANDROIDCONTENTFILEENGINE_H diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp index 38b1ed0952..2010a9e03b 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.cpp +++ b/src/plugins/platforms/android/androidjniaccessibility.cpp @@ -1,6 +1,7 @@ // Copyright (C) 2021 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 +#include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" #include "androidjnimain.h" #include "qandroidplatformintegration.h" @@ -14,11 +15,12 @@ #include <QtCore/private/qjnihelpers_p.h> #include <QtCore/QJniObject> #include <QtGui/private/qhighdpiscaling_p.h> + #include <QtCore/QObject> +#include <QtCore/qpointer.h> #include <QtCore/qvarlengtharray.h> static const char m_qtTag[] = "Qt A11Y"; -static const char m_classErrorMsg[] = "Can't find class \"%s\""; QT_BEGIN_NAMESPACE @@ -47,7 +49,7 @@ namespace QtAndroidAccessibility // Because of that almost every method here is split into two parts. // The _helper part is executed in the context of m_accessibilityContext // on the main thread. The other part is executed in Java thread. - static QPointer<QObject> m_accessibilityContext = nullptr; + Q_CONSTINIT static QPointer<QObject> m_accessibilityContext = {}; // This method is called from the Qt main thread, and normally a // QGuiApplication instance will be used as a parent. @@ -61,6 +63,14 @@ namespace QtAndroidAccessibility template <typename Func, typename Ret> void runInObjectContext(QObject *context, Func &&func, Ret *retVal) { + AndroidDeadlockProtector protector; + if (!protector.acquire()) { + __android_log_print(ANDROID_LOG_WARN, m_qtTag, + "Could not run accessibility call in object context, accessing " + "main thread could lead to deadlock"); + return; + } + if (!QtAndroid::blockEventLoopsWhenSuspended() || QGuiApplication::applicationState() != Qt::ApplicationSuspended) { QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal); @@ -72,8 +82,7 @@ namespace QtAndroidAccessibility void initialize() { - QJniObject::callStaticMethod<void>(QtAndroid::applicationClass(), - "initializeAccessibility"); + QtAndroid::initializeAccessibility(); } bool isActive() @@ -118,6 +127,12 @@ namespace QtAndroidAccessibility QtAndroid::notifyObjectHide(accessibilityObjectId, parentObjectId); } + void notifyObjectShow(uint accessibilityObjectId) + { + const auto parentObjectId = parentId_helper(accessibilityObjectId); + QtAndroid::notifyObjectShow(parentObjectId); + } + void notifyObjectFocus(uint accessibilityObjectId) { QtAndroid::notifyObjectFocus(accessibilityObjectId); @@ -131,6 +146,11 @@ namespace QtAndroidAccessibility QtAndroid::notifyValueChanged(accessibilityObjectId, value); } + void notifyScrolledEvent(uint accessiblityObjectId) + { + QtAndroid::notifyScrolledEvent(accessiblityObjectId); + } + static QVarLengthArray<int, 8> childIdListForAccessibleObject_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); @@ -188,7 +208,7 @@ namespace QtAndroidAccessibility return result; } - static QRect screenRect_helper(int objectId) + static QRect screenRect_helper(int objectId, bool clip = true) { QRect rect; QAccessibleInterface *iface = interfaceFromId(objectId); @@ -196,7 +216,7 @@ namespace QtAndroidAccessibility rect = QHighDpi::toNativePixels(iface->rect(), iface->window()); } // If the widget is not fully in-bound in its parent then we have to clip the rectangle to draw - if (iface && iface->parent() && iface->parent()->isValid()) { + if (clip && iface && iface->parent() && iface->parent()->isValid()) { const auto parentRect = QHighDpi::toNativePixels(iface->parent()->rect(), iface->parent()->window()); rect = rect.intersected(parentRect); } @@ -298,34 +318,44 @@ namespace QtAndroidAccessibility static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) { bool result = false; + + const auto& ids = childIdListForAccessibleObject_helper(objectId); + if (ids.isEmpty()) + return false; + + const int firstChildId = ids.first(); + const QRect oldPosition = screenRect_helper(firstChildId, false); + if (m_accessibilityContext) { runInObjectContext(m_accessibilityContext, [objectId]() { return scroll_helper(objectId, QAccessibleActionInterface::increaseAction()); }, &result); } - return result; + + // Don't check for position change if the call was not successful + return result && oldPosition != screenRect_helper(firstChildId, false); } static jboolean scrollBackward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) { bool result = false; + + const auto& ids = childIdListForAccessibleObject_helper(objectId); + if (ids.isEmpty()) + return false; + + const int firstChildId = ids.first(); + const QRect oldPosition = screenRect_helper(firstChildId, false); + if (m_accessibilityContext) { runInObjectContext(m_accessibilityContext, [objectId]() { return scroll_helper(objectId, QAccessibleActionInterface::decreaseAction()); }, &result); } - return result; - } - - -#define FIND_AND_CHECK_CLASS(CLASS_NAME) \ -clazz = env->FindClass(CLASS_NAME); \ -if (!clazz) { \ - __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_classErrorMsg, CLASS_NAME); \ - return JNI_FALSE; \ -} - //__android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); + // Don't check for position change if the call was not successful + return result && oldPosition != screenRect_helper(firstChildId, false); + } static QString textFromValue(QAccessibleInterface *iface) { @@ -440,6 +470,7 @@ if (!clazz) { \ QAccessible::Role role; QStringList actions; QString description; + QString identifier; bool hasTextSelection = false; int selectionStart = 0; int selectionEnd = 0; @@ -455,6 +486,7 @@ if (!clazz) { \ info.role = iface->role(); info.actions = QAccessibleBridgeUtils::effectiveActionNames(iface); info.description = descriptionForInterface(iface); + info.identifier = QAccessibleBridgeUtils::accessibleId(iface); QAccessibleTextInterface *textIface = iface->textInterface(); if (textIface && (textIface->selectionCount() > 0)) { info.hasTextSelection = true; @@ -520,10 +552,12 @@ if (!clazz) { \ //CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc) env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc); + QJniObject(node).callMethod<void>("setViewIdResourceName", info.identifier); + return true; } - static JNINativeMethod methods[] = { + static const JNINativeMethod methods[] = { {"setActive","(Z)V",(void*)setActive}, {"childIdListForAccessibleObject", "(I)[I", (jintArray)childIdListForAccessibleObject}, {"parentId", "(I)I", (void*)parentId}, @@ -543,13 +577,10 @@ if (!clazz) { \ return false; \ } - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { - jclass clazz; - FIND_AND_CHECK_CLASS("org/qtproject/qt/android/accessibility/QtNativeAccessibility"); - jclass appClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods("org/qtproject/qt/android/QtNativeAccessibility", + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt A11y", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/androidjniaccessibility.h b/src/plugins/platforms/android/androidjniaccessibility.h index 94e64762e4..6e8e059334 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.h +++ b/src/plugins/platforms/android/androidjniaccessibility.h @@ -9,16 +9,19 @@ QT_BEGIN_NAMESPACE class QObject; +class QJniEnvironment; namespace QtAndroidAccessibility { void initialize(); bool isActive(); - bool registerNatives(JNIEnv *env); + bool registerNatives(QJniEnvironment &env); void notifyLocationChange(uint accessibilityObjectId); void notifyObjectHide(uint accessibilityObjectId); + void notifyObjectShow(uint accessibilityObjectId); void notifyObjectFocus(uint accessibilityObjectId); void notifyValueChanged(uint accessibilityObjectId); + void notifyScrolledEvent(uint accessibilityObjectId); void createAccessibilityContextObject(QObject *parent); } diff --git a/src/plugins/platforms/android/androidjniclipboard.cpp b/src/plugins/platforms/android/androidjniclipboard.cpp deleted file mode 100644 index 7b2c2c0443..0000000000 --- a/src/plugins/platforms/android/androidjniclipboard.cpp +++ /dev/null @@ -1,98 +0,0 @@ -// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#include "androidjniclipboard.h" -#include <QtCore/QUrl> -#include <QtCore/QJniObject> -#include <QtCore/QJniEnvironment> - -QT_BEGIN_NAMESPACE - -using namespace QtAndroid; -namespace QtAndroidClipboard -{ - QAndroidPlatformClipboard *m_manager = nullptr; - - static JNINativeMethod methods[] = { - {"onClipboardDataChanged", "()V", (void *)onClipboardDataChanged} - }; - - void setClipboardManager(QAndroidPlatformClipboard *manager) - { - m_manager = manager; - QJniObject::callStaticMethod<void>(applicationClass(), "registerClipboardManager"); - jclass appClass = QtAndroid::applicationClass(); - QJniEnvironment env; - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { - __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); - return; - } - } - void clearClipboardData() - { - QJniObject::callStaticMethod<void>(applicationClass(), "clearClipData"); - } - void setClipboardMimeData(QMimeData *data) - { - clearClipboardData(); - if (data->hasText()) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardText", "(Ljava/lang/String;)V", - QJniObject::fromString(data->text()).object()); - } - if (data->hasHtml()) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardHtml", - "(Ljava/lang/String;Ljava/lang/String;)V", - QJniObject::fromString(data->text()).object(), - QJniObject::fromString(data->html()).object()); - } - if (data->hasUrls()) { - QList<QUrl> urls = data->urls(); - for (const auto &u : qAsConst(urls)) { - QJniObject::callStaticMethod<void>(applicationClass(), - "setClipboardUri", - "(Ljava/lang/String;)V", - QJniObject::fromString(u.toEncoded()).object()); - } - } - } - - QMimeData *getClipboardMimeData() - { - QMimeData *data = new QMimeData; - if (QJniObject::callStaticMethod<jboolean>(applicationClass(), "hasClipboardText")) { - data->setText(QJniObject::callStaticObjectMethod(applicationClass(), - "getClipboardText", - "()Ljava/lang/String;").toString()); - } - if (QJniObject::callStaticMethod<jboolean>(applicationClass(), "hasClipboardHtml")) { - data->setHtml(QJniObject::callStaticObjectMethod(applicationClass(), - "getClipboardHtml", - "()Ljava/lang/String;").toString()); - } - if (QJniObject::callStaticMethod<jboolean>(applicationClass(), "hasClipboardUri")) { - QJniObject uris = QJniObject::callStaticObjectMethod(applicationClass(), - "getClipboardUris", - "()[Ljava/lang/String;"); - if (uris.isValid()) { - QList<QUrl> urls; - QJniEnvironment env; - jobjectArray juris = uris.object<jobjectArray>(); - const jint nUris = env->GetArrayLength(juris); - urls.reserve(static_cast<int>(nUris)); - for (int i = 0; i < nUris; ++i) - urls << QUrl(QJniObject(env->GetObjectArrayElement(juris, i)).toString()); - data->setUrls(urls); - } - } - return data; - } - - void onClipboardDataChanged(JNIEnv */*env*/, jobject /*thiz*/) - { - m_manager->emitChanged(QClipboard::Clipboard); - } -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjniclipboard.h b/src/plugins/platforms/android/androidjniclipboard.h deleted file mode 100644 index 24feeef9b3..0000000000 --- a/src/plugins/platforms/android/androidjniclipboard.h +++ /dev/null @@ -1,27 +0,0 @@ -// Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef ANDROIDJNICLIPBOARD_H -#define ANDROIDJNICLIPBOARD_H - -#include <QString> -#include "qandroidplatformclipboard.h" -#include "androidjnimain.h" - -QT_BEGIN_NAMESPACE - -class QAndroidPlatformClipboard; -namespace QtAndroidClipboard -{ - // Clipboard support - void setClipboardManager(QAndroidPlatformClipboard *manager); - void setClipboardMimeData(QMimeData *data); - QMimeData *getClipboardMimeData(); - void clearClipboardData(); - void onClipboardDataChanged(JNIEnv */*env*/, jobject /*thiz*/); - // Clipboard support -} - -QT_END_NAMESPACE - -#endif // ANDROIDJNICLIPBOARD_H diff --git a/src/plugins/platforms/android/androidjniinput.cpp b/src/plugins/platforms/android/androidjniinput.cpp index fae8668f63..2692488ec6 100644 --- a/src/plugins/platforms/android/androidjniinput.cpp +++ b/src/plugins/platforms/android/androidjniinput.cpp @@ -1,3 +1,4 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // Copyright (C) 2016 Olivier Goffart <ogoffart@woboq.com> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -8,6 +9,7 @@ #include "androidjnimain.h" #include "qandroidplatformintegration.h" +#include <qpa/qplatformwindow.h> #include <qpa/qwindowsysteminterface.h> #include <QTouchEvent> #include <QPointer> @@ -17,66 +19,69 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaInputMethods, "qt.qpa.input.methods"); + using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtLayout, "org/qtproject/qt/android/QtLayout") +Q_DECLARE_JNI_CLASS(QtLayoutInterface, "org/qtproject/qt/android/QtLayoutInterface") +Q_DECLARE_JNI_CLASS(QtInputInterface, "org/qtproject/qt/android/QtInputInterface") + namespace QtAndroidInput { static bool m_ignoreMouseEvents = false; + static Qt::MouseButtons m_buttons = Qt::NoButton; + static QRect m_softwareKeyboardRect; static QList<QWindowSystemInterface::TouchPoint> m_touchPoints; static QPointer<QWindow> m_mouseGrabber; + QJniObject qtLayout() + { + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtLayoutInterface, QtJniTypes::QtLayout>( + "getQtLayout"); + } + void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; -#endif - QJniObject::callStaticMethod<void>(applicationClass(), - "updateSelection", - "(IIII)V", - selStart, - selEnd, - candidatesStart, - candidatesEnd); + qCDebug(lcQpaInputMethods) << ">>> UPDATESELECTION" << selStart << selEnd << candidatesStart << candidatesEnd; + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("updateSelection", selStart, selEnd, + candidatesStart, candidatesEnd); } void showSoftwareKeyboard(int left, int top, int width, int height, int inputHints, int enterKeyType) { - QJniObject::callStaticMethod<void>(applicationClass(), - "showSoftwareKeyboard", - "(IIIIII)V", - left, - top, - width, - height, - inputHints, - enterKeyType); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>( + "showSoftwareKeyboard", QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), left, top, width, height, inputHints, + enterKeyType); + qCDebug(lcQpaInputMethods) << "@@@ SHOWSOFTWAREKEYBOARD" << left << top << width << height << inputHints << enterKeyType; } void resetSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ RESETSOFTWAREKEYBOARD"); -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("resetSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ RESETSOFTWAREKEYBOARD"; } void hideSoftwareKeyboard() { - QJniObject::callStaticMethod<void>(applicationClass(), "hideSoftwareKeyboard"); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ HIDESOFTWAREKEYBOARD"); -#endif + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>("hideSoftwareKeyboard"); + qCDebug(lcQpaInputMethods) << "@@@ HIDESOFTWAREKEYBOARD"; } bool isSoftwareKeyboardVisible() { - return QJniObject::callStaticMethod<jboolean>(applicationClass(), "isSoftwareKeyboardVisible"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtInputInterface, jboolean>( + "isSoftwareKeyboardVisible"); } QRect softwareKeyboardRect() @@ -86,81 +91,150 @@ namespace QtAndroidInput int getSelectHandleWidth() { - return QJniObject::callStaticMethod<jint>(applicationClass(), "getSelectHandleWidth"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + return reg->callInterface<QtJniTypes::QtInputInterface, jint>("getSelectHandleWidth"); } void updateHandles(int mode, QPoint editMenuPos, uint32_t editButtons, QPoint cursor, QPoint anchor, bool rtl) { - QJniObject::callStaticMethod<void>(applicationClass(), "updateHandles", "(IIIIIIIIZ)V", - mode, editMenuPos.x(), editMenuPos.y(), editButtons, - cursor.x(), cursor.y(), - anchor.x(), anchor.y(), rtl); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtInputInterface, void>( + "updateHandles", QtAndroidPrivate::activity(), + qtLayout().object<QtJniTypes::QtLayout>(), mode, editMenuPos.x(), editMenuPos.y(), + editButtons, cursor.x(), cursor.y(), anchor.x(), anchor.y(), rtl); + } + + // from https://developer.android.com/reference/android/view/MotionEvent#getButtonState() + enum AndroidMouseButton { + BUTTON_PRIMARY = 0x00000001, + BUTTON_SECONDARY = 0x00000002, + BUTTON_TERTIARY = 0x00000004, + BUTTON_BACK = 0x00000008, + BUTTON_FORWARD = 0x00000010, + BUTTON_STYLUS_PRIMARY = 0x00000020, + BUTTON_STYLUS_SECONDARY = 0x00000040, + }; + Q_DECLARE_FLAGS(AndroidMouseButtons, AndroidMouseButton) + + static Qt::MouseButtons toMouseButtons(jint j_buttons) + { + const auto buttons = static_cast<AndroidMouseButtons>(j_buttons); + Qt::MouseButtons mouseButtons; + if (buttons.testFlag(BUTTON_PRIMARY)) + mouseButtons.setFlag(Qt::LeftButton); + + if (buttons.testFlag(BUTTON_SECONDARY)) + mouseButtons.setFlag(Qt::RightButton); + + if (buttons.testFlag(BUTTON_TERTIARY)) + mouseButtons.setFlag(Qt::MiddleButton); + + if (buttons.testFlag(BUTTON_BACK)) + mouseButtons.setFlag(Qt::BackButton); + + if (buttons.testFlag(BUTTON_FORWARD)) + mouseButtons.setFlag(Qt::ForwardButton); + + if (buttons.testFlag(BUTTON_STYLUS_PRIMARY)) + mouseButtons.setFlag(Qt::LeftButton); + + if (buttons.testFlag(BUTTON_STYLUS_SECONDARY)) + mouseButtons.setFlag(Qt::RightButton); + + // Fall back to left button + if (Q_UNLIKELY(buttons != 0 && mouseButtons == Qt::NoButton)) { + qWarning() << "Unhandled button value:" << buttons << "Falling back to Qt::LeftButton"; + mouseButtons = Qt::LeftButton; + } + return mouseButtons; + } + + static void sendMouseButtonEvents(QWindow *topLevel, QPoint localPos, QPoint globalPos, + jint mouseButtonState, QEvent::Type type) + { + const Qt::MouseButtons mouseButtons = toMouseButtons(mouseButtonState); + const Qt::MouseButtons changedButtons = mouseButtons & ~m_buttons; + + if (changedButtons == Qt::NoButton) + return; + + static_assert (sizeof(changedButtons) <= sizeof(uint), "Qt::MouseButtons size changed. Adapt code."); + + for (uint buttonInt = 0x1; static_cast<uint>(changedButtons) >= buttonInt; buttonInt <<= 1) { + const auto button = static_cast<Qt::MouseButton>(buttonInt); + if (changedButtons.testFlag(button)) { + QWindowSystemInterface::handleMouseEvent(topLevel, localPos, globalPos, + mouseButtons, button, type); + } + } } - static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jint mouseButtonState) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = topLevelWindowAt(globalPos); - m_mouseGrabber = tlw; - QPoint localPos = tlw ? (globalPos - tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, - Qt::MouseButtons(Qt::LeftButton), - Qt::LeftButton, QEvent::MouseButtonPress); - } - - static void mouseUp(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) - { - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos -tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, - Qt::MouseButtons(Qt::NoButton), - Qt::LeftButton, QEvent::MouseButtonRelease); + const QPoint globalPos(x,y); + QWindow *window = windowFromId(winId); + m_mouseGrabber = window; + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + sendMouseButtonEvents(window, localPos, globalPos, mouseButtonState, QEvent::MouseButtonPress); + } + + static void mouseUp(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jint mouseButtonState) + { + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + + sendMouseButtonEvents(window, localPos, globalPos, mouseButtonState, QEvent::MouseButtonRelease); m_ignoreMouseEvents = false; - m_mouseGrabber = 0; + m_mouseGrabber.clear(); } - static void mouseMove(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void mouseMove(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(m_mouseGrabber ? Qt::LeftButton : Qt::NoButton), Qt::NoButton, QEvent::MouseMove); } - static void mouseWheel(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y, jfloat hdelta, jfloat vdelta) + static void mouseWheel(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y, jfloat hdelta, jfloat vdelta) { if (m_ignoreMouseEvents) return; - QPoint globalPos(x,y); - QWindow *tlw = m_mouseGrabber.data(); - if (!tlw) - tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; - QPoint angleDelta(hdelta * 120, vdelta * 120); + const QPoint globalPos(x,y); + QWindow *window = m_mouseGrabber.data(); + if (!window) + window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; + const QPoint angleDelta(hdelta * 120, vdelta * 120); - QWindowSystemInterface::handleWheelEvent(tlw, + QWindowSystemInterface::handleWheelEvent(window, localPos, globalPos, QPoint(), angleDelta); } - static void longPress(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) + static void longPress(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint x, jint y) { QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) @@ -171,16 +245,17 @@ namespace QtAndroidInput if (!rightMouseFromLongPress) return; m_ignoreMouseEvents = true; - QPoint globalPos(x,y); - QWindow *tlw = topLevelWindowAt(globalPos); - QPoint localPos = tlw ? (globalPos-tlw->position()) : globalPos; + const QPoint globalPos(x,y); + QWindow *window = windowFromId(winId); + const QPoint localPos = window && window->handle() ? + window->handle()->mapFromGlobal(globalPos) : globalPos; // Click right button if no other button is already pressed. if (!m_mouseGrabber) { - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(Qt::RightButton), Qt::RightButton, QEvent::MouseButtonPress); - QWindowSystemInterface::handleMouseEvent(tlw, localPos, globalPos, + QWindowSystemInterface::handleMouseEvent(window, localPos, globalPos, Qt::MouseButtons(Qt::NoButton), Qt::RightButton, QEvent::MouseButtonRelease); } @@ -191,7 +266,7 @@ namespace QtAndroidInput m_touchPoints.clear(); } - static void touchAdd(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint id, jint action, jboolean /*primary*/, jint x, jint y, + static void touchAdd(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint id, jint action, jboolean /*primary*/, jint x, jint y, jfloat major, jfloat minor, jfloat rotation, jfloat pressure) { QEventPoint::State state = QEventPoint::State::Stationary; @@ -212,18 +287,26 @@ namespace QtAndroidInput const int dw = availableWidthPixels(); const int dh = availableHeightPixels(); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) { + qCWarning(lcQpaInputMethods, "Touch event received for non-existing window %d", winId); + return; + } + + QPointF mappedTouchPoint = window->mapToGlobal(QPointF(x, y)); QWindowSystemInterface::TouchPoint touchPoint; touchPoint.id = id; touchPoint.pressure = pressure; touchPoint.rotation = qRadiansToDegrees(rotation); - touchPoint.normalPosition = QPointF(double(x / dw), double(y / dh)); + touchPoint.normalPosition = QPointF((mappedTouchPoint.x() / dw), + (mappedTouchPoint.y() / dh)); touchPoint.state = state; - touchPoint.area = QRectF(x - double(minor), - y - double(major), - double(minor * 2), - double(major * 2)); - m_touchPoints.push_back(touchPoint); + touchPoint.area = QRectF(mappedTouchPoint.x() - double(minor * 0.5f), + mappedTouchPoint.y() - double(major * 0.5f), + double(minor), + double(major)); + m_touchPoints.push_back(touchPoint); if (state == QEventPoint::State::Pressed) { QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) @@ -254,31 +337,35 @@ namespace QtAndroidInput return touchDevice; } - static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/, jint /*action*/) + static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint winId, jint /*action*/) { if (m_touchPoints.isEmpty()) return; QMutexLocker lock(QtAndroid::platformInterfaceMutex()); - QPointingDevice *touchDevice = getTouchDevice(); + const QPointingDevice *touchDevice = getTouchDevice(); if (!touchDevice) return; - QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) + return; QWindowSystemInterface::handleTouchEvent(window, touchDevice, m_touchPoints); } - static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/) + static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint winId) { if (m_touchPoints.isEmpty()) return; QMutexLocker lock(QtAndroid::platformInterfaceMutex()); - QPointingDevice *touchDevice = getTouchDevice(); + const QPointingDevice *touchDevice = getTouchDevice(); if (!touchDevice) return; - QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindow *window = QtAndroid::windowFromId(winId); + if (!window) + return; QWindowSystemInterface::handleTouchCancelEvent(window, touchDevice); } @@ -291,14 +378,14 @@ namespace QtAndroidInput #endif // QT_CONFIG(tabletevent) } - static void tabletEvent(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint deviceId, jlong time, jint action, + static void tabletEvent(JNIEnv */*env*/, jobject /*thiz*/, jint winId, jint deviceId, jlong time, jint action, jint pointerType, jint buttonState, jfloat x, jfloat y, jfloat pressure) { #if QT_CONFIG(tabletevent) - QPointF globalPosF(x, y); - QPoint globalPos((int)x, (int)y); - QWindow *tlw = topLevelWindowAt(globalPos); - QPointF localPos = tlw ? (globalPosF - tlw->position()) : globalPosF; + const QPointF globalPosF(x, y); + QWindow *window = windowFromId(winId); + const QPointF localPos = window && window->handle() ? + window->handle()->mapFromGlobalF(globalPosF) : globalPosF; // Galaxy Note with plain Android: // 0 1 0 stylus press @@ -318,6 +405,7 @@ namespace QtAndroidInput Qt::MouseButtons buttons = Qt::NoButton; switch (action) { case 1: // ACTION_UP + case 6: // ACTION_POINTER_UP, happens if stylus is not the primary pointer case 212: // stylus release while side-button held on Galaxy Note 4 buttons = Qt::NoButton; break; @@ -329,11 +417,9 @@ namespace QtAndroidInput break; } -#ifdef QT_DEBUG_ANDROID_STYLUS - qDebug() << action << pointerType << buttonState << '@' << x << y << "pressure" << pressure << ": buttons" << buttons; -#endif + qCDebug(lcQpaInputMethods) << action << pointerType << buttonState << '@' << x << y << "pressure" << pressure << ": buttons" << buttons; - QWindowSystemInterface::handleTabletEvent(tlw, ulong(time), + QWindowSystemInterface::handleTabletEvent(window, ulong(time), localPos, globalPosF, int(QInputDevice::DeviceType::Stylus), pointerType, buttons, pressure, 0, 0, 0., 0., 0, deviceId, Qt::NoModifier); #endif // QT_CONFIG(tabletevent) @@ -797,9 +883,7 @@ namespace QtAndroidInput QMetaObject::invokeMethod(inputContext, "hideSelectionHandles", Qt::QueuedConnection); } } -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ KEYBOARDVISIBILITYCHANGED" << inputContext; -#endif + qCDebug(lcQpaInputMethods) << "@@@ KEYBOARDVISIBILITYCHANGED" << inputContext; } static void keyboardGeometryChanged(JNIEnv */*env*/, jobject /*thiz*/, jint x, jint y, jint w, jint h) @@ -812,16 +896,12 @@ namespace QtAndroidInput if (inputContext && qGuiApp) inputContext->emitKeyboardRectChanged(); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ KEYBOARDRECTCHANGED" << m_softwareKeyboardRect; -#endif + qCDebug(lcQpaInputMethods) << "@@@ KEYBOARDRECTCHANGED" << m_softwareKeyboardRect; } static void handleLocationChanged(JNIEnv */*env*/, jobject /*thiz*/, int id, int x, int y) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ handleLocationChanged" << id << x << y; -#endif + qCDebug(lcQpaInputMethods) << "@@@ handleLocationChanged" << id << x << y; QAndroidInputContext *inputContext = QAndroidInputContext::androidInputContext(); if (inputContext && qGuiApp) QMetaObject::invokeMethod(inputContext, "handleLocationChanged", Qt::BlockingQueuedConnection, @@ -829,13 +909,14 @@ namespace QtAndroidInput } - static JNINativeMethod methods[] = { + + static const JNINativeMethod methods[] = { {"touchBegin","(I)V",(void*)touchBegin}, {"touchAdd","(IIIZIIFFFF)V",(void*)touchAdd}, {"touchEnd","(II)V",(void*)touchEnd}, {"touchCancel", "(I)V", (void *)touchCancel}, - {"mouseDown", "(III)V", (void *)mouseDown}, - {"mouseUp", "(III)V", (void *)mouseUp}, + {"mouseDown", "(IIII)V", (void *)mouseDown}, + {"mouseUp", "(IIII)V", (void *)mouseUp}, {"mouseMove", "(III)V", (void *)mouseMove}, {"mouseWheel", "(IIIFF)V", (void *)mouseWheel}, {"longPress", "(III)V", (void *)longPress}, @@ -845,14 +926,13 @@ namespace QtAndroidInput {"keyUp", "(IIIZ)V", (void *)keyUp}, {"keyboardVisibilityChanged", "(Z)V", (void *)keyboardVisibilityChanged}, {"keyboardGeometryChanged", "(IIII)V", (void *)keyboardGeometryChanged}, - {"handleLocationChanged", "(III)V", (void *)handleLocationChanged} + {"handleLocationChanged", "(III)V", (void *)handleLocationChanged}, }; - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { - jclass appClass = QtAndroid::applicationClass(); - - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(QtJniTypes::Traits<QtJniTypes::QtInputDelegate>::className(), + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/androidjniinput.h b/src/plugins/platforms/android/androidjniinput.h index 7ef51ef9f7..629b2937f0 100644 --- a/src/plugins/platforms/android/androidjniinput.h +++ b/src/plugins/platforms/android/androidjniinput.h @@ -6,10 +6,15 @@ #include <jni.h> #include <QtCore/qglobal.h> +#include <QtCore/QLoggingCategory> #include <QtCore/QRect> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaInputMethods); + +class QJniEnvironment; + namespace QtAndroidInput { // Software keyboard support @@ -26,7 +31,7 @@ namespace QtAndroidInput QPoint cursor = QPoint(), QPoint anchor = QPoint(), bool rtl = false); int getSelectHandleWidth(); - bool registerNatives(JNIEnv *env); + bool registerNatives(QJniEnvironment &env); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp index 3b5e656630..5bd2b924fc 100644 --- a/src/plugins/platforms/android/androidjnimain.cpp +++ b/src/plugins/platforms/android/androidjnimain.cpp @@ -10,14 +10,16 @@ #include "androidcontentfileengine.h" #include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" -#include "androidjniclipboard.h" #include "androidjniinput.h" #include "androidjnimain.h" #include "androidjnimenu.h" +#include "androidwindowembedding.h" #include "qandroidassetsfileenginehandler.h" #include "qandroideventdispatcher.h" #include "qandroidplatformdialoghelpers.h" #include "qandroidplatformintegration.h" +#include "qandroidplatformclipboard.h" +#include "qandroidplatformwindow.h" #include <android/api-level.h> #include <android/asset_manager_jni.h> @@ -27,13 +29,18 @@ #include <QtCore/qbasicatomic.h> #include <QtCore/qjnienvironment.h> #include <QtCore/qjniobject.h> +#include <QtCore/qprocess.h> #include <QtCore/qresource.h> +#include <QtCore/qscopeguard.h> #include <QtCore/qthread.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qhighdpiscaling_p.h> #include <qpa/qwindowsysteminterface.h> + +using namespace Qt::StringLiterals; + QT_BEGIN_NAMESPACE static JavaVM *m_javaVM = nullptr; @@ -43,11 +50,9 @@ static jmethodID m_loadClassMethodID = nullptr; static AAssetManager *m_assetManager = nullptr; static jobject m_assets = nullptr; static jobject m_resourcesObj = nullptr; -static QtJniTypes::Activity m_activityObject = nullptr; -static jmethodID m_createSurfaceMethodID = nullptr; -static QtJniTypes::Service m_serviceObject = nullptr; -static jmethodID m_setSurfaceGeometryMethodID = nullptr; -static jmethodID m_destroySurfaceMethodID = nullptr; + +static jclass m_qtActivityClass = nullptr; +static jclass m_qtServiceClass = nullptr; static int m_pendingApplicationState = -1; static QBasicMutex m_platformMutex; @@ -66,10 +71,6 @@ static void *m_mainLibraryHnd = nullptr; static QList<QByteArray> m_applicationParams; static sem_t m_exitSemaphore, m_terminateSemaphore; -QHash<int, AndroidSurfaceClient *> m_surfaces; - -static QBasicMutex m_surfacesMutex; - static QAndroidPlatformIntegration *m_androidPlatformIntegration = nullptr; @@ -81,13 +82,16 @@ static double m_density = 1.0; static AndroidAssetsFileEngineHandler *m_androidAssetsFileEngineHandler = nullptr; static AndroidContentFileEngineHandler *m_androidContentFileEngineHandler = nullptr; - +static AndroidBackendRegister *m_backendRegister = nullptr; static const char m_qtTag[] = "Qt"; static const char m_classErrorMsg[] = "Can't find class \"%s\""; static const char m_methodErrorMsg[] = "Can't find method \"%s%s\""; -static QBasicAtomicInt startQtAndroidPluginCalled = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_CONSTINIT static QBasicAtomicInt startQtAndroidPluginCalled = Q_BASIC_ATOMIC_INITIALIZER(0); + +Q_DECLARE_JNI_CLASS(QtWindowInterface, "org/qtproject/qt/android/QtWindowInterface") +Q_DECLARE_JNI_CLASS(QtAccessibilityInterface, "org/qtproject/qt/android/QtAccessibilityInterface"); namespace QtAndroid { @@ -99,6 +103,7 @@ namespace QtAndroid void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration) { m_androidPlatformIntegration = androidPlatformIntegration; + QtAndroid::notifyNativePluginIntegrationReady((bool)m_androidPlatformIntegration); // flush the pending state if necessary. if (m_androidPlatformIntegration && (m_pendingApplicationState != -1)) { @@ -124,6 +129,21 @@ namespace QtAndroid : 0; } + QWindow *windowFromId(int windowId) + { + if (!qGuiApp) + return nullptr; + + for (QWindow *w : qGuiApp->allWindows()) { + if (!w->handle()) + continue; + QAndroidPlatformWindow *window = static_cast<QAndroidPlatformWindow *>(w->handle()); + if (window->nativeViewId() == windowId) + return w; + } + return nullptr; + } + int availableWidthPixels() { return m_availableWidthPixels; @@ -159,47 +179,79 @@ namespace QtAndroid return m_applicationClass; } - QtJniTypes::Activity activity() + // TODO move calls from here to where they logically belong + void setSystemUiVisibility(SystemUiVisibility uiVisibility) { - return m_activityObject; + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("setSystemUiVisibility", + jint(uiVisibility)); } - QtJniTypes::Service service() + bool isQtApplication() { - return m_serviceObject; + // Returns true if the app is a Qt app, i.e. Qt controls the whole app and + // the Activity/Service is created by Qt. Returns false if instead Qt is + // embedded into a native Android app, where the Activity/Service is created + // by the user, outside of Qt, and Qt content is added as a view. + JNIEnv *env = QJniEnvironment::getJniEnv(); + auto activity = QtAndroidPrivate::activity(); + if (activity.isValid()) + return env->IsInstanceOf(activity.object(), m_qtActivityClass); + auto service = QtAndroidPrivate::service(); + if (service.isValid()) + return env->IsInstanceOf(QtAndroidPrivate::service().object(), m_qtServiceClass); + // return true as default as Qt application is our default use case. + // famous last words: we should not end up here + return true; } - void setSystemUiVisibility(SystemUiVisibility uiVisibility) + void initializeAccessibility() { - QJniObject::callStaticMethod<void>(m_applicationClass, "setSystemUiVisibility", "(I)V", jint(uiVisibility)); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "initializeAccessibility"); } void notifyAccessibilityLocationChange(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyAccessibilityLocationChange", - "(I)V", accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyLocationChange", accessibilityObjectId); } void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectHide", "(II)V", - accessibilityObjectId, parentObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectHide", accessibilityObjectId, parentObjectId); + } + + void notifyObjectShow(uint parentObjectId) + { + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectShow", parentObjectId); } void notifyObjectFocus(uint accessibilityObjectId) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyObjectFocus","(I)V", accessibilityObjectId); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyObjectFocus", accessibilityObjectId); } void notifyValueChanged(uint accessibilityObjectId, jstring value) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyValueChanged", - "(ILjava/lang/String;)V", accessibilityObjectId, value); + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyValueChanged", accessibilityObjectId, value); + } + + void notifyScrolledEvent(uint accessibilityObjectId) + { + m_backendRegister->callInterface<QtJniTypes::QtAccessibilityInterface, void>( + "notifyScrolledEvent", accessibilityObjectId); } - void notifyQtAndroidPluginRunning(bool running) + void notifyNativePluginIntegrationReady(bool ready) { - QJniObject::callStaticMethod<void>(m_applicationClass, "notifyQtAndroidPluginRunning","(Z)V", running); + QJniObject::callStaticMethod<void>(m_applicationClass, + "notifyNativePluginIntegrationReady", + ready); } jobject createBitmap(QImage img, JNIEnv *env) @@ -208,7 +260,7 @@ namespace QtAndroid return 0; if (img.format() != QImage::Format_RGBA8888 && img.format() != QImage::Format_RGB16) - img = img.convertToFormat(QImage::Format_RGBA8888); + img = std::move(img).convertToFormat(QImage::Format_RGBA8888); jobject bitmap = env->CallStaticObjectMethod(m_bitmapClass, m_createBitmapMethodID, @@ -296,62 +348,6 @@ namespace QtAndroid return manufacturer + u' ' + model; } - jint generateViewId() - { - return QJniObject::callStaticMethod<jint>("android/view/View", "generateViewId", "()I"); - } - - int createSurface(AndroidSurfaceClient *client, const QRect &geometry, bool onTop, int imageDepth) - { - QJniEnvironment env; - if (!env.jniEnv()) - return -1; - - m_surfacesMutex.lock(); - jint surfaceId = generateViewId(); - m_surfaces[surfaceId] = client; - m_surfacesMutex.unlock(); - - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) { - x = geometry.x(); - y = geometry.y(); - w = std::max(geometry.width(), 1); - h = std::max(geometry.height(), 1); - } - env->CallStaticVoidMethod(m_applicationClass, - m_createSurfaceMethodID, - surfaceId, - jboolean(onTop), - x, y, w, h, - imageDepth); - return surfaceId; - } - - int insertNativeView(jobject view, const QRect &geometry) - { - m_surfacesMutex.lock(); - jint surfaceId = generateViewId(); - m_surfaces[surfaceId] = nullptr; // dummy - m_surfacesMutex.unlock(); - - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) - geometry.getRect(&x, &y, &w, &h); - - QJniObject::callStaticMethod<void>(m_applicationClass, - "insertNativeView", - "(ILandroid/view/View;IIII)V", - surfaceId, - view, - x, - y, - qMax(w, 1), - qMax(h, 1)); - - return surfaceId; - } - void setViewVisibility(jobject view, bool visible) { QJniObject::callStaticMethod<void>(m_applicationClass, @@ -361,69 +357,6 @@ namespace QtAndroid visible); } - void setSurfaceGeometry(int surfaceId, const QRect &geometry) - { - if (surfaceId == -1) - return; - - QJniEnvironment env; - if (!env.jniEnv()) - return; - jint x = 0, y = 0, w = -1, h = -1; - if (!geometry.isNull()) { - x = geometry.x(); - y = geometry.y(); - w = geometry.width(); - h = geometry.height(); - } - env->CallStaticVoidMethod(m_applicationClass, - m_setSurfaceGeometryMethodID, - surfaceId, - x, y, w, h); - } - - - void destroySurface(int surfaceId) - { - if (surfaceId == -1) - return; - - { - QMutexLocker lock(&m_surfacesMutex); - const auto &it = m_surfaces.find(surfaceId); - if (it != m_surfaces.end()) - m_surfaces.erase(it); - } - - QJniEnvironment env; - if (env.jniEnv()) - env->CallStaticVoidMethod(m_applicationClass, - m_destroySurfaceMethodID, - surfaceId); - } - - void bringChildToFront(int surfaceId) - { - if (surfaceId == -1) - return; - - QJniObject::callStaticMethod<void>(m_applicationClass, - "bringChildToFront", - "(I)V", - surfaceId); - } - - void bringChildToBack(int surfaceId) - { - if (surfaceId == -1) - return; - - QJniObject::callStaticMethod<void>(m_applicationClass, - "bringChildToBack", - "(I)V", - surfaceId); - } - bool blockEventLoopsWhenSuspended() { static bool block = qEnvironmentVariableIntValue("QT_BLOCK_EVENT_LOOPS_WHEN_SUSPENDED"); @@ -435,21 +368,27 @@ namespace QtAndroid return m_assets; } + AndroidBackendRegister *backendRegister() + { + return m_backendRegister; + } + } // namespace QtAndroid static jboolean startQtAndroidPlugin(JNIEnv *env, jobject /*object*/, jstring paramsString) { + Q_UNUSED(env) + m_androidPlatformIntegration = nullptr; m_androidAssetsFileEngineHandler = new AndroidAssetsFileEngineHandler(); m_androidContentFileEngineHandler = new AndroidContentFileEngineHandler(); m_mainLibraryHnd = nullptr; + m_backendRegister = new AndroidBackendRegister(); - const char *nativeString = env->GetStringUTFChars(paramsString, 0); - QByteArray string = nativeString; - env->ReleaseStringUTFChars(paramsString, nativeString); + const QStringList argsList = QProcess::splitCommand(QJniObject(paramsString).toString()); - for (auto str : string.split('\t')) - m_applicationParams.append(str.split(' ')); + for (const QString &arg : argsList) + m_applicationParams.append(arg.toUtf8()); // Go home QDir::setCurrent(QDir::homePath()); @@ -489,7 +428,7 @@ static void waitForServiceSetup(JNIEnv *env, jclass /*clazz*/) Q_UNUSED(env); // The service must wait until the QCoreApplication starts otherwise onBind will be // called too early - if (m_serviceObject) + if (QtAndroidPrivate::service().isValid() && QtAndroid::isQtApplication()) QtAndroidPrivate::waitForServiceSetup(); } @@ -520,7 +459,8 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) argv[argc] = nullptr; startQtAndroidPluginCalled.fetchAndAddRelease(1); - int ret = m_main(argc, argv.data()); + const int ret = m_main(argc, argv.data()); + qInfo() << "main() returned" << ret; if (m_mainLibraryHnd) { int res = dlclose(m_mainLibraryHnd); @@ -528,10 +468,8 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) qWarning() << "dlclose failed:" << dlerror(); } - if (m_applicationClass) { - qWarning("exit app 0"); + if (m_applicationClass) QJniObject::callStaticMethod<void>(m_applicationClass, "quitApp", "()V"); - } sem_post(&m_terminateSemaphore); sem_wait(&m_exitSemaphore); @@ -576,10 +514,6 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) env->DeleteGlobalRef(m_classLoaderObject); if (m_resourcesObj) env->DeleteGlobalRef(m_resourcesObj); - if (m_activityObject) - env->DeleteGlobalRef(m_activityObject); - if (m_serviceObject) - env->DeleteGlobalRef(m_serviceObject); if (m_bitmapClass) env->DeleteGlobalRef(m_bitmapClass); if (m_ARGB_8888_BitmapConfigValue) @@ -590,53 +524,51 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) env->DeleteGlobalRef(m_bitmapDrawableClass); if (m_assets) env->DeleteGlobalRef(m_assets); + if (m_qtActivityClass) + env->DeleteGlobalRef(m_qtActivityClass); + if (m_qtServiceClass) + env->DeleteGlobalRef(m_qtServiceClass); m_androidPlatformIntegration = nullptr; delete m_androidAssetsFileEngineHandler; m_androidAssetsFileEngineHandler = nullptr; + delete m_backendRegister; + m_backendRegister = nullptr; sem_post(&m_exitSemaphore); } -static void setSurface(JNIEnv *env, jobject /*thiz*/, jint id, jobject jSurface, jint w, jint h) -{ - QMutexLocker lock(&m_surfacesMutex); - const auto &it = m_surfaces.find(id); - if (it == m_surfaces.end()) - return; - - auto surfaceClient = it.value(); - if (surfaceClient) - surfaceClient->surfaceChanged(env, jSurface, w, h); -} - static void setDisplayMetrics(JNIEnv * /*env*/, jclass /*clazz*/, jint screenWidthPixels, jint screenHeightPixels, jint availableLeftPixels, jint availableTopPixels, jint availableWidthPixels, jint availableHeightPixels, jdouble xdpi, jdouble ydpi, jdouble scaledDensity, jdouble density, jfloat refreshRate) { + Q_UNUSED(availableLeftPixels) + Q_UNUSED(availableTopPixels) + m_availableWidthPixels = availableWidthPixels; m_availableHeightPixels = availableHeightPixels; m_scaledDensity = scaledDensity; m_density = density; + const QSize screenSize(screenWidthPixels, screenHeightPixels); + // available geometry always starts from top left + const QRect availableGeometry(0, 0, availableWidthPixels, availableHeightPixels); + const QSize physicalSize(qRound(double(screenWidthPixels) / xdpi * 25.4), + qRound(double(screenHeightPixels) / ydpi * 25.4)); + QMutexLocker lock(&m_platformMutex); if (!m_androidPlatformIntegration) { QAndroidPlatformIntegration::setDefaultDisplayMetrics( - availableLeftPixels, availableTopPixels, availableWidthPixels, - availableHeightPixels, qRound(double(screenWidthPixels) / xdpi * 25.4), - qRound(double(screenHeightPixels) / ydpi * 25.4), screenWidthPixels, - screenHeightPixels); + availableGeometry.left(), availableGeometry.top(), availableGeometry.width(), + availableGeometry.height(), physicalSize.width(), physicalSize.height(), + screenSize.width(), screenSize.height()); } else { - const QSize physicalSize(qRound(double(screenWidthPixels) / xdpi * 25.4), - qRound(double(screenHeightPixels) / ydpi * 25.4)); - const QSize screenSize(screenWidthPixels, screenHeightPixels); - const QRect availableGeometry(availableLeftPixels, availableTopPixels, - availableWidthPixels, availableHeightPixels); m_androidPlatformIntegration->setScreenSizeParameters(physicalSize, screenSize, availableGeometry); m_androidPlatformIntegration->setRefreshRate(refreshRate); } } +Q_DECLARE_JNI_NATIVE_METHOD(setDisplayMetrics) static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/) { @@ -656,10 +588,6 @@ static void updateWindow(JNIEnv */*env*/, jobject /*thiz*/) QWindowSystemInterface::handleExposeEvent(w, QRegion(QRect(QPoint(), w->geometry().size()))); } } - - QAndroidPlatformScreen *screen = static_cast<QAndroidPlatformScreen *>(m_androidPlatformIntegration->screen()); - if (screen->rasterSurfaces()) - QMetaObject::invokeMethod(screen, "setDirty", Qt::QueuedConnection, Q_ARG(QRect,screen->geometry())); } static void updateApplicationState(JNIEnv */*env*/, jobject /*thiz*/, jint state) @@ -738,12 +666,42 @@ static void handleOrientationChanged(JNIEnv */*env*/, jobject /*thiz*/, jint new } } } +Q_DECLARE_JNI_NATIVE_METHOD(handleOrientationChanged) static void handleRefreshRateChanged(JNIEnv */*env*/, jclass /*cls*/, jfloat refreshRate) { if (m_androidPlatformIntegration) m_androidPlatformIntegration->setRefreshRate(refreshRate); } +Q_DECLARE_JNI_NATIVE_METHOD(handleRefreshRateChanged) + +static void handleScreenAdded(JNIEnv */*env*/, jclass /*cls*/, jint displayId) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->handleScreenAdded(displayId); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenAdded) + +static void handleScreenChanged(JNIEnv */*env*/, jclass /*cls*/, jint displayId) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->handleScreenChanged(displayId); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenChanged) + +static void handleScreenRemoved(JNIEnv */*env*/, jclass /*cls*/, jint displayId) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->handleScreenRemoved(displayId); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleScreenRemoved) + +static void handleUiDarkModeChanged(JNIEnv */*env*/, jobject /*thiz*/, jint newUiMode) +{ + QAndroidPlatformIntegration::updateColorScheme( + (newUiMode == 1 ) ? Qt::ColorScheme::Dark : Qt::ColorScheme::Light); +} +Q_DECLARE_JNI_NATIVE_METHOD(handleUiDarkModeChanged) static void onActivityResult(JNIEnv */*env*/, jclass /*cls*/, jint requestCode, @@ -770,112 +728,131 @@ static JNINativeMethod methods[] = { { "quitQtCoreApplication", "()V", (void *)quitQtCoreApplication }, { "terminateQt", "()V", (void *)terminateQt }, { "waitForServiceSetup", "()V", (void *)waitForServiceSetup }, - { "setDisplayMetrics", "(IIIIIIDDDDF)V", (void *)setDisplayMetrics }, - { "setSurface", "(ILjava/lang/Object;II)V", (void *)setSurface }, { "updateWindow", "()V", (void *)updateWindow }, { "updateApplicationState", "(I)V", (void *)updateApplicationState }, - { "handleOrientationChanged", "(II)V", (void *)handleOrientationChanged }, { "onActivityResult", "(IILandroid/content/Intent;)V", (void *)onActivityResult }, { "onNewIntent", "(Landroid/content/Intent;)V", (void *)onNewIntent }, - { "onBind", "(Landroid/content/Intent;)Landroid/os/IBinder;", (void *)onBind }, - { "handleRefreshRateChanged", "(F)V", (void *)handleRefreshRateChanged } + { "onBind", "(Landroid/content/Intent;)Landroid/os/IBinder;", (void *)onBind } }; #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ clazz = env->FindClass(CLASS_NAME); \ if (!clazz) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_classErrorMsg, CLASS_NAME); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_STATIC_METHOD(VAR, CLASS, METHOD_NAME, METHOD_SIGNATURE) \ VAR = env->GetStaticMethodID(CLASS, METHOD_NAME, METHOD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \ VAR = env->GetFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, FIELD_NAME, FIELD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } #define GET_AND_CHECK_STATIC_FIELD(VAR, CLASS, FIELD_NAME, FIELD_SIGNATURE) \ VAR = env->GetStaticFieldID(CLASS, FIELD_NAME, FIELD_SIGNATURE); \ if (!VAR) { \ __android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, FIELD_NAME, FIELD_SIGNATURE); \ - return JNI_FALSE; \ + return false; \ } -static int registerNatives(JNIEnv *env) +Q_DECLARE_JNI_CLASS(QtDisplayManager, "org/qtproject/qt/android/QtDisplayManager") + +static bool registerNatives(QJniEnvironment &env) { jclass clazz; FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtNative"); m_applicationClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - if (env->RegisterNatives(m_applicationClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(m_applicationClass, + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); - return JNI_FALSE; + return false; } - GET_AND_CHECK_STATIC_METHOD(m_createSurfaceMethodID, m_applicationClass, "createSurface", "(IZIIIII)V"); - GET_AND_CHECK_STATIC_METHOD(m_setSurfaceGeometryMethodID, m_applicationClass, "setSurfaceGeometry", "(IIIII)V"); - GET_AND_CHECK_STATIC_METHOD(m_destroySurfaceMethodID, m_applicationClass, "destroySurface", "(I)V"); + bool success = env.registerNativeMethods( + QtJniTypes::Traits<QtJniTypes::QtDisplayManager>::className(), + { + Q_JNI_NATIVE_METHOD(setDisplayMetrics), + Q_JNI_NATIVE_METHOD(handleOrientationChanged), + Q_JNI_NATIVE_METHOD(handleRefreshRateChanged), + Q_JNI_NATIVE_METHOD(handleScreenAdded), + Q_JNI_NATIVE_METHOD(handleScreenChanged), + Q_JNI_NATIVE_METHOD(handleScreenRemoved), + Q_JNI_NATIVE_METHOD(handleUiDarkModeChanged) + }); + + if (!success) { + qCritical() << "QtDisplayManager: registerNativeMethods() failed"; + return JNI_FALSE; + } jmethodID methodID; GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "activity", "()Landroid/app/Activity;"); - jobject activityObject = env->CallStaticObjectMethod(m_applicationClass, methodID); - GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "service", "()Landroid/app/Service;"); - jobject serviceObject = env->CallStaticObjectMethod(m_applicationClass, methodID); + jobject contextObject = env->CallStaticObjectMethod(m_applicationClass, methodID); + if (!contextObject) { + GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "service", "()Landroid/app/Service;"); + contextObject = env->CallStaticObjectMethod(m_applicationClass, methodID); + } + + if (!contextObject) { + __android_log_print(ANDROID_LOG_FATAL,"Qt", "Failed to get Activity or Service object"); + return false; + } + const auto releaseContextObject = qScopeGuard([&env, contextObject]{ + env->DeleteLocalRef(contextObject); + }); + GET_AND_CHECK_STATIC_METHOD(methodID, m_applicationClass, "classLoader", "()Ljava/lang/ClassLoader;"); m_classLoaderObject = env->NewGlobalRef(env->CallStaticObjectMethod(m_applicationClass, methodID)); clazz = env->GetObjectClass(m_classLoaderObject); GET_AND_CHECK_METHOD(m_loadClassMethodID, clazz, "loadClass", "(Ljava/lang/String;)Ljava/lang/Class;"); - if (serviceObject) - m_serviceObject = env->NewGlobalRef(serviceObject); - - if (activityObject) - m_activityObject = env->NewGlobalRef(activityObject); - - jobject object = activityObject ? activityObject : serviceObject; - if (object) { - FIND_AND_CHECK_CLASS("android/content/ContextWrapper"); - GET_AND_CHECK_METHOD(methodID, clazz, "getAssets", "()Landroid/content/res/AssetManager;"); - m_assets = env->NewGlobalRef(env->CallObjectMethod(object, methodID)); - m_assetManager = AAssetManager_fromJava(env, m_assets); - - GET_AND_CHECK_METHOD(methodID, clazz, "getResources", "()Landroid/content/res/Resources;"); - m_resourcesObj = env->NewGlobalRef(env->CallObjectMethod(object, methodID)); - - FIND_AND_CHECK_CLASS("android/graphics/Bitmap"); - m_bitmapClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - GET_AND_CHECK_STATIC_METHOD(m_createBitmapMethodID, m_bitmapClass - , "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); - FIND_AND_CHECK_CLASS("android/graphics/Bitmap$Config"); - jfieldID fieldId; - GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "ARGB_8888", "Landroid/graphics/Bitmap$Config;"); - m_ARGB_8888_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); - GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "RGB_565", "Landroid/graphics/Bitmap$Config;"); - m_RGB_565_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); - - FIND_AND_CHECK_CLASS("android/graphics/drawable/BitmapDrawable"); - m_bitmapDrawableClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - GET_AND_CHECK_METHOD(m_bitmapDrawableConstructorMethodID, - m_bitmapDrawableClass, - "<init>", - "(Landroid/content/res/Resources;Landroid/graphics/Bitmap;)V"); - } - - return JNI_TRUE; + + FIND_AND_CHECK_CLASS("android/content/ContextWrapper"); + GET_AND_CHECK_METHOD(methodID, clazz, "getAssets", "()Landroid/content/res/AssetManager;"); + m_assets = env->NewGlobalRef(env->CallObjectMethod(contextObject, methodID)); + m_assetManager = AAssetManager_fromJava(env.jniEnv(), m_assets); + + GET_AND_CHECK_METHOD(methodID, clazz, "getResources", "()Landroid/content/res/Resources;"); + m_resourcesObj = env->NewGlobalRef(env->CallObjectMethod(contextObject, methodID)); + + FIND_AND_CHECK_CLASS("android/graphics/Bitmap"); + m_bitmapClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + GET_AND_CHECK_STATIC_METHOD(m_createBitmapMethodID, m_bitmapClass, + "createBitmap", "(IILandroid/graphics/Bitmap$Config;)Landroid/graphics/Bitmap;"); + FIND_AND_CHECK_CLASS("android/graphics/Bitmap$Config"); + jfieldID fieldId; + GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "ARGB_8888", "Landroid/graphics/Bitmap$Config;"); + m_ARGB_8888_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); + GET_AND_CHECK_STATIC_FIELD(fieldId, clazz, "RGB_565", "Landroid/graphics/Bitmap$Config;"); + m_RGB_565_BitmapConfigValue = env->NewGlobalRef(env->GetStaticObjectField(clazz, fieldId)); + + FIND_AND_CHECK_CLASS("android/graphics/drawable/BitmapDrawable"); + m_bitmapDrawableClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + GET_AND_CHECK_METHOD(m_bitmapDrawableConstructorMethodID, + m_bitmapDrawableClass, + "<init>", "(Landroid/content/res/Resources;Landroid/graphics/Bitmap;)V"); + + FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtActivityBase"); + m_qtActivityClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtServiceBase"); + m_qtServiceClass = static_cast<jclass>(env->NewGlobalRef(clazz)); + + return true; } QT_END_NAMESPACE @@ -888,36 +865,30 @@ Q_DECL_EXPORT jint JNICALL JNI_OnLoad(JavaVM *vm, void */*reserved*/) initialized = true; QT_USE_NAMESPACE - typedef union { - JNIEnv *nativeEnvironment; - void *venv; - } UnionJNIEnvToVoid; - - UnionJNIEnvToVoid uenv; - uenv.venv = nullptr; - m_javaVM = nullptr; - - if (vm->GetEnv(&uenv.venv, JNI_VERSION_1_6) != JNI_OK) { - __android_log_print(ANDROID_LOG_FATAL, "Qt", "GetEnv failed"); + m_javaVM = vm; + QJniEnvironment env; + if (!env.isValid()) { + m_javaVM = nullptr; + __android_log_print(ANDROID_LOG_FATAL, "Qt", "Failed to initialize the JNI Environment"); return -1; } - JNIEnv *env = uenv.nativeEnvironment; if (!registerNatives(env) || !QtAndroidInput::registerNatives(env) || !QtAndroidMenu::registerNatives(env) || !QtAndroidAccessibility::registerNatives(env) - || !QtAndroidDialogHelpers::registerNatives(env)) { + || !QtAndroidDialogHelpers::registerNatives(env) + || !QAndroidPlatformClipboard::registerNatives(env) + || !QAndroidPlatformWindow::registerNatives(env) + || !QtAndroidWindowEmbedding::registerNatives(env) + || !AndroidBackendRegister::registerNatives()) { __android_log_print(ANDROID_LOG_FATAL, "Qt", "registerNatives failed"); return -1; } QWindowSystemInterfacePrivate::TabletEvent::setPlatformSynthesizesMouse(false); - m_javaVM = vm; // attach qt main thread data to this thread - QObject threadSetter; - if (threadSetter.thread()) - threadSetter.thread()->setObjectName("QtMainLoopThread"); + QThread::currentThread()->setObjectName("QtMainLoopThread"); __android_log_print(ANDROID_LOG_INFO, "Qt", "qt started"); return JNI_VERSION_1_6; } diff --git a/src/plugins/platforms/android/androidjnimain.h b/src/plugins/platforms/android/androidjnimain.h index 8d05e31f66..9d616b18fb 100644 --- a/src/plugins/platforms/android/androidjnimain.h +++ b/src/plugins/platforms/android/androidjnimain.h @@ -12,6 +12,8 @@ #include <QImage> #include <private/qjnihelpers_p.h> +#include <QtCore/QJniObject> +#include <androidbackendregister.h> QT_BEGIN_NAMESPACE @@ -22,26 +24,23 @@ class QAndroidPlatformIntegration; class QWidget; class QString; class QWindow; -class AndroidSurfaceClient; +class QAndroidPlatformWindow; class QBasicMutex; +Q_DECLARE_JNI_CLASS(QtActivityDelegateBase, "org/qtproject/qt/android/QtActivityDelegateBase") +Q_DECLARE_JNI_CLASS(QtInputDelegate, "org/qtproject/qt/android/QtInputDelegate") + namespace QtAndroid { QBasicMutex *platformInterfaceMutex(); QAndroidPlatformIntegration *androidPlatformIntegration(); + AndroidBackendRegister *backendRegister(); void setAndroidPlatformIntegration(QAndroidPlatformIntegration *androidPlatformIntegration); void setQtThread(QThread *thread); - - - int createSurface(AndroidSurfaceClient * client, const QRect &geometry, bool onTop, int imageDepth); - int insertNativeView(jobject view, const QRect &geometry); void setViewVisibility(jobject view, bool visible); - void setSurfaceGeometry(int surfaceId, const QRect &geometry); - void destroySurface(int surfaceId); - void bringChildToFront(int surfaceId); - void bringChildToBack(int surfaceId); QWindow *topLevelWindowAt(const QPoint &globalPos); + QWindow *windowFromId(int windowId); int availableWidthPixels(); int availableHeightPixels(); double scaledDensity(); @@ -50,8 +49,6 @@ namespace QtAndroid jobject assets(); AAssetManager *assetManager(); jclass applicationClass(); - QtJniTypes::Activity activity(); - QtJniTypes::Service service(); // Keep synchronized with flags in ActivityDelegate.java enum SystemUiVisibility { @@ -65,11 +62,14 @@ namespace QtAndroid jobject createBitmap(int width, int height, QImage::Format format, JNIEnv *env); jobject createBitmapDrawable(jobject bitmap, JNIEnv *env = nullptr); + void initializeAccessibility(); void notifyAccessibilityLocationChange(uint accessibilityObjectId); void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId); + void notifyObjectShow(uint parentObjectId); void notifyObjectFocus(uint accessibilityObjectId); void notifyValueChanged(uint accessibilityObjectId, jstring value); - void notifyQtAndroidPluginRunning(bool running); + void notifyScrolledEvent(uint accessibilityObjectId); + void notifyNativePluginIntegrationReady(bool ready); const char *classErrorMsgFmt(); const char *methodErrorMsgFmt(); @@ -77,6 +77,8 @@ namespace QtAndroid QString deviceName(); bool blockEventLoopsWhenSuspended(); + + bool isQtApplication(); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjnimenu.cpp b/src/plugins/platforms/android/androidjnimenu.cpp index 7b0091d277..5f8a0b92e9 100644 --- a/src/plugins/platforms/android/androidjnimenu.cpp +++ b/src/plugins/platforms/android/androidjnimenu.cpp @@ -20,18 +20,18 @@ QT_BEGIN_NAMESPACE using namespace QtAndroid; +Q_DECLARE_JNI_CLASS(QtMenuInterface, "org/qtproject/qt/android/QtMenuInterface"); + namespace QtAndroidMenu { static QList<QAndroidPlatformMenu *> pendingContextMenus; static QAndroidPlatformMenu *visibleMenu = nullptr; - static QRecursiveMutex visibleMenuMutex; + Q_CONSTINIT static QRecursiveMutex visibleMenuMutex; static QSet<QAndroidPlatformMenuBar *> menuBars; static QAndroidPlatformMenuBar *visibleMenuBar = nullptr; static QWindow *activeTopLevelWindow = nullptr; - static QRecursiveMutex menuBarMutex; - - static jmethodID openContextMenuMethodID = 0; + Q_CONSTINIT static QRecursiveMutex menuBarMutex; static jmethodID clearMenuMethodID = 0; static jmethodID addMenuItemMethodID = 0; @@ -46,29 +46,35 @@ namespace QtAndroidMenu void resetMenuBar() { - QJniObject::callStaticMethod<void>(applicationClass(), "resetOptionsMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("resetOptionsMenu"); } void openOptionsMenu() { - QJniObject::callStaticMethod<void>(applicationClass(), "openOptionsMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("openOptionsMenu"); } - void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env) + void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu) pendingContextMenus.append(visibleMenu); visibleMenu = menu; menu->aboutToShow(); - env->CallStaticVoidMethod(applicationClass(), openContextMenuMethodID, anchorRect.x(), anchorRect.y(), anchorRect.width(), anchorRect.height()); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("openContextMenu", anchorRect.x(), + anchorRect.y(), anchorRect.width(), + anchorRect.height()); } void hideContextMenu(QAndroidPlatformMenu *menu) { QMutexLocker lock(&visibleMenuMutex); if (visibleMenu == menu) { - QJniObject::callStaticMethod<void>(applicationClass(), "closeContextMenu"); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtMenuInterface, void>("closeContextMenu"); pendingContextMenus.clear(); } else { pendingContextMenus.removeOne(menu); @@ -117,7 +123,7 @@ namespace QtAndroidMenu visibleMenuBar = 0; activeTopLevelWindow = window; - for (QAndroidPlatformMenuBar *menuBar : qAsConst(menuBars)) { + for (QAndroidPlatformMenuBar *menuBar : std::as_const(menuBars)) { if (menuBar->parentWindow() == window) { visibleMenuBar = menuBar; resetMenuBar(); @@ -211,8 +217,10 @@ namespace QtAndroidMenu return order; } - static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static jboolean onPrepareOptionsMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) + env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) @@ -249,8 +257,11 @@ namespace QtAndroidMenu return order ? JNI_TRUE : JNI_FALSE; } - static jboolean onOptionsItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) + static jboolean onOptionsItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { + Q_UNUSED(env) + Q_UNUSED(thiz) + QMutexLocker lock(&menuBarMutex); if (!visibleMenuBar) return JNI_FALSE; @@ -260,7 +271,7 @@ namespace QtAndroidMenu QAndroidPlatformMenuItem *item = static_cast<QAndroidPlatformMenuItem *>(menus.front()->menuItemForId(menuId)); if (item) { if (item->menu()) { - showContextMenu(item->menu(), QRect(), env); + showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); @@ -270,18 +281,23 @@ namespace QtAndroidMenu } else { QAndroidPlatformMenu *menu = static_cast<QAndroidPlatformMenu *>(visibleMenuBar->menuForId(menuId)); if (menu) - showContextMenu(menu, QRect(), env); + showContextMenu(menu, QRect()); } return JNI_TRUE; } - static void onOptionsMenuClosed(JNIEnv */*env*/, jobject /*thiz*/, jobject /*menu*/) + static void onOptionsMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(env) + Q_UNUSED(thiz) + Q_UNUSED(menu) } - static void onCreateContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static void onCreateContextMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) + env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) @@ -295,8 +311,9 @@ namespace QtAndroidMenu addAllMenuItemsToMenu(env, menu, visibleMenu); } - static void fillContextMenu(JNIEnv *env, jobject /*thiz*/, jobject menu) + static void fillContextMenu(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(thiz) env->CallVoidMethod(menu, clearMenuMethodID); QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) @@ -305,20 +322,23 @@ namespace QtAndroidMenu addAllMenuItemsToMenu(env, menu, visibleMenu); } - static jboolean onContextItemSelected(JNIEnv *env, jobject /*thiz*/, jint menuId, jboolean checked) + static jboolean onContextItemSelected(JNIEnv *env, jobject thiz, jint menuId, jboolean checked) { + Q_UNUSED(env) + Q_UNUSED(thiz) + QMutexLocker lock(&visibleMenuMutex); QAndroidPlatformMenuItem * item = static_cast<QAndroidPlatformMenuItem *>(visibleMenu->menuItemForId(menuId)); if (item) { if (item->menu()) { - showContextMenu(item->menu(), QRect(), env); + showContextMenu(item->menu(), QRect()); } else { if (item->isCheckable()) item->setChecked(checked); item->activated(); visibleMenu->aboutToHide(); visibleMenu = 0; - for (QAndroidPlatformMenu *menu : qAsConst(pendingContextMenus)) { + for (QAndroidPlatformMenu *menu : std::as_const(pendingContextMenus)) { if (menu->isVisible()) menu->aboutToHide(); } @@ -328,8 +348,12 @@ namespace QtAndroidMenu return JNI_TRUE; } - static void onContextMenuClosed(JNIEnv *env, jobject /*thiz*/, jobject /*menu*/) + static void onContextMenuClosed(JNIEnv *env, jobject thiz, jobject menu) { + Q_UNUSED(env) + Q_UNUSED(thiz) + Q_UNUSED(menu) + QMutexLocker lock(&visibleMenuMutex); if (!visibleMenu) return; @@ -337,7 +361,7 @@ namespace QtAndroidMenu visibleMenu->aboutToHide(); visibleMenu = 0; if (!pendingContextMenus.empty()) - showContextMenu(pendingContextMenus.takeLast(), QRect(), env); + showContextMenu(pendingContextMenus.takeLast(), QRect()); } static JNINativeMethod methods[] = { @@ -378,17 +402,15 @@ namespace QtAndroidMenu return false; \ } - bool registerNatives(JNIEnv *env) + bool registerNatives(QJniEnvironment &env) { jclass appClass = applicationClass(); - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods(appClass, methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL,"Qt", "RegisterNatives failed"); return false; } - GET_AND_CHECK_STATIC_METHOD(openContextMenuMethodID, appClass, "openContextMenu", "(IIII)V"); - jclass clazz; FIND_AND_CHECK_CLASS("android/view/Menu"); GET_AND_CHECK_METHOD(clearMenuMethodID, clazz, "clear", "()V"); diff --git a/src/plugins/platforms/android/androidjnimenu.h b/src/plugins/platforms/android/androidjnimenu.h index 1645ce750b..e10ad930d9 100644 --- a/src/plugins/platforms/android/androidjnimenu.h +++ b/src/plugins/platforms/android/androidjnimenu.h @@ -15,12 +15,13 @@ class QAndroidPlatformMenuItem; class QWindow; class QRect; class QPoint; +class QJniEnvironment; namespace QtAndroidMenu { // Menu support void openOptionsMenu(); - void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect, JNIEnv *env); + void showContextMenu(QAndroidPlatformMenu *menu, const QRect &anchorRect); void hideContextMenu(QAndroidPlatformMenu *menu); void syncMenu(QAndroidPlatformMenu *menu); void androidPlatformMenuDestroyed(QAndroidPlatformMenu *menu); @@ -31,7 +32,7 @@ namespace QtAndroidMenu void removeMenuBar(QAndroidPlatformMenuBar *menuBar); // Menu support - bool registerNatives(JNIEnv *env); + bool registerNatives(QJniEnvironment &env); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidsurfaceclient.h b/src/plugins/platforms/android/androidsurfaceclient.h deleted file mode 100644 index dded9a1f66..0000000000 --- a/src/plugins/platforms/android/androidsurfaceclient.h +++ /dev/null @@ -1,24 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only - -#ifndef ANDROIDSURFACECLIENT_H -#define ANDROIDSURFACECLIENT_H -#include <QMutex> -#include <jni.h> - -QT_BEGIN_NAMESPACE - -class AndroidSurfaceClient -{ -public: - virtual void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) = 0; - void lockSurface() { m_surfaceMutex.lock(); } - void unlockSurface() { m_surfaceMutex.unlock(); } - -protected: - QMutex m_surfaceMutex; -}; - -QT_END_NAMESPACE - -#endif // ANDROIDSURFACECLIENT_H diff --git a/src/plugins/platforms/android/androidwindowembedding.cpp b/src/plugins/platforms/android/androidwindowembedding.cpp new file mode 100644 index 0000000000..65dabcac66 --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.cpp @@ -0,0 +1,70 @@ +// Copyright (C) 2023 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 + +#include "androidwindowembedding.h" + +#include <QtCore/qcoreapplication.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtGui/qwindow.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(QtView, "org/qtproject/qt/android/QtView"); + +namespace QtAndroidWindowEmbedding { + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView, + jint x, jint y, jint width, jint height) + { + // QWindow should be constructed on the Qt thread rather than directly in the caller thread + // To avoid hitting checkReceiverThread assert in QCoreApplication::doNotify + QMetaObject::invokeMethod(qApp, [rootView, x, y, width, height] { + QWindow *parentWindow = QWindow::fromWinId(reinterpret_cast<WId>(rootView.object())); + parentWindow->setGeometry(x, y, width, height); + rootView.callMethod<void>("createWindow", reinterpret_cast<jlong>(parentWindow)); + }); + } + + void deleteWindow(JNIEnv *, jclass, jlong windowRef) + { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + window->deleteLater(); + } + + void setWindowVisible(JNIEnv *, jclass, jlong windowRef, jboolean visible) + { + QMetaObject::invokeMethod(qApp, [windowRef, visible] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + if (visible) { + window->showNormal(); + if (!window->parent()->isVisible()) + window->parent()->showNormal(); + } else { + window->hide(); + } + }); + } + + void resizeWindow(JNIEnv *, jclass, jlong windowRef, jint x, jint y, jint width, jint height) + { + QMetaObject::invokeMethod(qApp, [windowRef, x, y, width, height] { + QWindow *window = reinterpret_cast<QWindow*>(windowRef); + QWindow *parent = window->parent(); + if (parent) + parent->setGeometry(x, y, width, height); + window->setGeometry(0, 0, width, height); + }); + } + + bool registerNatives(QJniEnvironment& env) { + return env.registerNativeMethods( + QtJniTypes::Traits<QtJniTypes::QtView>::className(), + { Q_JNI_NATIVE_SCOPED_METHOD(createRootWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(deleteWindow, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(setWindowVisible, QtAndroidWindowEmbedding), + Q_JNI_NATIVE_SCOPED_METHOD(resizeWindow, QtAndroidWindowEmbedding) }); + } +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidwindowembedding.h b/src/plugins/platforms/android/androidwindowembedding.h new file mode 100644 index 0000000000..b7b0e1205f --- /dev/null +++ b/src/plugins/platforms/android/androidwindowembedding.h @@ -0,0 +1,41 @@ +// Copyright (C) 2023 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 + +#ifndef QTANDROIDWINDOWEMBEDDING_H +#define QTANDROIDWINDOWEMBEDDING_H + +// +// W A R N I N G +// ------------- +// +// This file is not part of the Qt API. It exists purely as an +// implementation detail. This header file may change from version to +// version without notice, or even be removed. +// +// We mean it. +// + +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjnitypes.h> + +QT_BEGIN_NAMESPACE + +Q_DECLARE_JNI_CLASS(View, "android/view/View"); + +namespace QtAndroidWindowEmbedding +{ + bool registerNatives(QJniEnvironment& env); + void createRootWindow(JNIEnv *, jclass, QtJniTypes::View rootView, + jint x, jint y,jint width, jint height); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(createRootWindow) + void deleteWindow(JNIEnv *, jclass, jlong window); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(deleteWindow) + void setWindowVisible(JNIEnv *, jclass, jlong window, jboolean visible); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setWindowVisible) + void resizeWindow(JNIEnv *, jclass, jlong windowRef, jint x, jint y, jint width, jint height); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(resizeWindow) +}; + +QT_END_NAMESPACE + +#endif // QTANDROIDWINDOWEMBEDDING_H diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 36fa2dd945..4ea6536cef 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -108,6 +108,8 @@ public: FolderIterator(const QString &path) : m_path(path) { + // Note that empty dirs in the assets dir before the build are not going to be + // included in the final apk, so no empty folders should expected to be listed. QJniObject files = QJniObject::callStaticObjectMethod(QtAndroid::applicationClass(), "listAssetContent", "(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;", @@ -140,17 +142,13 @@ public: return m_path + at(m_index).name; } - bool hasNext() const + bool advance() { - return !empty() && m_index + 1 < size(); - } - - std::optional<std::pair<QString, AssetItem>> next() - { - if (!hasNext()) - return {}; - ++m_index; - return std::pair<QString, AssetItem>(currentFileName(), at(m_index)); + if (!empty() && m_index + 1 < size()) { + ++m_index; + return true; + } + return false; } private: @@ -161,7 +159,7 @@ private: }; QCache<QString, QSharedPointer<FolderIterator>> FolderIterator::m_assetsCache(std::max(50, qEnvironmentVariableIntValue("QT_ANDROID_MAX_ASSETS_CACHE_SIZE"))); -QMutex FolderIterator::m_assetsCacheMutex; +Q_CONSTINIT QMutex FolderIterator::m_assetsCacheMutex; class AndroidAbstractFileEngineIterator: public QAbstractFileEngineIterator { @@ -169,7 +167,7 @@ public: AndroidAbstractFileEngineIterator(QDir::Filters filters, const QStringList &nameFilters, const QString &path) - : QAbstractFileEngineIterator(filters, nameFilters) + : QAbstractFileEngineIterator(path, filters, nameFilters) { m_currentIterator = FolderIterator::fromCache(cleanedAssetPath(path), true); } @@ -186,28 +184,16 @@ public: return m_currentIterator->currentFileName(); } - virtual QString currentFilePath() const + QString currentFilePath() const override { if (!m_currentIterator) return {}; return m_currentIterator->currentFilePath(); } - bool hasNext() const override - { - if (!m_currentIterator) - return false; - return m_currentIterator->hasNext(); - } - - QString next() override + bool advance() override { - if (!m_currentIterator) - return {}; - auto res = m_currentIterator->next(); - if (!res) - return {}; - return res->first; + return m_currentIterator ? m_currentIterator->advance() : false; } private: @@ -305,9 +291,9 @@ public: return prefixedPath(m_fileName); case BaseName: if ((pos = m_fileName.lastIndexOf(u'/')) != -1) - return prefixedPath(m_fileName.mid(pos)); + return m_fileName.mid(pos + 1); else - return prefixedPath(m_fileName); + return m_fileName; case PathName: case AbsolutePathName: case CanonicalPathName: @@ -350,8 +336,13 @@ public: } else { auto *assetDir = AAssetManager_openDir(m_assetManager, m_fileName.toUtf8()); if (assetDir) { - if (AAssetDir_getNextFileName(assetDir)) + if (AAssetDir_getNextFileName(assetDir) + || (!FolderIterator::fromCache(m_fileName, false)->empty())) { + // If AAssetDir_getNextFileName is not valid, it still can be a directory that + // contains only other directories (no files). FolderIterator will not be called + // on the directory containing files so it should not be too time consuming now. m_assetInfo->type = AssetItem::Type::Folder; + } AAssetDir_close(assetDir); } } @@ -360,10 +351,12 @@ public: m_assetsInfoCache.insert(m_fileName, newAssetInfoPtr); } - Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override + IteratorUniquePtr + beginEntryList(const QString &, QDir::Filters filters, const QStringList &filterNames) override { + // AndroidAbstractFileEngineIterator use `m_fileName` as the path if (m_assetInfo && m_assetInfo->type == AssetItem::Type::Folder) - return new AndroidAbstractFileEngineIterator(filters, filterNames, m_fileName); + return std::make_unique<AndroidAbstractFileEngineIterator>(filters, filterNames, m_fileName); return nullptr; } @@ -379,20 +372,21 @@ private: }; QCache<QString, QSharedPointer<AssetItem>> AndroidAbstractFileEngine::m_assetsInfoCache(std::max(200, qEnvironmentVariableIntValue("QT_ANDROID_MAX_FILEINFO_ASSETS_CACHE_SIZE"))); -QMutex AndroidAbstractFileEngine::m_assetsInfoCacheMutex; +Q_CONSTINIT QMutex AndroidAbstractFileEngine::m_assetsInfoCacheMutex; AndroidAssetsFileEngineHandler::AndroidAssetsFileEngineHandler() { m_assetManager = QtAndroid::assetManager(); } -QAbstractFileEngine * AndroidAssetsFileEngineHandler::create(const QString &fileName) const +std::unique_ptr<QAbstractFileEngine> +AndroidAssetsFileEngineHandler::create(const QString &fileName) const { if (fileName.isEmpty()) - return nullptr; + return {}; if (!fileName.startsWith(assetsPrefix)) - return nullptr; + return {}; QString path = fileName.mid(prefixSize); path.replace("//"_L1, "/"_L1); @@ -400,7 +394,7 @@ QAbstractFileEngine * AndroidAssetsFileEngineHandler::create(const QString &file path.remove(0, 1); if (path.endsWith(u'/')) path.chop(1); - return new AndroidAbstractFileEngine(m_assetManager, path); + return std::make_unique<AndroidAbstractFileEngine>(m_assetManager, path); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.h b/src/plugins/platforms/android/qandroidassetsfileenginehandler.h index 50c6914c24..973a61fbfa 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.h +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.h @@ -15,9 +15,10 @@ QT_BEGIN_NAMESPACE class AndroidAssetsFileEngineHandler: public QAbstractFileEngineHandler { + Q_DISABLE_COPY_MOVE(AndroidAssetsFileEngineHandler) public: AndroidAssetsFileEngineHandler(); - QAbstractFileEngine *create(const QString &fileName) const override; + std::unique_ptr<QAbstractFileEngine> create(const QString &fileName) const override; private: AAssetManager *m_assetManager; diff --git a/src/plugins/platforms/android/qandroideventdispatcher.cpp b/src/plugins/platforms/android/qandroideventdispatcher.cpp index 238addee58..8d1a085844 100644 --- a/src/plugins/platforms/android/qandroideventdispatcher.cpp +++ b/src/plugins/platforms/android/qandroideventdispatcher.cpp @@ -65,7 +65,7 @@ bool QAndroidEventDispatcher::processEvents(QEventLoop::ProcessEventsFlags flags QAndroidEventDispatcherStopper *QAndroidEventDispatcherStopper::instance() { - static QAndroidEventDispatcherStopper androidEventDispatcherStopper; + Q_CONSTINIT static QAndroidEventDispatcherStopper androidEventDispatcherStopper; return &androidEventDispatcherStopper; } @@ -75,7 +75,7 @@ void QAndroidEventDispatcherStopper::startAll() if (!m_started.testAndSetOrdered(0, 1)) return; - for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers)) + for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers)) d->start(); } @@ -85,7 +85,7 @@ void QAndroidEventDispatcherStopper::stopAll() if (!m_started.testAndSetOrdered(1, 0)) return; - for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers)) + for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers)) d->stop(); } @@ -104,6 +104,6 @@ void QAndroidEventDispatcherStopper::removeEventDispatcher(QAndroidEventDispatch void QAndroidEventDispatcherStopper::goingToStop(bool stop) { QMutexLocker lock(&m_mutex); - for (QAndroidEventDispatcher *d : qAsConst(m_dispatchers)) + for (QAndroidEventDispatcher *d : std::as_const(m_dispatchers)) d->goingToStop(stop); } diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 679c142289..62212ff63d 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -80,9 +80,7 @@ static jboolean beginBatchEdit(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ BEGINBATCH"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ BEGINBATCH"; jboolean res = JNI_FALSE; runOnQtThread([&res]{res = m_androidInputContext->beginBatchEdit();}); return res; @@ -93,9 +91,7 @@ static jboolean endBatchEdit(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ ENDBATCH"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ ENDBATCH"; jboolean res = JNI_FALSE; runOnQtThread([&res]{res = m_androidInputContext->endBatchEdit();}); @@ -113,9 +109,7 @@ static jboolean commitText(JNIEnv *env, jobject /*thiz*/, jstring text, jint new QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text)); env->ReleaseStringChars(text, jstr); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ COMMIT" << str << newCursorPosition; -#endif + qCDebug(lcQpaInputMethods) << "@@@ COMMIT" << str << newCursorPosition; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->commitText(str, newCursorPosition);}); return res; @@ -126,9 +120,7 @@ static jboolean deleteSurroundingText(JNIEnv */*env*/, jobject /*thiz*/, jint le if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ DELETE" << leftLength << rightLength; -#endif + qCDebug(lcQpaInputMethods) << "@@@ DELETE" << leftLength << rightLength; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->deleteSurroundingText(leftLength, rightLength);}); return res; @@ -139,9 +131,7 @@ static jboolean finishComposingText(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ FINISH"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ FINISH"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->finishComposingText();}); return res; @@ -165,9 +155,7 @@ static jobject getExtractedText(JNIEnv *env, jobject /*thiz*/, int hintMaxChars, QAndroidInputContext::ExtractedText extractedText; runOnQtThread([&]{extractedText = m_androidInputContext->getExtractedText(hintMaxChars, hintMaxLines, flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETEX" << hintMaxChars << hintMaxLines << QString::fromLatin1("0x") + QString::number(flags,16) << extractedText.text << "partOff:" << extractedText.partialStartOffset << extractedText.partialEndOffset << "sel:" << extractedText.selectionStart << extractedText.selectionEnd << "offset:" << extractedText.startOffset; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETEX" << hintMaxChars << hintMaxLines << QString::fromLatin1("0x") + QString::number(flags,16) << extractedText.text << "partOff:" << extractedText.partialStartOffset << extractedText.partialEndOffset << "sel:" << extractedText.selectionStart << extractedText.selectionEnd << "offset:" << extractedText.startOffset; jobject object = env->NewObject(m_extractedTextClass, m_classConstructorMethodID); env->SetIntField(object, m_partialStartOffsetFieldID, extractedText.partialStartOffset); @@ -190,9 +178,7 @@ static jstring getSelectedText(JNIEnv *env, jobject /*thiz*/, jint flags) QString text; runOnQtThread([&]{text = m_androidInputContext->getSelectedText(flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETSEL" << text; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETSEL" << text; if (text.isEmpty()) return 0; return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length())); @@ -205,9 +191,7 @@ static jstring getTextAfterCursor(JNIEnv *env, jobject /*thiz*/, jint length, ji QString text; runOnQtThread([&]{text = m_androidInputContext->getTextAfterCursor(length, flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETA" << length << text; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETA" << length << text; return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length())); } @@ -218,9 +202,7 @@ static jstring getTextBeforeCursor(JNIEnv *env, jobject /*thiz*/, jint length, j QString text; runOnQtThread([&]{text = m_androidInputContext->getTextBeforeCursor(length, flags);}); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ GETB" << length << text; -#endif + qCDebug(lcQpaInputMethods) << "@@@ GETB" << length << text; return env->NewString(reinterpret_cast<const jchar *>(text.constData()), jsize(text.length())); } @@ -234,9 +216,7 @@ static jboolean setComposingText(JNIEnv *env, jobject /*thiz*/, jstring text, ji QString str(reinterpret_cast<const QChar *>(jstr), env->GetStringLength(text)); env->ReleaseStringChars(text, jstr); -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SET" << str << newCursorPosition; -#endif + qCDebug(lcQpaInputMethods) << "@@@ SET" << str << newCursorPosition; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->setComposingText(str, newCursorPosition);}); return res; @@ -247,9 +227,7 @@ static jboolean setComposingRegion(JNIEnv */*env*/, jobject /*thiz*/, jint start if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SETR" << start << end; -#endif + qCDebug(lcQpaInputMethods) << "@@@ SETR" << start << end; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->setComposingRegion(start, end);}); return res; @@ -261,9 +239,7 @@ static jboolean setSelection(JNIEnv */*env*/, jobject /*thiz*/, jint start, jint if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug() << "@@@ SETSEL" << start << end; -#endif + qCDebug(lcQpaInputMethods) << "@@@ SETSEL" << start << end; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->setSelection(start, end);}); return res; @@ -275,9 +251,7 @@ static jboolean selectAll(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ SELALL"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ SELALL"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->selectAll();}); return res; @@ -288,9 +262,7 @@ static jboolean cut(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@"); -#endif + qCDebug(lcQpaInputMethods) << "@@@"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->cut();}); return res; @@ -301,9 +273,7 @@ static jboolean copy(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@"); -#endif + qCDebug(lcQpaInputMethods) << "@@@"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->copy();}); return res; @@ -314,9 +284,7 @@ static jboolean copyURL(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@"); -#endif + qCDebug(lcQpaInputMethods) << "@@@"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->copyURL();}); return res; @@ -327,9 +295,7 @@ static jboolean paste(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ PASTE"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ PASTE"; jboolean res = JNI_FALSE; runOnQtThread([&]{res = m_androidInputContext->paste();}); return res; @@ -340,14 +306,24 @@ static jboolean updateCursorPosition(JNIEnv */*env*/, jobject /*thiz*/) if (!m_androidInputContext) return JNI_FALSE; -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qDebug("@@@ UPDATECURSORPOS"); -#endif + qCDebug(lcQpaInputMethods) << "@@@ UPDATECURSORPOS"; runOnQtThread([&]{m_androidInputContext->updateCursorPosition();}); return true; } +static void reportFullscreenMode(JNIEnv */*env*/, jobject /*thiz*/, jboolean enabled) +{ + if (!m_androidInputContext) + return; + + runOnQtThread([&]{m_androidInputContext->reportFullscreenMode(enabled);}); +} + +static jboolean fullscreenMode(JNIEnv */*env*/, jobject /*thiz*/) +{ + return m_androidInputContext ? m_androidInputContext->fullscreenMode() : false; +} static JNINativeMethod methods[] = { {"beginBatchEdit", "()Z", (void *)beginBatchEdit}, @@ -368,7 +344,9 @@ static JNINativeMethod methods[] = { {"copy", "()Z", (void *)copy}, {"copyURL", "()Z", (void *)copyURL}, {"paste", "()Z", (void *)paste}, - {"updateCursorPosition", "()Z", (void *)updateCursorPosition} + {"updateCursorPosition", "()Z", (void *)updateCursorPosition}, + {"reportFullscreenMode", "(Z)V", (void *)reportFullscreenMode}, + {"fullscreenMode", "()Z", (void *)fullscreenMode} }; static QRect screenInputItemRectangle() @@ -385,6 +363,7 @@ QAndroidInputContext::QAndroidInputContext() , m_handleMode(Hidden) , m_batchEditNestingLevel(0) , m_focusObject(0) + , m_fullScreenMode(false) { QJniEnvironment env; jclass clazz = env.findClass(QtNativeInputConnectionClassName); @@ -567,12 +546,29 @@ void QAndroidInputContext::updateCursorPosition() } } +bool QAndroidInputContext::isImhNoTextHandlesSet() +{ + QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(); + if (query.isNull()) + return false; + return query->value(Qt::ImHints).toUInt() & Qt::ImhNoTextHandles; +} + void QAndroidInputContext::updateSelectionHandles() { + if (m_fullScreenMode) { + QtAndroidInput::updateHandles(Hidden); + return; + } static bool noHandles = qEnvironmentVariableIntValue("QT_QPA_NO_TEXT_HANDLES"); if (noHandles || !m_focusObject) return; + if (isImhNoTextHandlesSet()) { + QtAndroidInput::updateHandles(Hidden); + return; + } + auto im = qGuiApp->inputMethod(); QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition | Qt::ImEnabled @@ -586,6 +582,11 @@ void QAndroidInputContext::updateSelectionHandles() bool readOnly = readOnlyVariant.toBool(); QPlatformWindow *qPlatformWindow = qGuiApp->focusWindow()->handle(); + if (!readOnly && ((m_handleMode & 0xff) == Hidden)) { + QtAndroidInput::updateHandles(Hidden); + return; + } + if ( cpos == anchor && (!readOnlyVariant.isValid() || readOnly)) { QtAndroidInput::updateHandles(Hidden); return; @@ -612,9 +613,8 @@ void QAndroidInputContext::updateSelectionHandles() if (!query.value(Qt::ImSurroundingText).toString().isEmpty()) buttons |= EditContext::SelectAllButton; QtAndroidInput::updateHandles(m_handleMode, editMenuPoint, buttons, cursorPointGlobal); - // The VK is hidden, reset the timer - if (m_hideCursorHandleTimer.isActive()) - m_hideCursorHandleTimer.start(); + m_hideCursorHandleTimer.start(); + return; } @@ -788,7 +788,15 @@ void QAndroidInputContext::touchDown(int x, int y) focusObjectStopComposing(); } - updateSelectionHandles(); + // Check if cursor is visible in focused window before updating handles + QPlatformWindow *window = qGuiApp->focusWindow()->handle(); + const QRectF curRect = cursorRectangle(); + const QPoint cursorGlobalPoint = window->mapToGlobal(QPoint(curRect.x(), curRect.y())); + const QRect windowRect = QPlatformInputContext::inputItemClipRectangle().toRect(); + const QRect windowGlobalRect = QRect(window->mapToGlobal(windowRect.topLeft()), windowRect.size()); + + if (windowGlobalRect.contains(cursorGlobalPoint.x(), cursorGlobalPoint.y())) + updateSelectionHandles(); } } @@ -895,10 +903,15 @@ void QAndroidInputContext::showInputPanel() if (query.isNull()) return; + if (!qGuiApp->focusWindow()->handle()) + return; // not a real window, probably VR/XR + disconnect(m_updateCursorPosConnection); + m_updateCursorPosConnection = {}; + if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged(int,int)") >= 0) // QLineEdit breaks the pattern m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged(int,int)), this, SLOT(updateCursorPosition())); - else + else if (qGuiApp->focusObject()->metaObject()->indexOfSignal("cursorPositionChanged()") >= 0) m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition())); QRect rect = screenInputItemRectangle(); @@ -1110,6 +1123,25 @@ jboolean QAndroidInputContext::finishComposingText() return JNI_TRUE; } +void QAndroidInputContext::reportFullscreenMode(jboolean enabled) +{ + m_fullScreenMode = enabled; + BatchEditLock batchEditLock(this); + if (!focusObjectStopComposing()) + return; + + if (enabled) + m_handleMode = Hidden; + + updateSelectionHandles(); +} + +// Called in calling thread's context +jboolean QAndroidInputContext::fullscreenMode() +{ + return m_fullScreenMode; +} + bool QAndroidInputContext::focusObjectIsComposing() const { return m_composingCursor != -1; @@ -1532,9 +1564,7 @@ jboolean QAndroidInputContext::setComposingRegion(jint start, jint end) } if (start < textOffset || end - textOffset > text.length()) { -#ifdef QT_DEBUG_ANDROID_IM_PROTOCOL - qWarning("setComposingRegion: failed to retrieve text from composing region"); -#endif + qCDebug(lcQpaInputMethods) << "Warning: setComposingRegion: failed to retrieve text from composing region"; return JNI_TRUE; } diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h index c93aae142a..038286c4b8 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.h +++ b/src/plugins/platforms/android/qandroidinputcontext.h @@ -9,6 +9,8 @@ #include <functional> #include <jni.h> #include <qevent.h> + +#include <QtCore/qpointer.h> #include <QTimer> QT_BEGIN_NAMESPACE @@ -98,6 +100,8 @@ public: jboolean copy(); jboolean copyURL(); jboolean paste(); + void reportFullscreenMode(jboolean enabled); + jboolean fullscreenMode(); public slots: void safeCall(const std::function<void()> &func, Qt::ConnectionType conType = Qt::BlockingQueuedConnection); @@ -113,6 +117,7 @@ private slots: void showInputPanelLater(Qt::ApplicationState); private: + bool isImhNoTextHandlesSet(); void sendInputMethodEvent(QInputMethodEvent *event); QSharedPointer<QInputMethodQueryEvent> focusObjectInputMethodQuery(Qt::InputMethodQueries queries = Qt::ImQueryAll); bool focusObjectIsComposing() const; @@ -129,6 +134,7 @@ private: int m_batchEditNestingLevel; QPointer<QObject> m_focusObject; QTimer m_hideCursorHandleTimer; + bool m_fullScreenMode; }; Q_DECLARE_OPERATORS_FOR_FLAGS(QAndroidInputContext::HandleModes) QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp index 33eafd54aa..ea7f22295d 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp @@ -28,10 +28,14 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent * QtAndroidAccessibility::notifyLocationChange(event->uniqueId()); } else if (event->type() == QAccessible::ObjectHide) { QtAndroidAccessibility::notifyObjectHide(event->uniqueId()); + } else if (event->type() == QAccessible::ObjectShow) { + QtAndroidAccessibility::notifyObjectShow(event->uniqueId()); } else if (event->type() == QAccessible::Focus) { QtAndroidAccessibility::notifyObjectFocus(event->uniqueId()); } else if (event->type() == QAccessible::ValueChanged) { QtAndroidAccessibility::notifyValueChanged(event->uniqueId()); + } else if (event->type() == QAccessible::ScrollingEnd) { + QtAndroidAccessibility::notifyScrolledEvent(event->uniqueId()); } } diff --git a/src/plugins/platforms/android/qandroidplatformbackingstore.cpp b/src/plugins/platforms/android/qandroidplatformbackingstore.cpp deleted file mode 100644 index 07a1c835d3..0000000000 --- a/src/plugins/platforms/android/qandroidplatformbackingstore.cpp +++ /dev/null @@ -1,52 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2016 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 - -#include "qandroidplatformbackingstore.h" -#include "qandroidplatformscreen.h" -#include "qandroidplatformwindow.h" -#include <qpa/qplatformscreen.h> - -QT_BEGIN_NAMESPACE - -QAndroidPlatformBackingStore::QAndroidPlatformBackingStore(QWindow *window) - : QPlatformBackingStore(window) -{ - if (window->handle()) - setBackingStore(window); -} - -QPaintDevice *QAndroidPlatformBackingStore::paintDevice() -{ - return &m_image; -} - -void QAndroidPlatformBackingStore::flush(QWindow *window, const QRegion ®ion, const QPoint &offset) -{ - Q_UNUSED(offset); - - if (!m_backingStoreSet) - setBackingStore(window); - - (static_cast<QAndroidPlatformWindow *>(window->handle()))->repaint(region); -} - -void QAndroidPlatformBackingStore::resize(const QSize &size, const QRegion &staticContents) -{ - Q_UNUSED(staticContents); - - if (m_image.size() != size) - m_image = QImage(size, window()->screen()->handle()->format()); -} - -void QAndroidPlatformBackingStore::setBackingStore(QWindow *window) -{ - if (window->surfaceType() == QSurface::RasterSurface || window->surfaceType() == QSurface::RasterGLSurface) { - (static_cast<QAndroidPlatformWindow *>(window->handle()))->setBackingStore(this); - m_backingStoreSet = true; - } else { - qWarning("QAndroidPlatformBackingStore does not support OpenGL-only windows."); - } -} - -QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformbackingstore.h b/src/plugins/platforms/android/qandroidplatformbackingstore.h deleted file mode 100644 index 810305ac45..0000000000 --- a/src/plugins/platforms/android/qandroidplatformbackingstore.h +++ /dev/null @@ -1,29 +0,0 @@ -// Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -// Copyright (C) 2016 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 - -#ifndef QANDROIDPLATFORMBACKINGSTORE_H -#define QANDROIDPLATFORMBACKINGSTORE_H - -#include <qpa/qplatformbackingstore.h> -#include <qpa/qwindowsysteminterface.h> - -QT_BEGIN_NAMESPACE - -class QAndroidPlatformBackingStore : public QPlatformBackingStore -{ -public: - explicit QAndroidPlatformBackingStore(QWindow *window); - QPaintDevice *paintDevice() override; - void flush(QWindow *window, const QRegion ®ion, const QPoint &offset) override; - void resize(const QSize &size, const QRegion &staticContents) override; - QImage toImage() const override { return m_image; } - void setBackingStore(QWindow *window); -protected: - QImage m_image; - bool m_backingStoreSet = false; -}; - -QT_END_NAMESPACE - -#endif // QANDROIDPLATFORMBACKINGSTORE_H diff --git a/src/plugins/platforms/android/qandroidplatformclipboard.cpp b/src/plugins/platforms/android/qandroidplatformclipboard.cpp index 39a508a1b3..e5ed33b9b0 100644 --- a/src/plugins/platforms/android/qandroidplatformclipboard.cpp +++ b/src/plugins/platforms/android/qandroidplatformclipboard.cpp @@ -1,15 +1,34 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include "qandroidplatformclipboard.h" -#include "androidjniclipboard.h" + +#include <QtCore/QUrl> +#include <QtCore/QJniEnvironment> +#include <QtCore/QJniObject> +#include <QtCore/private/qjnihelpers_p.h> + #ifndef QT_NO_CLIPBOARD +using namespace QtJniTypes; + QT_BEGIN_NAMESPACE +void QAndroidPlatformClipboard::onClipboardDataChanged(JNIEnv *env, jobject obj, jlong nativePointer) +{ + Q_UNUSED(env) + Q_UNUSED(obj) + + auto *clipboardManager = reinterpret_cast<QAndroidPlatformClipboard *>(nativePointer); + if (clipboardManager) + clipboardManager->emitChanged(QClipboard::Clipboard); +} + QAndroidPlatformClipboard::QAndroidPlatformClipboard() { - QtAndroidClipboard::setClipboardManager(this); + m_clipboardManager = QtClipboardManager::construct(QtAndroidPrivate::context(), + reinterpret_cast<jlong>(this)); } QAndroidPlatformClipboard::~QAndroidPlatformClipboard() @@ -18,24 +37,66 @@ QAndroidPlatformClipboard::~QAndroidPlatformClipboard() delete data; } +QMimeData *QAndroidPlatformClipboard::getClipboardMimeData() +{ + QMimeData *data = new QMimeData; + if (m_clipboardManager.callMethod<jboolean>("hasClipboardText")) { + data->setText(m_clipboardManager.callMethod<QString>("getClipboardText")); + } + if (m_clipboardManager.callMethod<jboolean>("hasClipboardHtml")) { + data->setHtml(m_clipboardManager.callMethod<QString>("getClipboardHtml")); + } + if (m_clipboardManager.callMethod<jboolean>("hasClipboardUri")) { + auto uris = m_clipboardManager.callMethod<QString[]>("getClipboardUris"); + if (uris.isValid()) { + QList<QUrl> urls; + for (const QString &uri : uris) + urls << QUrl(uri); + data->setUrls(urls); + } + } + return data; +} + QMimeData *QAndroidPlatformClipboard::mimeData(QClipboard::Mode mode) { Q_UNUSED(mode); Q_ASSERT(supportsMode(mode)); if (data) data->deleteLater(); - data = QtAndroidClipboard::getClipboardMimeData(); + data = getClipboardMimeData(); return data; } +void QAndroidPlatformClipboard::clearClipboardData() +{ + m_clipboardManager.callMethod<void>("clearClipData"); +} + +void QAndroidPlatformClipboard::setClipboardMimeData(QMimeData *data) +{ + clearClipboardData(); + auto context = QtAndroidPrivate::context(); + if (data->hasUrls()) { + QList<QUrl> urls = data->urls(); + for (const auto &u : std::as_const(urls)) + m_clipboardManager.callMethod<void>("setClipboardUri", context, u.toEncoded()); + } else if (data->hasHtml()) { // html can contain text + m_clipboardManager.callMethod<void>("setClipboardHtml", + context, data->text(), data->html()); + } else if (data->hasText()) { // hasText must be the last (the order matter here) + m_clipboardManager.callMethod<void>("setClipboardText", context, data->text()); + } +} + void QAndroidPlatformClipboard::setMimeData(QMimeData *data, QClipboard::Mode mode) { if (!data) { - QtAndroidClipboard::clearClipboardData(); + clearClipboardData(); return; } if (data && supportsMode(mode)) - QtAndroidClipboard::setClipboardMimeData(data); + setClipboardMimeData(data); if (data != 0) data->deleteLater(); } @@ -45,6 +106,18 @@ bool QAndroidPlatformClipboard::supportsMode(QClipboard::Mode mode) const return QClipboard::Clipboard == mode; } +bool QAndroidPlatformClipboard::registerNatives(QJniEnvironment &env) +{ + bool success = env.registerNativeMethods(Traits<QtClipboardManager>::className(), + { Q_JNI_NATIVE_SCOPED_METHOD(onClipboardDataChanged, QAndroidPlatformClipboard) }); + if (!success) { + qCritical() << "QtClipboardManager: registerNativeMethods() failed"; + return false; + } + + return true; +} + QT_END_NAMESPACE #endif // QT_NO_CLIPBOARD diff --git a/src/plugins/platforms/android/qandroidplatformclipboard.h b/src/plugins/platforms/android/qandroidplatformclipboard.h index 1778ca5b28..ab5c527f88 100644 --- a/src/plugins/platforms/android/qandroidplatformclipboard.h +++ b/src/plugins/platforms/android/qandroidplatformclipboard.h @@ -1,3 +1,4 @@ +// Copyright (C) 2023 The Qt Company Ltd. // Copyright (C) 2012 BogDan Vatra <bogdan@kde.org> // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only @@ -6,10 +7,14 @@ #include <qpa/qplatformclipboard.h> #include <QMimeData> +#include <QtCore/qjnitypes.h> #ifndef QT_NO_CLIPBOARD + QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(QtClipboardManager, "org/qtproject/qt/android/QtClipboardManager"); + class QAndroidPlatformClipboard : public QPlatformClipboard { public: @@ -18,8 +23,19 @@ public: QMimeData *mimeData(QClipboard::Mode mode = QClipboard::Clipboard) override; void setMimeData(QMimeData *data, QClipboard::Mode mode = QClipboard::Clipboard) override; bool supportsMode(QClipboard::Mode mode) const override; + + static bool registerNatives(QJniEnvironment &env); + private: + QMimeData *getClipboardMimeData(); + void setClipboardMimeData(QMimeData *data); + void clearClipboardData(); + + static void onClipboardDataChanged(JNIEnv *env, jobject obj, jlong nativePointer); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(onClipboardDataChanged) + QMimeData *data = nullptr; + QtJniTypes::QtClipboardManager m_clipboardManager = nullptr; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp b/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp index 97ae5064bd..2b9a1194d2 100644 --- a/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp +++ b/src/plugins/platforms/android/qandroidplatformdialoghelpers.cpp @@ -19,7 +19,7 @@ static jclass g_messageDialogHelperClass = nullptr; QAndroidPlatformMessageDialogHelper::QAndroidPlatformMessageDialogHelper() : m_javaMessageDialog(g_messageDialogHelperClass, "(Landroid/app/Activity;)V", - QtAndroid::activity()) + QtAndroidPrivate::activity().object()) { } @@ -54,7 +54,10 @@ bool QAndroidPlatformMessageDialogHelper::show(Qt::WindowFlags windowFlags, if (!opt.data()) return false; - m_javaMessageDialog.callMethod<void>("setIcon", "(I)V", opt->icon()); + if (!opt->checkBoxLabel().isNull()) + return false; // Can't support + + m_javaMessageDialog.callMethod<void>("setStandardIcon", "(I)V", opt->standardIcon()); QString str = htmlText(opt->windowTitle()); if (!str.isEmpty()) { @@ -147,7 +150,7 @@ static void dialogResult(JNIEnv * /*env*/, jobject /*thiz*/, jlong handler, int QMetaObject::invokeMethod(object, "dialogResult", Qt::QueuedConnection, Q_ARG(int, buttonID)); } -static JNINativeMethod methods[] = { +static const JNINativeMethod methods[] = { {"dialogResult", "(JI)V", (void *)dialogResult} }; @@ -159,21 +162,19 @@ static JNINativeMethod methods[] = { return false; \ } -bool registerNatives(JNIEnv *env) +bool registerNatives(QJniEnvironment &env) { const char QtMessageHandlerHelperClassName[] = "org/qtproject/qt/android/QtMessageDialogHelper"; - QJniEnvironment qenv; - jclass clazz = qenv.findClass(QtMessageHandlerHelperClassName); + jclass clazz = env.findClass(QtMessageHandlerHelperClassName); if (!clazz) { __android_log_print(ANDROID_LOG_FATAL, QtAndroid::qtTagText(), QtAndroid::classErrorMsgFmt() , QtMessageHandlerHelperClassName); return false; } g_messageDialogHelperClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - FIND_AND_CHECK_CLASS("org/qtproject/qt/android/QtNativeDialogHelper"); - jclass appClass = static_cast<jclass>(env->NewGlobalRef(clazz)); - if (env->RegisterNatives(appClass, methods, sizeof(methods) / sizeof(methods[0])) < 0) { + if (!env.registerNativeMethods("org/qtproject/qt/android/QtNativeDialogHelper", + methods, sizeof(methods) / sizeof(methods[0]))) { __android_log_print(ANDROID_LOG_FATAL, "Qt", "RegisterNatives failed"); return false; } diff --git a/src/plugins/platforms/android/qandroidplatformdialoghelpers.h b/src/plugins/platforms/android/qandroidplatformdialoghelpers.h index f06d2a9990..86f1cf77e7 100644 --- a/src/plugins/platforms/android/qandroidplatformdialoghelpers.h +++ b/src/plugins/platforms/android/qandroidplatformdialoghelpers.h @@ -8,12 +8,13 @@ #include <jni.h> #include <QEventLoop> -#include <QtCore/QJniEnvironment> #include <QtCore/QJniObject> #include <qpa/qplatformdialoghelper.h> QT_BEGIN_NAMESPACE +class QJniEnvironment; + namespace QtAndroidDialogHelpers { class QAndroidPlatformMessageDialogHelper: public QPlatformMessageDialogHelper @@ -41,7 +42,7 @@ private: }; -bool registerNatives(JNIEnv *env); +bool registerNatives(QJniEnvironment &env); } diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp index 6d28bd2388..d8a5c58d2c 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp @@ -25,7 +25,7 @@ const char JniIntentClass[] = "android/content/Intent"; QAndroidPlatformFileDialogHelper::QAndroidPlatformFileDialogHelper() : QPlatformFileDialogHelper(), - m_activity(QtAndroid::activity()) + m_activity(QtAndroidPrivate::activity()) { } @@ -45,8 +45,8 @@ bool QAndroidPlatformFileDialogHelper::handleActivityResult(jint requestCode, ji if (uri.isValid()) { takePersistableUriPermission(uri); m_selectedFile.append(QUrl(uri.toString())); - Q_EMIT fileSelected(m_selectedFile.first()); - Q_EMIT currentChanged(m_selectedFile.first()); + Q_EMIT fileSelected(m_selectedFile.constFirst()); + Q_EMIT currentChanged(m_selectedFile.constFirst()); Q_EMIT accept(); return true; @@ -65,7 +65,7 @@ bool QAndroidPlatformFileDialogHelper::handleActivityResult(jint requestCode, ji m_selectedFile.append(itemUri.toString()); } Q_EMIT filesSelected(m_selectedFile); - Q_EMIT currentChanged(m_selectedFile.first()); + Q_EMIT currentChanged(m_selectedFile.constFirst()); Q_EMIT accept(); } @@ -97,6 +97,22 @@ void QAndroidPlatformFileDialogHelper::setInitialFileName(const QString &title) extraTitle.object(), QJniObject::fromString(title).object()); } +void QAndroidPlatformFileDialogHelper::setInitialDirectoryUri(const QString &directory) +{ + if (directory.isEmpty()) + return; + + if (QNativeInterface::QAndroidApplication::sdkVersion() < 26) + return; + + const auto extraInitialUri = QJniObject::getStaticObjectField( + "android/provider/DocumentsContract", "EXTRA_INITIAL_URI", "Ljava/lang/String;"); + m_intent.callObjectMethod("putExtra", + "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;", + extraInitialUri.object(), + QJniObject::fromString(directory).object()); +} + void QAndroidPlatformFileDialogHelper::setOpenableCategory() { const QJniObject CATEGORY_OPENABLE = QJniObject::getStaticObjectField( @@ -130,12 +146,15 @@ void QAndroidPlatformFileDialogHelper::setMimeTypes() { QStringList mimeTypes = options()->mimeTypeFilters(); const QStringList nameFilters = options()->nameFilters(); - const QString nameFilter = nameFilters.isEmpty() ? QString() : nameFilters.first(); - if (!nameFilter.isEmpty()) { + if (!nameFilters.isEmpty()) { QMimeDatabase db; - for (const QString &filter : nameFilterExtensions(nameFilter)) - mimeTypes.append(db.mimeTypeForFile(filter, QMimeDatabase::MatchExtension).name()); + for (auto filter : nameFilters) { + if (!filter.isEmpty()) { + for (const QString &filter : nameFilterExtensions(filter)) + mimeTypes.append(db.mimeTypeForFile(filter, QMimeDatabase::MatchExtension).name()); + } + } } const QString initialType = mimeTypes.size() == 1 ? mimeTypes.at(0) : "*/*"_L1; @@ -179,11 +198,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win if (options()->acceptMode() == QFileDialogOptions::AcceptSave) { m_intent = getFileDialogIntent("ACTION_CREATE_DOCUMENT"); const QList<QUrl> selectedFiles = options()->initiallySelectedFiles(); - if (selectedFiles.size() > 0) { - // TODO: The initial folder to show at the start should be handled by EXTRA_INITIAL_URI - // Take only the file name. + if (selectedFiles.size() > 0) setInitialFileName(selectedFiles.first().fileName()); - } } else if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) { switch (options()->fileMode()) { case QFileDialogOptions::FileMode::DirectoryOnly: @@ -207,6 +223,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win setMimeTypes(); } + setInitialDirectoryUri(m_directory.toString()); + QtAndroidPrivate::registerActivityResultListener(this); m_activity.callMethod<void>("startActivityForResult", "(Landroid/content/Intent;I)V", m_intent.object(), REQUEST_CODE); @@ -220,6 +238,11 @@ void QAndroidPlatformFileDialogHelper::hide() QtAndroidPrivate::unregisterActivityResultListener(this); } +void QAndroidPlatformFileDialogHelper::setDirectory(const QUrl &directory) +{ + m_directory = directory; +} + void QAndroidPlatformFileDialogHelper::exec() { m_eventLoop.exec(QEventLoop::DialogExec); diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h index 4281cb8198..156eda9142 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h @@ -32,8 +32,8 @@ public: void setFilter() override {} QList<QUrl> selectedFiles() const override { return m_selectedFile; } void selectFile(const QUrl &) override {} - QUrl directory() const override { return QUrl(); } - void setDirectory(const QUrl &) override {} + QUrl directory() const override { return m_directory; } + void setDirectory(const QUrl &directory) override; bool defaultNameFilterDisables() const override { return false; } bool handleActivityResult(jint requestCode, jint resultCode, jobject data) override; @@ -41,12 +41,14 @@ private: QJniObject getFileDialogIntent(const QString &intentType); void takePersistableUriPermission(const QJniObject &uri); void setInitialFileName(const QString &title); + void setInitialDirectoryUri(const QString &directory); void setOpenableCategory(); void setAllowMultipleSelections(bool allowMultiple); void setMimeTypes(); QEventLoop m_eventLoop; QList<QUrl> m_selectedFile; + QUrl m_directory; QJniObject m_intent; const QJniObject m_activity; }; diff --git a/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp b/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp index 01636d2ba5..82a10dac07 100644 --- a/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp +++ b/src/plugins/platforms/android/qandroidplatformfontdatabase.cpp @@ -2,6 +2,7 @@ // SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only #include <QDir> +#include <QLocale> #include "qandroidplatformfontdatabase.h" @@ -47,6 +48,38 @@ QStringList QAndroidPlatformFontDatabase::fallbacksForFamily(const QString &fami QChar::Script script) const { QStringList result; + + // Prepend CJK fonts by the locale. + QLocale locale = QLocale::system(); + switch (locale.language()) { + case QLocale::Chinese: { + switch (locale.territory()) { + case QLocale::China: + case QLocale::Singapore: + result.append(QStringLiteral("Noto Sans Mono CJK SC")); + break; + case QLocale::Taiwan: + case QLocale::HongKong: + case QLocale::Macao: + result.append(QStringLiteral("Noto Sans Mono CJK TC")); + break; + default: + // no modifications. + break; + } + break; + } + case QLocale::Japanese: + result.append(QStringLiteral("Noto Sans Mono CJK JP")); + break; + case QLocale::Korean: + result.append(QStringLiteral("Noto Sans Mono CJK KR")); + break; + default: + // no modifications. + break; + } + if (styleHint == QFont::Monospace || styleHint == QFont::Courier) result.append(QString(qgetenv("QT_ANDROID_FONTS_MONOSPACE")).split(u';')); else if (styleHint == QFont::Serif) diff --git a/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp b/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp index c2ce2c84b2..e84a481a2b 100644 --- a/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformforeignwindow.cpp @@ -6,84 +6,101 @@ #include <QtCore/qvariant.h> #include <qpa/qwindowsysteminterface.h> #include <QtCore/private/qjnihelpers_p.h> +#include <QtCore/qjnitypes.h> QT_BEGIN_NAMESPACE QAndroidPlatformForeignWindow::QAndroidPlatformForeignWindow(QWindow *window, WId nativeHandle) - : QAndroidPlatformWindow(window), - m_surfaceId(-1) + : QAndroidPlatformWindow(window), m_view(nullptr), m_nativeViewInserted(false) { m_view = reinterpret_cast<jobject>(nativeHandle); - if (m_view.isValid()) - QtAndroid::setViewVisibility(m_view.object(), false); -} + if (isEmbeddingContainer()) { + m_nativeViewId = m_view.callMethod<jint>("getId"); + return; + } -QAndroidPlatformForeignWindow::~QAndroidPlatformForeignWindow() -{ if (m_view.isValid()) QtAndroid::setViewVisibility(m_view.object(), false); - if (m_surfaceId != -1) - QtAndroid::destroySurface(m_surfaceId); } -void QAndroidPlatformForeignWindow::lower() +QAndroidPlatformForeignWindow::~QAndroidPlatformForeignWindow() { - if (m_surfaceId == -1) + if (isEmbeddingContainer()) return; - QAndroidPlatformWindow::lower(); - QtAndroid::bringChildToBack(m_surfaceId); -} + if (m_view.isValid()) + QtAndroid::setViewVisibility(m_view.object(), false); -void QAndroidPlatformForeignWindow::raise() -{ - if (m_surfaceId == -1) - return; + m_nativeQtWindow.callMethod<void>("removeNativeView"); - QAndroidPlatformWindow::raise(); - QtAndroid::bringChildToFront(m_surfaceId); } void QAndroidPlatformForeignWindow::setGeometry(const QRect &rect) { QAndroidPlatformWindow::setGeometry(rect); - if (m_surfaceId != -1) - QtAndroid::setSurfaceGeometry(m_surfaceId, rect); + if (isEmbeddingContainer()) + return; + + if (m_nativeViewInserted) + setNativeGeometry(rect); } void QAndroidPlatformForeignWindow::setVisible(bool visible) { + if (isEmbeddingContainer()) { + QAndroidPlatformWindow::setVisible(visible); + return; + } + if (!m_view.isValid()) return; QtAndroid::setViewVisibility(m_view.object(), visible); - QAndroidPlatformWindow::setVisible(visible); - if (!visible && m_surfaceId != -1) { - QtAndroid::destroySurface(m_surfaceId); - m_surfaceId = -1; - } else if (m_surfaceId == -1) { - m_surfaceId = QtAndroid::insertNativeView(m_view.object(), geometry()); + if (!visible && m_nativeViewInserted) { + m_nativeQtWindow.callMethod<void>("removeNativeView"); + m_nativeViewInserted = false; + } else if (!m_nativeViewInserted) { + addViewToWindow(); } } void QAndroidPlatformForeignWindow::applicationStateChanged(Qt::ApplicationState state) { - if (state <= Qt::ApplicationHidden - && m_surfaceId != -1) { - QtAndroid::destroySurface(m_surfaceId); - m_surfaceId = -1; - } else if (m_view.isValid() && m_surfaceId == -1){ - m_surfaceId = QtAndroid::insertNativeView(m_view.object(), geometry()); + if (!isEmbeddingContainer()) { + if (state <= Qt::ApplicationHidden + && m_nativeViewInserted) { + m_nativeQtWindow.callMethod<void>("removeNativeView"); + m_nativeViewInserted = false; + } else if (m_view.isValid() && !m_nativeViewInserted){ + addViewToWindow(); + } } QAndroidPlatformWindow::applicationStateChanged(state); } -void QAndroidPlatformForeignWindow::setParent(const QPlatformWindow *window) +WId QAndroidPlatformForeignWindow::winId() const { - Q_UNUSED(window); + if (isEmbeddingContainer() && m_view.isValid()) + return reinterpret_cast<WId>(m_view.object()); + if (m_nativeQtWindow.isValid()) + return reinterpret_cast<WId>(m_nativeQtWindow.object()); + return 0L; +} + +void QAndroidPlatformForeignWindow::addViewToWindow() +{ + if (isEmbeddingContainer()) + return; + + jint x = 0, y = 0, w = -1, h = -1; + if (!geometry().isNull()) + geometry().getRect(&x, &y, &w, &h); + + m_nativeQtWindow.callMethod<void>("setNativeView", m_view, x, y, qMax(w, 1), qMax(h, 1)); + m_nativeViewInserted = true; } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformforeignwindow.h b/src/plugins/platforms/android/qandroidplatformforeignwindow.h index 7b576ecec4..503524cced 100644 --- a/src/plugins/platforms/android/qandroidplatformforeignwindow.h +++ b/src/plugins/platforms/android/qandroidplatformforeignwindow.h @@ -4,29 +4,31 @@ #ifndef QANDROIDPLATFORMFOREIGNWINDOW_H #define QANDROIDPLATFORMFOREIGNWINDOW_H -#include "androidsurfaceclient.h" #include "qandroidplatformwindow.h" #include <QtCore/QJniObject> QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(View, "android/view/View") + class QAndroidPlatformForeignWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformForeignWindow(QWindow *window, WId nativeHandle); ~QAndroidPlatformForeignWindow(); - void lower() override; - void raise() override; void setGeometry(const QRect &rect) override; void setVisible(bool visible) override; void applicationStateChanged(Qt::ApplicationState state) override; - void setParent(const QPlatformWindow *window) override; bool isForeignWindow() const override { return true; } + WId winId() const override; + private: - int m_surfaceId; - QJniObject m_view; + void addViewToWindow(); + + QtJniTypes::View m_view; + bool m_nativeViewInserted; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformiconengine.cpp b/src/plugins/platforms/android/qandroidplatformiconengine.cpp new file mode 100644 index 0000000000..faa63dcca1 --- /dev/null +++ b/src/plugins/platforms/android/qandroidplatformiconengine.cpp @@ -0,0 +1,616 @@ +// Copyright (C) 2023 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 + +#include "qandroidplatformiconengine.h" +#include "androidjnimain.h" + +#include <QtCore/qdebug.h> +#include <QtCore/qjniarray.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qfile.h> +#include <QtCore/qset.h> + +#include <QtGui/qfontdatabase.h> +#include <QtGui/qpainter.h> +#include <QtGui/qpalette.h> + +QT_BEGIN_NAMESPACE + +using namespace Qt::StringLiterals; +Q_LOGGING_CATEGORY(lcIconEngineFontDownload, "qt.qpa.iconengine.fontdownload") + +// the primary types to work with the FontRequest API +Q_DECLARE_JNI_CLASS(FontRequest, "androidx/core/provider/FontRequest") +Q_DECLARE_JNI_CLASS(FontsContractCompat, "androidx/core/provider/FontsContractCompat") +Q_DECLARE_JNI_CLASS(FontFamilyResult, "androidx/core/provider/FontsContractCompat$FontFamilyResult") +Q_DECLARE_JNI_CLASS(FontInfo, "androidx/core/provider/FontsContractCompat$FontInfo") + +// various utility types +Q_DECLARE_JNI_CLASS(List, "java/util/List"); // List is just an Interface +Q_DECLARE_JNI_CLASS(ArrayList, "java/util/ArrayList"); +Q_DECLARE_JNI_CLASS(HashSet, "java/util/HashSet"); +Q_DECLARE_JNI_CLASS(Uri, "android/net/Uri") +Q_DECLARE_JNI_CLASS(CancellationSignal, "android/os/CancellationSignal") +Q_DECLARE_JNI_CLASS(ParcelFileDescriptor, "android/os/ParcelFileDescriptor") +Q_DECLARE_JNI_CLASS(ContentResolver, "android/content/ContentResolver") +Q_DECLARE_JNI_CLASS(PackageManager, "android/content/pm/PackageManager") +Q_DECLARE_JNI_CLASS(ProviderInfo, "android/content/pm/ProviderInfo") +Q_DECLARE_JNI_CLASS(PackageInfo, "android/content/pm/PackageInfo") +Q_DECLARE_JNI_CLASS(Signature, "android/content/pm/Signature") + +namespace FontProvider { + +static QString fetchFont(const QString &query) +{ + using namespace QtJniTypes; + + static QMap<QString, QString> triedFonts; + const auto it = triedFonts.find(query); + if (it != triedFonts.constEnd()) + return it.value(); + + QString fontFamily; + triedFonts[query] = fontFamily; // mark as tried + + QStringList loadedFamilies; + if (QFile file(query); file.open(QIODevice::ReadOnly)) { + qCDebug(lcIconEngineFontDownload) << "Loading font from resource" << query; + const QByteArray fontData = file.readAll(); + int fontId = QFontDatabase::addApplicationFontFromData(fontData); + loadedFamilies << QFontDatabase::applicationFontFamilies(fontId); + } else if (!query.startsWith(u":/"_s)) { + const QString package = u"com.google.android.gms"_s; + const QString authority = u"com.google.android.gms.fonts"_s; + + // First we access the content provider to get the signatures of the authority for the package + const auto context = QtAndroidPrivate::context(); + + auto packageManager = context.callMethod<PackageManager>("getPackageManager"); + if (!packageManager.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to instantiate PackageManager"); + return fontFamily; + } + const int signaturesField = PackageManager::getStaticField<int>("GET_SIGNATURES"); + auto providerInfo = packageManager.callMethod<ProviderInfo>("resolveContentProvider", + authority, 0); + if (!providerInfo.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to resolve content provider"); + return fontFamily; + } + const QString packageName = providerInfo.getField<QString>("packageName"); + if (packageName != package) { + qCWarning(lcIconEngineFontDownload, "Mismatched provider package - expected '%s', got '%s'", + package.toUtf8().constData(), packageName.toUtf8().constData()); + return fontFamily; + } + auto packageInfo = packageManager.callMethod<PackageInfo>("getPackageInfo", + package, signaturesField); + if (!packageInfo.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to get package info with signature field %d", + signaturesField); + return fontFamily; + } + const auto signatures = packageInfo.getField<Signature[]>("signatures"); + if (!signatures.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to get signature array from package info"); + return fontFamily; + } + + // FontRequest wants a list of sets for the certificates + ArrayList outerList; + HashSet innerSet; + Q_ASSERT(outerList.isValid() && innerSet.isValid()); + + for (const auto &signature : signatures) { + const QJniArray<jbyte> byteArray = signature.callMethod<jbyte[]>("toByteArray"); + + // add takes an Object, not an Array + if (!innerSet.callMethod<jboolean>("add", byteArray.object<jobject>())) + qCWarning(lcIconEngineFontDownload, "Failed to add signature to set"); + } + // Add the set to the list + if (!outerList.callMethod<jboolean>("add", innerSet.object())) + qCWarning(lcIconEngineFontDownload, "Failed to add set to certificate list"); + + // FontRequest constructor wants a List interface, not an ArrayList + FontRequest fontRequest(authority, package, query, outerList.object<List>()); + if (!fontRequest.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to create font request for '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + // Call FontsContractCompat::fetchFonts with the FontRequest object + auto fontFamilyResult = FontsContractCompat::callStaticMethod<FontFamilyResult>( + "fetchFonts", + context, + CancellationSignal(nullptr), + fontRequest); + if (!fontFamilyResult.isValid()) { + qCWarning(lcIconEngineFontDownload, "Failed to fetch fonts for query '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + enum class StatusCode { + OK = 0, + UNEXPECTED_DATA_PROVIDED = 1, + WRONG_CERTIFICATES = 2, + }; + + const StatusCode statusCode = fontFamilyResult.callMethod<StatusCode>("getStatusCode"); + switch (statusCode) { + case StatusCode::OK: + break; + case StatusCode::UNEXPECTED_DATA_PROVIDED: + qCWarning(lcIconEngineFontDownload, "Provider returned unexpected data for query '%s'", + query.toUtf8().constData()); + return fontFamily; + case StatusCode::WRONG_CERTIFICATES: + qCWarning(lcIconEngineFontDownload, "Wrong Certificates provided in query '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + const auto fontInfos = fontFamilyResult.callMethod<FontInfo[]>("getFonts"); + if (!fontInfos.isValid()) { + qCWarning(lcIconEngineFontDownload, "FontFamilyResult::getFonts returned null object for '%s'", + query.toUtf8().constData()); + return fontFamily; + } + + auto contentResolver = context.callMethod<ContentResolver>("getContentResolver"); + + for (QJniObject fontInfo : fontInfos) { + if (!fontInfo.isValid()) { + qCDebug(lcIconEngineFontDownload, "Received null-fontInfo object, skipping"); + continue; + } + enum class ResultCode { + OK = 0, + FONT_NOT_FOUND = 1, + FONT_UNAVAILABLE = 2, + MALFORMED_QUERY = 3, + }; + const ResultCode resultCode = fontInfo.callMethod<ResultCode>("getResultCode"); + switch (resultCode) { + case ResultCode::OK: + break; + case ResultCode::FONT_NOT_FOUND: + qCWarning(lcIconEngineFontDownload, "Font '%s' could not be found", + query.toUtf8().constData()); + return fontFamily; + case ResultCode::FONT_UNAVAILABLE: + qCWarning(lcIconEngineFontDownload, "Font '%s' is unavailable at", + query.toUtf8().constData()); + return fontFamily; + case ResultCode::MALFORMED_QUERY: + qCWarning(lcIconEngineFontDownload, "Query string '%s' is malformed", + query.toUtf8().constData()); + return fontFamily; + } + auto fontUri = fontInfo.callMethod<Uri>("getUri"); + // in this case the Font URI is always a content scheme file, made + // so the app requesting it has permissions to open + auto fileDescriptor = contentResolver.callMethod<ParcelFileDescriptor>("openFileDescriptor", + fontUri, u"r"_s); + if (!fileDescriptor.isValid()) { + qCWarning(lcIconEngineFontDownload, "Font file '%s' not accessible", + fontUri.toString().toUtf8().constData()); + continue; + } + + int fd = fileDescriptor.callMethod<int>("detachFd"); + QFile file; + file.open(fd, QFile::OpenModeFlag::ReadOnly, QFile::FileHandleFlag::AutoCloseHandle); + const QByteArray fontData = file.readAll(); + qCDebug(lcIconEngineFontDownload) << "Font file read:" << fontData.size() << "bytes"; + int fontId = QFontDatabase::addApplicationFontFromData(fontData); + loadedFamilies << QFontDatabase::applicationFontFamilies(fontId); + } + } + + qCDebug(lcIconEngineFontDownload) << "Query '" << query << "' added families" << loadedFamilies; + if (!loadedFamilies.isEmpty()) + fontFamily = loadedFamilies.first(); + triedFonts[query] = fontFamily; + return fontFamily; +} +} + +QString QAndroidPlatformIconEngine::glyphs() const +{ + if (!QFontInfo(m_iconFont).exactMatch()) + return {}; + + static constexpr std::pair<QLatin1StringView, QStringView> glyphMap[] = { + {"address-book-new"_L1, u"\ue0e0"}, + {"application-exit"_L1, u"\ue5cd"}, + {"appointment-new"_L1, u"\ue878"}, + {"call-start"_L1, u"\ue0b0"}, + {"call-stop"_L1, u"\ue0b1"}, + {"contact-new"_L1, u"\uf22e"}, + {"document-new"_L1, u"\ue89c"}, + {"document-open"_L1, u"\ue2c8"}, + {"document-open-recent"_L1, u"\ue4a7"}, + {"document-page-setup"_L1, u"\uf88c"}, + {"document-print"_L1, u"\ue8ad"}, + {"document-print-preview"_L1, u"\uefb2"}, + {"document-properties"_L1, u"\uf775"}, + {"document-revert"_L1, u"\ue929"}, + {"document-save"_L1, u"\ue161"}, + {"document-save-as"_L1, u"\ueb60"}, + {"document-send"_L1, u"\uf09b"}, + {"edit-clear"_L1, u"\ue872"}, + {"edit-copy"_L1, u"\ue14d"}, + {"edit-cut"_L1, u"\ue14e"}, + {"edit-delete"_L1, u"\ue14a"}, + {"edit-find"_L1, u"\ue8b6"}, + {"edit-find-replace"_L1, u"\ue881"}, + {"edit-paste"_L1, u"\ue14f"}, + {"edit-redo"_L1, u"\ue15a"}, + {"edit-select-all"_L1, u"\ue162"}, + {"edit-undo"_L1, u"\ue166"}, + {"folder-new"_L1, u"\ue2cc"}, + {"format-indent-less"_L1, u"\ue23d"}, + {"format-indent-more"_L1, u"\ue23e"}, + {"format-justify-center"_L1, u"\ue234"}, + {"format-justify-fill"_L1, u"\ue235"}, + {"format-justify-left"_L1, u"\ue236"}, + {"format-justify-right"_L1, u"\ue237"}, + {"format-text-direction-ltr"_L1, u"\ue247"}, + {"format-text-direction-rtl"_L1, u"\ue248"}, + {"format-text-bold"_L1, u"\ue238"}, + {"format-text-italic"_L1, u"\ue23f"}, + {"format-text-underline"_L1, u"\ue249"}, + {"format-text-strikethrough"_L1, u"\ue246"}, + {"go-bottom"_L1,u"\ue258"}, + {"go-down"_L1,u"\uf1e3"}, + {"go-first"_L1, u"\ue5dc"}, + {"go-home"_L1, u"\ue88a"}, + {"go-jump"_L1, u"\uf719"}, + {"go-last"_L1, u"\ue5dd"}, + {"go-next"_L1, u"\ue5c8"}, + {"go-previous"_L1, u"\ue5c4"}, + {"go-top"_L1, u"\ue25a"}, + {"go-up"_L1, u"\uf1e0"}, + {"help-about"_L1, u"\ue88e"}, + {"help-contents"_L1, u"\ue8de"}, + {"help-faq"_L1, u"\uf04c"}, + {"insert-image"_L1, u"\ue43e"}, + {"insert-link"_L1, u"\ue178"}, + //{"insert-object"_L1, u"\u"}, + {"insert-text"_L1, u"\uf827"}, + {"list-add"_L1, u"\ue145"}, + {"list-remove"_L1, u"\ue15b"}, + {"mail-forward"_L1, u"\ue154"}, + {"mail-mark-important"_L1, u"\ue937"}, + //{"mail-mark-junk"_L1, u"\u"}, + //{"mail-mark-notjunk"_L1, u"\u"}, + {"mail-mark-read"_L1, u"\uf18c"}, + {"mail-mark-unread"_L1, u"\ue9bc"}, + {"mail-message-new"_L1, u"\ue3c9"}, + {"mail-reply-all"_L1, u"\ue15f"}, + {"mail-reply-sender"_L1, u"\ue15e"}, + {"mail-send"_L1, u"\ue163"}, + //{"mail-send-receive"_L1, u"\u"}, + {"media-eject"_L1, u"\ue8fb"}, + {"media-playback-pause"_L1, u"\ue034"}, + {"media-playback-start"_L1, u"\ue037"}, + {"media-playback-stop"_L1, u"\ue047"}, + {"media-record"_L1, u"\uf679"}, + {"media-seek-backward"_L1, u"\ue020"}, + {"media-seek-forward"_L1, u"\ue01f"}, + {"media-skip-backward"_L1, u"\ue045"}, + {"media-skip-forward"_L1, u"\ue044"}, + //{"object-flip-horizontal"_L1, u"\u"}, + //{"object-flip-vertical"_L1, u"\u"}, + {"object-rotate-left"_L1, u"\ue419"}, + {"object-rotate-right"_L1, u"\ue41a"}, + {"process-stop"_L1, u"\ue5c9"}, + {"system-lock-screen"_L1, u"\ue897"}, + {"system-log-out"_L1, u"\ue9ba"}, + //{"system-run"_L1, u"\u"}, + {"system-search"_L1, u"\uef70"}, + {"system-reboot"_L1, u"\uf053"}, + {"system-shutdown"_L1, u"\ue8ac"}, + {"tools-check-spelling"_L1, u"\ue8ce"}, + {"view-fullscreen"_L1, u"\ue5d0"}, + {"view-refresh"_L1, u"\ue5d5"}, + {"view-restore"_L1, u"\uf1cf"}, + {"view-sort-ascending"_L1, u"\ue25a"}, + {"view-sort-descending"_L1, u"\ue258"}, + {"window-close"_L1, u"\ue5cd"}, + {"window-new"_L1, u"\uf710"}, + {"zoom-fit-best"_L1, u"\uea10"}, + {"zoom-in"_L1, u"\ue8ff"}, + {"zoom-original"_L1, u"\ue5d1"}, + {"zoom-out"_L1, u"\ue900"}, + {"process-working"_L1, u"\uef64"}, + {"accessories-calculator"_L1, u"\uea5f"}, + {"accessories-character-map"_L1, u"\uf8a3"}, + {"accessories-dictionary"_L1, u"\uf539"}, + {"accessories-text-editor"_L1, u"\ue262"}, + {"help-browser"_L1, u"\ue887"}, + {"multimedia-volume-control"_L1, u"\ue050"}, + {"preferences-desktop-accessibility"_L1, u"\uf05d"}, + {"preferences-desktop-font"_L1, u"\ue165"}, + {"preferences-desktop-keyboard"_L1, u"\ue312"}, + //{"preferences-desktop-locale"_L1, u"\u"}, + {"preferences-desktop-multimedia"_L1, u"\uea75"}, + //{"preferences-desktop-screensaver"_L1, u"\u"}, + {"preferences-desktop-theme"_L1, u"\uf560"}, + {"preferences-desktop-wallpaper"_L1, u"\ue1bc"}, + {"system-file-manager"_L1, u"\ue2c7"}, + {"system-software-install"_L1, u"\ueb71"}, + {"system-software-update"_L1, u"\ue8d7"}, + {"utilities-system-monitor"_L1, u"\uef5b"}, + {"utilities-terminal"_L1, u"\ueb8e"}, + //{"applications-accessories"_L1, u"\u"}, + {"applications-development"_L1, u"\ue720"}, + {"applications-engineering"_L1, u"\uea3d"}, + {"applications-games"_L1, u"\uf135"}, + //{"applications-graphics"_L1, u"\u"}, + {"applications-internet"_L1, u"\ue80b"}, + {"applications-multimedia"_L1, u"\uf06a"}, + //{"applications-office"_L1, u"\u"}, + //{"applications-other"_L1, u"\u"}, + {"applications-science"_L1, u"\uea4b"}, + //{"applications-system"_L1, u"\u"}, + //{"applications-utilities"_L1, u"\u"}, + {"preferences-desktop"_L1, u"\ueb97"}, + //{"preferences-desktop-peripherals"_L1, u"\u"}, + {"preferences-desktop-personal"_L1, u"\uf835"}, + //{"preferences-other"_L1, u"\u"}, + {"preferences-system"_L1, u"\ue8b8"}, + {"preferences-system-network"_L1, u"\ue894"}, + {"system-help"_L1, u"\ue887"}, + //{"audio-card"_L1, u"\u"}, + {"audio-input-microphone"_L1, u"\ue029"}, + {"battery"_L1, u"\ue1a4"}, + {"camera-photo"_L1, u"\ue412"}, + {"camera-video"_L1, u"\ue04b"}, + {"camera-web"_L1, u"\uf7a6"}, + {"computer"_L1, u"\ue30a"}, + {"drive-harddisk"_L1, u"\uf80e"}, + {"drive-optical"_L1, u"\ue019"}, // same as media-optical + //{"drive-removable-media"_L1, u"\u"}, + {"input-gaming"_L1, u"\uf5ee"}, + {"input-keyboard"_L1, u"\ue312"}, + {"input-mouse"_L1, u"\ue323"}, + //{"input-tablet"_L1, u"\u"}, + //{"media-flash"_L1, u"\u"}, + //{"media-floppy"_L1, u"\u"}, + {"media-optical"_L1, u"\ue019"}, + //{"media-tape"_L1, u"\u"}, + //{"modem"_L1, u"\u"}, + //{"multimedia-player"_L1, u"\u"}, + //{"network-wired"_L1, u"\u"}, + {"network-wireless"_L1, u"\ue63e"}, + //{"pda"_L1, u"\u"}, + {"phone"_L1, u"\ue32c"}, + {"printer"_L1, u"\ue8ad"}, + {"scanner"_L1, u"\ue329"}, + {"video-display"_L1, u"\uf06a"}, + //{"emblem-default"_L1, u"\u"}, + {"emblem-documents"_L1, u"\ue873"}, + {"emblem-downloads"_L1, u"\uf090"}, + {"emblem-favorite"_L1, u"\uf090"}, + {"emblem-important"_L1, u"\ue645"}, + {"emblem-mail"_L1, u"\ue158"}, + {"emblem-photos"_L1, u"\ue413"}, + //{"emblem-readonly"_L1, u"\u"}, + {"emblem-shared"_L1, u"\ue413"}, + //{"emblem-symbolic-link"_L1, u"\u"}, + //{"emblem-synchronized"_L1, u"\u"}, + {"emblem-system"_L1, u"\ue8b8"}, + //{"emblem-unreadable"_L1, u"\u"}, + {"folder"_L1, u"\ue2c7"}, + //{"folder-remote"_L1, u"\u"}, + {"network-server"_L1, u"\ue875"}, + {"network-workgroup"_L1, u"\ue1a0"}, + {"start-here"_L1, u"\ue089"}, + {"user-bookmarks"_L1, u"\ue98b"}, + {"user-desktop"_L1, u"\ue30a"}, + {"user-home"_L1, u"\ue88a"}, + {"user-trash"_L1, u"\ue872"}, + {"appointment-missed"_L1, u"\ue615"}, + {"appointment-soon"_L1, u"\uf540"}, + {"audio-volume-high"_L1, u"\ue050"}, + {"audio-volume-low"_L1, u"\ue04d"}, + //{"audio-volume-medium"_L1, u"\u"}, + {"audio-volume-muted"_L1, u"\ue04e"}, + {"battery-caution"_L1, u"\ue19c"}, + {"battery-low"_L1, u"\uf147"}, + {"dialog-error"_L1, u"\ue000"}, + {"dialog-information"_L1, u"\ue88e"}, + {"dialog-password"_L1, u"\uf042"}, + {"dialog-question"_L1, u"\ueb8b"}, + {"dialog-warning"_L1, u"\ue002"}, + {"folder-drag-accept"_L1, u"\ue9a3"}, + {"folder-open"_L1, u"\ue2c8"}, + {"folder-visiting"_L1, u"\ue8a7"}, + {"image-loading"_L1, u"\ue41a"}, + {"image-missing"_L1, u"\ue3ad"}, + {"mail-attachment"_L1, u"\ue2bc"}, + {"mail-unread"_L1, u"\uf18a"}, + {"mail-read"_L1, u"\uf18c"}, + //{"mail-replied"_L1, u"\u"}, + //{"mail-signed"_L1, u"\u"}, + //{"mail-signed-verified"_L1, u"\u"}, + {"media-playlist-repeat"_L1, u"\ue040"}, + {"media-playlist-shuffle"_L1, u"\ue043"}, + {"network-error"_L1, u"\uead9"}, + {"network-idle"_L1, u"\ue51f"}, + {"network-offline"_L1, u"\uf239"}, + {"network-receive"_L1, u"\ue2c0"}, + {"network-transmit"_L1, u"\ue2c3"}, + {"network-transmit-receive"_L1, u"\uea18"}, + {"printer-error"_L1, u"\uf7a0"}, + {"printer-printing"_L1, u"\uf7a1"}, + {"security-high"_L1, u"\ue32a"}, + {"security-medium"_L1, u"\ue9e0"}, + {"security-low"_L1, u"\uf012"}, + {"software-update-available"_L1, u"\ue923"}, + {"software-update-urgent"_L1, u"\uf05a"}, + {"sync-error"_L1, u"\ue629"}, + {"sync-synchronizing"_L1, u"\ue627"}, + //{"task-due"_L1, u"\u"}, + //{"task-past-due"_L1, u"\u"}, + {"user-available"_L1, u"\uf565"}, + {"user-away"_L1, u"\ue510"}, + //{"user-idle"_L1, u"\u"}, + {"user-offline"_L1, u"\uf7b3"}, + {"user-trash-full"_L1, u"\ue872"}, //delete + //{"user-trash-full"_L1, u"\ue92b"}, //delete_forever + {"weather-clear"_L1, u"\uf157"}, + {"weather-clear-night"_L1, u"\uf159"}, + {"weather-few-clouds"_L1, u"\uf172"}, + {"weather-few-clouds-night"_L1, u"\uf174"}, + {"weather-fog"_L1, u"\ue818"}, + //{"weather-overcast"_L1, u"\u"}, + {"weather-severe-alert"_L1, u"\ue002"}, //warning + //{"weather-severe-alert"_L1, u"\uebd3"},//severe_cold + {"weather-showers"_L1, u"\uf176"}, + //{"weather-showers-scattered"_L1, u"\u"}, + {"weather-snow"_L1, u"\ue80f"}, //snowing + //{"weather-snow"_L1, u"\ue2cd"}, //weather_snowy + //{"weather-snow"_L1, u"\ue810"},//cloudy_snowing + {"weather-storm"_L1, u"\uf070"}, + }; + + const auto it = std::find_if(std::begin(glyphMap), std::end(glyphMap), [this](const auto &c){ + return c.first == m_iconName; + }); + return it != std::end(glyphMap) ? it->second.toString() + : (m_iconName.length() == 1 ? m_iconName : QString()); +} + +QAndroidPlatformIconEngine::QAndroidPlatformIconEngine(const QString &iconName) + : m_iconName(iconName) + , m_glyphs(glyphs()) +{ + QString fontFamily; + // The MaterialIcons-*.ttf and MaterialSymbols* font files are available from + // https://github.com/google/material-design-icons/tree/master. If one of them is + // packaged as a resource with the application, then we use it. We prioritize + // a variable font. + const QStringList fontCandidates = { + "MaterialSymbolsOutlined[FILL,GRAD,opsz,wght].ttf", + "MaterialSymbolsRounded[FILL,GRAD,opsz,wght].ttf", + "MaterialSymbolsSharp[FILL,GRAD,opsz,wght].ttf", + "MaterialIcons-Regular.ttf", + "MaterialIconsOutlined-Regular.otf", + "MaterialIconsRound-Regular.otf", + "MaterialIconsSharp-Regular.otf", + "MaterialIconsTwoTone-Regular.otf", + }; + for (const auto &fontCandidate : fontCandidates) { + fontFamily = FontProvider::fetchFont(u":/qt-project.org/icons/%1"_s.arg(fontCandidate)); + if (!fontFamily.isEmpty()) + break; + } + + // Otherwise we try to download the Outlined version of Material Symbols + const QString key = qEnvironmentVariable("QT_GOOGLE_FONTS_KEY"); + if (fontFamily.isEmpty() && !key.isEmpty()) + fontFamily = FontProvider::fetchFont(u"key=%1&name=Material+Symbols+Outlined"_s.arg(key)); + + // last resort - use any Material Icons + if (fontFamily.isEmpty()) + fontFamily = u"Material Icons"_s; + m_iconFont = QFont(fontFamily); +} + +QAndroidPlatformIconEngine::~QAndroidPlatformIconEngine() +{} + +QIconEngine *QAndroidPlatformIconEngine::clone() const +{ + return new QAndroidPlatformIconEngine(m_iconName); +} + +QString QAndroidPlatformIconEngine::key() const +{ + return u"QAndroidPlatformIconEngine"_s; +} + +QString QAndroidPlatformIconEngine::iconName() +{ + return m_iconName; +} + +bool QAndroidPlatformIconEngine::isNull() +{ + if (m_glyphs.isEmpty()) + return true; + const QChar c0 = m_glyphs.at(0); + const QFontMetrics fontMetrics(m_iconFont); + if (c0.category() == QChar::Other_Surrogate && m_glyphs.size() > 1) + return !fontMetrics.inFontUcs4(QChar::surrogateToUcs4(c0, m_glyphs.at(1))); + return !fontMetrics.inFont(c0); +} + +QList<QSize> QAndroidPlatformIconEngine::availableSizes(QIcon::Mode, QIcon::State) +{ + return {{16, 16}, {24, 24}, {48, 48}, {128, 128}}; +} + +QSize QAndroidPlatformIconEngine::actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return QIconEngine::actualSize(size, mode, state); +} + +QPixmap QAndroidPlatformIconEngine::pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) +{ + return scaledPixmap(size, mode, state, 1.0); +} + +QPixmap QAndroidPlatformIconEngine::scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) +{ + const quint64 cacheKey = calculateCacheKey(mode, state); + if (cacheKey != m_cacheKey || m_pixmap.size() != size || m_pixmap.devicePixelRatio() != scale) { + m_pixmap = QPixmap(size * scale); + m_pixmap.fill(Qt::transparent); + m_pixmap.setDevicePixelRatio(scale); + + QPainter painter(&m_pixmap); + paint(&painter, QRect(QPoint(), size), mode, state); + + m_cacheKey = cacheKey; + } + + return m_pixmap; +} + +void QAndroidPlatformIconEngine::paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) +{ + Q_UNUSED(state); + + painter->save(); + QFont renderFont(m_iconFont); + renderFont.setPixelSize(rect.height()); + painter->setFont(renderFont); + + QPalette palette; + switch (mode) { + case QIcon::Active: + painter->setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Normal: + painter->setPen(palette.color(QPalette::Active, QPalette::Text)); + break; + case QIcon::Disabled: + painter->setPen(palette.color(QPalette::Disabled, QPalette::Text)); + break; + case QIcon::Selected: + painter->setPen(palette.color(QPalette::Active, QPalette::HighlightedText)); + break; + } + + painter->drawText(rect, Qt::AlignCenter, m_glyphs); + painter->restore(); +} + +QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformiconengine.h b/src/plugins/platforms/android/qandroidplatformiconengine.h new file mode 100644 index 0000000000..cac54481d9 --- /dev/null +++ b/src/plugins/platforms/android/qandroidplatformiconengine.h @@ -0,0 +1,44 @@ +// Copyright (C) 2023 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 + +#ifndef QANDROIDPLATFORMICONENGINE_H +#define QANDROIDPLATFORMICONENGINE_H + +#include <QtGui/qiconengine.h> +#include <QtGui/qfont.h> + +QT_BEGIN_NAMESPACE + +class QAndroidPlatformIconEngine : public QIconEngine +{ +public: + QAndroidPlatformIconEngine(const QString &iconName); + ~QAndroidPlatformIconEngine(); + QIconEngine *clone() const override; + QString key() const override; + QString iconName() override; + bool isNull() override; + + QList<QSize> availableSizes(QIcon::Mode, QIcon::State) override; + QSize actualSize(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap pixmap(const QSize &size, QIcon::Mode mode, QIcon::State state) override; + QPixmap scaledPixmap(const QSize &size, QIcon::Mode mode, QIcon::State state, qreal scale) override; + void paint(QPainter *painter, const QRect &rect, QIcon::Mode mode, QIcon::State state) override; + +private: + static constexpr quint64 calculateCacheKey(QIcon::Mode mode, QIcon::State state) + { + return (quint64(mode) << 32) | state; + } + QString glyphs() const; + + const QString m_iconName; + QFont m_iconFont; + const QString m_glyphs; + mutable QPixmap m_pixmap; + mutable quint64 m_cacheKey = {}; +}; + +QT_END_NAMESPACE + +#endif // QANDROIDPLATFORMICONENGINE_H diff --git a/src/plugins/platforms/android/qandroidplatformintegration.cpp b/src/plugins/platforms/android/qandroidplatformintegration.cpp index 19a7326115..6efd3fc631 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.cpp +++ b/src/plugins/platforms/android/qandroidplatformintegration.cpp @@ -9,7 +9,6 @@ #include "qabstracteventdispatcher.h" #include "qandroideventdispatcher.h" #include "qandroidplatformaccessibility.h" -#include "qandroidplatformbackingstore.h" #include "qandroidplatformclipboard.h" #include "qandroidplatformfontdatabase.h" #include "qandroidplatformforeignwindow.h" @@ -29,6 +28,7 @@ #include <QtGui/private/qeglpbuffer_p.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qoffscreensurface_p.h> +#include <QtGui/private/qrhibackingstore_p.h> #include <qpa/qplatformoffscreensurface.h> #include <qpa/qplatformwindow.h> #include <qpa/qwindowsysteminterface.h> @@ -46,24 +46,52 @@ QT_BEGIN_NAMESPACE using namespace Qt::StringLiterals; -QSize QAndroidPlatformIntegration::m_defaultScreenSize = QSize(320, 455); -QRect QAndroidPlatformIntegration::m_defaultAvailableGeometry = QRect(0, 0, 320, 455); -QSize QAndroidPlatformIntegration::m_defaultPhysicalSize = QSize(50, 71); +Q_CONSTINIT QSize QAndroidPlatformIntegration::m_defaultScreenSize = QSize(320, 455); +Q_CONSTINIT QRect QAndroidPlatformIntegration::m_defaultAvailableGeometry = QRect(0, 0, 320, 455); +Q_CONSTINIT QSize QAndroidPlatformIntegration::m_defaultPhysicalSize = QSize(50, 71); Qt::ScreenOrientation QAndroidPlatformIntegration::m_orientation = Qt::PrimaryOrientation; Qt::ScreenOrientation QAndroidPlatformIntegration::m_nativeOrientation = Qt::PrimaryOrientation; bool QAndroidPlatformIntegration::m_showPasswordEnabled = false; -static bool m_running = false; + +Q_DECLARE_JNI_CLASS(QtNative, "org/qtproject/qt/android/QtNative") +Q_DECLARE_JNI_CLASS(QtDisplayManager, "org/qtproject/qt/android/QtDisplayManager") +Q_DECLARE_JNI_CLASS(Display, "android/view/Display") + +Q_DECLARE_JNI_CLASS(List, "java/util/List") + +namespace { + +QAndroidPlatformScreen* createScreenForDisplayId(int displayId) +{ + const QJniObject display = QtJniTypes::QtDisplayManager::callStaticMethod<QtJniTypes::Display>( + "getDisplay", QtAndroidPrivate::context(), displayId); + if (!display.isValid()) + return nullptr; + return new QAndroidPlatformScreen(display); +} + +static bool isValidAndroidContextForRendering() +{ + return QtAndroid::isQtApplication() ? QtAndroidPrivate::activity().isValid() + : QtAndroidPrivate::context().isValid(); +} + +} // anonymous namespace void *QAndroidPlatformNativeInterface::nativeResourceForIntegration(const QByteArray &resource) { if (resource=="JavaVM") return QtAndroid::javaVM(); - if (resource == "QtActivity") - return QtAndroid::activity(); - if (resource == "QtService") - return QtAndroid::service(); + if (resource == "QtActivity") { + extern Q_CORE_EXPORT jobject qt_androidActivity(); + return qt_androidActivity(); + } + if (resource == "QtService") { + extern Q_CORE_EXPORT jobject qt_androidService(); + return qt_androidService(); + } if (resource == "AndroidStyleData") { if (m_androidStyle) { if (m_androidStyle->m_styleData.isEmpty()) @@ -136,10 +164,6 @@ void QAndroidPlatformNativeInterface::customEvent(QEvent *event) api->accessibility()->setActive(QtAndroidAccessibility::isActive()); #endif // QT_CONFIG(accessibility) - if (!m_running) { - m_running = true; - QtAndroid::notifyQtAndroidPluginRunning(m_running); - } api->flushPendingUpdates(); } @@ -163,10 +187,32 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ if (Q_UNLIKELY(!eglBindAPI(EGL_OPENGL_ES_API))) qFatal("Could not bind GL_ES API"); - m_primaryScreen = new QAndroidPlatformScreen(); - QWindowSystemInterface::handleScreenAdded(m_primaryScreen); - m_primaryScreen->setSizeParameters(m_defaultPhysicalSize, m_defaultScreenSize, - m_defaultAvailableGeometry); + using namespace QtJniTypes; + m_primaryDisplayId = Display::getStaticField<jint>("DEFAULT_DISPLAY"); + const QJniObject nativeDisplaysList = QtDisplayManager::callStaticMethod<List>( + "getAvailableDisplays", QtAndroidPrivate::context()); + + const int numberOfAvailableDisplays = nativeDisplaysList.callMethod<jint>("size"); + for (int i = 0; i < numberOfAvailableDisplays; ++i) { + const QJniObject display = + nativeDisplaysList.callObjectMethod<jobject, jint>("get", jint(i)); + const int displayId = display.callMethod<jint>("getDisplayId"); + const bool isPrimary = (m_primaryDisplayId == displayId); + auto screen = new QAndroidPlatformScreen(display); + + if (isPrimary) + m_primaryScreen = screen; + + QWindowSystemInterface::handleScreenAdded(screen, isPrimary); + m_screens[displayId] = screen; + } + + if (numberOfAvailableDisplays == 0) { + // If no displays are found, add a dummy display + auto defaultScreen = new QAndroidPlatformScreen(QJniObject {}); + m_primaryScreen = defaultScreen; + QWindowSystemInterface::handleScreenAdded(defaultScreen, true); + } m_mainThread = QThread::currentThread(); @@ -183,9 +229,9 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ m_accessibility = new QAndroidPlatformAccessibility(); #endif // QT_CONFIG(accessibility) - QJniObject javaActivity(QtAndroid::activity()); + QJniObject javaActivity = QtAndroidPrivate::activity(); if (!javaActivity.isValid()) - javaActivity = QtAndroid::service(); + javaActivity = QtAndroidPrivate::service(); if (javaActivity.isValid()) { QJniObject resources = javaActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;"); @@ -225,6 +271,10 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ maxTouchPoints, 0); QWindowSystemInterface::registerInputDevice(m_touchDevice); + + QWindowSystemInterface::registerInputDevice( + new QInputDevice("Virtual keyboard"_L1, 0, QInputDevice::DeviceType::Keyboard, + {}, qApp)); } auto contentResolver = javaActivity.callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); @@ -260,11 +310,11 @@ static bool needsBasicRenderloopWorkaround() void QAndroidPlatformIntegration::initialize() { - const QString icStr = QPlatformInputContextFactory::requested(); - if (icStr.isNull()) + const auto icStrs = QPlatformInputContextFactory::requested(); + if (icStrs.isEmpty()) m_inputContext.reset(new QAndroidInputContext); else - m_inputContext.reset(QPlatformInputContextFactory::create(icStr)); + m_inputContext.reset(QPlatformInputContextFactory::create(icStrs)); } bool QAndroidPlatformIntegration::hasCapability(Capability cap) const @@ -272,13 +322,19 @@ bool QAndroidPlatformIntegration::hasCapability(Capability cap) const switch (cap) { case ApplicationState: return true; case ThreadedPixmaps: return true; - case NativeWidgets: return QtAndroid::activity(); - case OpenGL: return QtAndroid::activity(); - case ForeignWindows: return QtAndroid::activity(); - case ThreadedOpenGL: return !needsBasicRenderloopWorkaround() && QtAndroid::activity(); - case RasterGLSurface: return QtAndroid::activity(); + case NativeWidgets: return QtAndroidPrivate::activity().isValid(); + case OpenGL: + return isValidAndroidContextForRendering(); + case ForeignWindows: + return isValidAndroidContextForRendering(); + case ThreadedOpenGL: + return !needsBasicRenderloopWorkaround() && isValidAndroidContextForRendering(); + case RasterGLSurface: return QtAndroidPrivate::activity().isValid(); case TopStackedNativeChildWindows: return false; case MaximizeUsingFullscreenGeometry: return true; + // FIXME QTBUG-118849 - we do not implement grabWindow() anymore, calling it will return + // a null QPixmap also for raster windows - for OpenGL windows this was always true + case ScreenWindowGrabbing: return false; default: return QPlatformIntegration::hasCapability(cap); } @@ -286,15 +342,15 @@ bool QAndroidPlatformIntegration::hasCapability(Capability cap) const QPlatformBackingStore *QAndroidPlatformIntegration::createPlatformBackingStore(QWindow *window) const { - if (!QtAndroid::activity()) + if (!QtAndroidPrivate::activity().isValid()) return nullptr; - return new QAndroidPlatformBackingStore(window); + return new QRhiBackingStore(window); } QPlatformOpenGLContext *QAndroidPlatformIntegration::createPlatformOpenGLContext(QOpenGLContext *context) const { - if (!QtAndroid::activity()) + if (!isValidAndroidContextForRendering()) return nullptr; QSurfaceFormat format(context->format()); format.setAlphaBufferSize(8); @@ -312,7 +368,7 @@ QOpenGLContext *QAndroidPlatformIntegration::createOpenGLContext(EGLContext cont QPlatformOffscreenSurface *QAndroidPlatformIntegration::createPlatformOffscreenSurface(QOffscreenSurface *surface) const { - if (!QtAndroid::activity()) + if (!QtAndroidPrivate::activity().isValid()) return nullptr; QSurfaceFormat format(surface->requestedFormat()); @@ -326,7 +382,7 @@ QPlatformOffscreenSurface *QAndroidPlatformIntegration::createPlatformOffscreenS QOffscreenSurface *QAndroidPlatformIntegration::createOffscreenSurface(ANativeWindow *nativeSurface) const { - if (!QtAndroid::activity() || !nativeSurface) + if (!QtAndroidPrivate::activity().isValid() || !nativeSurface) return nullptr; auto *surface = new QOffscreenSurface; @@ -337,7 +393,7 @@ QOffscreenSurface *QAndroidPlatformIntegration::createOffscreenSurface(ANativeWi QPlatformWindow *QAndroidPlatformIntegration::createPlatformWindow(QWindow *window) const { - if (!QtAndroid::activity()) + if (!isValidAndroidContextForRendering()) return nullptr; #if QT_CONFIG(vulkan) @@ -432,7 +488,7 @@ QStringList QAndroidPlatformIntegration::themeNames() const QPlatformTheme *QAndroidPlatformIntegration::createPlatformTheme(const QString &name) const { if (androidThemeName == name) - return new QAndroidPlatformTheme(m_androidPlatformNativeInterface); + return QAndroidPlatformTheme::instance(m_androidPlatformNativeInterface); return 0; } @@ -488,6 +544,18 @@ void QAndroidPlatformIntegration::setScreenSize(int width, int height) QMetaObject::invokeMethod(m_primaryScreen, "setSize", Qt::AutoConnection, Q_ARG(QSize, QSize(width, height))); } +Qt::ColorScheme QAndroidPlatformIntegration::m_colorScheme = Qt::ColorScheme::Light; + +void QAndroidPlatformIntegration::updateColorScheme(Qt::ColorScheme colorScheme) +{ + if (m_colorScheme == colorScheme) + return; + m_colorScheme = colorScheme; + + QMetaObject::invokeMethod(qGuiApp, + [] () { QAndroidPlatformTheme::instance()->updateColorScheme();}); +} + void QAndroidPlatformIntegration::setScreenSizeParameters(const QSize &physicalSize, const QSize &screenSize, const QRect &availableGeometry) @@ -505,6 +573,49 @@ void QAndroidPlatformIntegration::setRefreshRate(qreal refreshRate) QMetaObject::invokeMethod(m_primaryScreen, "setRefreshRate", Qt::AutoConnection, Q_ARG(qreal, refreshRate)); } + +void QAndroidPlatformIntegration::handleScreenAdded(int displayId) +{ + auto result = m_screens.insert(displayId, nullptr); + if (result.first->second == nullptr) { + auto it = result.first; + it->second = createScreenForDisplayId(displayId); + if (it->second == nullptr) + return; + const bool isPrimary = (m_primaryDisplayId == displayId); + if (isPrimary) + m_primaryScreen = it->second; + QWindowSystemInterface::handleScreenAdded(it->second, isPrimary); + } else { + qWarning() << "Display with id" << displayId << "already exists."; + } +} + +void QAndroidPlatformIntegration::handleScreenChanged(int displayId) +{ + auto it = m_screens.find(displayId); + if (it == m_screens.end() || it->second == nullptr) { + handleScreenAdded(displayId); + } + // We do not do anything more here as handling of change of + // rotation and refresh rate is done in QtActivityDelegate java class + // which calls QAndroidPlatformIntegration::setOrientation, and + // QAndroidPlatformIntegration::setRefreshRate accordingly. +} + +void QAndroidPlatformIntegration::handleScreenRemoved(int displayId) +{ + auto it = m_screens.find(displayId); + + if (it == m_screens.end()) + return; + + if (it->second != nullptr) + QWindowSystemInterface::handleScreenRemoved(it->second); + + m_screens.erase(it); +} + #if QT_CONFIG(vulkan) QPlatformVulkanInstance *QAndroidPlatformIntegration::createPlatformVulkanInstance(QVulkanInstance *instance) const diff --git a/src/plugins/platforms/android/qandroidplatformintegration.h b/src/plugins/platforms/android/qandroidplatformintegration.h index 6e87c9c02b..b7bfb58d1d 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.h +++ b/src/plugins/platforms/android/qandroidplatformintegration.h @@ -13,6 +13,9 @@ #include <qpa/qplatformnativeinterface.h> #include <qpa/qplatformopenglcontext.h> #include <qpa/qplatformoffscreensurface.h> +#include <qpa/qplatformtheme.h> +#include <private/qflatmap_p.h> +#include <QtCore/qvarlengtharray.h> #include <EGL/egl.h> #include <memory> @@ -74,6 +77,10 @@ public: QPlatformFontDatabase *fontDatabase() const override; + void handleScreenAdded(int displayId); + void handleScreenChanged(int displayId); + void handleScreenRemoved(int displayId); + #ifndef QT_NO_CLIPBOARD QPlatformClipboard *clipboard() const override; #endif @@ -103,6 +110,8 @@ public: void flushPendingUpdates(); + static void updateColorScheme(Qt::ColorScheme colorScheme); + static Qt::ColorScheme colorScheme() { return m_colorScheme; } #if QT_CONFIG(vulkan) QPlatformVulkanInstance *createPlatformVulkanInstance(QVulkanInstance *instance) const override; #endif @@ -115,6 +124,8 @@ private: QThread *m_mainThread; + static Qt::ColorScheme m_colorScheme; + static QRect m_defaultAvailableGeometry; static QSize m_defaultPhysicalSize; static QSize m_defaultScreenSize; @@ -127,6 +138,17 @@ private: QAndroidPlatformNativeInterface *m_androidPlatformNativeInterface; QAndroidPlatformServices *m_androidPlatformServices; + // Handling the multiple screens connected. Every display is identified + // with an unique (autoincremented) displayID. The values of this ID will + // not repeat during the OS runtime. We use this value as the key in the + // storage of screens. + QFlatMap<int, QAndroidPlatformScreen *, std::less<int> + , QVarLengthArray<int, 10> + , QVarLengthArray<QAndroidPlatformScreen *, 10> > m_screens; + // ID of the primary display, in documentation it is said to be always 0, + // but nevertheless it is retrieved + int m_primaryDisplayId = 0; + #ifndef QT_NO_CLIPBOARD QPlatformClipboard *m_androidPlatformClipboard; #endif diff --git a/src/plugins/platforms/android/qandroidplatformmenu.cpp b/src/plugins/platforms/android/qandroidplatformmenu.cpp index 4ddd6ea29a..e59fd2089d 100644 --- a/src/plugins/platforms/android/qandroidplatformmenu.cpp +++ b/src/plugins/platforms/android/qandroidplatformmenu.cpp @@ -119,7 +119,7 @@ void QAndroidPlatformMenu::showPopup(const QWindow *parentWindow, const QRect &t Q_UNUSED(parentWindow); Q_UNUSED(item); setVisible(true); - QtAndroidMenu::showContextMenu(this, targetRect, QJniEnvironment().jniEnv()); + QtAndroidMenu::showContextMenu(this, targetRect); } QPlatformMenuItem *QAndroidPlatformMenu::menuItemForTag(quintptr tag) const diff --git a/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp b/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp index c1ec2fbdd6..13d14eb391 100644 --- a/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformopenglwindow.cpp @@ -24,41 +24,19 @@ QT_BEGIN_NAMESPACE QAndroidPlatformOpenGLWindow::QAndroidPlatformOpenGLWindow(QWindow *window, EGLDisplay display) :QAndroidPlatformWindow(window), m_eglDisplay(display) { + if (window->surfaceType() == QSurface::RasterSurface) + window->setSurfaceType(QSurface::OpenGLSurface); } QAndroidPlatformOpenGLWindow::~QAndroidPlatformOpenGLWindow() { m_surfaceWaitCondition.wakeOne(); lockSurface(); - if (m_nativeSurfaceId != -1) - QtAndroid::destroySurface(m_nativeSurfaceId); + destroySurface(); clearEgl(); unlockSurface(); } -void QAndroidPlatformOpenGLWindow::repaint(const QRegion ®ion) -{ - // This is only for real raster top-level windows. Stop in all other cases. - if ((window()->surfaceType() == QSurface::RasterGLSurface && qt_window_private(window())->compositing) - || window()->surfaceType() == QSurface::OpenGLSurface - || QAndroidPlatformWindow::parent()) - return; - - QRect currentGeometry = geometry(); - - QRect dirtyClient = region.boundingRect(); - QRect dirtyRegion(currentGeometry.left() + dirtyClient.left(), - currentGeometry.top() + dirtyClient.top(), - dirtyClient.width(), - dirtyClient.height()); - QRect mOldGeometryLocal = m_oldGeometry; - m_oldGeometry = currentGeometry; - // If this is a move, redraw the previous location - if (mOldGeometryLocal != currentGeometry) - platformScreen()->setDirty(mOldGeometryLocal); - platformScreen()->setDirty(dirtyRegion); -} - void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect) { if (rect == geometry()) @@ -67,8 +45,9 @@ void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect) m_oldGeometry = geometry(); QAndroidPlatformWindow::setGeometry(rect); - if (m_nativeSurfaceId != -1) - QtAndroid::setSurfaceGeometry(m_nativeSurfaceId, rect); + + + setNativeGeometry(rect); QRect availableGeometry = screen()->availableGeometry(); if (rect.width() > 0 @@ -77,25 +56,23 @@ void QAndroidPlatformOpenGLWindow::setGeometry(const QRect &rect) && availableGeometry.height() > 0) { QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), rect.size())); } - - if (rect.topLeft() != m_oldGeometry.topLeft()) - repaint(QRegion(rect)); } EGLSurface QAndroidPlatformOpenGLWindow::eglSurface(EGLConfig config) { - if (QAndroidEventDispatcherStopper::stopped() || QGuiApplication::applicationState() == Qt::ApplicationSuspended) + if (QAndroidEventDispatcherStopper::stopped() || + QGuiApplication::applicationState() == Qt::ApplicationSuspended) { return m_eglSurface; + } QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1) { + if (!m_surfaceCreated) { AndroidDeadlockProtector protector; if (!protector.acquire()) return m_eglSurface; - const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); - m_nativeSurfaceId = QtAndroid::createSurface(this, geometry(), windowStaysOnTop, 32); + createSurface(); m_surfaceWaitCondition.wait(&m_surfaceMutex); } @@ -110,7 +87,7 @@ EGLSurface QAndroidPlatformOpenGLWindow::eglSurface(EGLConfig config) bool QAndroidPlatformOpenGLWindow::checkNativeSurface(EGLConfig config) { QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1 || !m_androidSurfaceObject.isValid()) + if (!m_surfaceCreated || !m_androidSurfaceObject.isValid()) return false; // makeCurrent is NOT needed. createEgl(config); @@ -127,10 +104,7 @@ void QAndroidPlatformOpenGLWindow::applicationStateChanged(Qt::ApplicationState QAndroidPlatformWindow::applicationStateChanged(state); if (state <= Qt::ApplicationHidden) { lockSurface(); - if (m_nativeSurfaceId != -1) { - QtAndroid::destroySurface(m_nativeSurfaceId); - m_nativeSurfaceId = -1; - } + destroySurface(); clearEgl(); unlockSurface(); } @@ -173,24 +147,4 @@ void QAndroidPlatformOpenGLWindow::clearEgl() } } -void QAndroidPlatformOpenGLWindow::surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) -{ - Q_UNUSED(jniEnv); - Q_UNUSED(w); - Q_UNUSED(h); - - lockSurface(); - m_androidSurfaceObject = surface; - if (surface) // wait until we have a valid surface to draw into - m_surfaceWaitCondition.wakeOne(); - unlockSurface(); - - if (surface) { - // repaint the window, when we have a valid surface - QRect availableGeometry = screen()->availableGeometry(); - if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) - QWindowSystemInterface::handleExposeEvent(window(), QRegion(QRect(QPoint(), geometry().size()))); - } -} - QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformopenglwindow.h b/src/plugins/platforms/android/qandroidplatformopenglwindow.h index 8c31368b65..c1ae57fe85 100644 --- a/src/plugins/platforms/android/qandroidplatformopenglwindow.h +++ b/src/plugins/platforms/android/qandroidplatformopenglwindow.h @@ -5,7 +5,6 @@ #ifndef QANDROIDPLATFORMOPENGLWINDOW_H #define QANDROIDPLATFORMOPENGLWINDOW_H -#include "androidsurfaceclient.h" #include "qandroidplatformwindow.h" #include <QWaitCondition> @@ -16,7 +15,7 @@ QT_BEGIN_NAMESPACE -class QAndroidPlatformOpenGLWindow : public QAndroidPlatformWindow, public AndroidSurfaceClient +class QAndroidPlatformOpenGLWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformOpenGLWindow(QWindow *window, EGLDisplay display); @@ -30,10 +29,7 @@ public: void applicationStateChanged(Qt::ApplicationState) override; - void repaint(const QRegion ®ion) override; - protected: - void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) override; void createEgl(EGLConfig config); void clearEgl(); @@ -42,9 +38,6 @@ private: EGLSurface m_eglSurface = EGL_NO_SURFACE; EGLNativeWindowType m_nativeWindow = nullptr; - int m_nativeSurfaceId = -1; - QJniObject m_androidSurfaceObject; - QWaitCondition m_surfaceWaitCondition; QSurfaceFormat m_format; QRect m_oldGeometry; }; diff --git a/src/plugins/platforms/android/qandroidplatformscreen.cpp b/src/plugins/platforms/android/qandroidplatformscreen.cpp index d11b7b197b..4d752a3cc3 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.cpp +++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp @@ -8,7 +8,6 @@ #include <qpa/qwindowsysteminterface.h> #include "qandroidplatformscreen.h" -#include "qandroidplatformbackingstore.h" #include "qandroidplatformintegration.h" #include "qandroidplatformwindow.h" #include "androidjnimain.h" @@ -52,11 +51,23 @@ private: # define PROFILE_SCOPE #endif -QAndroidPlatformScreen::QAndroidPlatformScreen() +Q_DECLARE_JNI_CLASS(Display, "android/view/Display") +Q_DECLARE_JNI_CLASS(DisplayMetrics, "android/util/DisplayMetrics") +Q_DECLARE_JNI_CLASS(Resources, "android/content/res/Resources") +Q_DECLARE_JNI_CLASS(Size, "android/util/Size") +Q_DECLARE_JNI_CLASS(QtNative, "org/qtproject/qt/android/QtNative") +Q_DECLARE_JNI_CLASS(QtDisplayManager, "org/qtproject/qt/android/QtDisplayManager") +Q_DECLARE_JNI_CLASS(QtWindowInterface, "org/qtproject/qt/android/QtWindowInterface") + +Q_DECLARE_JNI_CLASS(DisplayMode, "android/view/Display$Mode") + +QAndroidPlatformScreen::QAndroidPlatformScreen(const QJniObject &displayObject) : QObject(), QPlatformScreen() { m_availableGeometry = QAndroidPlatformIntegration::m_defaultAvailableGeometry; m_size = QAndroidPlatformIntegration::m_defaultScreenSize; + m_physicalSize = QAndroidPlatformIntegration::m_defaultPhysicalSize; + // Raster only apps should set QT_ANDROID_RASTER_IMAGE_DEPTH to 16 // is way much faster than 32 if (qEnvironmentVariableIntValue("QT_ANDROID_RASTER_IMAGE_DEPTH") == 16) { @@ -66,69 +77,72 @@ QAndroidPlatformScreen::QAndroidPlatformScreen() m_format = QImage::Format_ARGB32_Premultiplied; m_depth = 32; } - m_physicalSize = QAndroidPlatformIntegration::m_defaultPhysicalSize; - connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QAndroidPlatformScreen::applicationStateChanged); - - QJniObject activity(QtAndroid::activity()); - if (!activity.isValid()) - return; - QJniObject display; - if (QNativeInterface::QAndroidApplication::sdkVersion() < 30) { - display = activity.callObjectMethod("getWindowManager", "()Landroid/view/WindowManager;") - .callObjectMethod("getDefaultDisplay", "()Landroid/view/Display;"); - } else { - display = activity.callObjectMethod("getDisplay", "()Landroid/view/Display;"); - } - if (!display.isValid()) - return; - m_name = display.callObjectMethod("getName", "()Ljava/lang/String;").toString(); - m_refreshRate = display.callMethod<jfloat>("getRefreshRate"); + connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, + &QAndroidPlatformScreen::applicationStateChanged); - if (QNativeInterface::QAndroidApplication::sdkVersion() < 23) { - m_modes << Mode { .size = m_physicalSize.toSize(), .refreshRate = m_refreshRate }; + if (!displayObject.isValid()) return; - } - QJniEnvironment env; - const jint currentMode = display.callObjectMethod("getMode", "()Landroid/view/Display$Mode;") - .callMethod<jint>("getModeId"); - const auto modes = display.callObjectMethod("getSupportedModes", - "()[Landroid/view/Display$Mode;"); - const auto modesArray = jobjectArray(modes.object()); - const auto sz = env->GetArrayLength(modesArray); - for (jsize i = 0; i < sz; ++i) { - auto mode = QJniObject::fromLocalRef(env->GetObjectArrayElement(modesArray, i)); - if (currentMode == mode.callMethod<jint>("getModeId")) - m_currentMode = m_modes.size(); - m_modes << Mode { .size = QSize { mode.callMethod<jint>("getPhysicalHeight"), - mode.callMethod<jint>("getPhysicalWidth") }, - .refreshRate = mode.callMethod<jfloat>("getRefreshRate") }; + m_name = displayObject.callObjectMethod<jstring>("getName").toString(); + m_refreshRate = displayObject.callMethod<jfloat>("getRefreshRate"); + m_displayId = displayObject.callMethod<jint>("getDisplayId"); + + const QJniObject context = QNativeInterface::QAndroidApplication::context(); + const auto displayContext = context.callMethod<QtJniTypes::Context>("createDisplayContext", + displayObject.object<QtJniTypes::Display>()); + + const auto sizeObj = QtJniTypes::QtDisplayManager::callStaticMethod<QtJniTypes::Size>( + "getDisplaySize", displayContext, + displayObject.object<QtJniTypes::Display>()); + m_size = QSize(sizeObj.callMethod<int>("getWidth"), sizeObj.callMethod<int>("getHeight")); + + const auto resources = displayContext.callMethod<QtJniTypes::Resources>("getResources"); + const auto metrics = resources.callMethod<QtJniTypes::DisplayMetrics>("getDisplayMetrics"); + const float xdpi = metrics.getField<float>("xdpi"); + const float ydpi = metrics.getField<float>("ydpi"); + + // Potentially densityDpi could be used instead of xpdi/ydpi to do the calculation, + // but the results are not consistent with devices specs. + // (https://issuetracker.google.com/issues/194120500) + m_physicalSize.setWidth(qRound(m_size.width() / xdpi * 25.4)); + m_physicalSize.setHeight(qRound(m_size.height() / ydpi * 25.4)); + + if (QNativeInterface::QAndroidApplication::sdkVersion() >= 23) { + const QJniObject currentMode = displayObject.callObjectMethod<QtJniTypes::DisplayMode>("getMode"); + m_currentMode = currentMode.callMethod<jint>("getModeId"); + + const QJniObject supportedModes = displayObject.callObjectMethod<QtJniTypes::DisplayMode[]>( + "getSupportedModes"); + const auto modeArray = jobjectArray(supportedModes.object()); + + QJniEnvironment env; + const auto size = env->GetArrayLength(modeArray); + for (jsize i = 0; i < size; ++i) { + const auto mode = QJniObject::fromLocalRef(env->GetObjectArrayElement(modeArray, i)); + m_modes << QPlatformScreen::Mode { + .size = QSize { mode.callMethod<jint>("getPhysicalWidth"), + mode.callMethod<jint>("getPhysicalHeight") }, + .refreshRate = mode.callMethod<jfloat>("getRefreshRate") + }; + } } - - if (m_modes.isEmpty()) - m_modes << Mode { .size = m_physicalSize.toSize(), .refreshRate = m_refreshRate }; } QAndroidPlatformScreen::~QAndroidPlatformScreen() { - if (m_id != -1) { - QtAndroid::destroySurface(m_id); - m_surfaceWaitCondition.wakeOne(); - releaseSurface(); - } } -QWindow *QAndroidPlatformScreen::topWindow() const +QWindow *QAndroidPlatformScreen::topVisibleWindow() const { for (QAndroidPlatformWindow *w : m_windowStack) { - if (w->window()->type() == Qt::Window || - w->window()->type() == Qt::Popup || - w->window()->type() == Qt::Dialog) { + Qt::WindowType type = w->window()->type(); + if (w->window()->isVisible() && + (type == Qt::Window || type == Qt::Popup || type == Qt::Dialog)) { return w->window(); } } - return 0; + return nullptr; } QWindow *QAndroidPlatformScreen::topLevelAt(const QPoint &p) const @@ -140,16 +154,6 @@ QWindow *QAndroidPlatformScreen::topLevelAt(const QPoint &p) const return 0; } -bool QAndroidPlatformScreen::event(QEvent *event) -{ - if (event->type() == QEvent::UpdateRequest) { - doRedraw(); - m_updatePending = false; - return true; - } - return QObject::event(event); -} - void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) { if (window->parent() && window->isRaster()) @@ -159,83 +163,56 @@ void QAndroidPlatformScreen::addWindow(QAndroidPlatformWindow *window) return; m_windowStack.prepend(window); - if (window->isRaster()) { - m_rasterSurfaces.ref(); - setDirty(window->geometry()); - } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("addTopLevelWindow", + window->nativeWindow()); + + if (window->window()->isVisible()) + topVisibleWindowChanged(); } void QAndroidPlatformScreen::removeWindow(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - m_windowStack.removeOne(window); if (m_windowStack.contains(window)) qWarning() << "Failed to remove window"; - if (window->isRaster()) { - m_rasterSurfaces.deref(); - setDirty(window->geometry()); - } + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("removeTopLevelWindow", + window->nativeViewId()); - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::raise(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - int index = m_windowStack.indexOf(window); - if (index <= 0) + if (index < 0) return; - m_windowStack.move(index, 0); - if (window->isRaster()) { - setDirty(window->geometry()); + if (index > 0) { + m_windowStack.move(index, 0); + + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("bringChildToFront", + window->nativeViewId()); } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::lower(QAndroidPlatformWindow *window) { - if (window->parent() && window->isRaster()) - return; - int index = m_windowStack.indexOf(window); if (index == -1 || index == (m_windowStack.size() - 1)) return; m_windowStack.move(index, m_windowStack.size() - 1); - if (window->isRaster()) { - setDirty(window->geometry()); - } - QWindow *w = topWindow(); - QWindowSystemInterface::handleWindowActivated(w, Qt::ActiveWindowFocusReason); - topWindowChanged(w); -} -void QAndroidPlatformScreen::scheduleUpdate() -{ - if (!m_updatePending) { - m_updatePending = true; - QCoreApplication::postEvent(this, new QEvent(QEvent::UpdateRequest)); - } -} + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + reg->callInterface<QtJniTypes::QtWindowInterface, void>("bringChildToBack", + window->nativeViewId()); -void QAndroidPlatformScreen::setDirty(const QRect &rect) -{ - QRect intersection = rect.intersected(m_availableGeometry); - m_dirtyRect |= intersection; - scheduleUpdate(); + topVisibleWindowChanged(); } void QAndroidPlatformScreen::setPhysicalSize(const QSize &size) @@ -267,6 +244,11 @@ void QAndroidPlatformScreen::setSizeParameters(const QSize &physicalSize, const } } +int QAndroidPlatformScreen::displayId() const +{ + return m_displayId; +} + void QAndroidPlatformScreen::setRefreshRate(qreal refreshRate) { if (refreshRate == m_refreshRate) @@ -282,7 +264,6 @@ void QAndroidPlatformScreen::setOrientation(Qt::ScreenOrientation orientation) void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) { - QMutexLocker lock(&m_surfaceMutex); if (m_availableGeometry == rect) return; @@ -303,170 +284,31 @@ void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) } } } - - if (m_id != -1) { - releaseSurface(); - QtAndroid::setSurfaceGeometry(m_id, rect); - } } void QAndroidPlatformScreen::applicationStateChanged(Qt::ApplicationState state) { - for (QAndroidPlatformWindow *w : qAsConst(m_windowStack)) + for (QAndroidPlatformWindow *w : std::as_const(m_windowStack)) w->applicationStateChanged(state); - - if (state <= Qt::ApplicationHidden) { - lockSurface(); - QtAndroid::destroySurface(m_id); - m_id = -1; - releaseSurface(); - unlockSurface(); - } } -void QAndroidPlatformScreen::topWindowChanged(QWindow *w) +void QAndroidPlatformScreen::topVisibleWindowChanged() { + QWindow *w = topVisibleWindow(); + QWindowSystemInterface::handleFocusWindowChanged(w, Qt::ActiveWindowFocusReason); QtAndroidMenu::setActiveTopLevelWindow(w); - - if (w != 0) { + if (w && w->handle()) { QAndroidPlatformWindow *platformWindow = static_cast<QAndroidPlatformWindow *>(w->handle()); - if (platformWindow != 0) + if (platformWindow) platformWindow->updateSystemUiVisibility(); } } -int QAndroidPlatformScreen::rasterSurfaces() -{ - return m_rasterSurfaces; -} - -void QAndroidPlatformScreen::doRedraw(QImage* screenGrabImage) -{ - PROFILE_SCOPE; - if (!QtAndroid::activity()) - return; - - if (m_dirtyRect.isEmpty()) - return; - - // Stop if there are no visible raster windows. If we only have RasterGLSurface - // windows that have renderToTexture children (i.e. they need the OpenGL path) then - // we do not need an overlay surface. - bool hasVisibleRasterWindows = false; - for (QAndroidPlatformWindow *window : qAsConst(m_windowStack)) { - if (window->window()->isVisible() && window->isRaster() && !qt_window_private(window->window())->compositing) { - hasVisibleRasterWindows = true; - break; - } - } - if (!hasVisibleRasterWindows) { - lockSurface(); - if (m_id != -1) { - QtAndroid::destroySurface(m_id); - releaseSurface(); - m_id = -1; - } - unlockSurface(); - return; - } - QMutexLocker lock(&m_surfaceMutex); - if (m_id == -1 && m_rasterSurfaces) { - m_id = QtAndroid::createSurface(this, geometry(), true, m_depth); - AndroidDeadlockProtector protector; - if (!protector.acquire()) - return; - m_surfaceWaitCondition.wait(&m_surfaceMutex); - } - - if (!m_nativeSurface) - return; - - ANativeWindow_Buffer nativeWindowBuffer; - ARect nativeWindowRect; - nativeWindowRect.top = m_dirtyRect.top(); - nativeWindowRect.left = m_dirtyRect.left(); - nativeWindowRect.bottom = m_dirtyRect.bottom() + 1; // for some reason that I don't understand the QRect bottom needs to +1 to be the same with ARect bottom - nativeWindowRect.right = m_dirtyRect.right() + 1; // same for the right - - int ret; - if ((ret = ANativeWindow_lock(m_nativeSurface, &nativeWindowBuffer, &nativeWindowRect)) < 0) { - qWarning() << "ANativeWindow_lock() failed! error=" << ret; - return; - } - - int bpp = 4; - if (nativeWindowBuffer.format == WINDOW_FORMAT_RGB_565) { - bpp = 2; - m_pixelFormat = QImage::Format_RGB16; - } - - QImage screenImage(reinterpret_cast<uchar *>(nativeWindowBuffer.bits) - , nativeWindowBuffer.width, nativeWindowBuffer.height - , nativeWindowBuffer.stride * bpp , m_pixelFormat); - - QPainter compositePainter(&screenImage); - compositePainter.setCompositionMode(QPainter::CompositionMode_Source); - - QRegion visibleRegion(m_dirtyRect); - for (QAndroidPlatformWindow *window : qAsConst(m_windowStack)) { - if (!window->window()->isVisible() - || qt_window_private(window->window())->compositing - || !window->isRaster()) - continue; - - for (const QRect &rect : std::vector<QRect>(visibleRegion.begin(), visibleRegion.end())) { - QRect targetRect = window->geometry(); - targetRect &= rect; - - if (targetRect.isNull()) - continue; - - visibleRegion -= targetRect; - QRect windowRect = targetRect.translated(-window->geometry().topLeft()); - QAndroidPlatformBackingStore *backingStore = static_cast<QAndroidPlatformWindow *>(window)->backingStore(); - if (backingStore) - compositePainter.drawImage(targetRect.topLeft(), backingStore->toImage(), windowRect); - } - } - - for (const QRect &rect : visibleRegion) - compositePainter.fillRect(rect, QColor(Qt::transparent)); - - ret = ANativeWindow_unlockAndPost(m_nativeSurface); - if (ret >= 0) - m_dirtyRect = QRect(); - - if (screenGrabImage) { - if (screenGrabImage->size() != screenImage.size()) { - uchar* bytes = static_cast<uchar*>(malloc(screenImage.height() * screenImage.bytesPerLine())); - *screenGrabImage = QImage(bytes, screenImage.width(), screenImage.height(), - screenImage.bytesPerLine(), m_pixelFormat, - [](void* ptr){ if (ptr) free (ptr);}); - } - memcpy(screenGrabImage->bits(), - screenImage.bits(), - screenImage.bytesPerLine() * screenImage.height()); - } - m_repaintOccurred = true; -} - -QPixmap QAndroidPlatformScreen::doScreenShot(QRect grabRect) -{ - if (!m_repaintOccurred) - return QPixmap::fromImage(m_lastScreenshot.copy(grabRect)); - QRect tmp = m_dirtyRect; - m_dirtyRect = geometry(); - doRedraw(&m_lastScreenshot); - m_dirtyRect = tmp; - m_repaintOccurred = false; - return QPixmap::fromImage(m_lastScreenshot.copy(grabRect)); -} - static const int androidLogicalDpi = 72; QDpi QAndroidPlatformScreen::logicalDpi() const { - qreal lDpi = QtAndroid::scaledDensity() * androidLogicalDpi; + qreal lDpi = QtAndroid::pixelDensity() * androidLogicalDpi; return QDpi(lDpi, lDpi); } @@ -484,67 +326,4 @@ Qt::ScreenOrientation QAndroidPlatformScreen::nativeOrientation() const { return QAndroidPlatformIntegration::m_nativeOrientation; } - -void QAndroidPlatformScreen::surfaceChanged(JNIEnv *env, jobject surface, int w, int h) -{ - lockSurface(); - if (surface && w > 0 && h > 0) { - releaseSurface(); - m_nativeSurface = ANativeWindow_fromSurface(env, surface); - QMetaObject::invokeMethod(this, "setDirty", Qt::QueuedConnection, Q_ARG(QRect, QRect(0, 0, w, h))); - } else { - releaseSurface(); - } - unlockSurface(); - m_surfaceWaitCondition.wakeOne(); -} - -void QAndroidPlatformScreen::releaseSurface() -{ - if (m_nativeSurface) { - ANativeWindow_release(m_nativeSurface); - m_nativeSurface = 0; - } -} - -/*! - This function is called when Qt needs to be able to grab the content of a window. - - Returns the content of the window specified with the WId handle within the boundaries of - QRect(x, y, width, height). -*/ -QPixmap QAndroidPlatformScreen::grabWindow(WId window, int x, int y, int width, int height) const -{ - QRectF screenshotRect(x, y, width, height); - QWindow* wnd = 0; - if (window) - { - const auto windowList = qApp->allWindows(); - for (QWindow *w : windowList) - if (w->winId() == window) { - wnd = w; - break; - } - } - if (wnd) { - const qreal factor = logicalDpi().first / androidLogicalDpi; //HighDPI factor; - QRectF wndRect = wnd->geometry(); - if (wnd->parent()) - wndRect.moveTopLeft(wnd->parent()->mapToGlobal(wndRect.topLeft().toPoint())); - if (!qFuzzyCompare(factor, 1)) - wndRect = QRectF(wndRect.left() * factor, wndRect.top() * factor, - wndRect.width() * factor, wndRect.height() * factor); - - if (!screenshotRect.isEmpty()) { - screenshotRect.moveTopLeft(wndRect.topLeft() + screenshotRect.topLeft()); - screenshotRect = screenshotRect.intersected(wndRect); - } else { - screenshotRect = wndRect; - } - } else { - screenshotRect = screenshotRect.isValid() ? screenshotRect : geometry(); - } - return const_cast<QAndroidPlatformScreen *>(this)->doScreenShot(screenshotRect.toRect()); -} - QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformscreen.h b/src/plugins/platforms/android/qandroidplatformscreen.h index ef8371e72d..d850d0db09 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.h +++ b/src/plugins/platforms/android/qandroidplatformscreen.h @@ -5,26 +5,26 @@ #ifndef QANDROIDPLATFORMSCREEN_H #define QANDROIDPLATFORMSCREEN_H -#include "androidsurfaceclient.h" - #include <QList> #include <QPainter> #include <QTimer> #include <QWaitCondition> #include <QtCore/QJniObject> #include <qpa/qplatformscreen.h> - -#include <android/native_window.h> +#include <QtGui/qscreen_platform.h> QT_BEGIN_NAMESPACE class QAndroidPlatformWindow; -class QAndroidPlatformScreen: public QObject, public QPlatformScreen, public AndroidSurfaceClient + +class QAndroidPlatformScreen : public QObject, + public QPlatformScreen, + public QNativeInterface::QAndroidScreen { Q_OBJECT public: - QAndroidPlatformScreen(); + QAndroidPlatformScreen(const QJniObject &displayObject); ~QAndroidPlatformScreen(); QRect geometry() const override { return QRect(QPoint(), m_size); } @@ -38,22 +38,17 @@ public: int currentMode() const override { return m_currentMode; } int preferredMode() const override { return m_currentMode; } qreal refreshRate() const override { return m_refreshRate; } - - inline QWindow *topWindow() const; + inline QWindow *topVisibleWindow() const; QWindow *topLevelAt(const QPoint & p) const override; - // compositor api void addWindow(QAndroidPlatformWindow *window); void removeWindow(QAndroidPlatformWindow *window); void raise(QAndroidPlatformWindow *window); void lower(QAndroidPlatformWindow *window); - - void scheduleUpdate(); - void topWindowChanged(QWindow *w); - int rasterSurfaces(); + void topVisibleWindowChanged(); + int displayId() const override; public slots: - void setDirty(const QRect &rect); void setPhysicalSize(const QSize &size); void setAvailableGeometry(const QRect &rect); void setSize(const QSize &size); @@ -63,13 +58,8 @@ public slots: void setOrientation(Qt::ScreenOrientation orientation); protected: - bool event(QEvent *event) override; - typedef QList<QAndroidPlatformWindow *> WindowStackType; WindowStackType m_windowStack; - QRect m_dirtyRect; - bool m_updatePending = false; - QRect m_availableGeometry; int m_depth; QImage::Format m_format; @@ -78,31 +68,16 @@ protected: QString m_name; QList<Mode> m_modes; int m_currentMode = 0; + int m_displayId = -1; private: QDpi logicalDpi() const override; QDpi logicalBaseDpi() const override; Qt::ScreenOrientation orientation() const override; Qt::ScreenOrientation nativeOrientation() const override; - QPixmap grabWindow(WId window, int x, int y, int width, int height) const override; - void surfaceChanged(JNIEnv *env, jobject surface, int w, int h) override; - void releaseSurface(); void applicationStateChanged(Qt::ApplicationState); - QPixmap doScreenShot(QRect grabRect = QRect()); - -private slots: - void doRedraw(QImage *screenGrabImage = nullptr); - private: - int m_id = -1; - QAtomicInt m_rasterSurfaces = 0; - ANativeWindow* m_nativeSurface = nullptr; - QWaitCondition m_surfaceWaitCondition; QSize m_size; - - QImage m_lastScreenshot; - QImage::Format m_pixelFormat = QImage::Format_RGBA8888_Premultiplied; - bool m_repaintOccurred = false; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformservices.cpp b/src/plugins/platforms/android/qandroidplatformservices.cpp index 8f8f702011..39287aa905 100644 --- a/src/plugins/platforms/android/qandroidplatformservices.cpp +++ b/src/plugins/platforms/android/qandroidplatformservices.cpp @@ -24,17 +24,25 @@ QAndroidPlatformServices::QAndroidPlatformServices() QtAndroidPrivate::registerNewIntentListener(this); - QMetaObject::invokeMethod( - this, - [this] { - QJniObject context = QJniObject(QtAndroidPrivate::context()); - QJniObject intent = - context.callObjectMethod("getIntent", "()Landroid/content/Intent;"); - handleNewIntent(nullptr, intent.object()); - }, - Qt::QueuedConnection); + // Qt applications without Activity contexts cannot retrieve intents from the Activity. + if (QNativeInterface::QAndroidApplication::isActivityContext()) { + QMetaObject::invokeMethod( + this, + [this] { + QJniObject context = QJniObject(QtAndroidPrivate::context()); + QJniObject intent = + context.callObjectMethod("getIntent", "()Landroid/content/Intent;"); + handleNewIntent(nullptr, intent.object()); + }, + Qt::QueuedConnection); + } } +Q_DECLARE_JNI_CLASS(UriType, "android/net/Uri") +Q_DECLARE_JNI_CLASS(FileType, "java/io/File") +Q_DECLARE_JNI_CLASS(File, "java/io/File") +Q_DECLARE_JNI_CLASS(FileProvider, "androidx/core/content/FileProvider"); + bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) { QString mime; @@ -47,20 +55,44 @@ bool QAndroidPlatformServices::openUrl(const QUrl &theUrl) // if the file is local, we need to pass the MIME type, otherwise Android // does not start an Intent to view this file const auto fileScheme = "file"_L1; - if ((url.scheme().isEmpty() || url.scheme() == fileScheme) && QFile::exists(url.path())) { - // a real URL including the scheme is needed, else the Intent can not be started + + // a real URL including the scheme is needed, else the Intent can not be started + if (url.scheme().isEmpty()) url.setScheme(fileScheme); - QMimeDatabase mimeDb; - mime = mimeDb.mimeTypeForUrl(url).name(); - } + + if (url.scheme() == fileScheme) + mime = QMimeDatabase().mimeTypeForUrl(url).name(); + + const QJniObject mimeString = QJniObject::fromString(mime); using namespace QNativeInterface; - QJniObject urlString = QJniObject::fromString(url.toString()); - QJniObject mimeString = QJniObject::fromString(mime); - return QJniObject::callStaticMethod<jboolean>( - QtAndroid::applicationClass(), "openURL", - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)Z", - QAndroidApplication::context(), urlString.object(), mimeString.object()); + + auto openUrl = [mimeString](const QJniObject &url) { + return QJniObject::callStaticMethod<jboolean>(QtAndroid::applicationClass(), "openURL", + QAndroidApplication::context(), url.object<jstring>(), mimeString.object<jstring>()); + }; + + if (url.scheme() != fileScheme || QNativeInterface::QAndroidApplication::sdkVersion() < 24) + return openUrl(QJniObject::fromString(url.toString())); + + // Use FileProvider for file scheme with sdk >= 24 + const QJniObject context = QAndroidApplication::context(); + const auto appId = context.callMethod<jstring>("getPackageName").toString(); + const auto providerName = QJniObject::fromString(appId + ".qtprovider"_L1); + + const auto urlPath = QJniObject::fromString(url.path()); + const auto urlFile = QJniObject(QtJniTypes::Traits<QtJniTypes::File>::className(), + urlPath.object<jstring>()); + + const auto fileProviderUri = QJniObject::callStaticMethod<QtJniTypes::UriType>( + QtJniTypes::Traits<QtJniTypes::FileProvider>::className(), "getUriForFile", + QAndroidApplication::context(), providerName.object<jstring>(), + urlFile.object<QtJniTypes::FileType>()); + + if (fileProviderUri.isValid()) + return openUrl(fileProviderUri.callMethod<jstring>("toString")); + + return false; } bool QAndroidPlatformServices::openDocument(const QUrl &url) diff --git a/src/plugins/platforms/android/qandroidplatformtheme.cpp b/src/plugins/platforms/android/qandroidplatformtheme.cpp index c5b9ba9dee..99eeabac1d 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.cpp +++ b/src/plugins/platforms/android/qandroidplatformtheme.cpp @@ -4,6 +4,7 @@ #include "androidjnimain.h" #include "androidjnimenu.h" #include "qandroidplatformtheme.h" +#include "qandroidplatformiconengine.h" #include "qandroidplatformmenubar.h" #include "qandroidplatformmenu.h" #include "qandroidplatformmenuitem.h" @@ -22,6 +23,8 @@ QT_BEGIN_NAMESPACE +Q_LOGGING_CATEGORY(lcQpaMenus, "qt.qpa.menus") + using namespace Qt::StringLiterals; namespace { @@ -158,14 +161,13 @@ QJsonObject AndroidStyle::loadStyleData() if (!stylePath.isEmpty() && !stylePath.endsWith(slashChar)) stylePath += slashChar; - Q_ASSERT(!stylePath.isEmpty()); - - QString androidTheme = QLatin1StringView(qgetenv("QT_ANDROID_THEME")); - if (!androidTheme.isEmpty() && !androidTheme.endsWith(slashChar)) - androidTheme += slashChar; + const Qt::ColorScheme colorScheme = QAndroidPlatformTheme::instance() + ? QAndroidPlatformTheme::instance()->colorScheme() + : QAndroidPlatformIntegration::colorScheme(); + if (colorScheme == Qt::ColorScheme::Dark) + stylePath += "darkUiMode/"_L1; - if (!androidTheme.isEmpty() && QFileInfo::exists(stylePath + androidTheme + "style.json"_L1)) - stylePath += androidTheme; + Q_ASSERT(!stylePath.isEmpty()); QFile f(stylePath + "style.json"_L1); if (!f.open(QIODevice::ReadOnly)) @@ -185,13 +187,22 @@ QJsonObject AndroidStyle::loadStyleData() return document.object(); } -static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette) +static void loadAndroidStyle(QPalette *defaultPalette, std::shared_ptr<AndroidStyle> &style) { double pixelDensity = QHighDpiScaling::isActive() ? QtAndroid::pixelDensity() : 1.0; - std::shared_ptr<AndroidStyle> style = std::make_shared<AndroidStyle>(); + if (style) { + style->m_standardPalette = QPalette(); + style->m_palettes.clear(); + style->m_fonts.clear(); + style->m_QWidgetsFonts.clear(); + } else { + style = std::make_shared<AndroidStyle>(); + } + style->m_styleData = AndroidStyle::loadStyleData(); + if (style->m_styleData.isEmpty()) - return std::shared_ptr<AndroidStyle>(); + return; { QFont font("Droid Sans Mono"_L1, 14.0 * 100 / 72); @@ -293,11 +304,43 @@ static std::shared_ptr<AndroidStyle> loadAndroidStyle(QPalette *defaultPalette) // Extract palette information } } - return style; +} + +QAndroidPlatformTheme *QAndroidPlatformTheme::m_instance = nullptr; + +QAndroidPlatformTheme *QAndroidPlatformTheme::instance( + QAndroidPlatformNativeInterface *androidPlatformNativeInterface) +{ + if (androidPlatformNativeInterface && !m_instance) { + m_instance = new QAndroidPlatformTheme(androidPlatformNativeInterface); + } + return m_instance; } QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *androidPlatformNativeInterface) { + updateStyle(); + + androidPlatformNativeInterface->m_androidStyle = m_androidStyleData; + + // default in case the style has not set a font + m_systemFont = QFont("Roboto"_L1, 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi +} + +QAndroidPlatformTheme::~QAndroidPlatformTheme() +{ + m_instance = nullptr; +} + +void QAndroidPlatformTheme::updateColorScheme() +{ + updateStyle(); + QWindowSystemInterface::handleThemeChange(); +} + +void QAndroidPlatformTheme::updateStyle() +{ + QColor windowText = Qt::black; QColor background(229, 229, 229); QColor light = background.lighter(150); QColor mid(background.darker(130)); @@ -314,7 +357,27 @@ QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *an QColor highlight(148, 210, 231); QColor disabledShadow = shadow.lighter(150); - m_defaultPalette = QPalette(Qt::black,background,light,dark,mid,text,base); + if (colorScheme() == Qt::ColorScheme::Dark) { + // Colors were prepared based on Theme.DeviceDefault.DayNight + windowText = QColor(250, 250, 250); + background = QColor(48, 48, 48); + light = background.darker(150); + mid = background.lighter(130); + midLight = mid.darker(110); + base = background; + disabledBase = background; + dark = background.darker(150); + darkDisabled = dark.darker(110); + text = QColor(250, 250, 250); + highlightedText = QColor(250, 250, 250); + disabledText = QColor(96, 96, 96); + button = QColor(48, 48, 48); + shadow = QColor(32, 32, 32); + highlight = QColor(102, 178, 204); + disabledShadow = shadow.darker(150); + } + + m_defaultPalette = QPalette(windowText,background,light,dark,mid,text,base); m_defaultPalette.setBrush(QPalette::Midlight, midLight); m_defaultPalette.setBrush(QPalette::Button, button); m_defaultPalette.setBrush(QPalette::Shadow, shadow); @@ -330,34 +393,52 @@ QAndroidPlatformTheme::QAndroidPlatformTheme(QAndroidPlatformNativeInterface *an m_defaultPalette.setBrush(QPalette::Active, QPalette::Highlight, highlight); m_defaultPalette.setBrush(QPalette::Inactive, QPalette::Highlight, highlight); m_defaultPalette.setBrush(QPalette::Disabled, QPalette::Highlight, highlight.lighter(150)); - m_androidStyleData = loadAndroidStyle(&m_defaultPalette); - QGuiApplication::setPalette(m_defaultPalette); - androidPlatformNativeInterface->m_androidStyle = m_androidStyleData; - // default in case the style has not set a font - m_systemFont = QFont("Roboto"_L1, 14.0 * 100 / 72); // keep default size the same after changing from 100 dpi to 72 dpi + loadAndroidStyle(&m_defaultPalette, m_androidStyleData); } QPlatformMenuBar *QAndroidPlatformTheme::createPlatformMenuBar() const { - return new QAndroidPlatformMenuBar; + auto *menuBar = new QAndroidPlatformMenuBar; + qCDebug(lcQpaMenus) << "Created" << menuBar; + return menuBar; } QPlatformMenu *QAndroidPlatformTheme::createPlatformMenu() const { - return new QAndroidPlatformMenu; + auto *menu = new QAndroidPlatformMenu; + qCDebug(lcQpaMenus) << "Created" << menu; + return menu; } QPlatformMenuItem *QAndroidPlatformTheme::createPlatformMenuItem() const { - return new QAndroidPlatformMenuItem; + auto *menuItem = new QAndroidPlatformMenuItem; + qCDebug(lcQpaMenus) << "Created" << menuItem; + return menuItem; } void QAndroidPlatformTheme::showPlatformMenuBar() { + qCDebug(lcQpaMenus) << "Showing platform menu bar"; QtAndroidMenu::openOptionsMenu(); } +Qt::ColorScheme QAndroidPlatformTheme::colorScheme() const +{ + if (m_colorSchemeOverride != Qt::ColorScheme::Unknown) + return m_colorSchemeOverride; + return QAndroidPlatformIntegration::colorScheme(); +} + +void QAndroidPlatformTheme::requestColorScheme(Qt::ColorScheme scheme) +{ + m_colorSchemeOverride = scheme; + QMetaObject::invokeMethod(qGuiApp, [this]{ + updateColorScheme(); + }); +} + static inline int paletteType(QPlatformTheme::Palette type) { switch (type) { @@ -423,6 +504,11 @@ const QFont *QAndroidPlatformTheme::font(Font type) const return 0; } +QIconEngine *QAndroidPlatformTheme::createIconEngine(const QString &iconName) const +{ + return new QAndroidPlatformIconEngine(iconName); +} + QVariant QAndroidPlatformTheme::themeHint(ThemeHint hint) const { switch (hint) { diff --git a/src/plugins/platforms/android/qandroidplatformtheme.h b/src/plugins/platforms/android/qandroidplatformtheme.h index ec39ed4794..1b4ab5664d 100644 --- a/src/plugins/platforms/android/qandroidplatformtheme.h +++ b/src/plugins/platforms/android/qandroidplatformtheme.h @@ -9,13 +9,15 @@ #include <QtGui/qpalette.h> #include <QtCore/qhash.h> #include <QtCore/qbytearray.h> - +#include <QtCore/qloggingcategory.h> #include <QJsonObject> #include <memory> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaMenus) + struct AndroidStyle { static QJsonObject loadStyleData(); @@ -30,23 +32,34 @@ class QAndroidPlatformNativeInterface; class QAndroidPlatformTheme: public QPlatformTheme { public: - QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface); + ~QAndroidPlatformTheme(); + void updateColorScheme(); + void updateStyle(); QPlatformMenuBar *createPlatformMenuBar() const override; QPlatformMenu *createPlatformMenu() const override; QPlatformMenuItem *createPlatformMenuItem() const override; void showPlatformMenuBar() override; + Qt::ColorScheme colorScheme() const override; + void requestColorScheme(Qt::ColorScheme scheme) override; + const QPalette *palette(Palette type = SystemPalette) const override; const QFont *font(Font type = SystemFont) const override; + QIconEngine *createIconEngine(const QString &iconName) const override; QVariant themeHint(ThemeHint hint) const override; QString standardButtonText(int button) const override; bool usePlatformNativeDialog(DialogType type) const override; QPlatformDialogHelper *createPlatformDialogHelper(DialogType type) const override; + static QAndroidPlatformTheme *instance( + QAndroidPlatformNativeInterface * androidPlatformNativeInterface = nullptr); private: + QAndroidPlatformTheme(QAndroidPlatformNativeInterface * androidPlatformNativeInterface); + static QAndroidPlatformTheme * m_instance; std::shared_ptr<AndroidStyle> m_androidStyleData; QPalette m_defaultPalette; QFont m_systemFont; + Qt::ColorScheme m_colorSchemeOverride = Qt::ColorScheme::Unknown; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp b/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp index cb6bb6d390..4bf4f44fa1 100644 --- a/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformvulkanwindow.cpp @@ -18,7 +18,6 @@ QT_BEGIN_NAMESPACE QAndroidPlatformVulkanWindow::QAndroidPlatformVulkanWindow(QWindow *window) : QAndroidPlatformWindow(window), - m_nativeSurfaceId(-1), m_nativeWindow(nullptr), m_vkSurface(0), m_createVkSurface(nullptr), @@ -29,11 +28,7 @@ QAndroidPlatformVulkanWindow::QAndroidPlatformVulkanWindow(QWindow *window) QAndroidPlatformVulkanWindow::~QAndroidPlatformVulkanWindow() { m_surfaceWaitCondition.wakeOne(); - lockSurface(); - if (m_nativeSurfaceId != -1) - QtAndroid::destroySurface(m_nativeSurfaceId); - clearSurface(); - unlockSurface(); + destroyAndClearSurface(); } void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) @@ -44,8 +39,8 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) m_oldGeometry = geometry(); QAndroidPlatformWindow::setGeometry(rect); - if (m_nativeSurfaceId != -1) - QtAndroid::setSurfaceGeometry(m_nativeSurfaceId, rect); + if (m_surfaceCreated) + setNativeGeometry(rect); QRect availableGeometry = screen()->availableGeometry(); if (rect.width() > 0 @@ -54,22 +49,13 @@ void QAndroidPlatformVulkanWindow::setGeometry(const QRect &rect) && availableGeometry.height() > 0) { QWindowSystemInterface::handleExposeEvent(window(), QRect(QPoint(0, 0), rect.size())); } - - if (rect.topLeft() != m_oldGeometry.topLeft()) - repaint(QRegion(rect)); } void QAndroidPlatformVulkanWindow::applicationStateChanged(Qt::ApplicationState state) { QAndroidPlatformWindow::applicationStateChanged(state); if (state <= Qt::ApplicationHidden) { - lockSurface(); - if (m_nativeSurfaceId != -1) { - QtAndroid::destroySurface(m_nativeSurfaceId); - m_nativeSurfaceId = -1; - } - clearSurface(); - unlockSurface(); + destroyAndClearSurface(); } } @@ -91,27 +77,12 @@ void QAndroidPlatformVulkanWindow::clearSurface() } } -void QAndroidPlatformVulkanWindow::sendExpose() -{ - QRect availableGeometry = screen()->availableGeometry(); - if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) - QWindowSystemInterface::handleExposeEvent(window(), QRegion(QRect(QPoint(), geometry().size()))); -} - -void QAndroidPlatformVulkanWindow::surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) +void QAndroidPlatformVulkanWindow::destroyAndClearSurface() { - Q_UNUSED(jniEnv); - Q_UNUSED(w); - Q_UNUSED(h); - lockSurface(); - m_androidSurfaceObject = surface; - if (surface) - m_surfaceWaitCondition.wakeOne(); + destroySurface(); + clearSurface(); unlockSurface(); - - if (surface) - sendExpose(); } VkSurfaceKHR *QAndroidPlatformVulkanWindow::vkSurface() @@ -124,16 +95,15 @@ VkSurfaceKHR *QAndroidPlatformVulkanWindow::vkSurface() clearSurface(); QMutexLocker lock(&m_surfaceMutex); - if (m_nativeSurfaceId == -1) { + if (!m_surfaceCreated) { AndroidDeadlockProtector protector; if (!protector.acquire()) return &m_vkSurface; - const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); - m_nativeSurfaceId = QtAndroid::createSurface(this, geometry(), windowStaysOnTop, 32); + createSurface(); m_surfaceWaitCondition.wait(&m_surfaceMutex); } - if (m_nativeSurfaceId == -1 || !m_androidSurfaceObject.isValid()) + if (!m_surfaceCreated || !m_androidSurfaceObject.isValid()) return &m_vkSurface; QJniEnvironment env; diff --git a/src/plugins/platforms/android/qandroidplatformvulkanwindow.h b/src/plugins/platforms/android/qandroidplatformvulkanwindow.h index 14d5b7fc0e..fa959239d1 100644 --- a/src/plugins/platforms/android/qandroidplatformvulkanwindow.h +++ b/src/plugins/platforms/android/qandroidplatformvulkanwindow.h @@ -10,7 +10,6 @@ #define VK_USE_PLATFORM_ANDROID_KHR -#include "androidsurfaceclient.h" #include "qandroidplatformvulkaninstance.h" #include "qandroidplatformwindow.h" @@ -20,7 +19,7 @@ QT_BEGIN_NAMESPACE -class QAndroidPlatformVulkanWindow : public QAndroidPlatformWindow, public AndroidSurfaceClient +class QAndroidPlatformVulkanWindow : public QAndroidPlatformWindow { public: explicit QAndroidPlatformVulkanWindow(QWindow *window); @@ -32,17 +31,11 @@ public: VkSurfaceKHR *vkSurface(); -protected: - void surfaceChanged(JNIEnv *jniEnv, jobject surface, int w, int h) override; - private: - void sendExpose(); void clearSurface(); + void destroyAndClearSurface(); - int m_nativeSurfaceId; ANativeWindow *m_nativeWindow; - QJniObject m_androidSurfaceObject; - QWaitCondition m_surfaceWaitCondition; QSurfaceFormat m_format; QRect m_oldGeometry; VkSurfaceKHR m_vkSurface; diff --git a/src/plugins/platforms/android/qandroidplatformwindow.cpp b/src/plugins/platforms/android/qandroidplatformwindow.cpp index 76c034db50..2482160573 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.cpp +++ b/src/plugins/platforms/android/qandroidplatformwindow.cpp @@ -14,40 +14,102 @@ QT_BEGIN_NAMESPACE -static QBasicAtomicInt winIdGenerator = Q_BASIC_ATOMIC_INITIALIZER(0); +Q_LOGGING_CATEGORY(lcQpaWindow, "qt.qpa.window") + +Q_DECLARE_JNI_CLASS(QtInputInterface, "org/qtproject/qt/android/QtInputInterface") +Q_DECLARE_JNI_CLASS(QtInputConnectionListener, + "org/qtproject/qt/android/QtInputConnection$QtInputConnectionListener") QAndroidPlatformWindow::QAndroidPlatformWindow(QWindow *window) - : QPlatformWindow(window) + : QPlatformWindow(window), m_nativeQtWindow(nullptr), + m_surfaceContainerType(SurfaceContainer::TextureView), m_nativeParentQtWindow(nullptr), + m_androidSurfaceObject(nullptr) { m_windowFlags = Qt::Widget; m_windowState = Qt::WindowNoState; - m_windowId = winIdGenerator.fetchAndAddRelaxed(1) + 1; + // the surfaceType is overwritten in QAndroidPlatformOpenGLWindow ctor so let's save + // the fact that it's a raster window for now + m_isRaster = window->surfaceType() == QSurface::RasterSurface; setWindowState(window->windowStates()); // the following is in relation to the virtual geometry const bool forceMaximize = m_windowState & (Qt::WindowMaximized | Qt::WindowFullScreen); - const QRect requestedNativeGeometry = - forceMaximize ? QRect() : QHighDpi::toNativePixels(window->geometry(), window); - const QRect availableDeviceIndependentGeometry = (window->parent()) - ? window->parent()->geometry() - : QHighDpi::fromNativePixels(platformScreen()->availableGeometry(), window); + const QRect nativeScreenGeometry = platformScreen()->availableGeometry(); + if (forceMaximize) { + setGeometry(nativeScreenGeometry); + } else { + const QRect requestedNativeGeometry = QHighDpi::toNativePixels(window->geometry(), window); + const QRect availableDeviceIndependentGeometry = (window->parent()) + ? window->parent()->geometry() + : QHighDpi::fromNativePixels(nativeScreenGeometry, window); + // initialGeometry returns in native pixels + const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( + window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), + availableDeviceIndependentGeometry.height()); + if (requestedNativeGeometry != finalNativeGeometry) + setGeometry(finalNativeGeometry); + } + + if (isEmbeddingContainer()) + return; - // initialGeometry returns in native pixels - const QRect finalNativeGeometry = QPlatformWindow::initialGeometry( - window, requestedNativeGeometry, availableDeviceIndependentGeometry.width(), - availableDeviceIndependentGeometry.height()); + if (parent()) { + QAndroidPlatformWindow *androidParent = static_cast<QAndroidPlatformWindow*>(parent()); + if (!androidParent->isEmbeddingContainer()) + m_nativeParentQtWindow = androidParent->nativeWindow(); + } + + AndroidBackendRegister *reg = QtAndroid::backendRegister(); + QtJniTypes::QtInputConnectionListener listener = + reg->callInterface<QtJniTypes::QtInputInterface, QtJniTypes::QtInputConnectionListener>( + "getInputConnectionListener"); + + m_nativeQtWindow = QJniObject::construct<QtJniTypes::QtWindow>( + QNativeInterface::QAndroidApplication::context(), m_nativeParentQtWindow, listener); + m_nativeViewId = m_nativeQtWindow.callMethod<jint>("getId"); + + if (window->isTopLevel()) + platformScreen()->addWindow(this); - if (requestedNativeGeometry != finalNativeGeometry) - setGeometry(finalNativeGeometry); + static bool ok = false; + static const int value = qEnvironmentVariableIntValue("QT_ANDROID_SURFACE_CONTAINER_TYPE", &ok); + if (ok) { + static const SurfaceContainer type = static_cast<SurfaceContainer>(value); + if (type == SurfaceContainer::SurfaceView || type == SurfaceContainer::TextureView) + m_surfaceContainerType = type; + } else if (platformScreen()->windows().size() <= 1) { + // TODO should handle case where this changes at runtime -> need to change existing window + // into TextureView (or perhaps not, if the parent window would be SurfaceView, as long as + // onTop was false it would stay below the children) + m_surfaceContainerType = SurfaceContainer::SurfaceView; + } + qCDebug(lcQpaWindow) << "Window" << m_nativeViewId << "using surface container type" + << static_cast<int>(m_surfaceContainerType); +} + +QAndroidPlatformWindow::~QAndroidPlatformWindow() +{ + if (window()->isTopLevel()) + platformScreen()->removeWindow(this); } + void QAndroidPlatformWindow::lower() { + if (m_nativeParentQtWindow.isValid()) { + m_nativeParentQtWindow.callMethod<void>("bringChildToBack", nativeViewId()); + return; + } platformScreen()->lower(this); } void QAndroidPlatformWindow::raise() { + if (m_nativeParentQtWindow.isValid()) { + m_nativeParentQtWindow.callMethod<void>("bringChildToFront", nativeViewId()); + QWindowSystemInterface::handleFocusWindowChanged(window(), Qt::ActiveWindowFocusReason); + return; + } updateSystemUiVisibility(); platformScreen()->raise(this); } @@ -71,23 +133,25 @@ void QAndroidPlatformWindow::setGeometry(const QRect &rect) void QAndroidPlatformWindow::setVisible(bool visible) { - if (visible) - updateSystemUiVisibility(); + if (isEmbeddingContainer()) + return; + m_nativeQtWindow.callMethod<void>("setVisible", visible); if (visible) { - if ((m_windowState & Qt::WindowFullScreen) - || ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) { - setGeometry(platformScreen()->geometry()); - } else if (m_windowState & Qt::WindowMaximized) { - setGeometry(platformScreen()->availableGeometry()); + if (window()->isTopLevel()) { + updateSystemUiVisibility(); + if ((m_windowState & Qt::WindowFullScreen) + || ((m_windowState & Qt::WindowMaximized) && (window()->flags() & Qt::MaximizeUsingFullscreenGeometryHint))) { + setGeometry(platformScreen()->geometry()); + } else if (m_windowState & Qt::WindowMaximized) { + setGeometry(platformScreen()->availableGeometry()); + } + requestActivateWindow(); } + } else if (window()->isTopLevel() && window() == qGuiApp->focusWindow()) { + platformScreen()->topVisibleWindowChanged(); } - if (visible) - platformScreen()->addWindow(this); - else - platformScreen()->removeWindow(this); - QRect availableGeometry = screen()->availableGeometry(); if (geometry().width() > 0 && geometry().height() > 0 && availableGeometry.width() > 0 && availableGeometry.height() > 0) QPlatformWindow::setVisible(visible); @@ -120,7 +184,30 @@ Qt::WindowFlags QAndroidPlatformWindow::windowFlags() const void QAndroidPlatformWindow::setParent(const QPlatformWindow *window) { - Q_UNUSED(window); + using namespace QtJniTypes; + + if (window) { + auto androidWindow = static_cast<const QAndroidPlatformWindow*>(window); + if (androidWindow->isEmbeddingContainer()) + return; + // If we were a top level window, remove from screen + if (!m_nativeParentQtWindow.isValid()) + platformScreen()->removeWindow(this); + + const QtWindow parentWindow = androidWindow->nativeWindow(); + // If this was a child window of another window, the java method takes care of that + m_nativeQtWindow.callMethod<void, QtWindow>("setParent", parentWindow.object()); + m_nativeParentQtWindow = parentWindow; + } else if (QtAndroid::isQtApplication()) { + m_nativeQtWindow.callMethod<void, QtWindow>("setParent", nullptr); + m_nativeParentQtWindow = QJniObject(); + platformScreen()->addWindow(this); + } +} + +WId QAndroidPlatformWindow::winId() const +{ + return m_nativeQtWindow.isValid() ? reinterpret_cast<WId>(m_nativeQtWindow.object()) : 0L; } QAndroidPlatformScreen *QAndroidPlatformWindow::platformScreen() const @@ -135,7 +222,9 @@ void QAndroidPlatformWindow::propagateSizeHints() void QAndroidPlatformWindow::requestActivateWindow() { - platformScreen()->topWindowChanged(window()); + // raise() will handle differences between top level and child windows, and requesting focus + if (!blockedByModal()) + raise(); } void QAndroidPlatformWindow::updateSystemUiVisibility() @@ -169,4 +258,134 @@ void QAndroidPlatformWindow::applicationStateChanged(Qt::ApplicationState) QWindowSystemInterface::flushWindowSystemEvents(); } +void QAndroidPlatformWindow::createSurface() +{ + const QRect rect = geometry(); + jint x = 0, y = 0, w = -1, h = -1; + if (!rect.isNull()) { + x = rect.x(); + y = rect.y(); + w = std::max(rect.width(), 1); + h = std::max(rect.height(), 1); + } + + const bool windowStaysOnTop = bool(window()->flags() & Qt::WindowStaysOnTopHint); + const bool isOpaque = !format().hasAlpha() && qFuzzyCompare(window()->opacity(), 1.0); + + m_nativeQtWindow.callMethod<void>("createSurface", windowStaysOnTop, x, y, w, h, 32, isOpaque, + m_surfaceContainerType); + m_surfaceCreated = true; +} + +void QAndroidPlatformWindow::destroySurface() +{ + if (m_surfaceCreated) { + m_nativeQtWindow.callMethod<void>("destroySurface"); + m_surfaceCreated = false; + } +} + +void QAndroidPlatformWindow::setNativeGeometry(const QRect &geometry) +{ + if (!m_surfaceCreated) + return; + + jint x = 0; + jint y = 0; + jint w = -1; + jint h = -1; + if (!geometry.isNull()) { + x = geometry.x(); + y = geometry.y(); + w = geometry.width(); + h = geometry.height(); + } + m_nativeQtWindow.callMethod<void>("setGeometry", x, y, w, h); +} + +void QAndroidPlatformWindow::onSurfaceChanged(QtJniTypes::Surface surface) +{ + lockSurface(); + m_androidSurfaceObject = surface; + if (m_androidSurfaceObject.isValid()) // wait until we have a valid surface to draw into + m_surfaceWaitCondition.wakeOne(); + unlockSurface(); + + if (m_androidSurfaceObject.isValid()) { + // repaint the window, when we have a valid surface + sendExpose(); + } +} + +void QAndroidPlatformWindow::sendExpose() const +{ + QRect availableGeometry = screen()->availableGeometry(); + if (!geometry().isNull() && !availableGeometry.isNull()) { + QWindowSystemInterface::handleExposeEvent(window(), + QRegion(QRect(QPoint(), geometry().size()))); + } +} + +bool QAndroidPlatformWindow::blockedByModal() const +{ + QWindow *modalWindow = QGuiApplication::modalWindow(); + return modalWindow && modalWindow != window(); +} + +bool QAndroidPlatformWindow::isEmbeddingContainer() const +{ + // Returns true if the window is a wrapper for a foreign window solely to allow embedding Qt + // into a native Android app, in which case we should not try to control it more than we "need" to + return !QtAndroid::isQtApplication() && window()->isTopLevel(); +} + +void QAndroidPlatformWindow::setSurface(JNIEnv *env, jobject object, jint windowId, + QtJniTypes::Surface surface) +{ + Q_UNUSED(env) + Q_UNUSED(object) + + if (!qGuiApp) + return; + + const QList<QWindow*> windows = qGuiApp->allWindows(); + for (QWindow * window : windows) { + if (!window->handle()) + continue; + QAndroidPlatformWindow *platformWindow = + static_cast<QAndroidPlatformWindow *>(window->handle()); + if (platformWindow->nativeViewId() == windowId) + platformWindow->onSurfaceChanged(surface); + } +} + +void QAndroidPlatformWindow::windowFocusChanged(JNIEnv *env, jobject object, + jboolean focus, jint windowId) +{ + Q_UNUSED(env) + Q_UNUSED(object) + QWindow* window = QtAndroid::windowFromId(windowId); + Q_ASSERT_X(window, "QAndroidPlatformWindow", "windowFocusChanged event window should exist"); + if (focus) { + QWindowSystemInterface::handleFocusWindowChanged(window); + } else if (!focus && window == qGuiApp->focusWindow()) { + // Clear focus if current window has lost focus + QWindowSystemInterface::handleFocusWindowChanged(nullptr); + } +} + +bool QAndroidPlatformWindow::registerNatives(QJniEnvironment &env) +{ + if (!env.registerNativeMethods(QtJniTypes::Traits<QtJniTypes::QtWindow>::className(), + { + Q_JNI_NATIVE_SCOPED_METHOD(setSurface, QAndroidPlatformWindow), + Q_JNI_NATIVE_SCOPED_METHOD(windowFocusChanged, QAndroidPlatformWindow) + })) { + qCCritical(lcQpaWindow) << "RegisterNatives failed for" + << QtJniTypes::Traits<QtJniTypes::QtWindow>::className(); + return false; + } + return true; +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformwindow.h b/src/plugins/platforms/android/qandroidplatformwindow.h index 6fccc2e7fe..3f1e8ac992 100644 --- a/src/plugins/platforms/android/qandroidplatformwindow.h +++ b/src/plugins/platforms/android/qandroidplatformwindow.h @@ -7,17 +7,32 @@ #include <qobject.h> #include <qrect.h> #include <qpa/qplatformwindow.h> +#include <QtCore/qjnienvironment.h> +#include <QtCore/qjniobject.h> +#include <QtCore/qjnitypes.h> +#include <QtCore/qloggingcategory.h> +#include <QtCore/qmutex.h> +#include <QtCore/qwaitcondition.h> +#include <jni.h> QT_BEGIN_NAMESPACE +Q_DECLARE_LOGGING_CATEGORY(lcQpaWindow) +Q_DECLARE_JNI_CLASS(QtWindow, "org/qtproject/qt/android/QtWindow") +Q_DECLARE_JNI_CLASS(Surface, "android/view/Surface") + class QAndroidPlatformScreen; -class QAndroidPlatformBackingStore; class QAndroidPlatformWindow: public QPlatformWindow { public: - explicit QAndroidPlatformWindow(QWindow *window); + enum class SurfaceContainer { + SurfaceView, + TextureView + }; + explicit QAndroidPlatformWindow(QWindow *window); + ~QAndroidPlatformWindow(); void lower() override; void raise() override; @@ -27,7 +42,8 @@ public: void setWindowFlags(Qt::WindowFlags flags) override; Qt::WindowFlags windowFlags() const; void setParent(const QPlatformWindow *window) override; - WId winId() const override { return m_windowId; } + + WId winId() const override; bool setMouseGrabEnabled(bool grab) override { Q_UNUSED(grab); return false; } bool setKeyboardGrabEnabled(bool grab) override { Q_UNUSED(grab); return false; } @@ -39,32 +55,46 @@ public: void propagateSizeHints() override; void requestActivateWindow() override; void updateSystemUiVisibility(); - inline bool isRaster() const { - if (isForeignWindow()) - return false; - - return window()->surfaceType() == QSurface::RasterSurface - || window()->surfaceType() == QSurface::RasterGLSurface; - } + inline bool isRaster() const { return m_isRaster; } bool isExposed() const override; + QtJniTypes::QtWindow nativeWindow() const { return m_nativeQtWindow; } virtual void applicationStateChanged(Qt::ApplicationState); + int nativeViewId() const { return m_nativeViewId; } - void setBackingStore(QAndroidPlatformBackingStore *store) { m_backingStore = store; } - QAndroidPlatformBackingStore *backingStore() const { return m_backingStore; } - - virtual void repaint(const QRegion &) { } + static bool registerNatives(QJniEnvironment &env); + void onSurfaceChanged(QtJniTypes::Surface surface); protected: void setGeometry(const QRect &rect) override; + void lockSurface() { m_surfaceMutex.lock(); } + void unlockSurface() { m_surfaceMutex.unlock(); } + void createSurface(); + void destroySurface(); + void setNativeGeometry(const QRect &geometry); + void sendExpose() const; + bool blockedByModal() const; + bool isEmbeddingContainer() const; -protected: Qt::WindowFlags m_windowFlags; Qt::WindowStates m_windowState; - - WId m_windowId; - - QAndroidPlatformBackingStore *m_backingStore = nullptr; + bool m_isRaster; + + int m_nativeViewId = -1; + QtJniTypes::QtWindow m_nativeQtWindow; + SurfaceContainer m_surfaceContainerType = SurfaceContainer::SurfaceView; + QtJniTypes::QtWindow m_nativeParentQtWindow; + // The Android Surface, accessed from multiple threads, guarded by m_surfaceMutex + QtJniTypes::Surface m_androidSurfaceObject; + QWaitCondition m_surfaceWaitCondition; + bool m_surfaceCreated = false; + QMutex m_surfaceMutex; + +private: + static void setSurface(JNIEnv *env, jobject obj, jint windowId, QtJniTypes::Surface surface); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(setSurface) + static void windowFocusChanged(JNIEnv *env, jobject object, jboolean focus, jint windowId); + Q_DECLARE_JNI_NATIVE_METHOD_IN_CURRENT_SCOPE(windowFocusChanged) }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidsystemlocale.cpp b/src/plugins/platforms/android/qandroidsystemlocale.cpp index 858934b1f8..c5f2e59097 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.cpp +++ b/src/plugins/platforms/android/qandroidsystemlocale.cpp @@ -12,34 +12,38 @@ QT_BEGIN_NAMESPACE +Q_DECLARE_JNI_CLASS(Locale, "java/util/Locale") +Q_DECLARE_JNI_CLASS(Resources, "android/content/res/Resources") +Q_DECLARE_JNI_CLASS(Configuration, "android/content/res/Configuration") +Q_DECLARE_JNI_CLASS(LocaleList, "android/os/LocaleList") + +using namespace QtJniTypes; + QAndroidSystemLocale::QAndroidSystemLocale() : m_locale(QLocale::C) { } void QAndroidSystemLocale::getLocaleFromJava() const { - QWriteLocker locker(&m_lock); - - QJniObject javaLocaleObject; - QJniObject javaActivity(QtAndroid::activity()); - if (!javaActivity.isValid()) - javaActivity = QtAndroid::service(); - if (javaActivity.isValid()) { - QJniObject resources = javaActivity.callObjectMethod("getResources", "()Landroid/content/res/Resources;"); - QJniObject configuration = resources.callObjectMethod("getConfiguration", "()Landroid/content/res/Configuration;"); - - javaLocaleObject = configuration.getObjectField("locale", "Ljava/util/Locale;"); - } else { - javaLocaleObject = QJniObject::callStaticObjectMethod("java/util/Locale", "getDefault", "()Ljava/util/Locale;"); - } + const Locale javaLocaleObject = []{ + const QJniObject javaContext = QtAndroidPrivate::context(); + if (javaContext.isValid()) { + const QJniObject resources = javaContext.callMethod<Resources>("getResources"); + const QJniObject configuration = resources.callMethod<Configuration>("getConfiguration"); + return configuration.getField<Locale>("locale"); + } else { + return Locale::callStaticMethod<Locale>("getDefault"); + } + }(); - QString languageCode = javaLocaleObject.callObjectMethod("getLanguage", "()Ljava/lang/String;").toString(); - QString countryCode = javaLocaleObject.callObjectMethod("getCountry", "()Ljava/lang/String;").toString(); + const QString languageCode = javaLocaleObject.callMethod<QString>("getLanguage"); + const QString countryCode = javaLocaleObject.callMethod<QString>("getCountry"); + QWriteLocker locker(&m_lock); m_locale = QLocale(languageCode + u'_' + countryCode); } -QVariant QAndroidSystemLocale::query(QueryType type, QVariant in) const +QVariant QAndroidSystemLocale::query(QueryType type, QVariant &&in) const { if (type == LocaleChanged) { getLocaleFromJava(); @@ -142,12 +146,9 @@ QVariant QAndroidSystemLocale::query(QueryType type, QVariant in) const Q_ASSERT_X(false, Q_FUNC_INFO, "This can't happen."); case UILanguages: { if (QtAndroidPrivate::androidSdkVersion() >= 24) { - QJniObject localeListObject = - QJniObject::callStaticObjectMethod("android/os/LocaleList", "getDefault", - "()Landroid/os/LocaleList;"); + LocaleList localeListObject = LocaleList::callStaticMethod<LocaleList>("getDefault"); if (localeListObject.isValid()) { - QString lang = localeListObject.callObjectMethod("toLanguageTags", - "()Ljava/lang/String;").toString(); + QString lang = localeListObject.callMethod<QString>("toLanguageTags"); // Some devices return with it enclosed in []'s so check if both exists before // removing to ensure it is formatted correctly if (lang.startsWith(QChar('[')) && lang.endsWith(QChar(']'))) diff --git a/src/plugins/platforms/android/qandroidsystemlocale.h b/src/plugins/platforms/android/qandroidsystemlocale.h index 48e1d94a56..cd37b48270 100644 --- a/src/plugins/platforms/android/qandroidsystemlocale.h +++ b/src/plugins/platforms/android/qandroidsystemlocale.h @@ -11,10 +11,11 @@ QT_BEGIN_NAMESPACE class QAndroidSystemLocale : public QSystemLocale { + Q_DISABLE_COPY_MOVE(QAndroidSystemLocale) public: QAndroidSystemLocale(); - QVariant query(QueryType type, QVariant in) const override; + QVariant query(QueryType type, QVariant &&in) const override; QLocale fallbackLocale() const override; private: |