// Copyright 2017 Google // // 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. #import #import "Private/FIRReachabilityChecker+Internal.h" #import "Private/FIRReachabilityChecker.h" #import "Private/FIRLogger.h" #import "Private/FIRNetwork.h" #import "Private/FIRNetworkMessageCode.h" static void ReachabilityCallback(SCNetworkReachabilityRef reachability, SCNetworkReachabilityFlags flags, void *info); static const struct FIRReachabilityApi kFIRDefaultReachabilityApi = { SCNetworkReachabilityCreateWithName, SCNetworkReachabilitySetCallback, SCNetworkReachabilityScheduleWithRunLoop, SCNetworkReachabilityUnscheduleFromRunLoop, CFRelease, }; static NSString *const kFIRReachabilityUnknownStatus = @"Unknown"; static NSString *const kFIRReachabilityConnectedStatus = @"Connected"; static NSString *const kFIRReachabilityDisconnectedStatus = @"Disconnected"; @interface FIRReachabilityChecker () @property(nonatomic, assign) const struct FIRReachabilityApi *reachabilityApi; @property(nonatomic, assign) FIRReachabilityStatus reachabilityStatus; @property(nonatomic, copy) NSString *host; @property(nonatomic, assign) SCNetworkReachabilityRef reachability; @end @implementation FIRReachabilityChecker @synthesize reachabilityApi = reachabilityApi_; @synthesize reachability = reachability_; - (const struct FIRReachabilityApi *)reachabilityApi { return reachabilityApi_; } - (void)setReachabilityApi:(const struct FIRReachabilityApi *)reachabilityApi { if (reachability_) { NSString *message = @"Cannot change reachability API while reachability is running. " @"Call stop first."; [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError messageCode:kFIRNetworkMessageCodeReachabilityChecker000 message:message]; return; } reachabilityApi_ = reachabilityApi; } @synthesize reachabilityStatus = reachabilityStatus_; @synthesize host = host_; @synthesize reachabilityDelegate = reachabilityDelegate_; @synthesize loggerDelegate = loggerDelegate_; - (BOOL)isActive { return reachability_ != nil; } - (void)setReachabilityDelegate:(id)reachabilityDelegate { if (reachabilityDelegate && (![(NSObject *)reachabilityDelegate conformsToProtocol:@protocol(FIRReachabilityDelegate)])) { FIRLogError(kFIRLoggerCore, [NSString stringWithFormat:@"I-NET%06ld", (long)kFIRNetworkMessageCodeReachabilityChecker005], @"Reachability delegate doesn't conform to Reachability protocol."); return; } reachabilityDelegate_ = reachabilityDelegate; } - (void)setLoggerDelegate:(id)loggerDelegate { if (loggerDelegate && (![(NSObject *)loggerDelegate conformsToProtocol:@protocol(FIRNetworkLoggerDelegate)])) { FIRLogError(kFIRLoggerCore, [NSString stringWithFormat:@"I-NET%06ld", (long)kFIRNetworkMessageCodeReachabilityChecker006], @"Reachability delegate doesn't conform to Logger protocol."); return; } loggerDelegate_ = loggerDelegate; } - (instancetype)initWithReachabilityDelegate:(id)reachabilityDelegate loggerDelegate:(id)loggerDelegate withHost:(NSString *)host { self = [super init]; [self setLoggerDelegate:loggerDelegate]; if (!host || !host.length) { [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError messageCode:kFIRNetworkMessageCodeReachabilityChecker001 message:@"Invalid host specified"]; return nil; } if (self) { [self setReachabilityDelegate:reachabilityDelegate]; reachabilityApi_ = &kFIRDefaultReachabilityApi; reachabilityStatus_ = kFIRReachabilityUnknown; host_ = [host copy]; reachability_ = nil; } return self; } - (void)dealloc { reachabilityDelegate_ = nil; loggerDelegate_ = nil; [self stop]; } - (BOOL)start { if (!reachability_) { reachability_ = reachabilityApi_->createWithNameFn(kCFAllocatorDefault, [host_ UTF8String]); if (!reachability_) { return NO; } SCNetworkReachabilityContext context = { 0, /* version */ (__bridge void *)(self), /* info (passed as last parameter to reachability callback) */ NULL, /* retain */ NULL, /* release */ NULL /* copyDescription */ }; if (!reachabilityApi_->setCallbackFn(reachability_, ReachabilityCallback, &context) || !reachabilityApi_->scheduleWithRunLoopFn(reachability_, CFRunLoopGetMain(), kCFRunLoopCommonModes)) { reachabilityApi_->releaseFn(reachability_); reachability_ = nil; [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelError messageCode:kFIRNetworkMessageCodeReachabilityChecker002 message:@"Failed to start reachability handle"]; return NO; } } [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug messageCode:kFIRNetworkMessageCodeReachabilityChecker003 message:@"Monitoring the network status"]; return YES; } - (void)stop { if (reachability_) { reachabilityStatus_ = kFIRReachabilityUnknown; reachabilityApi_->unscheduleFromRunLoopFn(reachability_, CFRunLoopGetMain(), kCFRunLoopCommonModes); reachabilityApi_->releaseFn(reachability_); reachability_ = nil; } } - (FIRReachabilityStatus)statusForFlags:(SCNetworkReachabilityFlags)flags { FIRReachabilityStatus status = kFIRReachabilityNotReachable; // If the Reachable flag is not set, we definitely don't have connectivity. if (flags & kSCNetworkReachabilityFlagsReachable) { // Reachable flag is set. Check further flags. if (!(flags & kSCNetworkReachabilityFlagsConnectionRequired)) { // Connection required flag is not set, so we have connectivity. #if TARGET_OS_IOS || TARGET_OS_TV status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular : kFIRReachabilityViaWifi; #elif TARGET_OS_OSX status = kFIRReachabilityViaWifi; #endif } else if ((flags & (kSCNetworkReachabilityFlagsConnectionOnDemand | kSCNetworkReachabilityFlagsConnectionOnTraffic)) && !(flags & kSCNetworkReachabilityFlagsInterventionRequired)) { // If the connection on demand or connection on traffic flag is set, and user intervention // is not required, we have connectivity. #if TARGET_OS_IOS || TARGET_OS_TV status = (flags & kSCNetworkReachabilityFlagsIsWWAN) ? kFIRReachabilityViaCellular : kFIRReachabilityViaWifi; #elif TARGET_OS_OSX status = kFIRReachabilityViaWifi; #endif } } return status; } - (void)reachabilityFlagsChanged:(SCNetworkReachabilityFlags)flags { FIRReachabilityStatus status = [self statusForFlags:flags]; if (reachabilityStatus_ != status) { NSString *reachabilityStatusString; if (status == kFIRReachabilityUnknown) { reachabilityStatusString = kFIRReachabilityUnknownStatus; } else { reachabilityStatusString = (status == kFIRReachabilityNotReachable) ? kFIRReachabilityDisconnectedStatus : kFIRReachabilityConnectedStatus; } [loggerDelegate_ firNetwork_logWithLevel:kFIRNetworkLogLevelDebug messageCode:kFIRNetworkMessageCodeReachabilityChecker004 message:@"Network status has changed. Code, status" contexts:@[ @(status), reachabilityStatusString ]]; reachabilityStatus_ = status; [reachabilityDelegate_ reachability:self statusChanged:reachabilityStatus_]; } } @end static void ReachabilityCallback(SCNetworkReachabilityRef reachability, SCNetworkReachabilityFlags flags, void *info) { FIRReachabilityChecker *checker = (__bridge FIRReachabilityChecker *)info; [checker reachabilityFlagsChanged:flags]; } // This function used to be at the top of the file, but it was moved here // as a workaround for a suspected compiler bug. When compiled in Release mode // and run on an iOS device with WiFi disabled, the reachability code crashed // when calling SCNetworkReachabilityScheduleWithRunLoop, or shortly thereafter. // After unsuccessfully trying to diagnose the cause of the crash, it was // discovered that moving this function to the end of the file magically fixed // the crash. If you are going to edit this file, exercise caution and make sure // to test thoroughly with an iOS device under various network conditions. const NSString *FIRReachabilityStatusString(FIRReachabilityStatus status) { switch (status) { case kFIRReachabilityUnknown: return @"Reachability Unknown"; case kFIRReachabilityNotReachable: return @"Not reachable"; case kFIRReachabilityViaWifi: return @"Reachable via Wifi"; case kFIRReachabilityViaCellular: return @"Reachable via Cellular Data"; default: return [NSString stringWithFormat:@"Invalid reachability status %d", (int)status]; } }