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 return QVariant::fromValue(m_object.callMethod<QByteArray>("getBlob", columnIndex));
363 }
364 return {};
365 }
366
367 static std::unique_ptr<Cursor> queryUri(const QJniObject &uri,
368 const QStringList &projection = {},
369 const QString &selection = {},
370 const QStringList &selectionArgs = {},
371 const QString &sortOrder = {})
372 {
373 using namespace QtJniTypes;
374 auto cursor = QtContentFileEngine::callStaticMethod<CursorType>("query",
375 contentResolverInstance().object<ContentResolver>(),
376 uri.object<Uri>(),
377 QJniArray(projection),
378 selection.isEmpty() ? nullptr : selection,
379 QJniArray(selectionArgs),
380 sortOrder.isEmpty() ? nullptr : sortOrder);
381 if (!cursor.isValid())
382 return {};
383 return std::make_unique<Cursor>(cursor);
384 }
385
386 static QVariant queryColumn(const QJniObject &uri, const QString &column)
387 {
388 const auto query = queryUri(uri, {column});
389 if (!query)
390 return {};
391
392 if (query->rowCount() != 1 || query->columnCount() != 1)
393 return {};
394 query->moveToFirst();
395 return query->data(0);
396 }
397
398 bool isNull(int columnIndex) const
399 {
400 return m_object.callMethod<jboolean>("isNull", columnIndex);
401 }
402
403 int columnCount() const { return m_object.callMethod<jint>("getColumnCount"); }
404 int rowCount() const { return m_object.callMethod<jint>("getCount"); }
405 int row() const { return m_object.callMethod<jint>("getPosition"); }
406 bool isFirst() const { return m_object.callMethod<jboolean>("isFirst"); }
407 bool isLast() const { return m_object.callMethod<jboolean>("isLast"); }
408 bool moveToFirst() { return m_object.callMethod<jboolean>("moveToFirst"); }
409 bool moveToLast() { return m_object.callMethod<jboolean>("moveToLast"); }
410 bool moveToNext() { return m_object.callMethod<jboolean>("moveToNext"); }
411
412private:
413 QJniObject m_object;
414};
415
416// End of Cursor
417
418// Start of DocumentsContract
419
420Q_DECLARE_JNI_CLASS(DocumentsContract, "android/provider/DocumentsContract");
421
422/*!
423 *
424 * DocumentsContract Api.
425 * Check https://developer.android.com/reference/android/provider/DocumentsContract
426 * for more information.
427 *
428 * \note This does not implement all facilities of the native API.
429 *
430 */
432{
433
434namespace Document {
435const QLatin1String COLUMN_DISPLAY_NAME("_display_name");
438const QLatin1String COLUMN_LAST_MODIFIED("last_modified");
441
442constexpr int FLAG_DIR_SUPPORTS_CREATE = 0x00000008;
443constexpr int FLAG_SUPPORTS_DELETE = 0x00000004;
444constexpr int FLAG_SUPPORTS_MOVE = 0x00000100;
445constexpr int FLAG_SUPPORTS_RENAME = 0x00000040;
446constexpr int FLAG_SUPPORTS_WRITE = 0x00000002;
447constexpr int FLAG_VIRTUAL_DOCUMENT = 0x00000200;
448
449const QLatin1String MIME_TYPE_DIR("vnd.android.document/directory");
450} // namespace Document
451
452QString documentId(const QJniObject &uri)
453{
454 return QJniObject::callStaticMethod<jstring, QtJniTypes::Uri>(
455 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
456 "getDocumentId",
457 uri.object()).toString();
458}
459
460QString treeDocumentId(const QJniObject &uri)
461{
462 return QJniObject::callStaticMethod<jstring, QtJniTypes::Uri>(
463 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
464 "getTreeDocumentId",
465 uri.object()).toString();
466}
467
468QJniObject buildChildDocumentsUriUsingTree(const QJniObject &uri, const QString &parentDocumentId)
469{
470 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
471 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
472 "buildChildDocumentsUriUsingTree",
473 uri.object<QtJniTypes::Uri>(),
474 QJniObject::fromString(parentDocumentId).object<jstring>());
475
476}
477
478QJniObject buildDocumentUriUsingTree(const QJniObject &treeUri, const QString &documentId)
479{
480 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
481 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
482 "buildDocumentUriUsingTree",
483 treeUri.object<QtJniTypes::Uri>(),
484 QJniObject::fromString(documentId).object<jstring>());
485}
486
487bool isDocumentUri(const QJniObject &uri)
488{
489 return QJniObject::callStaticMethod<jboolean>(
490 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
491 "isDocumentUri",
492 QNativeInterface::QAndroidApplication::context(),
493 uri.object<QtJniTypes::Uri>());
494}
495
496bool isTreeUri(const QJniObject &uri)
497{
498 return QJniObject::callStaticMethod<jboolean>(
499 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
500 "isTreeUri",
501 uri.object<QtJniTypes::Uri>());
502}
503
504QJniObject createDocument(const QJniObject &parentDocumentUri, const QString &mimeType,
505 const QString &displayName)
506{
507 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
508 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
509 "createDocument",
510 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
511 parentDocumentUri.object<QtJniTypes::Uri>(),
512 QJniObject::fromString(mimeType).object<jstring>(),
513 QJniObject::fromString(displayName).object<jstring>());
514}
515
516bool deleteDocument(const QJniObject &documentUri)
517{
518 const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
519 if (!(flags & Document::FLAG_SUPPORTS_DELETE))
520 return {};
521
522 return QJniObject::callStaticMethod<jboolean>(
523 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
524 "deleteDocument",
525 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
526 documentUri.object<QtJniTypes::Uri>());
527}
528
529QJniObject moveDocument(const QJniObject &sourceDocumentUri,
530 const QJniObject &sourceParentDocumentUri,
531 const QJniObject &targetParentDocumentUri)
532{
533 const int flags = Cursor::queryColumn(sourceDocumentUri, Document::COLUMN_FLAGS).toInt();
534 if (!(flags & Document::FLAG_SUPPORTS_MOVE))
535 return {};
536
537 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
538 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
539 "moveDocument",
540 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
541 sourceDocumentUri.object<QtJniTypes::Uri>(),
542 sourceParentDocumentUri.object<QtJniTypes::Uri>(),
543 targetParentDocumentUri.object<QtJniTypes::Uri>());
544}
545
546QJniObject renameDocument(const QJniObject &documentUri, const QString &displayName)
547{
548 const int flags = Cursor::queryColumn(documentUri, Document::COLUMN_FLAGS).toInt();
549 if (!(flags & Document::FLAG_SUPPORTS_RENAME))
550 return {};
551
552 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
553 QtJniTypes::Traits<QtJniTypes::DocumentsContract>::className(),
554 "renameDocument",
555 contentResolverInstance().object<QtJniTypes::ContentResolver>(),
556 documentUri.object<QtJniTypes::Uri>(),
557 QJniObject::fromString(displayName).object<jstring>());
558}
559} // End DocumentsContract namespace
560
561// Start of DocumentFile
562
563using namespace DocumentsContract;
564
565namespace {
566class MakeableDocumentFile : public DocumentFile
567{
568public:
569 MakeableDocumentFile(const QJniObject &uri, const DocumentFilePtr &parent = {})
570 : DocumentFile(uri, QString(), parent)
571 {
572 QString uriString = uri.toString();
573
574 if (uriString.endsWith(childrenSegment)) {
575 // A URI ending with /children is a query for accessing documents under
576 // the parent tree, so the closest name would be that of the parent.
577 uriString.chop(std::size(childrenSegment) - 1);
578 }
579
580 const QString path = QUrl(uriString).path();
581 if (path.isEmpty() || path == u"/")
582 return;
583
584 int displayNameStartIndex = uriString.lastIndexOf(u'/') + 1;
585
586 const int encodedSlashPos = uriString.lastIndexOf(u"%2F");
587 if (encodedSlashPos != -1)
588 displayNameStartIndex = qMax(displayNameStartIndex, encodedSlashPos + 3);
589
590 const int encodedColonPos = uriString.lastIndexOf(u"%3A");
591 if (encodedColonPos != -1)
592 displayNameStartIndex = qMax(displayNameStartIndex, encodedColonPos + 3);
593
594 m_displayName = uriString.mid(displayNameStartIndex);
595 }
596
597 MakeableDocumentFile(const QJniObject &uri, const QString &displayName, const DocumentFilePtr &parent = {})
598 : DocumentFile(uri, displayName, parent)
599 {}
600};
601}
602
603DocumentFile::DocumentFile(const QJniObject &uri,
604 const QString &displayName,
605 const DocumentFilePtr &parent)
607 m_uri{uri},
609{}
610
611QStringList DocumentFile::getPathSegments(const QJniObject &uri)
612{
613 if (!uri.isValid())
614 return {};
615
616 const auto jSegments = uri.callMethod<QtJniTypes::List>("getPathSegments");
617 if (!jSegments.isValid())
618 return {};
619
620 QStringList segments;
621 for (int i = 0; i < jSegments.callMethod<jint>("size"); ++i)
622 segments.append(jSegments.callMethod<QJniObject>("get", i).toString());
623
624 return segments;
625}
626
627QJniObject parseUri(const QString &uri)
628{
629 return QJniObject::callStaticMethod<QtJniTypes::Uri>(
630 QtJniTypes::Traits<QtJniTypes::Uri>::className(),
631 "parse",
632 QJniObject::fromString(uri).object<jstring>());
633}
634
635DocumentFilePtr DocumentFile::parseFromAnyUri(const QString &fileName)
636{
637 const QJniObject uri = parseUri(fileName);
638 if (!uri.isValid() || uri.toString().isEmpty())
639 return {};
640
641 const auto scheme = uri.callMethod<QString>("getScheme");
642 if (scheme != QLatin1String(contentScheme))
643 return std::make_shared<MakeableDocumentFile>(uri);
644
645 const auto authority = uri.callMethod<QString>("getAuthority");
646 const QStringList segments = getPathSegments(uri);
647
648 const int treeIndex = segments.indexOf(treeSegment);
649 const int docIndex = segments.indexOf(documentSegment);
650
651 DocumentFilePtr parent;
652 QJniObject parsedUri;
653
654 if (treeIndex != -1) {
655 // the segment after "tree" is the tree ID, otherwise it's malformed uri
656 if (segments.size() <= treeIndex + 1)
657 return fromSingleUri(uri);
658
659 const QString treeId = segments.at(treeIndex + 1);
660 const QString encodedTreeId = QUrl::toPercentEncoding(treeId);
661 const QString treeUriString = "%1://%2/%3/%4"_L1.arg(scheme, authority,
662 treeSegment, encodedTreeId);
663 const QJniObject treeUri = parseUri(treeUriString);
664
665 const int midIndex = (docIndex > treeIndex) ? docIndex + 1 : treeIndex + 2;
666 QString docIdOrPath = segments.mid(midIndex).join('/');
667
668 if (docIdOrPath.isEmpty())
669 return fromTreeUri(treeUri);
670
671 QString fullDocId;
672 if (docIndex > treeIndex)
673 fullDocId = docIdOrPath; // full ID
674 else
675 fullDocId = treeId + u'/' + docIdOrPath; // relative path
676
677 parsedUri = buildDocumentUriUsingTree(treeUri, fullDocId);
678
679 const int lastSlash = fullDocId.lastIndexOf('/');
680 if (lastSlash != -1) {
681 const QString parentDocId = fullDocId.left(lastSlash);
682 // Parent must be within the same tree, or be the tree root
683 if (!parentDocId.isEmpty() && parentDocId.startsWith(treeId)) {
684 QJniObject parentUri = buildDocumentUriUsingTree(treeUri, parentDocId);
685 parent = std::make_shared<MakeableDocumentFile>(parentUri);
686 }
687 }
688
689 if (!parent)
690 parent = fromTreeUri(treeUri);
691 } else if (docIndex != -1) {
692 const QString docId = segments.mid(docIndex + 1).join('/');
693 const QString encodedDocId = QUrl::toPercentEncoding(docId);
694 const QString docUriStr = "%1://%2/%3/%4"_L1.arg(scheme, authority,
695 documentSegment, encodedDocId);
696 parsedUri = parseUri(docUriStr);
697
698 const int lastSlash = docId.lastIndexOf('/');
699 if (lastSlash != -1) {
700 const QString parentDocId = docId.left(lastSlash);
701 const QString encodedParentDocId = QUrl::toPercentEncoding(parentDocId);
702 const QString parentUriStr = "%1://%2/%3/%4"_L1.arg(scheme, authority, documentSegment,
703 encodedParentDocId);
704 parent = fromSingleUri(parseUri(parentUriStr));
705 }
706 } else {
707 parsedUri = uri;
708 if (segments.size() > 1) {
709 QStringList parentSegments = segments;
710 parentSegments.removeLast();
711 const QString parendDocId = parentSegments.join('/');
712 const QString parentUriStr = "%1://%2/%3"_L1.arg(scheme, authority, parendDocId);
713 parent = fromSingleUri(parseUri(parentUriStr));
714 }
715 }
716
717 if (!parsedUri.isValid())
718 return fromSingleUri(uri);
719
720 auto docFile = std::make_shared<MakeableDocumentFile>(parsedUri);
721 if (parent)
722 docFile->m_parent = parent;
723
724 return docFile;
725}
726
727DocumentFilePtr DocumentFile::fromSingleUri(const QJniObject &uri)
728{
729 if (!uri.isValid())
730 return {};
731
732 return std::make_shared<MakeableDocumentFile>(uri);
733}
734
735DocumentFilePtr DocumentFile::fromTreeUri(const QJniObject &treeUri)
736{
737 if (!treeUri.isValid())
738 return {};
739
740 if (isDocumentUri(treeUri))
741 return std::make_shared<MakeableDocumentFile>(treeUri);
742
743 if (isTreeUri(treeUri)) {
744 const QString docId = treeDocumentId(treeUri);
745 if (!docId.isEmpty()) {
746 const QJniObject docUri = buildDocumentUriUsingTree(treeUri, docId);
747 return std::make_shared<MakeableDocumentFile>(docUri);
748 }
749 }
750
751 return std::make_shared<MakeableDocumentFile>(treeUri);
752}
753
754DocumentFilePtr DocumentFile::createFile(const QString &mimeType, const QString &displayName)
755{
756 if (isDirectory()) {
757 const QString decodedName = QUrl::fromPercentEncoding(displayName.toUtf8());
758 return std::make_shared<MakeableDocumentFile>(
759 createDocument(m_uri, mimeType, decodedName),
760 shared_from_this());
761 }
762 return {};
763}
764
765DocumentFilePtr DocumentFile::createDirectory(const QString &displayName)
766{
767 if (isDirectory()) {
768 return std::make_shared<MakeableDocumentFile>(
769 createDocument(m_uri, Document::MIME_TYPE_DIR, displayName),
770 shared_from_this());
771 }
772 return {};
773}
774
776{
777 return m_uri;
778}
779
781{
782 return m_parent;
783}
784
786{
787 return m_displayName;
788}
789
791{
792 return Cursor::queryColumn(m_uri, Document::COLUMN_DISPLAY_NAME).toString();
793}
794
796{
797 return DocumentsContract::documentId(uri());
798}
799
801{
802 return Cursor::queryColumn(m_uri, Document::COLUMN_MIME_TYPE).toString();
803}
804
806{
807 return mimeType() == Document::MIME_TYPE_DIR;
808}
809
810bool DocumentFile::isFile() const
811{
812 const QString type = mimeType();
813 return type != Document::MIME_TYPE_DIR && !type.isEmpty();
814}
815
817{
818 return isDocumentUri(m_uri) && (Cursor::queryColumn(m_uri,
819 Document::COLUMN_FLAGS).toInt() & Document::FLAG_VIRTUAL_DOCUMENT);
820}
821
823{
824 const auto timeVariant = Cursor::queryColumn(m_uri, Document::COLUMN_LAST_MODIFIED);
825 if (timeVariant.isValid())
826 return QDateTime::fromMSecsSinceEpoch(timeVariant.toLongLong());
827 return {};
828}
829
831{
832 return Cursor::queryColumn(m_uri, Document::COLUMN_SIZE).toLongLong();
833}
834
835namespace {
836constexpr int FLAG_GRANT_READ_URI_PERMISSION = 0x00000001;
837constexpr int FLAG_GRANT_WRITE_URI_PERMISSION = 0x00000002;
838}
839
841{
842 const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
843 const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
844 m_uri.object<QtJniTypes::Uri>(),
845 FLAG_GRANT_READ_URI_PERMISSION);
846 if (selfUriPermission != 0)
847 return false;
848
849 return !mimeType().isEmpty();
850}
851
853{
854 const auto context = QJniObject(QNativeInterface::QAndroidApplication::context());
855 const bool selfUriPermission = context.callMethod<jint>("checkCallingOrSelfUriPermission",
856 m_uri.object<QtJniTypes::Uri>(),
857 FLAG_GRANT_WRITE_URI_PERMISSION);
858 if (selfUriPermission != 0)
859 return false;
860
861 const QString type = mimeType();
862 if (type.isEmpty())
863 return false;
864
865 const int flags = Cursor::queryColumn(m_uri, Document::COLUMN_FLAGS).toInt();
867 return true;
868
869 const bool supportsWrite = (flags & Document::FLAG_SUPPORTS_WRITE);
870 const bool isDir = (type == Document::MIME_TYPE_DIR);
871 const bool dirSupportsCreate = (isDir && (flags & Document::FLAG_DIR_SUPPORTS_CREATE));
872
873 return dirSupportsCreate || supportsWrite;
874}
875
877{
878 return deleteDocument(m_uri);
879}
880
881bool DocumentFile::exists() const
882{
883 return !name().isEmpty();
884}
885
887{
888 std::vector<DocumentFilePtr> res;
889 const auto childrenUri = buildChildDocumentsUriUsingTree(m_uri, documentId(m_uri));
890 const auto query = Cursor::queryUri(childrenUri, {Document::COLUMN_DOCUMENT_ID});
891 if (!query)
892 return res;
893
894 while (query->moveToNext()) {
895 const auto uri = buildDocumentUriUsingTree(m_uri, query->data(0).toString());
896 res.push_back(std::make_shared<MakeableDocumentFile>(uri, shared_from_this()));
897 }
898 return res;
899}
900
901bool DocumentFile::rename(const QString &newName)
902{
903 if (!newName.startsWith(contentSchemeFull)) {
904 // Simple rename
905 QJniObject renamedUri = renameDocument(m_uri, newName);
906 if (!renamedUri.isValid())
907 return false;
908
909 m_uri = renamedUri;
910 m_displayName = newName;
911
912 return true;
913 }
914
915 // Mixed move and rename
916 auto destDoc = parseFromAnyUri(newName);
917 if (!destDoc)
918 return false;
919
920 DocumentFilePtr destParent = destDoc->parent();
921 const QString oldName = name();
922 const QString destName = QUrl::fromPercentEncoding(QFileInfo(newName).fileName().toUtf8());
923
924 bool isMove = false;
925 if (parent() && destParent && parent()->uri().toString() != destParent->uri().toString())
926 isMove = true;
927
928 QJniObject currentUri = m_uri;
929 if (isMove) {
930 if (!parent()) // Cannot move if source parent is unknown
931 return false;
932
933 currentUri = moveDocument(m_uri, parent()->uri(), destParent->uri());
934 if (!currentUri.isValid())
935 return false;
936 }
937
938 if (oldName != destName) {
939 QJniObject renamedUri = renameDocument(currentUri, destName);
940 if (!renamedUri.isValid())
941 return false;
942
943 currentUri = renamedUri;
944 }
945
946 m_uri = currentUri;
947 m_displayName = destName;
948 if (isMove)
949 m_parent = destParent;
950
951 return true;
952}
953
954QT_END_NAMESPACE
955
956// 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)
Combined button and popup list for selecting options.
QT_BEGIN_NAMESPACE Q_STATIC_LOGGING_CATEGORY(lcSynthesizedIterableAccess, "qt.iterable.synthesized", QtWarningMsg)
Q_DECLARE_JNI_CLASS(MotionEvent, "android/view/MotionEvent")