TwitterImagePipeline/TIPImageFetchDownload.m (254 lines of code) (raw):
//
// TIPImageFetchDownload.m
// TwitterImagePipeline
//
// Created on 8/24/16.
// Copyright © 2020 Twitter. All rights reserved.
//
#import "TIP_Project.h"
#import "TIPImageFetchDownloadInternal.h"
@class TIPImageFetchDownloadInternalURLSessionDelegate;
@class NSURLSessionTaskMetrics;
NS_ASSUME_NONNULL_BEGIN
NSString * const TIPImageFetchDownloadConstructorExceptionName = @"TIPImageFetchDownloadConstructorException";
static NSURLSession *sTIPImageFetchDownloadInternalURLSession = nil;
static NSOperationQueue *sTIPImageFetchDownloadInternalOperationQueue = nil;
static TIPImageFetchDownloadInternalURLSessionDelegate *sTIPImageFetchDownloadInternalURLSessionDelegate = nil;
static float ConvertNSOperationQueuePriorityToNSURLSessionTaskPriority(NSOperationQueuePriority pri);
@interface TIPImageFetchDownloadInternalURLSessionDelegate : NSObject <NSURLSessionDataDelegate>
- (void)addDownload:(TIPImageFetchDownloadInternal *)download;
- (void)removeDownloadWithTask:(NSURLSessionDataTask *)task;
@end
@interface TIPImageFetchDownloadInternal ()
@property (nonatomic, nullable) id downloadMetrics;
@property (nonatomic, nullable, readonly) NSURLSessionDataTask *task;
@property (nonatomic, readonly) dispatch_queue_t contextQueue;
static void _PrepareGlobalState(void);
@end
@implementation TIPImageFetchDownloadProviderInternal
- (id<TIPImageFetchDownload>)imageFetchDownloadWithContext:(id<TIPImageFetchDownloadContext>)context
{
return [[TIPImageFetchDownloadInternal alloc] initWithContext:context];
}
@end
@implementation TIPImageFetchDownloadInternal
{
NSOperationQueuePriority _priority;
NSURLSession *_session;
BOOL _started;
BOOL _cancelled;
}
@synthesize context = _context;
- (instancetype)initWithContext:(id<TIPImageFetchDownloadContext>)context
{
if (self = [super init]) {
_context = context;
_contextQueue = context.downloadQueue;
}
return self;
}
- (void)dealloc
{
NSURLSessionDataTask *task = _task;
if (task) {
TIPImageFetchDownloadInternalURLSessionDelegate *delegate = (id)_session.delegate;
[_session.delegateQueue addOperationWithBlock:^{
if (task) {
[delegate removeDownloadWithTask:task];
}
}];
}
}
- (void)start
{
if (_started || _cancelled) {
return;
}
_started = YES;
_session = [self URLSession];
id<TIPImageFetchDownloadContext> context = self.context;
[context.client imageFetchDownloadDidStart:self];
[context.client imageFetchDownload:self hydrateRequest:context.originalRequest completion:^(NSError *hydrateError) {
if (!hydrateError) {
[context.client imageFetchDownload:self authorizeRequest:context.hydratedRequest completion:^(NSError * _Nullable authError) {
if (!authError) {
NSURLRequest *request = context.hydratedRequest;
if (context.authorization) {
request = [request mutableCopy];
[(NSMutableURLRequest *)request setValue:context.authorization forHTTPHeaderField:@"Authorization"];
}
self->_task = [self->_session dataTaskWithRequest:request];
[self->_session.delegateQueue addOperationWithBlock:^{
[(TIPImageFetchDownloadInternalURLSessionDelegate *)self->_session.delegate addDownload:self];
}];
[self->_task resume];
} else {
[context.client imageFetchDownload:self didCompleteWithError:authError];
}
}];
} else {
[context.client imageFetchDownload:self didCompleteWithError:hydrateError];
}
}];
}
- (void)cancelWithDescription:(NSString *)cancelDescription
{
if (_cancelled) {
return;
}
_cancelled = YES;
if (_task) {
[_task cancel];
} else if (_context) {
tip_dispatch_async_autoreleasing(self.contextQueue, ^{
[self.context.client imageFetchDownload:self
didCompleteWithError:[NSError errorWithDomain:NSURLErrorDomain
code:NSURLErrorCancelled
userInfo:nil]];
});
}
}
#pragma mark Properties
- (void)discardContext
{
_context = nil;
}
- (void)setPriority:(NSOperationQueuePriority)priority
{
_priority = priority;
_task.priority = ConvertNSOperationQueuePriorityToNSURLSessionTaskPriority(_priority);
}
- (NSOperationQueuePriority)priority
{
return _priority;
}
- (nullable NSURLRequest *)finalURLRequest
{
return _task.currentRequest;
}
#pragma mark Private
static void _PrepareGlobalState(void)
{
static dispatch_once_t onceToken;
dispatch_once(&onceToken, ^{
dispatch_queue_t queue = dispatch_queue_create("TIPImageFetchDownloadInternal.queue", DISPATCH_QUEUE_SERIAL);
sTIPImageFetchDownloadInternalOperationQueue = [[NSOperationQueue alloc] init];
sTIPImageFetchDownloadInternalOperationQueue.maxConcurrentOperationCount = 1;
sTIPImageFetchDownloadInternalOperationQueue.qualityOfService = NSQualityOfServiceUtility;
sTIPImageFetchDownloadInternalOperationQueue.underlyingQueue = queue;
sTIPImageFetchDownloadInternalURLSessionDelegate = [[TIPImageFetchDownloadInternalURLSessionDelegate alloc] init];
NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration];
config.HTTPCookieStorage = nil;
config.HTTPCookieAcceptPolicy = NSHTTPCookieAcceptPolicyNever;
config.HTTPShouldSetCookies = NO;
config.URLCache = nil;
config.timeoutIntervalForResource = 60 * 3;
sTIPImageFetchDownloadInternalURLSession = [NSURLSession sessionWithConfiguration:config
delegate:sTIPImageFetchDownloadInternalURLSessionDelegate
delegateQueue:sTIPImageFetchDownloadInternalOperationQueue];
});
}
- (NSURLSession *)URLSession
{
if (nil == (__bridge void *)sTIPImageFetchDownloadInternalURLSession) {
_PrepareGlobalState();
}
return sTIPImageFetchDownloadInternalURLSession;
}
@end
@implementation TIPImageFetchDownloadInternalURLSessionDelegate
{
NSMapTable<NSNumber *, TIPImageFetchDownloadInternal *> *_downloadContexts;
}
- (instancetype)init
{
if (self = [super init]) {
_downloadContexts = [NSMapTable strongToWeakObjectsMapTable];
}
return self;
}
- (void)addDownload:(TIPImageFetchDownloadInternal *)download
{
[_downloadContexts setObject:download forKey:@(download.task.taskIdentifier)];
}
- (void)removeDownloadWithTask:(NSURLSessionDataTask *)task
{
[_downloadContexts removeObjectForKey:@(task.taskIdentifier)];
}
#pragma mark Delegate
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveResponse:(NSURLResponse *)response
completionHandler:(void (^)(NSURLSessionResponseDisposition))completionHandler
{
TIPImageFetchDownloadInternal *download = [_downloadContexts objectForKey:@(dataTask.taskIdentifier)];
if (download) {
tip_dispatch_async_autoreleasing(download.contextQueue, ^{
[download.context.client imageFetchDownload:download
didReceiveURLResponse:(NSHTTPURLResponse *)response];
});
}
completionHandler(NSURLSessionResponseAllow);
}
- (void)URLSession:(NSURLSession *)session
dataTask:(NSURLSessionDataTask *)dataTask
didReceiveData:(NSData *)data
{
TIPImageFetchDownloadInternal *download = [_downloadContexts objectForKey:@(dataTask.taskIdentifier)];
if (download) {
tip_dispatch_async_autoreleasing(download.contextQueue, ^{
[download.context.client imageFetchDownload:download
didReceiveData:data];
});
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didCompleteWithError:(nullable NSError *)error
{
TIPImageFetchDownloadInternal *download = [_downloadContexts objectForKey:@(task.taskIdentifier)];
if (download) {
tip_dispatch_async_autoreleasing(download.contextQueue, ^{
[download.context.client imageFetchDownload:download
didCompleteWithError:error];
});
[_downloadContexts removeObjectForKey:@(task.taskIdentifier)];
}
}
- (void)URLSession:(NSURLSession *)session
task:(NSURLSessionTask *)task
didFinishCollectingMetrics:(NSURLSessionTaskMetrics *)metrics
{
TIPImageFetchDownloadInternal *download = [_downloadContexts objectForKey:@(task.taskIdentifier)];
if (download) {
tip_dispatch_async_autoreleasing(download.contextQueue, ^{
download.downloadMetrics = metrics;
});
}
}
@end
@implementation NSHTTPURLResponse (TIPStubbingSupport)
+ (instancetype)tip_responseWithRequestURL:(NSURL *)requestURL
dataLength:(NSUInteger)dataLength
responseMIMEType:(nullable NSString *)MIMEType
{
NSInteger statusCode = 404;
NSMutableDictionary *headerFields = [[NSMutableDictionary alloc] init];
if (dataLength > 0) {
statusCode = 200;
headerFields[@"Accept-Ranges"] = @"bytes";
headerFields[@"Last-Modified"] = @"Wed, 15 Nov 1995 04:58:08 GMT";
}
if (MIMEType) {
headerFields[@"Content-Type"] = MIMEType;
}
NSHTTPURLResponse *response = [[NSHTTPURLResponse alloc] initWithURL:requestURL statusCode:statusCode HTTPVersion:@"HTTP/1.1" headerFields:headerFields];
return response;
}
@end
static float ConvertNSOperationQueuePriorityToNSURLSessionTaskPriority(NSOperationQueuePriority pri)
{
NSInteger priShifted = pri + 9;
if (priShifted < 0) {
priShifted = 0;
}
float taskPri = priShifted / 18;
if (taskPri > 1.f) {
taskPri = 1.f;
}
return taskPri;
}
NS_ASSUME_NONNULL_END