| @@ -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<AndroidDocumentPermission> 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<InputStream> createInputStream() const; | |||||
| /** Creates a stream for writing to this document. */ | |||||
| std::unique_ptr<OutputStream> 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<Pimpl>); | |||||
| void swap (AndroidDocument& other) noexcept { std::swap (other.pimpl, pimpl); } | |||||
| std::unique_ptr<Pimpl> 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<AndroidDocument> findAllChildrenRecursive (const AndroidDocument& parent) | |||||
| { | |||||
| std::vector<AndroidDocument> 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<Pimpl>); | |||||
| std::shared_ptr<Pimpl> pimpl; | |||||
| }; | |||||
| } // namespace juce | |||||
| @@ -33,17 +33,34 @@ struct MimeTypeTableEntry | |||||
| static MimeTypeTableEntry table[641]; | 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; | StringArray result; | ||||
| for (auto type : MimeTypeTableEntry::table) | for (auto type : MimeTypeTableEntry::table) | ||||
| if (fileExtension == type.fileExtension) | |||||
| result.add (type.mimeType); | |||||
| if (toMatch == type.*matchField) | |||||
| result.add (type.*returnField); | |||||
| return result; | 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] = | MimeTypeTableEntry MimeTypeTableEntry::table[641] = | ||||
| { | { | ||||
| @@ -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 | |||||
| @@ -248,6 +248,8 @@ | |||||
| #endif | #endif | ||||
| #include "files/juce_common_MimeTypes.cpp" | |||||
| #include "native/juce_android_AndroidDocument.cpp" | |||||
| #include "threads/juce_HighResolutionTimer.cpp" | #include "threads/juce_HighResolutionTimer.cpp" | ||||
| #include "threads/juce_WaitableEvent.cpp" | #include "threads/juce_WaitableEvent.cpp" | ||||
| #include "network/juce_URL.cpp" | #include "network/juce_URL.cpp" | ||||
| @@ -342,6 +342,7 @@ JUCE_END_IGNORE_WARNINGS_MSVC | |||||
| #include "memory/juce_SharedResourcePointer.h" | #include "memory/juce_SharedResourcePointer.h" | ||||
| #include "memory/juce_AllocationHooks.h" | #include "memory/juce_AllocationHooks.h" | ||||
| #include "memory/juce_Reservoir.h" | #include "memory/juce_Reservoir.h" | ||||
| #include "files/juce_AndroidDocument.h" | |||||
| #if JUCE_CORE_INCLUDE_OBJC_HELPERS && (JUCE_MAC || JUCE_IOS) | #if JUCE_CORE_INCLUDE_OBJC_HELPERS && (JUCE_MAC || JUCE_IOS) | ||||
| #include "native/juce_mac_ObjCHelpers.h" | #include "native/juce_mac_ObjCHelpers.h" | ||||
| @@ -33,18 +33,30 @@ DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection | |||||
| #undef JNI_CLASS_MEMBERS | #undef JNI_CLASS_MEMBERS | ||||
| #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ | #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") | DECLARE_JNI_CLASS (ContentResolver, "android/content/ContentResolver") | ||||
| #undef JNI_CLASS_MEMBERS | #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) \ | #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ | ||||
| METHOD (moveToFirst, "moveToFirst", "()Z") \ | METHOD (moveToFirst, "moveToFirst", "()Z") \ | ||||
| METHOD (moveToNext, "moveToNext", "()Z") \ | |||||
| METHOD (getColumnIndex, "getColumnIndex", "(Ljava/lang/String;)I") \ | METHOD (getColumnIndex, "getColumnIndex", "(Ljava/lang/String;)I") \ | ||||
| METHOD (getString, "getString", "(I)Ljava/lang/String;") \ | 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") | DECLARE_JNI_CLASS (AndroidCursor, "android/database/Cursor") | ||||
| #undef JNI_CLASS_MEMBERS | #undef JNI_CLASS_MEMBERS | ||||
| @@ -65,14 +77,73 @@ DECLARE_JNI_CLASS (AndroidEnvironment, "android/os/Environment") | |||||
| DECLARE_JNI_CLASS (AndroidOutputStream, "java/io/OutputStream") | DECLARE_JNI_CLASS (AndroidOutputStream, "java/io/OutputStream") | ||||
| #undef JNI_CLASS_MEMBERS | #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) \ | #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ | ||||
| FIELD (publicSourceDir, "publicSourceDir", "Ljava/lang/String;") \ | 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") | DECLARE_JNI_CLASS (AndroidApplicationInfo, "android/content/pm/ApplicationInfo") | ||||
| #undef JNI_CLASS_MEMBERS | #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<jobject> obj) | static File juceFile (LocalRef<jobject> obj) | ||||
| { | { | ||||
| auto* env = getEnv(); | auto* env = getEnv(); | ||||
| @@ -118,21 +189,9 @@ static LocalRef<jobject> urlToUri (const URL& url) | |||||
| struct AndroidContentUriResolver | struct AndroidContentUriResolver | ||||
| { | { | ||||
| public: | public: | ||||
| static LocalRef<jobject> getStreamForContentUri (const URL& url, bool inputStream) | |||||
| static LocalRef<jobject> getContentResolver() | |||||
| { | { | ||||
| // only use this method for content URIs | |||||
| jassert (url.getScheme() == "content"); | |||||
| auto* env = getEnv(); | |||||
| LocalRef<jobject> contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); | |||||
| if (contentResolver) | |||||
| return LocalRef<jobject> ((env->CallObjectMethod (contentResolver.get(), | |||||
| inputStream ? ContentResolver.openInputStream | |||||
| : ContentResolver.openOutputStream, | |||||
| urlToUri (url).get()))); | |||||
| return LocalRef<jobject>(); | |||||
| return LocalRef<jobject> (getEnv()->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); | |||||
| } | } | ||||
| static File getLocalFileFromContentUri (const URL& url) | static File getLocalFileFromContentUri (const URL& url) | ||||
| @@ -160,18 +219,15 @@ public: | |||||
| auto downloadId = tokens[1]; | auto downloadId = tokens[1]; | ||||
| if (type.equalsIgnoreCase ("raw")) | if (type.equalsIgnoreCase ("raw")) | ||||
| { | |||||
| return File (downloadId); | return File (downloadId); | ||||
| } | |||||
| else if (type.equalsIgnoreCase ("downloads")) | |||||
| if (type.equalsIgnoreCase ("downloads")) | |||||
| { | { | ||||
| auto subDownloadPath = url.getSubPath().fromFirstOccurrenceOf ("tree/downloads", false, false); | auto subDownloadPath = url.getSubPath().fromFirstOccurrenceOf ("tree/downloads", false, false); | ||||
| return File (getWellKnownFolder ("DIRECTORY_DOWNLOADS").getFullPathName() + "/" + subDownloadPath); | 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()) | else if (authority == "com.android.providers.media.documents" && documentId.isNotEmpty()) | ||||
| { | { | ||||
| @@ -192,7 +248,7 @@ public: | |||||
| { | { | ||||
| auto uri = urlToUri (url); | auto uri = urlToUri (url); | ||||
| auto* env = getEnv(); | auto* env = getEnv(); | ||||
| LocalRef<jobject> contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); | |||||
| const auto contentResolver = getContentResolver(); | |||||
| if (contentResolver == nullptr) | if (contentResolver == nullptr) | ||||
| return {}; | return {}; | ||||
| @@ -216,7 +272,7 @@ private: | |||||
| { | { | ||||
| auto uri = urlToUri (url); | auto uri = urlToUri (url); | ||||
| auto* env = getEnv(); | auto* env = getEnv(); | ||||
| LocalRef<jobject> contentResolver (env->CallObjectMethod (getAppContext().get(), AndroidContext.getContentResolver)); | |||||
| const auto contentResolver = getContentResolver(); | |||||
| if (contentResolver) | if (contentResolver) | ||||
| { | { | ||||
| @@ -285,8 +341,7 @@ private: | |||||
| static File getPrimaryStorageDirectory() | static File getPrimaryStorageDirectory() | ||||
| { | { | ||||
| auto* env = getEnv(); | |||||
| return juceFile (LocalRef<jobject> (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); | |||||
| return juceFile (LocalRef<jobject> (getEnv()->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); | |||||
| } | } | ||||
| static Array<File> getSecondaryStorageDirectories() | static Array<File> getSecondaryStorageDirectories() | ||||
| @@ -433,10 +488,8 @@ private: | |||||
| //============================================================================== | //============================================================================== | ||||
| struct AndroidContentUriOutputStream : public OutputStream | struct AndroidContentUriOutputStream : public OutputStream | ||||
| { | { | ||||
| AndroidContentUriOutputStream (LocalRef<jobject>&& outputStream) | |||||
| : stream (outputStream) | |||||
| { | |||||
| } | |||||
| explicit AndroidContentUriOutputStream (LocalRef<jobject>&& streamIn) | |||||
| : stream (std::move (streamIn)) {} | |||||
| ~AndroidContentUriOutputStream() override | ~AndroidContentUriOutputStream() override | ||||
| { | { | ||||
| @@ -479,12 +532,79 @@ struct AndroidContentUriOutputStream : public OutputStream | |||||
| int64 pos = 0; | 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<jbyteArray> { getEnv()->NewByteArray (sizeIn) } }, | |||||
| size (sizeIn) {} | |||||
| jbyteArray getNativeArray() const { return byteArray.get(); } | |||||
| jsize getSize() const { return size; } | |||||
| private: | |||||
| GlobalRefImpl<jbyteArray> byteArray; | |||||
| jsize size = 0; | |||||
| }; | |||||
| //============================================================================== | |||||
| struct AndroidContentUriInputStream : public InputStream | |||||
| { | |||||
| explicit AndroidContentUriInputStream (LocalRef<jobject>&& 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<size_t> (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 | class MediaScannerConnectionClient : public AndroidInterfaceImplementer | ||||
| @@ -31,11 +31,11 @@ template <typename JavaType> | |||||
| class LocalRef | class LocalRef | ||||
| { | { | ||||
| public: | 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() | void clear() | ||||
| { | { | ||||
| @@ -61,8 +61,8 @@ public: | |||||
| return *this; | 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: | private: | ||||
| JavaType obj; | JavaType obj; | ||||
| @@ -74,19 +74,19 @@ private: | |||||
| }; | }; | ||||
| //============================================================================== | //============================================================================== | ||||
| class GlobalRef | |||||
| template <typename JavaType> | |||||
| class GlobalRefImpl | |||||
| { | { | ||||
| public: | public: | ||||
| inline GlobalRef() noexcept : obj (nullptr) {} | |||||
| inline explicit GlobalRef (const LocalRef<jobject>& o) : obj (retain (o.get(), getEnv())) {} | |||||
| inline explicit GlobalRef (const LocalRef<jobject>& 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<JavaType>& o) : obj (retain (o.get(), getEnv())) {} | |||||
| GlobalRefImpl (const LocalRef<JavaType>& 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) | 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(); | clear(); | ||||
| obj = newObj; | obj = newObj; | ||||
| return *this; | return *this; | ||||
| } | } | ||||
| inline GlobalRef& operator= (GlobalRef&& other) | |||||
| GlobalRefImpl& operator= (GlobalRefImpl&& other) | |||||
| { | { | ||||
| clear(); | clear(); | ||||
| std::swap (obj, other.obj); | 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) \ | #define DECLARE_CALL_TYPE_METHOD(returnType, typeName) \ | ||||
| @@ -147,14 +147,20 @@ public: | |||||
| private: | 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<JavaType> (env->NewGlobalRef (obj)) | |||||
| : nullptr; | |||||
| } | } | ||||
| }; | }; | ||||
| class GlobalRef : public GlobalRefImpl<jobject> | |||||
| { | |||||
| public: | |||||
| using GlobalRefImpl::GlobalRefImpl; | |||||
| }; | |||||
| //============================================================================== | //============================================================================== | ||||
| extern LocalRef<jobject> getAppContext() noexcept; | extern LocalRef<jobject> getAppContext() noexcept; | ||||
| @@ -166,15 +172,15 @@ struct SystemJavaClassComparator; | |||||
| class JNIClassBase | class JNIClassBase | ||||
| { | { | ||||
| public: | 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(); | virtual ~JNIClassBase(); | ||||
| inline operator jclass() const noexcept { return classRef; } | |||||
| operator jclass() const noexcept { return classRef; } | |||||
| static void initialiseAllClasses (JNIEnv*); | static void initialiseAllClasses (JNIEnv*); | ||||
| static void releaseAllClasses (JNIEnv*); | static void releaseAllClasses (JNIEnv*); | ||||
| inline const char* getClassPath() const noexcept { return classPath; } | |||||
| const char* getClassPath() const noexcept { return classPath; } | |||||
| protected: | protected: | ||||
| virtual void initialiseFields (JNIEnv*) = 0; | virtual void initialiseFields (JNIEnv*) = 0; | ||||
| @@ -252,6 +258,7 @@ private: | |||||
| METHOD (getApplicationContext, "getApplicationContext", "()Landroid/content/Context;") \ | METHOD (getApplicationContext, "getApplicationContext", "()Landroid/content/Context;") \ | ||||
| METHOD (getApplicationInfo, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;") \ | METHOD (getApplicationInfo, "getApplicationInfo", "()Landroid/content/pm/ApplicationInfo;") \ | ||||
| METHOD (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)I") \ | METHOD (checkCallingOrSelfPermission, "checkCallingOrSelfPermission", "(Ljava/lang/String;)I") \ | ||||
| METHOD (checkCallingOrSelfUriPermission, "checkCallingOrSelfUriPermission", "(Landroid/net/Uri;I)I") \ | |||||
| METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") | METHOD (getCacheDir, "getCacheDir", "()Ljava/io/File;") | ||||
| DECLARE_JNI_CLASS (AndroidContext, "android/content/Context") | 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 (getAction, "getAction", "()Ljava/lang/String;") \ | ||||
| METHOD (getCategories, "getCategories", "()Ljava/util/Set;") \ | METHOD (getCategories, "getCategories", "()Ljava/util/Set;") \ | ||||
| METHOD (getData, "getData", "()Landroid/net/Uri;") \ | METHOD (getData, "getData", "()Landroid/net/Uri;") \ | ||||
| METHOD (getClipData, "getClipData", "()Landroid/content/ClipData;") \ | |||||
| METHOD (getExtras, "getExtras", "()Landroid/os/Bundle;") \ | METHOD (getExtras, "getExtras", "()Landroid/os/Bundle;") \ | ||||
| METHOD (getIntExtra, "getIntExtra", "(Ljava/lang/String;I)I") \ | METHOD (getIntExtra, "getIntExtra", "(Ljava/lang/String;I)I") \ | ||||
| METHOD (getStringExtra, "getStringExtra", "(Ljava/lang/String;)Ljava/lang/String;") \ | 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 (putExtraString, "putExtra", "(Ljava/lang/String;Ljava/lang/String;)Landroid/content/Intent;") \ | ||||
| METHOD (putExtraStrings, "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 (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 (putParcelableArrayListExtra, "putParcelableArrayListExtra", "(Ljava/lang/String;Ljava/util/ArrayList;)Landroid/content/Intent;") \ | ||||
| METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \ | METHOD (setAction, "setAction", "(Ljava/lang/String;)Landroid/content/Intent;") \ | ||||
| METHOD (setFlags, "setFlags", "(I)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) \ | #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ | ||||
| METHOD (get, "get", "([B)Ljava/nio/ByteBuffer;") \ | METHOD (get, "get", "([B)Ljava/nio/ByteBuffer;") \ | ||||
| METHOD (remaining, "remaining", "()I") \ | 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;") | STATICMETHOD (wrap, "wrap", "([B)Ljava/nio/ByteBuffer;") | ||||
| DECLARE_JNI_CLASS (JavaByteBuffer, "java/nio/ByteBuffer") | DECLARE_JNI_CLASS (JavaByteBuffer, "java/nio/ByteBuffer") | ||||
| @@ -837,15 +849,12 @@ namespace | |||||
| { | { | ||||
| auto* env = getEnv(); | auto* env = getEnv(); | ||||
| LocalRef<jobject> 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; | |||||
| } | } | ||||
| } | } | ||||
| @@ -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)) | DECLARE_JNI_CLASS_WITH_BYTECODE (HTTPStream, "com/rmsl/juce/JuceHTTPStream", 16, javaJuceHttpStream, sizeof(javaJuceHttpStream)) | ||||
| #undef JNI_CLASS_MEMBERS | #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) \ | #define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD, CALLBACK) \ | ||||
| METHOD (acquire, "acquire", "()V") \ | METHOD (acquire, "acquire", "()V") \ | ||||
| @@ -318,6 +310,25 @@ String URL::getFileName() const | |||||
| return toString (false).fromLastOccurrenceOf ("/", false, true); | return toString (false).fromLastOccurrenceOf ("/", false, true); | ||||
| } | } | ||||
| struct AndroidStreamHelpers | |||||
| { | |||||
| enum class StreamKind { output, input }; | |||||
| static LocalRef<jobject> createStream (const GlobalRef& uri, StreamKind kind) | |||||
| { | |||||
| auto* env = getEnv(); | |||||
| auto contentResolver = AndroidContentUriResolver::getContentResolver(); | |||||
| if (contentResolver == nullptr) | |||||
| return {}; | |||||
| return LocalRef<jobject> (env->CallObjectMethod (contentResolver.get(), | |||||
| kind == StreamKind::input ? ContentResolver.openInputStream | |||||
| : ContentResolver.openOutputStream, | |||||
| uri.get())); | |||||
| } | |||||
| }; | |||||
| //============================================================================== | //============================================================================== | ||||
| class WebInputStream::Pimpl | class WebInputStream::Pimpl | ||||
| { | { | ||||
| @@ -363,7 +374,8 @@ public: | |||||
| if (isContentURL) | if (isContentURL) | ||||
| { | { | ||||
| auto inputStream = AndroidContentUriResolver::getStreamForContentUri (url, true); | |||||
| GlobalRef urlRef { urlToUri (url) }; | |||||
| auto inputStream = AndroidStreamHelpers::createStream (urlRef, AndroidStreamHelpers::StreamKind::input); | |||||
| if (inputStream != nullptr) | if (inputStream != nullptr) | ||||
| { | { | ||||
| @@ -796,6 +796,11 @@ OutputStream* juce_CreateContentURIOutputStream (const URL&); | |||||
| std::unique_ptr<OutputStream> URL::createOutputStream() const | std::unique_ptr<OutputStream> URL::createOutputStream() const | ||||
| { | { | ||||
| #if JUCE_ANDROID | |||||
| if (auto stream = AndroidDocument::fromDocument (*this).createOutputStream()) | |||||
| return stream; | |||||
| #endif | |||||
| if (isLocalFile()) | if (isLocalFile()) | ||||
| { | { | ||||
| #if JUCE_IOS | #if JUCE_IOS | ||||
| @@ -806,11 +811,7 @@ std::unique_ptr<OutputStream> URL::createOutputStream() const | |||||
| #endif | #endif | ||||
| } | } | ||||
| #if JUCE_ANDROID | |||||
| return std::unique_ptr<OutputStream> (juce_CreateContentURIOutputStream (*this)); | |||||
| #else | |||||
| return nullptr; | return nullptr; | ||||
| #endif | |||||
| } | } | ||||
| //============================================================================== | //============================================================================== | ||||
| @@ -333,9 +333,9 @@ JUCE_END_IGNORE_WARNINGS_GCC_LIKE | |||||
| #include "native/juce_linux_FileChooser.cpp" | #include "native/juce_linux_FileChooser.cpp" | ||||
| #elif JUCE_ANDROID | #elif JUCE_ANDROID | ||||
| #include "juce_core/files/juce_common_MimeTypes.h" | |||||
| #include "native/accessibility/juce_android_Accessibility.cpp" | #include "native/accessibility/juce_android_Accessibility.cpp" | ||||
| #include "native/juce_android_Windowing.cpp" | #include "native/juce_android_Windowing.cpp" | ||||
| #include "native/juce_common_MimeTypes.cpp" | |||||
| #include "native/juce_android_FileChooser.cpp" | #include "native/juce_android_FileChooser.cpp" | ||||
| #if JUCE_CONTENT_SHARING | #if JUCE_CONTENT_SHARING | ||||
| @@ -337,7 +337,7 @@ private: | |||||
| canSpecifyMimeTypes = fileExtension.isNotEmpty(); | canSpecifyMimeTypes = fileExtension.isNotEmpty(); | ||||
| if (canSpecifyMimeTypes) | if (canSpecifyMimeTypes) | ||||
| mimeTypes.addArray (getMimeTypesForFileExtension (fileExtension)); | |||||
| mimeTypes.addArray (MimeTypeTable::getMimeTypesForFileExtension (fileExtension)); | |||||
| else | else | ||||
| mimeTypes.clear(); | mimeTypes.clear(); | ||||
| @@ -597,8 +597,8 @@ public: | |||||
| if (extension.isEmpty()) | if (extension.isEmpty()) | ||||
| return nullptr; | return nullptr; | ||||
| return juceStringArrayToJava (filterMimeTypes (getMimeTypesForFileExtension (extension), | |||||
| juceString (mimeTypeFilter.get()))); | |||||
| return juceStringArrayToJava (filterMimeTypes (MimeTypeTable::getMimeTypesForFileExtension (extension), | |||||
| juceString (mimeTypeFilter.get()))); | |||||
| } | } | ||||
| void sharingFinished (int resultCode) | void sharingFinished (int resultCode) | ||||
| @@ -26,6 +26,19 @@ | |||||
| namespace juce | 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 | class FileChooser::Native : public FileChooser::Pimpl | ||||
| { | { | ||||
| public: | public: | ||||
| @@ -40,6 +53,7 @@ public: | |||||
| auto sdkVersion = getAndroidSDKVersion(); | auto sdkVersion = getAndroidSDKVersion(); | ||||
| auto saveMode = ((flags & FileBrowserComponent::saveMode) != 0); | auto saveMode = ((flags & FileBrowserComponent::saveMode) != 0); | ||||
| auto selectsDirectories = ((flags & FileBrowserComponent::canSelectDirectories) != 0); | auto selectsDirectories = ((flags & FileBrowserComponent::canSelectDirectories) != 0); | ||||
| auto canSelectMultiple = ((flags & FileBrowserComponent::canSelectMultipleItems) != 0); | |||||
| // You cannot save a directory | // You cannot save a directory | ||||
| jassert (! (saveMode && selectsDirectories)); | jassert (! (saveMode && selectsDirectories)); | ||||
| @@ -85,6 +99,13 @@ public: | |||||
| uri.get()); | uri.get()); | ||||
| } | } | ||||
| if (canSelectMultiple && sdkVersion >= 18) | |||||
| { | |||||
| env->CallObjectMethod (intent.get(), | |||||
| AndroidIntent.putExtraBool, | |||||
| javaString ("android.intent.extra.ALLOW_MULTIPLE").get(), | |||||
| true); | |||||
| } | |||||
| if (! selectsDirectories) | if (! selectsDirectories) | ||||
| { | { | ||||
| @@ -169,22 +190,41 @@ public: | |||||
| currentFileChooser = nullptr; | currentFileChooser = nullptr; | ||||
| auto* env = getEnv(); | auto* env = getEnv(); | ||||
| Array<URL> chosenURLs; | |||||
| if (resultCode == /*Activity.RESULT_OK*/ -1 && intentData != nullptr) | |||||
| const auto getUrls = [&]() -> Array<URL> | |||||
| { | { | ||||
| LocalRef<jobject> 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<URL> 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))); | chosenURLs.add (URL (juceString (env, jStr))); | ||||
| }; | |||||
| if (LocalRef<jobject> 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<jobject> item { env->CallObjectMethod (clipData.get(), ClipData.getItemAt, i) }) | |||||
| { | |||||
| if (LocalRef<jobject> itemUri { env->CallObjectMethod (item.get(), ClipDataItem.getUri) }) | |||||
| addUrl (itemUri.get()); | |||||
| } | |||||
| } | |||||
| } | } | ||||
| } | |||||
| else if (LocalRef<jobject> uri { env->CallObjectMethod (intentData.get(), AndroidIntent.getData )}) | |||||
| { | |||||
| addUrl (uri.get()); | |||||
| } | |||||
| return chosenURLs; | |||||
| }; | |||||
| owner.finished (chosenURLs); | |||||
| owner.finished (getUrls()); | |||||
| } | } | ||||
| static Native* currentFileChooser; | static Native* currentFileChooser; | ||||
| @@ -200,7 +240,7 @@ public: | |||||
| { | { | ||||
| auto extension = wildcard.fromLastOccurrenceOf (".", false, false); | auto extension = wildcard.fromLastOccurrenceOf (".", false, false); | ||||
| result.addArray (getMimeTypesForFileExtension (extension)); | |||||
| result.addArray (MimeTypeTable::getMimeTypesForFileExtension (extension)); | |||||
| } | } | ||||
| } | } | ||||