@@ -80,9 +80,17 @@ public: | |||||
{} | {} | ||||
~Pimpl() | ~Pimpl() | ||||
{ | |||||
cancel(); | |||||
} | |||||
void cancel() | |||||
{ | { | ||||
if (stream != 0) | if (stream != 0) | ||||
{ | |||||
stream.callVoidMethod (HTTPStream.release); | stream.callVoidMethod (HTTPStream.release); | ||||
stream.clear(); | |||||
} | |||||
} | } | ||||
bool connect (WebInputStream::Listener* listener) | bool connect (WebInputStream::Listener* listener) | ||||
@@ -154,7 +162,11 @@ public: | |||||
responseHeaders.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value)); | responseHeaders.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value)); | ||||
} | } | ||||
return true; | |||||
} | } | ||||
return false; | |||||
} | } | ||||
//============================================================================== | //============================================================================== | ||||
@@ -219,3 +231,8 @@ private: | |||||
GlobalRef stream; | GlobalRef stream; | ||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | ||||
}; | }; | ||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener); | |||||
} |
@@ -135,6 +135,11 @@ public: | |||||
} | } | ||||
} | } | ||||
void cancel() | |||||
{ | |||||
cleanup(); | |||||
} | |||||
//============================================================================== | //============================================================================== | ||||
bool setOptions () | bool setOptions () | ||||
{ | { | ||||
@@ -512,3 +517,8 @@ public: | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | ||||
}; | }; | ||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener); | |||||
} |
@@ -138,6 +138,14 @@ public: | |||||
return (statusCode != 0); | return (statusCode != 0); | ||||
} | } | ||||
void cancel() | |||||
{ | |||||
statusCode = -1; | |||||
finished = true; | |||||
closeSocket(); | |||||
} | |||||
//==============================================================================w | //==============================================================================w | ||||
bool isError() const { return socketHandle < 0; } | bool isError() const { return socketHandle < 0; } | ||||
bool isExhausted() { return finished; } | bool isExhausted() { return finished; } | ||||
@@ -556,4 +564,9 @@ private: | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (Pimpl) | ||||
}; | }; | ||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener); | |||||
} | |||||
#endif | #endif |
@@ -150,6 +150,12 @@ public: | |||||
[data release]; | [data release]; | ||||
} | } | ||||
void cancel() | |||||
{ | |||||
signalThreadShouldExit(); | |||||
stopThread (10000); | |||||
} | |||||
bool start (WebInputStream& inputStream, WebInputStream::Listener* listener) | bool start (WebInputStream& inputStream, WebInputStream::Listener* listener) | ||||
{ | { | ||||
startThread(); | startThread(); | ||||
@@ -369,6 +375,200 @@ private: | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (URLConnectionState) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (URLConnectionState) | ||||
}; | }; | ||||
//============================================================================== | |||||
#if JUCE_IOS | |||||
struct BackgroundDownloadTask : public URL::DownloadTask | |||||
{ | |||||
BackgroundDownloadTask (const URL& urlToUse, | |||||
const File& targetLocationToUse, | |||||
String extraHeadersToUse, | |||||
URL::DownloadTask::Listener* listenerToUse) | |||||
: targetLocation (targetLocationToUse), listener (listenerToUse), | |||||
delegate (nullptr), session (nullptr), downloadTask (nullptr), | |||||
connectFinished (false), calledComplete (0) | |||||
{ | |||||
downloaded = -1; | |||||
static DelegateClass cls; | |||||
delegate = [cls.createInstance() init]; | |||||
DelegateClass::setState (delegate, this); | |||||
String uniqueIdentifier = String (urlToUse.toString (true).hashCode64()) + String (Random().nextInt64()); | |||||
NSMutableURLRequest* request = [[NSMutableURLRequest alloc] initWithURL:[NSURL URLWithString:juceStringToNS (urlToUse.toString (true))]]; | |||||
StringArray headerLines; | |||||
headerLines.addLines (extraHeadersToUse); | |||||
headerLines.removeEmptyStrings (true); | |||||
for (int i = 0; i < headerLines.size(); ++i) | |||||
{ | |||||
String key = headerLines[i].upToFirstOccurrenceOf (":", false, false).trim(); | |||||
String value = headerLines[i].fromFirstOccurrenceOf (":", false, false).trim(); | |||||
if (key.isNotEmpty() && value.isNotEmpty()) | |||||
[request addValue: juceStringToNS (value) forHTTPHeaderField: juceStringToNS (key)]; | |||||
} | |||||
session = | |||||
[NSURLSession sessionWithConfiguration:[NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:juceStringToNS (uniqueIdentifier)] | |||||
delegate:delegate | |||||
delegateQueue:nullptr]; | |||||
if (session != nullptr) | |||||
downloadTask = [session downloadTaskWithRequest:request]; | |||||
[request release]; | |||||
} | |||||
~BackgroundDownloadTask() | |||||
{ | |||||
[session release]; | |||||
[delegate release]; | |||||
} | |||||
bool initOK() | |||||
{ | |||||
return (downloadTask != nullptr); | |||||
} | |||||
bool connect() | |||||
{ | |||||
[downloadTask resume]; | |||||
while (downloaded == -1 && finished == false) | |||||
Thread::sleep (1); | |||||
connectFinished = true; | |||||
return ! error; | |||||
} | |||||
//============================================================================== | |||||
File targetLocation; | |||||
URL::DownloadTask::Listener* listener; | |||||
NSObject<NSURLSessionDelegate>* delegate; | |||||
NSURLSession* session; | |||||
NSURLSessionDownloadTask* downloadTask; | |||||
bool connectFinished; | |||||
Atomic<int> calledComplete; | |||||
void didWriteData (int64 totalBytesWritten, int64 totalBytesExpectedToWrite) | |||||
{ | |||||
downloaded = totalBytesWritten; | |||||
if (contentLength == -1) | |||||
contentLength = totalBytesExpectedToWrite; | |||||
if (connectFinished && error == false && finished == false && listener != nullptr) | |||||
listener->progress (this, totalBytesWritten, contentLength); | |||||
} | |||||
void didFinishDownloadingToURL (NSURL* location) | |||||
{ | |||||
NSFileManager* fileManager = [[NSFileManager alloc] init]; | |||||
error = ([fileManager moveItemAtURL:location | |||||
toURL:[NSURL fileURLWithPath:juceStringToNS (targetLocation.getFullPathName())] | |||||
error:nullptr] == NO); | |||||
httpCode = 200; | |||||
finished = true; | |||||
if (listener != nullptr && calledComplete.exchange (1) == 0) | |||||
{ | |||||
if (contentLength > 0 && downloaded < contentLength) | |||||
{ | |||||
downloaded = contentLength; | |||||
listener->progress (this, downloaded, contentLength); | |||||
} | |||||
listener->didComplete (this, !error); | |||||
} | |||||
} | |||||
void didCompleteWithError (NSError* nsError) | |||||
{ | |||||
if (calledComplete.exchange (1) == 0) | |||||
{ | |||||
httpCode = -1; | |||||
if (nsError != nullptr) | |||||
{ | |||||
// see https://developer.apple.com/reference/foundation/nsurlsessiondownloadtask?language=objc | |||||
switch ([nsError code]) | |||||
{ | |||||
case NSURLErrorUserAuthenticationRequired: | |||||
httpCode = 401; | |||||
break; | |||||
case NSURLErrorNoPermissionsToReadFile: | |||||
httpCode = 403; | |||||
break; | |||||
case NSURLErrorFileDoesNotExist: | |||||
httpCode = 404; | |||||
break; | |||||
default: | |||||
httpCode = 500; | |||||
} | |||||
} | |||||
error = true; | |||||
finished = true; | |||||
if (listener != nullptr) | |||||
listener->didComplete (this, ! error); | |||||
} | |||||
} | |||||
//============================================================================== | |||||
struct DelegateClass : public ObjCClass<NSObject<NSURLSessionDelegate> > | |||||
{ | |||||
DelegateClass() : ObjCClass<NSObject<NSURLSessionDelegate> > ("JUCE_URLDelegate_") | |||||
{ | |||||
addIvar<BackgroundDownloadTask*> ("state"); | |||||
addMethod (@selector (URLSession:downloadTask:didWriteData:totalBytesWritten:totalBytesExpectedToWrite:), | |||||
didWriteData, "v@:@@qqq"); | |||||
addMethod (@selector (URLSession:downloadTask:didFinishDownloadingToURL:), | |||||
didFinishDownloadingToURL, "v@:@@@"); | |||||
addMethod (@selector (URLSession:task:didCompleteWithError:), | |||||
didCompleteWithError, "v@:@@@"); | |||||
registerClass(); | |||||
} | |||||
static void setState (id self, BackgroundDownloadTask* state) { object_setInstanceVariable (self, "state", state); } | |||||
static BackgroundDownloadTask* getState (id self) { return getIvar<BackgroundDownloadTask*> (self, "state"); } | |||||
private: | |||||
static void didWriteData (id self, SEL, NSURLSession*, NSURLSessionDownloadTask*, int64_t, int64_t totalBytesWritten, int64_t totalBytesExpectedToWrite) | |||||
{ | |||||
if (auto state = getState (self)) state->didWriteData (totalBytesWritten, totalBytesExpectedToWrite); | |||||
} | |||||
static void didFinishDownloadingToURL (id self, SEL, NSURLSession*, NSURLSessionDownloadTask*, NSURL* location) | |||||
{ | |||||
if (auto state = getState (self)) state->didFinishDownloadingToURL (location); | |||||
} | |||||
static void didCompleteWithError (id self, SEL, NSURLSession*, NSURLSessionTask*, NSError* nsError) | |||||
{ | |||||
if (auto state = getState (self)) state->didCompleteWithError (nsError); | |||||
} | |||||
}; | |||||
}; | |||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
ScopedPointer<BackgroundDownloadTask> downloadTask = new BackgroundDownloadTask (*this, targetLocation, extraHeaders, listener); | |||||
if (downloadTask->initOK() && downloadTask->connect()) | |||||
return downloadTask.release(); | |||||
return nullptr; | |||||
} | |||||
#else | |||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener); | |||||
} | |||||
#endif | |||||
//============================================================================== | //============================================================================== | ||||
#else | #else | ||||
@@ -440,6 +640,12 @@ public: | |||||
stopThread (10000); | stopThread (10000); | ||||
} | } | ||||
void cancel() | |||||
{ | |||||
hasFinished = hasFailed = true; | |||||
stop(); | |||||
} | |||||
int read (char* dest, int numBytes) | int read (char* dest, int numBytes) | ||||
{ | { | ||||
int numDone = 0; | int numDone = 0; | ||||
@@ -615,6 +821,11 @@ private: | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (URLConnectionState) | JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (URLConnectionState) | ||||
}; | }; | ||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener); | |||||
} | |||||
#pragma clang diagnostic pop | #pragma clang diagnostic pop | ||||
#endif | #endif | ||||
@@ -728,6 +939,15 @@ public: | |||||
return true; | return true; | ||||
} | } | ||||
void cancel() | |||||
{ | |||||
if (finished || isError()) | |||||
return; | |||||
if (connection != nullptr) | |||||
connection->cancel(); | |||||
} | |||||
int statusCode; | int statusCode; | ||||
private: | private: | ||||
@@ -185,6 +185,11 @@ public: | |||||
return (int) bytesRead; | return (int) bytesRead; | ||||
} | } | ||||
void cancel() | |||||
{ | |||||
close(); | |||||
} | |||||
bool setPosition (int64 wantedPos) | bool setPosition (int64 wantedPos) | ||||
{ | { | ||||
if (isError()) | if (isError()) | ||||
@@ -231,11 +236,11 @@ private: | |||||
void close() | void close() | ||||
{ | { | ||||
if (request != 0) | |||||
{ | |||||
InternetCloseHandle (request); | |||||
request = 0; | |||||
} | |||||
HINTERNET requestCopy = request; | |||||
request = 0; | |||||
if (requestCopy != 0) | |||||
InternetCloseHandle (requestCopy); | |||||
if (connection != 0) | if (connection != 0) | ||||
{ | { | ||||
@@ -548,3 +553,8 @@ bool JUCE_CALLTYPE Process::openEmailWithAttachments (const String& targetEmailA | |||||
return mapiSendMail (0, 0, &message, MAPI_DIALOG | MAPI_LOGON_UI, 0) == SUCCESS_SUCCESS; | return mapiSendMail (0, 0, &message, MAPI_DIALOG | MAPI_LOGON_UI, 0) == SUCCESS_SUCCESS; | ||||
} | } | ||||
URL::DownloadTask* URL::downloadToFile (const File& targetLocation, String extraHeaders, DownloadTask::Listener* listener) | |||||
{ | |||||
return URL::DownloadTask::createFallbackDownloader (*this, targetLocation, extraHeaders, listener); | |||||
} |
@@ -26,6 +26,104 @@ | |||||
============================================================================== | ============================================================================== | ||||
*/ | */ | ||||
//============================================================================== | |||||
struct FallbackDownloadTask : public URL::DownloadTask, | |||||
public Thread | |||||
{ | |||||
FallbackDownloadTask (FileOutputStream* outputStreamToUse, | |||||
size_t bufferSizeToUse, | |||||
WebInputStream* streamToUse, | |||||
URL::DownloadTask::Listener* listenerToUse) | |||||
: Thread ("DownloadTask thread"), | |||||
fileStream (outputStreamToUse), | |||||
bufferSize (bufferSizeToUse), | |||||
buffer (bufferSize), | |||||
stream (streamToUse), | |||||
listener (listenerToUse) | |||||
{ | |||||
contentLength = stream->getTotalLength(); | |||||
httpCode = stream->getStatusCode(); | |||||
startThread (); | |||||
} | |||||
~FallbackDownloadTask() | |||||
{ | |||||
signalThreadShouldExit(); | |||||
stream->cancel(); | |||||
waitForThreadToExit (-1); | |||||
} | |||||
//============================================================================== | |||||
void run() override | |||||
{ | |||||
while (! stream->isExhausted() && ! stream->isError() && ! threadShouldExit()) | |||||
{ | |||||
if (listener != nullptr) | |||||
listener->progress (this, downloaded, contentLength); | |||||
const int max = jmin ((int) bufferSize, contentLength < 0 ? std::numeric_limits<int>::max() | |||||
: static_cast<int> (contentLength - downloaded)); | |||||
const int actual = stream->read (buffer.getData(), max); | |||||
if (threadShouldExit() || stream->isError()) | |||||
break; | |||||
if (! fileStream->write (buffer.getData(), static_cast<size_t> (actual))) | |||||
{ | |||||
error = true; | |||||
break; | |||||
} | |||||
downloaded += actual; | |||||
} | |||||
if (threadShouldExit() || (stream != nullptr && stream->isError())) | |||||
error = true; | |||||
finished = true; | |||||
if (listener != nullptr && ! threadShouldExit()) | |||||
listener->finished (this, ! error); | |||||
} | |||||
//============================================================================== | |||||
ScopedPointer<FileOutputStream> fileStream; | |||||
size_t bufferSize; | |||||
HeapBlock<char> buffer; | |||||
ScopedPointer<WebInputStream> stream; | |||||
URL::DownloadTask::Listener* listener; | |||||
}; | |||||
void URL::DownloadTask::Listener::progress (DownloadTask*, int64, int64) {} | |||||
URL::DownloadTask::Listener::~Listener() {} | |||||
//============================================================================== | |||||
URL::DownloadTask* URL::DownloadTask::createFallbackDownloader (const URL& urlToUse, | |||||
const File& targetFileToUse, | |||||
const String& extraHeadersToUse, | |||||
Listener* listenerToUse) | |||||
{ | |||||
const size_t bufferSize = 0x8000; | |||||
targetFileToUse.deleteFile(); | |||||
if (ScopedPointer<FileOutputStream> outputStream = targetFileToUse.createOutputStream (bufferSize)) | |||||
{ | |||||
ScopedPointer<WebInputStream> stream = new WebInputStream (urlToUse, false); | |||||
stream->withExtraHeaders (extraHeadersToUse); | |||||
if (stream->connect (nullptr)) | |||||
return new FallbackDownloadTask (outputStream.release(), bufferSize, stream.release(), listenerToUse); | |||||
} | |||||
return nullptr; | |||||
} | |||||
URL::DownloadTask::DownloadTask() : contentLength (-1), downloaded (0), finished (false), error (false), httpCode (-1) {} | |||||
URL::DownloadTask::~DownloadTask() {} | |||||
//============================================================================== | |||||
URL::URL() | URL::URL() | ||||
{ | { | ||||
} | } | ||||
@@ -322,6 +322,81 @@ public: | |||||
int numRedirectsToFollow = 5, | int numRedirectsToFollow = 5, | ||||
String httpRequestCmd = String()) const; | String httpRequestCmd = String()) const; | ||||
//============================================================================== | |||||
/** Represents a download task. | |||||
Returned by downloadToFile to allow querying and controling the download task. | |||||
*/ | |||||
class DownloadTask | |||||
{ | |||||
public: | |||||
struct Listener | |||||
{ | |||||
virtual ~Listener(); | |||||
/** Called when the download has finished. Be aware that this callback may | |||||
come on an arbitrary thread. */ | |||||
virtual void finished (DownloadTask* task, bool success) = 0; | |||||
/** Called periodically by the OS to indicate download progress. | |||||
Beware that this callback may come on an arbitrary thread. | |||||
*/ | |||||
virtual void progress (DownloadTask* task, int64 bytesDownloaded, int64 totalLength); | |||||
}; | |||||
/** Releases the resources of the download task, unregisters the listener | |||||
and cancels the download if necessary. */ | |||||
virtual ~DownloadTask(); | |||||
/** Returns the total length of the download task. This may return -1 if the length | |||||
was not returned by the server. */ | |||||
inline int64 getTotalLength() const { return contentLength; } | |||||
/** Returns the number of bytes that have been downloaded so far. */ | |||||
inline int64 getLengthDownloaded() const { return downloaded; } | |||||
/** Returns true if the download finished or there was an error. */ | |||||
inline bool isFinished() const { return finished; } | |||||
/** Returns the status code of the server's response. This will only be valid | |||||
after the download has finished. | |||||
@see isFinished | |||||
*/ | |||||
inline int statusCode() const { return httpCode; } | |||||
/** Returns true if there was an error. */ | |||||
inline bool hadError() const { return error; } | |||||
protected: | |||||
int64 contentLength, downloaded; | |||||
bool finished, error; | |||||
int httpCode; | |||||
DownloadTask (); | |||||
private: | |||||
friend class URL; | |||||
static DownloadTask* createFallbackDownloader (const URL&, const File&, const String&, Listener*); | |||||
JUCE_DECLARE_NON_COPYABLE_WITH_LEAK_DETECTOR (DownloadTask) | |||||
}; | |||||
/** Download the URL to a file. | |||||
This method attempts to download the URL to a given file location. | |||||
Using this method to download files on mobile is less flexible but more reliable | |||||
than using createInputStream or WebInputStreams as it will attempt to download the file | |||||
using a native OS background network task. Such tasks automatically deal with | |||||
network re-connections and continuing your download while your app is suspended but are | |||||
limited to simple GET requests. | |||||
*/ | |||||
DownloadTask* downloadToFile (const File& targetLocation, | |||||
String extraHeaders = String(), | |||||
DownloadTask::Listener* listener = nullptr); | |||||
//============================================================================== | //============================================================================== | ||||
/** Tries to download the entire contents of this URL into a binary data block. | /** Tries to download the entire contents of this URL into a binary data block. | ||||
@@ -42,6 +42,7 @@ WebInputStream& WebInputStream::withNumRedirectsToFollow (int num) { | |||||
StringPairArray WebInputStream::getRequestHeaders() const { return pimpl->getRequestHeaders(); } | StringPairArray WebInputStream::getRequestHeaders() const { return pimpl->getRequestHeaders(); } | ||||
StringPairArray WebInputStream::getResponseHeaders() { connect (nullptr); return pimpl->getResponseHeaders(); } | StringPairArray WebInputStream::getResponseHeaders() { connect (nullptr); return pimpl->getResponseHeaders(); } | ||||
bool WebInputStream::isError() const { return pimpl->isError(); } | bool WebInputStream::isError() const { return pimpl->isError(); } | ||||
void WebInputStream::cancel() { pimpl->cancel(); } | |||||
bool WebInputStream::isExhausted() { return pimpl->isExhausted(); } | bool WebInputStream::isExhausted() { return pimpl->isExhausted(); } | ||||
int64 WebInputStream::getPosition() { return pimpl->getPosition(); } | int64 WebInputStream::getPosition() { return pimpl->getPosition(); } | ||||
int64 WebInputStream::getTotalLength() { connect (nullptr); return pimpl->getTotalLength(); } | int64 WebInputStream::getTotalLength() { connect (nullptr); return pimpl->getTotalLength(); } | ||||
@@ -146,6 +146,9 @@ class JUCE_API WebInputStream : public InputStream | |||||
/** Returns true if there was an error during the connection attempt */ | /** Returns true if there was an error during the connection attempt */ | ||||
bool isError() const; | bool isError() const; | ||||
/** Will cancel a blocking read. */ | |||||
void cancel(); | |||||
//============================================================================== | //============================================================================== | ||||
/** Returns the total number of bytes available for reading in this stream. | /** Returns the total number of bytes available for reading in this stream. | ||||