From b17806fbfc58cc40811cf9d95f810083d82c6c3e Mon Sep 17 00:00:00 2001 From: reuk Date: Wed, 11 May 2022 20:07:57 +0100 Subject: [PATCH] AndroidDocument: Support file access to shared storage locations on Android 30+ --- .../juce_core/files/juce_AndroidDocument.h | 476 ++++++++ .../files}/juce_common_MimeTypes.cpp | 23 +- .../juce_core/files/juce_common_MimeTypes.h | 42 + modules/juce_core/juce_core.cpp | 2 + modules/juce_core/juce_core.h | 1 + .../native/juce_android_AndroidDocument.cpp | 1085 +++++++++++++++++ .../juce_core/native/juce_android_Files.cpp | 198 ++- .../native/juce_android_JNIHelpers.h | 85 +- .../juce_core/native/juce_android_Network.cpp | 30 +- modules/juce_core/network/juce_URL.cpp | 9 +- modules/juce_gui_basics/juce_gui_basics.cpp | 2 +- .../native/juce_android_ContentSharer.cpp | 6 +- .../native/juce_android_FileChooser.cpp | 62 +- 13 files changed, 1913 insertions(+), 108 deletions(-) create mode 100644 modules/juce_core/files/juce_AndroidDocument.h rename modules/{juce_gui_basics/native => juce_core/files}/juce_common_MimeTypes.cpp (94%) create mode 100644 modules/juce_core/files/juce_common_MimeTypes.h create mode 100644 modules/juce_core/native/juce_android_AndroidDocument.cpp diff --git a/modules/juce_core/files/juce_AndroidDocument.h b/modules/juce_core/files/juce_AndroidDocument.h new file mode 100644 index 0000000000..883ca7f2e8 --- /dev/null +++ b/modules/juce_core/files/juce_AndroidDocument.h @@ -0,0 +1,476 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +//============================================================================== +/** + Some information about a document. + + Each instance represents some information about the document at the point when the instance + was created. + + Instance information is not updated automatically. If you think some file information may + have changed, create a new instance. + + @tags{Core} +*/ +class AndroidDocumentInfo +{ +public: + AndroidDocumentInfo() = default; + + /** True if this file really exists. */ + bool exists() const { return isJuceFlagSet (flagExists); } + + /** True if this is a directory rather than a file. */ + bool isDirectory() const; + + /** True if this is a file rather than a directory. */ + bool isFile() const { return type.isNotEmpty() && ! isDirectory(); } + + /** True if this process has permission to read this file. + + If this returns true, and the AndroidDocument refers to a file rather than a directory, + then AndroidDocument::createInputStream should work on this document. + */ + bool canRead() const { return isJuceFlagSet (flagHasReadPermission) && type.isNotEmpty(); } + + /** True if this is a document that can be written, or a directory that can be modified. + + If this returns true, and the AndroidDocument refers to a file rather than a directory, + then AndroidDocument::createOutputStream should work on this document. + */ + bool canWrite() const + { + return isJuceFlagSet (flagHasWritePermission) + && type.isNotEmpty() + && (isNativeFlagSet (flagSupportsWrite) + || isNativeFlagSet (flagSupportsDelete) + || isNativeFlagSet (flagDirSupportsCreate)); + } + + /** True if this document can be removed completely from the filesystem. */ + bool canDelete() const { return isNativeFlagSet (flagSupportsDelete); } + + /** True if this is a directory and adding child documents is supported. */ + bool canCreateChildren() const { return isNativeFlagSet (flagDirSupportsCreate); } + + /** True if this document can be renamed. */ + bool canRename() const { return isNativeFlagSet (flagSupportsRename); } + + /** True if this document can be copied. */ + bool canCopy() const { return isNativeFlagSet (flagSupportsCopy); } + + /** True if this document can be moved. */ + bool canMove() const { return isNativeFlagSet (flagSupportsMove); } + + /** True if this document isn't a physical file on storage. */ + bool isVirtual() const { return isNativeFlagSet (flagVirtualDocument); } + + /** The user-facing name. + + This may or may not contain a file extension. For files identified by a URL, the MIME type + is stored separately. + */ + String getName() const { return name; } + + /** The MIME type of this document. */ + String getType() const { return isDirectory() ? String{} : type; } + + /** Timestamp when a document was last modified, in milliseconds since January 1, 1970 00:00:00.0 UTC. + + Use isLastModifiedValid() to determine whether or not the result of this + function is valid. + */ + int64 getLastModified() const { return isJuceFlagSet (flagValidModified) ? lastModified : 0; } + + /** True if the filesystem provided a modification time. */ + bool isLastModifiedValid() const { return isJuceFlagSet (flagValidModified); } + + /** The size of the document in bytes, if known. + + Use isSizeInBytesValid() to determine whether or not the result of this + function is valid. + */ + int64 getSizeInBytes() const { return isJuceFlagSet (flagValidSize) ? sizeInBytes : 0; } + + /** True if the filesystem provided a size in bytes. */ + bool isSizeInBytesValid() const { return isJuceFlagSet (flagValidSize); } + + /** @internal */ + class Args; + +private: + explicit AndroidDocumentInfo (Args); + + bool isNativeFlagSet (int flag) const { return (nativeFlags & flag) != 0; } + bool isJuceFlagSet (int flag) const { return (juceFlags & flag) != 0; } + + /* Native Android flags that might be set in the COLUMN_FLAGS for a particular document */ + enum + { + flagSupportsWrite = 0x0002, + flagSupportsDelete = 0x0004, + flagDirSupportsCreate = 0x0008, + flagSupportsRename = 0x0040, + flagSupportsCopy = 0x0080, + flagSupportsMove = 0x0100, + flagVirtualDocument = 0x0200, + }; + + /* Flags for other binary properties that aren't exposed in COLUMN_FLAGS */ + enum + { + flagExists = 1 << 0, + flagValidModified = 1 << 1, + flagValidSize = 1 << 2, + flagHasReadPermission = 1 << 3, + flagHasWritePermission = 1 << 4, + }; + + String name; + String type; + int64 lastModified = 0; + int64 sizeInBytes = 0; + int nativeFlags = 0, juceFlags = 0; +}; + +//============================================================================== +/** + Represents a permission granted to an application to read and/or write to a particular document + or tree. + + This class also contains static methods to request, revoke, and query the permissions of your + app. These functions are no-ops on all platforms other than Android. + + @tags{Core} +*/ +class AndroidDocumentPermission +{ +public: + /** The url of the document with persisted permissions. */ + URL getUrl() const { return url; } + + /** The time when the permissions were persisted, in milliseconds since January 1, 1970 00:00:00.0 UTC. */ + int64 getPersistedTime() const { return time; } + + /** True if the permission allows read access. */ + bool isReadPermission() const { return read; } + + /** True if the permission allows write access. */ + bool isWritePermission() const { return write; } + + /** Gives your app access to a particular document or tree, even after the device is rebooted. + + If you want to persist access to a folder selected through a native file chooser, make sure + to pass the exact URL returned by the file picker. Do NOT call AndroidDocument::fromTree + and then pass the result of getUrl to this function, as the resulting URL may differ from + the result of the file picker. + */ + static void takePersistentReadWriteAccess (const URL&); + + /** Revokes persistent access to a document or tree. */ + static void releasePersistentReadWriteAccess (const URL&); + + /** Returns all of the permissions that have previously been granted to the app, via + takePersistentReadWriteAccess(); + */ + static std::vector getPersistedPermissions(); + +private: + URL url; + int64 time = 0; + bool read = false, write = false; +}; + +//============================================================================== +/** + Provides access to a document on Android devices. + + In this context, a 'document' may be a file or a directory. + + The main purpose of this class is to provide access to files in shared storage on Android. + On newer Android versions, such files cannot be accessed directly by a file path, and must + instead be read and modified using a new URI-based DocumentsContract API. + + Example use-cases: + + - After showing the system open dialog to allow the user to open a file, pass the FileChooser's + URL result to AndroidDocument::fromDocument. Then, you can use getInfo() to retrieve + information about the file, and createInputStream to read from the file. Other functions allow + moving, copying, and deleting the file. + + - Similarly to the 'open' use-case, you may use createOutputStream to write to a file, normally + located using the system save dialog. + + - To allow reading or writing to a tree of files in shared storage, you can show the system + open dialog in 'selects directories' mode, and pass the resulting URL to + AndroidDocument::fromTree. Then, you can iterate the files in the directory, query them, + and create new files. This is a good way to store multiple files that the user can access from + other apps, and that will be persistent after uninstalling and reinstalling your app. + + Note that you probably do *not* need this class if your app only needs to access files in its + own internal sandbox. juce::File instances should work as expected in that case. + + AndroidDocument is a bit like the DocumentFile class from the androidx extension library, + in that it represents a single document, and is implemented using DocumentsContract functions. + + @tags{Core} +*/ +class AndroidDocument +{ +public: + /** Create a null document. */ + AndroidDocument(); + + /** Create an AndroidDocument representing a file or directory at a particular path. + + This is provided for use on older API versions (lower than 19), or on other platforms, so + that the same AndroidDocument API can be used regardless of the runtime platform version. + + If the runtime platform version is 19 or higher, and you wish to work with a URI obtained + from a native file picker, use fromDocument() or fromTree() instead. + + If this function fails, hasValue() will return false on the returned document. + */ + static AndroidDocument fromFile (const File& filePath); + + /** Create an AndroidDocument representing a single document. + + The argument should be a URL representing a document. Such URLs are returned by the system + file-picker when it is not in folder-selection mode. If you pass a tree URL, this function + will fail. + + This function may fail on Android devices with API level 18 or lower, and on non-Android + platforms. If this function fails, hasValue() will return false on the returned document. + If calling this function fails, you may want to retry creating an AndroidDocument + with fromFile(), passing the result of URL::getLocalFile(). + */ + static AndroidDocument fromDocument (const URL& documentUrl); + + /** Create an AndroidDocument representing the root of a tree of files. + + The argument should be a URL representing a tree. Such URLs are returned by the system + file-picker when it is in folder-selection mode. If you pass a URL referring to a document + inside a tree, this will return a document referring to the root of the tree. If you pass + a URL referring to a single file, this will fail. + + When targeting platform version 30 or later, access to the filesystem via file paths is + heavily restricted, and access to shared storage must use a new URI-based system instead. + At time of writing, apps uploaded to the Play Store must target API 30 or higher. + If you want read/write access to a shared folder, you must: + + - Use a native FileChooser in canSelectDirectories mode, to allow the user to select a + folder that your app can access. Your app will only have access to the contents of this + directory; it cannot escape to the filesystem root. The system will not allow the user + to grant access to certain locations, including filesystem roots and the Download folder. + - Pass the URI that the user selected to fromTree(), and use the resulting AndroidDocument + to read/write to the file system. + + This function may fail on Android devices with API level 20 or lower, and on non-Android + platforms. If this function fails, hasValue() will return false on the returned document. + */ + static AndroidDocument fromTree (const URL& treeUrl); + + AndroidDocument (const AndroidDocument&); + AndroidDocument (AndroidDocument&&) noexcept; + + AndroidDocument& operator= (const AndroidDocument&); + AndroidDocument& operator= (AndroidDocument&&) noexcept; + + ~AndroidDocument(); + + /** True if the URLs of the two documents match. */ + bool operator== (const AndroidDocument&) const; + + /** False if the URLs of the two documents match. */ + bool operator!= (const AndroidDocument&) const; + + /** Attempts to delete this document, and returns true on success. */ + bool deleteDocument() const; + + /** Renames the document, and returns true on success. + + This may cause the document's URI and metadata to change, so ensure to invalidate any + cached information about the document (URLs, AndroidDocumentInfo instances) after calling + this function. + */ + bool renameTo (const String& newDisplayName); + + /** Attempts to create a new nested document with a particular type and name. + + The type should be a standard MIME type string, e.g. "image/png", "text/plain". + + The file name doesn't need to contain an extension, as this information is passed via the + type argument. If this document is File-based rather than URL-based, then an appropriate + file extension will be chosen based on the MIME type. + + On failure, the returned AndroidDocument may be invalid, and will return false from hasValue(). + */ + AndroidDocument createChildDocumentWithTypeAndName (const String& type, const String& name) const; + + /** Attempts to create a new nested directory with a particular name. + + On failure, the returned AndroidDocument may be invalid, and will return false from hasValue(). + */ + AndroidDocument createChildDirectory (const String& name) const; + + /** True if this object actually refers to a document. + + If this function returns false, you *must not* call any function on this instance other + than the special member functions to copy, move, and/or destruct the instance. + */ + bool hasValue() const { return pimpl != nullptr; } + + /** Like hasValue(), but allows declaring AndroidDocument instances directly in 'if' statements. */ + explicit operator bool() const { return hasValue(); } + + /** Creates a stream for reading from this document. */ + std::unique_ptr createInputStream() const; + + /** Creates a stream for writing to this document. */ + std::unique_ptr createOutputStream() const; + + /** Returns the content URL describing this document. */ + URL getUrl() const; + + /** Fetches information about this document. */ + AndroidDocumentInfo getInfo() const; + + /** Experimental: Attempts to copy this document to a new parent, and returns an AndroidDocument + representing the copy. + + On failure, the returned AndroidDocument may be invalid, and will return false from hasValue(). + + This function may fail if the document doesn't allow copying, and when using URI-based + documents on devices with API level 23 or lower. On failure, the returned AndroidDocument + will return false from hasValue(). In testing, copying was not supported on the Android + emulator for API 24, 30, or 31, so there's a good chance this function won't work on real + devices. + + @see AndroidDocumentInfo::canCopy + */ + AndroidDocument copyDocumentToParentDocument (const AndroidDocument& target) const; + + /** Experimental: Attempts to move this document from one parent to another, and returns true on + success. + + This may cause the document's URI and metadata to change, so ensure to invalidate any + cached information about the document (URLs, AndroidDocumentInfo instances) after calling + this function. + + This function may fail if the document doesn't allow moving, and when using URI-based + documents on devices with API level 23 or lower. + */ + bool moveDocumentFromParentToParent (const AndroidDocument& currentParent, + const AndroidDocument& newParent); + + /** @internal */ + struct NativeInfo; + + /** @internal */ + NativeInfo getNativeInfo() const; + +private: + struct Utils; + class Pimpl; + + explicit AndroidDocument (std::unique_ptr); + + void swap (AndroidDocument& other) noexcept { std::swap (other.pimpl, pimpl); } + + std::unique_ptr pimpl; +}; + +//============================================================================== +/** + An iterator that visits child documents in a directory. + + Instances of this iterator can be created by calling makeRecursive() or + makeNonRecursive(). The results of these functions can additionally be used + in standard algorithms, and in range-for loops: + + @code + AndroidDocument findFileWithName (const AndroidDocument& parent, const String& name) + { + for (const auto& child : AndroidDocumentIterator::makeNonRecursive (parent)) + if (child.getInfo().getName() == name) + return child; + + return AndroidDocument(); + } + + std::vector findAllChildrenRecursive (const AndroidDocument& parent) + { + std::vector children; + std::copy (AndroidDocumentIterator::makeRecursive (doc), + AndroidDocumentIterator(), + std::back_inserter (children)); + return children; + } + @endcode + + @tags{Core} +*/ +class AndroidDocumentIterator final +{ +public: + using difference_type = std::ptrdiff_t; + using pointer = void; + using iterator_category = std::input_iterator_tag; + + /** Create an iterator that will visit each item in this directory. */ + static AndroidDocumentIterator makeNonRecursive (const AndroidDocument&); + + /** Create an iterator that will visit each item in this directory, and all nested directories. */ + static AndroidDocumentIterator makeRecursive (const AndroidDocument&); + + /** Creates an end/sentinel iterator. */ + AndroidDocumentIterator() = default; + + bool operator== (const AndroidDocumentIterator& other) const noexcept { return pimpl == nullptr && other.pimpl == nullptr; } + bool operator!= (const AndroidDocumentIterator& other) const noexcept { return ! operator== (other); } + + /** Returns the document to which this iterator points. */ + AndroidDocument operator*() const; + + /** Moves this iterator to the next position. */ + AndroidDocumentIterator& operator++(); + + /** Allows this iterator to be used directly in a range-for. */ + AndroidDocumentIterator begin() const { return *this; } + + /** Allows this iterator to be used directly in a range-for. */ + AndroidDocumentIterator end() const { return AndroidDocumentIterator{}; } + +private: + struct Utils; + struct Pimpl; + + explicit AndroidDocumentIterator (std::unique_ptr); + + std::shared_ptr pimpl; +}; + +} // namespace juce diff --git a/modules/juce_gui_basics/native/juce_common_MimeTypes.cpp b/modules/juce_core/files/juce_common_MimeTypes.cpp similarity index 94% rename from modules/juce_gui_basics/native/juce_common_MimeTypes.cpp rename to modules/juce_core/files/juce_common_MimeTypes.cpp index 0474ec345a..8298d181cc 100644 --- a/modules/juce_gui_basics/native/juce_common_MimeTypes.cpp +++ b/modules/juce_core/files/juce_common_MimeTypes.cpp @@ -33,17 +33,34 @@ struct MimeTypeTableEntry static MimeTypeTableEntry table[641]; }; -static StringArray getMimeTypesForFileExtension (const String& fileExtension) +static StringArray getMatches (const String& toMatch, + const char* MimeTypeTableEntry::* matchField, + const char* MimeTypeTableEntry::* returnField) { StringArray result; for (auto type : MimeTypeTableEntry::table) - if (fileExtension == type.fileExtension) - result.add (type.mimeType); + if (toMatch == type.*matchField) + result.add (type.*returnField); return result; } +namespace MimeTypeTable +{ + +StringArray getMimeTypesForFileExtension (const String& fileExtension) +{ + return getMatches (fileExtension, &MimeTypeTableEntry::fileExtension, &MimeTypeTableEntry::mimeType); +} + +StringArray getFileExtensionsForMimeType (const String& mimeType) +{ + return getMatches (mimeType, &MimeTypeTableEntry::mimeType, &MimeTypeTableEntry::fileExtension); +} + +} // namespace MimeTypeTable + //============================================================================== MimeTypeTableEntry MimeTypeTableEntry::table[641] = { diff --git a/modules/juce_core/files/juce_common_MimeTypes.h b/modules/juce_core/files/juce_common_MimeTypes.h new file mode 100644 index 0000000000..1d79b41bd8 --- /dev/null +++ b/modules/juce_core/files/juce_common_MimeTypes.h @@ -0,0 +1,42 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + By using JUCE, you agree to the terms of both the JUCE 6 End-User License + Agreement and JUCE Privacy Policy (both effective as of the 16th June 2020). + + End User License Agreement: www.juce.com/juce-6-licence + Privacy Policy: www.juce.com/juce-privacy-policy + + Or: You may also use this code under the terms of the GPL v3 (see + www.gnu.org/licenses). + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +#pragma once + +namespace juce +{ + +namespace MimeTypeTable +{ + +/* @internal */ +StringArray getMimeTypesForFileExtension (const String& fileExtension); + +/* @internal */ +StringArray getFileExtensionsForMimeType (const String& mimeType); + +} // namespace MimeTypeTable + +} // namespace juce diff --git a/modules/juce_core/juce_core.cpp b/modules/juce_core/juce_core.cpp index 5ffbd6b443..364615b67d 100644 --- a/modules/juce_core/juce_core.cpp +++ b/modules/juce_core/juce_core.cpp @@ -248,6 +248,8 @@ #endif +#include "files/juce_common_MimeTypes.cpp" +#include "native/juce_android_AndroidDocument.cpp" #include "threads/juce_HighResolutionTimer.cpp" #include "threads/juce_WaitableEvent.cpp" #include "network/juce_URL.cpp" diff --git a/modules/juce_core/juce_core.h b/modules/juce_core/juce_core.h index 41a292204f..fc54ad63cd 100644 --- a/modules/juce_core/juce_core.h +++ b/modules/juce_core/juce_core.h @@ -342,6 +342,7 @@ JUCE_END_IGNORE_WARNINGS_MSVC #include "memory/juce_SharedResourcePointer.h" #include "memory/juce_AllocationHooks.h" #include "memory/juce_Reservoir.h" +#include "files/juce_AndroidDocument.h" #if JUCE_CORE_INCLUDE_OBJC_HELPERS && (JUCE_MAC || JUCE_IOS) #include "native/juce_mac_ObjCHelpers.h" diff --git a/modules/juce_core/native/juce_android_AndroidDocument.cpp b/modules/juce_core/native/juce_android_AndroidDocument.cpp new file mode 100644 index 0000000000..5b09605fc1 --- /dev/null +++ b/modules/juce_core/native/juce_android_AndroidDocument.cpp @@ -0,0 +1,1085 @@ +/* + ============================================================================== + + This file is part of the JUCE library. + Copyright (c) 2020 - Raw Material Software Limited + + JUCE is an open source library subject to commercial or open-source + licensing. + + The code included in this file is provided under the terms of the ISC license + http://www.isc.org/downloads/software-support-policy/isc-license. Permission + To use, copy, modify, and/or distribute this software for any purpose with or + without fee is hereby granted provided that the above copyright notice and + this permission notice appear in all copies. + + JUCE IS PROVIDED "AS IS" WITHOUT ANY WARRANTY, AND ALL WARRANTIES, WHETHER + EXPRESSED OR IMPLIED, INCLUDING MERCHANTABILITY AND FITNESS FOR PURPOSE, ARE + DISCLAIMED. + + ============================================================================== +*/ + +namespace juce +{ + +/* This is mainly used to pass implementation information from AndroidDocument to + AndroidDocumentIterator. This needs to be defined in a .cpp because it uses the internal + GlobalRef type. + + To preserve encapsulation, this struct should only contain information that would normally be + public, were internal types not in use. +*/ +struct AndroidDocument::NativeInfo +{ + #if JUCE_ANDROID + GlobalRef uri; + #endif +}; + +//============================================================================== +struct AndroidDocumentDetail +{ + ~AndroidDocumentDetail() = delete; // This struct is a single-file namespace + + struct Opt + { + Opt() = default; + + explicit Opt (int64 v) : value (v), valid (true) {} + + int64 value = 0; + bool valid = false; + }; + + static constexpr auto dirMime = "vnd.android.document/directory"; + + #if JUCE_ANDROID + /* + A very basic type that acts a bit like an iterator, in that it can be incremented, and read-from. + + Instances of this type can be passed to the constructor of AndroidDirectoryIterator to provide + stdlib-like iterator facilities. + */ + template + class AndroidIteratorEngine + { + public: + AndroidIteratorEngine (Columns columnsIn, jobject uri) + : columns (std::move (columnsIn)), + cursor { LocalRef { getEnv()->CallObjectMethod (AndroidContentUriResolver::getContentResolver().get(), + ContentResolver.query, + uri, + columns.getColumnNames().get(), + nullptr, + nullptr, + nullptr) } } + { + // Creating the cursor may throw if the document doesn't exist. + // In that case, cursor will still be null. + jniCheckHasExceptionOccurredAndClear(); + } + + auto read() const { return columns.readFromCursor (cursor.get()); } + + bool increment() + { + if (cursor.get() == nullptr) + return false; + + return getEnv()->CallBooleanMethod (cursor.get(), AndroidCursor.moveToNext); + } + + private: + Columns columns; + GlobalRef cursor; + }; + + template + static LocalRef makeStringArray (std::index_sequence, Args&&... args) + { + auto* env = getEnv(); + LocalRef array { env->NewObjectArray (sizeof... (args), JavaString, nullptr) }; + + int unused[] { (env->SetObjectArrayElement (array.get(), Ix, args.get()), 0)... }; + ignoreUnused (unused); + + return array; + } + + template + static LocalRef makeStringArray (Args&&... args) + { + return makeStringArray (std::make_index_sequence(), std::forward (args)...); + } + + static URL uriToUrl (jobject uri) + { + return URL (juceString ((jstring) getEnv()->CallObjectMethod (uri, AndroidUri.toString))); + } + + struct Columns + { + GlobalRef treeUri; + GlobalRefImpl idColumn; + + auto getColumnNames() const + { + return makeStringArray (idColumn); + } + + auto readFromCursor (jobject cursor) const + { + auto* env = getEnv(); + const auto idColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, idColumn.get()); + + const auto documentUri = [&] + { + if (idColumnIndex < 0) + return LocalRef{}; + + LocalRef documentId { (jstring) env->CallObjectMethod (cursor, AndroidCursor.getString, idColumnIndex) }; + return LocalRef { getEnv()->CallStaticObjectMethod (DocumentsContract21, + DocumentsContract21.buildDocumentUriUsingTree, + treeUri.get(), + documentId.get()) }; + }(); + + return AndroidDocument::fromDocument (uriToUrl (documentUri)); + } + }; + + using DocumentsContractIteratorEngine = AndroidIteratorEngine; + + static DocumentsContractIteratorEngine makeDocumentsContractIteratorEngine (const GlobalRef& uri) + { + const LocalRef documentId { getEnv()->CallStaticObjectMethod (DocumentsContract19, + DocumentsContract19.getDocumentId, + uri.get()) }; + const LocalRef childrenUri { getEnv()->CallStaticObjectMethod (DocumentsContract21, + DocumentsContract21.buildChildDocumentsUriUsingTree, + uri.get(), + documentId.get()) }; + + return DocumentsContractIteratorEngine { Columns { GlobalRef { uri }, + GlobalRefImpl { javaString("document_id") } }, + childrenUri.get() }; + } + + class RecursiveEngine + { + public: + explicit RecursiveEngine (GlobalRef uri) + : engine (makeDocumentsContractIteratorEngine (uri)) {} + + AndroidDocument read() const + { + return subIterator != nullptr ? subIterator->read() : engine.read(); + } + + bool increment() + { + if (directory && subIterator == nullptr) + subIterator = std::make_unique (engine.read().getNativeInfo().uri); + + if (subIterator != nullptr) + { + if (subIterator->increment()) + return true; + + subIterator = nullptr; + } + + if (! engine.increment()) + return false; + + directory = engine.read().getInfo().isDirectory(); + return true; + } + + private: + DocumentsContractIteratorEngine engine; + std::unique_ptr subIterator; + bool directory = false; + }; + + enum { FLAG_GRANT_READ_URI_PERMISSION = 1, FLAG_GRANT_WRITE_URI_PERMISSION = 2 }; + + static void setPermissions (const URL& url, jmethodID func) + { + if (getAndroidSDKVersion() < 19) + return; + + const auto javaUri = urlToUri (url); + + if (const auto resolver = AndroidContentUriResolver::getContentResolver()) + { + const jint flags = FLAG_GRANT_READ_URI_PERMISSION | FLAG_GRANT_WRITE_URI_PERMISSION; + getEnv()->CallVoidMethod (resolver, func, javaUri.get(), flags); + jniCheckHasExceptionOccurredAndClear(); + } + } + #endif + + struct DirectoryIteratorEngine + { + JUCE_BEGIN_IGNORE_WARNINGS_GCC_LIKE ("-Wdeprecated-declarations") + JUCE_BEGIN_IGNORE_WARNINGS_MSVC (4996) + DirectoryIteratorEngine (const File& dir, bool recursive) + : iterator (dir, recursive, "*", File::findFilesAndDirectories) {} + JUCE_END_IGNORE_WARNINGS_MSVC + JUCE_END_IGNORE_WARNINGS_GCC_LIKE + + auto read() const { return AndroidDocument::fromFile (iterator.getFile()); } + bool increment() { return iterator.next(); } + DirectoryIterator iterator; + }; + +}; + +//============================================================================== +class AndroidDocumentInfo::Args +{ +public: + using Detail = AndroidDocumentDetail; + + Args withName (String x) const { return with (&Args::name, std::move (x)); } + Args withType (String x) const { return with (&Args::type, std::move (x)); } + Args withFlags (int x) const { return with (&Args::flags, x); } + Args withSize (Detail::Opt x) const { return with (&Args::sizeInBytes, x); } + Args withModified (Detail::Opt x) const { return with (&Args::lastModified, x); } + Args withReadPermission (bool x) const { return with (&Args::readPermission, x); } + Args withWritePermission (bool x) const { return with (&Args::writePermission, x); } + + String name; + String type; + Detail::Opt sizeInBytes, lastModified; + int flags = 0; + bool readPermission = false, writePermission = false; + + static int getFlagsForFile (const File& file) + { + int flags = 0; + + if (file.hasReadAccess()) + flags |= AndroidDocumentInfo::flagSupportsCopy; + + if (file.hasWriteAccess()) + flags |= AndroidDocumentInfo::flagSupportsWrite + | AndroidDocumentInfo::flagDirSupportsCreate + | AndroidDocumentInfo::flagSupportsMove + | AndroidDocumentInfo::flagSupportsRename + | AndroidDocumentInfo::flagSupportsDelete; + + return flags; + } + + AndroidDocumentInfo build() const + { + return AndroidDocumentInfo (*this); + } + +private: + template + Args with (Value Args::* member, Value v) const + { + auto copy = *this; + copy.*member = std::move (v); + return copy; + } +}; + +AndroidDocumentInfo::AndroidDocumentInfo (Args args) + : name (args.name), + type (args.type), + lastModified (args.lastModified.value), + sizeInBytes (args.sizeInBytes.value), + nativeFlags (args.flags), + juceFlags (flagExists + | (args.lastModified.valid ? flagValidModified : 0) + | (args.sizeInBytes.valid ? flagValidSize : 0) + | (args.readPermission ? flagHasReadPermission : 0) + | (args.writePermission ? flagHasWritePermission : 0)) +{ +} + +bool AndroidDocumentInfo::isDirectory() const { return type == AndroidDocumentDetail::dirMime; } + +//============================================================================== +class AndroidDocument::Pimpl +{ +public: + Pimpl() = default; + Pimpl (const Pimpl&) = default; + Pimpl (Pimpl&&) noexcept = default; + Pimpl& operator= (const Pimpl&) = default; + Pimpl& operator= (Pimpl&&) noexcept = default; + + virtual ~Pimpl() = default; + virtual std::unique_ptr clone() const = 0; + virtual bool deleteDocument() const = 0; + virtual std::unique_ptr createInputStream() const = 0; + virtual std::unique_ptr createOutputStream() const = 0; + virtual AndroidDocumentInfo getInfo() const = 0; + virtual URL getUrl() const = 0; + virtual NativeInfo getNativeInfo() const = 0; + + virtual std::unique_ptr copyDocumentToParentDocument (const Pimpl&) const + { + // This function is not supported on the current platform. + jassertfalse; + return {}; + } + + virtual std::unique_ptr moveDocumentFromParentToParent (const Pimpl&, const Pimpl&) const + { + // This function is not supported on the current platform. + jassertfalse; + return {}; + } + + virtual std::unique_ptr renameTo (const String&) const + { + // This function is not supported on the current platform. + jassertfalse; + return {}; + } + + virtual std::unique_ptr createChildDocumentWithTypeAndName (const String&, const String&) const + { + // This function is not supported on the current platform. + jassertfalse; + return {}; + } + + File getFile() const { return getUrl().getLocalFile(); } + + static const Pimpl& getPimpl (const AndroidDocument& doc) { return *doc.pimpl; } +}; + +//============================================================================== +struct AndroidDocument::Utils +{ + using Detail = AndroidDocumentDetail; + + ~Utils() = delete; // This stuct is a single-file namespace + + #if JUCE_ANDROID + template struct VersionTag { int version; }; + + class MimeConverter + { + public: + String getMimeTypeFromExtension (const String& str) const + { + const auto javaStr = javaString (str); + return juceString ((jstring) getEnv()->CallObjectMethod (map.get(), + AndroidMimeTypeMap.getMimeTypeFromExtension, + javaStr.get())); + } + + String getExtensionFromMimeType (const String& str) const + { + const auto javaStr = javaString (str); + return juceString ((jstring) getEnv()->CallObjectMethod (map.get(), + AndroidMimeTypeMap.getExtensionFromMimeType, + javaStr.get())); + } + + private: + GlobalRef map { LocalRef { getEnv()->CallStaticObjectMethod (AndroidMimeTypeMap, + AndroidMimeTypeMap.getSingleton) } }; + }; + + class AndroidDocumentPimplApi19 : public Pimpl + { + public: + AndroidDocumentPimplApi19() = default; + + explicit AndroidDocumentPimplApi19 (const URL& uriIn) + : AndroidDocumentPimplApi19 (urlToUri (uriIn)) {} + + explicit AndroidDocumentPimplApi19 (const LocalRef& uriIn) + : uri (uriIn) {} + + std::unique_ptr clone() const override { return std::make_unique (*this); } + + bool deleteDocument() const override + { + if (const auto resolver = AndroidContentUriResolver::getContentResolver()) + { + return getEnv()->CallStaticBooleanMethod (DocumentsContract19, + DocumentsContract19.deleteDocument, + resolver.get(), + uri.get()); + } + + return false; + } + + std::unique_ptr createInputStream() const override + { + return makeStream (AndroidStreamHelpers::StreamKind::input); + } + + std::unique_ptr createOutputStream() const override + { + return makeStream (AndroidStreamHelpers::StreamKind::output); + } + + AndroidDocumentInfo getInfo() const override + { + struct Columns + { + auto getColumnNames() const + { + return Detail::makeStringArray (flagsColumn, nameColumn, mimeColumn, idColumn, modifiedColumn, sizeColumn); + } + + auto readFromCursor (jobject cursor) const + { + auto* env = getEnv(); + + const auto flagsColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, flagsColumn.get()); + const auto nameColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, nameColumn.get()); + const auto mimeColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, mimeColumn.get()); + const auto idColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, idColumn.get()); + const auto modColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, modifiedColumn.get()); + const auto sizeColumnIndex = env->CallIntMethod (cursor, AndroidCursor.getColumnIndex, sizeColumn.get()); + + const auto indices = { flagsColumnIndex, nameColumnIndex, mimeColumnIndex, idColumnIndex, modColumnIndex, sizeColumnIndex }; + + if (std::any_of (indices.begin(), indices.end(), [] (auto index) { return index < 0; })) + return AndroidDocumentInfo::Args{}; + + const LocalRef nameString { (jstring) env->CallObjectMethod (cursor, AndroidCursor.getString, nameColumnIndex) }; + const LocalRef mimeString { (jstring) env->CallObjectMethod (cursor, AndroidCursor.getString, mimeColumnIndex) }; + + const auto readOpt = [&] (int column) -> Detail::Opt + { + const auto missing = env->CallBooleanMethod (cursor, AndroidCursor.isNull, column); + + if (missing) + return {}; + + return Detail::Opt { env->CallLongMethod (cursor, AndroidCursor.getLong, column) }; + }; + + return AndroidDocumentInfo::Args{}.withName (juceString (nameString.get())) + .withType (juceString (mimeString.get())) + .withFlags (env->CallIntMethod (cursor, AndroidCursor.getInt, flagsColumnIndex)) + .withModified (readOpt (modColumnIndex)) + .withSize (readOpt (sizeColumnIndex)); + } + + GlobalRefImpl flagsColumn { javaString ("flags") }; + GlobalRefImpl nameColumn { javaString ("_display_name") }; + GlobalRefImpl mimeColumn { javaString ("mime_type") }; + GlobalRefImpl idColumn { javaString ("document_id") }; + GlobalRefImpl modifiedColumn { javaString ("last_modified") }; + GlobalRefImpl sizeColumn { javaString ("_size") }; + }; + + Detail::AndroidIteratorEngine iterator { Columns{}, uri }; + + if (! iterator.increment()) + return AndroidDocumentInfo{}; + + auto* env = getEnv(); + auto ctx = getAppContext(); + + const auto hasPermission = [&] (auto permission) + { + return env->CallIntMethod (ctx, AndroidContext.checkCallingOrSelfUriPermission, uri.get(), permission) == 0; + }; + + return iterator.read() + .withReadPermission (hasPermission (Detail::FLAG_GRANT_READ_URI_PERMISSION)) + .withWritePermission (hasPermission (Detail::FLAG_GRANT_WRITE_URI_PERMISSION)) + .build(); + } + + URL getUrl() const override + { + return Detail::uriToUrl (uri); + } + + NativeInfo getNativeInfo() const override { return { uri }; } + + private: + template + std::unique_ptr makeStream (AndroidStreamHelpers::StreamKind kind) const + { + auto stream = AndroidStreamHelpers::createStream (uri, kind); + + return stream.get() != nullptr ? std::make_unique (std::move (stream)) + : nullptr; + } + + GlobalRef uri; + }; + + //============================================================================== + class AndroidDocumentPimplApi21 : public AndroidDocumentPimplApi19 + { + public: + using AndroidDocumentPimplApi19::AndroidDocumentPimplApi19; + + std::unique_ptr clone() const override { return std::make_unique (*this); } + + std::unique_ptr createChildDocumentWithTypeAndName (const String& type, const String& name) const override + { + return Utils::createPimplForSdk (LocalRef { getEnv()->CallStaticObjectMethod (DocumentsContract21, + DocumentsContract21.createDocument, + AndroidContentUriResolver::getContentResolver().get(), + getNativeInfo().uri.get(), + javaString (type).get(), + javaString (name).get()) }); + } + + std::unique_ptr renameTo (const String& name) const override + { + if (const auto resolver = AndroidContentUriResolver::getContentResolver()) + { + return Utils::createPimplForSdk (LocalRef { getEnv()->CallStaticObjectMethod (DocumentsContract21, + DocumentsContract21.renameDocument, + resolver.get(), + getNativeInfo().uri.get(), + javaString (name).get()) }); + } + + return nullptr; + } + }; + + //============================================================================== + class AndroidDocumentPimplApi24 final : public AndroidDocumentPimplApi21 + { + public: + using AndroidDocumentPimplApi21::AndroidDocumentPimplApi21; + + std::unique_ptr clone() const override { return std::make_unique (*this); } + + std::unique_ptr copyDocumentToParentDocument (const Pimpl& target) const override + { + if (target.getNativeInfo().uri == nullptr) + { + // Cannot copy to a non-URI-based AndroidDocument + return {}; + } + + return Utils::createPimplForSdk (LocalRef { getEnv()->CallStaticObjectMethod (DocumentsContract24, + DocumentsContract24.copyDocument, + AndroidContentUriResolver::getContentResolver().get(), + getNativeInfo().uri.get(), + target.getNativeInfo().uri.get()) }); + } + + std::unique_ptr moveDocumentFromParentToParent (const Pimpl& currentParent, const Pimpl& newParent) const override + { + if (currentParent.getNativeInfo().uri == nullptr || newParent.getNativeInfo().uri == nullptr) + { + // Cannot move document between non-URI-based AndroidDocuments + return {}; + } + + return Utils::createPimplForSdk (LocalRef { getEnv()->CallStaticObjectMethod (DocumentsContract24, + DocumentsContract24.moveDocument, + AndroidContentUriResolver::getContentResolver().get(), + getNativeInfo().uri.get(), + currentParent.getNativeInfo().uri.get(), + newParent.getNativeInfo().uri.get()) }); + } + }; + + static std::unique_ptr createPimplForSdk (const LocalRef& uri) + { + if (jniCheckHasExceptionOccurredAndClear()) + return nullptr; + + return createPimplForSdkImpl (uri, + VersionTag { 24 }, + VersionTag { 21 }, + VersionTag { 19 }); + } + + static std::unique_ptr createPimplForSdkImpl (const LocalRef&) + { + // Failed to find a suitable implementation for this platform + jassertfalse; + return nullptr; + } + + template + static std::unique_ptr createPimplForSdkImpl (const LocalRef& uri, + VersionTag head, + VersionTag... tail) + { + if (head.version <= getAndroidSDKVersion()) + return std::make_unique (uri); + + return createPimplForSdkImpl (uri, tail...); + } + + #else + class MimeConverter + { + public: + static String getMimeTypeFromExtension (const String& str) + { + return MimeTypeTable::getMimeTypesForFileExtension (str)[0]; + } + + static String getExtensionFromMimeType (const String& str) + { + return MimeTypeTable::getFileExtensionsForMimeType (str)[0]; + } + }; + #endif + + //============================================================================== + class AndroidDocumentPimplFile final : public Pimpl + { + public: + explicit AndroidDocumentPimplFile (const File& f) + : file (f) + { + } + + std::unique_ptr clone() const override { return std::make_unique (*this); } + + bool deleteDocument() const override + { + return file.deleteRecursively (false); + } + + std::unique_ptr renameTo (const String& name) const override + { + const auto target = file.getSiblingFile (name); + + return file.moveFileTo (target) ? std::make_unique (target) + : nullptr; + } + + std::unique_ptr createInputStream() const override { return file.createInputStream(); } + + std::unique_ptr createOutputStream() const override + { + auto result = file.createOutputStream(); + result->setPosition (0); + result->truncate(); + return result; + } + + std::unique_ptr copyDocumentToParentDocument (const Pimpl& target) const override + { + const auto parent = target.getFile(); + + if (parent == File()) + return nullptr; + + const auto actual = parent.getChildFile (file.getFileName()); + + if (actual.exists()) + return nullptr; + + const auto success = file.isDirectory() ? file.copyDirectoryTo (actual) + : file.copyFileTo (actual); + + return success ? std::make_unique (actual) + : nullptr; + } + + std::unique_ptr createChildDocumentWithTypeAndName (const String& type, + const String& name) const override + { + const auto extension = mimeConverter.getExtensionFromMimeType (type); + const auto target = file.getChildFile (extension.isNotEmpty() ? name + "." + extension : name); + + if (! target.exists() && (type == Detail::dirMime ? target.createDirectory() : target.create())) + return std::make_unique (target); + + return nullptr; + } + + std::unique_ptr moveDocumentFromParentToParent (const Pimpl& currentParentPimpl, + const Pimpl& newParentPimpl) const override + { + const auto currentParent = currentParentPimpl.getFile(); + const auto newParent = newParentPimpl.getFile(); + + if (! file.isAChildOf (currentParent) || newParent == File()) + return nullptr; + + const auto target = newParent.getChildFile (file.getFileName()); + + if (target.exists() || ! file.moveFileTo (target)) + return nullptr; + + return std::make_unique (target); + } + + AndroidDocumentInfo getInfo() const override + { + if (! file.exists()) + return AndroidDocumentInfo{}; + + const auto size = file.getSize(); + const auto extension = file.getFileExtension().removeCharacters (".").toLowerCase(); + const auto type = file.isDirectory() ? Detail::dirMime + : mimeConverter.getMimeTypeFromExtension (extension); + + return AndroidDocumentInfo::Args{}.withName (file.getFileName()) + .withType (type.isNotEmpty() ? type : "application/octet-stream") + .withFlags (AndroidDocumentInfo::Args::getFlagsForFile (file)) + .withModified (Detail::Opt { file.getLastModificationTime().toMilliseconds() }) + .withSize (size != 0 ? Detail::Opt { size } : Detail::Opt{}) + .withReadPermission (file.hasReadAccess()) + .withWritePermission (file.hasWriteAccess()) + .build(); + } + + URL getUrl() const override { return URL (file); } + + NativeInfo getNativeInfo() const override { return {}; } + + private: + File file; + MimeConverter mimeConverter; + }; +}; + +//============================================================================== +void AndroidDocumentPermission::takePersistentReadWriteAccess (const URL& url) +{ + #if JUCE_ANDROID + AndroidDocumentDetail::setPermissions (url, ContentResolver19.takePersistableUriPermission); + #else + ignoreUnused (url); + #endif +} + +void AndroidDocumentPermission::releasePersistentReadWriteAccess (const URL& url) +{ + #if JUCE_ANDROID + AndroidDocumentDetail::setPermissions (url, ContentResolver19.releasePersistableUriPermission); + #else + ignoreUnused (url); + #endif +} + +std::vector AndroidDocumentPermission::getPersistedPermissions() +{ + #if ! JUCE_ANDROID + return {}; + #else + if (getAndroidSDKVersion() < 19) + return {}; + + auto* env = getEnv(); + const LocalRef permissions { env->CallObjectMethod (AndroidContentUriResolver::getContentResolver().get(), + ContentResolver19.getPersistedUriPermissions) }; + + if (permissions == nullptr) + return {}; + + std::vector result; + const auto size = env->CallIntMethod (permissions, JavaList.size); + + for (auto i = (decltype (size)) 0; i < size; ++i) + { + const LocalRef uriPermission { env->CallObjectMethod (permissions, JavaList.get, i) }; + + AndroidDocumentPermission permission; + permission.time = env->CallLongMethod (uriPermission, AndroidUriPermission.getPersistedTime); + permission.read = env->CallBooleanMethod (uriPermission, AndroidUriPermission.isReadPermission); + permission.write = env->CallBooleanMethod (uriPermission, AndroidUriPermission.isWritePermission); + permission.url = AndroidDocumentDetail::uriToUrl (env->CallObjectMethod (uriPermission, AndroidUriPermission.getUri)); + + result.push_back (std::move (permission)); + } + + return result; + #endif +} + +//============================================================================== +AndroidDocument::AndroidDocument() = default; + +AndroidDocument AndroidDocument::fromFile (const File& filePath) +{ + #if JUCE_ANDROID + const LocalRef info { getEnv()->CallObjectMethod (getAppContext(), AndroidContext.getApplicationInfo) }; + const auto targetSdkVersion = getEnv()->GetIntField (info.get(), AndroidApplicationInfo.targetSdkVersion); + + // At the current API level, plain file paths may not work for accessing files in shared + // locations. It's recommended to use fromDocument() or fromTree() instead when targeting this + // API level. + jassert (__ANDROID_API_Q__ <= targetSdkVersion); + #endif + + return AndroidDocument { filePath != File() ? std::make_unique (filePath) + : nullptr }; +} + +AndroidDocument AndroidDocument::fromDocument (const URL& documentUrl) +{ + #if JUCE_ANDROID + if (getAndroidSDKVersion() < 19) + { + // This function is unsupported on this platform. + jassertfalse; + return AndroidDocument{}; + } + + const auto javaUri = urlToUri (documentUrl); + + if (! getEnv()->CallStaticBooleanMethod (DocumentsContract19, + DocumentsContract19.isDocumentUri, + getAppContext().get(), + javaUri.get())) + { + return AndroidDocument{}; + } + + return AndroidDocument { Utils::createPimplForSdk (javaUri) }; + #else + ignoreUnused (documentUrl); + return AndroidDocument{}; + #endif +} + +AndroidDocument AndroidDocument::fromTree (const URL& treeUrl) +{ + #if JUCE_ANDROID + if (getAndroidSDKVersion() < 21) + { + // This function is unsupported on this platform. + jassertfalse; + return AndroidDocument{}; + } + + const auto javaUri = urlToUri (treeUrl); + LocalRef treeDocumentId { getEnv()->CallStaticObjectMethod (DocumentsContract21, + DocumentsContract21.getTreeDocumentId, + javaUri.get()) }; + + jniCheckHasExceptionOccurredAndClear(); + + if (treeDocumentId == nullptr) + { + jassertfalse; + return AndroidDocument{}; + } + + LocalRef documentUri { getEnv()->CallStaticObjectMethod (DocumentsContract21, + DocumentsContract21.buildDocumentUriUsingTree, + javaUri.get(), + treeDocumentId.get()) }; + + return AndroidDocument { Utils::createPimplForSdk (documentUri) }; + #else + ignoreUnused (treeUrl); + return AndroidDocument{}; + #endif +} + +AndroidDocument::AndroidDocument (const AndroidDocument& other) + : AndroidDocument (other.pimpl != nullptr ? other.pimpl->clone() : nullptr) {} + +AndroidDocument::AndroidDocument (std::unique_ptr pimplIn) + : pimpl (std::move (pimplIn)) {} + +AndroidDocument::AndroidDocument (AndroidDocument&&) noexcept = default; + +AndroidDocument& AndroidDocument::operator= (const AndroidDocument& other) +{ + AndroidDocument { other }.swap (*this); + return *this; +} + +AndroidDocument& AndroidDocument::operator= (AndroidDocument&&) noexcept = default; + +AndroidDocument::~AndroidDocument() = default; + +bool AndroidDocument::deleteDocument() const { return pimpl->deleteDocument(); } + +bool AndroidDocument::renameTo (const String& newDisplayName) +{ + jassert (hasValue()); + + auto renamed = pimpl->renameTo (newDisplayName); + + if (renamed == nullptr) + return false; + + pimpl = std::move (renamed); + return true; +} + +AndroidDocument AndroidDocument::copyDocumentToParentDocument (const AndroidDocument& target) const +{ + jassert (hasValue() && target.hasValue()); + return AndroidDocument { pimpl->copyDocumentToParentDocument (*target.pimpl) }; +} + +AndroidDocument AndroidDocument::createChildDocumentWithTypeAndName (const String& type, + const String& name) const +{ + jassert (hasValue()); + return AndroidDocument { pimpl->createChildDocumentWithTypeAndName (type, name) }; +} + +AndroidDocument AndroidDocument::createChildDirectory (const String& name) const +{ + return createChildDocumentWithTypeAndName (AndroidDocumentDetail::dirMime, name); +} + +bool AndroidDocument::moveDocumentFromParentToParent (const AndroidDocument& currentParent, + const AndroidDocument& newParent) +{ + jassert (hasValue() && currentParent.hasValue() && newParent.hasValue()); + auto moved = pimpl->moveDocumentFromParentToParent (*currentParent.pimpl, *newParent.pimpl); + + if (moved == nullptr) + return false; + + pimpl = std::move (moved); + return true; +} + +std::unique_ptr AndroidDocument::createInputStream() const +{ + jassert (hasValue()); + return pimpl->createInputStream(); +} + +std::unique_ptr AndroidDocument::createOutputStream() const +{ + jassert (hasValue()); + return pimpl->createOutputStream(); +} + +URL AndroidDocument::getUrl() const +{ + jassert (hasValue()); + return pimpl->getUrl(); +} + +AndroidDocumentInfo AndroidDocument::getInfo() const +{ + jassert (hasValue()); + return pimpl->getInfo(); +} + +bool AndroidDocument::operator== (const AndroidDocument& other) const +{ + return getUrl() == other.getUrl(); +} + +bool AndroidDocument::operator!= (const AndroidDocument& other) const +{ + return ! operator== (other); +} + +AndroidDocument::NativeInfo AndroidDocument::getNativeInfo() const +{ + jassert (hasValue()); + return pimpl->getNativeInfo(); +} + +//============================================================================== +struct AndroidDocumentIterator::Pimpl +{ + virtual ~Pimpl() = default; + virtual AndroidDocument read() const = 0; + virtual bool increment() = 0; +}; + +struct AndroidDocumentIterator::Utils +{ + using Detail = AndroidDocumentDetail; + + ~Utils() = delete; // This struct is a single-file namespace + + template + struct TemplatePimpl final : public Pimpl, public Engine + { + template + TemplatePimpl (Args&&... args) : Engine (std::forward (args)...) {} + + AndroidDocument read() const override { return Engine::read(); } + bool increment() override { return Engine::increment(); } + }; + + template + static AndroidDocumentIterator makeWithEngineInplace (Args&&... args) + { + return AndroidDocumentIterator { std::make_unique> (std::forward (args)...) }; + } + + template + static AndroidDocumentIterator makeWithEngine (Engine engine) + { + return AndroidDocumentIterator { std::make_unique> (std::move (engine)) }; + } + + static void increment (AndroidDocumentIterator& it) + { + if (it.pimpl == nullptr || ! it.pimpl->increment()) + it.pimpl = nullptr; + } +}; + +//============================================================================== +AndroidDocumentIterator AndroidDocumentIterator::makeNonRecursive (const AndroidDocument& dir) +{ + if (! dir.hasValue()) + return {}; + + using Detail = AndroidDocumentDetail; + + #if JUCE_ANDROID + if (21 <= getAndroidSDKVersion()) + { + if (auto uri = dir.getNativeInfo().uri) + return Utils::makeWithEngine (Detail::makeDocumentsContractIteratorEngine (uri)); + } + #endif + + return Utils::makeWithEngineInplace (dir.getUrl().getLocalFile(), false); +} + +AndroidDocumentIterator AndroidDocumentIterator::makeRecursive (const AndroidDocument& dir) +{ + if (! dir.hasValue()) + return {}; + + using Detail = AndroidDocumentDetail; + + #if JUCE_ANDROID + if (21 <= getAndroidSDKVersion()) + { + if (auto uri = dir.getNativeInfo().uri) + return Utils::makeWithEngine (Detail::RecursiveEngine { uri }); + } + #endif + + return Utils::makeWithEngineInplace (dir.getUrl().getLocalFile(), true); +} + +AndroidDocumentIterator::AndroidDocumentIterator (std::unique_ptr engine) + : pimpl (std::move (engine)) +{ + Utils::increment (*this); +} + +AndroidDocument AndroidDocumentIterator::operator*() const { return pimpl->read(); } + +AndroidDocumentIterator& AndroidDocumentIterator::operator++() +{ + Utils::increment (*this); + return *this; +} + +} // namespace juce diff --git a/modules/juce_core/native/juce_android_Files.cpp b/modules/juce_core/native/juce_android_Files.cpp index ef99443f83..e701c713f1 100644 --- a/modules/juce_core/native/juce_android_Files.cpp +++ b/modules/juce_core/native/juce_android_Files.cpp @@ -33,18 +33,30 @@ DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection #undef JNI_CLASS_MEMBERS #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - METHOD (query, "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;") \ - METHOD (openInputStream, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;") \ - METHOD (openOutputStream, "openOutputStream", "(Landroid/net/Uri;)Ljava/io/OutputStream;") + METHOD (query, "query", "(Landroid/net/Uri;[Ljava/lang/String;Ljava/lang/String;[Ljava/lang/String;Ljava/lang/String;)Landroid/database/Cursor;") \ + METHOD (openInputStream, "openInputStream", "(Landroid/net/Uri;)Ljava/io/InputStream;") \ + METHOD (openOutputStream, "openOutputStream", "(Landroid/net/Uri;)Ljava/io/OutputStream;") DECLARE_JNI_CLASS (ContentResolver, "android/content/ContentResolver") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (takePersistableUriPermission, "takePersistableUriPermission", "(Landroid/net/Uri;I)V") \ + METHOD (releasePersistableUriPermission, "releasePersistableUriPermission", "(Landroid/net/Uri;I)V") \ + METHOD (getPersistedUriPermissions, "getPersistedUriPermissions", "()Ljava/util/List;") + +DECLARE_JNI_CLASS (ContentResolver19, "android/content/ContentResolver") +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (moveToFirst, "moveToFirst", "()Z") \ + METHOD (moveToNext, "moveToNext", "()Z") \ METHOD (getColumnIndex, "getColumnIndex", "(Ljava/lang/String;)I") \ METHOD (getString, "getString", "(I)Ljava/lang/String;") \ - METHOD (close, "close", "()V") \ + METHOD (isNull, "isNull", "(I)Z") \ + METHOD (getInt, "getInt", "(I)I") \ + METHOD (getLong, "getLong", "(I)J") \ + METHOD (close, "close", "()V") DECLARE_JNI_CLASS (AndroidCursor, "android/database/Cursor") #undef JNI_CLASS_MEMBERS @@ -65,14 +77,73 @@ DECLARE_JNI_CLASS (AndroidEnvironment, "android/os/Environment") DECLARE_JNI_CLASS (AndroidOutputStream, "java/io/OutputStream") #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (close, "close", "()V") \ + METHOD (read, "read", "([B)I") + +DECLARE_JNI_CLASS (AndroidInputStream, "java/io/InputStream") +#undef JNI_CLASS_MEMBERS + #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ FIELD (publicSourceDir, "publicSourceDir", "Ljava/lang/String;") \ - FIELD (dataDir, "dataDir", "Ljava/lang/String;") + FIELD (dataDir, "dataDir", "Ljava/lang/String;") \ + FIELD (targetSdkVersion, "targetSdkVersion", "I") DECLARE_JNI_CLASS (AndroidApplicationInfo, "android/content/pm/ApplicationInfo") #undef JNI_CLASS_MEMBERS -//============================================================================== +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (buildChildDocumentsUri, "buildChildDocumentsUri", "(Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildDocumentUri, "buildDocumentUri", "(Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildRecentDocumentsUri, "buildRecentDocumentsUri", "(Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildRootUri, "buildRootUri", "(Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildRootsUri, "buildRootsUri", "(Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildSearchDocumentsUri, "buildSearchDocumentsUri", "(Ljava/lang/String;Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (deleteDocument, "deleteDocument", "(Landroid/content/ContentResolver;Landroid/net/Uri;)Z") \ + STATICMETHOD (getDocumentId, "getDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;") \ + STATICMETHOD (getRootId, "getRootId", "(Landroid/net/Uri;)Ljava/lang/String;") \ + STATICMETHOD (isDocumentUri, "isDocumentUri", "(Landroid/content/Context;Landroid/net/Uri;)Z") + +DECLARE_JNI_CLASS_WITH_MIN_SDK (DocumentsContract19, "android/provider/DocumentsContract", 19) +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (buildChildDocumentsUriUsingTree, "buildChildDocumentsUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildDocumentUriUsingTree, "buildDocumentUriUsingTree", "(Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (buildTreeDocumentUri, "buildTreeDocumentUri", "(Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (createDocument, "createDocument", "(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;Ljava/lang/String;)Landroid/net/Uri;") \ + STATICMETHOD (getTreeDocumentId, "getTreeDocumentId", "(Landroid/net/Uri;)Ljava/lang/String;") \ + STATICMETHOD (renameDocument, "renameDocument", "(Landroid/content/ContentResolver;Landroid/net/Uri;Ljava/lang/String;)Landroid/net/Uri;") + +DECLARE_JNI_CLASS_WITH_MIN_SDK (DocumentsContract21, "android/provider/DocumentsContract", 21) +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (copyDocument, "copyDocument", "(Landroid/content/ContentResolver;Landroid/net/Uri;Landroid/net/Uri;)Landroid/net/Uri;") \ + STATICMETHOD (moveDocument, "moveDocument", "(Landroid/content/ContentResolver;Landroid/net/Uri;Landroid/net/Uri;Landroid/net/Uri;)Landroid/net/Uri;") \ + STATICMETHOD (removeDocument, "removeDocument", "(Landroid/content/ContentResolver;Landroid/net/Uri;Landroid/net/Uri;)Z") + +DECLARE_JNI_CLASS_WITH_MIN_SDK (DocumentsContract24, "android/provider/DocumentsContract", 24) +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + STATICMETHOD (getSingleton, "getSingleton", "()Landroid/webkit/MimeTypeMap;") \ + METHOD (getExtensionFromMimeType, "getExtensionFromMimeType", "(Ljava/lang/String;)Ljava/lang/String;") \ + METHOD (getMimeTypeFromExtension, "getMimeTypeFromExtension", "(Ljava/lang/String;)Ljava/lang/String;") + +DECLARE_JNI_CLASS (AndroidMimeTypeMap, "android/webkit/MimeTypeMap") +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (getPersistedTime, "getPersistedTime", "()J") \ + METHOD (getUri, "getUri", "()Landroid/net/Uri;") \ + METHOD (isReadPermission, "isReadPermission", "()Z") \ + METHOD (isWritePermission, "isWritePermission", "()Z") + +DECLARE_JNI_CLASS_WITH_MIN_SDK (AndroidUriPermission, "android/content/UriPermission", 19) +#undef JNI_CLASS_MEMBERS + + //============================================================================== static File juceFile (LocalRef obj) { auto* env = getEnv(); @@ -118,21 +189,9 @@ static LocalRef urlToUri (const URL& url) struct AndroidContentUriResolver { public: - static LocalRef getStreamForContentUri (const URL& url, bool inputStream) + static LocalRef getContentResolver() { - // only use this method for content URIs - jassert (url.getScheme() == "content"); - auto* env = getEnv(); - - LocalRef contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); - - if (contentResolver) - return LocalRef ((env->CallObjectMethod (contentResolver.get(), - inputStream ? ContentResolver.openInputStream - : ContentResolver.openOutputStream, - urlToUri (url).get()))); - - return LocalRef(); + return LocalRef (getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); } static File getLocalFileFromContentUri (const URL& url) @@ -160,18 +219,15 @@ public: auto downloadId = tokens[1]; if (type.equalsIgnoreCase ("raw")) - { return File (downloadId); - } - else if (type.equalsIgnoreCase ("downloads")) + + if (type.equalsIgnoreCase ("downloads")) { auto subDownloadPath = url.getSubPath().fromFirstOccurrenceOf ("tree/downloads", false, false); return File (getWellKnownFolder ("DIRECTORY_DOWNLOADS").getFullPathName() + "/" + subDownloadPath); } - else - { - return getLocalFileFromContentUri (URL ("content://downloads/public_downloads/" + documentId)); - } + + return getLocalFileFromContentUri (URL ("content://downloads/public_downloads/" + documentId)); } else if (authority == "com.android.providers.media.documents" && documentId.isNotEmpty()) { @@ -192,7 +248,7 @@ public: { auto uri = urlToUri (url); auto* env = getEnv(); - LocalRef contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); + const auto contentResolver = getContentResolver(); if (contentResolver == nullptr) return {}; @@ -216,7 +272,7 @@ private: { auto uri = urlToUri (url); auto* env = getEnv(); - LocalRef contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); + const auto contentResolver = getContentResolver(); if (contentResolver) { @@ -285,8 +341,7 @@ private: static File getPrimaryStorageDirectory() { - auto* env = getEnv(); - return juceFile (LocalRef (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); + return juceFile (LocalRef (getEnv()->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); } static Array getSecondaryStorageDirectories() @@ -433,10 +488,8 @@ private: //============================================================================== struct AndroidContentUriOutputStream : public OutputStream { - AndroidContentUriOutputStream (LocalRef&& outputStream) - : stream (outputStream) - { - } + explicit AndroidContentUriOutputStream (LocalRef&& streamIn) + : stream (std::move (streamIn)) {} ~AndroidContentUriOutputStream() override { @@ -479,12 +532,79 @@ struct AndroidContentUriOutputStream : public OutputStream int64 pos = 0; }; -OutputStream* juce_CreateContentURIOutputStream (const URL& url) +//============================================================================== +class CachedByteArray { - auto stream = AndroidContentUriResolver::getStreamForContentUri (url, false); +public: + CachedByteArray() = default; - return (stream.get() != nullptr ? new AndroidContentUriOutputStream (std::move (stream)) : nullptr); -} + explicit CachedByteArray (jsize sizeIn) + : byteArray { LocalRef { getEnv()->NewByteArray (sizeIn) } }, + size (sizeIn) {} + + jbyteArray getNativeArray() const { return byteArray.get(); } + jsize getSize() const { return size; } + +private: + GlobalRefImpl byteArray; + jsize size = 0; +}; + +//============================================================================== +struct AndroidContentUriInputStream : public InputStream +{ + explicit AndroidContentUriInputStream (LocalRef&& streamIn) + : stream (std::move (streamIn)) {} + + ~AndroidContentUriInputStream() override + { + getEnv()->CallVoidMethod (stream.get(), AndroidInputStream.close); + } + + int64 getTotalLength() override { return -1; } + + bool isExhausted() override { return exhausted; } + + int read (void* destBuffer, int maxBytesToRead) override + { + auto* env = getEnv(); + + if ((jsize) maxBytesToRead > byteArray.getSize()) + byteArray = CachedByteArray { (jsize) maxBytesToRead }; + + const auto result = env->CallIntMethod (stream.get(), AndroidInputStream.read, byteArray.getNativeArray()); + + if (result != -1) + { + pos += result; + + auto* rawBytes = env->GetByteArrayElements (byteArray.getNativeArray(), nullptr); + std::memcpy (destBuffer, rawBytes, static_cast (result)); + env->ReleaseByteArrayElements (byteArray.getNativeArray(), rawBytes, 0); + } + else + { + exhausted = true; + } + + return result; + } + + bool setPosition (int64 newPos) override + { + return (newPos == pos); + } + + int64 getPosition() override + { + return pos; + } + + CachedByteArray byteArray; + GlobalRef stream; + int64 pos = 0; + bool exhausted = false; +}; //============================================================================== class MediaScannerConnectionClient : public AndroidInterfaceImplementer diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 4131d326fb..23f1d02f1a 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -31,11 +31,11 @@ template class LocalRef { public: - explicit inline LocalRef() noexcept : obj (nullptr) {} - explicit inline LocalRef (JavaType o) noexcept : obj (o) {} - inline LocalRef (const LocalRef& other) noexcept : obj (retain (other.obj)) {} - inline LocalRef (LocalRef&& other) noexcept : obj (nullptr) { std::swap (obj, other.obj); } - ~LocalRef() { clear(); } + LocalRef() noexcept : obj (nullptr) {} + explicit LocalRef (JavaType o) noexcept : obj (o) {} + LocalRef (const LocalRef& other) noexcept : obj (retain (other.obj)) {} + LocalRef (LocalRef&& other) noexcept : obj (nullptr) { std::swap (obj, other.obj); } + ~LocalRef() { clear(); } void clear() { @@ -61,8 +61,8 @@ public: return *this; } - inline operator JavaType() const noexcept { return obj; } - inline JavaType get() const noexcept { return obj; } + operator JavaType() const noexcept { return obj; } + JavaType get() const noexcept { return obj; } private: JavaType obj; @@ -74,19 +74,19 @@ private: }; //============================================================================== -class GlobalRef +template +class GlobalRefImpl { public: - inline GlobalRef() noexcept : obj (nullptr) {} - inline explicit GlobalRef (const LocalRef& o) : obj (retain (o.get(), getEnv())) {} - inline explicit GlobalRef (const LocalRef& o, JNIEnv* env) : obj (retain (o.get(), env)) {} - inline GlobalRef (const GlobalRef& other) : obj (retain (other.obj, getEnv())) {} - inline GlobalRef (GlobalRef && other) noexcept : obj (nullptr) { std::swap (other.obj, obj); } - ~GlobalRef() { clear(); } - - - inline void clear() { if (obj != nullptr) clear (getEnv()); } - inline void clear (JNIEnv* env) + GlobalRefImpl() noexcept : obj (nullptr) {} + explicit GlobalRefImpl (const LocalRef& o) : obj (retain (o.get(), getEnv())) {} + GlobalRefImpl (const LocalRef& o, JNIEnv* env) : obj (retain (o.get(), env)) {} + GlobalRefImpl (const GlobalRefImpl& other) : obj (retain (other.obj, getEnv())) {} + GlobalRefImpl (GlobalRefImpl&& other) noexcept : obj (nullptr) { std::swap (other.obj, obj); } + ~GlobalRefImpl() { clear(); } + + void clear() { if (obj != nullptr) clear (getEnv()); } + void clear (JNIEnv* env) { if (obj != nullptr) { @@ -95,15 +95,15 @@ public: } } - inline GlobalRef& operator= (const GlobalRef& other) + GlobalRefImpl& operator= (const GlobalRefImpl& other) { - jobject newObj = retain (other.obj, getEnv()); + JavaType newObj = retain (other.obj, getEnv()); clear(); obj = newObj; return *this; } - inline GlobalRef& operator= (GlobalRef&& other) + GlobalRefImpl& operator= (GlobalRefImpl&& other) { clear(); std::swap (obj, other.obj); @@ -112,8 +112,8 @@ public: } //============================================================================== - inline operator jobject() const noexcept { return obj; } - inline jobject get() const noexcept { return obj; } + operator JavaType() const noexcept { return obj; } + JavaType get() const noexcept { return obj; } //============================================================================== #define DECLARE_CALL_TYPE_METHOD(returnType, typeName) \ @@ -147,14 +147,20 @@ public: private: //============================================================================== - jobject obj = nullptr; + JavaType obj = nullptr; - static jobject retain (jobject obj, JNIEnv* env) + static JavaType retain (JavaType obj, JNIEnv* env) { - return obj == nullptr ? nullptr : env->NewGlobalRef (obj); + return obj != nullptr ? static_cast (env->NewGlobalRef (obj)) + : nullptr; } }; +class GlobalRef : public GlobalRefImpl +{ +public: + using GlobalRefImpl::GlobalRefImpl; +}; //============================================================================== extern LocalRef getAppContext() noexcept; @@ -166,15 +172,15 @@ struct SystemJavaClassComparator; class JNIClassBase { public: - explicit JNIClassBase (const char* classPath, int minSDK, const void* byteCode, size_t byteCodeSize); + JNIClassBase (const char* classPath, int minSDK, const void* byteCode, size_t byteCodeSize); virtual ~JNIClassBase(); - inline operator jclass() const noexcept { return classRef; } + operator jclass() const noexcept { return classRef; } static void initialiseAllClasses (JNIEnv*); static void releaseAllClasses (JNIEnv*); - inline const char* getClassPath() const noexcept { return classPath; } + const char* getClassPath() const noexcept { return classPath; } protected: virtual void initialiseFields (JNIEnv*) = 0; @@ -252,6 +258,7 @@ private: METHOD (getApplicationContext, "getApplicationContext", "()Landroid/content/Context;") \ METHOD (getApplicationInfo, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;") \ METHOD (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)I") \ + METHOD (checkCallingOrSelfUriPermission, "checkCallingOrSelfUriPermission", "(Landroid/net/Uri;I)I") \ METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") DECLARE_JNI_CLASS (AndroidContext, "android/content/Context") @@ -392,6 +399,7 @@ DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread") METHOD (getAction, "getAction", "()Ljava/lang/String;") \ METHOD (getCategories, "getCategories", "()Ljava/util/Set;") \ METHOD (getData, "getData", "()Landroid/net/Uri;") \ + METHOD (getClipData, "getClipData", "()Landroid/content/ClipData;") \ METHOD (getExtras, "getExtras", "()Landroid/os/Bundle;") \ METHOD (getIntExtra, "getIntExtra", "(Ljava/lang/String;I)I") \ METHOD (getStringExtra, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;") \ @@ -400,6 +408,7 @@ DECLARE_JNI_CLASS (AndroidHandlerThread, "android/os/HandlerThread") METHOD (putExtraString, "putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (putExtraStrings, "putExtra", "(Ljava/lang/String;[Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (putExtraParcelable, "putExtra", "(Ljava/lang/String;Landroid/os/Parcelable;)Landroid/content/Intent;") \ + METHOD (putExtraBool, "putExtra", "(Ljava/lang/String;Z)Landroid/content/Intent;") \ METHOD (putParcelableArrayListExtra, "putParcelableArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;") \ METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \ METHOD (setFlags, "setFlags", "(I)Landroid/content/Intent;") \ @@ -596,6 +605,9 @@ DECLARE_JNI_CLASS (JavaBoolean, "java/lang/Boolean") #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (get, "get", "([B)Ljava/nio/ByteBuffer;") \ METHOD (remaining, "remaining", "()I") \ + METHOD (hasArray, "hasArray", "()Z") \ + METHOD (array, "array", "()[B") \ + METHOD (setOrder, "order", "(Ljava/nio/ByteOrder;)Ljava/nio/ByteBuffer;") \ STATICMETHOD (wrap, "wrap", "([B)Ljava/nio/ByteBuffer;") DECLARE_JNI_CLASS (JavaByteBuffer, "java/nio/ByteBuffer") @@ -837,15 +849,12 @@ namespace { auto* env = getEnv(); - LocalRef exception (env->ExceptionOccurred()); - - if (exception != nullptr) - { - env->ExceptionClear(); - return true; - } - - return false; + const auto result = env->ExceptionCheck(); + #if JUCE_DEBUG + env->ExceptionDescribe(); + #endif + env->ExceptionClear(); + return result; } } diff --git a/modules/juce_core/native/juce_android_Network.cpp b/modules/juce_core/native/juce_android_Network.cpp index cdc67e62c8..4603f5cdb7 100644 --- a/modules/juce_core/native/juce_android_Network.cpp +++ b/modules/juce_core/native/juce_android_Network.cpp @@ -198,14 +198,6 @@ DECLARE_JNI_CLASS (StringBuffer, "java/lang/StringBuffer") DECLARE_JNI_CLASS_WITH_BYTECODE (HTTPStream, "com/rmsl/juce/JuceHTTPStream", 16, javaJuceHttpStream, sizeof(javaJuceHttpStream)) #undef JNI_CLASS_MEMBERS -//============================================================================== -#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ - METHOD (close, "close", "()V") \ - METHOD (read, "read", "([BII)I") \ - -DECLARE_JNI_CLASS (AndroidInputStream, "java/io/InputStream") -#undef JNI_CLASS_MEMBERS - //============================================================================== #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ METHOD (acquire, "acquire", "()V") \ @@ -318,6 +310,25 @@ String URL::getFileName() const return toString (false).fromLastOccurrenceOf ("/", false, true); } +struct AndroidStreamHelpers +{ + enum class StreamKind { output, input }; + + static LocalRef createStream (const GlobalRef& uri, StreamKind kind) + { + auto* env = getEnv(); + auto contentResolver = AndroidContentUriResolver::getContentResolver(); + + if (contentResolver == nullptr) + return {}; + + return LocalRef (env->CallObjectMethod (contentResolver.get(), + kind == StreamKind::input ? ContentResolver.openInputStream + : ContentResolver.openOutputStream, + uri.get())); + } +}; + //============================================================================== class WebInputStream::Pimpl { @@ -363,7 +374,8 @@ public: if (isContentURL) { - auto inputStream = AndroidContentUriResolver::getStreamForContentUri (url, true); + GlobalRef urlRef { urlToUri (url) }; + auto inputStream = AndroidStreamHelpers::createStream (urlRef, AndroidStreamHelpers::StreamKind::input); if (inputStream != nullptr) { diff --git a/modules/juce_core/network/juce_URL.cpp b/modules/juce_core/network/juce_URL.cpp index 1a1ba00438..171f602064 100644 --- a/modules/juce_core/network/juce_URL.cpp +++ b/modules/juce_core/network/juce_URL.cpp @@ -796,6 +796,11 @@ OutputStream* juce_CreateContentURIOutputStream (const URL&); std::unique_ptr URL::createOutputStream() const { + #if JUCE_ANDROID + if (auto stream = AndroidDocument::fromDocument (*this).createOutputStream()) + return stream; + #endif + if (isLocalFile()) { #if JUCE_IOS @@ -806,11 +811,7 @@ std::unique_ptr URL::createOutputStream() const #endif } - #if JUCE_ANDROID - return std::unique_ptr (juce_CreateContentURIOutputStream (*this)); - #else return nullptr; - #endif } //============================================================================== diff --git a/modules/juce_gui_basics/juce_gui_basics.cpp b/modules/juce_gui_basics/juce_gui_basics.cpp index 6447746313..8f2ef0b6b7 100644 --- a/modules/juce_gui_basics/juce_gui_basics.cpp +++ b/modules/juce_gui_basics/juce_gui_basics.cpp @@ -333,9 +333,9 @@ JUCE_END_IGNORE_WARNINGS_GCC_LIKE #include "native/juce_linux_FileChooser.cpp" #elif JUCE_ANDROID + #include "juce_core/files/juce_common_MimeTypes.h" #include "native/accessibility/juce_android_Accessibility.cpp" #include "native/juce_android_Windowing.cpp" - #include "native/juce_common_MimeTypes.cpp" #include "native/juce_android_FileChooser.cpp" #if JUCE_CONTENT_SHARING diff --git a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp index 57c86d0e83..29dbbfbaea 100644 --- a/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp +++ b/modules/juce_gui_basics/native/juce_android_ContentSharer.cpp @@ -337,7 +337,7 @@ private: canSpecifyMimeTypes = fileExtension.isNotEmpty(); if (canSpecifyMimeTypes) - mimeTypes.addArray (getMimeTypesForFileExtension (fileExtension)); + mimeTypes.addArray (MimeTypeTable::getMimeTypesForFileExtension (fileExtension)); else mimeTypes.clear(); @@ -597,8 +597,8 @@ public: if (extension.isEmpty()) return nullptr; - return juceStringArrayToJava (filterMimeTypes (getMimeTypesForFileExtension (extension), - juceString (mimeTypeFilter.get()))); + return juceStringArrayToJava (filterMimeTypes (MimeTypeTable::getMimeTypesForFileExtension (extension), + juceString (mimeTypeFilter.get()))); } void sharingFinished (int resultCode) diff --git a/modules/juce_gui_basics/native/juce_android_FileChooser.cpp b/modules/juce_gui_basics/native/juce_android_FileChooser.cpp index b354224e23..9b06b3ab24 100644 --- a/modules/juce_gui_basics/native/juce_android_FileChooser.cpp +++ b/modules/juce_gui_basics/native/juce_android_FileChooser.cpp @@ -26,6 +26,19 @@ namespace juce { +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (getItemCount, "getItemCount", "()I") \ + METHOD (getItemAt, "getItemAt", "(I)Landroid/content/ClipData$Item;") + +DECLARE_JNI_CLASS (ClipData, "android/content/ClipData") +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ + METHOD (getUri, "getUri", "()Landroid/net/Uri;") + +DECLARE_JNI_CLASS (ClipDataItem, "android/content/ClipData$Item") +#undef JNI_CLASS_MEMBERS + class FileChooser::Native : public FileChooser::Pimpl { public: @@ -40,6 +53,7 @@ public: auto sdkVersion = getAndroidSDKVersion(); auto saveMode = ((flags & FileBrowserComponent::saveMode) != 0); auto selectsDirectories = ((flags & FileBrowserComponent::canSelectDirectories) != 0); + auto canSelectMultiple = ((flags & FileBrowserComponent::canSelectMultipleItems) != 0); // You cannot save a directory jassert (! (saveMode && selectsDirectories)); @@ -85,6 +99,13 @@ public: uri.get()); } + if (canSelectMultiple && sdkVersion >= 18) + { + env->CallObjectMethod (intent.get(), + AndroidIntent.putExtraBool, + javaString ("android.intent.extra.ALLOW_MULTIPLE").get(), + true); + } if (! selectsDirectories) { @@ -169,22 +190,41 @@ public: currentFileChooser = nullptr; auto* env = getEnv(); - Array chosenURLs; - - if (resultCode == /*Activity.RESULT_OK*/ -1 && intentData != nullptr) + const auto getUrls = [&]() -> Array { - LocalRef uri (env->CallObjectMethod (intentData.get(), AndroidIntent.getData)); + if (resultCode != /*Activity.RESULT_OK*/ -1 || intentData == nullptr) + return {}; - if (uri != nullptr) - { - auto jStr = (jstring) env->CallObjectMethod (uri, JavaObject.toString); + Array chosenURLs; - if (jStr != nullptr) + const auto addUrl = [env, &chosenURLs] (jobject uri) + { + if (auto jStr = (jstring) env->CallObjectMethod (uri, JavaObject.toString)) chosenURLs.add (URL (juceString (env, jStr))); + }; + + if (LocalRef clipData { env->CallObjectMethod (intentData.get(), AndroidIntent.getClipData) }) + { + const auto count = env->CallIntMethod (clipData.get(), ClipData.getItemCount); + + for (auto i = 0; i < count; ++i) + { + if (LocalRef item { env->CallObjectMethod (clipData.get(), ClipData.getItemAt, i) }) + { + if (LocalRef itemUri { env->CallObjectMethod (item.get(), ClipDataItem.getUri) }) + addUrl (itemUri.get()); + } + } } - } + else if (LocalRef uri { env->CallObjectMethod (intentData.get(), AndroidIntent.getData )}) + { + addUrl (uri.get()); + } + + return chosenURLs; + }; - owner.finished (chosenURLs); + owner.finished (getUrls()); } static Native* currentFileChooser; @@ -200,7 +240,7 @@ public: { auto extension = wildcard.fromLastOccurrenceOf (".", false, false); - result.addArray (getMimeTypesForFileExtension (extension)); + result.addArray (MimeTypeTable::getMimeTypesForFileExtension (extension)); } }