49 explicit Opt (
int64 v) : value (v), valid (
true) {}
55 static constexpr auto dirMime =
"vnd.android.document/directory";
64 template <
typename Columns>
65 class AndroidIteratorEngine
68 AndroidIteratorEngine (Columns columnsIn, jobject uri)
69 : columns (
std::move (columnsIn)),
70 cursor { LocalRef<jobject> { getEnv()->CallObjectMethod (AndroidContentUriResolver::getContentResolver().get(),
71 ContentResolver.query,
73 columns.getColumnNames().get(),
80 jniCheckHasExceptionOccurredAndClear();
83 auto read()
const {
return columns.readFromCursor (cursor.get()); }
87 if (cursor.get() ==
nullptr)
90 return getEnv()->CallBooleanMethod (cursor.get(), AndroidCursor.moveToNext);
98 template <
typename... Args,
size_t... Ix>
101 auto* env = getEnv();
102 LocalRef<jobjectArray> array { env->NewObjectArray (
sizeof... (args), JavaString,
nullptr) };
104 (env->SetObjectArrayElement (array.get(), Ix, args.get()), ...);
109 template <
typename... Args>
110 static LocalRef<jobjectArray> makeStringArray (Args&&... args)
115 static URL uriToUrl (jobject uri)
117 return URL (juceString ((jstring) getEnv()->CallObjectMethod (uri, AndroidUri.toString)));
123 GlobalRefImpl<jstring> idColumn;
125 auto getColumnNames()
const
127 return makeStringArray (idColumn);
130 auto readFromCursor (jobject cursor)
const
132 auto* env = getEnv();
133 const auto idColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, idColumn.get());
135 const auto documentUri = [&]
137 if (idColumnIndex < 0)
138 return LocalRef<jobject>{};
140 LocalRef<jstring> documentId { (jstring) env->CallObjectMethod (cursor, AndroidCursor.getString, idColumnIndex) };
141 return LocalRef<jobject> { getEnv()->CallStaticObjectMethod (DocumentsContract21,
142 DocumentsContract21.buildDocumentUriUsingTree,
147 return AndroidDocument::fromDocument (uriToUrl (documentUri));
151 using DocumentsContractIteratorEngine = AndroidIteratorEngine<Columns>;
153 static DocumentsContractIteratorEngine makeDocumentsContractIteratorEngine (
const GlobalRef& uri)
155 const LocalRef <jobject> documentId { getEnv()->CallStaticObjectMethod (DocumentsContract19,
156 DocumentsContract19.getDocumentId,
158 const LocalRef <jobject> childrenUri { getEnv()->CallStaticObjectMethod (DocumentsContract21,
159 DocumentsContract21.buildChildDocumentsUriUsingTree,
163 return DocumentsContractIteratorEngine { Columns { GlobalRef { uri },
164 GlobalRefImpl<jstring> { javaString (
"document_id") } },
168 class RecursiveEngine
171 explicit RecursiveEngine (GlobalRef uri)
172 : engine (makeDocumentsContractIteratorEngine (uri)) {}
174 AndroidDocument
read()
const
176 return subIterator !=
nullptr ? subIterator->read() : engine.read();
181 if (directory && subIterator ==
nullptr)
184 if (subIterator !=
nullptr)
186 if (subIterator->increment())
189 subIterator =
nullptr;
192 if (! engine.increment())
195 directory = engine.read().getInfo().isDirectory();
200 DocumentsContractIteratorEngine engine;
202 bool directory =
false;
205 enum { FLAG_GRANT_READ_URI_PERMISSION = 1, FLAG_GRANT_WRITE_URI_PERMISSION = 2 };
207 static void setPermissions (
const URL& url, jmethodID func)
209 if (getAndroidSDKVersion() < 19)
212 const auto javaUri = urlToUri (url);
214 if (
const auto resolver = AndroidContentUriResolver::getContentResolver())
216 const jint flags = FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION;
217 getEnv()->CallVoidMethod (resolver, func, javaUri.get(), flags);
218 jniCheckHasExceptionOccurredAndClear();
225 JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE (
"-Wdeprecated-declarations")
226 JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996)
228 : iterator (dir, recursive,
"*", File::findFilesAndDirectories) {}
229 JUCE_END_IGNORE_WARNINGS_MSVC
230 JUCE_END_IGNORE_WARNINGS_GCC_LIKE
232 auto read()
const {
return AndroidDocument::fromFile (iterator.getFile()); }
233 bool increment() {
return iterator.next(); }
234 DirectoryIterator iterator;
367 template <
typename>
struct VersionTag {
int version; };
372 String getMimeTypeFromExtension (
const String& str)
const
374 const auto javaStr = javaString (str);
375 return juceString ((jstring) getEnv()->CallObjectMethod (map.get(),
376 AndroidMimeTypeMap.getMimeTypeFromExtension,
380 String getExtensionFromMimeType (
const String& str)
const
382 const auto javaStr = javaString (str);
383 return juceString ((jstring) getEnv()->CallObjectMethod (map.get(),
384 AndroidMimeTypeMap.getExtensionFromMimeType,
389 GlobalRef map { LocalRef<jobject> { getEnv()->CallStaticObjectMethod (AndroidMimeTypeMap,
390 AndroidMimeTypeMap.getSingleton) } };
393 class AndroidDocumentPimplApi19 :
public Pimpl
396 AndroidDocumentPimplApi19() =
default;
398 explicit AndroidDocumentPimplApi19 (
const URL& uriIn)
399 : AndroidDocumentPimplApi19 (urlToUri (uriIn)) {}
401 explicit AndroidDocumentPimplApi19 (
const LocalRef<jobject>& uriIn)
406 bool deleteDocument()
const override
408 if (
const auto resolver = AndroidContentUriResolver::getContentResolver())
410 return getEnv()->CallStaticBooleanMethod (DocumentsContract19,
411 DocumentsContract19.deleteDocument,
422 return result->openedSuccessfully() ? std::move (result) :
nullptr;
427 auto stream = AndroidStreamHelpers::createStream (uri, AndroidStreamHelpers::StreamKind::output);
437 auto getColumnNames()
const
439 return Detail::makeStringArray (flagsColumn, nameColumn, mimeColumn, idColumn, modifiedColumn, sizeColumn);
442 auto readFromCursor (jobject cursor)
const
444 auto* env = getEnv();
446 const auto flagsColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, flagsColumn.get());
447 const auto nameColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, nameColumn.get());
448 const auto mimeColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, mimeColumn.get());
449 const auto idColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, idColumn.get());
450 const auto modColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, modifiedColumn.get());
451 const auto sizeColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, sizeColumn.get());
453 const auto indices = { flagsColumnIndex, nameColumnIndex, mimeColumnIndex, idColumnIndex, modColumnIndex, sizeColumnIndex };
455 if (
std::any_of (indices.begin(), indices.end(), [] (
auto index) { return index < 0; }))
458 const LocalRef<jstring> nameString { (jstring) env->CallObjectMethod (cursor, AndroidCursor.getString, nameColumnIndex) };
459 const LocalRef<jstring> mimeString { (jstring) env->CallObjectMethod (cursor, AndroidCursor.getString, mimeColumnIndex) };
461 const auto readOpt = [&] (
int column) ->
Detail::Opt
463 const auto missing = env->CallBooleanMethod (cursor, AndroidCursor.isNull, column);
468 return Detail::Opt { env->CallLongMethod (cursor, AndroidCursor.getLong, column) };
472 .withType (juceString (mimeString.get()))
473 .withFlags (env->CallIntMethod (cursor, AndroidCursor.getInt, flagsColumnIndex))
474 .withModified (readOpt (modColumnIndex))
475 .withSize (readOpt (sizeColumnIndex));
478 GlobalRefImpl<jstring> flagsColumn { javaString (
"flags") };
479 GlobalRefImpl<jstring> nameColumn { javaString (
"_display_name") };
480 GlobalRefImpl<jstring> mimeColumn { javaString (
"mime_type") };
481 GlobalRefImpl<jstring> idColumn { javaString (
"document_id") };
482 GlobalRefImpl<jstring> modifiedColumn { javaString (
"last_modified") };
483 GlobalRefImpl<jstring> sizeColumn { javaString (
"_size") };
486 Detail::AndroidIteratorEngine<Columns> iterator { Columns{}, uri };
488 if (! iterator.increment())
491 auto* env = getEnv();
492 auto ctx = getAppContext();
494 const auto hasPermission = [&] (
auto permission)
496 return env->CallIntMethod (ctx, AndroidContext.checkCallingOrSelfUriPermission, uri.get(), permission) == 0;
499 return iterator.read()
500 .withReadPermission (hasPermission (Detail::FLAG_GRANT_READ_URI_PERMISSION))
501 .withWritePermission (hasPermission (Detail::FLAG_GRANT_WRITE_URI_PERMISSION))
505 URL getUrl()
const override
507 return Detail::uriToUrl (uri);
510 NativeInfo getNativeInfo()
const override {
return { uri }; }
517 class AndroidDocumentPimplApi21 :
public AndroidDocumentPimplApi19
520 using AndroidDocumentPimplApi19::AndroidDocumentPimplApi19;
526 return Utils::createPimplForSdk (LocalRef<jobject> { getEnv()->CallStaticObjectMethod (DocumentsContract21,
527 DocumentsContract21.createDocument,
528 AndroidContentUriResolver::getContentResolver().get(),
529 getNativeInfo().uri.get(),
530 javaString (type).get(),
531 javaString (name).get()) });
536 if (
const auto resolver = AndroidContentUriResolver::getContentResolver())
538 return Utils::createPimplForSdk (LocalRef<jobject> { getEnv()->CallStaticObjectMethod (DocumentsContract21,
539 DocumentsContract21.renameDocument,
541 getNativeInfo().uri.get(),
542 javaString (name).get()) });
550 class AndroidDocumentPimplApi24 final :
public AndroidDocumentPimplApi21
553 using AndroidDocumentPimplApi21::AndroidDocumentPimplApi21;
559 if (target.getNativeInfo().uri ==
nullptr)
565 return Utils::createPimplForSdk (LocalRef<jobject> { getEnv()->CallStaticObjectMethod (DocumentsContract24,
566 DocumentsContract24.copyDocument,
567 AndroidContentUriResolver::getContentResolver().get(),
568 getNativeInfo().uri.get(),
569 target.getNativeInfo().uri.get()) });
574 if (currentParent.getNativeInfo().uri ==
nullptr || newParent.getNativeInfo().uri ==
nullptr)
580 return Utils::createPimplForSdk (LocalRef<jobject> { getEnv()->CallStaticObjectMethod (DocumentsContract24,
581 DocumentsContract24.moveDocument,
582 AndroidContentUriResolver::getContentResolver().get(),
583 getNativeInfo().uri.get(),
584 currentParent.getNativeInfo().uri.get(),
585 newParent.getNativeInfo().uri.get()) });
591 if (jniCheckHasExceptionOccurredAndClear())
594 return createPimplForSdkImpl (uri,
595 VersionTag<AndroidDocumentPimplApi24> { 24 },
596 VersionTag<AndroidDocumentPimplApi21> { 21 },
597 VersionTag<AndroidDocumentPimplApi19> { 19 });
607 template <
typename T,
typename... Ts>
610 VersionTag<Ts>... tail)
612 if (head.version <= getAndroidSDKVersion())
615 return createPimplForSdkImpl (uri, tail...);
622 static String getMimeTypeFromExtension (
const String& str)
624 return detail::MimeTypeTable::getMimeTypesForFileExtension (str)[0];
627 static String getExtensionFromMimeType (
const String& str)
629 return detail::MimeTypeTable::getFileExtensionsForMimeType (str)[0];
645 bool deleteDocument()
const override
647 return file.deleteRecursively (
false);
652 const auto target = file.getSiblingFile (name);
662 auto result = file.createOutputStream();
663 result->setPosition (0);
670 const auto parent = target.getFile();
672 if (parent ==
File())
675 const auto actual = parent.
getChildFile (file.getFileName());
680 const auto success = file.
isDirectory() ? file.copyDirectoryTo (actual)
681 : file.copyFileTo (actual);
688 const String& name)
const override
690 const auto extension = mimeConverter.getExtensionFromMimeType (type);
691 const auto target = file.getChildFile (extension.isNotEmpty() ? name +
"." + extension : name);
693 if (! target.exists() && (type == Detail::dirMime ? target.createDirectory() : target.create()))
700 const Pimpl& newParentPimpl)
const override
702 const auto currentParent = currentParentPimpl.getFile();
703 const auto newParent = newParentPimpl.getFile();
705 if (! file.isAChildOf (currentParent) || newParent ==
File())
708 const auto target = newParent.getChildFile (file.getFileName());
710 if (target.exists() || ! file.moveFileTo (target))
721 const auto size = file.getSize();
722 const auto extension = file.getFileExtension().removeCharacters (
".").toLowerCase();
723 const auto type = file.isDirectory() ? Detail::dirMime
724 : mimeConverter.getMimeTypeFromExtension (extension);
727 .withType (type.
isNotEmpty() ? type :
"application/octet-stream")
728 .withFlags (AndroidDocumentInfo::Args::getFlagsForFile (file))
729 .withModified (
Detail::Opt { file.getLastModificationTime().toMilliseconds() })
731 .withReadPermission (file.hasReadAccess())
732 .withWritePermission (file.hasWriteAccess())
736 URL getUrl()
const override {
return URL (file); }
738 NativeInfo getNativeInfo()
const override {
return {}; }