| @@ -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. | |||