@@ -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<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 | |||
{ | |||
@@ -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; | |||
@@ -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<MACAddress>& /*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<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; | |||
@@ -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<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) | |||
{ | |||
@@ -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<jbyte*> (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) | |||
@@ -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<WebInputStream> 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<InputStream> in (createInputStream (usePostCommand)); | |||
const ScopedPointer<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | |||
: static_cast<InputStream*> (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<InputStream> in (createInputStream (usePostCommand)); | |||
const ScopedPointer<InputStream> in (isLocalFile() ? getLocalFile().createInputStream() | |||
: static_cast<InputStream*> (createInputStream (usePostCommand))); | |||
if (in != nullptr) | |||
return in->readEntireStreamAsString(); | |||
@@ -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<Upload> filesToUpload; | |||
URL (const String&, int); | |||
void init(); | |||
void addParameter (const String&, const String&); | |||
void createHeadersAndPostData (String&, MemoryBlock&) const; | |||
URL withUpload (Upload*) const; | |||