@@ -32,6 +32,328 @@ namespace juce | |||||
DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection"); | DECLARE_JNI_CLASS (MediaScannerConnection, "android/media/MediaScannerConnection"); | ||||
#undef JNI_CLASS_MEMBERS | #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<jobject> getInputStreamForContentUri (const URL& url) | |||||
{ | |||||
// only use this method for content URIs | |||||
jassert (url.getScheme() == "content"); | |||||
auto* env = getEnv(); | |||||
LocalRef<jobject> contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); | |||||
if (contentResolver) | |||||
return LocalRef<jobject> ((env->CallObjectMethod (contentResolver.get(), ContentResolver.openInputStream, urlToUri (url).get()))); | |||||
return LocalRef<jobject>(); | |||||
} | |||||
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<jobject> contentResolver (android.activity.callObjectMethod (JuceAppActivity.getContentResolver)); | |||||
if (contentResolver) | |||||
{ | |||||
LocalRef<jstring> columnName (javaString ("_data")); | |||||
LocalRef<jobjectArray> projection (env->NewObjectArray (1, JavaString, columnName.get())); | |||||
LocalRef<jobjectArray> args; | |||||
if (selection.isNotEmpty()) | |||||
{ | |||||
args = LocalRef<jobjectArray> (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<jstring> jSelection (selection.isNotEmpty() ? javaString (selection) : LocalRef<jstring>()); | |||||
LocalRef<jobject> 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<jstring> 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<jobject> 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<jobject> (env->CallStaticObjectMethod (AndroidEnvironment, AndroidEnvironment.getExternalStorageDirectory))); | |||||
} | |||||
static Array<File> getSecondaryStorageDirectories() | |||||
{ | |||||
Array<File> 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<jobject> (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<File> convertFileArray (LocalRef<jobject> obj) | |||||
{ | |||||
auto* env = getEnv(); | |||||
int n = (int) env->GetArrayLength ((jobjectArray) obj.get()); | |||||
Array<File> files; | |||||
for (int i = 0; i < n; ++i) | |||||
files.add (juceFile (LocalRef<jobject> (env->GetObjectArrayElement ((jobjectArray) obj.get(), | |||||
(jsize) i)))); | |||||
return files; | |||||
} | |||||
static File juceFile (LocalRef<jobject> obj) | |||||
{ | |||||
auto* env = getEnv(); | |||||
if (env->IsInstanceOf (obj.get(), AndroidFile) != 0) | |||||
return File (safeString (LocalRef<jobject> (env->CallObjectMethod (obj.get(), | |||||
AndroidFile.getAbsolutePath)))); | |||||
return {}; | |||||
} | |||||
//============================================================================== | |||||
static int getSDKVersion() | |||||
{ | |||||
static int sdkVersion | |||||
= getEnv()->CallStaticIntMethod (JuceAppActivity, | |||||
JuceAppActivity.getAndroidSDKVersion); | |||||
return sdkVersion; | |||||
} | |||||
static LocalRef<jobject> urlToUri (const URL& url) | |||||
{ | |||||
return LocalRef<jobject> (getEnv()->CallStaticObjectMethod (Uri, Uri.parse, javaString (url.toString (true)).get())); | |||||
} | |||||
static String safeString (LocalRef<jobject> str) | |||||
{ | |||||
if (str) | |||||
return juceString ((jstring) str.get()); | |||||
return {}; | |||||
} | |||||
}; | |||||
//============================================================================== | //============================================================================== | ||||
class MediaScannerConnectionClient : public AndroidInterfaceImplementer | class MediaScannerConnectionClient : public AndroidInterfaceImplementer | ||||
{ | { | ||||
@@ -324,6 +324,7 @@ extern AndroidSystem android; | |||||
METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ | METHOD (startIntentSenderForResult, "startIntentSenderForResult", "(Landroid/content/IntentSender;ILandroid/content/Intent;III)V") \ | ||||
METHOD (moveTaskToBack, "moveTaskToBack", "(Z)Z") \ | METHOD (moveTaskToBack, "moveTaskToBack", "(Z)Z") \ | ||||
METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ | METHOD (startActivity, "startActivity", "(Landroid/content/Intent;)V") \ | ||||
METHOD (getContentResolver, "getContentResolver", "()Landroid/content/ContentResolver;") \ | |||||
DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); | DECLARE_JNI_CLASS (JuceAppActivity, JUCE_ANDROID_ACTIVITY_CLASSPATH); | ||||
#undef JNI_CLASS_MEMBERS | #undef JNI_CLASS_MEMBERS | ||||
@@ -563,6 +564,13 @@ DECLARE_JNI_CLASS (JavaSet, "java/util/Set"); | |||||
DECLARE_JNI_CLASS (JavaString, "java/lang/String"); | DECLARE_JNI_CLASS (JavaString, "java/lang/String"); | ||||
#undef JNI_CLASS_MEMBERS | #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; | class AndroidInterfaceImplementer; | ||||
@@ -43,6 +43,13 @@ DECLARE_JNI_CLASS (StringBuffer, "java/lang/StringBuffer"); | |||||
DECLARE_JNI_CLASS (HTTPStream, JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream"); | DECLARE_JNI_CLASS (HTTPStream, JUCE_ANDROID_ACTIVITY_CLASSPATH "$HTTPStream"); | ||||
#undef JNI_CLASS_MEMBERS | #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<MACAddress>& /*result*/) | void MACAddress::findAllAddresses (Array<MACAddress>& /*result*/) | ||||
@@ -60,12 +67,43 @@ JUCE_API bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& /*t | |||||
return false; | 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 | class WebInputStream::Pimpl | ||||
{ | { | ||||
public: | public: | ||||
enum { contentStreamCacheSize = 1024 }; | |||||
Pimpl (WebInputStream&, const URL& urlToCopy, bool shouldBePost) | Pimpl (WebInputStream&, const URL& urlToCopy, bool shouldBePost) | ||||
: url (urlToCopy), isPost (shouldBePost), | |||||
: url (urlToCopy), | |||||
isContentURL (urlToCopy.getScheme() == "content"), | |||||
isPost (shouldBePost), | |||||
httpRequest (isPost ? "POST" : "GET") | httpRequest (isPost ? "POST" : "GET") | ||||
{} | {} | ||||
@@ -76,6 +114,12 @@ public: | |||||
void cancel() | void cancel() | ||||
{ | { | ||||
if (isContentURL) | |||||
{ | |||||
stream.callVoidMethod (AndroidInputStream.close); | |||||
return; | |||||
} | |||||
const ScopedLock lock (createStreamLock); | const ScopedLock lock (createStreamLock); | ||||
if (stream != 0) | if (stream != 0) | ||||
@@ -89,83 +133,98 @@ public: | |||||
bool connect (WebInputStream::Listener* /*listener*/) | 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<jsize> (postData.getSize())); | |||||
env->SetByteArrayRegion (postDataArray, 0, static_cast<jsize> (postData.getSize()), (const jbyte*) postData.getData()); | |||||
return true; | |||||
} | |||||
} | } | ||||
else | |||||
{ | |||||
String address = url.toString (! isPost); | |||||
LocalRef<jobject> 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<jsize> (postData.getSize())); | |||||
env->SetByteArrayRegion (postDataArray, 0, static_cast<jsize> (postData.getSize()), (const jbyte*) postData.getData()); | |||||
} | |||||
if (stream != 0 && ! stream.callBooleanMethod (HTTPStream.connect)) | |||||
stream.clear(); | |||||
LocalRef<jobject> 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<jstring> headersString ((jstring) env->CallObjectMethod (responseHeaderBuffer.get(), | |||||
StringBuffer.toString)); | |||||
headerLines.addLines (juceString (env, headersString)); | |||||
const ScopedLock lock (createStreamLock); | |||||
if (! hasBeenCancelled) | |||||
stream = GlobalRef (LocalRef<jobject> (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<jstring> 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; | return false; | ||||
@@ -193,11 +252,30 @@ public: | |||||
//============================================================================== | //============================================================================== | ||||
bool isError() const { return stream == nullptr; } | 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<char> buffer (bytesToSkip); | |||||
return (read (buffer.getData(), (int) bytesToSkip) > 0); | |||||
} | |||||
return stream != nullptr && stream.callBooleanMethod (HTTPStream.setPosition, (jlong) wantedPos); | |||||
} | |||||
int read (void* buffer, int bytesToRead) | int read (void* buffer, int bytesToRead) | ||||
{ | { | ||||
@@ -212,12 +290,19 @@ public: | |||||
jbyteArray javaArray = env->NewByteArray (bytesToRead); | 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) | if (numBytes > 0) | ||||
env->GetByteArrayRegion (javaArray, 0, numBytes, static_cast<jbyte*> (buffer)); | env->GetByteArrayRegion (javaArray, 0, numBytes, static_cast<jbyte*> (buffer)); | ||||
env->DeleteLocalRef (javaArray); | env->DeleteLocalRef (javaArray); | ||||
readPosition += jmax (0, numBytes); | |||||
if (numBytes == -1) | |||||
eofStreamReached = true; | |||||
return numBytes; | return numBytes; | ||||
} | } | ||||
@@ -226,12 +311,13 @@ public: | |||||
private: | private: | ||||
const URL url; | const URL url; | ||||
bool isPost; | |||||
bool isContentURL, isPost, eofStreamReached = false; | |||||
int numRedirectsToFollow = 5, timeOutMs = 0; | int numRedirectsToFollow = 5, timeOutMs = 0; | ||||
String httpRequest, headers; | String httpRequest, headers; | ||||
StringPairArray responseHeaders; | StringPairArray responseHeaders; | ||||
CriticalSection createStreamLock; | CriticalSection createStreamLock; | ||||
bool hasBeenCancelled = false; | bool hasBeenCancelled = false; | ||||
int readPosition = 0; | |||||
GlobalRef stream; | GlobalRef stream; | ||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | ||||
@@ -137,6 +137,28 @@ URL::DownloadTask::~DownloadTask() {} | |||||
URL::URL() noexcept {} | URL::URL() noexcept {} | ||||
URL::URL (const String& u) : url (u) | 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 ('?'); | int i = url.indexOfChar ('?'); | ||||
@@ -320,6 +342,40 @@ String URL::getScheme() const | |||||
return url.substring (0, URLHelpers::findEndOfScheme (url) - 1); | 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 | int URL::getPort() const | ||||
{ | { | ||||
auto colonPos = url.indexOfChar (URLHelpers::findStartOfNetLocation (url), ':'); | 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<WebInputStream> wi (new WebInputStream (*this, usePostCommand)); | ScopedPointer<WebInputStream> wi (new WebInputStream (*this, usePostCommand)); | ||||
struct ProgressCallbackCaller : WebInputStream::Listener | struct ProgressCallbackCaller : WebInputStream::Listener | ||||
@@ -501,7 +560,8 @@ WebInputStream* URL::createInputStream (const bool usePostCommand, | |||||
//============================================================================== | //============================================================================== | ||||
bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const | bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) const | ||||
{ | { | ||||
const ScopedPointer<InputStream> in (createInputStream (usePostCommand)); | |||||
const ScopedPointer<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | |||||
: static_cast<InputStream*> (createInputStream (usePostCommand))); | |||||
if (in != nullptr) | if (in != nullptr) | ||||
{ | { | ||||
@@ -514,7 +574,8 @@ bool URL::readEntireBinaryStream (MemoryBlock& destData, bool usePostCommand) co | |||||
String URL::readEntireTextStream (bool usePostCommand) const | String URL::readEntireTextStream (bool usePostCommand) const | ||||
{ | { | ||||
const ScopedPointer<InputStream> in (createInputStream (usePostCommand)); | |||||
const ScopedPointer<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | |||||
: static_cast<InputStream*> (createInputStream (usePostCommand))); | |||||
if (in != nullptr) | if (in != nullptr) | ||||
return in->readEntireStreamAsString(); | return in->readEntireStreamAsString(); | ||||
@@ -53,6 +53,9 @@ public: | |||||
URL (URL&&); | URL (URL&&); | ||||
URL& operator= (URL&&); | URL& operator= (URL&&); | ||||
/** Creates URL referring to a local file on your disk using the file:// scheme. */ | |||||
explicit URL (File); | |||||
/** Destructor. */ | /** Destructor. */ | ||||
~URL(); | ~URL(); | ||||
@@ -94,6 +97,20 @@ public: | |||||
*/ | */ | ||||
String getScheme() const; | 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. | /** Attempts to read a port number from the URL. | ||||
@returns the port number, or 0 if none is explicitly specified. | @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. | /** 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 | Note that this method will block until the first byte of data has been received or an | ||||
error has occurred. | error has occurred. | ||||
Note that on some platforms (Android, for example) it's not permitted to do any network | 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. | 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 | @param doPostLikeRequest if true, the parameters added to this class will be transferred | ||||
via the HTTP headers which is typical for POST requests. Otherwise | via the HTTP headers which is typical for POST requests. Otherwise | ||||
the parameters will be added to the URL address. Additionally, | 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 | @returns an input stream that the caller must delete, or a null pointer if there was an | ||||
error trying to open it. | 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. | /** Represents a download task. | ||||
@@ -487,6 +506,8 @@ private: | |||||
MemoryBlock postData; | MemoryBlock postData; | ||||
StringArray parameterNames, parameterValues; | StringArray parameterNames, parameterValues; | ||||
static File fileFromFileSchemeURL (const URL&); | |||||
struct Upload : public ReferenceCountedObject | struct Upload : public ReferenceCountedObject | ||||
{ | { | ||||
Upload (const String&, const String&, const String&, const File&, MemoryBlock*); | Upload (const String&, const String&, const String&, const File&, MemoryBlock*); | ||||
@@ -501,6 +522,7 @@ private: | |||||
ReferenceCountedArray<Upload> filesToUpload; | ReferenceCountedArray<Upload> filesToUpload; | ||||
URL (const String&, int); | URL (const String&, int); | ||||
void init(); | |||||
void addParameter (const String&, const String&); | void addParameter (const String&, const String&); | ||||
void createHeadersAndPostData (String&, MemoryBlock&) const; | void createHeadersAndPostData (String&, MemoryBlock&) const; | ||||
URL withUpload (Upload*) const; | URL withUpload (Upload*) const; | ||||