diff --git a/modules/juce_core/native/juce_android_Network.cpp b/modules/juce_core/native/juce_android_Network.cpp index fed05ca43c..1c5c10c390 100644 --- a/modules/juce_core/native/juce_android_Network.cpp +++ b/modules/juce_core/native/juce_android_Network.cpp @@ -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); +} diff --git a/modules/juce_core/native/juce_curl_Network.cpp b/modules/juce_core/native/juce_curl_Network.cpp index 3148ec840a..1a63932022 100644 --- a/modules/juce_core/native/juce_curl_Network.cpp +++ b/modules/juce_core/native/juce_curl_Network.cpp @@ -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); +} diff --git a/modules/juce_core/native/juce_linux_Network.cpp b/modules/juce_core/native/juce_linux_Network.cpp index 84904e1db2..d849cd8e4f 100644 --- a/modules/juce_core/native/juce_linux_Network.cpp +++ b/modules/juce_core/native/juce_linux_Network.cpp @@ -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 diff --git a/modules/juce_core/native/juce_mac_Network.mm b/modules/juce_core/native/juce_mac_Network.mm index 5ebd1a4fb6..c6bf225f80 100644 --- a/modules/juce_core/native/juce_mac_Network.mm +++ b/modules/juce_core/native/juce_mac_Network.mm @@ -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* delegate; + NSURLSession* session; + NSURLSessionDownloadTask* downloadTask; + bool connectFinished; + Atomic 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 > + { + DelegateClass() : ObjCClass > ("JUCE_URLDelegate_") + { + addIvar ("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 (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 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: diff --git a/modules/juce_core/native/juce_win32_Network.cpp b/modules/juce_core/native/juce_win32_Network.cpp index 174e351907..037f1a3801 100644 --- a/modules/juce_core/native/juce_win32_Network.cpp +++ b/modules/juce_core/native/juce_win32_Network.cpp @@ -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); +} diff --git a/modules/juce_core/network/juce_URL.cpp b/modules/juce_core/network/juce_URL.cpp index fad4585011..0b18b09f98 100644 --- a/modules/juce_core/network/juce_URL.cpp +++ b/modules/juce_core/network/juce_URL.cpp @@ -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::max() + : static_cast (contentLength - downloaded)); + + const int actual = stream->read (buffer.getData(), max); + + if (threadShouldExit() || stream->isError()) + break; + + if (! fileStream->write (buffer.getData(), static_cast (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 fileStream; + size_t bufferSize; + HeapBlock buffer; + ScopedPointer 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 outputStream = targetFileToUse.createOutputStream (bufferSize)) + { + ScopedPointer 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() { } diff --git a/modules/juce_core/network/juce_URL.h b/modules/juce_core/network/juce_URL.h index 20918dbf5f..8631557d28 100644 --- a/modules/juce_core/network/juce_URL.h +++ b/modules/juce_core/network/juce_URL.h @@ -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. diff --git a/modules/juce_core/network/juce_WebInputStream.cpp b/modules/juce_core/network/juce_WebInputStream.cpp index d5dae420f0..96d9fd7b86 100644 --- a/modules/juce_core/network/juce_WebInputStream.cpp +++ b/modules/juce_core/network/juce_WebInputStream.cpp @@ -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(); } diff --git a/modules/juce_core/network/juce_WebInputStream.h b/modules/juce_core/network/juce_WebInputStream.h index 84260c0635..b24f6d7a5e 100644 --- a/modules/juce_core/network/juce_WebInputStream.h +++ b/modules/juce_core/network/juce_WebInputStream.h @@ -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.