Source/TNLCommunicationAgent.m (994 lines of code) (raw):

// // TNLCommunicationAgent.m // TwitterNetworkLayer // // Created on 5/2/16. // Copyright © 2020 Twitter. All rights reserved. // #include <TargetConditionals.h> #if !TARGET_OS_WATCH // no communication agent for watchOS #import <Network/Network.h> #import <SystemConfiguration/SystemConfiguration.h> #import "NSDictionary+TNLAdditions.h" #import "NSURLSessionConfiguration+TNLAdditions.h" #import "TNL_Project.h" #import "TNLCommunicationAgent_Project.h" #import "TNLHTTP.h" #import "TNLPseudoURLProtocol.h" #define FORCE_LOG_REACHABILITY_CHANGE 0 static const NSTimeInterval kCaptivePortalQuietTime = 60.0; static NSString * const kCaptivePortalCheckEndpoint = @"http://connectivitycheck.gstatic.com/generate_204"; static void _ReachabilityCallback(__unused SCNetworkReachabilityRef target, const SCNetworkReachabilityFlags flags, void* info); static TNLNetworkReachabilityStatus _NetworkReachabilityStatusFromFlags(TNLNetworkReachabilityFlags flags) __attribute__((const)); static TNLNetworkReachabilityFlags _NetworkReachabilityFlagsFromPath(nw_path_t path); static BOOL _HasCellularInterface(void); #define _NWPathStatusToFlag(status) ((status > 0) ? ((uint32_t)1 << (uint32_t)((status) - 1)) : 0) #define _NWInterfaceTypeToFlag(itype) ((uint32_t)1 << (uint32_t)8 << (uint32_t)(itype)) #pragma clang diagnostic push #pragma clang diagnostic ignored "-Wunguarded-availability" TNLStaticAssert(_NWPathStatusToFlag(nw_path_status_satisfied) == TNLNetworkReachabilityMaskPathStatusSatisfied, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWPathStatusToFlag(nw_path_status_unsatisfied) == TNLNetworkReachabilityMaskPathStatusUnsatisfied, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWPathStatusToFlag(nw_path_status_satisfiable) == TNLNetworkReachabilityMaskPathStatusSatisfiable, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWInterfaceTypeToFlag(nw_interface_type_other) == TNLNetworkReachabilityMaskPathIntefaceTypeOther, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWInterfaceTypeToFlag(nw_interface_type_wifi) == TNLNetworkReachabilityMaskPathIntefaceTypeWifi, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWInterfaceTypeToFlag(nw_interface_type_cellular) == TNLNetworkReachabilityMaskPathIntefaceTypeCellular, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWInterfaceTypeToFlag(nw_interface_type_wired) == TNLNetworkReachabilityMaskPathIntefaceTypeWired, MISSMATCH_REACHABILITY_FLAGS); TNLStaticAssert(_NWInterfaceTypeToFlag(nw_interface_type_loopback) == TNLNetworkReachabilityMaskPathIntefaceTypeLoopback, MISSMATCH_REACHABILITY_FLAGS); #pragma clang diagnostic pop TNL_OBJC_FINAL TNL_OBJC_DIRECT_MEMBERS @interface TNLCommunicationAgentWeakWrapper : NSObject @property (nonatomic, weak) TNLCommunicationAgent *communicationAgent; @end @interface TNLCommunicationAgent () @property (atomic) TNLNetworkReachabilityStatus currentReachabilityStatus; @property (atomic) TNLNetworkReachabilityFlags currentReachabilityFlags; @property (atomic, copy, nullable) NSString *currentWWANRadioAccessTechnology; @property (atomic) TNLCaptivePortalStatus currentCaptivePortalStatus; @property (atomic, nullable) id<TNLCarrierInfo> currentCarrierInfo; @end TNL_OBJC_DIRECT_MEMBERS @interface TNLCommunicationAgent (Agent) - (void)_agent_initialize; - (void)_agent_initializeLegacyReachability; - (void)_agent_initializeModernReachability; - (void)_agent_initializeTelephony; - (void)_agent_updateModernReachabilityWithNetworkPath:(nonnull nw_path_t)path; - (void)_agent_forciblyUpdateLegacyReachability; - (void)_agent_updateReachabilityFlags:(TNLNetworkReachabilityFlags)newFlags status:(TNLNetworkReachabilityStatus)newStatus; - (void)_agent_addObserver:(id<TNLCommunicationAgentObserver>)observer; - (void)_agent_removeObserver:(id<TNLCommunicationAgentObserver>)observer; - (void)_agent_identifyReachability:(TNLCommunicationAgentIdentifyReachabilityCallback)callback; - (void)_agent_identifyCarrierInfo:(TNLCommunicationAgentIdentifyCarrierInfoCallback)callback; - (void)_agent_identifyWWANRadioAccessTechnology:(TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback)callback; - (void)_agent_identifyCaptivePortalStatus:(TNLCommunicationAgentIdentifyCaptivePortalStatusCallback)callback; - (void)_agent_startCaptivePortalCheckTimerWithDelay:(NSTimeInterval)delay; - (void)_agent_triggerCaptivePortalCheck; - (void)_agent_triggerCaptivePortalCheckIfNeeded; - (void)_agent_handleCaptivePortalResponse:(nullable NSHTTPURLResponse *)response data:(nullable NSData *)data dataTask:(nullable NSURLSessionDataTask *)dataTask error:(nullable NSError *)error; @end @interface TNLCommunicationAgent (Private) #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (void)_updateCarrier:(CTCarrier *)carrier TNL_OBJC_DIRECT; #endif - (void)private_updateRadioAccessTechnology:(NSNotification *)note; @end @implementation TNLCommunicationAgent { NSMutableArray<id<TNLCommunicationAgentObserver>> *_queuedObservers; NSMutableArray<TNLCommunicationAgentIdentifyReachabilityCallback> *_queuedReachabilityCallbacks; NSMutableArray<TNLCommunicationAgentIdentifyCarrierInfoCallback> *_queuedCarrierInfoCallbacks; NSMutableArray<TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback> *_queuedRadioTechInfoCallbacks; NSMutableArray<TNLCommunicationAgentIdentifyCaptivePortalStatusCallback> *_queuedCaptivePortalCallbacks; NSMutableArray<TNLCommunicationAgentIdentifyCaptivePortalStatusCallback> *_captivePortalCheckCallbacks; NSHashTable<id<TNLCommunicationAgentObserver>> *_observers; dispatch_queue_t _agentQueue; NSOperationQueue *_agentOperationQueue; TNLCommunicationAgentWeakWrapper *_agentWrapper; SCNetworkReachabilityRef _legacyReachabilityRef; nw_path_monitor_t _modernReachabilityNetworkPathMonitor; // supports ARC #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST CTTelephonyNetworkInfo *_internalTelephonyNetworkInfo; #endif NSURLSessionConfiguration *_captivePortalSessionConfiguration; NSURLSessionDataTask *_captivePortalTask; NSDate *_lastCaptivePortalCheck; struct { BOOL initialized:1; BOOL initializedReachability:1; BOOL initializedCarrier:1; BOOL initializedRadioTech:1; } _flags; } + (BOOL)hasCellularInterface { static BOOL sHasCellular = NO; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ sHasCellular = _HasCellularInterface(); }); return sHasCellular; } - (instancetype)initWithInternetReachabilityHost:(NSString *)host { TNLAssert(host != nil); if (!host) { return nil; } if (self = [super init]) { _host = [host copy]; _observers = [NSHashTable weakObjectsHashTable]; _queuedObservers = [[NSMutableArray alloc] init]; _queuedReachabilityCallbacks = [[NSMutableArray alloc] init]; _queuedCarrierInfoCallbacks = [[NSMutableArray alloc] init]; _queuedRadioTechInfoCallbacks = [[NSMutableArray alloc] init]; _queuedCaptivePortalCallbacks = [[NSMutableArray alloc] init]; _captivePortalCheckCallbacks = [[NSMutableArray alloc] init]; _agentQueue = dispatch_queue_create("TNLCommunicationAgent.queue", DISPATCH_QUEUE_SERIAL); _agentOperationQueue = [[NSOperationQueue alloc] init]; _agentOperationQueue.name = @"TNLCommunicationAgent.queue"; _agentOperationQueue.maxConcurrentOperationCount = 1; _agentOperationQueue.underlyingQueue = _agentQueue; _agentOperationQueue.qualityOfService = NSQualityOfServiceUtility; _agentWrapper = [[TNLCommunicationAgentWeakWrapper alloc] init]; _agentWrapper.communicationAgent = self; tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_initialize]; }); } return self; } - (void)dealloc { if (_legacyReachabilityRef) { SCNetworkReachabilitySetCallback(_legacyReachabilityRef, NULL, NULL); SCNetworkReachabilitySetDispatchQueue(_legacyReachabilityRef, NULL); CFRelease(_legacyReachabilityRef); } if (tnl_available_ios_12) { if (_modernReachabilityNetworkPathMonitor) { nw_path_monitor_cancel(_modernReachabilityNetworkPathMonitor); } } #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST [[NSNotificationCenter defaultCenter] removeObserver:self name:CTRadioAccessTechnologyDidChangeNotification object:nil]; #endif // Give the SCNetworkReachability callbacks time to flush. // // Since the weak wrapper is used for the context of the reachability function callback it means // there needs to be a strong reference when that callback is executed. // We clear the callback above, but due to async behavior, the weak wrapper reference could // still be lingering to a callback. // Thus, we'll ensure that the weak wrapper instance is strongly held beyond the lifetime of the // dealloc so that it survives longer than any callbacks that are triggered. // Assigning communicationAgent to nil is really an arbitrary method call in order to keep the // strong reference around, and is effectively a no-op. dispatch_queue_t agentQueue = _agentQueue; TNLCommunicationAgentWeakWrapper *weakWrapper = _agentWrapper; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(5.0 * NSEC_PER_SEC)), agentQueue, ^{ tnl_dispatch_async_autoreleasing(agentQueue, ^{ weakWrapper.communicationAgent = nil; }); }); } - (void)addObserver:(id<TNLCommunicationAgentObserver>)observer { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_addObserver:observer]; }); } - (void)removeObserver:(id<TNLCommunicationAgentObserver>)observer { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_removeObserver:observer]; }); } - (void)identifyReachability:(TNLCommunicationAgentIdentifyReachabilityCallback)callback { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_identifyReachability:callback]; }); } - (void)identifyCarrierInfo:(TNLCommunicationAgentIdentifyCarrierInfoCallback)callback { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_identifyCarrierInfo:callback]; }); } - (void)identifyWWANRadioAccessTechnology:(TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback)callback { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_identifyWWANRadioAccessTechnology:callback]; }); } - (void)identifyCaptivePortalStatus:(TNLCommunicationAgentIdentifyCaptivePortalStatusCallback)callback { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ [self _agent_identifyCaptivePortalStatus:callback]; }); } @end TNL_OBJC_DIRECT_MEMBERS @implementation TNLCommunicationAgent (Agent) #pragma mark Legacy Reachability - (void)_agent_forciblyUpdateLegacyReachability { SCNetworkReachabilityFlags flags; if (SCNetworkReachabilityGetFlags(_legacyReachabilityRef, &flags)) { self.currentReachabilityFlags = flags; self.currentReachabilityStatus = _NetworkReachabilityStatusFromFlags(flags); } else { self.currentReachabilityFlags = 0; self.currentReachabilityStatus = TNLNetworkReachabilityUndetermined; } } - (void)_agent_initializeLegacyReachability { _legacyReachabilityRef = SCNetworkReachabilityCreateWithName(kCFAllocatorDefault, self.host.UTF8String); [self _agent_forciblyUpdateLegacyReachability]; SCNetworkReachabilityContext context = { 0, (__bridge void*)_agentWrapper, NULL, NULL, NULL }; if (SCNetworkReachabilitySetCallback(_legacyReachabilityRef, _ReachabilityCallback, &context)) { if (SCNetworkReachabilitySetDispatchQueue(_legacyReachabilityRef, _agentQueue)) { _flags.initializedReachability = 1; } else { SCNetworkReachabilitySetCallback(_legacyReachabilityRef, NULL, NULL); CFRelease(_legacyReachabilityRef); _legacyReachabilityRef = NULL; } } if (!_flags.initializedReachability) { TNLLogError(@"Failed to start reachability: %@", self.host); if (_legacyReachabilityRef) { CFRelease(_legacyReachabilityRef); _legacyReachabilityRef = NULL; } } } #pragma mark Modern Reachability - (void)_agent_updateModernReachabilityWithNetworkPath:(nonnull nw_path_t)path { if (tnl_available_ios_12) { #if DEBUG TNLLogDebug(@"network path monitor update: %@", path.description); #endif const TNLNetworkReachabilityFlags newFlags = _NetworkReachabilityFlagsFromPath(path); const TNLNetworkReachabilityStatus newStatus = _NetworkReachabilityStatusFromFlags(newFlags); [self _agent_updateReachabilityFlags:newFlags status:newStatus]; } } - (void)_agent_initializeModernReachability { if (tnl_available_ios_12) { __weak typeof(self) weakSelf = self; _modernReachabilityNetworkPathMonitor = nw_path_monitor_create(); nw_path_monitor_set_queue(_modernReachabilityNetworkPathMonitor, _agentQueue); // nw_path_monitor_set_cancel_handler // don't need a cancel handler nw_path_monitor_set_update_handler(_modernReachabilityNetworkPathMonitor, ^(nw_path_t __nonnull path) { [weakSelf _agent_updateModernReachabilityWithNetworkPath:path]; }); nw_path_monitor_start(_modernReachabilityNetworkPathMonitor); // will trigger an update callback (but async) _flags.initializedReachability = 1; } } #pragma mark Telephony - (void)_agent_initializeTelephony { #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST __weak typeof(self) weakSelf = self; _internalTelephonyNetworkInfo = [[CTTelephonyNetworkInfo alloc] init]; _internalTelephonyNetworkInfo.subscriberCellularProviderDidUpdateNotifier = ^(CTCarrier *carrier) { [weakSelf _updateCarrier:carrier]; }; [[NSNotificationCenter defaultCenter] addObserver:self selector:@selector(private_updateRadioAccessTechnology:) name:CTRadioAccessTechnologyDidChangeNotification object:nil]; self.currentCarrierInfo = [TNLCarrierInfoInternal carrierWithCarrier:_internalTelephonyNetworkInfo.subscriberCellularProvider]; self.currentWWANRadioAccessTechnology = [_internalTelephonyNetworkInfo.currentRadioAccessTechnology copy]; #endif // #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST _flags.initializedCarrier = 1; _flags.initializedRadioTech = 1; } #pragma mark Captive Portal - (void)_agent_initializeCaptivePortalStatus { self.currentCaptivePortalStatus = TNLCaptivePortalStatusUndetermined; NSURLSessionConfiguration *config = [NSURLSessionConfiguration defaultSessionConfiguration]; config.timeoutIntervalForRequest = 30; config.timeoutIntervalForResource = 30; config.URLCache = nil; config.URLCredentialStorage = nil; config.HTTPCookieStorage = nil; config.TLSMinimumSupportedProtocol = 0; config.TLSMaximumSupportedProtocol = 0; config.allowsCellularAccess = YES; config.requestCachePolicy = NSURLRequestReloadIgnoringLocalCacheData; config.HTTPShouldSetCookies = NO; config.HTTPMaximumConnectionsPerHost = 1; [config tnl_insertProtocolClasses:@[[TNLPseudoURLProtocol class]]]; _captivePortalSessionConfiguration = [config copy]; TNLCommunicationAgentIdentifyCaptivePortalStatusCallback callback = ^(TNLCaptivePortalStatus status) { // nothing }; [_queuedCaptivePortalCallbacks addObject:[callback copy]]; } #pragma mark Private Methods - (void)_agent_initialize { TNLAssert(!_flags.initialized); TNLAssert(!_flags.initializedReachability); TNLAssert(!_flags.initializedCarrier); TNLAssert(!_flags.initializedRadioTech); TNLAssert(!_legacyReachabilityRef); TNLAssert(!_modernReachabilityNetworkPathMonitor); if (tnl_available_ios_12) { [self _agent_initializeModernReachability]; } else { [self _agent_initializeLegacyReachability]; } [self _agent_initializeTelephony]; [self _agent_initializeCaptivePortalStatus]; NSArray<id<TNLCommunicationAgentObserver>> *queuedObservers = [_queuedObservers copy]; NSArray<TNLCommunicationAgentIdentifyReachabilityCallback> *reachBlocks = [_queuedReachabilityCallbacks copy]; NSArray<TNLCommunicationAgentIdentifyCarrierInfoCallback> *carrierBlocks = [_queuedCarrierInfoCallbacks copy]; NSArray<TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback> *radioBlocks = [_queuedRadioTechInfoCallbacks copy]; NSArray<TNLCommunicationAgentIdentifyCaptivePortalStatusCallback> *captivePortalBlocks = [_queuedCaptivePortalCallbacks copy]; _queuedObservers = nil; _queuedReachabilityCallbacks = nil; _queuedCarrierInfoCallbacks = nil; _queuedRadioTechInfoCallbacks = nil; _queuedCaptivePortalCallbacks = nil; _flags.initialized = 1; for (id<TNLCommunicationAgentObserver> observer in queuedObservers) { [self _agent_addObserver:observer]; } for (TNLCommunicationAgentIdentifyReachabilityCallback block in reachBlocks) { [self _agent_identifyReachability:block]; } for (TNLCommunicationAgentIdentifyCarrierInfoCallback block in carrierBlocks) { [self _agent_identifyCarrierInfo:block]; } for (TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback block in radioBlocks) { [self _agent_identifyWWANRadioAccessTechnology:block]; } for (TNLCommunicationAgentIdentifyCaptivePortalStatusCallback block in captivePortalBlocks) { [self _agent_identifyCaptivePortalStatus:block]; } } - (void)_agent_addObserver:(id<TNLCommunicationAgentObserver>)observer { if (!_flags.initialized) { [_queuedObservers addObject:observer]; return; } [_observers addObject:observer]; static SEL legacySelector = nil; static SEL modernSelector = nil; if (!legacySelector || !modernSelector) { legacySelector = NSSelectorFromString(@"tnl_communicationAgent:didRegisterObserverWithInitialReachabilityFlags:status:carrierInfo:WWANRadioAccessTechnology:"); modernSelector = @selector(tnl_communicationAgent:didRegisterObserverWithInitialReachabilityFlags:status:carrierInfo:WWANRadioAccessTechnology:captivePortalStatus:); // TODO: once TNL moves to version 3.0, remove this legacy selector safety check } if ([observer respondsToSelector:modernSelector]) { TNLNetworkReachabilityFlags flags = self.currentReachabilityFlags; TNLNetworkReachabilityStatus status = self.currentReachabilityStatus; id<TNLCarrierInfo> info = self.currentCarrierInfo; NSString *radioTech = self.currentWWANRadioAccessTechnology; TNLCaptivePortalStatus portalStatus = self.currentCaptivePortalStatus; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ [observer tnl_communicationAgent:self didRegisterObserverWithInitialReachabilityFlags:flags status:status carrierInfo:info WWANRadioAccessTechnology:radioTech captivePortalStatus:portalStatus]; }); } else if ([observer respondsToSelector:legacySelector]) { TNLLogError(@"Method signature of TNLCommunicationAgentObserver callback has changed! Please update from `%@` to `%@`", NSStringFromSelector(legacySelector), NSStringFromSelector(modernSelector)); TNLAssertMessage(NO, @"Method signature of TNLCommunicationAgentObserver callback has changed! Please update from `%@` to `%@`", NSStringFromSelector(legacySelector), NSStringFromSelector(modernSelector)); } } - (void)_agent_removeObserver:(id<TNLCommunicationAgentObserver>)observer { if (!_flags.initialized) { [_queuedObservers removeObject:observer]; return; } [_observers removeObject:observer]; } - (void)_agent_identifyReachability:(TNLCommunicationAgentIdentifyReachabilityCallback)callback { if (!_flags.initialized) { [_queuedReachabilityCallbacks addObject:callback]; return; } TNLNetworkReachabilityFlags flags = self.currentReachabilityFlags; TNLNetworkReachabilityStatus status = self.currentReachabilityStatus; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ callback(flags, status); }); } - (void)_agent_identifyCarrierInfo:(TNLCommunicationAgentIdentifyCarrierInfoCallback)callback { if (!_flags.initialized) { [_queuedCarrierInfoCallbacks addObject:callback]; return; } id<TNLCarrierInfo> info = self.currentCarrierInfo; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ callback(info); }); } - (void)_agent_identifyWWANRadioAccessTechnology:(TNLCommunicationAgentIdentifyWWANRadioAccessTechnologyCallback)callback { if (!_flags.initialized) { [_queuedRadioTechInfoCallbacks addObject:callback]; return; } NSString *radioTech = self.currentWWANRadioAccessTechnology; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ callback(radioTech); }); } - (void)_agent_identifyCaptivePortalStatus:(TNLCommunicationAgentIdentifyCaptivePortalStatusCallback)callback { if (!_flags.initialized) { [_queuedCaptivePortalCallbacks addObject:callback]; return; } const TNLCaptivePortalStatus status = self.currentCaptivePortalStatus; if (status != TNLCaptivePortalStatusUndetermined) { callback(status); return; } [_captivePortalCheckCallbacks addObject:callback]; [self _agent_triggerCaptivePortalCheckIfNeeded]; } - (void)_agent_startCaptivePortalCheckTimerWithDelay:(NSTimeInterval)delay { __weak typeof(self) weakSelf = self; dispatch_after(dispatch_time(DISPATCH_TIME_NOW, (int64_t)(delay * NSEC_PER_SEC)), _agentQueue, ^{ @autoreleasepool { [weakSelf _agent_triggerCaptivePortalCheckIfNeeded]; } }); } - (void)_agent_triggerCaptivePortalCheck { _lastCaptivePortalCheck = nil; // clear to force the check [self _agent_triggerCaptivePortalCheckIfNeeded]; } - (void)_agent_triggerCaptivePortalCheckIfNeeded { if (_captivePortalTask) { // already running return; } if (_lastCaptivePortalCheck) { const NSTimeInterval delay = kCaptivePortalQuietTime - [[NSDate date] timeIntervalSinceDate:_lastCaptivePortalCheck]; if (delay > 0.0) { // ran recently [self _agent_startCaptivePortalCheckTimerWithDelay:delay]; return; } } // create a new session every time we check the captive portal state to avoid reusing connections NSURLSession *session = [NSURLSession sessionWithConfiguration:_captivePortalSessionConfiguration delegate:nil delegateQueue:_agentOperationQueue]; __weak typeof(self) weakSelf = self; __block NSURLSessionDataTask *dataTask = nil; dataTask = [session dataTaskWithURL:[NSURL URLWithString:kCaptivePortalCheckEndpoint] completionHandler:^(NSData * _Nullable data, NSURLResponse * _Nullable response, NSError * _Nullable error) { [weakSelf _agent_handleCaptivePortalResponse:(NSHTTPURLResponse *)response data:data dataTask:dataTask error:error]; }]; _captivePortalTask = dataTask; [dataTask resume]; } - (void)_agent_handleCaptivePortalResponse:(nullable NSHTTPURLResponse *)response data:(nullable NSData *)data dataTask:(nullable NSURLSessionDataTask *)dataTask error:(nullable NSError *)error { if (dataTask != _captivePortalTask) { return; } _captivePortalTask = nil; _lastCaptivePortalCheck = [NSDate date]; [self _agent_startCaptivePortalCheckTimerWithDelay:kCaptivePortalQuietTime]; TNLCaptivePortalStatus status = TNLCaptivePortalStatusNoCaptivePortal; if ([error.domain isEqualToString:NSURLErrorDomain] && error.code == NSURLErrorAppTransportSecurityRequiresSecureConnection) { status = TNLCaptivePortalStatusDetectionBlockedByAppTransportSecurity; } else if (response) { const BOOL captive = (response.statusCode != TNLHTTPStatusCodeNoContent) || (data.length > 0) || ([[response.allHeaderFields tnl_objectForCaseInsensitiveKey:@"content-length"] integerValue] > 0); if (captive) { status = TNLCaptivePortalStatusCaptivePortalDetected; } } const TNLCaptivePortalStatus oldStatus = self.currentCaptivePortalStatus; if (oldStatus == status) { return; } self.currentCaptivePortalStatus = status; NSArray<TNLCommunicationAgentIdentifyCaptivePortalStatusCallback> *callbacks = [_captivePortalCheckCallbacks copy]; [_captivePortalCheckCallbacks removeAllObjects]; NSArray<id<TNLCommunicationAgentObserver>> *observers = _observers.allObjects; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ for (TNLCommunicationAgentIdentifyCaptivePortalStatusCallback callback in callbacks) { callback(status); } for (id<TNLCommunicationAgentObserver> observer in observers) { if ([observer respondsToSelector:@selector(tnl_communicationAgent:didUpdateCaptivePortalStatusFromPreviousStatus:toCurrentStatus:)]) { [observer tnl_communicationAgent:self didUpdateCaptivePortalStatusFromPreviousStatus:oldStatus toCurrentStatus:status]; } } }); } - (void)_agent_updateReachabilityFlags:(TNLNetworkReachabilityFlags)newFlags status:(TNLNetworkReachabilityStatus)newStatus { const TNLNetworkReachabilityFlags oldFlags = self.currentReachabilityFlags; const TNLNetworkReachabilityStatus oldStatus = self.currentReachabilityStatus; if (oldFlags == newFlags && oldStatus == newStatus) { return; } self.currentReachabilityStatus = newStatus; self.currentReachabilityFlags = newFlags; #if FORCE_LOG_REACHABILITY_CHANGE NSLog(@"reachability change: %@", TNLDebugStringFromNetworkReachabilityFlags(newFlags)); #endif [self _agent_triggerCaptivePortalCheck]; NSArray<id<TNLCommunicationAgentObserver>> *observers = _observers.allObjects; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ for (id<TNLCommunicationAgentObserver> observer in observers) { if ([observer respondsToSelector:@selector(tnl_communicationAgent:didUpdateReachabilityFromPreviousFlags:previousStatus:toCurrentFlags:currentStatus:)]) { [observer tnl_communicationAgent:self didUpdateReachabilityFromPreviousFlags:oldFlags previousStatus:oldStatus toCurrentFlags:newFlags currentStatus:newStatus]; } } }); } @end @implementation TNLCommunicationAgent (Private) #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (void)_updateCarrier:(CTCarrier *)carrier { tnl_dispatch_async_autoreleasing(_agentQueue, ^{ TNLCarrierInfoInternal *newInfo = [TNLCarrierInfoInternal carrierWithCarrier:carrier]; TNLCarrierInfoInternal *oldInfo = self.currentCarrierInfo; self.currentCarrierInfo = newInfo; NSArray<id<TNLCommunicationAgentObserver>> *observers = self->_observers.allObjects; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ for (id<TNLCommunicationAgentObserver> observer in observers) { if ([observer respondsToSelector:@selector(tnl_communicationAgent:didUpdateCarrierFromPreviousInfo:toCurrentInfo:)]) { [observer tnl_communicationAgent:self didUpdateCarrierFromPreviousInfo:oldInfo toCurrentInfo:newInfo]; } } }); }); } #endif // #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST - (void)private_updateRadioAccessTechnology:(NSNotification *)note { NSString *newTech = note.object; tnl_dispatch_async_autoreleasing(_agentQueue, ^{ NSString *oldTech = self.currentWWANRadioAccessTechnology; if (oldTech == newTech || ([oldTech isEqualToString:newTech])) { return; } self.currentWWANRadioAccessTechnology = newTech; [self _agent_triggerCaptivePortalCheck]; NSArray<id<TNLCommunicationAgentObserver>> *observers = self->_observers.allObjects; tnl_dispatch_async_autoreleasing(dispatch_get_main_queue(), ^{ for (id<TNLCommunicationAgentObserver> observer in observers) { if ([observer respondsToSelector:@selector(tnl_communicationAgent:didUpdateWWANRadioAccessTechnologyFromPreviousTech:toCurrentTech:)]) { [observer tnl_communicationAgent:self didUpdateWWANRadioAccessTechnologyFromPreviousTech:oldTech toCurrentTech:newTech]; } } }); }); } @end #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST @implementation TNLCarrierInfoInternal @synthesize carrierName = _carrierName; @synthesize mobileCountryCode = _mobileCountryCode; @synthesize mobileNetworkCode = _mobileNetworkCode; @synthesize isoCountryCode = _isoCountryCode; @synthesize allowsVOIP = _allowsVOIP; + (instancetype)carrierWithCarrier:(id<TNLCarrierInfo>)carrier { if (!carrier) { return nil; } return [[TNLCarrierInfoInternal alloc] initWithCarrier:carrier]; } - (instancetype)initWithCarrier:(id<TNLCarrierInfo>)carrier { return [self initWithCarrierName:carrier.carrierName mobileCountryCode:carrier.mobileCountryCode mobileNetworkCode:carrier.mobileNetworkCode isoCountryCode:carrier.isoCountryCode allowsVOIP:carrier.allowsVOIP]; } - (instancetype)initWithCarrierName:(NSString *)carrierName mobileCountryCode:(NSString *)mobileCountryCode mobileNetworkCode:(NSString *)mobileNetworkCode isoCountryCode:(NSString *)isoCountryCode allowsVOIP:(BOOL)allowsVOIP { if (self = [super init]) { _carrierName = [carrierName copy]; _mobileCountryCode = [mobileCountryCode copy]; _mobileNetworkCode = [mobileNetworkCode copy]; _isoCountryCode = [isoCountryCode copy]; _allowsVOIP = allowsVOIP; } return self; } - (NSString *)description { NSMutableDictionary *info = [[NSMutableDictionary alloc] init]; if (_carrierName) { info[@"carrierName"] = _carrierName; } if (_mobileCountryCode) { info[@"mobileCountryCode"] = _mobileCountryCode; } if (_mobileNetworkCode) { info[@"mobileNetworkCode"] = _mobileNetworkCode; } if (_isoCountryCode) { info[@"isoCountryCode"] = _isoCountryCode; } info[@"allowsVOIP"] = _allowsVOIP ? @"YES" : @"NO"; NSMutableString *description = [[NSMutableString alloc] init]; [description appendFormat:@"<%@ %p", NSStringFromClass([self class]), self]; [info enumerateKeysAndObjectsUsingBlock:^(id _Nonnull key, id _Nonnull obj, BOOL * _Nonnull stop) { [description appendFormat:@", %@=%@", key, obj]; }]; [description appendString:@">"]; return description; } @end #endif // #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST @implementation TNLCommunicationAgent (UnsafeSynchronousAccess) - (id<TNLCarrierInfo>)synchronousCarrierInfo { if ([NSThread isMainThread]) { TNLLogWarning(@"Calling -[%@ %@] from main thread, which can lead to very slow XPC!", NSStringFromClass([self class]), NSStringFromSelector(_cmd)); } __block id<TNLCarrierInfo> carrierInfo = nil; dispatch_sync(_agentQueue, ^{ carrierInfo = self.currentCarrierInfo; }); return carrierInfo; } @end @implementation TNLCommunicationAgentWeakWrapper @end static void _ReachabilityCallback(__unused SCNetworkReachabilityRef target, const SCNetworkReachabilityFlags flags, void* info) { TNLAssert(info != NULL); TNLAssert([(__bridge NSObject*)info isKindOfClass:[TNLCommunicationAgentWeakWrapper class]]); TNLCommunicationAgent *agent = [(__bridge TNLCommunicationAgentWeakWrapper *)info communicationAgent]; if (agent) { [agent _agent_updateReachabilityFlags:flags status:_NetworkReachabilityStatusFromFlags(flags)]; } } static TNLNetworkReachabilityStatus _NetworkReachabilityStatusFromFlags(TNLNetworkReachabilityFlags flags) { if (tnl_available_ios_12) { const TNLNetworkReachabilityMask mask = flags; if (TNL_BITMASK_EXCLUDES_FLAGS(mask, TNLNetworkReachabilityMaskPathStatusSatisfied)) { return TNLNetworkReachabilityNotReachable; } if (TNL_BITMASK_INTERSECTS_FLAGS(mask, TNLNetworkReachabilityMaskPathIntefaceTypeWifi)) { return TNLNetworkReachabilityReachableViaEthernet; } if (TNL_BITMASK_INTERSECTS_FLAGS(mask, TNLNetworkReachabilityMaskPathIntefaceTypeWired)) { return TNLNetworkReachabilityReachableViaEthernet; } if (TNL_BITMASK_INTERSECTS_FLAGS(mask, TNLNetworkReachabilityMaskPathIntefaceTypeCellular)) { return TNLNetworkReachabilityReachableViaWWAN; } // "Other" happens when using VPN or other tunneling protocol. // On iOS/tvOS/watchOS devices: WiFi or Wired or Cellular would have been hit above. // On Mac and iOS Simulator: WiFi or Wired will _NOT_ be provide in the flags so, even though we coerce to WiFi in this case on Mac, presume Ethernet if we get here. if (TNL_BITMASK_INTERSECTS_FLAGS(mask, TNLNetworkReachabilityMaskPathIntefaceTypeOther)) { return TNLNetworkReachabilityReachableViaEthernet; } return TNLNetworkReachabilityUndetermined; } if (TNL_BITMASK_EXCLUDES_FLAGS(flags, kSCNetworkReachabilityFlagsReachable)) { return TNLNetworkReachabilityNotReachable; } #if TARGET_OS_IOS if (TNL_BITMASK_INTERSECTS_FLAGS(flags, kSCNetworkReachabilityFlagsIsWWAN)) { return TNLNetworkReachabilityReachableViaWWAN; } #endif if (TNL_BITMASK_EXCLUDES_FLAGS(flags, kSCNetworkReachabilityFlagsConnectionRequired)) { return TNLNetworkReachabilityReachableViaEthernet; } if (TNL_BITMASK_EXCLUDES_FLAGS(flags, kSCNetworkReachabilityFlagsInterventionRequired)) { if (TNL_BITMASK_INTERSECTS_FLAGS(flags, kSCNetworkReachabilityFlagsConnectionOnDemand)) { return TNLNetworkReachabilityReachableViaEthernet; } if (TNL_BITMASK_INTERSECTS_FLAGS(flags, kSCNetworkReachabilityFlagsConnectionOnTraffic)) { return TNLNetworkReachabilityReachableViaEthernet; } } return TNLNetworkReachabilityNotReachable; } static TNLNetworkReachabilityFlags _NetworkReachabilityFlagsFromPath(nw_path_t path) { if (tnl_available_ios_12) { TNLNetworkReachabilityMask flags = 0; if (path != nil) { const nw_path_status_t status = nw_path_get_status(path); if (status > 0) { #if DEBUG if (gTwitterNetworkLayerAssertEnabled) { switch (status) { case nw_path_status_invalid: case nw_path_status_satisfied: case nw_path_status_unsatisfied: case nw_path_status_satisfiable: break; default: TNLAssertMessage(0, @"the nw_path_status_t enum has expanded! Need to update TNLNetworkReachabilityMask."); break; } } #endif flags |= _NWPathStatusToFlag(status); } for (nw_interface_type_t itype = 0; itype <= 4; itype++) { const bool usesInterface = nw_path_uses_interface_type(path, itype); if (usesInterface) { flags |= _NWInterfaceTypeToFlag(itype); } } #if TARGET_OS_SIMULATOR || TARGET_OS_MACCATALYST || TARGET_OS_OSX // When run on macOS (however the avenue) we will coerce // to have an ethernet connection when we detect `Other` but no actual interface. // This is most commonly due to VPN connections "hiding" the physical interface on Macs. if (TNL_BITMASK_INTERSECTS_FLAGS(flags, TNLNetworkReachabilityMaskPathIntefaceTypeOther)) { if (TNL_BITMASK_EXCLUDES_FLAGS(flags, TNLNetworkReachabilityMaskPathIntefaceTypeWifi | TNLNetworkReachabilityMaskPathIntefaceTypeCellular | TNLNetworkReachabilityMaskPathIntefaceTypeWired)) { flags |= TNLNetworkReachabilityMaskPathIntefaceTypeWifi; } } #endif if (tnl_available_ios_13) { if (nw_path_is_expensive(path)) { flags |= TNLNetworkReachabilityMaskPathConditionExpensive; } if (nw_path_is_constrained(path)) { flags |= TNLNetworkReachabilityMaskPathConditionConstrained; } } } return flags; } return 0; } TNLWWANRadioAccessTechnologyValue TNLWWANRadioAccessTechnologyValueFromString(NSString *WWANTechString) { static NSDictionary* sTechStringToValueMap = nil; static dispatch_once_t onceToken; dispatch_once(&onceToken, ^{ #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST sTechStringToValueMap = @{ CTRadioAccessTechnologyGPRS : @(TNLWWANRadioAccessTechnologyValueGPRS), CTRadioAccessTechnologyEdge: @(TNLWWANRadioAccessTechnologyValueEDGE), CTRadioAccessTechnologyWCDMA: @(TNLWWANRadioAccessTechnologyValueUMTS), CTRadioAccessTechnologyHSDPA: @(TNLWWANRadioAccessTechnologyValueHSDPA), CTRadioAccessTechnologyHSUPA: @(TNLWWANRadioAccessTechnologyValueHSUPA), CTRadioAccessTechnologyCDMA1x: @(TNLWWANRadioAccessTechnologyValue1xRTT), CTRadioAccessTechnologyCDMAEVDORev0: @(TNLWWANRadioAccessTechnologyValueEVDO_0), CTRadioAccessTechnologyCDMAEVDORevA: @(TNLWWANRadioAccessTechnologyValueEVDO_A), CTRadioAccessTechnologyCDMAEVDORevB: @(TNLWWANRadioAccessTechnologyValueEVDO_B), CTRadioAccessTechnologyeHRPD: @(TNLWWANRadioAccessTechnologyValueEHRPD), CTRadioAccessTechnologyLTE: @(TNLWWANRadioAccessTechnologyValueLTE) }; #else sTechStringToValueMap = @{}; #endif }); NSNumber *valueNumber = (WWANTechString) ? sTechStringToValueMap[WWANTechString] : nil; return (valueNumber) ? [valueNumber integerValue] : TNLWWANRadioAccessTechnologyValueUnknown; } NSString *TNLWWANRadioAccessTechnologyValueToString(TNLWWANRadioAccessTechnologyValue value) { #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST switch (value) { case TNLWWANRadioAccessTechnologyValueGPRS: return CTRadioAccessTechnologyGPRS; case TNLWWANRadioAccessTechnologyValueEDGE: return CTRadioAccessTechnologyEdge; case TNLWWANRadioAccessTechnologyValueUMTS: return CTRadioAccessTechnologyWCDMA; case TNLWWANRadioAccessTechnologyValueHSDPA: return CTRadioAccessTechnologyHSDPA; case TNLWWANRadioAccessTechnologyValueHSUPA: return CTRadioAccessTechnologyHSUPA; case TNLWWANRadioAccessTechnologyValueEVDO_0: return CTRadioAccessTechnologyCDMAEVDORev0; case TNLWWANRadioAccessTechnologyValueEVDO_A: return CTRadioAccessTechnologyCDMAEVDORevA; case TNLWWANRadioAccessTechnologyValueEVDO_B: return CTRadioAccessTechnologyCDMAEVDORevB; case TNLWWANRadioAccessTechnologyValue1xRTT: return CTRadioAccessTechnologyCDMA1x; case TNLWWANRadioAccessTechnologyValueLTE: return CTRadioAccessTechnologyLTE; case TNLWWANRadioAccessTechnologyValueEHRPD: return CTRadioAccessTechnologyeHRPD; case TNLWWANRadioAccessTechnologyValueHSPA: case TNLWWANRadioAccessTechnologyValueCDMA: case TNLWWANRadioAccessTechnologyValueIDEN: case TNLWWANRadioAccessTechnologyValueHSPAP: case TNLWWANRadioAccessTechnologyValueUnknown: break; } #endif // TARGET_OS_IOS && !TARGET_OS_MACCATALYST return @"unknown"; } TNLWWANRadioAccessGeneration TNLWWANRadioAccessGenerationForTechnologyValue(TNLWWANRadioAccessTechnologyValue value) { #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST switch (value) { case TNLWWANRadioAccessTechnologyValueEVDO_0: case TNLWWANRadioAccessTechnologyValue1xRTT: return TNLWWANRadioAccessGeneration1G; case TNLWWANRadioAccessTechnologyValueGPRS: case TNLWWANRadioAccessTechnologyValueEDGE: case TNLWWANRadioAccessTechnologyValueIDEN: case TNLWWANRadioAccessTechnologyValueCDMA: return TNLWWANRadioAccessGeneration2G; case TNLWWANRadioAccessTechnologyValueUMTS: case TNLWWANRadioAccessTechnologyValueHSDPA: case TNLWWANRadioAccessTechnologyValueHSUPA: case TNLWWANRadioAccessTechnologyValueHSPA: case TNLWWANRadioAccessTechnologyValueEVDO_A: case TNLWWANRadioAccessTechnologyValueEVDO_B: return TNLWWANRadioAccessGeneration3G; case TNLWWANRadioAccessTechnologyValueLTE: case TNLWWANRadioAccessTechnologyValueEHRPD: case TNLWWANRadioAccessTechnologyValueHSPAP: return TNLWWANRadioAccessGeneration4G; case TNLWWANRadioAccessTechnologyValueUnknown: break; } #endif // #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST return TNLWWANRadioAccessGenerationUnknown; } NSString *TNLNetworkReachabilityStatusToString(TNLNetworkReachabilityStatus status) { switch (status) { case TNLNetworkReachabilityNotReachable: return @"unreachable"; case TNLNetworkReachabilityReachableViaEthernet: return @"wifi"; case TNLNetworkReachabilityReachableViaWWAN: return @"wwan"; case TNLNetworkReachabilityUndetermined: break; } return @"undetermined"; } NSString *TNLCaptivePortalStatusToString(TNLCaptivePortalStatus status) { switch (status) { case TNLCaptivePortalStatusUndetermined: break; case TNLCaptivePortalStatusNoCaptivePortal: return @"not_captive"; case TNLCaptivePortalStatusCaptivePortalDetected: return @"captive"; case TNLCaptivePortalStatusDetectionBlockedByAppTransportSecurity: return @"ats_blocked"; } return @"undetermined"; } #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST NSDictionary * __nullable TNLCarrierInfoToDictionary(id<TNLCarrierInfo> __nullable carrierInfo) { if (!carrierInfo) { return nil; } NSMutableDictionary *dict = [[NSMutableDictionary alloc] init]; if (carrierInfo.carrierName) { dict[@"carrierName"] = carrierInfo.carrierName; } if (carrierInfo.mobileNetworkCode) { dict[@"mobileNetworkCode"] = carrierInfo.mobileNetworkCode; } if (carrierInfo.mobileCountryCode) { dict[@"mobileCountryCode"] = carrierInfo.mobileCountryCode; } if (carrierInfo.isoCountryCode) { dict[@"isoCountryCode"] = carrierInfo.isoCountryCode; } dict[@"allowsVOIP"] = @(carrierInfo.allowsVOIP); return [dict copy]; } id<TNLCarrierInfo> __nullable TNLCarrierInfoFromDictionary(NSDictionary * __nullable dict) { if (!dict.count) { return nil; } return [[TNLCarrierInfoInternal alloc] initWithCarrierName:dict[@"carrierName"] mobileCountryCode:dict[@"mobileNetworkCode"] mobileNetworkCode:dict[@"mobileCountryCode"] isoCountryCode:dict[@"isoCountryCode"] allowsVOIP:[dict[@"allowsVOIP"] boolValue]]; } #endif // #if TARGET_OS_IOS && !TARGET_OS_MACCATALYST NS_INLINE const char _DebugCharFromReachabilityFlag(TNLNetworkReachabilityFlags flags, uint32_t flag, const char presentChar) { return TNL_BITMASK_HAS_SUBSET_FLAGS(flags, flag) ? presentChar : '_'; } NSString *TNLDebugStringFromNetworkReachabilityFlags(TNLNetworkReachabilityFlags flags) { if (tnl_available_ios_12) { NSString *dbgStr; dbgStr = [NSString stringWithFormat:@"%c%c%c%c%c%c%c%c", _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathStatusUnsatisfied, 'U'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathStatusSatisfied, 'S'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathStatusSatisfiable, 's'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathIntefaceTypeOther, 'o'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathIntefaceTypeWifi, 'w'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathIntefaceTypeCellular, 'c'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathIntefaceTypeWired, 'e'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathIntefaceTypeLoopback, 'l') ]; if (tnl_available_ios_13) { dbgStr = [dbgStr stringByAppendingFormat:@"%c%c", _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathConditionExpensive, '$'), _DebugCharFromReachabilityFlag(flags, TNLNetworkReachabilityMaskPathConditionConstrained, 'C')]; } return dbgStr; } return [NSString stringWithFormat: #if TARGET_OS_IOS @"%c%c%c%c%c%c%c%c%c", #else @"%c%c%c%c%c%c%c%c", #endif _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsTransientConnection, 'T'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsReachable, 'R'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsConnectionRequired, 'r'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsConnectionOnTraffic, 't'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsInterventionRequired, 'i'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsConnectionOnDemand, 'd'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsIsLocalAddress, 'L'), _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsIsDirect, 'D') #if TARGET_OS_IOS , _DebugCharFromReachabilityFlag(flags, kSCNetworkReachabilityFlagsIsWWAN, 'W') #endif ]; } NSDictionary<NSString *, id> *TNLCarrierInfoToDictionaryDescription(id<TNLCarrierInfo> carrierInfo) { return @{ @"carrierName" : carrierInfo.carrierName ?: [NSNull null], @"mobileCountryCode" : carrierInfo.mobileCountryCode ?: [NSNull null], @"mobileNetworkCode" : carrierInfo.mobileNetworkCode ?: [NSNull null], @"isoCountryCode" : carrierInfo.isoCountryCode ?: [NSNull null], @"allowsVOIP" : @(carrierInfo.allowsVOIP) }; } #import <ifaddrs.h> static BOOL _HasCellularInterface() { struct ifaddrs * addrs; if (getifaddrs(&addrs) != 0) { return NO; } tnl_defer(^{ freeifaddrs(addrs); }); for (const struct ifaddrs * cursor = addrs; cursor != NULL; cursor = cursor->ifa_next) { NSString *name = @(cursor->ifa_name); if ([name isEqualToString:@"pdp_ip0"]) { // All cellular interfaces are `pdp_ip`. // There can be multiple, but the first one will always be number `0`. return YES; } } return NO; } #endif // !TARGET_OS_WATCH