diff options
Diffstat (limited to 'src/plugins/platforms')
15 files changed, 875 insertions, 92 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 0f0cd1e16f..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" @@ -96,6 +97,14 @@ namespace QtAndroidAccessibility template <typename Func, typename Ret> void runInObjectContext(QObject *context, Func &&func, Ret *retVal) { + AndroidDeadlockProtector protector; + if (!protector.acquire()) { + __android_log_print(ANDROID_LOG_WARN, m_qtTag, + "Could not run accessibility call in object context, accessing " + "main thread could lead to deadlock"); + return; + } + if (!QtAndroid::blockEventLoopsWhenSuspended() || QGuiApplication::applicationState() != Qt::ApplicationSuspended) { QMetaObject::invokeMethod(context, func, Qt::BlockingQueuedConnection, retVal); diff --git a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp index 316859b2a6..180dc248d6 100644 --- a/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp +++ b/src/plugins/platforms/android/qandroidassetsfileenginehandler.cpp @@ -140,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;", @@ -218,7 +220,7 @@ public: return m_currentIterator->currentFileName(); } - virtual QString currentFilePath() const + QString currentFilePath() const override { if (!m_currentIterator) return {}; @@ -390,8 +392,13 @@ public: } else { auto *assetDir = AAssetManager_openDir(m_assetManager, m_fileName.toUtf8()); if (assetDir) { - if (AAssetDir_getNextFileName(assetDir)) + if (AAssetDir_getNextFileName(assetDir) + || (!FolderIterator::fromCache(m_fileName, false)->empty())) { + // If AAssetDir_getNextFileName is not valid, it still can be a directory that + // contains only other directories (no files). FolderIterator will not be called + // on the directory containing files so it should not be too time consuming now. m_assetInfo->type = AssetItem::Type::Folder; + } AAssetDir_close(assetDir); } } diff --git a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp index 8a8ecc4489..b4f1c95746 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.cpp @@ -128,6 +128,22 @@ void QAndroidPlatformFileDialogHelper::setInitialFileName(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( @@ -210,11 +226,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win if (options()->acceptMode() == QFileDialogOptions::AcceptSave) { m_intent = getFileDialogIntent("ACTION_CREATE_DOCUMENT"); const QList<QUrl> selectedFiles = options()->initiallySelectedFiles(); - if (selectedFiles.size() > 0) { - // TODO: The initial folder to show at the start should be handled by EXTRA_INITIAL_URI - // Take only the file name. + if (selectedFiles.size() > 0) setInitialFileName(selectedFiles.first().fileName()); - } } else if (options()->acceptMode() == QFileDialogOptions::AcceptOpen) { switch (options()->fileMode()) { case QFileDialogOptions::FileMode::DirectoryOnly: @@ -238,6 +251,8 @@ bool QAndroidPlatformFileDialogHelper::show(Qt::WindowFlags windowFlags, Qt::Win setMimeTypes(); } + setInitialDirectoryUri(m_directory.toString()); + QtAndroidPrivate::registerActivityResultListener(this); m_activity.callMethod<void>("startActivityForResult", "(Landroid/content/Intent;I)V", m_intent.object(), REQUEST_CODE); @@ -251,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 1442c845cd..20fc9fdccd 100644 --- a/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h +++ b/src/plugins/platforms/android/qandroidplatformfiledialoghelper.h @@ -67,8 +67,8 @@ 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; @@ -76,12 +76,14 @@ private: QJNIObjectPrivate getFileDialogIntent(const QString &intentType); void takePersistableUriPermission(const QJNIObjectPrivate &uri); void setInitialFileName(const QString &title); + void setInitialDirectoryUri(const QString &directory); void setOpenableCategory(); void setAllowMultipleSelections(bool allowMultiple); void setMimeTypes(); QEventLoop m_eventLoop; QList<QUrl> m_selectedFile; + QUrl m_directory; QJNIObjectPrivate m_intent; const QJNIObjectPrivate m_activity; }; diff --git a/src/plugins/platforms/android/qandroidplatformscreen.cpp b/src/plugins/platforms/android/qandroidplatformscreen.cpp index 0f60202eb9..b2ca70ce45 100644 --- a/src/plugins/platforms/android/qandroidplatformscreen.cpp +++ b/src/plugins/platforms/android/qandroidplatformscreen.cpp @@ -497,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/ios/quiview_accessibility.mm b/src/plugins/platforms/ios/quiview_accessibility.mm index 9a103509cc..3e2d4c59ae 100644 --- a/src/plugins/platforms/ios/quiview_accessibility.mm +++ b/src/plugins/platforms/ios/quiview_accessibility.mm @@ -59,9 +59,11 @@ if (!iface) return; - [self createAccessibleElement: iface]; for (int i = 0; i < iface->childCount(); ++i) [self createAccessibleContainer: iface->child(i)]; + + // The container element must go last, so that it underlays all its children + [self createAccessibleElement:iface]; } - (void)initAccessibility diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp index 754ded14f1..a01104a8e3 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.cpp @@ -74,10 +74,13 @@ QT_BEGIN_NAMESPACE using namespace QWindowsUiAutomation; +QMutex QWindowsUiaMainProvider::m_mutex; // Returns a cached instance of the provider for a specific acessible interface. QWindowsUiaMainProvider *QWindowsUiaMainProvider::providerForAccessible(QAccessibleInterface *accessible) { + QMutexLocker locker(&m_mutex); + if (!accessible) return nullptr; @@ -271,6 +274,8 @@ ULONG QWindowsUiaMainProvider::AddRef() ULONG STDMETHODCALLTYPE QWindowsUiaMainProvider::Release() { + QMutexLocker locker(&m_mutex); + if (!--m_ref) { delete this; return 0; diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h index f7320388f7..8aadabd227 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiamainprovider.h @@ -47,6 +47,7 @@ #include <QtCore/qpointer.h> #include <QtCore/qsharedpointer.h> +#include <QtCore/qmutex.h> #include <QtCore/qt_windows.h> #include <QtGui/qaccessible.h> @@ -98,6 +99,7 @@ public: private: QString automationIdForAccessible(const QAccessibleInterface *accessible); ULONG m_ref; + static QMutex m_mutex; }; QT_END_NAMESPACE diff --git a/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp b/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp index c55e827a46..0483cf4263 100644 --- a/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp +++ b/src/plugins/platforms/windows/uiautomation/qwindowsuiaprovidercache.cpp @@ -75,7 +75,7 @@ void QWindowsUiaProviderCache::insert(QAccessible::Id id, QWindowsUiaBaseProvide m_providerTable[id] = provider; m_inverseTable[provider] = id; // Connects the destroyed signal to our slot, to remove deleted objects from the cache. - QObject::connect(provider, &QObject::destroyed, this, &QWindowsUiaProviderCache::objectDestroyed); + QObject::connect(provider, &QObject::destroyed, this, &QWindowsUiaProviderCache::objectDestroyed, Qt::DirectConnection); } } diff --git a/src/plugins/platforms/xcb/qxcbatom.cpp b/src/plugins/platforms/xcb/qxcbatom.cpp index 780816605a..a769ddadbd 100644 --- a/src/plugins/platforms/xcb/qxcbatom.cpp +++ b/src/plugins/platforms/xcb/qxcbatom.cpp @@ -90,6 +90,8 @@ static const char *xcb_atomnames = { "_QT_CLOSE_CONNECTION\0" + "_QT_GET_TIMESTAMP\0" + "_MOTIF_WM_HINTS\0" "DTWM_IS_RUNNING\0" diff --git a/src/plugins/platforms/xcb/qxcbatom.h b/src/plugins/platforms/xcb/qxcbatom.h index 9cf93ec314..1ce6cca573 100644 --- a/src/plugins/platforms/xcb/qxcbatom.h +++ b/src/plugins/platforms/xcb/qxcbatom.h @@ -91,6 +91,8 @@ public: // Qt/XCB specific _QT_CLOSE_CONNECTION, + _QT_GET_TIMESTAMP, + _MOTIF_WM_HINTS, DTWM_IS_RUNNING, diff --git a/src/plugins/platforms/xcb/qxcbconnection.cpp b/src/plugins/platforms/xcb/qxcbconnection.cpp index 730b2efbb9..013ca7369f 100644 --- a/src/plugins/platforms/xcb/qxcbconnection.cpp +++ b/src/plugins/platforms/xcb/qxcbconnection.cpp @@ -802,8 +802,8 @@ xcb_timestamp_t QXcbConnection::getTimestamp() { // send a dummy event to myself to get the timestamp from X server. xcb_window_t window = rootWindow(); - xcb_atom_t dummyAtom = atom(QXcbAtom::CLIP_TEMPORARY); - xcb_change_property(xcb_connection(), XCB_PROP_MODE_APPEND, window, dummyAtom, + xcb_atom_t dummyAtom = atom(QXcbAtom::_QT_GET_TIMESTAMP); + xcb_change_property(xcb_connection(), XCB_PROP_MODE_REPLACE, window, dummyAtom, XCB_ATOM_INTEGER, 32, 0, nullptr); connection()->flush(); @@ -835,8 +835,6 @@ xcb_timestamp_t QXcbConnection::getTimestamp() xcb_timestamp_t timestamp = pn->time; free(event); - xcb_delete_property(xcb_connection(), window, dummyAtom); - return timestamp; } diff --git a/src/plugins/platforms/xcb/qxcbwindow.cpp b/src/plugins/platforms/xcb/qxcbwindow.cpp index bffad0fac5..45bac8ee85 100644 --- a/src/plugins/platforms/xcb/qxcbwindow.cpp +++ b/src/plugins/platforms/xcb/qxcbwindow.cpp @@ -299,11 +299,6 @@ void QXcbWindow::create() return; } - QPlatformWindow::setGeometry(rect); - - if (platformScreen != currentScreen) - QWindowSystemInterface::handleWindowScreenChanged(window(), platformScreen->QPlatformScreen::screen()); - const QSize minimumSize = windowMinimumSize(); if (rect.width() > 0 || rect.height() > 0) { rect.setWidth(qBound(1, rect.width(), XCOORD_MAX)); @@ -315,6 +310,11 @@ void QXcbWindow::create() rect.setHeight(QHighDpi::toNativePixels(int(defaultWindowHeight), platformScreen->QPlatformScreen::screen())); } + QPlatformWindow::setGeometry(rect); + + if (platformScreen != currentScreen) + QWindowSystemInterface::handleWindowScreenChanged(window(), platformScreen->QPlatformScreen::screen()); + xcb_window_t xcb_parent_id = platformScreen->root(); if (parent()) { xcb_parent_id = static_cast<QXcbWindow *>(parent())->xcb_window(); |