@@ -80,9 +80,17 @@ public: | |||
{} | |||
~Pimpl() | |||
{ | |||
cancel(); | |||
} | |||
void cancel() | |||
{ | |||
if (stream != 0) | |||
{ | |||
stream.callVoidMethod (HTTPStream.release); | |||
stream.clear(); | |||
} | |||
} | |||
bool connect (WebInputStream::Listener* listener) | |||
@@ -154,7 +162,11 @@ public: | |||
responseHeaders.set (key, previousValue.isEmpty() ? value : (previousValue + "," + value)); | |||
} | |||
return true; | |||
} | |||
return false; | |||
} | |||
//============================================================================== | |||
@@ -219,3 +231,8 @@ private: | |||
GlobalRef stream; | |||
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 () | |||
{ | |||
@@ -512,3 +517,8 @@ public: | |||
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); | |||
} | |||
void cancel() | |||
{ | |||
statusCode = -1; | |||
finished = true; | |||
closeSocket(); | |||
} | |||
//==============================================================================w | |||
bool isError() const { return socketHandle < 0; } | |||
bool isExhausted() { return finished; } | |||
@@ -556,4 +564,9 @@ private: | |||
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 |
@@ -150,6 +150,12 @@ public: | |||
[data release]; | |||
} | |||
void cancel() | |||
{ | |||
signalThreadShouldExit(); | |||
stopThread (10000); | |||
} | |||
bool start (WebInputStream& inputStream, WebInputStream::Listener* listener) | |||
{ | |||
startThread(); | |||
@@ -369,6 +375,200 @@ private: | |||
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 | |||
@@ -440,6 +640,12 @@ public: | |||
stopThread (10000); | |||
} | |||
void cancel() | |||
{ | |||
hasFinished = hasFailed = true; | |||
stop(); | |||
} | |||
int read (char* dest, int numBytes) | |||
{ | |||
int numDone = 0; | |||
@@ -615,6 +821,11 @@ private: | |||
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 | |||
#endif | |||
@@ -728,6 +939,15 @@ public: | |||
return true; | |||
} | |||
void cancel() | |||
{ | |||
if (finished || isError()) | |||
return; | |||
if (connection != nullptr) | |||
connection->cancel(); | |||
} | |||
int statusCode; | |||
private: | |||
@@ -185,6 +185,11 @@ public: | |||
return (int) bytesRead; | |||
} | |||
void cancel() | |||
{ | |||
close(); | |||
} | |||
bool setPosition (int64 wantedPos) | |||
{ | |||
if (isError()) | |||
@@ -231,11 +236,11 @@ private: | |||
void close() | |||
{ | |||
if (request != 0) | |||
{ | |||
InternetCloseHandle (request); | |||
request = 0; | |||
} | |||
HINTERNET requestCopy = request; | |||
request = 0; | |||
if (requestCopy != 0) | |||
InternetCloseHandle (requestCopy); | |||
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; | |||
} | |||
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() | |||
{ | |||
} | |||
@@ -322,6 +322,81 @@ public: | |||
int numRedirectsToFollow = 5, | |||
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. | |||
@@ -42,6 +42,7 @@ WebInputStream& WebInputStream::withNumRedirectsToFollow (int num) { | |||
StringPairArray WebInputStream::getRequestHeaders() const { return pimpl->getRequestHeaders(); } | |||
StringPairArray WebInputStream::getResponseHeaders() { connect (nullptr); return pimpl->getResponseHeaders(); } | |||
bool WebInputStream::isError() const { return pimpl->isError(); } | |||
void WebInputStream::cancel() { pimpl->cancel(); } | |||
bool WebInputStream::isExhausted() { return pimpl->isExhausted(); } | |||
int64 WebInputStream::getPosition() { return pimpl->getPosition(); } | |||
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 */ | |||
bool isError() const; | |||
/** Will cancel a blocking read. */ | |||
void cancel(); | |||
//============================================================================== | |||
/** Returns the total number of bytes available for reading in this stream. | |||