mirror of
https://github.com/juce-framework/JUCE.git
synced 2026-01-10 23:44:24 +00:00
AndroidDocument: Support file access to shared storage locations on Android 30+
This commit is contained in:
parent
dd746cc5a5
commit
b17806fbfc
13 changed files with 1913 additions and 108 deletions
476
modules/juce_core/files/juce_AndroidDocument.h
Normal file
476
modules/juce_core/files/juce_AndroidDocument.h
Normal file
|
|
@ -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] =
|
||||
{
|
||||
42
modules/juce_core/files/juce_common_MimeTypes.h
Normal file
42
modules/juce_core/files/juce_common_MimeTypes.h
Normal file
|
|
@ -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"
|
||||
|
|
|
|||
1085
modules/juce_core/native/juce_android_AndroidDocument.cpp
Normal file
1085
modules/juce_core/native/juce_android_AndroidDocument.cpp
Normal file
File diff suppressed because it is too large
Load diff
|
|
@ -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(); }
|
||||
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(); }
|
||||
|
||||
|
||||
inline void clear() { if (obj != nullptr) clear (getEnv()); }
|
||||
inline void clear (JNIEnv* env)
|
||||
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)
|
||||
Array<URL> chosenURLs;
|
||||
|
||||
const auto addUrl = [env, &chosenURLs] (jobject uri)
|
||||
{
|
||||
auto jStr = (jstring) env->CallObjectMethod (uri, JavaObject.toString);
|
||||
|
||||
if (jStr != nullptr)
|
||||
if (auto jStr = (jstring) env->CallObjectMethod (uri, JavaObject.toString))
|
||||
chosenURLs.add (URL (juceString (env, jStr)));
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
owner.finished (chosenURLs);
|
||||
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 (getUrls());
|
||||
}
|
||||
|
||||
static Native* currentFileChooser;
|
||||
|
|
@ -200,7 +240,7 @@ public:
|
|||
{
|
||||
auto extension = wildcard.fromLastOccurrenceOf (".", false, false);
|
||||
|
||||
result.addArray (getMimeTypesForFileExtension (extension));
|
||||
result.addArray (MimeTypeTable::getMimeTypesForFileExtension (extension));
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue