diff options
| author | Assam Boudjelthia <[email protected]> | 2025-07-14 17:55:31 +0300 |
|---|---|---|
| committer | Assam Boudjelthia <[email protected]> | 2025-08-14 00:11:41 +0300 |
| commit | f735cf7cd174c98606a03349507e3bb18589e417 (patch) | |
| tree | eb907ddb2ceec78a7a1a9f8cd19caab063b8b522 /src/plugins/platforms/android/androidcontentfileengine.cpp | |
| parent | 609725c651806e204e75f397ac60d98c23e8ba3c (diff) | |
Android: rework and fix issues with content file engine
Rework parts of the content file engine handling especially
when parsing the paths to get valid uri that can be used
seemlessly with DocumentsContract. This fixes few issues
with the content uri handling and allow more operations to
be done without awkward workarounds:
* The main issue was parsing the URI and creating a valid
DocumentFile and parent if applicable, this was not handled
quite properly before. So now, each case of a URI type is
delt with separately (e.g. document, tree, mixed, children
URIs) all while making sure to keep proper encoding while
parsing the URI.
* open() was getting involved into details like the path
structure and not relying on DocumentFile implementation.
* Handle file names, especially in cases where the file is not
yet created and doing query() would fail, so in such cases
initialize the DocumentFile with an initial name as fallback.
* Moving operations were always failing due to the various
name and path string manipulations, and not properly relying
on the DocumentFile child/parent implementation.
Pick-to: 6.10 6.9 6.8
Fixes: QTBUG-124011
Fixes: QTBUG-115143
Fixes: QTBUG-114979
Change-Id: I949f84fc658bf0588ebe239b07e6111d5b3b0cce
Reviewed-by: Ville Voutilainen <[email protected]>
Diffstat (limited to 'src/plugins/platforms/android/androidcontentfileengine.cpp')
| -rw-r--r-- | src/plugins/platforms/android/androidcontentfileengine.cpp | 363 |
1 files changed, 252 insertions, 111 deletions
diff --git a/src/plugins/platforms/android/androidcontentfileengine.cpp b/src/plugins/platforms/android/androidcontentfileengine.cpp index 46d78c688c5..da81035d9f4 100644 --- a/src/plugins/platforms/android/androidcontentfileengine.cpp +++ b/src/plugins/platforms/android/androidcontentfileengine.cpp @@ -11,15 +11,26 @@ #include <QtCore/qurl.h> #include <QtCore/qdatetime.h> #include <QtCore/qmimedatabase.h> +#include <QtCore/qdebug.h> +#include <QtCore/qloggingcategory.h> QT_BEGIN_NAMESPACE using namespace QNativeInterface; using namespace Qt::StringLiterals; +Q_STATIC_LOGGING_CATEGORY(lcAndroidContentFileEngine, "qt.qpa.contentfileengine") + Q_DECLARE_JNI_CLASS(ParcelFileDescriptorType, "android/os/ParcelFileDescriptor"); Q_DECLARE_JNI_CLASS(CursorType, "android/database/Cursor"); Q_DECLARE_JNI_CLASS(QtContentFileEngine, "org/qtproject/qt/android/QtContentFileEngine"); +Q_DECLARE_JNI_CLASS(List, "java/util/List"); + +constexpr char contentScheme[] = "content"; +constexpr char contentSchemeFull[] = "content://"; +constexpr char treeSegment[] = "tree"; +constexpr char documentSegment[] = "document"; +constexpr char childrenSegment[] = "%2Fchildren"; static QJniObject &contentResolverInstance() { @@ -43,41 +54,45 @@ bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode, std::optional<QFile::Permissions> permissions) { Q_UNUSED(permissions); - QString openModeStr; - if (openMode & QFileDevice::ReadOnly) { - openModeStr += u'r'; - } - if (openMode & QFileDevice::WriteOnly) { - openModeStr += u'w'; - if (!m_documentFile->exists()) { - if (QUrl(m_initialFile).path().startsWith("/tree/"_L1)) { - const int lastSeparatorIndex = m_initialFile.lastIndexOf('/'); - const QString fileName = m_initialFile.mid(lastSeparatorIndex + 1); - - QString mimeType; - const auto mimeTypes = QMimeDatabase().mimeTypesForFileName(fileName); - if (!mimeTypes.empty()) - mimeType = mimeTypes.first().name(); - else - mimeType = "application/octet-stream"; - - if (m_documentFile->parent()) { - auto createdFile = m_documentFile->parent()->createFile(mimeType, fileName); - if (createdFile) - m_documentFile = createdFile; - } - } else { - qWarning() << "open(): non-existent content URI with a document type provided"; - } + + if (openMode == QIODeviceBase::NotOpen) + return true; + + if (!m_documentFile->exists() && (openMode & QIODevice::WriteOnly)) { + // Create the file if it doesn't exist yet + DocumentFilePtr parent = m_documentFile->parent(); + if (!parent) { + qCWarning(lcAndroidContentFileEngine) << "Cannot create file under a null parent."; + return false; + } + + if (!parent->exists() || !parent->isDirectory()) { + qCWarning(lcAndroidContentFileEngine) + << "Cannot create file, parent doesn't exist or not a directory:" + << parent->uri().toString(); + return false; + } + + const QString fileName = m_documentFile->initialName(); + if (fileName.isEmpty()) { + qCWarning(lcAndroidContentFileEngine) << "Coudln't determine filename from content URI:" + << m_documentFile->uri().toString(); + return false; + } + + QMimeDatabase db; + QString mimeType = db.mimeTypeForFile(fileName, QMimeDatabase::MatchDefault).name(); + + m_documentFile = parent->createFile(mimeType, fileName); + if (!m_documentFile) { + qCWarning(lcAndroidContentFileEngine) << "Failed to create new document under parent:" + << parent->uri().toString(); + return false; } - } - if (openMode & QFileDevice::Truncate) { - openModeStr += u't'; - } else if (openMode & QFileDevice::Append) { - openModeStr += u'a'; } using namespace QtJniTypes; + const QString openModeStr = (openMode & QIODevice::WriteOnly) ? "w"_L1 : "r"_L1; m_pfd = QtContentFileEngine::callStaticMethod<ParcelFileDescriptorType>( "openFileDescriptor", contentResolverInstance().object<ContentResolver>(), @@ -124,7 +139,7 @@ bool AndroidContentFileEngine::remove() bool AndroidContentFileEngine::rename(const QString &newName) { if (m_documentFile->rename(newName)) { - m_initialFile = m_documentFile->uri().toString(); + m_initialFile = newName; return true; } return false; @@ -150,7 +165,7 @@ bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDi // 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()) { + if (dir == subDir->initialName() && subDir->isDirectory()) { createdDir = subDir; subDirExists = true; } @@ -173,19 +188,9 @@ bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDi 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; - } - } + Q_UNUSED(recurseParentDirectories) // DocumentFile deletes recursively by default - return deleted; + return DocumentFile::parseFromAnyUri(dirName)->remove(); } QByteArray AndroidContentFileEngine::id() const @@ -238,8 +243,10 @@ QString AndroidContentFileEngine::fileName(FileName f) const case AbsoluteName: case CanonicalName: return m_documentFile->uri().toString(); - case BaseName: - return m_documentFile->name(); + case BaseName: { + const QString queriedName = m_documentFile->name(); + return queriedName.isEmpty() ? m_documentFile->initialName() : queriedName; + } default: break; } @@ -260,7 +267,7 @@ AndroidContentFileEngineHandler::~AndroidContentFileEngineHandler() = default; std::unique_ptr<QAbstractFileEngine> AndroidContentFileEngineHandler::create(const QString &fileName) const { - if (fileName.startsWith("content"_L1)) + if (fileName.startsWith(contentScheme)) return std::make_unique<AndroidContentFileEngine>(fileName); return {}; @@ -568,81 +575,196 @@ class MakeableDocumentFile : public DocumentFile { public: MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {}) - : DocumentFile(uri, parent) + : DocumentFile(uri, QString(), parent) + { + QString uriString = uri.toString(); + + if (uriString.endsWith(childrenSegment)) { + // A URI ending with /children is a query for accessing documents under + // the parent tree, so the closest name would be that of the parent. + uriString.chop(std::size(childrenSegment) - 1); + } + + const QString path = QUrl(uriString).path(); + if (path.isEmpty() || path == u"/") + return; + + int displayNameStartIndex = uriString.lastIndexOf(u'/') + 1; + + const int encodedSlashPos = uriString.lastIndexOf(u"%2F"); + if (encodedSlashPos != -1) + displayNameStartIndex = qMax(displayNameStartIndex, encodedSlashPos + 3); + + const int encodedColonPos = uriString.lastIndexOf(u"%3A"); + if (encodedColonPos != -1) + displayNameStartIndex = qMax(displayNameStartIndex, encodedColonPos + 3); + + m_displayName = uriString.mid(displayNameStartIndex); + } + + MakeableDocumentFile(const QJniObject &uri, const QString &displayName, const DocumentFilePtr &parent = {}) + : DocumentFile(uri, displayName, parent) {} }; } DocumentFile::DocumentFile(const QJniObject &uri, + const QString &displayName, const DocumentFilePtr &parent) - : m_uri{uri} - , m_parent{parent} + : m_displayName{displayName}, + m_uri{uri}, + m_parent{parent} {} -QJniObject parseUri(const QString &uri) +QStringList DocumentFile::getPathSegments(const QJniObject &uri) { - QString uriToParse = uri; - if (uriToParse.contains(' ')) - uriToParse.replace(' ', QUrl::toPercentEncoding(" ")); + if (!uri.isValid()) + return {}; + + const auto jSegments = uri.callMethod<QtJniTypes::List>("getPathSegments"); + if (!jSegments.isValid()) + return {}; + + QStringList segments; + for (int i = 0; i < jSegments.callMethod<jint>("size"); ++i) + segments.append(jSegments.callMethod<QJniObject>("get", i).toString()); + return segments; +} + +QJniObject parseUri(const QString &uri) +{ return QJniObject::callStaticMethod<QtJniTypes::Uri>( QtJniTypes::Traits<QtJniTypes::Uri>::className(), "parse", - QJniObject::fromString(uriToParse).object<jstring>()); + QJniObject::fromString(uri).object<jstring>()); } DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName) { - const QString encodedUri = QUrl(fileName).toEncoded(); - const QJniObject uri = parseUri(encodedUri); + const QJniObject uri = parseUri(fileName); + if (!uri.isValid() || uri.toString().isEmpty()) + return {}; - if (DocumentsContract::isDocumentUri(uri) || !DocumentsContract::isTreeUri(uri)) - return fromSingleUri(uri); + const auto scheme = uri.callMethod<QString>("getScheme"); + if (scheme != QLatin1String(contentScheme)) + return std::make_shared<MakeableDocumentFile>(uri); - const QString documentType = "/document/"_L1; - const QString treeType = "/tree/"_L1; + const auto authority = uri.callMethod<QString>("getAuthority"); + const QStringList segments = getPathSegments(uri); - const int treeIndex = encodedUri.indexOf(treeType); - const int documentIndex = encodedUri.indexOf(documentType); - const int index = fileName.lastIndexOf("/"); + const int treeIndex = segments.indexOf(treeSegment); + const int docIndex = segments.indexOf(documentSegment); - if (index <= treeIndex + treeType.size() || index <= documentIndex + documentType.size()) - return fromTreeUri(uri); + DocumentFilePtr parent; + QJniObject parsedUri; - const QString parentUrl = encodedUri.left(index); - DocumentFilePtr parentDocFile = fromTreeUri(parseUri(parentUrl)); + if (treeIndex != -1) { + // the segment after "tree" is the tree ID, otherwise it's malformed uri + if (segments.size() <= treeIndex + 1) + return fromSingleUri(uri); - const QString baseName = encodedUri.mid(index); - const QString fileUrl = parentUrl + QUrl::toPercentEncoding(baseName); + const QString treeId = segments.at(treeIndex + 1); + const QString encodedTreeId = QUrl::toPercentEncoding(treeId); + const QString treeUriString = "%1://%2/%3/%4"_L1.arg(scheme, authority, + treeSegment, encodedTreeId); + const QJniObject treeUri = parseUri(treeUriString); - DocumentFilePtr docFile = std::make_shared<MakeableDocumentFile>(parseUri(fileUrl)); - if (parentDocFile && parentDocFile->isDirectory()) - docFile->m_parent = parentDocFile; + const int midIndex = (docIndex > treeIndex) ? docIndex + 1 : treeIndex + 2; + QString docIdOrPath = segments.mid(midIndex).join('/'); + + if (docIdOrPath.isEmpty()) + return fromTreeUri(treeUri); + + QString fullDocId; + if (docIndex > treeIndex) + fullDocId = docIdOrPath; // full ID + else + fullDocId = treeId + u'/' + docIdOrPath; // relative path + + parsedUri = buildDocumentUriUsingTree(treeUri, fullDocId); + + const int lastSlash = fullDocId.lastIndexOf('/'); + if (lastSlash != -1) { + const QString parentDocId = fullDocId.left(lastSlash); + // Parent must be within the same tree, or be the tree root + if (!parentDocId.isEmpty() && parentDocId.startsWith(treeId)) { + QJniObject parentUri = buildDocumentUriUsingTree(treeUri, parentDocId); + parent = std::make_shared<MakeableDocumentFile>(parentUri); + } + } + + if (!parent) + parent = fromTreeUri(treeUri); + } else if (docIndex != -1) { + const QString docId = segments.mid(docIndex + 1).join('/'); + const QString encodedDocId = QUrl::toPercentEncoding(docId); + const QString docUriStr = "%1://%2/%3/%4"_L1.arg(scheme, authority, + documentSegment, encodedDocId); + parsedUri = parseUri(docUriStr); + + const int lastSlash = docId.lastIndexOf('/'); + if (lastSlash != -1) { + const QString parentDocId = docId.left(lastSlash); + const QString encodedParentDocId = QUrl::toPercentEncoding(parentDocId); + const QString parentUriStr = "%1://%2/%3/%4"_L1.arg(scheme, authority, documentSegment, + encodedParentDocId); + parent = fromSingleUri(parseUri(parentUriStr)); + } + } else { + parsedUri = uri; + if (segments.size() > 1) { + QStringList parentSegments = segments; + parentSegments.removeLast(); + const QString parendDocId = parentSegments.join('/'); + const QString parentUriStr = "%1://%2/%3"_L1.arg(scheme, authority, parendDocId); + parent = fromSingleUri(parseUri(parentUriStr)); + } + } + + if (!parsedUri.isValid()) + return fromSingleUri(uri); + + auto docFile = std::make_shared<MakeableDocumentFile>(parsedUri); + if (parent) + docFile->m_parent = parent; return docFile; } DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri) { + if (!uri.isValid()) + return {}; + return std::make_shared<MakeableDocumentFile>(uri); } DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri) { - QString docId; + if (!treeUri.isValid()) + return {}; + if (isDocumentUri(treeUri)) - docId = documentId(treeUri); - else - docId = treeDocumentId(treeUri); + return std::make_shared<MakeableDocumentFile>(treeUri); - return std::make_shared<MakeableDocumentFile>(buildDocumentUriUsingTree(treeUri, docId)); + if (isTreeUri(treeUri)) { + const QString docId = treeDocumentId(treeUri); + if (!docId.isEmpty()) { + const QJniObject docUri = buildDocumentUriUsingTree(treeUri, docId); + return std::make_shared<MakeableDocumentFile>(docUri); + } + } + + return std::make_shared<MakeableDocumentFile>(treeUri); } DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName) { if (isDirectory()) { + const QString decodedName = QUrl::fromPercentEncoding(displayName.toUtf8()); return std::make_shared<MakeableDocumentFile>( - createDocument(m_uri, mimeType, displayName), + createDocument(m_uri, mimeType, decodedName), shared_from_this()); } return {}; @@ -668,6 +790,11 @@ const DocumentFilePtr &DocumentFile::parent() const return m_parent; } +QString DocumentFile::initialName() const +{ + return m_displayName; +} + QString DocumentFile::name() const { return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString(); @@ -781,41 +908,55 @@ std::vector<DocumentFilePtr> DocumentFile::listFiles() bool DocumentFile::rename(const QString &newName) { - QJniObject uri; - if (newName.startsWith("content://"_L1)) { - auto lastSeparatorIndex = [](const QString &file) { - int posDecoded = file.lastIndexOf("/"); - int posEncoded = file.lastIndexOf(QUrl::toPercentEncoding("/")); - return posEncoded > posDecoded ? posEncoded : posDecoded; - }; - - // first try to see if the new file is under the same tree and thus used rename only - const QString parent = m_uri.toString().left(lastSeparatorIndex(m_uri.toString())); - if (newName.contains(parent)) { - QString displayName = newName.mid(lastSeparatorIndex(newName)); - if (displayName.startsWith('/')) - displayName.remove(0, 1); - else if (displayName.startsWith(QUrl::toPercentEncoding("/"))) - displayName.remove(0, 3); - - uri = renameDocument(m_uri, displayName); - } else { - // Move - QJniObject srcParentUri = fromTreeUri(parseUri(parent))->uri(); - const QString destParent = newName.left(lastSeparatorIndex(newName)); - QJniObject targetParentUri = fromTreeUri(parseUri(destParent))->uri(); - uri = moveDocument(m_uri, srcParentUri, targetParentUri); - } - } else { - uri = renameDocument(m_uri, newName); - } + if (!newName.startsWith(contentSchemeFull)) { + // Simple rename + QJniObject renamedUri = renameDocument(m_uri, newName); + if (!renamedUri.isValid()) + return false; + + m_uri = renamedUri; + m_displayName = newName; - if (uri.isValid()) { - m_uri = uri; return true; } - return false; + // Mixed move and rename + auto destDoc = parseFromAnyUri(newName); + if (!destDoc) + return false; + + DocumentFilePtr destParent = destDoc->parent(); + const QString oldName = name(); + const QString destName = QUrl::fromPercentEncoding(QFileInfo(newName).fileName().toUtf8()); + + bool isMove = false; + if (parent() && destParent && parent()->uri().toString() != destParent->uri().toString()) + isMove = true; + + QJniObject currentUri = m_uri; + if (isMove) { + if (!parent()) // Cannot move if source parent is unknown + return false; + + currentUri = moveDocument(m_uri, parent()->uri(), destParent->uri()); + if (!currentUri.isValid()) + return false; + } + + if (oldName != destName) { + QJniObject renamedUri = renameDocument(currentUri, destName); + if (!renamedUri.isValid()) + return false; + + currentUri = renamedUri; + } + + m_uri = currentUri; + m_displayName = destName; + if (isMove) + m_parent = destParent; + + return true; } QT_END_NAMESPACE |
