aboutsummaryrefslogtreecommitdiffhomepage
path: root/Firebase/Core/FIRReachabilityChecker.m
blob: cac87ff98aa1939390d27d8b869670935c16de9c (plain)
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
// 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 <Foundation/Foundation.h>

#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<FIRReachabilityDelegate>)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<FIRNetworkLoggerDelegate>)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<FIRReachabilityDelegate>)reachabilityDelegate
                              loggerDelegate:(id<FIRNetworkLoggerDelegate>)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];
  }
}