/iOS/Pods/GTMSessionFetcher/Source/GTMSessionFetcher.m
https://bitbucket.org/bakke458/dvhndemoapp · Objective C · 4453 lines · 3331 code · 633 blank · 489 comment · 633 complexity · a6bdb28901cb9439b64982b72b991389 MD5 · raw file
Large files are truncated click here to view the full file
- /* Copyright 2014 Google Inc. All rights reserved.
- *
- * Licensed under the Apache License, Version 2.0 (the "License");
- * you may not use this file except in compliance with the License.
- * You may obtain a copy of the License at
- *
- * http://www.apache.org/licenses/LICENSE-2.0
- *
- * Unless required by applicable law or agreed to in writing, software
- * distributed under the License is distributed on an "AS IS" BASIS,
- * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
- * See the License for the specific language governing permissions and
- * limitations under the License.
- */
- #if !defined(__has_feature) || !__has_feature(objc_arc)
- #error "This file requires ARC support."
- #endif
- #import "GTMSessionFetcher.h"
- #import <sys/utsname.h>
- GTM_ASSUME_NONNULL_BEGIN
- NSString *const kGTMSessionFetcherStartedNotification = @"kGTMSessionFetcherStartedNotification";
- NSString *const kGTMSessionFetcherStoppedNotification = @"kGTMSessionFetcherStoppedNotification";
- NSString *const kGTMSessionFetcherRetryDelayStartedNotification = @"kGTMSessionFetcherRetryDelayStartedNotification";
- NSString *const kGTMSessionFetcherRetryDelayStoppedNotification = @"kGTMSessionFetcherRetryDelayStoppedNotification";
- NSString *const kGTMSessionFetcherCompletionInvokedNotification = @"kGTMSessionFetcherCompletionInvokedNotification";
- NSString *const kGTMSessionFetcherCompletionDataKey = @"data";
- NSString *const kGTMSessionFetcherCompletionErrorKey = @"error";
- NSString *const kGTMSessionFetcherErrorDomain = @"com.google.GTMSessionFetcher";
- NSString *const kGTMSessionFetcherStatusDomain = @"com.google.HTTPStatus";
- NSString *const kGTMSessionFetcherStatusDataKey = @"data"; // data returned with a kGTMSessionFetcherStatusDomain error
- NSString *const kGTMSessionFetcherNumberOfRetriesDoneKey = @"kGTMSessionFetcherNumberOfRetriesDoneKey";
- NSString *const kGTMSessionFetcherElapsedIntervalWithRetriesKey = @"kGTMSessionFetcherElapsedIntervalWithRetriesKey";
- static NSString *const kGTMSessionIdentifierPrefix = @"com.google.GTMSessionFetcher";
- static NSString *const kGTMSessionIdentifierDestinationFileURLMetadataKey = @"_destURL";
- static NSString *const kGTMSessionIdentifierBodyFileURLMetadataKey = @"_bodyURL";
- // The default max retry interview is 10 minutes for uploads (POST/PUT/PATCH),
- // 1 minute for downloads.
- static const NSTimeInterval kUnsetMaxRetryInterval = -1.0;
- static const NSTimeInterval kDefaultMaxDownloadRetryInterval = 60.0;
- static const NSTimeInterval kDefaultMaxUploadRetryInterval = 60.0 * 10.;
- #ifdef GTMSESSION_PERSISTED_DESTINATION_KEY
- // Projects using unique class names should also define a unique persisted destination key.
- static NSString * const kGTMSessionFetcherPersistedDestinationKey =
- GTMSESSION_PERSISTED_DESTINATION_KEY;
- #else
- static NSString * const kGTMSessionFetcherPersistedDestinationKey =
- @"com.google.GTMSessionFetcher.downloads";
- #endif
- GTM_ASSUME_NONNULL_END
- //
- // GTMSessionFetcher
- //
- #if 0
- #define GTM_LOG_BACKGROUND_SESSION(...) GTMSESSION_LOG_DEBUG(__VA_ARGS__)
- #else
- #define GTM_LOG_BACKGROUND_SESSION(...)
- #endif
- #ifndef GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
- #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_11) && MAC_OS_X_VERSION_MAX_ALLOWED >= MAC_OS_X_VERSION_10_11) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_9_0) && __IPHONE_OS_VERSION_MAX_ALLOWED >= __IPHONE_9_0)
- #define GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY 1
- #endif
- #endif
- @interface GTMSessionFetcher ()
- @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadedData;
- @property(atomic, strong, readwrite, GTM_NULLABLE) NSData *downloadResumeData;
- #if GTM_BACKGROUND_TASK_FETCHING
- @property(assign, atomic) UIBackgroundTaskIdentifier backgroundTaskIdentifier;
- #endif
- @property(atomic, readwrite, getter=isUsingBackgroundSession) BOOL usingBackgroundSession;
- @end
- #if !GTMSESSION_BUILD_COMBINED_SOURCES
- @interface GTMSessionFetcher (GTMSessionFetcherLoggingInternal)
- - (void)logFetchWithError:(NSError *)error;
- - (void)logNowWithError:(GTM_NULLABLE NSError *)error;
- - (NSInputStream *)loggedInputStreamForInputStream:(NSInputStream *)inputStream;
- - (GTMSessionFetcherBodyStreamProvider)loggedStreamProviderForStreamProvider:
- (GTMSessionFetcherBodyStreamProvider)streamProvider;
- @end
- #endif // !GTMSESSION_BUILD_COMBINED_SOURCES
- GTM_ASSUME_NONNULL_BEGIN
- static NSTimeInterval InitialMinRetryInterval(void) {
- return 1.0 + ((double)(arc4random_uniform(0x0FFFF)) / (double) 0x0FFFF);
- }
- static BOOL IsLocalhost(NSString * GTM_NULLABLE_TYPE host) {
- // We check if there's host, and then make the comparisons.
- if (host == nil) return NO;
- return ([host caseInsensitiveCompare:@"localhost"] == NSOrderedSame
- || [host isEqual:@"::1"]
- || [host isEqual:@"127.0.0.1"]);
- }
- static GTMSessionFetcherTestBlock GTM_NULLABLE_TYPE gGlobalTestBlock;
- @implementation GTMSessionFetcher {
- NSMutableURLRequest *_request; // after beginFetch, changed only in delegate callbacks
- BOOL _useUploadTask; // immutable after beginFetch
- NSURL *_bodyFileURL; // immutable after beginFetch
- GTMSessionFetcherBodyStreamProvider _bodyStreamProvider; // immutable after beginFetch
- NSURLSession *_session;
- BOOL _shouldInvalidateSession; // immutable after beginFetch
- NSURLSession *_sessionNeedingInvalidation;
- NSURLSessionConfiguration *_configuration;
- NSURLSessionTask *_sessionTask;
- NSString *_taskDescription;
- float _taskPriority;
- NSURLResponse *_response;
- NSString *_sessionIdentifier;
- BOOL _wasCreatedFromBackgroundSession;
- BOOL _didCreateSessionIdentifier;
- NSString *_sessionIdentifierUUID;
- BOOL _userRequestedBackgroundSession;
- BOOL _usingBackgroundSession;
- NSMutableData * GTM_NULLABLE_TYPE _downloadedData;
- NSError *_downloadFinishedError;
- NSData *_downloadResumeData; // immutable after construction
- NSURL *_destinationFileURL;
- int64_t _downloadedLength;
- NSURLCredential *_credential; // username & password
- NSURLCredential *_proxyCredential; // credential supplied to proxy servers
- BOOL _isStopNotificationNeeded; // set when start notification has been sent
- BOOL _isUsingTestBlock; // set when a test block was provided (remains set when the block is released)
- id _userData; // retained, if set by caller
- NSMutableDictionary *_properties; // more data retained for caller
- dispatch_queue_t _callbackQueue;
- dispatch_group_t _callbackGroup; // read-only after creation
- NSOperationQueue *_delegateQueue; // immutable after beginFetch
- id<GTMFetcherAuthorizationProtocol> _authorizer; // immutable after beginFetch
- // The service object that created and monitors this fetcher, if any.
- id<GTMSessionFetcherServiceProtocol> _service; // immutable; set by the fetcher service upon creation
- NSString *_serviceHost;
- NSInteger _servicePriority; // immutable after beginFetch
- BOOL _hasStoppedFetching; // counterpart to _initialBeginFetchDate
- BOOL _userStoppedFetching;
- BOOL _isRetryEnabled; // user wants auto-retry
- NSTimer *_retryTimer;
- NSUInteger _retryCount;
- NSTimeInterval _maxRetryInterval; // default 60 (download) or 600 (upload) seconds
- NSTimeInterval _minRetryInterval; // random between 1 and 2 seconds
- NSTimeInterval _retryFactor; // default interval multiplier is 2
- NSTimeInterval _lastRetryInterval;
- NSDate *_initialBeginFetchDate; // date that beginFetch was first invoked; immutable after initial beginFetch
- NSDate *_initialRequestDate; // date of first request to the target server (ignoring auth)
- BOOL _hasAttemptedAuthRefresh; // accessed only in shouldRetryNowForStatus:
- NSString *_comment; // comment for log
- NSString *_log;
- #if !STRIP_GTM_FETCH_LOGGING
- NSMutableData *_loggedStreamData;
- NSURL *_redirectedFromURL;
- NSString *_logRequestBody;
- NSString *_logResponseBody;
- BOOL _hasLoggedError;
- BOOL _deferResponseBodyLogging;
- #endif
- }
- #if !GTMSESSION_UNIT_TESTING
- + (void)load {
- [self fetchersForBackgroundSessions];
- }
- #endif
- + (instancetype)fetcherWithRequest:(GTM_NULLABLE NSURLRequest *)request {
- return [[self alloc] initWithRequest:request configuration:nil];
- }
- + (instancetype)fetcherWithURL:(NSURL *)requestURL {
- return [self fetcherWithRequest:[NSURLRequest requestWithURL:requestURL]];
- }
- + (instancetype)fetcherWithURLString:(NSString *)requestURLString {
- return [self fetcherWithURL:(NSURL *)[NSURL URLWithString:requestURLString]];
- }
- + (instancetype)fetcherWithDownloadResumeData:(NSData *)resumeData {
- GTMSessionFetcher *fetcher = [self fetcherWithRequest:nil];
- fetcher.comment = @"Resuming download";
- fetcher.downloadResumeData = resumeData;
- return fetcher;
- }
- + (GTM_NULLABLE instancetype)fetcherWithSessionIdentifier:(NSString *)sessionIdentifier {
- GTMSESSION_ASSERT_DEBUG(sessionIdentifier != nil, @"Invalid session identifier");
- NSMapTable *sessionIdentifierToFetcherMap = [self sessionIdentifierToFetcherMap];
- GTMSessionFetcher *fetcher = [sessionIdentifierToFetcherMap objectForKey:sessionIdentifier];
- if (!fetcher && [sessionIdentifier hasPrefix:kGTMSessionIdentifierPrefix]) {
- fetcher = [self fetcherWithRequest:nil];
- [fetcher setSessionIdentifier:sessionIdentifier];
- [sessionIdentifierToFetcherMap setObject:fetcher forKey:sessionIdentifier];
- fetcher->_wasCreatedFromBackgroundSession = YES;
- [fetcher setCommentWithFormat:@"Resuming %@",
- fetcher && fetcher->_sessionIdentifierUUID ? fetcher->_sessionIdentifierUUID : @"?"];
- }
- return fetcher;
- }
- + (NSMapTable *)sessionIdentifierToFetcherMap {
- // TODO: What if a service is involved in creating the fetcher? Currently, when re-creating
- // fetchers, if a service was involved, it is not re-created. Should the service maintain a map?
- static NSMapTable *gSessionIdentifierToFetcherMap = nil;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- gSessionIdentifierToFetcherMap = [NSMapTable strongToWeakObjectsMapTable];
- });
- return gSessionIdentifierToFetcherMap;
- }
- #if !GTM_ALLOW_INSECURE_REQUESTS
- + (BOOL)appAllowsInsecureRequests {
- // If the main bundle Info.plist key NSAppTransportSecurity is present, and it specifies
- // NSAllowsArbitraryLoads, then we need to explicitly enforce secure schemes.
- #if GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
- static BOOL allowsInsecureRequests;
- static dispatch_once_t onceToken;
- dispatch_once(&onceToken, ^{
- NSBundle *mainBundle = [NSBundle mainBundle];
- NSDictionary *appTransportSecurity =
- [mainBundle objectForInfoDictionaryKey:@"NSAppTransportSecurity"];
- allowsInsecureRequests =
- [[appTransportSecurity objectForKey:@"NSAllowsArbitraryLoads"] boolValue];
- });
- return allowsInsecureRequests;
- #else
- // For builds targeting iOS 8 or 10.10 and earlier, we want to require fetcher
- // security checks.
- return YES;
- #endif // GTM_TARGET_SUPPORTS_APP_TRANSPORT_SECURITY
- }
- #else // GTM_ALLOW_INSECURE_REQUESTS
- + (BOOL)appAllowsInsecureRequests {
- return YES;
- }
- #endif // !GTM_ALLOW_INSECURE_REQUESTS
- - (instancetype)init {
- return [self initWithRequest:nil configuration:nil];
- }
- - (instancetype)initWithRequest:(NSURLRequest *)request {
- return [self initWithRequest:request configuration:nil];
- }
- - (instancetype)initWithRequest:(GTM_NULLABLE NSURLRequest *)request
- configuration:(GTM_NULLABLE NSURLSessionConfiguration *)configuration {
- self = [super init];
- if (self) {
- if (![NSURLSession class]) {
- Class oldFetcherClass = NSClassFromString(@"GTMHTTPFetcher");
- if (oldFetcherClass && request) {
- self = [[oldFetcherClass alloc] initWithRequest:(NSURLRequest *)request];
- } else {
- self = nil;
- }
- return self;
- }
- #if GTM_BACKGROUND_TASK_FETCHING
- _backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- #endif
- _request = [request mutableCopy];
- _configuration = configuration;
- NSData *bodyData = request.HTTPBody;
- if (bodyData) {
- _bodyLength = (int64_t)bodyData.length;
- } else {
- _bodyLength = NSURLSessionTransferSizeUnknown;
- }
- _callbackQueue = dispatch_get_main_queue();
- _callbackGroup = dispatch_group_create();
- _delegateQueue = [NSOperationQueue mainQueue];
- _minRetryInterval = InitialMinRetryInterval();
- _maxRetryInterval = kUnsetMaxRetryInterval;
- _taskPriority = -1.0f; // Valid values if set are 0.0...1.0.
- #if !STRIP_GTM_FETCH_LOGGING
- // Encourage developers to set the comment property or use
- // setCommentWithFormat: by providing a default string.
- _comment = @"(No fetcher comment set)";
- #endif
- }
- return self;
- }
- - (id)copyWithZone:(NSZone *)zone {
- // disallow use of fetchers in a copy property
- [self doesNotRecognizeSelector:_cmd];
- return nil;
- }
- - (NSString *)description {
- NSString *requestStr = self.request.URL.description;
- if (requestStr.length == 0) {
- if (self.downloadResumeData.length > 0) {
- requestStr = @"<download resume data>";
- } else if (_wasCreatedFromBackgroundSession) {
- requestStr = @"<from bg session>";
- } else {
- requestStr = @"<no request>";
- }
- }
- return [NSString stringWithFormat:@"%@ %p (%@)", [self class], self, requestStr];
- }
- - (void)dealloc {
- GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded,
- @"unbalanced fetcher notification for %@", _request.URL);
- [self forgetSessionIdentifierForFetcherWithoutSyncCheck];
- // Note: if a session task or a retry timer was pending, then this instance
- // would be retained by those so it wouldn't be getting dealloc'd,
- // hence we don't need to stopFetch here
- }
- #pragma mark -
- // Begin fetching the URL (or begin a retry fetch). The delegate is retained
- // for the duration of the fetch connection.
- - (void)beginFetchWithCompletionHandler:(GTM_NULLABLE GTMSessionFetcherCompletionHandler)handler {
- GTMSessionCheckNotSynchronized(self);
- _completionHandler = [handler copy];
- // The user may have called setDelegate: earlier if they want to use other
- // delegate-style callbacks during the fetch; otherwise, the delegate is nil,
- // which is fine.
- [self beginFetchMayDelay:YES mayAuthorize:YES];
- }
- - (GTMSessionFetcherCompletionHandler)completionHandlerWithTarget:(GTM_NULLABLE_TYPE id)target
- didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
- GTMSessionFetcherAssertValidSelector(target, finishedSelector, @encode(GTMSessionFetcher *),
- @encode(NSData *), @encode(NSError *), 0);
- GTMSessionFetcherCompletionHandler completionHandler = ^(NSData *data, NSError *error) {
- if (target && finishedSelector) {
- id selfArg = self; // Placate ARC.
- NSMethodSignature *sig = [target methodSignatureForSelector:finishedSelector];
- NSInvocation *invocation = [NSInvocation invocationWithMethodSignature:sig];
- [invocation setSelector:(SEL)finishedSelector];
- [invocation setTarget:target];
- [invocation setArgument:&selfArg atIndex:2];
- [invocation setArgument:&data atIndex:3];
- [invocation setArgument:&error atIndex:4];
- [invocation invoke];
- }
- };
- return completionHandler;
- }
- - (void)beginFetchWithDelegate:(GTM_NULLABLE_TYPE id)target
- didFinishSelector:(GTM_NULLABLE_TYPE SEL)finishedSelector {
- GTMSessionCheckNotSynchronized(self);
- GTMSessionFetcherCompletionHandler handler = [self completionHandlerWithTarget:target
- didFinishSelector:finishedSelector];
- [self beginFetchWithCompletionHandler:handler];
- }
- - (void)beginFetchMayDelay:(BOOL)mayDelay
- mayAuthorize:(BOOL)mayAuthorize {
- // This is the internal entry point for re-starting fetches.
- GTMSessionCheckNotSynchronized(self);
- NSMutableURLRequest *fetchRequest = _request; // The request property is now externally immutable.
- NSURL *fetchRequestURL = fetchRequest.URL;
- NSString *priorSessionIdentifier = self.sessionIdentifier;
- // A utility block for creating error objects when we fail to start the fetch.
- NSError *(^beginFailureError)(NSInteger) = ^(NSInteger code){
- NSString *urlString = fetchRequestURL.absoluteString;
- NSDictionary *userInfo = @{
- NSURLErrorFailingURLStringErrorKey : (urlString ? urlString : @"(missing URL)")
- };
- return [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
- code:code
- userInfo:userInfo];
- };
- // Catch delegate queue maxConcurrentOperationCount values other than 1, particularly
- // NSOperationQueueDefaultMaxConcurrentOperationCount (-1), to avoid the additional complexity
- // of simultaneous or out-of-order delegate callbacks.
- GTMSESSION_ASSERT_DEBUG(_delegateQueue.maxConcurrentOperationCount == 1,
- @"delegate queue %@ should support one concurrent operation, not %zd",
- _delegateQueue.name, _delegateQueue.maxConcurrentOperationCount);
- if (!_initialBeginFetchDate) {
- // This ivar is set only here on the initial beginFetch so need not be synchronized.
- _initialBeginFetchDate = [[NSDate alloc] init];
- }
- if (self.sessionTask != nil) {
- // If cached fetcher returned through fetcherWithSessionIdentifier:, then it's
- // already begun, but don't consider this a failure, since the user need not know this.
- if (self.sessionIdentifier != nil) {
- return;
- }
- GTMSESSION_ASSERT_DEBUG(NO, @"Fetch object %@ being reused; this should never happen", self);
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
- return;
- }
- if (fetchRequestURL == nil && !_downloadResumeData && !priorSessionIdentifier) {
- GTMSESSION_ASSERT_DEBUG(NO, @"Beginning a fetch requires a request with a URL");
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorDownloadFailed)];
- return;
- }
- // We'll respect the user's request for a background session (unless this is
- // an upload fetcher, which does its initial request foreground.)
- self.usingBackgroundSession = self.useBackgroundSession && [self canFetchWithBackgroundSession];
- NSURL *bodyFileURL = self.bodyFileURL;
- if (bodyFileURL) {
- NSError *fileCheckError;
- if (![bodyFileURL checkResourceIsReachableAndReturnError:&fileCheckError]) {
- // This assert fires when the file being uploaded no longer exists once
- // the fetcher is ready to start the upload.
- GTMSESSION_ASSERT_DEBUG_OR_LOG(0, @"Body file is unreachable: %@\n %@",
- bodyFileURL.path, fileCheckError);
- [self failToBeginFetchWithError:fileCheckError];
- return;
- }
- }
- NSString *requestScheme = fetchRequestURL.scheme;
- BOOL isDataRequest = [requestScheme isEqual:@"data"];
- if (isDataRequest) {
- // NSURLSession does not support data URLs in background sessions.
- #if DEBUG
- if (priorSessionIdentifier || self.sessionIdentifier) {
- GTMSESSION_LOG_DEBUG(@"Converting background to foreground session for %@",
- fetchRequest);
- }
- #endif
- [self setSessionIdentifierInternal:nil];
- self.useBackgroundSession = NO;
- }
- #if GTM_ALLOW_INSECURE_REQUESTS
- BOOL shouldCheckSecurity = NO;
- #else
- BOOL shouldCheckSecurity = (fetchRequestURL != nil
- && !isDataRequest
- && [[self class] appAllowsInsecureRequests]);
- #endif
- if (shouldCheckSecurity) {
- // Allow https only for requests, unless overridden by the client.
- //
- // Non-https requests may too easily be snooped, so we disallow them by default.
- //
- // file: and data: schemes are usually safe if they are hardcoded in the client or provided
- // by a trusted source, but since it's fairly rare to need them, it's safest to make clients
- // explicitly whitelist them.
- BOOL isSecure =
- requestScheme != nil && [requestScheme caseInsensitiveCompare:@"https"] == NSOrderedSame;
- if (!isSecure) {
- BOOL allowRequest = NO;
- NSString *host = fetchRequestURL.host;
- // Check schemes first. A file scheme request may be allowed here, or as a localhost request.
- for (NSString *allowedScheme in _allowedInsecureSchemes) {
- if (requestScheme != nil &&
- [requestScheme caseInsensitiveCompare:allowedScheme] == NSOrderedSame) {
- allowRequest = YES;
- break;
- }
- }
- if (!allowRequest) {
- // Check for localhost requests. Security checks only occur for non-https requests, so
- // this check won't happen for an https request to localhost.
- BOOL isLocalhostRequest = (host.length == 0 && [fetchRequestURL isFileURL]) || IsLocalhost(host);
- if (isLocalhostRequest) {
- if (self.allowLocalhostRequest) {
- allowRequest = YES;
- } else {
- GTMSESSION_ASSERT_DEBUG(NO, @"Fetch request for localhost but fetcher"
- @" allowLocalhostRequest is not set: %@", fetchRequestURL);
- }
- } else {
- GTMSESSION_ASSERT_DEBUG(NO, @"Insecure fetch request has a scheme (%@)"
- @" not found in fetcher allowedInsecureSchemes (%@): %@",
- requestScheme, _allowedInsecureSchemes ?: @" @[] ", fetchRequestURL);
- }
- }
- if (!allowRequest) {
- #if !DEBUG
- NSLog(@"Insecure fetch disallowed for %@", fetchRequestURL.description ?: @"nil request URL");
- #endif
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorInsecureRequest)];
- return;
- }
- } // !isSecure
- } // (requestURL != nil) && !isDataRequest
- if (self.cookieStorage == nil) {
- self.cookieStorage = [[self class] staticCookieStorage];
- }
- BOOL isRecreatingSession = (self.sessionIdentifier != nil) && (fetchRequest == nil);
- self.canShareSession = !isRecreatingSession && !self.usingBackgroundSession;
- if (!self.session && self.canShareSession) {
- self.session = [_service sessionForFetcherCreation];
- // If _session is nil, then the service's session creation semaphore will block
- // until this fetcher invokes fetcherDidCreateSession: below, so this *must* invoke
- // that method, even if the session fails to be created.
- }
- if (!self.session) {
- // Create a session.
- if (!_configuration) {
- if (priorSessionIdentifier || self.usingBackgroundSession) {
- NSString *sessionIdentifier = priorSessionIdentifier;
- if (!sessionIdentifier) {
- sessionIdentifier = [self createSessionIdentifierWithMetadata:nil];
- }
- NSMapTable *sessionIdentifierToFetcherMap = [[self class] sessionIdentifierToFetcherMap];
- [sessionIdentifierToFetcherMap setObject:self forKey:self.sessionIdentifier];
- #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
- // iOS 8/10.10 builds require the new backgroundSessionConfiguration method name.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
- #elif (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED < MAC_OS_X_VERSION_10_10) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED < __IPHONE_8_0)
- // Do a runtime check to avoid a deprecation warning about using
- // +backgroundSessionConfiguration: on iOS 8.
- if ([NSURLSessionConfiguration respondsToSelector:@selector(backgroundSessionConfigurationWithIdentifier:)]) {
- // Running on iOS 8+/OS X 10.10+.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfigurationWithIdentifier:sessionIdentifier];
- } else {
- // Running on iOS 7/OS X 10.9.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
- }
- #else
- // Building with an SDK earlier than iOS 8/OS X 10.10.
- _configuration =
- [NSURLSessionConfiguration backgroundSessionConfiguration:sessionIdentifier];
- #endif
- self.usingBackgroundSession = YES;
- self.canShareSession = NO;
- } else {
- _configuration = [NSURLSessionConfiguration ephemeralSessionConfiguration];
- }
- #if !GTM_ALLOW_INSECURE_REQUESTS
- _configuration.TLSMinimumSupportedProtocol = kTLSProtocol12;
- #endif
- } // !_configuration
- _configuration.HTTPCookieStorage = self.cookieStorage;
- if (_configurationBlock) {
- _configurationBlock(self, _configuration);
- }
- id<NSURLSessionDelegate> delegate = [_service sessionDelegate];
- if (!delegate || !self.canShareSession) {
- delegate = self;
- }
- self.session = [NSURLSession sessionWithConfiguration:_configuration
- delegate:delegate
- delegateQueue:self.sessionDelegateQueue];
- GTMSESSION_ASSERT_DEBUG(self.session, @"Couldn't create session");
- // Tell the service about the session created by this fetcher. This also signals the
- // service's semaphore to allow other fetchers to request this session.
- [_service fetcherDidCreateSession:self];
- // If this assertion fires, the client probably tried to use a session identifier that was
- // already used. The solution is to make the client use a unique identifier (or better yet let
- // the session fetcher assign the identifier).
- GTMSESSION_ASSERT_DEBUG(self.session.delegate == delegate, @"Couldn't assign delegate.");
- if (self.session) {
- BOOL isUsingSharedDelegate = (delegate != self);
- if (!isUsingSharedDelegate) {
- _shouldInvalidateSession = YES;
- }
- }
- }
- if (isRecreatingSession) {
- _shouldInvalidateSession = YES;
- // Let's make sure there are tasks still running or if not that we get a callback from a
- // completed one; otherwise, we assume the tasks failed.
- // This is the observed behavior perhaps 25% of the time within the Simulator running 7.0.3 on
- // exiting the app after starting an upload and relaunching the app if we manage to relaunch
- // after the task has completed, but before the system relaunches us in the background.
- [self.session getTasksWithCompletionHandler:^(NSArray *dataTasks, NSArray *uploadTasks,
- NSArray *downloadTasks) {
- if (dataTasks.count == 0 && uploadTasks.count == 0 && downloadTasks.count == 0) {
- double const kDelayInSeconds = 1.0; // We should get progress indication or completion soon
- dispatch_time_t checkForFeedbackDelay =
- dispatch_time(DISPATCH_TIME_NOW, (int64_t)(kDelayInSeconds * NSEC_PER_SEC));
- dispatch_after(checkForFeedbackDelay, dispatch_get_main_queue(), ^{
- if (!self.sessionTask && !fetchRequest) {
- // If our task and/or request haven't been restored, then we assume task feedback lost.
- [self removePersistedBackgroundSessionFromDefaults];
- NSError *sessionError =
- [NSError errorWithDomain:kGTMSessionFetcherErrorDomain
- code:GTMSessionFetcherErrorBackgroundFetchFailed
- userInfo:nil];
- [self failToBeginFetchWithError:sessionError];
- }
- });
- }
- }];
- return;
- }
- self.downloadedData = nil;
- self.downloadedLength = 0;
- if (_servicePriority == NSIntegerMin) {
- mayDelay = NO;
- }
- if (mayDelay && _service) {
- BOOL shouldFetchNow = [_service fetcherShouldBeginFetching:self];
- if (!shouldFetchNow) {
- // The fetch is deferred, but will happen later.
- //
- // If this session is held by the fetcher service, clear the session now so that we don't
- // assume it's still valid after the fetcher is restarted.
- if (self.canShareSession) {
- self.session = nil;
- }
- return;
- }
- }
- NSString *effectiveHTTPMethod = [fetchRequest valueForHTTPHeaderField:@"X-HTTP-Method-Override"];
- if (effectiveHTTPMethod == nil) {
- effectiveHTTPMethod = fetchRequest.HTTPMethod;
- }
- BOOL isEffectiveHTTPGet = (effectiveHTTPMethod == nil
- || [effectiveHTTPMethod isEqual:@"GET"]);
- BOOL needsUploadTask = (self.useUploadTask || self.bodyFileURL || self.bodyStreamProvider);
- if (_bodyData || self.bodyStreamProvider || fetchRequest.HTTPBodyStream) {
- if (isEffectiveHTTPGet) {
- fetchRequest.HTTPMethod = @"POST";
- isEffectiveHTTPGet = NO;
- }
- if (_bodyData) {
- if (!needsUploadTask) {
- fetchRequest.HTTPBody = _bodyData;
- }
- #if !STRIP_GTM_FETCH_LOGGING
- } else if (fetchRequest.HTTPBodyStream) {
- if ([self respondsToSelector:@selector(loggedInputStreamForInputStream:)]) {
- fetchRequest.HTTPBodyStream =
- [self performSelector:@selector(loggedInputStreamForInputStream:)
- withObject:fetchRequest.HTTPBodyStream];
- }
- #endif
- }
- }
- // We authorize after setting up the http method and body in the request
- // because OAuth 1 may need to sign the request body
- if (mayAuthorize && _authorizer && !isDataRequest) {
- BOOL isAuthorized = [_authorizer isAuthorizedRequest:fetchRequest];
- if (!isAuthorized) {
- // Authorization needed.
- //
- // If this session is held by the fetcher service, clear the session now so that we don't
- // assume it's still valid after authorization completes.
- if (self.canShareSession) {
- self.session = nil;
- }
- // Authorizing the request will recursively call this beginFetch:mayDelay:
- // or failToBeginFetchWithError:.
- [self authorizeRequest];
- return;
- }
- }
- // set the default upload or download retry interval, if necessary
- if ([self isRetryEnabled] && self.maxRetryInterval <= 0) {
- if (isEffectiveHTTPGet || [effectiveHTTPMethod isEqual:@"HEAD"]) {
- [self setMaxRetryInterval:kDefaultMaxDownloadRetryInterval];
- } else {
- [self setMaxRetryInterval:kDefaultMaxUploadRetryInterval];
- }
- }
- // finally, start the connection
- NSURLSessionTask *newSessionTask;
- BOOL needsDataAccumulator = NO;
- if (_downloadResumeData) {
- newSessionTask = [_session downloadTaskWithResumeData:_downloadResumeData];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
- @"Failed downloadTaskWithResumeData for %@, resume data %tu bytes",
- _session, _downloadResumeData.length);
- } else if (_destinationFileURL && !isDataRequest) {
- newSessionTask = [_session downloadTaskWithRequest:fetchRequest];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed downloadTaskWithRequest for %@, %@",
- _session, fetchRequest);
- } else if (needsUploadTask) {
- if (bodyFileURL) {
- newSessionTask = [_session uploadTaskWithRequest:fetchRequest
- fromFile:bodyFileURL];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
- @"Failed uploadTaskWithRequest for %@, %@, file %@",
- _session, fetchRequest, bodyFileURL.path);
- } else if (self.bodyStreamProvider) {
- newSessionTask = [_session uploadTaskWithStreamedRequest:fetchRequest];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
- @"Failed uploadTaskWithStreamedRequest for %@, %@",
- _session, fetchRequest);
- } else {
- GTMSESSION_ASSERT_DEBUG_OR_LOG(_bodyData != nil,
- @"Upload task needs body data, %@", fetchRequest);
- newSessionTask = [_session uploadTaskWithRequest:fetchRequest
- fromData:(NSData * GTM_NONNULL_TYPE)_bodyData];
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask,
- @"Failed uploadTaskWithRequest for %@, %@, body data %tu bytes",
- _session, fetchRequest, _bodyData.length);
- }
- needsDataAccumulator = YES;
- } else {
- newSessionTask = [_session dataTaskWithRequest:fetchRequest];
- needsDataAccumulator = YES;
- GTMSESSION_ASSERT_DEBUG_OR_LOG(newSessionTask, @"Failed dataTaskWithRequest for %@, %@",
- _session, fetchRequest);
- }
- self.sessionTask = newSessionTask;
- if (!newSessionTask) {
- // We shouldn't get here; if we're here, an earlier assertion should have fired to explain
- // which session task creation failed.
- [self failToBeginFetchWithError:beginFailureError(GTMSessionFetcherErrorTaskCreationFailed)];
- return;
- }
- if (needsDataAccumulator && _accumulateDataBlock == nil) {
- self.downloadedData = [NSMutableData data];
- }
- if (_taskDescription) {
- newSessionTask.taskDescription = _taskDescription;
- }
- if (_taskPriority >= 0) {
- #if (!TARGET_OS_IPHONE && defined(MAC_OS_X_VERSION_10_10) && MAC_OS_X_VERSION_MIN_REQUIRED >= MAC_OS_X_VERSION_10_10) \
- || (TARGET_OS_IPHONE && defined(__IPHONE_8_0) && __IPHONE_OS_VERSION_MIN_REQUIRED >= __IPHONE_8_0)
- BOOL hasTaskPriority = YES;
- #else
- BOOL hasTaskPriority = [newSessionTask respondsToSelector:@selector(setPriority:)];
- #endif
- if (hasTaskPriority) {
- newSessionTask.priority = _taskPriority;
- }
- }
- #if GTM_DISABLE_FETCHER_TEST_BLOCK
- GTMSESSION_ASSERT_DEBUG(_testBlock == nil && gGlobalTestBlock == nil, @"test blocks disabled");
- _testBlock = nil;
- #else
- if (!_testBlock) {
- if (gGlobalTestBlock) {
- // Note that the test block may pass nil for all of its response parameters,
- // indicating that the fetch should actually proceed. This is useful when the
- // global test block has been set, and the app is only testing a specific
- // fetcher. The block simulation code will then resume the task.
- _testBlock = gGlobalTestBlock;
- }
- }
- _isUsingTestBlock = (_testBlock != nil);
- #endif // GTM_DISABLE_FETCHER_TEST_BLOCK
- #if GTM_BACKGROUND_TASK_FETCHING
- // Background tasks seem to interfere with out-of-process uploads and downloads.
- if (!self.skipBackgroundTask && !self.useBackgroundSession) {
- // Tell UIApplication that we want to continue even when the app is in the
- // background.
- id<GTMUIApplicationProtocol> app = [[self class] fetcherUIApplication];
- #if DEBUG
- NSString *bgTaskName = [NSString stringWithFormat:@"%@-%@",
- [self class], fetchRequest.URL.host];
- #else
- NSString *bgTaskName = @"GTMSessionFetcher";
- #endif
- __block UIBackgroundTaskIdentifier bgTaskID = [app beginBackgroundTaskWithName:bgTaskName
- expirationHandler:^{
- // Background task expiration callback - this block is always invoked by
- // UIApplication on the main thread.
- if (bgTaskID != UIBackgroundTaskInvalid) {
- if (bgTaskID == self.backgroundTaskIdentifier) {
- self.backgroundTaskIdentifier = UIBackgroundTaskInvalid;
- }
- [app endBackgroundTask:bgTaskID];
- }
- }];
- self.backgroundTaskIdentifier = bgTaskID;
- }
- #endif
- if (!_initialRequestDate) {
- _initialRequestDate = [[NSDate alloc] init];
- }
- // We don't expect to reach here even on retry or auth until a stop notification has been sent
- // for the previous task, but we should ensure that we don't unbalance that.
- GTMSESSION_ASSERT_DEBUG(!_isStopNotificationNeeded, @"Start notification without a prior stop");
- [self sendStopNotificationIfNeeded];
- [self addPersistedBackgroundSessionToDefaults];
- [self setStopNotificationNeeded:YES];
- [self postNotificationOnMainThreadWithName:kGTMSessionFetcherStartedNotification
- userInfo:nil
- requireAsync:NO];
- // The service needs to know our task if it is serving as NSURLSession delegate.
- [_service fetcherDidBeginFetching:self];
- if (_testBlock) {
- #if !GTM_DISABLE_FETCHER_TEST_BLOCK
- [self simulateFetchForTestBlock];
- #endif
- } else {
- // We resume the session task after posting the notification since the
- // delegate callbacks may happen immediately if the fetch is started off
- // the main thread or the session delegate queue is on a background thread,
- // and we don't want to post a start notification after a premature finish
- // of the session task.
- [newSessionTask resume];
- }
- }
- NSData * GTM_NULLABLE_TYPE GTMDataFromInputStream(NSInputStream *inputStream, NSError **outError) {
- NSMutableData *data = [NSMutableData data];
- [inputStream open];
- NSInteger numberOfBytesRead = 0;
- while ([inputStream hasBytesAvailable]) {
- uint8_t buffer[512];
- numberOfBytesRead = [inputStream read:buffer maxLength:sizeof(buffer)];
- if (numberOfBytesRead > 0) {
- [data appendBytes:buffer length:(NSUInteger)numberOfBytesRead];
- } else {
- break;
- }
- }
- [inputStream close];
- NSError *streamError = inputStream.streamError;
- if (streamError) {
- data = nil;
- }
- if (outError) {
- *outError = streamError;
- }
- return data;
- }
- #if !GTM_DISABLE_FETCHER_TEST_BLOCK
- - (void)simulateFetchForTestBlock {
- // This is invoked on the same thread as the beginFetch method was.
- //
- // Callbacks will all occur on the callback queue.
- _testBlock(self, ^(NSURLResponse *response, NSData *responseData, NSError *error) {
- // Callback from test block.
- if (response == nil && responseData == nil && error == nil) {
- // Assume the fetcher should execute rather than be tested.
- _testBlock = nil;
- _isUsingTestBlock = NO;
- [_sessionTask resume];
- return;
- }
- GTMSessionFetcherBodyStreamProvider bodyStreamProvider = self.bodyStreamProvider;
- if (bodyStreamProvider) {
- bodyStreamProvider(^(NSInputStream *bodyStream){
- // Read from the input stream into an NSData buffer. We'll drain the stream
- // explicitly on a background queue.
- [self invokeOnCallbackQueue:dispatch_get_global_queue(DISPATCH_QUEUE_PRIORITY_HIGH, 0)
- afterUserStopped:NO
- block:^{
- NSError *streamError;
- NSData *streamedData = GTMDataFromInputStream(bodyStream, &streamError);
- dispatch_async(dispatch_get_main_queue(), ^{
- // Continue callbacks on the main thread, since serial behavior
- // is more reliable for tests.
- [self simulateDataCallbacksForTestBlockWithBodyData:streamedData
- response:response
- responseData:responseData
- error:(error ?: streamError)];
- });
- }];
- });
- } else {
- // No input stream; use the supplied data or file URL.
- NSURL *bodyFileURL = self.bodyFileURL;
- if (bodyFileURL) {
- NSError *readError;
- _bodyData = [NSData dataWithContentsOfURL:bodyFileURL
- options:NSDataReadingMappedIfSafe
- error:&readError];
- error = readError;
- }
- // No stream provider.
- // In real fetches, nothing happens until the run loop spins, so apps have leeway to
- // set callbacks after they call beginFetch. We'll mirror that fetcher behavior by
- // delaying callbacks here at least to the next spin of the run loop. That keeps
- // immediate, synchronous setting of callback blocks after beginFetch working in tests.
- dispatch_async(dispatch_get_main_queue(), ^{
- [self simulateDataCallbacksForTestBlockWithBodyData:_bodyData
- response:response
- responseData:responseData
- error:error];
- });
- }
- });
- }
- - (void)simulateByteTransferReportWithDataLength:(int64_t)totalDataLength
- block:(GTMSessionFetcherSendProgressBlock)block {
- // This utility method simulates transfer progress with up to three callbacks.
- // It is used to call back to any of the progress blocks.
- int64_t sendReportSize = totalDataLength / 3 + 1;
- int64_t totalSent = 0;
- while (totalSent < totalDataLength) {
- int64_t bytesRemaining = totalDataLength - totalSent;
- sendReportSize = MIN(sendReportSize, bytesRemaining);
- totalSent += sendReportSize;
- [self invokeOnCallbackQueueUnlessStopped:^{
- block(sendReportSize, totalSent, totalDataLength);
- }];
- }
- }
- - (void)simulateDataCallbacksForTestBlockWithBodyData:(NSData * GTM_NULLABLE_TYPE)bodyData
- response:(NSURLResponse *)response
- responseData:(NSData *)suppliedData
- error:(NSError *)suppliedError {
- __block NSData *responseData = suppliedData;
- __block NSError *responseError = suppliedError;
- // This method does the test simulation of callbacks once the upload
- // and download data are known.
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- // Get copies of ivars we'll access in async invocations. This simulation assumes
- // they won't change during fetcher execution.
- NSURL *destinationFileURL = _destinationFileURL;
- GTMSessionFetcherWillRedirectBlock willRedirectBlock = _willRedirectBlock;
- GTMSessionFetcherDidReceiveResponseBlock didReceiveResponseBlock = _didReceiveResponseBlock;
- GTMSessionFetcherSendProgressBlock sendProgressBlock = _sendProgressBlock;
- GTMSessionFetcherDownloadProgressBlock downloadProgressBlock = _downloadProgressBlock;
- GTMSessionFetcherAccumulateDataBlock accumulateDataBlock = _accumulateDataBlock;
- GTMSessionFetcherReceivedProgressBlock receivedProgressBlock = _receivedProgressBlock;
- GTMSessionFetcherWillCacheURLResponseBlock willCacheURLResponseBlock =
- _willCacheURLResponseBlock;
- // Simulate receipt of redirection.
- if (willRedirectBlock) {
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- willRedirectBlock((NSHTTPURLResponse *)response, _request,
- ^(NSURLRequest *redirectRequest) {
- // For simulation, we'll assume the app will just continue.
- });
- }];
- }
- // If the fetcher has a challenge block, simulate a challenge.
- //
- // It might be nice to eventually let the user determine which testBlock
- // fetches get challenged rather than always executing the supplied
- // challenge block.
- if (_challengeBlock) {
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- if (_challengeBlock) {
- NSURL *requestURL = _request.URL;
- NSString *host = requestURL.host;
- NSURLProtectionSpace *pspace =
- [[NSURLProtectionSpace alloc] initWithHost:host
- port:requestURL.port.integerValue
- protocol:requestURL.scheme
- realm:nil
- authenticationMethod:NSURLAuthenticationMethodHTTPBasic];
- id<NSURLAuthenticationChallengeSender> unusedSender =
- (id<NSURLAuthenticationChallengeSender>)[NSNull null];
- NSURLAuthenticationChallenge *challenge =
- [[NSURLAuthenticationChallenge alloc] initWithProtectionSpace:pspace
- proposedCredential:nil
- previousFailureCount:0
- failureResponse:nil
- error:nil
- sender:unusedSender];
- _challengeBlock(self, challenge, ^(NSURLSessionAuthChallengeDisposition disposition,
- NSURLCredential * GTM_NULLABLE_TYPE credential){
- // We could change the responseData and responseError based on the disposition,
- // but it's easier for apps to just supply the expected data and error
- // directly to the test block. So this simulation ignores the disposition.
- });
- }
- }];
- }
- // Simulate receipt of an initial response.
- if (didReceiveResponseBlock) {
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- didReceiveResponseBlock(response, ^(NSURLSessionResponseDisposition desiredDisposition) {
- // For simulation, we'll assume the disposition is to continue.
- });
- }];
- }
- // Simulate reporting send progress.
- if (sendProgressBlock) {
- [self simulateByteTransferReportWithDataLength:(int64_t)bodyData.length
- block:^(int64_t bytesSent,
- int64_t totalBytesSent,
- int64_t totalBytesExpectedToSend) {
- // This is invoked on the callback queue unless stopped.
- sendProgressBlock(bytesSent, totalBytesSent, totalBytesExpectedToSend);
- }];
- }
- if (destinationFileURL) {
- // Simulate download to file progress.
- if (downloadProgressBlock) {
- [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
- block:^(int64_t bytesDownloaded,
- int64_t totalBytesDownloaded,
- int64_t totalBytesExpectedToDownload) {
- // This is invoked on the callback queue unless stopped.
- downloadProgressBlock(bytesDownloaded, totalBytesDownloaded,
- totalBytesExpectedToDownload);
- }];
- }
- NSError *writeError;
- [responseData writeToURL:destinationFileURL
- options:NSDataWritingAtomic
- error:&writeError];
- if (writeError) {
- // Tell the test code that writing failed.
- responseError = writeError;
- }
- } else {
- // Simulate download to NSData progress.
- if (accumulateDataBlock) {
- if (responseData) {
- [self invokeOnCallbackQueueUnlessStopped:^{
- accumulateDataBlock(responseData);
- }];
- }
- } else {
- _downloadedData = [responseData mutableCopy];
- }
- if (receivedProgressBlock) {
- [self simulateByteTransferReportWithDataLength:(int64_t)responseData.length
- block:^(int64_t bytesReceived,
- int64_t totalBytesReceived,
- int64_t totalBytesExpectedToReceive) {
- // This is invoked on the callback queue unless stopped.
- receivedProgressBlock(bytesReceived, totalBytesReceived);
- }];
- }
- if (willCacheURLResponseBlock) {
- // Simulate letting the client inspect and alter the cached response.
- NSData *cachedData = responseData ?: [[NSData alloc] init]; // Always have non-nil data.
- NSCachedURLResponse *cachedResponse =
- [[NSCachedURLResponse alloc] initWithResponse:response
- data:cachedData];
- [self invokeOnCallbackUnsynchronizedQueueAfterUserStopped:YES
- block:^{
- willCacheURLResponseBlock(cachedResponse, ^(NSCachedURLResponse *responseToCache){
- // The app may provide an alternative response, or nil to defeat caching.
- });
- }];
- }
- }
- _response = response;
- } // @synchronized(self)
- NSOperationQueue *queue = self.sessionDelegateQueue;
- [queue addOperationWithBlock:^{
- // Rather than invoke failToBeginFetchWithError: we want to simulate completion of
- // a connection that started and ended, so we'll call down to finishWithError:
- NSInteger status = responseError ? responseError.code : 200;
- if (status >= 200 && status <= 399) {
- [self finishWithError:nil shouldRetry:NO];
- } else {
- [self shouldRetryNowForStatus:status
- error:responseError
- forceAssumeRetry:NO
- response:^(BOOL shouldRetry) {
- [self finishWithError:responseError shouldRetry:shouldRetry];
- }];
- }
- }];
- }
- #endif // !GTM_DISABLE_FETCHER_TEST_BLOCK
- - (void)setSessionTask:(NSURLSessionTask *)sessionTask {
- @synchronized(self) {
- GTMSessionMonitorSynchronized(self);
- if (_sessionTask != sessionTask) {
- _sessionTask = sessionTask;
- if (_sessionTask) {
- // Request could be nil on restoring this fetcher from a background session.
- if (!_request) {
- _request = [_sessionTask.originalRequest mutableCopy];
- }
- }
- }
- } // @synchronized(self)
- }
- - (NSURLSessionTask * GTM_NULLABLE_TYPE)sessionTask {
- @synchronized(self) {
- GTMSessionMonitorSynchronize…