| @@ -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 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] = | |||
| { | |||
| @@ -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 | |||
| #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" | |||
| @@ -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" | |||
| @@ -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<jobject> obj) | |||
| { | |||
| auto* env = getEnv(); | |||
| @@ -118,21 +189,9 @@ static LocalRef<jobject> urlToUri (const URL& url) | |||
| struct AndroidContentUriResolver | |||
| { | |||
| 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) | |||
| @@ -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<jobject> 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<jobject> 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<jobject> (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); | |||
| return juceFile (LocalRef<jobject> (getEnv()->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); | |||
| } | |||
| static Array<File> getSecondaryStorageDirectories() | |||
| @@ -433,10 +488,8 @@ private: | |||
| //============================================================================== | |||
| struct AndroidContentUriOutputStream : public OutputStream | |||
| { | |||
| AndroidContentUriOutputStream (LocalRef<jobject>&& outputStream) | |||
| : stream (outputStream) | |||
| { | |||
| } | |||
| explicit AndroidContentUriOutputStream (LocalRef<jobject>&& 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<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 | |||
| @@ -31,11 +31,11 @@ template <typename JavaType> | |||
| 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 <typename JavaType> | |||
| class GlobalRefImpl | |||
| { | |||
| 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) | |||
| { | |||
| @@ -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<JavaType> (env->NewGlobalRef (obj)) | |||
| : nullptr; | |||
| } | |||
| }; | |||
| class GlobalRef : public GlobalRefImpl<jobject> | |||
| { | |||
| public: | |||
| using GlobalRefImpl::GlobalRefImpl; | |||
| }; | |||
| //============================================================================== | |||
| extern LocalRef<jobject> 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<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)) | |||
| #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<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 | |||
| { | |||
| @@ -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) | |||
| { | |||
| @@ -796,6 +796,11 @@ OutputStream* juce_CreateContentURIOutputStream (const URL&); | |||
| std::unique_ptr<OutputStream> 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<OutputStream> URL::createOutputStream() const | |||
| #endif | |||
| } | |||
| #if JUCE_ANDROID | |||
| return std::unique_ptr<OutputStream> (juce_CreateContentURIOutputStream (*this)); | |||
| #else | |||
| return nullptr; | |||
| #endif | |||
| } | |||
| //============================================================================== | |||
| @@ -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 | |||
| @@ -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) | |||
| @@ -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<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))); | |||
| }; | |||
| 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; | |||
| @@ -200,7 +240,7 @@ public: | |||
| { | |||
| auto extension = wildcard.fromLastOccurrenceOf (".", false, false); | |||
| result.addArray (getMimeTypesForFileExtension (extension)); | |||
| result.addArray (MimeTypeTable::getMimeTypesForFileExtension (extension)); | |||
| } | |||
| } | |||