Qt
Internal/Contributor docs for the Qt SDK. Note: These are NOT official API docs; those are found at https://doc.qt.io/
Loading...
Searching...
No Matches
androidcontentfileengine.cpp
Go to the documentation of this file.
1// Copyright (C) 2019 Volker Krause <vkrause@kde.org>
2// Copyright (C) 2022 The Qt Company Ltd.
3// SPDX-License-Identifier: LicenseRef-Qt-Commercial OR LGPL-3.0-only OR GPL-2.0-only OR GPL-3.0-only
4// Qt-Security score:critical reason:data-parser
5
7
8#include <QtCore/qcoreapplication.h>
9#include <QtCore/qjnienvironment.h>
10#include <QtCore/qjniobject.h>
11#include <QtCore/qurl.h>
12#include <QtCore/qdatetime.h>
13#include <QtCore/qmimedatabase.h>
14#include <QtCore/qdebug.h>
15#include <QtCore/qloggingcategory.h>
16
18
19using namespace QNativeInterface;
20using namespace Qt::StringLiterals;
21
22Q_STATIC_LOGGING_CATEGORY(lcAndroidContentFileEngine, "qt.qpa.contentfileengine")
23
24Q_DECLARE_JNI_CLASS(ParcelFileDescriptorType, "android/os/ParcelFileDescriptor");
25Q_DECLARE_JNI_CLASS(CursorType, "android/database/Cursor");
26Q_DECLARE_JNI_CLASS(QtContentFileEngine, "org/qtproject/qt/android/QtContentFileEngine");
27Q_DECLARE_JNI_CLASS(List, "java/util/List");
28
29constexpr char contentScheme[] = "content";
30constexpr char contentSchemeFull[] = "content://";
31constexpr char treeSegment[] = "tree";
32constexpr char documentSegment[] = "document";
33constexpr char childrenSegment[] = "%2Fchildren";
34
36{
37 static QJniObject contentResolver;
38 if (!contentResolver.isValid()) {
39 contentResolver = QJniObject(QNativeInterface::QAndroidApplication::context())
40 .callMethod<QtJniTypes::ContentResolver>("getContentResolver");
41 }
42
43 return contentResolver;
44}
45
49{
50 setFileName(filename);
51}
52
53bool AndroidContentFileEngine::open(QIODevice::OpenMode openMode,
54 std::optional<QFile::Permissions> permissions)
55{
56 Q_UNUSED(permissions);
57
58 if (openMode == QIODeviceBase::NotOpen)
59 return true;
60
61 if (!m_documentFile->exists() && (openMode & QIODevice::WriteOnly)) {
62 // Create the file if it doesn't exist yet
63 DocumentFilePtr parent = m_documentFile->parent();
64 if (!parent) {
65 qCWarning(lcAndroidContentFileEngine) << "Cannot create file under a null parent.";
66 return false;
67 }
68
69 if (!parent->exists() || !parent->isDirectory()) {
70 qCWarning(lcAndroidContentFileEngine)
71 << "Cannot create file, parent doesn't exist or not a directory:"
72 << parent->uri().toString();
73 return false;
74 }
75
76 const QString fileName = m_documentFile->initialName();
77 if (fileName.isEmpty()) {
78 qCWarning(lcAndroidContentFileEngine) << "Coudln't determine filename from content URI:"
79 << m_documentFile->uri().toString();
80 return false;
81 }
82
83 QMimeDatabase db;
84 QString mimeType = db.mimeTypeForFile(fileName, QMimeDatabase::MatchDefault).name();
85
86 m_documentFile = parent->createFile(mimeType, fileName);
87 if (!m_documentFile) {
88 qCWarning(lcAndroidContentFileEngine) << "Failed to create new document under parent:"
89 << parent->uri().toString();
90 return false;
91 }
92 }
93
94 using namespace QtJniTypes;
95 const QString openModeStr = (openMode & QIODevice::WriteOnly) ? "w"_L1 : "r"_L1;
96 m_pfd = QtContentFileEngine::callStaticMethod<ParcelFileDescriptorType>(
97 "openFileDescriptor",
98 contentResolverInstance().object<ContentResolver>(),
99 m_documentFile->uri().object<Uri>(),
100 openModeStr);
101
102 if (!m_pfd.isValid())
103 return false;
104
105 const auto fd = m_pfd.callMethod<jint>("getFd");
106
107 if (fd < 0) {
108 closeNativeFileDescriptor();
109 return false;
110 }
111
112 return QFSFileEngine::open(openMode, fd, QFile::DontCloseHandle);
113}
114
116{
117 closeNativeFileDescriptor();
118 return QFSFileEngine::close();
119}
120
121void AndroidContentFileEngine::closeNativeFileDescriptor()
122{
123 if (m_pfd.isValid()) {
124 m_pfd.callMethod<void>("close");
125 m_pfd = QJniObject();
126 }
127}
128
130{
131 return m_documentFile->length();
132}
133
135{
136 return m_documentFile->remove();
137}
138
139bool AndroidContentFileEngine::rename(const QString &newName)
140{
141 if (m_documentFile->rename(newName)) {
142 m_initialFile = newName;
143 return true;
144 }
145 return false;
146}
147
148bool AndroidContentFileEngine::mkdir(const QString &dirName, bool createParentDirectories,
149 std::optional<QFileDevice::Permissions> permissions) const
150{
151 Q_UNUSED(permissions)
152
153 QString tmp = dirName;
154 tmp.remove(m_initialFile);
155
156 QStringList dirParts = tmp.split(u'/');
157 dirParts.removeAll("");
158
159 if (dirParts.isEmpty())
160 return false;
161
162 auto createdDir = m_documentFile;
163 bool allDirsCreated = true;
164 for (const auto &dir : dirParts) {
165 // Find if the sub-dir already exists and then don't re-create it
166 bool subDirExists = false;
167 for (const DocumentFilePtr &subDir : m_documentFile->listFiles()) {
168 if (dir == subDir->initialName() && subDir->isDirectory()) {
169 createdDir = subDir;
170 subDirExists = true;
171 }
172 }
173
174 if (!subDirExists) {
175 createdDir = createdDir->createDirectory(dir);
176 if (!createdDir) {
177 allDirsCreated = false;
178 break;
179 }
180 }
181
182 if (!createParentDirectories)
183 break;
184 }
185
186 return allDirsCreated;
187}
188
189bool AndroidContentFileEngine::rmdir(const QString &dirName, bool recurseParentDirectories) const
190{
191 Q_UNUSED(recurseParentDirectories) // DocumentFile deletes recursively by default
192
193 return DocumentFile::parseFromAnyUri(dirName)->remove();
194}
195
197{
198 return m_documentFile->id().toUtf8();
199}
200
201QDateTime AndroidContentFileEngine::fileTime(QFile::FileTime time) const
202{
203 switch (time) {
204 case QFile::FileModificationTime:
205 return m_documentFile->lastModified();
206 break;
207 default:
208 break;
209 }
210
211 return QDateTime();
212}
213
215{
216 FileFlags flags;
217 if (!m_documentFile->exists())
218 return flags;
219
220 flags = ExistsFlag;
221 if (!m_documentFile->canRead())
222 return flags;
223
224 flags |= ReadOwnerPerm|ReadUserPerm|ReadGroupPerm|ReadOtherPerm;
225
226 if (m_documentFile->isDirectory()) {
227 flags |= DirectoryType;
228 } else {
229 flags |= FileType;
230 if (m_documentFile->canWrite())
231 flags |= WriteOwnerPerm|WriteUserPerm|WriteGroupPerm|WriteOtherPerm;
232 }
233 return type & flags;
234}
235
237{
238 switch (f) {
239 case PathName:
240 case AbsolutePathName:
241 case CanonicalPathName:
242 case DefaultName:
243 case AbsoluteName:
244 case CanonicalName:
245 return m_documentFile->uri().toString();
246 case BaseName: {
247 const QString queriedName = m_documentFile->name();
248 return queriedName.isEmpty() ? m_documentFile->initialName() : queriedName;
249 }
250 default:
251 break;
252 }
253
254 return QString();
255}
256
258AndroidContentFileEngine::beginEntryList(const QString &path, QDirListing::IteratorFlags filters,
259 const QStringList &filterNames)
260{
261 return std::make_unique<AndroidContentFileEngineIterator>(path, filters, filterNames);
262}
263
264AndroidContentFileEngineHandler::AndroidContentFileEngineHandler() = default;
266
268AndroidContentFileEngineHandler::create(const QString &fileName) const
269{
270 if (fileName.startsWith(contentScheme))
271 return std::make_unique<AndroidContentFileEngine>(fileName);
272
273 return {};
274
275}
276
278 const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
280{
281}
282
286
288{
289 if (m_index == -1 && m_files.isEmpty()) {
290 const auto currentPath = path();
291 if (currentPath.isEmpty())
292 return false;
293
294 const auto iterDoc = DocumentFile::parseFromAnyUri(currentPath);
295 if (iterDoc->isDirectory())
296 for (const auto &doc : iterDoc->listFiles())
297 m_files.append(doc);
298 if (m_files.isEmpty())
299 return false;
300 m_index = 0;
301 return true;
302 }
303
304 if (m_index < m_files.size() - 1) {
305 ++m_index;
306 return true;
307 }
308
309 return false;
310}
311
313{
314 if (m_index < 0 || m_index > m_files.size())
315 return QString();
316 return m_files.at(m_index)->name();
317}
318
320{
321 if (m_index < 0 || m_index > m_files.size())
322 return QString();
323 return m_files.at(m_index)->uri().toString();
324}
325
326// Start of Cursor
327
329{
330public:
331 explicit Cursor(const QJniObject &object)
332 : m_object{object} { }
333
335 {
336 if (m_object.isValid())
337 m_object.callMethod<void>("close");
338 }
339
340 enum Type {
341 FIELD_TYPE_NULL = 0x00000000,
342 FIELD_TYPE_INTEGER = 0x00000001,
343 FIELD_TYPE_FLOAT = 0x00000002,
344 FIELD_TYPE_STRING = 0x00000003,
345 FIELD_TYPE_BLOB = 0x00000004
346 };
347
348 QVariant data(int columnIndex) const
349 {
350 int type = m_object.callMethod<jint>("getType", columnIndex);
351 switch (type) {
352 case FIELD_TYPE_NULL:
353 return {};
354 case FIELD_TYPE_INTEGER:
355 return QVariant::fromValue(m_object.callMethod<jlong>("getLong", columnIndex));
356 case FIELD_TYPE_FLOAT:
357 return QVariant::fromValue(m_object.callMethod<jdouble>("getDouble", columnIndex));
358 case FIELD_TYPE_STRING:
359 return QVariant::fromValue(m_object.callMethod<jstring>("getString",
360 columnIndex).toString());
361 case FIELD_TYPE_BLOB: {
362 auto blob = m_object.callMethod<jbyteArray>("getBlob", columnIndex);
363 QJniEnvironment env;
364 const auto blobArray = blob.object<jbyteArray>();
365 const int size = env->GetArrayLength(blobArray);
366 const auto byteArray = env->GetByteArrayElements(blobArray, nullptr);
367 QByteArray data{reinterpret_cast<const char *>(byteArray), size};
368 env->ReleaseByteArrayElements(blobArray, byteArray, 0);
369 return QVariant::fromValue(data);
370 }
371 }
372 return {};
373 }
374
375 static std::unique_ptr<Cursor> queryUri(const QJniObject &uri,
376 const QStringList &projection = {},
377 const QString &selection = {},
378 const QStringList &selectionArgs = {},
379 const QString &sortOrder = {})
380 {
381 using namespace QtJniTypes;
382 auto cursor = QtContentFileEngine::callStaticMethod<CursorType>("query",
383 contentResolverInstance().object<ContentResolver>(),
384 uri.object<Uri>(),
385 QJniArray(projection),
386 selection.isEmpty() ? nullptr : selection,
387 QJniArray(selectionArgs),
388 sortOrder.isEmpty() ? nullptr : sortOrder);
389 if (!cursor.isValid())
390 return {};
391 return std::make_unique<Cursor>(cursor);
392 }
393
394 static QVariant queryColumn(const QJniObject &uri, const QString &column)
395 {
396 const auto query = queryUri(uri, {column});
397 if (!query)
398 return {};
399
400 if (query->rowCount() != 1 || query->columnCount() != 1)
401 return {};
402 query->moveToFirst();
403 return query->data(0);
404 }
405
406 bool isNull(int columnIndex) const
407 {
408 return m_object.callMethod<jboolean>("isNull", columnIndex);
409 }
410
411 int columnCount() const { return m_object.callMethod<jint>("getColumnCount"); }
412 int rowCount() const { return m_object.callMethod<jint>("getCount"); }
413 int row() const { return m_object.callMethod<jint>("getPosition"); }
414 bool isFirst() const { return m_object.callMethod<jboolean>("isFirst"); }
415 bool isLast() const { return m_object.callMethod<jboolean>("isLast"); }
416 bool moveToFirst() { return m_object.callMethod<jboolean>("moveToFirst"); }
417 bool moveToLast() { return m_object.callMethod<jboolean>("moveToLast"); }
418 bool moveToNext() { return m_object.callMethod<jboolean>("moveToNext"); }
419
420private:
421 QJniObject m_object;
422};
423
424// End of Cursor
425
426// Start of DocumentsContract
427
428Q_DECLARE_JNI_CLASS(DocumentsContract, "android/provider/DocumentsContract");
429
430/*!
431 *
432 * DocumentsContract Api.
433 * Check https://developer.android.com/reference/android/provider/DocumentsContract
434 * for more information.
435 *
436 * \note This does not implement all facilities of the native API.
437 *
438 */
440{
441
442namespace Document {
443const QLatin1String COLUMN_DISPLAY_NAME("_display_name");
446const QLatin1String COLUMN_LAST_MODIFIED("last_modified");
449
450constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008;
451constexpr int FLAG_SUPPORTS_DELETE = 0x00000004;
452constexpr int FLAG_SUPPORTS_MOVE = 0x00000100;
453constexpr int FLAG_SUPPORTS_RENAME = 0x00000040;
454constexpr int FLAG_SUPPORTS_WRITE = 0x00000002;
455constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200;
456
457const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory");
458} // namespace Document
459
460QString documentId(const QJniObject &uri)
461{
462 return QJniObject::callStaticMethod<jstring, QtJniTypes::Uri>(
463 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
464 "getDocumentId",
465 uri.object()).toString();
466}
467
468QString treeDocumentId(const QJniObject &uri)
469{
470 return QJniObject::callStaticMethod<jstring, QtJniTypes::Uri>(
471 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
472 "getTreeDocumentId",
473 uri.object()).toString();
474}
475
476QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId)
477{
478 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
479 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
480 "buildChildDocumentsUriUsingTree",
481 uri.object<QtJniTypes::Uri>(),
482 QJniObject::fromString(parentDocumentId).object<jstring>());
483
484}
485
486QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId)
487{
488 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
489 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
490 "buildDocumentUriUsingTree",
491 treeUri.object<QtJniTypes::Uri>(),
492 QJniObject::fromString(documentId).object<jstring>());
493}
494
495bool isDocumentUri(const QJniObject &uri)
496{
497 return QJniObject::callStaticMethod<jboolean>(
498 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
499 "isDocumentUri",
500 QNativeInterface::QAndroidApplication::context(),
501 uri.object<QtJniTypes::Uri>());
502}
503
504bool isTreeUri(const QJniObject &uri)
505{
506 return QJniObject::callStaticMethod<jboolean>(
507 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
508 "isTreeUri",
509 uri.object<QtJniTypes::Uri>());
510}
511
512QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType,
513 const QString &displayName)
514{
515 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
516 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
517 "createDocument",
518 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
519 parentDocumentUri.object<QtJniTypes::Uri>(),
520 QJniObject::fromString(mimeType).object<jstring>(),
521 QJniObject::fromString(displayName).object<jstring>());
522}
523
524bool deleteDocument(const QJniObject &documentUri)
525{
526 const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
527 if (!(flags & Document::FLAG_SUPPORTS_DELETE))
528 return {};
529
530 return QJniObject::callStaticMethod<jboolean>(
531 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
532 "deleteDocument",
533 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
534 documentUri.object<QtJniTypes::Uri>());
535}
536
537QJniObject moveDocument(const QJniObject &sourceDocumentUri,
538 const QJniObject &sourceParentDocumentUri,
539 const QJniObject &targetParentDocumentUri)
540{
541 const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt();
542 if (!(flags & Document::FLAG_SUPPORTS_MOVE))
543 return {};
544
545 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
546 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
547 "moveDocument",
548 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
549 sourceDocumentUri.object<QtJniTypes::Uri>(),
550 sourceParentDocumentUri.object<QtJniTypes::Uri>(),
551 targetParentDocumentUri.object<QtJniTypes::Uri>());
552}
553
554QJniObject renameDocument(const QJniObject &documentUri, const QString &displayName)
555{
556 const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
557 if (!(flags & Document::FLAG_SUPPORTS_RENAME))
558 return {};
559
560 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
561 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
562 "renameDocument",
563 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
564 documentUri.object<QtJniTypes::Uri>(),
565 QJniObject::fromString(displayName).object<jstring>());
566}
567} // End DocumentsContract namespace
568
569// Start of DocumentFile
570
571using namespace DocumentsContract;
572
573namespace {
574class MakeableDocumentFile : public DocumentFile
575{
576public:
577 MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {})
578 : DocumentFile(uri, QString(), parent)
579 {
580 QString uriString = uri.toString();
581
582 if (uriString.endsWith(childrenSegment)) {
583 // A URI ending with /children is a query for accessing documents under
584 // the parent tree, so the closest name would be that of the parent.
585 uriString.chop(std::size(childrenSegment) - 1);
586 }
587
588 const QString path = QUrl(uriString).path();
589 if (path.isEmpty() || path == u"/")
590 return;
591
592 int displayNameStartIndex = uriString.lastIndexOf(u'/') + 1;
593
594 const int encodedSlashPos = uriString.lastIndexOf(u"%2F");
595 if (encodedSlashPos != -1)
596 displayNameStartIndex = qMax(displayNameStartIndex, encodedSlashPos + 3);
597
598 const int encodedColonPos = uriString.lastIndexOf(u"%3A");
599 if (encodedColonPos != -1)
600 displayNameStartIndex = qMax(displayNameStartIndex, encodedColonPos + 3);
601
602 m_displayName = uriString.mid(displayNameStartIndex);
603 }
604
605 MakeableDocumentFile(const QJniObject &uri, const QString &displayName, const DocumentFilePtr &parent = {})
606 : DocumentFile(uri, displayName, parent)
607 {}
608};
609}
610
611DocumentFile::DocumentFile(const QJniObject &uri,
612 const QString &displayName,
613 const DocumentFilePtr &parent)
615 m_uri{uri},
617{}
618
619QStringList DocumentFile::getPathSegments(const QJniObject &uri)
620{
621 if (!uri.isValid())
622 return {};
623
624 const auto jSegments = uri.callMethod<QtJniTypes::List>("getPathSegments");
625 if (!jSegments.isValid())
626 return {};
627
628 QStringList segments;
629 for (int i = 0; i < jSegments.callMethod<jint>("size"); ++i)
630 segments.append(jSegments.callMethod<QJniObject>("get", i).toString());
631
632 return segments;
633}
634
635QJniObject parseUri(const QString &uri)
636{
637 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
638 QtJniTypes::Traits<QtJniTypes::Uri>::className(),
639 "parse",
640 QJniObject::fromString(uri).object<jstring>());
641}
642
643DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName)
644{
645 const QJniObject uri = parseUri(fileName);
646 if (!uri.isValid() || uri.toString().isEmpty())
647 return {};
648
649 const auto scheme = uri.callMethod<QString>("getScheme");
650 if (scheme != QLatin1String(contentScheme))
651 return std::make_shared<MakeableDocumentFile>(uri);
652
653 const auto authority = uri.callMethod<QString>("getAuthority");
654 const QStringList segments = getPathSegments(uri);
655
656 const int treeIndex = segments.indexOf(treeSegment);
657 const int docIndex = segments.indexOf(documentSegment);
658
659 DocumentFilePtr parent;
660 QJniObject parsedUri;
661
662 if (treeIndex != -1) {
663 // the segment after "tree" is the tree ID, otherwise it's malformed uri
664 if (segments.size() <= treeIndex + 1)
665 return fromSingleUri(uri);
666
667 const QString treeId = segments.at(treeIndex + 1);
668 const QString encodedTreeId = QUrl::toPercentEncoding(treeId);
669 const QString treeUriString = "%1://%2/%3/%4"_L1.arg(scheme, authority,
670 treeSegment, encodedTreeId);
671 const QJniObject treeUri = parseUri(treeUriString);
672
673 const int midIndex = (docIndex > treeIndex) ? docIndex + 1 : treeIndex + 2;
674 QString docIdOrPath = segments.mid(midIndex).join('/');
675
676 if (docIdOrPath.isEmpty())
677 return fromTreeUri(treeUri);
678
679 QString fullDocId;
680 if (docIndex > treeIndex)
681 fullDocId = docIdOrPath; // full ID
682 else
683 fullDocId = treeId + u'/' + docIdOrPath; // relative path
684
685 parsedUri = buildDocumentUriUsingTree(treeUri, fullDocId);
686
687 const int lastSlash = fullDocId.lastIndexOf('/');
688 if (lastSlash != -1) {
689 const QString parentDocId = fullDocId.left(lastSlash);
690 // Parent must be within the same tree, or be the tree root
691 if (!parentDocId.isEmpty() && parentDocId.startsWith(treeId)) {
692 QJniObject parentUri = buildDocumentUriUsingTree(treeUri, parentDocId);
693 parent = std::make_shared<MakeableDocumentFile>(parentUri);
694 }
695 }
696
697 if (!parent)
698 parent = fromTreeUri(treeUri);
699 } else if (docIndex != -1) {
700 const QString docId = segments.mid(docIndex + 1).join('/');
701 const QString encodedDocId = QUrl::toPercentEncoding(docId);
702 const QString docUriStr = "%1://%2/%3/%4"_L1.arg(scheme, authority,
703 documentSegment, encodedDocId);
704 parsedUri = parseUri(docUriStr);
705
706 const int lastSlash = docId.lastIndexOf('/');
707 if (lastSlash != -1) {
708 const QString parentDocId = docId.left(lastSlash);
709 const QString encodedParentDocId = QUrl::toPercentEncoding(parentDocId);
710 const QString parentUriStr = "%1://%2/%3/%4"_L1.arg(scheme, authority, documentSegment,
711 encodedParentDocId);
712 parent = fromSingleUri(parseUri(parentUriStr));
713 }
714 } else {
715 parsedUri = uri;
716 if (segments.size() > 1) {
717 QStringList parentSegments = segments;
718 parentSegments.removeLast();
719 const QString parendDocId = parentSegments.join('/');
720 const QString parentUriStr = "%1://%2/%3"_L1.arg(scheme, authority, parendDocId);
721 parent = fromSingleUri(parseUri(parentUriStr));
722 }
723 }
724
725 if (!parsedUri.isValid())
726 return fromSingleUri(uri);
727
728 auto docFile = std::make_shared<MakeableDocumentFile>(parsedUri);
729 if (parent)
730 docFile->m_parent = parent;
731
732 return docFile;
733}
734
735DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri)
736{
737 if (!uri.isValid())
738 return {};
739
740 return std::make_shared<MakeableDocumentFile>(uri);
741}
742
743DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri)
744{
745 if (!treeUri.isValid())
746 return {};
747
748 if (isDocumentUri(treeUri))
749 return std::make_shared<MakeableDocumentFile>(treeUri);
750
751 if (isTreeUri(treeUri)) {
752 const QString docId = treeDocumentId(treeUri);
753 if (!docId.isEmpty()) {
754 const QJniObject docUri = buildDocumentUriUsingTree(treeUri, docId);
755 return std::make_shared<MakeableDocumentFile>(docUri);
756 }
757 }
758
759 return std::make_shared<MakeableDocumentFile>(treeUri);
760}
761
762DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName)
763{
764 if (isDirectory()) {
765 const QString decodedName = QUrl::fromPercentEncoding(displayName.toUtf8());
766 return std::make_shared<MakeableDocumentFile>(
767 createDocument(m_uri, mimeType, decodedName),
768 shared_from_this());
769 }
770 return {};
771}
772
773DocumentFilePtr DocumentFile::createDirectory(const QString &displayName)
774{
775 if (isDirectory()) {
776 return std::make_shared<MakeableDocumentFile>(
777 createDocument(m_uri, Document::MIME_TYPE_DIR, displayName),
778 shared_from_this());
779 }
780 return {};
781}
782
784{
785 return m_uri;
786}
787
789{
790 return m_parent;
791}
792
794{
795 return m_displayName;
796}
797
799{
800 return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString();
801}
802
804{
805 return DocumentsContract::documentId(uri());
806}
807
809{
810 return Cursor::queryColumn(m_uri, Document::COLUMN_MIME_TYPE).toString();
811}
812
814{
815 return mimeType() == Document::MIME_TYPE_DIR;
816}
817
818bool DocumentFile::isFile() const
819{
820 const QString type = mimeType();
821 return type != Document::MIME_TYPE_DIR && !type.isEmpty();
822}
823
825{
826 return isDocumentUri(m_uri) && (Cursor::queryColumn(m_uri,
827 Document::COLUMN_FLAGS).toInt() & Document::FLAG_VIRTUAL_DOCUMENT);
828}
829
831{
832 const auto timeVariant = Cursor::queryColumn(m_uri, Document::COLUMN_LAST_MODIFIED);
833 if (timeVariant.isValid())
834 return QDateTime::fromMSecsSinceEpoch(timeVariant.toLongLong());
835 return {};
836}
837
839{
840 return Cursor::queryColumn(m_uri, Document::COLUMN_SIZE).toLongLong();
841}
842
843namespace {
844constexpr int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
845constexpr int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
846}
847
849{
850 const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
851 const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
852 m_uri.object<QtJniTypes::Uri>(),
853 FLAG_GRANT_READ_URI_PERMISSION);
854 if (selfUriPermission != 0)
855 return false;
856
857 return !mimeType().isEmpty();
858}
859
861{
862 const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
863 const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
864 m_uri.object<QtJniTypes::Uri>(),
865 FLAG_GRANT_WRITE_URI_PERMISSION);
866 if (selfUriPermission != 0)
867 return false;
868
869 const QString type = mimeType();
870 if (type.isEmpty())
871 return false;
872
873 const int flags = Cursor::queryColumn(m_uri, Document::COLUMN_FLAGS).toInt();
875 return true;
876
877 const bool supportsWrite = (flags & Document::FLAG_SUPPORTS_WRITE);
878 const bool isDir = (type == Document::MIME_TYPE_DIR);
879 const bool dirSupportsCreate = (isDir && (flags & Document::FLAG_DIR_SUPPORTS_CREATE));
880
881 return dirSupportsCreate || supportsWrite;
882}
883
885{
886 return deleteDocument(m_uri);
887}
888
889bool DocumentFile::exists() const
890{
891 return !name().isEmpty();
892}
893
895{
896 std::vector<DocumentFilePtr> res;
897 const auto childrenUri = buildChildDocumentsUriUsingTree(m_uri, documentId(m_uri));
898 const auto query = Cursor::queryUri(childrenUri, {Document::COLUMN_DOCUMENT_ID});
899 if (!query)
900 return res;
901
902 while (query->moveToNext()) {
903 const auto uri = buildDocumentUriUsingTree(m_uri, query->data(0).toString());
904 res.push_back(std::make_shared<MakeableDocumentFile>(uri, shared_from_this()));
905 }
906 return res;
907}
908
909bool DocumentFile::rename(const QString &newName)
910{
911 if (!newName.startsWith(contentSchemeFull)) {
912 // Simple rename
913 QJniObject renamedUri = renameDocument(m_uri, newName);
914 if (!renamedUri.isValid())
915 return false;
916
917 m_uri = renamedUri;
918 m_displayName = newName;
919
920 return true;
921 }
922
923 // Mixed move and rename
924 auto destDoc = parseFromAnyUri(newName);
925 if (!destDoc)
926 return false;
927
928 DocumentFilePtr destParent = destDoc->parent();
929 const QString oldName = name();
930 const QString destName = QUrl::fromPercentEncoding(QFileInfo(newName).fileName().toUtf8());
931
932 bool isMove = false;
933 if (parent() && destParent && parent()->uri().toString() != destParent->uri().toString())
934 isMove = true;
935
936 QJniObject currentUri = m_uri;
937 if (isMove) {
938 if (!parent()) // Cannot move if source parent is unknown
939 return false;
940
941 currentUri = moveDocument(m_uri, parent()->uri(), destParent->uri());
942 if (!currentUri.isValid())
943 return false;
944 }
945
946 if (oldName != destName) {
947 QJniObject renamedUri = renameDocument(currentUri, destName);
948 if (!renamedUri.isValid())
949 return false;
950
951 currentUri = renamedUri;
952 }
953
954 m_uri = currentUri;
955 m_displayName = destName;
956 if (isMove)
957 m_parent = destParent;
958
959 return true;
960}
961
962QT_END_NAMESPACE
963
964// End of DocumentFile
static QJniObject & contentResolverInstance()
constexpr char documentSegment[]
constexpr char contentSchemeFull[]
constexpr char contentScheme[]
constexpr char treeSegment[]
constexpr char childrenSegment[]
QJniObject parseUri(const QString &uri)
std::unique_ptr< QAbstractFileEngine > create(const QString &fileName) const override
If this file handler can handle fileName, this method creates a file engine and returns it wrapped in...
bool advance() override
This pure virtual function advances the iterator to the next directory entry; if the operation was su...
QString currentFilePath() const override
Returns the path to the current directory entry.
AndroidContentFileEngineIterator(const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames)
QString currentFileName() const override
This pure virtual function returns the name of the current directory entry, excluding the path.
QByteArray id() const override
qint64 size() const override
Returns the size of the file.
bool mkdir(const QString &dirName, bool createParentDirectories, std::optional< QFile::Permissions > permissions=std::nullopt) const override
Requests that the directory dirName be created with the specified permissions.
FileFlags fileFlags(FileFlags type=FileInfoAll) const override
This function should return the set of OR'd flags that are true for the file engine's file,...
QDateTime fileTime(QFile::FileTime time) const override
If time is BirthTime, return when the file was born (created).
bool rmdir(const QString &dirName, bool recurseParentDirectories) const override
Requests that the directory dirName is deleted from the file system.
IteratorUniquePtr beginEntryList(const QString &path, QDirListing::IteratorFlags filters, const QStringList &filterNames) override
Returns a QAbstractFileEngine::IteratorUniquePtr, that can be used to iterate over the entries in pat...
bool close() override
Closes the file, returning true if successful; otherwise returns false.
AndroidContentFileEngine(const QString &fileName)
bool rename(const QString &newName) override
Requests that the file be renamed to newName in the file system.
bool remove() override
Requests that the file is deleted from the file system.
QString fileName(FileName file=DefaultName) const override
Return the file engine's current file name in the format specified by file.
Cursor(const QJniObject &object)
QVariant data(int columnIndex) const
static QVariant queryColumn(const QJniObject &uri, const QString &column)
bool isNull(int columnIndex) const
static std::unique_ptr< Cursor > queryUri(const QJniObject &uri, const QStringList &projection={}, const QString &selection={}, const QStringList &selectionArgs={}, const QString &sortOrder={})
int columnCount() const
DocumentFile Api.
const DocumentFilePtr & parent() const
bool rename(const QString &newName)
const QJniObject & uri() const
DocumentFilePtr createFile(const QString &mimeType, const QString &displayName)
DocumentFilePtr createDirectory(const QString &displayName)
std::vector< DocumentFilePtr > listFiles()
QString initialName() const
DocumentFile(const QJniObject &uri, const QString &displayName, const std::shared_ptr< DocumentFile > &parent)
QDateTime lastModified() const
const QLatin1String COLUMN_LAST_MODIFIED("last_modified")
const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory")
const QLatin1String COLUMN_MIME_TYPE("mime_type")
const QLatin1String COLUMN_DISPLAY_NAME("_display_name")
const QLatin1String COLUMN_DOCUMENT_ID("document_id")
const QLatin1String COLUMN_FLAGS("flags")
const QLatin1String COLUMN_SIZE("_size")
DocumentsContract Api.
bool isDocumentUri(const QJniObject &uri)
bool isTreeUri(const QJniObject &uri)
bool deleteDocument(const QJniObject &documentUri)
QJniObject renameDocument(const QJniObject &documentUri, const QString &displayName)
QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId)
QString treeDocumentId(const QJniObject &uri)
QString documentId(const QJniObject &uri)
QJniObject moveDocument(const QJniObject &sourceDocumentUri, const QJniObject &sourceParentDocumentUri, const QJniObject &targetParentDocumentUri)
QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId)
QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType, const QString &displayName)
Q_STATIC_LOGGING_CATEGORY(lcAccessibilityCore, "qt.accessibility.core")
Q_DECLARE_JNI_CLASS(MotionEvent, "android/view/MotionEvent")