diff --git a/modules/juce_core/native/juce_android_Files.cpp b/modules/juce_core/native/juce_android_Files.cpp index 3dfa21dafc..300c3f72f3 100644 --- a/modules/juce_core/native/juce_android_Files.cpp +++ b/modules/juce_core/native/juce_android_Files.cpp @@ -32,6 +32,328 @@ namespace juce DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection"); #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + 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;") \ + +DECLARE_JNI_CLASS (ContentResolver, "android/content/ContentResolver"); +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (moveToFirst, "moveToFirst", "()Z") \ + METHOD (getColumnIndex, "getColumnIndex", "(Ljava/lang/String;)I") \ + METHOD (getString, "getString", "(I)Ljava/lang/String;") \ + METHOD (close, "close", "()V") \ + +DECLARE_JNI_CLASS (AndroidCursor, "android/database/Cursor"); +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (getExternalStorageDirectory, "getExternalStorageDirectory", "()Ljava/io/File;") \ + STATICMETHOD (getExternalStoragePublicDirectory, "getExternalStoragePublicDirectory", "(Ljava/lang/String;)Ljava/io/File;") \ + +DECLARE_JNI_CLASS (AndroidEnvironment, "android/os/Environment"); +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (getAbsolutePath, "getAbsolutePath", "()Ljava/lang/String;") \ + +DECLARE_JNI_CLASS (AndroidFile, "java/io/File"); +#undef JNI_CLASS_MEMBERS + +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (withAppendedId, "withAppendedId", "(Landroid/net/Uri;J)Landroid/net/Uri;") \ + +DECLARE_JNI_CLASS (ContentUris, "android/content/ContentUris"); +#undef JNI_CLASS_MEMBERS + +//============================================================================== +struct AndroidContentUriResolver +{ +public: + static LocalRef getInputStreamForContentUri (const URL& url) + { + // only use this method for content URIs + jassert (url.getScheme() == "content"); + auto* env = getEnv(); + + LocalRef contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); + + if (contentResolver) + return LocalRef ((env->CallObjectMethod (contentResolver.get(), ContentResolver.openInputStream, urlToUri (url).get()))); + + return LocalRef(); + } + + static File getLocalFileFromContentUri (const URL& url) + { + // only use this method for content URIs + jassert (url.getScheme() == "content"); + + auto authority = url.getDomain(); + auto documentId = URL::removeEscapeChars (url.getSubPath().fromFirstOccurrenceOf ("/", false, false)); + auto tokens = StringArray::fromTokens (documentId, ":", ""); + + if (authority == "com.android.externalstorage.documents") + { + auto storageId = tokens[0]; + auto subpath = tokens[1]; + + auto storagePath = getStorageDevicePath (storageId); + + if (storagePath != File()) + return storagePath.getChildFile (subpath); + } + else if (authority == "com.android.providers.downloads.documents") + { + auto type = tokens[0]; + auto downloadId = tokens[1]; + + if (type.equalsIgnoreCase ("raw")) + { + return File (downloadId); + } + else if (type.equalsIgnoreCase ("downloads")) + { + auto subDownloadPath = url.getSubPath().fromFirstOccurrenceOf ("tree/downloads", false, false); + return File (getWellKnownFolder ("Download").getFullPathName() + "/" + subDownloadPath); + } + else + { + return getLocalFileFromContentUri (URL ("content://downloads/public_downloads/" + documentId)); + } + } + else if (authority == "com.android.providers.media.documents" && documentId.isNotEmpty()) + { + auto type = tokens[0]; + auto mediaId = tokens[1]; + + if (type == "image") + type = "images"; + + return getCursorDataColumn (URL (String ("content://media/external/") + type + "/media"), + "_id=?", StringArray {mediaId}); + } + + return getCursorDataColumn (url); + } +private: + //============================================================================== + static String getCursorDataColumn (const URL& url, const String& selection = {}, + const StringArray& selectionArgs = {}) + { + auto uri = urlToUri (url); + auto* env = getEnv(); + LocalRef contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); + + if (contentResolver) + { + LocalRef columnName (javaString ("_data")); + LocalRef projection (env->NewObjectArray (1, JavaString, columnName.get())); + + LocalRef args; + + if (selection.isNotEmpty()) + { + args = LocalRef (env->NewObjectArray (selectionArgs.size(), JavaString, javaString("").get())); + + for (int i = 0; i < selectionArgs.size(); ++i) + env->SetObjectArrayElement (args.get(), i, javaString (selectionArgs[i]).get()); + } + + LocalRef jSelection (selection.isNotEmpty() ? javaString (selection) : LocalRef()); + LocalRef cursor (env->CallObjectMethod (contentResolver.get(), ContentResolver.query, + uri.get(), projection.get(), jSelection.get(), + args.get(), nullptr)); + + if (cursor) + { + if (env->CallBooleanMethod (cursor.get(), AndroidCursor.moveToFirst) != 0) + { + auto columnIndex = env->CallIntMethod (cursor.get(), AndroidCursor.getColumnIndex, columnName.get()); + + if (columnIndex >= 0) + { + LocalRef value ((jstring) env->CallObjectMethod (cursor.get(), AndroidCursor.getString, columnIndex)); + + if (value) + return juceString (value.get()); + } + } + + env->CallVoidMethod (cursor.get(), AndroidCursor.close); + } + } + + return {}; + } + + //============================================================================== + static File getWellKnownFolder (const String& folderId) + { + auto* env = getEnv(); + LocalRef downloadFolder (env->CallStaticObjectMethod (AndroidEnvironment, + AndroidEnvironment.getExternalStoragePublicDirectory, + javaString (folderId).get())); + + return (downloadFolder ? juceFile (downloadFolder) : File()); + } + + //============================================================================== + static File getStorageDevicePath (const String& storageId) + { + // check for the primary alias + if (storageId == "primary") + return getPrimaryStorageDirectory(); + + auto storageDevices = getSecondaryStorageDirectories(); + + for (auto storageDevice : storageDevices) + if (getStorageIdForMountPoint (storageDevice) == storageId) + return storageDevice; + + return {}; + } + + static File getPrimaryStorageDirectory() + { + auto* env = getEnv(); + return juceFile (LocalRef (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); + } + + static Array getSecondaryStorageDirectories() + { + Array results; + + if (getSDKVersion() >= 19) + { + auto* env = getEnv(); + static jmethodID m = (env->GetMethodID (JuceAppActivity, "getExternalFilesDirs", + "(Ljava/lang/String;)[Ljava/io/File;")); + if (m == 0) + return {}; + + auto paths = convertFileArray (LocalRef (android.activity.callObjectMethod (m, nullptr))); + + for (auto path : paths) + results.add (getMountPointForFile (path)); + } + else + { + // on older SDKs other external storages are located "next" to the primary + // storage mount point + auto mountFolder = getMountPointForFile (getPrimaryStorageDirectory()) + .getParentDirectory(); + + // don't include every folder. Only folders which are actually mountpoints + juce_statStruct info; + if (! juce_stat (mountFolder.getFullPathName(), info)) + return {}; + + auto rootFsDevice = info.st_dev; + DirectoryIterator iter (mountFolder, false, "*", File::findDirectories); + + while (iter.next()) + { + auto candidate = iter.getFile(); + + if (juce_stat (candidate.getFullPathName(), info) + && info.st_dev != rootFsDevice) + results.add (candidate); + } + + } + + return results; + } + + //============================================================================== + static String getStorageIdForMountPoint (const File& mountpoint) + { + // currently this seems to work fine, but something + // more intelligent may be needed in the future + return mountpoint.getFileName(); + } + + static File getMountPointForFile (const File& file) + { + juce_statStruct info; + + if (juce_stat (file.getFullPathName(), info)) + { + auto dev = info.st_dev; + File mountPoint = file; + + for (;;) + { + auto parent = mountPoint.getParentDirectory(); + + if (parent == mountPoint) + break; + + juce_stat (parent.getFullPathName(), info); + + if (info.st_dev != dev) + break; + + mountPoint = parent; + } + + return mountPoint; + } + + return {}; + } + + //============================================================================== + static Array convertFileArray (LocalRef obj) + { + auto* env = getEnv(); + int n = (int) env->GetArrayLength ((jobjectArray) obj.get()); + Array files; + + for (int i = 0; i < n; ++i) + files.add (juceFile (LocalRef (env->GetObjectArrayElement ((jobjectArray) obj.get(), + (jsize) i)))); + + return files; + } + + static File juceFile (LocalRef obj) + { + auto* env = getEnv(); + + if (env->IsInstanceOf (obj.get(), AndroidFile) != 0) + return File (safeString (LocalRef (env->CallObjectMethod (obj.get(), + AndroidFile.getAbsolutePath)))); + + return {}; + } + + //============================================================================== + static int getSDKVersion() + { + static int sdkVersion + = getEnv()->CallStaticIntMethod (JuceAppActivity, + JuceAppActivity.getAndroidSDKVersion); + + return sdkVersion; + } + + static LocalRef urlToUri (const URL& url) + { + return LocalRef (getEnv()->CallStaticObjectMethod (Uri, Uri.parse, javaString (url.toString (true)).get())); + } + + static String safeString (LocalRef str) + { + if (str) + return juceString ((jstring) str.get()); + + return {}; + } +}; + //============================================================================== class MediaScannerConnectionClient : public AndroidInterfaceImplementer { diff --git a/modules/juce_core/native/juce_android_JNIHelpers.h b/modules/juce_core/native/juce_android_JNIHelpers.h index 161aa64ed0..544e440288 100644 --- a/modules/juce_core/native/juce_android_JNIHelpers.h +++ b/modules/juce_core/native/juce_android_JNIHelpers.h @@ -324,6 +324,7 @@ extern AndroidSystem android; METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ METHOD (moveTaskToBack, "moveTaskToBack", "(Z)Z") \ METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ + METHOD (getContentResolver, "getContentResolver", "()Landroid/content/ContentResolver;") \ DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); #undef JNI_CLASS_MEMBERS @@ -563,6 +564,13 @@ DECLARE_JNI_CLASS (JavaSet, "java/util/Set"); DECLARE_JNI_CLASS (JavaString, "java/lang/String"); #undef JNI_CLASS_MEMBERS +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + STATICMETHOD (parse, "parse", "(Ljava/lang/String;)Landroid/net/Uri;") \ + METHOD (getAuthority, "getAuthority", "()Ljava/lang/String;") \ + +DECLARE_JNI_CLASS (Uri, "android/net/Uri"); +#undef JNI_CLASS_MEMBERS + //============================================================================== class AndroidInterfaceImplementer; diff --git a/modules/juce_core/native/juce_android_Network.cpp b/modules/juce_core/native/juce_android_Network.cpp index 5b5864ebb1..c12aaa93e6 100644 --- a/modules/juce_core/native/juce_android_Network.cpp +++ b/modules/juce_core/native/juce_android_Network.cpp @@ -43,6 +43,13 @@ DECLARE_JNI_CLASS (StringBuffer, "java/lang/StringBuffer"); DECLARE_JNI_CLASS (HTTPStream, JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream"); #undef JNI_CLASS_MEMBERS +//============================================================================== +#define JNI_CLASS_MEMBERS(METHOD, STATICMETHOD, FIELD, STATICFIELD) \ + METHOD (close, "close", "()V") \ + METHOD (read, "read", "([BII)I") \ + +DECLARE_JNI_CLASS (AndroidInputStream, "java/io/InputStream"); +#undef JNI_CLASS_MEMBERS //============================================================================== void MACAddress::findAllAddresses (Array& /*result*/) @@ -60,12 +67,43 @@ JUCE_API bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& /*t return false; } +//============================================================================== +bool URL::isLocalFile() const +{ + if (getScheme() == "file") + return true; + + auto file = AndroidContentUriResolver::getLocalFileFromContentUri (*this); + return (file != File()); +} + +File URL::getLocalFile() const +{ + if (getScheme() == "content") + { + auto path = AndroidContentUriResolver::getLocalFileFromContentUri (*this); + + // This URL does not refer to a local file + // Call URL::isLocalFile to first check if the URL + // refers to a local file. + jassert (path != File()); + + return path; + } + + return fileFromFileSchemeURL (*this); +} + //============================================================================== class WebInputStream::Pimpl { public: + enum { contentStreamCacheSize = 1024 }; + Pimpl (WebInputStream&, const URL& urlToCopy, bool shouldBePost) - : url (urlToCopy), isPost (shouldBePost), + : url (urlToCopy), + isContentURL (urlToCopy.getScheme() == "content"), + isPost (shouldBePost), httpRequest (isPost ? "POST" : "GET") {} @@ -76,6 +114,12 @@ public: void cancel() { + if (isContentURL) + { + stream.callVoidMethod (AndroidInputStream.close); + return; + } + const ScopedLock lock (createStreamLock); if (stream != 0) @@ -89,83 +133,98 @@ public: bool connect (WebInputStream::Listener* /*listener*/) { - String address = url.toString (! isPost); + auto* env = getEnv(); - if (! address.contains ("://")) - address = "http://" + address; - - MemoryBlock postData; - if (isPost) - WebInputStream::createHeadersAndPostData (url, headers, postData); - - JNIEnv* env = getEnv(); + if (isContentURL) + { + auto inputStream = AndroidContentUriResolver::getInputStreamForContentUri (url); - jbyteArray postDataArray = 0; + if (inputStream != nullptr) + { + stream = GlobalRef (inputStream); + statusCode = 200; - if (postData.getSize() > 0) - { - postDataArray = env->NewByteArray (static_cast (postData.getSize())); - env->SetByteArrayRegion (postDataArray, 0, static_cast (postData.getSize()), (const jbyte*) postData.getData()); + return true; + } } + else + { + String address = url.toString (! isPost); - LocalRef responseHeaderBuffer (env->NewObject (StringBuffer, StringBuffer.constructor)); + if (! address.contains ("://")) + address = "http://" + address; - // Annoyingly, the android HTTP functions will choke on this call if you try to do it on the message - // thread. You'll need to move your networking code to a background thread to keep it happy.. - jassert (Thread::getCurrentThread() != nullptr); + MemoryBlock postData; + if (isPost) + WebInputStream::createHeadersAndPostData (url, headers, postData); - jintArray statusCodeArray = env->NewIntArray (1); - jassert (statusCodeArray != 0); + jbyteArray postDataArray = 0; - { - const ScopedLock lock (createStreamLock); - - if (! hasBeenCancelled) - stream = GlobalRef (env->CallStaticObjectMethod (JuceAppActivity, - JuceAppActivity.createHTTPStream, - javaString (address).get(), - (jboolean) isPost, - postDataArray, - javaString (headers).get(), - (jint) timeOutMs, - statusCodeArray, - responseHeaderBuffer.get(), - (jint) numRedirectsToFollow, - javaString (httpRequest).get())); - } + if (postData.getSize() > 0) + { + postDataArray = env->NewByteArray (static_cast (postData.getSize())); + env->SetByteArrayRegion (postDataArray, 0, static_cast (postData.getSize()), (const jbyte*) postData.getData()); + } - if (stream != 0 && ! stream.callBooleanMethod (HTTPStream.connect)) - stream.clear(); + LocalRef responseHeaderBuffer (env->NewObject (StringBuffer, StringBuffer.constructor)); - jint* const statusCodeElements = env->GetIntArrayElements (statusCodeArray, 0); - statusCode = statusCodeElements[0]; - env->ReleaseIntArrayElements (statusCodeArray, statusCodeElements, 0); - env->DeleteLocalRef (statusCodeArray); + // Annoyingly, the android HTTP functions will choke on this call if you try to do it on the message + // thread. You'll need to move your networking code to a background thread to keep it happy.. + jassert (Thread::getCurrentThread() != nullptr); - if (postDataArray != 0) - env->DeleteLocalRef (postDataArray); - - if (stream != 0) - { - StringArray headerLines; + jintArray statusCodeArray = env->NewIntArray (1); + jassert (statusCodeArray != 0); { - LocalRef headersString ((jstring) env->CallObjectMethod (responseHeaderBuffer.get(), - StringBuffer.toString)); - headerLines.addLines (juceString (env, headersString)); + const ScopedLock lock (createStreamLock); + + if (! hasBeenCancelled) + stream = GlobalRef (LocalRef (env->CallStaticObjectMethod (JuceAppActivity, + JuceAppActivity.createHTTPStream, + javaString (address).get(), + (jboolean) isPost, + postDataArray, + javaString (headers).get(), + (jint) timeOutMs, + statusCodeArray, + responseHeaderBuffer.get(), + (jint) numRedirectsToFollow, + javaString (httpRequest).get()))); } - for (int i = 0; i < headerLines.size(); ++i) + if (stream != 0 && ! stream.callBooleanMethod (HTTPStream.connect)) + stream.clear(); + + jint* const statusCodeElements = env->GetIntArrayElements (statusCodeArray, 0); + statusCode = statusCodeElements[0]; + env->ReleaseIntArrayElements (statusCodeArray, statusCodeElements, 0); + env->DeleteLocalRef (statusCodeArray); + + if (postDataArray != 0) + env->DeleteLocalRef (postDataArray); + + if (stream != 0) { - const String& header = headerLines[i]; - const String key (header.upToFirstOccurrenceOf (": ", false, false)); - const String value (header.fromFirstOccurrenceOf (": ", false, false)); - const String previousValue (responseHeaders[key]); + StringArray headerLines; - responseHeaders.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value)); - } + { + LocalRef headersString ((jstring) env->CallObjectMethod (responseHeaderBuffer.get(), + StringBuffer.toString)); + headerLines.addLines (juceString (env, headersString)); + } + + for (int i = 0; i < headerLines.size(); ++i) + { + const String& header = headerLines[i]; + const String key (header.upToFirstOccurrenceOf (": ", false, false)); + const String value (header.fromFirstOccurrenceOf (": ", false, false)); + const String previousValue (responseHeaders[key]); + + responseHeaders.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value)); + } - return true; + return true; + } } return false; @@ -193,11 +252,30 @@ public: //============================================================================== bool isError() const { return stream == nullptr; } + bool isExhausted() { return (isContentURL ? eofStreamReached : stream != nullptr && stream.callBooleanMethod (HTTPStream.isExhausted)); } + int64 getTotalLength() { return (isContentURL ? -1 : (stream != nullptr ? stream.callLongMethod (HTTPStream.getTotalLength) : 0)); } + int64 getPosition() { return (isContentURL ? readPosition : (stream != nullptr ? stream.callLongMethod (HTTPStream.getPosition) : 0)); } + + //============================================================================== + bool setPosition (int64 wantedPos) + { + if (isContentURL) + { + if (wantedPos < readPosition) + return false; + + auto bytesToSkip = wantedPos - readPosition; + + if (bytesToSkip == 0) + return true; - bool isExhausted() { return stream != nullptr && stream.callBooleanMethod (HTTPStream.isExhausted); } - int64 getTotalLength() { return stream != nullptr ? stream.callLongMethod (HTTPStream.getTotalLength) : 0; } - int64 getPosition() { return stream != nullptr ? stream.callLongMethod (HTTPStream.getPosition) : 0; } - bool setPosition (int64 wantedPos) { return stream != nullptr && stream.callBooleanMethod (HTTPStream.setPosition, (jlong) wantedPos); } + HeapBlock buffer (bytesToSkip); + + return (read (buffer.getData(), (int) bytesToSkip) > 0); + } + + return stream != nullptr && stream.callBooleanMethod (HTTPStream.setPosition, (jlong) wantedPos); + } int read (void* buffer, int bytesToRead) { @@ -212,12 +290,19 @@ public: jbyteArray javaArray = env->NewByteArray (bytesToRead); - int numBytes = stream.callIntMethod (HTTPStream.read, javaArray, (jint) bytesToRead); + auto numBytes = (isContentURL ? stream.callIntMethod (AndroidInputStream.read, javaArray, 0, (jint) bytesToRead) + : stream.callIntMethod (HTTPStream.read, javaArray, (jint) bytesToRead)); if (numBytes > 0) env->GetByteArrayRegion (javaArray, 0, numBytes, static_cast (buffer)); env->DeleteLocalRef (javaArray); + + readPosition += jmax (0, numBytes); + + if (numBytes == -1) + eofStreamReached = true; + return numBytes; } @@ -226,12 +311,13 @@ public: private: const URL url; - bool isPost; + bool isContentURL, isPost, eofStreamReached = false; int numRedirectsToFollow = 5, timeOutMs = 0; String httpRequest, headers; StringPairArray responseHeaders; CriticalSection createStreamLock; bool hasBeenCancelled = false; + int readPosition = 0; GlobalRef stream; JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) diff --git a/modules/juce_core/network/juce_URL.cpp b/modules/juce_core/network/juce_URL.cpp index a2dc715139..3dd57f68f1 100644 --- a/modules/juce_core/network/juce_URL.cpp +++ b/modules/juce_core/network/juce_URL.cpp @@ -137,6 +137,28 @@ URL::DownloadTask::~DownloadTask() {} URL::URL() noexcept {} URL::URL (const String& u) : url (u) +{ + init(); +} + +URL::URL (File localFile) +{ + while (! localFile.isRoot()) + { + url = "/" + addEscapeChars (localFile.getFileName(), false) + url; + localFile = localFile.getParentDirectory(); + } + + url = addEscapeChars (localFile.getFileName (), false) + url; + if (! url.startsWithChar (L'/')) + url = "/" + url; + + url = "file://" + url; + + jassert (isWellFormed()); +} + +void URL::init() { int i = url.indexOfChar ('?'); @@ -320,6 +342,40 @@ String URL::getScheme() const return url.substring (0, URLHelpers::findEndOfScheme (url) - 1); } +#ifndef JUCE_ANDROID +bool URL::isLocalFile() const +{ + return (getScheme() == "file"); +} + +File URL::getLocalFile() const +{ + return fileFromFileSchemeURL (*this); +} +#endif + +File URL::fileFromFileSchemeURL (const URL& fileURL) +{ + if (! fileURL.isLocalFile()) + { + jassertfalse; + return {}; + } + + auto path = removeEscapeChars (fileURL.getDomain()); + + #ifndef JUCE_WINDOWS + path = File::getSeparatorString() + path; + #endif + + auto urlElements = StringArray::fromTokens (fileURL.getSubPath(), "/", ""); + + for (auto urlElement : urlElements) + path += File::getSeparatorString() + removeEscapeChars (urlElement); + + return path; +} + int URL::getPort() const { auto colonPos = url.indexOfChar (URLHelpers::findStartOfNetLocation (url), ':'); @@ -438,16 +494,19 @@ bool URL::isProbablyAnEmailAddress (const String& possibleEmailAddress) } //============================================================================== -WebInputStream* URL::createInputStream (const bool usePostCommand, - OpenStreamProgressCallback* const progressCallback, - void* const progressCallbackContext, - String headers, - const int timeOutMs, - StringPairArray* const responseHeaders, - int* statusCode, - const int numRedirectsToFollow, - String httpRequestCmd) const -{ +InputStream* URL::createInputStream (const bool usePostCommand, + OpenStreamProgressCallback* const progressCallback, + void* const progressCallbackContext, + String headers, + const int timeOutMs, + StringPairArray* const responseHeaders, + int* statusCode, + const int numRedirectsToFollow, + String httpRequestCmd) const +{ + if (isLocalFile()) + return getLocalFile().createInputStream(); + ScopedPointer wi (new WebInputStream (*this, usePostCommand)); struct ProgressCallbackCaller : WebInputStream::Listener @@ -501,7 +560,8 @@ WebInputStream* URL::createInputStream (const bool usePostCommand, //============================================================================== bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const { - const ScopedPointer in (createInputStream (usePostCommand)); + const ScopedPointer in (isLocalFile() ? getLocalFile().createInputStream() + : static_cast (createInputStream (usePostCommand))); if (in != nullptr) { @@ -514,7 +574,8 @@ bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) co String URL::readEntireTextStream (bool usePostCommand) const { - const ScopedPointer in (createInputStream (usePostCommand)); + const ScopedPointer in (isLocalFile() ? getLocalFile().createInputStream() + : static_cast (createInputStream (usePostCommand))); if (in != nullptr) return in->readEntireStreamAsString(); diff --git a/modules/juce_core/network/juce_URL.h b/modules/juce_core/network/juce_URL.h index 83d4d40a6a..a88910bade 100644 --- a/modules/juce_core/network/juce_URL.h +++ b/modules/juce_core/network/juce_URL.h @@ -53,6 +53,9 @@ public: URL (URL&&); URL& operator= (URL&&); + /** Creates URL referring to a local file on your disk using the file:// scheme. */ + explicit URL (File); + /** Destructor. */ ~URL(); @@ -94,6 +97,20 @@ public: */ String getScheme() const; + /** Returns true if this URL refers to a local file. */ + bool isLocalFile() const; + + /** Returns the file path of the local file to which this URL refers to. + If the URL does not represent a local file URL (i.e. the URL's scheme is not 'file') + then this method will assert. + + This method also supports converting Android's content:// URLs to + local file paths. + + @see isLocalFile + */ + File getLocalFile() const; + /** Attempts to read a port number from the URL. @returns the port number, or 0 if none is explicitly specified. */ @@ -260,16 +277,18 @@ public: /** Attempts to open a stream that can read from this URL. - This method is a convenience wrapper for creating a new WebInputStream and setting some - commonly used options. The returned WebInputStream will have already been connected and - reading can start instantly. - Note that this method will block until the first byte of data has been received or an error has occurred. Note that on some platforms (Android, for example) it's not permitted to do any network action from the message thread, so you must only call it from a background thread. + Unless the URL represents a local file, this method returns an instance of a + WebInputStream. You can use dynamic_cast to cast the return value to a WebInputStream + which allows you more fine-grained control of the transfer process. + + If the URL represents a local file, then this method simply returns a FileInputStream. + @param doPostLikeRequest if true, the parameters added to this class will be transferred via the HTTP headers which is typical for POST requests. Otherwise the parameters will be added to the URL address. Additionally, @@ -298,15 +317,15 @@ public: @returns an input stream that the caller must delete, or a null pointer if there was an error trying to open it. */ - WebInputStream* createInputStream (bool doPostLikeRequest, - OpenStreamProgressCallback* progressCallback = nullptr, - void* progressCallbackContext = nullptr, - String extraHeaders = String(), - int connectionTimeOutMs = 0, - StringPairArray* responseHeaders = nullptr, - int* statusCode = nullptr, - int numRedirectsToFollow = 5, - String httpRequestCmd = String()) const; + InputStream* createInputStream (bool doPostLikeRequest, + OpenStreamProgressCallback* progressCallback = nullptr, + void* progressCallbackContext = nullptr, + String extraHeaders = String(), + int connectionTimeOutMs = 0, + StringPairArray* responseHeaders = nullptr, + int* statusCode = nullptr, + int numRedirectsToFollow = 5, + String httpRequestCmd = String()) const; //============================================================================== /** Represents a download task. @@ -487,6 +506,8 @@ private: MemoryBlock postData; StringArray parameterNames, parameterValues; + static File fileFromFileSchemeURL (const URL&); + struct Upload : public ReferenceCountedObject { Upload (const String&, const String&, const String&, const File&, MemoryBlock*); @@ -501,6 +522,7 @@ private: ReferenceCountedArray filesToUpload; URL (const String&, int); + void init(); void addParameter (const String&, const String&); void createHeadersAndPostData (String&, MemoryBlock&) const; URL withUpload (Upload*) const;