diff options
Diffstat (limited to 'src/plugins/platforms/android')
23 files changed, 1578 insertions, 329 deletions
diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 6c95a9b126..a03cc86b74 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -1,6 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2019 Volker Krause <vkrause@kde.org> +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. @@ -39,15 +40,51 @@ #include "androidcontentfileengine.h" -#include <private/qjni_p.h> #include <private/qjnihelpers_p.h> -#include <QDebug> +#include <QtCore/qcoreapplication.h> +#include <QtCore/qurl.h> +#include <QtCore/qdatetime.h> +#include <QtCore/qmimedatabase.h> +#include <QtCore/qdebug.h> -AndroidContentFileEngine::AndroidContentFileEngine(const QString &f) - : m_file(f) +class JniExceptionCleaner { - setFileName(f); +public: + JniExceptionCleaner() { clearException(); }; + ~JniExceptionCleaner() { clearException(); } + + bool clean() { return clearException(); }; +private: + bool clearException() + { + QJNIEnvironmentPrivate env; + if (Q_UNLIKELY(env->ExceptionCheck())) { + env->ExceptionDescribe(); + env->ExceptionClear(); + return true; + } + return false; + } +}; + +static QJNIObjectPrivate &contentResolverInstance() +{ + static QJNIObjectPrivate contentResolver; + if (!contentResolver.isValid()) { + JniExceptionCleaner cleaner; + contentResolver = QJNIObjectPrivate(QtAndroidPrivate::context()) + .callObjectMethod("getContentResolver", "()Landroid/content/ContentResolver;"); + } + + return contentResolver; +} + +AndroidContentFileEngine::AndroidContentFileEngine(const QString &filename) + : m_initialFile(filename), + m_documentFile(DocumentFile::parseFromAnyUri(filename)) +{ + setFileName(filename); } bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) @@ -58,6 +95,27 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) } if (openMode & QFileDevice::WriteOnly) { openModeStr += QLatin1Char('w'); + if (!m_documentFile->exists()) { + if (QUrl(m_initialFile).path().startsWith(QLatin1String("/tree/"))) { + const int lastSeparatorIndex = m_initialFile.lastIndexOf(QLatin1Char('/')); + 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 = QLatin1String("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 += QLatin1Char('t'); @@ -65,53 +123,149 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode) openModeStr += QLatin1Char('a'); } - const auto fd = QJNIObjectPrivate::callStaticMethod<jint>("org/qtproject/qt5/android/QtNative", - "openFdForContentUrl", - "(Landroid/content/Context;Ljava/lang/String;Ljava/lang/String;)I", - QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object(), - QJNIObjectPrivate::fromString(openModeStr).object()); + JniExceptionCleaner cleaner; + m_pfd = contentResolverInstance().callObjectMethod("openFileDescriptor", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/os/ParcelFileDescriptor;", + m_documentFile->uri().object(), + QJNIObjectPrivate::fromString(openModeStr).object()); + + if (!m_pfd.isValid()) + return false; + + const auto fd = m_pfd.callMethod<jint>("getFd", "()I"); if (fd < 0) { + closeNativeFileDescriptor(); return false; } - return QFSFileEngine::open(openMode, fd, QFile::AutoCloseHandle); + return QFSFileEngine::open(openMode, fd, QFile::DontCloseHandle); +} + +bool AndroidContentFileEngine::close() +{ + closeNativeFileDescriptor(); + return QFSFileEngine::close(); +} + +void AndroidContentFileEngine::closeNativeFileDescriptor() +{ + if (m_pfd.isValid()) { + JniExceptionCleaner cleaner; + m_pfd.callMethod<void>("close", "()V"); + m_pfd = QJNIObjectPrivate(); + } } qint64 AndroidContentFileEngine::size() const { - const jlong size = QJNIObjectPrivate::callStaticMethod<jlong>( - "org/qtproject/qt5/android/QtNative", "getSize", - "(Landroid/content/Context;Ljava/lang/String;)J", QtAndroidPrivate::context(), - QJNIObjectPrivate::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) const +{ + QString tmp = dirName; + tmp.remove(m_initialFile); + + QStringList dirParts = tmp.split(QLatin1Char('/')); + 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(FileTime time) const +{ + switch (time) { + case FileTime::ModificationTime: + 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 = QJNIObjectPrivate::callStaticMethod<jboolean>( - "org/qtproject/qt5/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), - QJNIObjectPrivate::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 : QJNIObjectPrivate::callStaticMethod<jboolean>( - "org/qtproject/qt5/android/QtNative", "checkFileExists", - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), - QJNIObjectPrivate::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 = QJNIObjectPrivate::callStaticMethod<jboolean>( - "org/qtproject/qt5/android/QtNative", "checkIfWritable", - "(Landroid/content/Context;Ljava/lang/String;)Z", QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(fileName(DefaultName)).object()); - if (writable) + flags |= FileType; + if (m_documentFile->canWrite()) flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm; } return type & flags; @@ -126,18 +280,18 @@ QString AndroidContentFileEngine::fileName(FileName f) const case DefaultName: case AbsoluteName: case CanonicalName: - return m_file; + return m_documentFile->uri().toString(); case BaseName: - { - const int pos = m_file.lastIndexOf(QChar(QLatin1Char('/'))); - return m_file.mid(pos); - } + return m_documentFile->name(); default: - return QString(); + break; } + + return QString(); } -QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, const QStringList &filterNames) +QAbstractFileEngine::Iterator *AndroidContentFileEngine::beginEntryList(QDir::Filters filters, + const QStringList &filterNames) { return new AndroidContentFileEngineIterator(filters, filterNames); } @@ -179,37 +333,557 @@ QString AndroidContentFileEngineIterator::next() bool AndroidContentFileEngineIterator::hasNext() const { - if (m_index == -1) { - if (path().isEmpty()) + if (m_index == -1 && m_files.isEmpty()) { + const auto currentPath = path(); + if (currentPath.isEmpty()) return false; - const bool isDir = QJNIObjectPrivate::callStaticMethod<jboolean>( - "org/qtproject/qt5/android/QtNative", "checkIfDir", - "(Landroid/content/Context;Ljava/lang/String;)Z", - QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(path()).object()); - if (isDir) { - QJNIObjectPrivate objArray = QJNIObjectPrivate::callStaticObjectMethod("org/qtproject/qt5/android/QtNative", - "listContentsFromTreeUri", - "(Landroid/content/Context;Ljava/lang/String;)[Ljava/lang/String;", - QtAndroidPrivate::context(), - QJNIObjectPrivate::fromString(path()).object()); - if (objArray.isValid()) { - QJNIEnvironmentPrivate env; - const jsize length = env->GetArrayLength(static_cast<jarray>(objArray.object())); - for (int i = 0; i != length; ++i) { - m_entries << QJNIObjectPrivate(env->GetObjectArrayElement( - static_cast<jobjectArray>(objArray.object()), i)).toString(); - } - } - } - m_index = 0; + + const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath); + if (iterDoc->isDirectory()) + for (const auto &doc : iterDoc->listFiles()) + m_files.append(doc); } - return m_index < m_entries.size(); + + return m_index < (m_files.size() - 1); } 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_entries.at(m_index - 1); + // Returns a full path since contstructing a content path from the file name + // and a tree URI only will not point to a valid file URI. + return m_files.at(m_index)->uri().toString(); +} + +QString AndroidContentFileEngineIterator::currentFilePath() const +{ + return currentFileName(); +} + +// Start of Cursor + +class Cursor +{ +public: + explicit Cursor(const QJNIObjectPrivate &object) + : m_object{object} { } + + ~Cursor() + { + if (m_object.isValid()) { + JniExceptionCleaner cleaner; + 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 + { + JniExceptionCleaner cleaner; + int type = m_object.callMethod<jint>("getType", "(I)I", columnIndex); + switch (type) { + case FIELD_TYPE_NULL: + return {}; + case FIELD_TYPE_INTEGER: + return QVariant::fromValue(m_object.callMethod<jlong>("getLong", "(I)J", columnIndex)); + case FIELD_TYPE_FLOAT: + return QVariant::fromValue(m_object.callMethod<jdouble>("getDouble", "(I)D", + columnIndex)); + case FIELD_TYPE_STRING: + return QVariant::fromValue(m_object.callObjectMethod("getString", + "(I)Ljava/lang/String;", + columnIndex).toString()); + case FIELD_TYPE_BLOB: { + auto blob = m_object.callObjectMethod("getBlob", "(I)[B", columnIndex); + QJNIEnvironmentPrivate env; + const auto blobArray = static_cast<jbyteArray>(blob.object()); + 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 QJNIObjectPrivate &uri, + const QStringList &projection = {}, + const QString &selection = {}, + const QStringList &selectionArgs = {}, + const QString &sortOrder = {}) + { + JniExceptionCleaner cleaner; + auto cursor = contentResolverInstance().callObjectMethod("query", + "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;", + uri.object(), + projection.isEmpty() ? nullptr : fromStringList(projection).object(), + selection.isEmpty() ? nullptr : QJNIObjectPrivate::fromString(selection).object(), + selectionArgs.isEmpty() ? nullptr : fromStringList(selectionArgs).object(), + sortOrder.isEmpty() ? nullptr : QJNIObjectPrivate::fromString(sortOrder).object()); + if (!cursor.isValid()) + return {}; + return std::make_unique<Cursor>(cursor); + } + + static QVariant queryColumn(const QJNIObjectPrivate &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", "(I)Z", 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: + static QJNIObjectPrivate fromStringList(const QStringList &list) + { + QJNIEnvironmentPrivate env; + JniExceptionCleaner cleaner; + auto array = env->NewObjectArray(list.size(), env->FindClass("java/lang/String"), nullptr); + for (int i = 0; i < list.size(); ++i) + env->SetObjectArrayElement(array, i, QJNIObjectPrivate::fromString(list[i]).object()); + return QJNIObjectPrivate::fromLocalRef(array); + } + + QJNIObjectPrivate m_object; +}; + +// End of Cursor + +// Start of 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 QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "getDocumentId", + "(Landroid/net/Uri;)Ljava/lang/String;", + uri.object()).toString(); +} + +QString treeDocumentId(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "getTreeDocumentId", + "(Landroid/net/Uri;)Ljava/lang/String;", + uri.object()).toString(); +} + +QJNIObjectPrivate buildChildDocumentsUriUsingTree(const QJNIObjectPrivate &uri, const QString &parentDocumentId) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "buildChildDocumentsUriUsingTree", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", + uri.object(), + QJNIObjectPrivate::fromString(parentDocumentId).object()); + +} + +QJNIObjectPrivate buildDocumentUriUsingTree(const QJNIObjectPrivate &treeUri, const QString &documentId) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "buildDocumentUriUsingTree", + "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", + treeUri.object(), + QJNIObjectPrivate::fromString(documentId).object()); +} + +bool isDocumentUri(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticMethod<jboolean>("android/provider/DocumentsContract", + "isDocumentUri", + "(Landroid/content/Context;Landroid/net/Uri;)Z", + QtAndroidPrivate::context(), + uri.object()); } + +bool isTreeUri(const QJNIObjectPrivate &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticMethod<jboolean>("android/provider/DocumentsContract", + "isTreeUri", + "(Landroid/net/Uri;)Z", + uri.object()); +} + +QJNIObjectPrivate createDocument(const QJNIObjectPrivate &parentDocumentUri, const QString &mimeType, + const QString &displayName) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "createDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;", + contentResolverInstance().object(), + parentDocumentUri.object(), + QJNIObjectPrivate::fromString(mimeType).object(), + QJNIObjectPrivate::fromString(displayName).object()); +} + +bool deleteDocument(const QJNIObjectPrivate &documentUri) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_DELETE)) + return {}; + + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticMethod<jboolean>("android/provider/DocumentsContract", + "deleteDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;)Z", + contentResolverInstance().object(), + documentUri.object()); +} + +QJNIObjectPrivate moveDocument(const QJNIObjectPrivate &sourceDocumentUri, + const QJNIObjectPrivate &sourceParentDocumentUri, + const QJNIObjectPrivate &targetParentDocumentUri) +{ + const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_MOVE)) + return {}; + + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "moveDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;Landroid/net/Uri;Landroid/net/Uri;)Landroid/net/Uri;", + contentResolverInstance().object(), + sourceDocumentUri.object(), + sourceParentDocumentUri.object(), + targetParentDocumentUri.object()); +} + +QJNIObjectPrivate renameDocument(const QJNIObjectPrivate &documentUri, const QString &displayName) +{ + const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt(); + if (!(flags & Document::FLAG_SUPPORTS_RENAME)) + return {}; + + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/provider/DocumentsContract", + "renameDocument", + "(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;", + contentResolverInstance().object(), + documentUri.object(), + QJNIObjectPrivate::fromString(displayName).object()); +} +} // End DocumentsContract namespace + +// Start of DocumentFile + +using namespace DocumentsContract; + +namespace { +class MakeableDocumentFile : public DocumentFile +{ +public: + MakeableDocumentFile(const QJNIObjectPrivate &uri, const DocumentFilePtr &parent = {}) + : DocumentFile(uri, parent) + {} +}; +} + +DocumentFile::DocumentFile(const QJNIObjectPrivate &uri, + const DocumentFilePtr &parent) + : m_uri{uri} + , m_parent{parent} +{} + +QJNIObjectPrivate parseUri(const QString &uri) +{ + JniExceptionCleaner cleaner; + return QJNIObjectPrivate::callStaticObjectMethod("android/net/Uri", + "parse", + "(Ljava/lang/String;)Landroid/net/Uri;", + QJNIObjectPrivate::fromString(uri).object()); +} + +DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName) +{ + const QJNIObjectPrivate uri = parseUri(fileName); + + if (DocumentsContract::isDocumentUri(uri)) + return fromSingleUri(uri); + + const QString documentType = QLatin1String("/document/"); + const QString treeType = QLatin1String("/tree/"); + + const int treeIndex = fileName.indexOf(treeType); + const int documentIndex = fileName.indexOf(documentType); + const int index = fileName.lastIndexOf(QLatin1Char('/')); + + if (index <= treeIndex + treeType.size() || index <= documentIndex + documentType.size()) + return fromTreeUri(uri); + + const QString parentUrl = fileName.left(index); + DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl)); + + const QString baseName = fileName.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 QJNIObjectPrivate &uri) +{ + return std::make_shared<MakeableDocumentFile>(uri); +} + +DocumentFilePtr DocumentFile::fromTreeUri(const QJNIObjectPrivate &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 QJNIObjectPrivate &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 +{ + JniExceptionCleaner cleaner; + const auto context = QJNIObjectPrivate(QtAndroidPrivate::context()); + const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission", + "(Landroid/net/Uri;I)I", + m_uri.object(), + FLAG_GRANT_READ_URI_PERMISSION); + if (selfUriPermission != 0) + return false; + + return !mimeType().isEmpty(); +} + +bool DocumentFile::canWrite() const +{ + JniExceptionCleaner cleaner; + const auto context = QJNIObjectPrivate(QtAndroidPrivate::context()); + const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission", + "(Landroid/net/Uri;I)I", + m_uri.object(), + 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) +{ + QJNIObjectPrivate uri; + if (newName.startsWith(QLatin1String("content://"))) { + auto lastSeparatorIndex = [](const QString &file) { + int posDecoded = file.lastIndexOf(QLatin1Char('/')); + int posEncoded = file.lastIndexOf(QUrl::toPercentEncoding(QLatin1String("/"))); + 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(QLatin1Char('/'))) + displayName.remove(0, 1); + else if (displayName.startsWith(QUrl::toPercentEncoding(QLatin1String("/")))) + displayName.remove(0, 3); + + uri = renameDocument(m_uri, displayName); + } else { + // Move + QJNIObjectPrivate srcParentUri = fromTreeUri(parseUri(parent))->uri(); + const QString destParent = newName.left(lastSeparatorIndex(newName)); + QJNIObjectPrivate 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; +} + +// End of DocumentFile diff --git a/src/plugins/platforms/android/androidcontentfileengine.h b/src/plugins/platforms/android/androidcontentfileengine.h index 31eaf9b0ab..a45f2753d6 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.h +++ b/src/plugins/platforms/android/androidcontentfileengine.h @@ -1,6 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2019 Volker Krause <vkrause@kde.org> +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. @@ -41,20 +42,37 @@ #define ANDROIDCONTENTFILEENGINE_H #include <private/qfsfileengine_p.h> +#include <private/qjni_p.h> +#include <QtCore/qlist.h> + + +using DocumentFilePtr = std::shared_ptr<class DocumentFile>; class AndroidContentFileEngine : public QFSFileEngine { public: AndroidContentFileEngine(const QString &fileName); bool open(QIODevice::OpenMode openMode) override; + bool close() override; qint64 size() const override; + bool remove() override; + bool rename(const QString &newName) override; + bool mkdir(const QString &dirName, bool createParentDirectories) const override; + bool rmdir(const QString &dirName, bool recurseParentDirectories) const override; + QByteArray id() const override; + bool caseSensitive() const override { return true; } + QDateTime fileTime(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; + private: - QString m_file; + void closeNativeFileDescriptor(); + QString m_initialFile; + QJNIObjectPrivate m_pfd; + DocumentFilePtr m_documentFile; }; class AndroidContentFileEngineHandler : public QAbstractFileEngineHandler @@ -73,9 +91,51 @@ public: QString next() override; bool hasNext() const 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 QJNIObjectPrivate &uri); + static DocumentFilePtr fromTreeUri(const QJNIObjectPrivate &treeUri); + + DocumentFilePtr createFile(const QString &mimeType, const QString &displayName); + DocumentFilePtr createDirectory(const QString &displayName); + const QJNIObjectPrivate &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 QJNIObjectPrivate &uri, const std::shared_ptr<DocumentFile> &parent); + +protected: + QJNIObjectPrivate m_uri; + DocumentFilePtr m_parent; }; #endif // ANDROIDCONTENTFILEENGINE_H diff --git a/src/plugins/platforms/android/androidjniaccessibility.cpp b/src/plugins/platforms/android/androidjniaccessibility.cpp index 989d0d18f4..adad9dde98 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.cpp +++ b/src/plugins/platforms/android/androidjniaccessibility.cpp @@ -37,6 +37,7 @@ ** ****************************************************************************/ +#include "androiddeadlockprotector.h" #include "androidjniaccessibility.h" #include "androidjnimain.h" #include "qandroidplatformintegration.h" @@ -50,6 +51,7 @@ #include <QtCore/private/qjnihelpers_p.h> #include <QtCore/private/qjni_p.h> #include <QtGui/private/qhighdpiscaling_p.h> +#include <QtCore/QObject> #include "qdebug.h" @@ -69,12 +71,49 @@ namespace QtAndroidAccessibility static jmethodID m_setEnabledMethodID = 0; static jmethodID m_setFocusableMethodID = 0; static jmethodID m_setFocusedMethodID = 0; + static jmethodID m_setHeadingMethodID = 0; static jmethodID m_setScrollableMethodID = 0; static jmethodID m_setTextSelectionMethodID = 0; static jmethodID m_setVisibleToUserMethodID = 0; static bool m_accessibilityActivated = false; + // This object is needed to schedule the execution of the code that + // deals with accessibility instances to the Qt main thread. + // 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; + + // This method is called from the Qt main thread, and normally a + // QGuiApplication instance will be used as a parent. + void createAccessibilityContextObject(QObject *parent) + { + if (m_accessibilityContext) + m_accessibilityContext->deleteLater(); + m_accessibilityContext = new QObject(parent); + } + + 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); + } else { + __android_log_print(ANDROID_LOG_WARN, m_qtTag, + "Could not run accessibility call in object context, event loop suspended."); + } + } + void initialize() { QJNIObjectPrivate::callStaticMethod<void>(QtAndroid::applicationClass(), @@ -110,14 +149,17 @@ namespace QtAndroidAccessibility return iface; } - void notifyLocationChange() + void notifyLocationChange(uint accessibilityObjectId) { - QtAndroid::notifyAccessibilityLocationChange(); + QtAndroid::notifyAccessibilityLocationChange(accessibilityObjectId); } + static int parentId_helper(int objectId); // forward declaration + void notifyObjectHide(uint accessibilityObjectId) { - QtAndroid::notifyObjectHide(accessibilityObjectId); + const auto parentObjectId = parentId_helper(accessibilityObjectId); + QtAndroid::notifyObjectHide(accessibilityObjectId, parentObjectId); } void notifyObjectFocus(uint accessibilityObjectId) @@ -125,7 +167,20 @@ namespace QtAndroidAccessibility QtAndroid::notifyObjectFocus(accessibilityObjectId); } - static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + static jstring jvalueForAccessibleObject(int objectId); // forward declaration + + void notifyValueChanged(uint accessibilityObjectId) + { + jstring value = jvalueForAccessibleObject(accessibilityObjectId); + QtAndroid::notifyValueChanged(accessibilityObjectId, value); + } + + void notifyScrolledEvent(uint accessiblityObjectId) + { + QtAndroid::notifyScrolledEvent(accessiblityObjectId); + } + + static QVarLengthArray<int, 8> childIdListForAccessibleObject_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); if (iface && iface->isValid()) { @@ -137,6 +192,18 @@ namespace QtAndroidAccessibility if (child && child->isValid()) ifaceIdArray.append(QAccessible::uniqueId(child)); } + return ifaceIdArray; + } + return {}; + } + + static jintArray childIdListForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + { + if (m_accessibilityContext) { + QVarLengthArray<jint, 8> ifaceIdArray; + runInObjectContext(m_accessibilityContext, [objectId]() { + return childIdListForAccessibleObject_helper(objectId); + }, &ifaceIdArray); jintArray jArray = env->NewIntArray(jsize(ifaceIdArray.count())); env->SetIntArrayRegion(jArray, 0, ifaceIdArray.count(), ifaceIdArray.data()); return jArray; @@ -145,7 +212,7 @@ namespace QtAndroidAccessibility return env->NewIntArray(jsize(0)); } - static jint parentId(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + static int parentId_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); if (iface && iface->isValid()) { @@ -159,7 +226,18 @@ namespace QtAndroidAccessibility return -1; } - static jobject screenRect(JNIEnv *env, jobject /*thiz*/, jint objectId) + static jint parentId(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + { + jint result = -1; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return parentId_helper(objectId); + }, &result); + } + return result; + } + + static QRect screenRect_helper(int objectId, bool clip = true) { QRect rect; QAccessibleInterface *iface = interfaceFromId(objectId); @@ -167,18 +245,28 @@ 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); } + return rect; + } + static jobject screenRect(JNIEnv *env, jobject /*thiz*/, jint objectId) + { + QRect rect; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return screenRect_helper(objectId); + }, &rect); + } jclass rectClass = env->FindClass("android/graphics/Rect"); jmethodID ctor = env->GetMethodID(rectClass, "<init>", "(IIII)V"); jobject jrect = env->NewObject(rectClass, ctor, rect.left(), rect.top(), rect.right(), rect.bottom()); return jrect; } - static jint hitTest(JNIEnv */*env*/, jobject /*thiz*/, jfloat x, jfloat y) + static int hitTest_helper(float x, float y) { QAccessibleInterface *root = interfaceFromId(-1); if (root && root->isValid()) { @@ -196,17 +284,29 @@ namespace QtAndroidAccessibility return -1; } + static jint hitTest(JNIEnv */*env*/, jobject /*thiz*/, jfloat x, jfloat y) + { + jint result = -1; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [x, y]() { + return hitTest_helper(x, y); + }, &result); + } + return result; + } + static void invokeActionOnInterfaceInMainThread(QAccessibleActionInterface* actionInterface, const QString& action) { + // Queue the action and return back to Java thread, so that we do not + // block it for too long QMetaObject::invokeMethod(qApp, [actionInterface, action]() { actionInterface->doAction(action); - }); + }, Qt::QueuedConnection); } - static jboolean clickAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + static bool clickAction_helper(int objectId) { -// qDebug() << "A11Y: CLICK: " << objectId; QAccessibleInterface *iface = interfaceFromId(objectId); if (!iface || !iface->isValid() || !iface->actionInterface()) return false; @@ -225,20 +325,65 @@ namespace QtAndroidAccessibility return true; } - static jboolean scrollForward(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + static jboolean clickAction(JNIEnv */*env*/, jobject /*thiz*/, jint objectId) + { + bool result = false; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return clickAction_helper(objectId); + }, &result); + } + return result; + } + + static bool scroll_helper(int objectId, const QString &actionName) { QAccessibleInterface *iface = interfaceFromId(objectId); if (iface && iface->isValid()) - return QAccessibleBridgeUtils::performEffectiveAction(iface, QAccessibleActionInterface::increaseAction()); + return QAccessibleBridgeUtils::performEffectiveAction(iface, actionName); return false; } + 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); + } + + // 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) { - QAccessibleInterface *iface = interfaceFromId(objectId); - if (iface && iface->isValid()) - return QAccessibleBridgeUtils::performEffectiveAction(iface, QAccessibleActionInterface::decreaseAction()); - return false; + 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); + } + + // Don't check for position change if the call was not successful + return result && oldPosition != screenRect_helper(firstChildId, false); } @@ -251,68 +396,182 @@ if (!clazz) { \ //__android_log_print(ANDROID_LOG_FATAL, m_qtTag, m_methodErrorMsg, METHOD_NAME, METHOD_SIGNATURE); + static QString textFromValue(QAccessibleInterface *iface) + { + QString valueStr; + QAccessibleValueInterface *valueIface = iface->valueInterface(); + if (valueIface) { + const QVariant valueVar = valueIface->currentValue(); + const auto type = static_cast<QMetaType::Type>(valueVar.type()); + if (type == QMetaType::Double || type == QMetaType::Float) { + // QVariant's toString() formats floating-point values with + // FloatingPointShortest, which is not an accessible + // representation; nor, in many cases, is it suitable to the UI + // element whose value we're looking at. So roll our own + // A11Y-friendly conversion to string. + const double val = valueVar.toDouble(); + // Try to use minimumStepSize() to determine precision + bool stepIsValid = false; + const double step = qAbs(valueIface->minimumStepSize().toDouble(&stepIsValid)); + if (!stepIsValid || qFuzzyIsNull(step)) { + // Ignore step, use default precision + valueStr = qFuzzyIsNull(val) ? QStringLiteral("0") : QString::number(val, 'f'); + } else { + const int precision = [](double s) { + int count = 0; + while (s < 1. && !qFuzzyCompare(s, 1.)) { + ++count; + s *= 10; + } + // If s is now 1.25, we want to show some more digits, + // but don't want to get silly with a step like 1./7; + // so only include a few extra digits. + const int stop = count + 3; + const auto fractional = [](double v) { + double whole = 0.0; + std::modf(v + 0.5, &whole); + return qAbs(v - whole); + }; + s = fractional(s); + while (count < stop && !qFuzzyIsNull(s)) { + ++count; + s = fractional(s * 10); + } + return count; + }(step); + valueStr = qFuzzyIsNull(val / step) ? QStringLiteral("0") + : QString::number(val, 'f', precision); + } + } else { + valueStr = valueVar.toString(); + } + } + return valueStr; + } + static jstring jvalueForAccessibleObject(int objectId) + { + QAccessibleInterface *iface = interfaceFromId(objectId); + const QString value = textFromValue(iface); + QJNIEnvironmentPrivate env; + jstring jstr = env->NewString((jchar*)value.constData(), (jsize)value.size()); +#ifdef QT_DEBUG + env->ExceptionDescribe(); +#endif // QT_DEBUG + env->ExceptionClear(); + return jstr; + } - static jstring descriptionForAccessibleObject_helper(JNIEnv *env, QAccessibleInterface *iface) + static QString descriptionForInterface(QAccessibleInterface *iface) { QString desc; if (iface && iface->isValid()) { + bool hasValue = false; desc = iface->text(QAccessible::Name); if (desc.isEmpty()) desc = iface->text(QAccessible::Description); if (desc.isEmpty()) { desc = iface->text(QAccessible::Value); - if (desc.isEmpty()) { - if (QAccessibleValueInterface *valueIface = iface->valueInterface()) { - desc= valueIface->currentValue().toString(); - } + hasValue = !desc.isEmpty(); + } + if (!hasValue && iface->valueInterface()) { + const QString valueStr = textFromValue(iface); + if (!valueStr.isEmpty()) { + if (!desc.isEmpty()) + desc.append(QChar(QChar::Space)); + desc.append(valueStr); } } } - return env->NewString((jchar*) desc.constData(), (jsize) desc.size()); + return desc; } - static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + static QString descriptionForAccessibleObject_helper(int objectId) { QAccessibleInterface *iface = interfaceFromId(objectId); - return descriptionForAccessibleObject_helper(env, iface); + return descriptionForInterface(iface); } - static bool populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node) + static jstring descriptionForAccessibleObject(JNIEnv *env, jobject /*thiz*/, jint objectId) + { + QString desc; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return descriptionForAccessibleObject_helper(objectId); + }, &desc); + } + return env->NewString((jchar*) desc.constData(), (jsize) desc.size()); + } + + + struct NodeInfo + { + bool valid = false; + QAccessible::State state; + QAccessible::Role role; + QStringList actions; + QString description; + bool hasTextSelection = false; + int selectionStart = 0; + int selectionEnd = 0; + }; + + static NodeInfo populateNode_helper(int objectId) { + NodeInfo info; QAccessibleInterface *iface = interfaceFromId(objectId); - if (!iface || !iface->isValid()) { + if (iface && iface->isValid()) { + info.valid = true; + info.state = iface->state(); + info.role = iface->role(); + info.actions = QAccessibleBridgeUtils::effectiveActionNames(iface); + info.description = descriptionForInterface(iface); + QAccessibleTextInterface *textIface = iface->textInterface(); + if (textIface && (textIface->selectionCount() > 0)) { + info.hasTextSelection = true; + textIface->selection(0, &info.selectionStart, &info.selectionEnd); + } + } + return info; + } + + static jboolean populateNode(JNIEnv *env, jobject /*thiz*/, jint objectId, jobject node) + { + NodeInfo info; + if (m_accessibilityContext) { + runInObjectContext(m_accessibilityContext, [objectId]() { + return populateNode_helper(objectId); + }, &info); + } + if (!info.valid) { __android_log_print(ANDROID_LOG_WARN, m_qtTag, "Accessibility: populateNode for Invalid ID"); return false; } - QAccessible::State state = iface->state(); - const QStringList actions = QAccessibleBridgeUtils::effectiveActionNames(iface); - const bool hasClickableAction = actions.contains(QAccessibleActionInterface::pressAction()) - || actions.contains(QAccessibleActionInterface::toggleAction()); - const bool hasIncreaseAction = actions.contains(QAccessibleActionInterface::increaseAction()); - const bool hasDecreaseAction = actions.contains(QAccessibleActionInterface::decreaseAction()); - // try to fill in the text property, this is what the screen reader reads - jstring jdesc = descriptionForAccessibleObject_helper(env, iface); - - if (QAccessibleTextInterface *textIface = iface->textInterface()) { - if (m_setTextSelectionMethodID && textIface->selectionCount() > 0) { - int startSelection; - int endSelection; - textIface->selection(0, &startSelection, &endSelection); - env->CallVoidMethod(node, m_setTextSelectionMethodID, startSelection, endSelection); - } + const bool hasClickableAction = + info.actions.contains(QAccessibleActionInterface::pressAction()) || + info.actions.contains(QAccessibleActionInterface::toggleAction()); + const bool hasIncreaseAction = + info.actions.contains(QAccessibleActionInterface::increaseAction()); + const bool hasDecreaseAction = + info.actions.contains(QAccessibleActionInterface::decreaseAction()); + + if (info.hasTextSelection && m_setTextSelectionMethodID) { + env->CallVoidMethod(node, m_setTextSelectionMethodID, info.selectionStart, + info.selectionEnd); } - env->CallVoidMethod(node, m_setCheckableMethodID, (bool)state.checkable); - env->CallVoidMethod(node, m_setCheckedMethodID, (bool)state.checked); - env->CallVoidMethod(node, m_setEditableMethodID, state.editable); - env->CallVoidMethod(node, m_setEnabledMethodID, !state.disabled); - env->CallVoidMethod(node, m_setFocusableMethodID, (bool)state.focusable); - env->CallVoidMethod(node, m_setFocusedMethodID, (bool)state.focused); - env->CallVoidMethod(node, m_setVisibleToUserMethodID, !state.invisible); + env->CallVoidMethod(node, m_setCheckableMethodID, (bool)info.state.checkable); + env->CallVoidMethod(node, m_setCheckedMethodID, (bool)info.state.checked); + env->CallVoidMethod(node, m_setEditableMethodID, info.state.editable); + env->CallVoidMethod(node, m_setEnabledMethodID, !info.state.disabled); + env->CallVoidMethod(node, m_setFocusableMethodID, (bool)info.state.focusable); + env->CallVoidMethod(node, m_setFocusedMethodID, (bool)info.state.focused); + if (m_setHeadingMethodID) + env->CallVoidMethod(node, m_setHeadingMethodID, info.role == QAccessible::Heading); + env->CallVoidMethod(node, m_setVisibleToUserMethodID, !info.state.invisible); env->CallVoidMethod(node, m_setScrollableMethodID, hasIncreaseAction || hasDecreaseAction); - env->CallVoidMethod(node, m_setClickableMethodID, hasClickableAction); + env->CallVoidMethod(node, m_setClickableMethodID, hasClickableAction || info.role == QAccessible::Link); // Add ACTION_CLICK if (hasClickableAction) @@ -326,7 +585,9 @@ if (!clazz) { \ if (hasDecreaseAction) env->CallVoidMethod(node, m_addActionMethodID, (int)0x00002000); // ACTION_SCROLL_BACKWARD defined in AccessibilityNodeInfo - + // try to fill in the text property, this is what the screen reader reads + jstring jdesc = env->NewString((jchar*)info.description.constData(), + (jsize)info.description.size()); //CALL_METHOD(node, "setText", "(Ljava/lang/CharSequence;)V", jdesc) env->CallVoidMethod(node, m_setContentDescriptionMethodID, jdesc); @@ -374,6 +635,9 @@ if (!clazz) { \ GET_AND_CHECK_STATIC_METHOD(m_setEnabledMethodID, nodeInfoClass, "setEnabled", "(Z)V"); GET_AND_CHECK_STATIC_METHOD(m_setFocusableMethodID, nodeInfoClass, "setFocusable", "(Z)V"); GET_AND_CHECK_STATIC_METHOD(m_setFocusedMethodID, nodeInfoClass, "setFocused", "(Z)V"); + if (QtAndroidPrivate::androidSdkVersion() >= 28) { + GET_AND_CHECK_STATIC_METHOD(m_setHeadingMethodID, nodeInfoClass, "setHeading", "(Z)V"); + } GET_AND_CHECK_STATIC_METHOD(m_setScrollableMethodID, nodeInfoClass, "setScrollable", "(Z)V"); GET_AND_CHECK_STATIC_METHOD(m_setVisibleToUserMethodID, nodeInfoClass, "setVisibleToUser", "(Z)V"); GET_AND_CHECK_STATIC_METHOD(m_setTextSelectionMethodID, nodeInfoClass, "setTextSelection", "(II)V"); diff --git a/src/plugins/platforms/android/androidjniaccessibility.h b/src/plugins/platforms/android/androidjniaccessibility.h index de9d32a099..212131ff62 100644 --- a/src/plugins/platforms/android/androidjniaccessibility.h +++ b/src/plugins/platforms/android/androidjniaccessibility.h @@ -44,14 +44,19 @@ QT_BEGIN_NAMESPACE +class QObject; + namespace QtAndroidAccessibility { void initialize(); bool isActive(); bool registerNatives(JNIEnv *env); - void notifyLocationChange(); + void notifyLocationChange(uint accessibilityObjectId); void notifyObjectHide(uint accessibilityObjectId); void notifyObjectFocus(uint accessibilityObjectId); + void notifyValueChanged(uint accessibilityObjectId); + void notifyScrolledEvent(uint accessibilityObjectId); + void createAccessibilityContextObject(QObject *parent); } QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/androidjniclipboard.cpp b/src/plugins/platforms/android/androidjniclipboard.cpp index c20ac456b1..763ecc0f62 100644 --- a/src/plugins/platforms/android/androidjniclipboard.cpp +++ b/src/plugins/platforms/android/androidjniclipboard.cpp @@ -70,26 +70,24 @@ namespace QtAndroidClipboard void setClipboardMimeData(QMimeData *data) { clearClipboardData(); - if (data->hasText()) { + if (data->hasUrls()) { + QList<QUrl> urls = data->urls(); + for (const auto &u : qAsConst(urls)) { + QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "setClipboardUri", + "(Ljava/lang/String;)V", + QJNIObjectPrivate::fromString(u.toEncoded()).object()); + } + } else if (data->hasText()) { // hasText || hasUrls, so the order matter here. QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "setClipboardText", "(Ljava/lang/String;)V", QJNIObjectPrivate::fromString(data->text()).object()); - } - if (data->hasHtml()) { + } else if (data->hasHtml()) { QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "setClipboardHtml", "(Ljava/lang/String;Ljava/lang/String;)V", QJNIObjectPrivate::fromString(data->text()).object(), QJNIObjectPrivate::fromString(data->html()).object()); } - if (data->hasUrls()) { - QList<QUrl> urls = data->urls(); - for (const auto &u : qAsConst(urls)) { - QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "setClipboardUri", - "(Ljava/lang/String;)V", - QJNIObjectPrivate::fromString(u.toEncoded()).object()); - } - } } QMimeData *getClipboardMimeData() diff --git a/src/plugins/platforms/android/androidjniinput.cpp b/src/plugins/platforms/android/androidjniinput.cpp index fe1aff0cc4..bb77752734 100644 --- a/src/plugins/platforms/android/androidjniinput.cpp +++ b/src/plugins/platforms/android/androidjniinput.cpp @@ -59,7 +59,6 @@ using namespace QtAndroid; namespace QtAndroidInput { static bool m_ignoreMouseEvents = false; - static bool m_softwareKeyboardVisible = false; static QRect m_softwareKeyboardRect; static QList<QWindowSystemInterface::TouchPoint> m_touchPoints; @@ -80,16 +79,15 @@ namespace QtAndroidInput candidatesEnd); } - void showSoftwareKeyboard(int left, int top, int width, int height, int editorHeight, int inputHints, int enterKeyType) + void showSoftwareKeyboard(int left, int top, int width, int height, int inputHints, int enterKeyType) { QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "showSoftwareKeyboard", - "(IIIIIII)V", + "(IIIIII)V", left, top, width, height, - editorHeight, inputHints, enterKeyType); #ifdef QT_DEBUG_ANDROID_IM_PROTOCOL @@ -115,7 +113,7 @@ namespace QtAndroidInput bool isSoftwareKeyboardVisible() { - return m_softwareKeyboardVisible; + return QJNIObjectPrivate::callStaticMethod<jboolean>(applicationClass(), "isSoftwareKeyboardVisible"); } QRect softwareKeyboardRect() @@ -123,6 +121,11 @@ namespace QtAndroidInput return m_softwareKeyboardRect; } + int getSelectHandleWidth() + { + return QJNIObjectPrivate::callStaticMethod<jint>(applicationClass(), "getSelectHandleWidth"); + } + void updateHandles(int mode, QPoint editMenuPos, uint32_t editButtons, QPoint cursor, QPoint anchor, bool rtl) { QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), "updateHandles", "(IIIIIIIIZ)V", @@ -131,17 +134,6 @@ namespace QtAndroidInput anchor.x(), anchor.y(), rtl); } - void updateInputItemRectangle(int left, int top, int width, int height) - { - QJNIObjectPrivate::callStaticMethod<void>(applicationClass(), - "updateInputItemRectangle", - "(IIII)V", - left, - top, - width, - height); - } - static void mouseDown(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint x, jint y) { if (m_ignoreMouseEvents) @@ -276,18 +268,14 @@ namespace QtAndroidInput } } - static void touchEnd(JNIEnv */*env*/, jobject /*thiz*/, jint /*winId*/, jint /*action*/) + static QTouchDevice *getTouchDevice() { - if (m_touchPoints.isEmpty()) - return; - - QMutexLocker lock(QtAndroid::platformInterfaceMutex()); QAndroidPlatformIntegration *platformIntegration = QtAndroid::androidPlatformIntegration(); if (!platformIntegration) - return; + return nullptr; QTouchDevice *touchDevice = platformIntegration->touchDevice(); - if (touchDevice == 0) { + if (!touchDevice) { touchDevice = new QTouchDevice; touchDevice->setType(QTouchDevice::TouchScreen); touchDevice->setCapabilities(QTouchDevice::Position @@ -298,10 +286,37 @@ namespace QtAndroidInput platformIntegration->setTouchDevice(touchDevice); } + return touchDevice; + } + + static void touchEnd(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/, jint /*action*/) + { + if (m_touchPoints.isEmpty()) + return; + + QMutexLocker lock(QtAndroid::platformInterfaceMutex()); + QTouchDevice *touchDevice = getTouchDevice(); + if (!touchDevice) + return; + QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); QWindowSystemInterface::handleTouchEvent(window, touchDevice, m_touchPoints); } + static void touchCancel(JNIEnv * /*env*/, jobject /*thiz*/, jint /*winId*/) + { + if (m_touchPoints.isEmpty()) + return; + + QMutexLocker lock(QtAndroid::platformInterfaceMutex()); + QTouchDevice *touchDevice = getTouchDevice(); + if (!touchDevice) + return; + + QWindow *window = QtAndroid::topLevelWindowAt(m_touchPoints.at(0).area.center().toPoint()); + QWindowSystemInterface::handleTouchCancelEvent(window, touchDevice); + } + static bool isTabletEventSupported(JNIEnv */*env*/, jobject /*thiz*/) { #if QT_CONFIG(tabletevent) @@ -806,7 +821,6 @@ namespace QtAndroidInput static void keyboardVisibilityChanged(JNIEnv */*env*/, jobject /*thiz*/, jboolean visibility) { - m_softwareKeyboardVisible = visibility; if (!visibility) m_softwareKeyboardRect = QRect(); @@ -854,6 +868,7 @@ namespace QtAndroidInput {"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}, {"mouseMove", "(III)V", (void *)mouseMove}, diff --git a/src/plugins/platforms/android/androidjniinput.h b/src/plugins/platforms/android/androidjniinput.h index c1442f1904..4b2aef07ca 100644 --- a/src/plugins/platforms/android/androidjniinput.h +++ b/src/plugins/platforms/android/androidjniinput.h @@ -49,7 +49,7 @@ QT_BEGIN_NAMESPACE namespace QtAndroidInput { // Software keyboard support - void showSoftwareKeyboard(int top, int left, int width, int editorHeight, int height, int inputHints, int enterKeyType); + void showSoftwareKeyboard(int top, int left, int width, int height, int inputHints, int enterKeyType); void resetSoftwareKeyboard(); void hideSoftwareKeyboard(); bool isSoftwareKeyboardVisible(); @@ -57,11 +57,11 @@ namespace QtAndroidInput void updateSelection(int selStart, int selEnd, int candidatesStart, int candidatesEnd); // Software keyboard support - // edit field resize - void updateInputItemRectangle(int left, int top, int width, int height); // cursor/selection handles void updateHandles(int handleCount, QPoint editMenuPos = QPoint(), uint32_t editButtons = 0, QPoint cursor = QPoint(), QPoint anchor = QPoint(), bool rtl = false); + int getSelectHandleWidth(); + bool registerNatives(JNIEnv *env); } diff --git a/src/plugins/platforms/android/androidjnimain.cpp b/src/plugins/platforms/android/androidjnimain.cpp index 9e4007b37a..577a7905b9 100644 --- a/src/plugins/platforms/android/androidjnimain.cpp +++ b/src/plugins/platforms/android/androidjnimain.cpp @@ -1,7 +1,7 @@ /**************************************************************************** ** ** Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> -** Copyright (C) 2016 The Qt Company Ltd. +** Copyright (C) 2022 The Qt Company Ltd. ** Contact: https://www.qt.io/licensing/ ** ** This file is part of the plugins of the Qt Toolkit. @@ -60,10 +60,11 @@ #include "qandroideventdispatcher.h" #include <android/api-level.h> -#include <QtCore/qresource.h> -#include <QtCore/qthread.h> #include <QtCore/private/qjnihelpers_p.h> #include <QtCore/private/qjni_p.h> +#include <QtCore/qbasicatomic.h> +#include <QtCore/qresource.h> +#include <QtCore/qthread.h> #include <QtGui/private/qguiapplication_p.h> #include <QtGui/private/qhighdpiscaling_p.h> @@ -106,7 +107,6 @@ static sem_t m_exitSemaphore, m_terminateSemaphore; QHash<int, AndroidSurfaceClient *> m_surfaces; static QBasicMutex m_surfacesMutex; -static int m_surfaceId = 1; static QAndroidPlatformIntegration *m_androidPlatformIntegration = nullptr; @@ -125,6 +125,8 @@ 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); + namespace QtAndroid { QBasicMutex *platformInterfaceMutex() @@ -223,14 +225,17 @@ namespace QtAndroid m_statusBarShowing = false; } - void notifyAccessibilityLocationChange() + void notifyAccessibilityLocationChange(uint accessibilityObjectId) { - QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyAccessibilityLocationChange"); + QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, + "notifyAccessibilityLocationChange", + "(I)V", accessibilityObjectId); } - void notifyObjectHide(uint accessibilityObjectId) + void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId) { - QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyObjectHide","(I)V", accessibilityObjectId); + QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyObjectHide", "(II)V", + accessibilityObjectId, parentObjectId); } void notifyObjectFocus(uint accessibilityObjectId) @@ -238,6 +243,24 @@ namespace QtAndroid QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyObjectFocus","(I)V", accessibilityObjectId); } + void notifyValueChanged(uint accessibilityObjectId, jstring value) + { + QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyValueChanged", + "(ILjava/lang/String;)V", accessibilityObjectId, + value); + } + + void notifyScrolledEvent(uint accessibilityObjectId) + { + QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyScrolledEvent", "(I)V", + accessibilityObjectId); + } + + void notifyQtAndroidPluginRunning(bool running) + { + QJNIObjectPrivate::callStaticMethod<void>(m_applicationClass, "notifyQtAndroidPluginRunning","(Z)V", running); + } + jobject createBitmap(QImage img, JNIEnv *env) { if (!m_bitmapClass) @@ -332,6 +355,12 @@ namespace QtAndroid return manufacturer + QLatin1Char(' ') + model; } + jint generateViewId() + { + return QJNIObjectPrivate::callStaticMethod<jint>("android/view/View","generateViewId", + "()I"); + } + int createSurface(AndroidSurfaceClient *client, const QRect &geometry, bool onTop, int imageDepth) { QJNIEnvironmentPrivate env; @@ -339,7 +368,7 @@ namespace QtAndroid return -1; m_surfacesMutex.lock(); - int surfaceId = m_surfaceId++; + jint surfaceId = generateViewId(); m_surfaces[surfaceId] = client; m_surfacesMutex.unlock(); @@ -362,7 +391,7 @@ namespace QtAndroid int insertNativeView(jobject view, const QRect &geometry) { m_surfacesMutex.lock(); - const int surfaceId = m_surfaceId++; + jint surfaceId = generateViewId(); m_surfaces[surfaceId] = nullptr; // dummy m_surfacesMutex.unlock(); @@ -546,15 +575,21 @@ static void startQtApplication(JNIEnv */*env*/, jclass /*clazz*/) vm->AttachCurrentThread(&env, &args); } + // Register type for invokeMethod() calls. + qRegisterMetaType<Qt::ScreenOrientation>("Qt::ScreenOrientation"); + // Register resources if they are available if (QFile{QStringLiteral("assets:/android_rcc_bundle.rcc")}.exists()) QResource::registerResource(QStringLiteral("assets:/android_rcc_bundle.rcc")); - QVarLengthArray<const char *> params(m_applicationParams.size()); - for (int i = 0; i < m_applicationParams.size(); i++) - params[i] = static_cast<const char *>(m_applicationParams[i].constData()); + const int argc = m_applicationParams.size(); + QVarLengthArray<char *> argv(argc + 1); + for (int i = 0; i < argc; i++) + argv[i] = m_applicationParams[i].data(); + argv[argc] = nullptr; - int ret = m_main(m_applicationParams.length(), const_cast<char **>(params.data())); + startQtAndroidPluginCalled.fetchAndAddRelease(1); + int ret = m_main(argc, argv.data()); if (m_mainLibraryHnd) { int res = dlclose(m_mainLibraryHnd); @@ -601,7 +636,9 @@ static void terminateQt(JNIEnv *env, jclass /*clazz*/) QAndroidEventDispatcherStopper::instance()->goingToStop(false); } - sem_wait(&m_terminateSemaphore); + if (startQtAndroidPluginCalled.loadAcquire()) + sem_wait(&m_terminateSemaphore); + sem_destroy(&m_terminateSemaphore); env->DeleteGlobalRef(m_applicationClass); @@ -644,32 +681,28 @@ static void setDisplayMetrics(JNIEnv */*env*/, jclass /*clazz*/, jint widthPixels, jint heightPixels, jint desktopWidthPixels, jint desktopHeightPixels, jdouble xdpi, jdouble ydpi, - jdouble scaledDensity, jdouble density) + jdouble scaledDensity, jdouble density, jfloat refreshRate) { - // Android does not give us the correct screen size for immersive mode, but - // the surface does have the right size - - widthPixels = qMax(widthPixels, desktopWidthPixels); - heightPixels = qMax(heightPixels, desktopHeightPixels); - m_desktopWidthPixels = desktopWidthPixels; m_desktopHeightPixels = desktopHeightPixels; m_scaledDensity = scaledDensity; m_density = density; + const QSize screenSize(widthPixels, heightPixels); + // available geometry always starts from top left + const QRect availableGeometry(0, 0, desktopWidthPixels, desktopHeightPixels); + const QSize physicalSize(qRound(double(widthPixels) / xdpi * 25.4), + qRound(double(heightPixels) / ydpi * 25.4)); + QMutexLocker lock(&m_platformMutex); if (!m_androidPlatformIntegration) { - QAndroidPlatformIntegration::setDefaultDisplayMetrics(desktopWidthPixels, - desktopHeightPixels, - qRound(double(widthPixels) / xdpi * 25.4), - qRound(double(heightPixels) / ydpi * 25.4), - widthPixels, - heightPixels); + QAndroidPlatformIntegration::setDefaultDisplayMetrics( + availableGeometry.width(), availableGeometry.height(), physicalSize.width(), + physicalSize.height(), screenSize.width(), screenSize.height()); } else { - m_androidPlatformIntegration->setDisplayMetrics(qRound(double(widthPixels) / xdpi * 25.4), - qRound(double(heightPixels) / ydpi * 25.4)); - m_androidPlatformIntegration->setScreenSize(widthPixels, heightPixels); - m_androidPlatformIntegration->setDesktopSize(desktopWidthPixels, desktopHeightPixels); + m_androidPlatformIntegration->setScreenSizeParameters(physicalSize, screenSize, + availableGeometry); + m_androidPlatformIntegration->setRefreshRate(refreshRate); } } @@ -764,12 +797,22 @@ static void handleOrientationChanged(JNIEnv */*env*/, jobject /*thiz*/, jint new QAndroidPlatformIntegration::setScreenOrientation(screenOrientation, native); QMutexLocker lock(&m_platformMutex); if (m_androidPlatformIntegration) { - QPlatformScreen *screen = m_androidPlatformIntegration->screen(); - QWindowSystemInterface::handleScreenOrientationChange(screen->screen(), - screenOrientation); + QAndroidPlatformScreen *screen = m_androidPlatformIntegration->screen(); + // Use invokeMethod to keep the certain order of the "geometry change" + // and "orientation change" event handling. + if (screen) { + QMetaObject::invokeMethod(screen, "setOrientation", Qt::AutoConnection, + Q_ARG(Qt::ScreenOrientation, screenOrientation)); + } } } +static void handleRefreshRateChanged(JNIEnv */*env*/, jclass /*cls*/, jfloat refreshRate) +{ + if (m_androidPlatformIntegration) + m_androidPlatformIntegration->setRefreshRate(refreshRate); +} + static void onActivityResult(JNIEnv */*env*/, jclass /*cls*/, jint requestCode, jint resultCode, @@ -795,14 +838,15 @@ static JNINativeMethod methods[] = { {"quitQtCoreApplication", "()V", (void *)quitQtCoreApplication}, {"terminateQt", "()V", (void *)terminateQt}, {"waitForServiceSetup", "()V", (void *)waitForServiceSetup}, - {"setDisplayMetrics", "(IIIIDDDD)V", (void *)setDisplayMetrics}, + {"setDisplayMetrics", "(IIIIDDDDF)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} + {"onBind", "(Landroid/content/Intent;)Landroid/os/IBinder;", (void *)onBind}, + {"handleRefreshRateChanged", "(F)V", (void *)handleRefreshRateChanged} }; #define FIND_AND_CHECK_CLASS(CLASS_NAME) \ diff --git a/src/plugins/platforms/android/androidjnimain.h b/src/plugins/platforms/android/androidjnimain.h index 72b864de19..cc2839c20e 100644 --- a/src/plugins/platforms/android/androidjnimain.h +++ b/src/plugins/platforms/android/androidjnimain.h @@ -95,9 +95,12 @@ namespace QtAndroid jobject createBitmap(int width, int height, QImage::Format format, JNIEnv *env); jobject createBitmapDrawable(jobject bitmap, JNIEnv *env = 0); - void notifyAccessibilityLocationChange(); - void notifyObjectHide(uint accessibilityObjectId); + void notifyAccessibilityLocationChange(uint accessibilityObjectId); + void notifyObjectHide(uint accessibilityObjectId, uint parentObjectId); void notifyObjectFocus(uint accessibilityObjectId); + void notifyValueChanged(uint accessibilityObjectId, jstring value); + void notifyScrolledEvent(uint accessibilityObjectId); + void notifyQtAndroidPluginRunning(bool running); const char *classErrorMsgFmt(); const char *methodErrorMsgFmt(); diff --git a/src/plugins/platforms/android/extract-dummy.cpp b/src/plugins/platforms/android/extract-dummy.cpp index fdce8ec64c..8cd317be4d 100644 --- a/src/plugins/platforms/android/extract-dummy.cpp +++ b/src/plugins/platforms/android/extract-dummy.cpp @@ -44,8 +44,3 @@ extern "C" JNIEXPORT jintArray JNICALL Java_org_qtproject_qt5_android_ExtractSty { return 0; } - -extern "C" JNIEXPORT jintArray JNICALL Java_org_qtproject_qt5_android_ExtractStyle_extractChunkInfo20(JNIEnv *, jobject, jbyteArray) -{ - return 0; -} diff --git a/src/plugins/platforms/android/extract.cpp b/src/plugins/platforms/android/extract.cpp index acffa353f1..6ce6153966 100644 --- a/src/plugins/platforms/android/extract.cpp +++ b/src/plugins/platforms/android/extract.cpp @@ -1,5 +1,6 @@ /**************************************************************************** ** +** Copyright (C) 2021 The Qt Company Ltd. ** Copyright (C) 2014 BogDan Vatra <bogdan@kde.org> ** Contact: https://www.qt.io/licensing/ ** @@ -123,20 +124,6 @@ extern "C" JNIEXPORT jintArray JNICALL Java_org_qtproject_qt5_android_ExtractSty return result; } -extern "C" JNIEXPORT jintArray JNICALL Java_org_qtproject_qt5_android_ExtractStyle_extractChunkInfo20(JNIEnv * env, jobject obj, jbyteArray chunkObj) -{ - size_t chunkSize = env->GetArrayLength(chunkObj); - void* storage = alloca(chunkSize); - env->GetByteArrayRegion(chunkObj, 0, chunkSize, - reinterpret_cast<jbyte*>(storage)); - - if (!env->ExceptionCheck()) - return Java_org_qtproject_qt5_android_ExtractStyle_extractNativeChunkInfo20(env, obj, long(storage)); - else - env->ExceptionClear(); - return 0; -} - static inline void fill9patchOffsets(Res_png_9patch20* patch) { patch->xDivsOffset = sizeof(Res_png_9patch20); patch->yDivsOffset = patch->xDivsOffset + (patch->numXDivs * sizeof(int32_t)); diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 07776a4a76..180dc248d6 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -86,6 +86,7 @@ struct AssetItem { } Type type = Type::File; QString name; + qint64 size = -1; }; using AssetItemList = QVector<AssetItem>; @@ -139,6 +140,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. QJNIObjectPrivate files = QJNIObjectPrivate::callStaticObjectMethod(QtAndroid::applicationClass(), "listAssetContent", "(Landroid/content/res/AssetManager;Ljava/lang/String;)[Ljava/lang/String;", @@ -217,7 +220,7 @@ public: return m_currentIterator->currentFileName(); } - virtual QString currentFilePath() const + QString currentFilePath() const override { if (!m_currentIterator) return {}; @@ -261,7 +264,7 @@ public: bool open(QIODevice::OpenMode openMode) override { - if (m_isFolder || (openMode & QIODevice::WriteOnly)) + if (!m_assetInfo || m_assetInfo->type != AssetItem::Type::File || (openMode & QIODevice::WriteOnly)) return false; close(); m_assetFile = AAssetManager_open(m_assetManager, m_fileName.toUtf8(), AASSET_MODE_BUFFER); @@ -275,14 +278,13 @@ public: m_assetFile = 0; return true; } - m_isFolder = false; return false; } qint64 size() const override { - if (m_assetFile) - return AAsset_getLength(m_assetFile); + if (m_assetInfo) + return m_assetInfo->size; return -1; } @@ -326,10 +328,12 @@ public: { FileFlags commonFlags(ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm|ExistsFlag); FileFlags flags; - if (m_assetFile) - flags = FileType | commonFlags; - else if (m_isFolder) - flags = DirectoryType | commonFlags; + if (m_assetInfo) { + if (m_assetInfo->type == AssetItem::Type::File) + flags = FileType | commonFlags; + else if (m_assetInfo->type == AssetItem::Type::Folder) + flags = DirectoryType | commonFlags; + } return type & flags; } @@ -364,21 +368,48 @@ public: return; close(); m_fileName = cleanedAssetPath(file); - switch (FolderIterator::fileType(m_fileName)) { - case AssetItem::Type::File: - open(QIODevice::ReadOnly); - break; - case AssetItem::Type::Folder: - m_isFolder = true; - break; - case AssetItem::Type::Invalid: - break; + + { + QMutexLocker lock(&m_assetsInfoCacheMutex); + QSharedPointer<AssetItem> *assetInfoPtr = m_assetsInfoCache.object(m_fileName); + if (assetInfoPtr) { + m_assetInfo = *assetInfoPtr; + return; + } } + + QSharedPointer<AssetItem> *newAssetInfoPtr = new QSharedPointer<AssetItem>(new AssetItem); + + m_assetInfo = *newAssetInfoPtr; + m_assetInfo->name = m_fileName; + m_assetInfo->type = AssetItem::Type::Invalid; + + m_assetFile = AAssetManager_open(m_assetManager, m_fileName.toUtf8(), AASSET_MODE_BUFFER); + + if (m_assetFile) { + m_assetInfo->type = AssetItem::Type::File; + m_assetInfo->size = AAsset_getLength(m_assetFile); + } else { + auto *assetDir = AAssetManager_openDir(m_assetManager, m_fileName.toUtf8()); + if (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); + } + } + + QMutexLocker lock(&m_assetsInfoCacheMutex); + m_assetsInfoCache.insert(m_fileName, newAssetInfoPtr); } Iterator *beginEntryList(QDir::Filters filters, const QStringList &filterNames) override { - if (m_isFolder) + if (m_assetInfo && m_assetInfo->type == AssetItem::Type::Folder) return new AndroidAbstractFileEngineIterator(filters, filterNames, m_fileName); return nullptr; } @@ -388,9 +419,14 @@ private: AAssetManager *m_assetManager = nullptr; // initialize with a name that can't be used as a file name QString m_fileName = QLatin1String("."); - bool m_isFolder = false; + QSharedPointer<AssetItem> m_assetInfo; + + static QCache<QString, QSharedPointer<AssetItem>> m_assetsInfoCache; + static QMutex m_assetsInfoCacheMutex; }; +QCache<QString, QSharedPointer<AssetItem>> AndroidAbstractFileEngine::m_assetsInfoCache(std::max(200, qEnvironmentVariableIntValue("QT_ANDROID_MAX_FILEINFO_ASSETS_CACHE_SIZE"))); +QMutex AndroidAbstractFileEngine::m_assetsInfoCacheMutex; AndroidAssetsFileEngineHandler::AndroidAssetsFileEngineHandler() { diff --git a/src/plugins/platforms/android/qandroidinputcontext.cpp b/src/plugins/platforms/android/qandroidinputcontext.cpp index 687cced1e2..8a44482d44 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.cpp +++ b/src/plugins/platforms/android/qandroidinputcontext.cpp @@ -95,6 +95,7 @@ private: static QAndroidInputContext *m_androidInputContext = 0; static char const *const QtNativeInputConnectionClassName = "org/qtproject/qt5/android/QtNativeInputConnection"; static char const *const QtExtractedTextClassName = "org/qtproject/qt5/android/QtExtractedText"; +static char const *const QtObjectType = "QDialog"; static jclass m_extractedTextClass = 0; static jmethodID m_classConstructorMethodID = 0; static jfieldID m_partialEndOffsetFieldID = 0; @@ -506,14 +507,19 @@ QAndroidInputContext::QAndroidInputContext() m_androidInputContext = this; QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::cursorRectangleChanged, - this, &QAndroidInputContext::updateInputItemRectangle); + this, &QAndroidInputContext::updateSelectionHandles); QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::anchorRectangleChanged, this, &QAndroidInputContext::updateSelectionHandles); QObject::connect(QGuiApplication::inputMethod(), &QInputMethod::inputItemClipRectangleChanged, this, [this]{ auto im = qGuiApp->inputMethod(); if (!im->inputItemClipRectangle().contains(im->anchorRectangle()) || !im->inputItemClipRectangle().contains(im->cursorRectangle())) { - m_handleMode = Hidden; + // Undoes the hidden request if the only reason for the hidden is that + // X of the cursorRectangle or X of the anchorRectangle is less than 0. + const int rectX = im->inputItemClipRectangle().x(); + if (im->cursorRectangle().x() > rectX && im->anchorRectangle().x() > rectX) + m_handleMode = Hidden; + updateSelectionHandles(); } }); @@ -622,13 +628,13 @@ void QAndroidInputContext::updateSelectionHandles() if (noHandles) return; + QWindow *window = qGuiApp->focusWindow(); auto im = qGuiApp->inputMethod(); - if (!m_focusObject || ((m_handleMode & 0xff) == Hidden)) { + if (!m_focusObject || ((m_handleMode & 0xff) == Hidden) || !window) { // Hide the handles QtAndroidInput::updateHandles(Hidden); return; } - QWindow *window = qGuiApp->focusWindow(); double pixelDensity = window ? QHighDpiScaling::factor(window) : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen()); @@ -646,14 +652,25 @@ void QAndroidInputContext::updateSelectionHandles() } auto curRect = im->cursorRectangle(); - QPoint cursorPoint = qGuiApp->focusWindow()->mapToGlobal(QPoint(curRect.x() + (curRect.width() / 2), curRect.y() + curRect.height())); - QPoint editMenuPoint(cursorPoint.x(), cursorPoint.y()); + QPoint cursorPointGlobal = window->mapToGlobal(QPoint(curRect.x() + (curRect.width() / 2), curRect.y() + curRect.height())); + QPoint cursorPoint(curRect.center().x(), curRect.bottom()); + int x = curRect.x(); + int y = curRect.y(); + + // Use x and y for the editMenuPoint from the cursorPointGlobal when the cursor is in the Dialog + if (cursorPointGlobal != cursorPoint) { + x = cursorPointGlobal.x(); + y = cursorPointGlobal.y(); + } + + QPoint editMenuPoint(x, y); m_handleMode &= ShowEditPopup; m_handleMode |= ShowCursor; uint32_t buttons = EditContext::PasteButton; if (!query.value(Qt::ImSurroundingText).toString().isEmpty()) buttons |= EditContext::SelectAllButton; - QtAndroidInput::updateHandles(m_handleMode, editMenuPoint * pixelDensity, buttons, cursorPoint * pixelDensity); + QtAndroidInput::updateHandles(m_handleMode, editMenuPoint * pixelDensity, buttons, + cursorPointGlobal * pixelDensity); // The VK is hidden, reset the timer if (m_hideCursorHandleTimer.isActive()) m_hideCursorHandleTimer.start(); @@ -666,10 +683,30 @@ void QAndroidInputContext::updateSelectionHandles() if (cpos > anchor) std::swap(leftRect, rightRect); - QPoint leftPoint(leftRect.bottomLeft().toPoint() * pixelDensity); - QPoint righPoint(rightRect.bottomRight().toPoint() * pixelDensity); - QPoint editPoint(leftRect.united(rightRect).topLeft().toPoint() * pixelDensity); - QtAndroidInput::updateHandles(m_handleMode, editPoint, EditContext::AllButtons, leftPoint, righPoint, + // Move the left or right select handle to the center from the screen edge + // the select handle is close to or over the screen edge. Otherwise, the + // select handle might go out of the screen and it would be impossible to drag. + QPoint leftPoint(window->mapToGlobal(leftRect.bottomLeft().toPoint())); + QPoint rightPoint(window->mapToGlobal(rightRect.bottomRight().toPoint())); + static int m_selectHandleWidth = 0; + // For comparison, get the width of the handle. + // Only half of the width will protrude from the cursor on each side + if (m_selectHandleWidth == 0) + m_selectHandleWidth = QtAndroidInput::getSelectHandleWidth() / 2; + + int rightSideOfScreen = QtAndroid::androidPlatformIntegration()->screen()->availableGeometry().right(); + + // Check if handle will fit the screen on left side. If not, then move it closer to the center + if (leftPoint.x() <= m_selectHandleWidth) + leftPoint.setX(m_selectHandleWidth / pixelDensity); + + // Check if handle will fit the screen on right side. If not, then move it closer to the center + if (rightPoint.x() >= (rightSideOfScreen / pixelDensity) - m_selectHandleWidth) + rightPoint.setX((rightSideOfScreen / pixelDensity) - (m_selectHandleWidth / pixelDensity)); + + QPoint editPoint(window->mapToGlobal(leftRect.united(rightRect).topLeft().toPoint())); + QtAndroidInput::updateHandles(m_handleMode, editPoint * pixelDensity, EditContext::AllButtons, + leftPoint * pixelDensity, rightPoint * pixelDensity, query.value(Qt::ImCurrentSelection).toString().isRightToLeft()); m_hideCursorHandleTimer.stop(); } @@ -693,7 +730,17 @@ void QAndroidInputContext::handleLocationChanged(int handleId, int x, int y) double pixelDensity = window ? QHighDpiScaling::factor(window) : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen()); - QPointF point(x / pixelDensity, y / pixelDensity); + auto object = m_focusObject->parent(); + int dialogMoveX = 0; + while (object) { + if (QString::compare(object->metaObject()->className(), + QtObjectType, Qt::CaseInsensitive) == 0) { + dialogMoveX += object->property("x").toInt(); + } + object = object->parent(); + }; + + QPointF point((x / pixelDensity) - dialogMoveX, y / pixelDensity); point.setY(point.y() - leftRect.width() / 2); QInputMethodQueryEvent query(Qt::ImCursorPosition | Qt::ImAnchorPosition @@ -931,50 +978,12 @@ void QAndroidInputContext::showInputPanel() else m_updateCursorPosConnection = connect(qGuiApp->focusObject(), SIGNAL(cursorPositionChanged()), this, SLOT(updateCursorPosition())); - QRect rect = cursorRect(); + QRect rect = inputItemRectangle(); QtAndroidInput::showSoftwareKeyboard(rect.left(), rect.top(), rect.width(), rect.height(), - inputItemRectangle().height(), query->value(Qt::ImHints).toUInt(), query->value(Qt::ImEnterKeyType).toUInt()); } -QRect QAndroidInputContext::cursorRect() -{ - QSharedPointer<QInputMethodQueryEvent> query = focusObjectInputMethodQuery(); - // if single line, we do not want to mess with the editor's position, as we do not - // have to follow the cursor in vertical axis - if (query.isNull() - || (query->value(Qt::ImHints).toUInt() & Qt::ImhMultiLine) != Qt::ImhMultiLine) - return {}; - - auto im = qGuiApp->inputMethod(); - if (!im) - return {}; - - const auto cursorRect= im->cursorRectangle().toRect(); - QRect finalRect(inputItemRectangle()); - const QWindow *window = qGuiApp->focusWindow(); - const double pd = window - ? QHighDpiScaling::factor(window) - : QHighDpiScaling::factor(QtAndroid::androidPlatformIntegration()->screen()); - finalRect.setY(cursorRect.y() * pd); - finalRect.setHeight(cursorRect.height() * pd); - //fiddle a bit with vert margins, so the tracking rectangle is not too tight. - finalRect += QMargins(0, cursorRect.height() / 4, 0, cursorRect.height() / 4); - return finalRect; -} - -void QAndroidInputContext::updateInputItemRectangle() -{ - QRect rect = cursorRect(); - - if (!rect.isValid()) - return; - QtAndroidInput::updateInputItemRectangle(rect.left(), rect.top(), - rect.width(), rect.height()); - updateSelectionHandles(); -} - void QAndroidInputContext::showInputPanelLater(Qt::ApplicationState state) { if (state != Qt::ApplicationActive) @@ -1234,13 +1243,21 @@ bool QAndroidInputContext::focusObjectStopComposing() m_composingCursor = -1; - // Moving Qt's cursor to where the preedit cursor used to be - QList<QInputMethodEvent::Attribute> attributes; - attributes.append(QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0)); - - QInputMethodEvent event(QString(), attributes); - event.setCommitString(m_composingText); - sendInputMethodEvent(&event); + { + // commit the composing test + QList<QInputMethodEvent::Attribute> attributes; + QInputMethodEvent event(QString(), attributes); + event.setCommitString(m_composingText); + sendInputMethodEvent(&event); + } + { + // Moving Qt's cursor to where the preedit cursor used to be + QList<QInputMethodEvent::Attribute> attributes; + attributes.append( + QInputMethodEvent::Attribute(QInputMethodEvent::Selection, localCursorPos, 0)); + QInputMethodEvent event(QString(), attributes); + sendInputMethodEvent(&event); + } return true; } diff --git a/src/plugins/platforms/android/qandroidinputcontext.h b/src/plugins/platforms/android/qandroidinputcontext.h index 02a66c367a..6b72cca15b 100644 --- a/src/plugins/platforms/android/qandroidinputcontext.h +++ b/src/plugins/platforms/android/qandroidinputcontext.h @@ -41,6 +41,7 @@ #ifndef ANDROIDINPUTCONTEXT_H #define ANDROIDINPUTCONTEXT_H +#include <QtCore/QPointer> #include <qpa/qplatforminputcontext.h> #include <functional> #include <jni.h> @@ -138,7 +139,6 @@ public: public slots: void safeCall(const std::function<void()> &func, Qt::ConnectionType conType = Qt::BlockingQueuedConnection); void updateCursorPosition(); - void updateInputItemRectangle(); void updateSelectionHandles(); void handleLocationChanged(int handleId, int x, int y); void touchDown(int x, int y); @@ -155,7 +155,6 @@ private: bool focusObjectIsComposing() const; void focusObjectStartComposing(); bool focusObjectStopComposing(); - QRect cursorRect(); private: ExtractedText m_extractedText; @@ -165,7 +164,7 @@ private: QMetaObject::Connection m_updateCursorPosConnection; HandleModes m_handleMode; int m_batchEditNestingLevel; - QObject *m_focusObject; + QPointer<QObject> m_focusObject; QTimer m_hideCursorHandleTimer; }; Q_DECLARE_OPERATORS_FOR_FLAGS(QAndroidInputContext::HandleModes) diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp index cc05dad749..b29190d3e9 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.cpp +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.cpp @@ -61,12 +61,22 @@ void QAndroidPlatformAccessibility::notifyAccessibilityUpdate(QAccessibleEvent * // so that the element can be moved on the screen if it's focused. if (event->type() == QAccessible::LocationChanged) { - QtAndroidAccessibility::notifyLocationChange(); + QtAndroidAccessibility::notifyLocationChange(event->uniqueId()); } else if (event->type() == QAccessible::ObjectHide) { QtAndroidAccessibility::notifyObjectHide(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()); } } +void QAndroidPlatformAccessibility::setRootObject(QObject *obj) +{ + QPlatformAccessibility::setRootObject(obj); + QtAndroidAccessibility::createAccessibilityContextObject(obj); +} + QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformaccessibility.h b/src/plugins/platforms/android/qandroidplatformaccessibility.h index 8216c05fa6..df3fe43a04 100644 --- a/src/plugins/platforms/android/qandroidplatformaccessibility.h +++ b/src/plugins/platforms/android/qandroidplatformaccessibility.h @@ -52,6 +52,7 @@ public: ~QAndroidPlatformAccessibility(); void notifyAccessibilityUpdate(QAccessibleEvent *event) override; + void setRootObject(QObject *obj) override; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp index 6bb3372380..b4f1c95746 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp @@ -45,6 +45,7 @@ #include <QMimeType> #include <QMimeDatabase> #include <QRegularExpression> +#include <QUrl> QT_BEGIN_NAMESPACE @@ -118,7 +119,7 @@ void QAndroidPlatformFileDialogHelper::takePersistableUriPermission(const QJNIOb uri.object(), modeFlags); } -void QAndroidPlatformFileDialogHelper::setIntentTitle(const QString &title) +void QAndroidPlatformFileDialogHelper::setInitialFileName(const QString &title) { const QJNIObjectPrivate extraTitle = QJNIObjectPrivate::getStaticObjectField( JniIntentClass, "EXTRA_TITLE", "Ljava/lang/String;"); @@ -127,6 +128,22 @@ void QAndroidPlatformFileDialogHelper::setIntentTitle(const QString &title) extraTitle.object(), QJNIObjectPrivate::fromString(title).object()); } +void QAndroidPlatformFileDialogHelper::setInitialDirectoryUri(const QString &directory) +{ + if (directory.isEmpty()) + return; + + if (QtAndroidPrivate::androidSdkVersion() < 26) + return; + + const auto extraInitialUri = QJNIObjectPrivate::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(), + QJNIObjectPrivate::fromString(directory).object()); +} + void QAndroidPlatformFileDialogHelper::setOpenableCategory() { const QJNIObjectPrivate CATEGORY_OPENABLE = QJNIObjectPrivate::getStaticObjectField( @@ -147,10 +164,10 @@ QStringList nameFilterExtensions(const QString nameFilters) { QStringList ret; #if QT_CONFIG(regularexpression) - QRegularExpression re("(\\*\\.?\\w*)"); + QRegularExpression re("(\\*\\.[a-z .]+)"); QRegularExpressionMatchIterator i = re.globalMatch(nameFilters); while (i.hasNext()) - ret << i.next().captured(1); + ret << i.next().captured(1).trimmed(); #endif // QT_CONFIG(regularexpression) ret.removeAll("*"); return ret; @@ -159,23 +176,24 @@ QStringList nameFilterExtensions(const QString nameFilters) void QAndroidPlatformFileDialogHelper::setMimeTypes() { QStringList mimeTypes = options()->mimeTypeFilters(); - const QString nameFilter = options()->initiallySelectedNameFilter(); + const QStringList nameFilters = options()->nameFilters(); + const QString nameFilter = nameFilters.isEmpty() ? QString() : nameFilters.first(); - if (mimeTypes.isEmpty() && !nameFilter.isEmpty()) { + if (!nameFilter.isEmpty()) { QMimeDatabase db; for (const QString &filter : nameFilterExtensions(nameFilter)) - mimeTypes.append(db.mimeTypeForFile(filter).name()); + mimeTypes.append(db.mimeTypeForFile(filter, QMimeDatabase::MatchExtension).name()); } - QString type = !mimeTypes.isEmpty() ? mimeTypes.at(0) : QLatin1String("*/*"); + const QString initialType = mimeTypes.size() == 1 ? mimeTypes.at(0) : QLatin1String("*/*"); m_intent.callObjectMethod("setType", "(Ljava/lang/String;)Landroid/content/Intent;", - QJNIObjectPrivate::fromString(type).object()); + QJNIObjectPrivate::fromString(initialType).object()); if (!mimeTypes.isEmpty()) { const QJNIObjectPrivate extraMimeType = QJNIObjectPrivate::getStaticObjectField( JniIntentClass, "EXTRA_MIME_TYPES", "Ljava/lang/String;"); - QJNIObjectPrivate mimeTypesArray = QJNIObjectPrivate::callStaticObjectMethod( + const QJNIObjectPrivate mimeTypesArray = QJNIObjectPrivate::callStaticObjectMethod( "org/qtproject/qt5/android/QtNative", "getStringArray", "(Ljava/lang/String;)[Ljava/lang/String;", @@ -207,6 +225,9 @@ 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) + setInitialFileName(selectedFiles.first().fileName()); } else if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) { switch (options()->fileMode()) { case QFileDialogOptions::FileMode::DirectoryOnly: @@ -230,7 +251,7 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win setMimeTypes(); } - setIntentTitle(options()->windowTitle()); + setInitialDirectoryUri(m_directory.toString()); QtAndroidPrivate::registerActivityResultListener(this); m_activity.callMethod<void>("startActivityForResult", "(Landroid/content/Intent;I)V", @@ -245,6 +266,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 5a7a28a8a0..20fc9fdccd 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h @@ -67,21 +67,23 @@ public: void setFilter() override {}; QList<QUrl> selectedFiles() const override { return m_selectedFile; }; void selectFile(const QUrl &file) override { Q_UNUSED(file) }; - QUrl directory() const override { return QUrl(); }; - void setDirectory(const QUrl &directory) override { Q_UNUSED(directory) }; + 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; private: QJNIObjectPrivate getFileDialogIntent(const QString &intentType); void takePersistableUriPermission(const QJNIObjectPrivate &uri); - void setIntentTitle(const QString &title); + 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; QJNIObjectPrivate m_intent; const QJNIObjectPrivate m_activity; }; diff --git a/src/plugins/platforms/android/qandroidplatformintegration.cpp b/src/plugins/platforms/android/qandroidplatformintegration.cpp index aaeb9199d8..6fe0caba2e 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.cpp +++ b/src/plugins/platforms/android/qandroidplatformintegration.cpp @@ -90,6 +90,7 @@ Qt::ScreenOrientation QAndroidPlatformIntegration::m_orientation = Qt::PrimaryOr Qt::ScreenOrientation QAndroidPlatformIntegration::m_nativeOrientation = Qt::PrimaryOrientation; bool QAndroidPlatformIntegration::m_showPasswordEnabled = false; +static bool m_running = false; void *QAndroidPlatformNativeInterface::nativeResourceForIntegration(const QByteArray &resource) { @@ -158,6 +159,10 @@ void QAndroidPlatformNativeInterface::customEvent(QEvent *event) api->accessibility()->setActive(QtAndroidAccessibility::isActive()); #endif // QT_NO_ACCESSIBILITY + if (!m_running) { + m_running = true; + QtAndroid::notifyQtAndroidPluginRunning(m_running); + } api->flushPendingUpdates(); } @@ -183,9 +188,10 @@ QAndroidPlatformIntegration::QAndroidPlatformIntegration(const QStringList ¶ m_primaryScreen = new QAndroidPlatformScreen(); QWindowSystemInterface::handleScreenAdded(m_primaryScreen); - m_primaryScreen->setPhysicalSize(QSize(m_defaultPhysicalSizeWidth, m_defaultPhysicalSizeHeight)); - m_primaryScreen->setSize(QSize(m_defaultScreenWidth, m_defaultScreenHeight)); - m_primaryScreen->setAvailableGeometry(QRect(0, 0, m_defaultGeometryWidth, m_defaultGeometryHeight)); + const QSize physicalSize(m_defaultPhysicalSizeWidth, m_defaultPhysicalSizeHeight); + const QSize screenSize(m_defaultScreenWidth, m_defaultScreenHeight); + const QRect geometry(0, 0, m_defaultGeometryWidth, m_defaultGeometryHeight); + m_primaryScreen->setSizeParameters(physicalSize, screenSize, geometry); m_mainThread = QThread::currentThread(); @@ -458,10 +464,10 @@ void QAndroidPlatformIntegration::setScreenOrientation(Qt::ScreenOrientation cur void QAndroidPlatformIntegration::flushPendingUpdates() { - m_primaryScreen->setPhysicalSize(QSize(m_defaultPhysicalSizeWidth, - m_defaultPhysicalSizeHeight)); - m_primaryScreen->setSize(QSize(m_defaultScreenWidth, m_defaultScreenHeight)); - m_primaryScreen->setAvailableGeometry(QRect(0, 0, m_defaultGeometryWidth, m_defaultGeometryHeight)); + const QSize physicalSize(m_defaultPhysicalSizeWidth, m_defaultPhysicalSizeHeight); + const QSize screenSize(m_defaultScreenWidth, m_defaultScreenHeight); + const QRect geometry(0, 0, m_defaultGeometryWidth, m_defaultGeometryHeight); + m_primaryScreen->setSizeParameters(physicalSize, screenSize, geometry); } #ifndef QT_NO_ACCESSIBILITY @@ -489,6 +495,23 @@ void QAndroidPlatformIntegration::setScreenSize(int width, int height) QMetaObject::invokeMethod(m_primaryScreen, "setSize", Qt::AutoConnection, Q_ARG(QSize, QSize(width, height))); } +void QAndroidPlatformIntegration::setScreenSizeParameters(const QSize &physicalSize, + const QSize &screenSize, + const QRect &availableGeometry) +{ + if (m_primaryScreen) { + QMetaObject::invokeMethod(m_primaryScreen, "setSizeParameters", Qt::AutoConnection, + Q_ARG(QSize, physicalSize), Q_ARG(QSize, screenSize), + Q_ARG(QRect, availableGeometry)); + } +} + +void QAndroidPlatformIntegration::setRefreshRate(qreal refreshRate) +{ + if (m_primaryScreen) + QMetaObject::invokeMethod(m_primaryScreen, "setRefreshRate", Qt::AutoConnection, + Q_ARG(qreal, refreshRate)); +} #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 ecbde4f951..f34972b81e 100644 --- a/src/plugins/platforms/android/qandroidplatformintegration.h +++ b/src/plugins/platforms/android/qandroidplatformintegration.h @@ -96,6 +96,13 @@ public: virtual void setDesktopSize(int width, int height); virtual void setDisplayMetrics(int width, int height); void setScreenSize(int width, int height); + // The 3 methods above were replaced by a new one, so that we could have + // a better control over "geometry changed" event handling. Technically + // they are no longer used and can be removed. Not doing it now, because + // I'm not sure if it might be helpful to have them or not. + void setScreenSizeParameters(const QSize &physicalSize, const QSize &screenSize, + const QRect &availableGeometry); + void setRefreshRate(qreal refreshRate); bool isVirtualDesktop() { return true; } QPlatformFontDatabase *fontDatabase() const override; diff --git a/src/plugins/platforms/android/qandroidplatformscreen.cpp b/src/plugins/platforms/android/qandroidplatformscreen.cpp index 7e036868fc..b2ca70ce45 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.cpp +++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp @@ -55,6 +55,7 @@ #include <android/native_window_jni.h> #include <qguiapplication.h> +#include <QtCore/private/qjnihelpers_p.h> #include <QtGui/QGuiApplication> #include <QtGui/QWindow> #include <QtGui/private/qwindow_p.h> @@ -104,6 +105,42 @@ QAndroidPlatformScreen::QAndroidPlatformScreen() m_physicalSize.setHeight(QAndroidPlatformIntegration::m_defaultPhysicalSizeHeight); m_physicalSize.setWidth(QAndroidPlatformIntegration::m_defaultPhysicalSizeWidth); connect(qGuiApp, &QGuiApplication::applicationStateChanged, this, &QAndroidPlatformScreen::applicationStateChanged); + + QJNIObjectPrivate activity(QtAndroid::activity()); + if (!activity.isValid()) + return; + QJNIObjectPrivate display; + if (QtAndroidPrivate::androidSdkVersion() < 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"); + if (QtAndroidPrivate::androidSdkVersion() < 23) { + m_modes << Mode { .size = m_physicalSize.toSize(), .refreshRate = m_refreshRate }; + return; + } + QJNIEnvironmentPrivate 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 = QJNIObjectPrivate::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") }; + } + if (m_modes.isEmpty()) + m_modes << Mode { .size = m_physicalSize.toSize(), .refreshRate = m_refreshRate }; } QAndroidPlatformScreen::~QAndroidPlatformScreen() @@ -243,6 +280,37 @@ void QAndroidPlatformScreen::setSize(const QSize &size) QWindowSystemInterface::handleScreenGeometryChange(QPlatformScreen::screen(), geometry(), availableGeometry()); } +void QAndroidPlatformScreen::setSizeParameters(const QSize &physicalSize, const QSize &size, + const QRect &availableGeometry) +{ + // The goal of this method is to set all geometry-related parameters + // at the same time and generate only one screen geometry change event. + m_physicalSize = physicalSize; + m_size = size; + // If available geometry has changed, the event will be handled in + // setAvailableGeometry. Otherwise we need to explicitly handle it to + // retain the behavior, because setSize() does the handling unconditionally. + if (m_availableGeometry != availableGeometry) { + setAvailableGeometry(availableGeometry); + } else { + QWindowSystemInterface::handleScreenGeometryChange(QPlatformScreen::screen(), geometry(), + this->availableGeometry()); + } +} + +void QAndroidPlatformScreen::setRefreshRate(qreal refreshRate) +{ + if (refreshRate == m_refreshRate) + return; + m_refreshRate = refreshRate; + QWindowSystemInterface::handleScreenRefreshRateChange(QPlatformScreen::screen(), refreshRate); +} + +void QAndroidPlatformScreen::setOrientation(Qt::ScreenOrientation orientation) +{ + QWindowSystemInterface::handleScreenOrientationChange(QPlatformScreen::screen(), orientation); +} + void QAndroidPlatformScreen::setAvailableGeometry(const QRect &rect) { QMutexLocker lock(&m_surfaceMutex); @@ -429,7 +497,7 @@ static const int androidLogicalDpi = 72; QDpi QAndroidPlatformScreen::logicalDpi() const { - qreal lDpi = QtAndroid::scaledDensity() * androidLogicalDpi; + qreal lDpi = QtAndroid::pixelDensity() * androidLogicalDpi; return QDpi(lDpi, lDpi); } diff --git a/src/plugins/platforms/android/qandroidplatformscreen.h b/src/plugins/platforms/android/qandroidplatformscreen.h index 54b3c5b8a8..e73ea31a86 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.h +++ b/src/plugins/platforms/android/qandroidplatformscreen.h @@ -69,6 +69,12 @@ public: QImage::Format format() const override { return m_format; } QSizeF physicalSize() const override { return m_physicalSize; } + QString name() const override { return m_name; } + QVector<Mode> modes() const override { return m_modes; } + 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; QWindow *topLevelAt(const QPoint & p) const override; @@ -87,6 +93,10 @@ public slots: void setPhysicalSize(const QSize &size); void setAvailableGeometry(const QRect &rect); void setSize(const QSize &size); + void setSizeParameters(const QSize &physicalSize, const QSize &size, + const QRect &availableGeometry); + void setRefreshRate(qreal refreshRate); + void setOrientation(Qt::ScreenOrientation orientation); protected: bool event(QEvent *event) override; @@ -100,6 +110,10 @@ protected: int m_depth; QImage::Format m_format; QSizeF m_physicalSize; + qreal m_refreshRate; + QString m_name; + QVector<Mode> m_modes; + int m_currentMode = 0; private: QDpi logicalDpi() const override; diff --git a/src/plugins/platforms/android/qandroidplatformservices.cpp b/src/plugins/platforms/android/qandroidplatformservices.cpp index c095613ce7..e317627770 100644 --- a/src/plugins/platforms/android/qandroidplatformservices.cpp +++ b/src/plugins/platforms/android/qandroidplatformservices.cpp @@ -59,12 +59,13 @@ 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 QLatin1String fileScheme("file"); - 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(); QJNIObjectPrivate urlString = QJNIObjectPrivate::fromString(url.toString()); QJNIObjectPrivate mimeString = QJNIObjectPrivate::fromString(mime); |