123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456 |
- // This file is based on third party code, see below for the original author
- // and original license.
- // Modifications are (c) by Threema GmbH and licensed under the AGPLv3.
- /*
- * This file is part of the SDWebImage package.
- * (c) Olivier Poitrey <rs@dailymotion.com>
- *
- * For the full copyright and license information, please view the LICENSE
- * file that was distributed with this source code.
- */
- #import "SDWebImageDownloaderOperation.h"
- #import "SDWebImageDecoder.h"
- #import "UIImage+MultiFormat.h"
- #import <ImageIO/ImageIO.h>
- #import "SDWebImageManager.h"
- @interface SDWebImageDownloaderOperation () <NSURLConnectionDataDelegate>
- @property (copy, nonatomic) SDWebImageDownloaderProgressBlock progressBlock;
- @property (copy, nonatomic) SDWebImageDownloaderCompletedBlock completedBlock;
- @property (copy, nonatomic) SDWebImageNoParamsBlock cancelBlock;
- @property (assign, nonatomic, getter = isExecuting) BOOL executing;
- @property (assign, nonatomic, getter = isFinished) BOOL finished;
- @property (assign, nonatomic) NSInteger expectedSize;
- @property (strong, nonatomic) NSMutableData *imageData;
- @property (strong, nonatomic) NSURLConnection *connection;
- @property (strong, atomic) NSThread *thread;
- #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0
- @property (assign, nonatomic) UIBackgroundTaskIdentifier backgroundTaskId;
- #endif
- @end
- @implementation SDWebImageDownloaderOperation {
- size_t width, height;
- UIImageOrientation orientation;
- BOOL responseFromCached;
- }
- @synthesize executing = _executing;
- @synthesize finished = _finished;
- - (id)initWithRequest:(NSURLRequest *)request
- options:(SDWebImageDownloaderOptions)options
- progress:(SDWebImageDownloaderProgressBlock)progressBlock
- completed:(SDWebImageDownloaderCompletedBlock)completedBlock
- cancelled:(SDWebImageNoParamsBlock)cancelBlock {
- if ((self = [super init])) {
- _request = request;
- _shouldDecompressImages = YES;
- _shouldUseCredentialStorage = YES;
- _options = options;
- _progressBlock = [progressBlock copy];
- _completedBlock = [completedBlock copy];
- _cancelBlock = [cancelBlock copy];
- _executing = NO;
- _finished = NO;
- _expectedSize = 0;
- responseFromCached = YES; // Initially wrong until `connection:willCacheResponse:` is called or not called
- }
- return self;
- }
- - (void)start {
- @synchronized (self) {
- if (self.isCancelled) {
- self.finished = YES;
- [self reset];
- return;
- }
- /***** BEGIN THREEMA MODIFICATION: disable to make code comply with extension restrictions *********/
- #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 && FALSE
- /***** END THREEMA MODIFICATION *********/
- if ([self shouldContinueWhenAppEntersBackground]) {
- __weak __typeof__ (self) wself = self;
- self.backgroundTaskId = [[UIApplication sharedApplication] beginBackgroundTaskWithExpirationHandler:^{
- __strong __typeof (wself) sself = wself;
- if (sself) {
- [sself cancel];
- [[UIApplication sharedApplication] endBackgroundTask:sself.backgroundTaskId];
- sself.backgroundTaskId = UIBackgroundTaskInvalid;
- }
- }];
- }
- #endif
- self.executing = YES;
- self.connection = [[NSURLConnection alloc] initWithRequest:self.request delegate:self startImmediately:NO];
- self.thread = [NSThread currentThread];
- }
- [self.connection start];
- if (self.connection) {
- if (self.progressBlock) {
- self.progressBlock(0, NSURLResponseUnknownLength);
- }
- dispatch_async(dispatch_get_main_queue(), ^{
- [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStartNotification object:self];
- });
- if (floor(NSFoundationVersionNumber) <= NSFoundationVersionNumber_iOS_5_1) {
- // Make sure to run the runloop in our background thread so it can process downloaded data
- // Note: we use a timeout to work around an issue with NSURLConnection cancel under iOS 5
- // not waking up the runloop, leading to dead threads (see https://github.com/rs/SDWebImage/issues/466)
- CFRunLoopRunInMode(kCFRunLoopDefaultMode, 10, false);
- }
- else {
- CFRunLoopRun();
- }
- if (!self.isFinished) {
- [self.connection cancel];
- [self connection:self.connection didFailWithError:[NSError errorWithDomain:NSURLErrorDomain code:NSURLErrorTimedOut userInfo:@{NSURLErrorFailingURLErrorKey : self.request.URL}]];
- }
- }
- else {
- if (self.completedBlock) {
- self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:0 userInfo:@{NSLocalizedDescriptionKey : @"Connection can't be initialized"}], YES);
- }
- }
- /***** BEGIN THREEMA MODIFICATION: disable to make code comply with extension restrictions *********/
- #if TARGET_OS_IPHONE && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_4_0 && FALSE
- /***** END THREEMA MODIFICATION *********/
- if (self.backgroundTaskId != UIBackgroundTaskInvalid) {
- [[UIApplication sharedApplication] endBackgroundTask:self.backgroundTaskId];
- self.backgroundTaskId = UIBackgroundTaskInvalid;
- }
- #endif
- }
- - (void)cancel {
- @synchronized (self) {
- if (self.thread) {
- [self performSelector:@selector(cancelInternalAndStop) onThread:self.thread withObject:nil waitUntilDone:NO];
- }
- else {
- [self cancelInternal];
- }
- }
- }
- - (void)cancelInternalAndStop {
- if (self.isFinished) return;
- [self cancelInternal];
- CFRunLoopStop(CFRunLoopGetCurrent());
- }
- - (void)cancelInternal {
- if (self.isFinished) return;
- [super cancel];
- if (self.cancelBlock) self.cancelBlock();
- if (self.connection) {
- [self.connection cancel];
- dispatch_async(dispatch_get_main_queue(), ^{
- [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:self];
- });
- // As we cancelled the connection, its callback won't be called and thus won't
- // maintain the isFinished and isExecuting flags.
- if (self.isExecuting) self.executing = NO;
- if (!self.isFinished) self.finished = YES;
- }
- [self reset];
- }
- - (void)done {
- self.finished = YES;
- self.executing = NO;
- [self reset];
- }
- - (void)reset {
- self.cancelBlock = nil;
- self.completedBlock = nil;
- self.progressBlock = nil;
- self.connection = nil;
- self.imageData = nil;
- self.thread = nil;
- }
- - (void)setFinished:(BOOL)finished {
- [self willChangeValueForKey:@"isFinished"];
- _finished = finished;
- [self didChangeValueForKey:@"isFinished"];
- }
- - (void)setExecuting:(BOOL)executing {
- [self willChangeValueForKey:@"isExecuting"];
- _executing = executing;
- [self didChangeValueForKey:@"isExecuting"];
- }
- - (BOOL)isConcurrent {
- return YES;
- }
- #pragma mark NSURLConnection (delegate)
- - (void)connection:(NSURLConnection *)connection didReceiveResponse:(NSURLResponse *)response {
-
- //'304 Not Modified' is an exceptional one
- if ((![response respondsToSelector:@selector(statusCode)] || [((NSHTTPURLResponse *)response) statusCode] < 400) && [((NSHTTPURLResponse *)response) statusCode] != 304) {
- NSInteger expected = response.expectedContentLength > 0 ? (NSInteger)response.expectedContentLength : 0;
- self.expectedSize = expected;
- if (self.progressBlock) {
- self.progressBlock(0, expected);
- }
- self.imageData = [[NSMutableData alloc] initWithCapacity:expected];
- }
- else {
- NSUInteger code = [((NSHTTPURLResponse *)response) statusCode];
-
- //This is the case when server returns '304 Not Modified'. It means that remote image is not changed.
- //In case of 304 we need just cancel the operation and return cached image from the cache.
- if (code == 304) {
- [self cancelInternal];
- } else {
- [self.connection cancel];
- }
- dispatch_async(dispatch_get_main_queue(), ^{
- [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];
- });
- if (self.completedBlock) {
- self.completedBlock(nil, nil, [NSError errorWithDomain:NSURLErrorDomain code:[((NSHTTPURLResponse *)response) statusCode] userInfo:nil], YES);
- }
- CFRunLoopStop(CFRunLoopGetCurrent());
- [self done];
- }
- }
- - (void)connection:(NSURLConnection *)connection didReceiveData:(NSData *)data {
- [self.imageData appendData:data];
- if ((self.options & SDWebImageDownloaderProgressiveDownload) && self.expectedSize > 0 && self.completedBlock) {
- // The following code is from http://www.cocoaintheshell.com/2011/05/progressive-images-download-imageio/
- // Thanks to the author @Nyx0uf
- // Get the total bytes downloaded
- const NSInteger totalSize = self.imageData.length;
- // Update the data source, we must pass ALL the data, not just the new bytes
- CGImageSourceRef imageSource = CGImageSourceCreateWithData((__bridge CFDataRef)self.imageData, NULL);
- if (width + height == 0) {
- CFDictionaryRef properties = CGImageSourceCopyPropertiesAtIndex(imageSource, 0, NULL);
- if (properties) {
- NSInteger orientationValue = -1;
- CFTypeRef val = CFDictionaryGetValue(properties, kCGImagePropertyPixelHeight);
- if (val) CFNumberGetValue(val, kCFNumberLongType, &height);
- val = CFDictionaryGetValue(properties, kCGImagePropertyPixelWidth);
- if (val) CFNumberGetValue(val, kCFNumberLongType, &width);
- val = CFDictionaryGetValue(properties, kCGImagePropertyOrientation);
- if (val) CFNumberGetValue(val, kCFNumberNSIntegerType, &orientationValue);
- CFRelease(properties);
- // When we draw to Core Graphics, we lose orientation information,
- // which means the image below born of initWithCGIImage will be
- // oriented incorrectly sometimes. (Unlike the image born of initWithData
- // in connectionDidFinishLoading.) So save it here and pass it on later.
- orientation = [[self class] orientationFromPropertyValue:(orientationValue == -1 ? 1 : orientationValue)];
- }
- }
- if (width + height > 0 && totalSize < self.expectedSize) {
- // Create the image
- CGImageRef partialImageRef = CGImageSourceCreateImageAtIndex(imageSource, 0, NULL);
- #ifdef TARGET_OS_IPHONE
- // Workaround for iOS anamorphic image
- if (partialImageRef) {
- const size_t partialHeight = CGImageGetHeight(partialImageRef);
- CGColorSpaceRef colorSpace = CGColorSpaceCreateDeviceRGB();
- CGContextRef bmContext = CGBitmapContextCreate(NULL, width, height, 8, width * 4, colorSpace, kCGBitmapByteOrderDefault | kCGImageAlphaPremultipliedFirst);
- CGColorSpaceRelease(colorSpace);
- if (bmContext) {
- CGContextDrawImage(bmContext, (CGRect){.origin.x = 0.0f, .origin.y = 0.0f, .size.width = width, .size.height = partialHeight}, partialImageRef);
- CGImageRelease(partialImageRef);
- partialImageRef = CGBitmapContextCreateImage(bmContext);
- CGContextRelease(bmContext);
- }
- else {
- CGImageRelease(partialImageRef);
- partialImageRef = nil;
- }
- }
- #endif
- if (partialImageRef) {
- UIImage *image = [UIImage imageWithCGImage:partialImageRef scale:1 orientation:orientation];
- NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
- UIImage *scaledImage = [self scaledImageForKey:key image:image];
- if (self.shouldDecompressImages) {
- image = [UIImage decodedImageWithImage:scaledImage];
- }
- else {
- image = scaledImage;
- }
- CGImageRelease(partialImageRef);
- dispatch_main_sync_safe(^{
- if (self.completedBlock) {
- self.completedBlock(image, nil, nil, NO);
- }
- });
- }
- }
- CFRelease(imageSource);
- }
- if (self.progressBlock) {
- self.progressBlock(self.imageData.length, self.expectedSize);
- }
- }
- + (UIImageOrientation)orientationFromPropertyValue:(NSInteger)value {
- switch (value) {
- case 1:
- return UIImageOrientationUp;
- case 3:
- return UIImageOrientationDown;
- case 8:
- return UIImageOrientationLeft;
- case 6:
- return UIImageOrientationRight;
- case 2:
- return UIImageOrientationUpMirrored;
- case 4:
- return UIImageOrientationDownMirrored;
- case 5:
- return UIImageOrientationLeftMirrored;
- case 7:
- return UIImageOrientationRightMirrored;
- default:
- return UIImageOrientationUp;
- }
- }
- - (UIImage *)scaledImageForKey:(NSString *)key image:(UIImage *)image {
- return SDScaledImageForKey(key, image);
- }
- - (void)connectionDidFinishLoading:(NSURLConnection *)aConnection {
- SDWebImageDownloaderCompletedBlock completionBlock = self.completedBlock;
- @synchronized(self) {
- CFRunLoopStop(CFRunLoopGetCurrent());
- self.thread = nil;
- self.connection = nil;
- dispatch_async(dispatch_get_main_queue(), ^{
- [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];
- });
- }
-
- if (![[NSURLCache sharedURLCache] cachedResponseForRequest:_request]) {
- responseFromCached = NO;
- }
-
- if (completionBlock) {
- if (self.options & SDWebImageDownloaderIgnoreCachedResponse && responseFromCached) {
- completionBlock(nil, nil, nil, YES);
- }
- else {
- UIImage *image = [UIImage sd_imageWithData:self.imageData];
- NSString *key = [[SDWebImageManager sharedManager] cacheKeyForURL:self.request.URL];
- image = [self scaledImageForKey:key image:image];
-
- // Do not force decoding animated GIFs
- if (!image.images) {
- if (self.shouldDecompressImages) {
- image = [UIImage decodedImageWithImage:image];
- }
- }
- if (CGSizeEqualToSize(image.size, CGSizeZero)) {
- completionBlock(nil, nil, [NSError errorWithDomain:@"SDWebImageErrorDomain" code:0 userInfo:@{NSLocalizedDescriptionKey : @"Downloaded image has 0 pixels"}], YES);
- }
- else {
- completionBlock(image, self.imageData, nil, YES);
- }
- }
- }
- self.completionBlock = nil;
- [self done];
- }
- - (void)connection:(NSURLConnection *)connection didFailWithError:(NSError *)error {
- @synchronized(self) {
- CFRunLoopStop(CFRunLoopGetCurrent());
- self.thread = nil;
- self.connection = nil;
- dispatch_async(dispatch_get_main_queue(), ^{
- [[NSNotificationCenter defaultCenter] postNotificationName:SDWebImageDownloadStopNotification object:nil];
- });
- }
- if (self.completedBlock) {
- self.completedBlock(nil, nil, error, YES);
- }
- self.completionBlock = nil;
- [self done];
- }
- - (NSCachedURLResponse *)connection:(NSURLConnection *)connection willCacheResponse:(NSCachedURLResponse *)cachedResponse {
- responseFromCached = NO; // If this method is called, it means the response wasn't read from cache
- if (self.request.cachePolicy == NSURLRequestReloadIgnoringLocalCacheData) {
- // Prevents caching of responses
- return nil;
- }
- else {
- return cachedResponse;
- }
- }
- - (BOOL)shouldContinueWhenAppEntersBackground {
- return self.options & SDWebImageDownloaderContinueInBackground;
- }
- - (BOOL)connectionShouldUseCredentialStorage:(NSURLConnection __unused *)connection {
- return self.shouldUseCredentialStorage;
- }
- - (void)connection:(NSURLConnection *)connection willSendRequestForAuthenticationChallenge:(NSURLAuthenticationChallenge *)challenge{
- if ([challenge.protectionSpace.authenticationMethod isEqualToString:NSURLAuthenticationMethodServerTrust]) {
- if (!(self.options & SDWebImageDownloaderAllowInvalidSSLCertificates) &&
- [challenge.sender respondsToSelector:@selector(performDefaultHandlingForAuthenticationChallenge:)]) {
- [challenge.sender performDefaultHandlingForAuthenticationChallenge:challenge];
- } else {
- NSURLCredential *credential = [NSURLCredential credentialForTrust:challenge.protectionSpace.serverTrust];
- [[challenge sender] useCredential:credential forAuthenticationChallenge:challenge];
- }
- } else {
- if ([challenge previousFailureCount] == 0) {
- if (self.credential) {
- [[challenge sender] useCredential:self.credential forAuthenticationChallenge:challenge];
- } else {
- [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
- }
- } else {
- [[challenge sender] continueWithoutCredentialForAuthenticationChallenge:challenge];
- }
- }
- }
- @end
|