forked from grpc/grpc-swift
-
Notifications
You must be signed in to change notification settings - Fork 0
/
ClientNetworkMonitor.swift
168 lines (148 loc) · 7.19 KB
/
ClientNetworkMonitor.swift
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
/*
* Copyright 2019, gRPC Authors 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 os(iOS)
import CoreTelephony
import Dispatch
import SystemConfiguration
/// This class may be used to monitor changes on the device that can cause gRPC to silently disconnect (making
/// it seem like active calls/connections are hanging), then manually shut down / restart gRPC channels as
/// needed. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations
/// made by iOS' networking stack when changes occur on the device such as switching from wifi to cellular,
/// switching between 3G and LTE, enabling/disabling airplane mode, etc.
/// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues
/// Original issue: https://github.com/grpc/grpc-swift/issues/337
open class ClientNetworkMonitor {
private let queue: DispatchQueue
private let useNewCellMonitor: Bool
private let callback: (State) -> Void
private let reachability: SCNetworkReachability
/// Instance of network info being used for obtaining cellular technology names.
public let cellularInfo = CTTelephonyNetworkInfo()
/// Whether the network is currently reachable. Backed by `SCNetworkReachability`.
public private(set) var isReachable: Bool?
/// Whether the device is currently using wifi (versus cellular).
public private(set) var isUsingWifi: Bool?
/// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`).
public private(set) var cellularName: String?
/// Represents a state of connectivity.
public struct State: Equatable {
/// The most recent change that was made to the state.
public let lastChange: Change
/// Whether this state is currently reachable/online.
public let isReachable: Bool
}
/// A change in network condition.
public enum Change: Equatable {
/// Reachability changed (online <> offline).
case reachability(isReachable: Bool)
/// The device switched from cellular to wifi.
case cellularToWifi
/// The device switched from wifi to cellular.
case wifiToCellular
/// The cellular technology changed (e.g., 3G <> LTE).
case cellularTechnology(technology: String)
}
/// Designated initializer for the network monitor. Initializer fails if reachability is unavailable.
///
/// - Parameter host: Host to use for monitoring reachability.
/// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`.
/// Should always be used when accessing properties of this class.
/// - Parameter useNewCellMonitor: Whether to use the new cellular monitor introduced in iOS 12.
/// Due to rdar://46873673 this defaults to false to prevent crashes.
/// - Parameter callback: Closure to call whenever state changes.
public init?(host: String = "google.com", queue: DispatchQueue? = nil, useNewCellMonitor: Bool = false,
callback: @escaping (State) -> Void)
{
guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else {
return nil
}
self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue")
self.useNewCellMonitor = useNewCellMonitor
self.callback = callback
self.reachability = reachability
self.startMonitoringReachability(reachability)
self.startMonitoringCellular()
}
deinit {
SCNetworkReachabilitySetCallback(self.reachability, nil, nil)
SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(),
CFRunLoopMode.commonModes.rawValue)
NotificationCenter.default.removeObserver(self)
}
// MARK: - Cellular
private func startMonitoringCellular() {
let notificationName: Notification.Name
if #available(iOS 12.0, *), self.useNewCellMonitor {
notificationName = .CTServiceRadioAccessTechnologyDidChange
} else {
notificationName = .CTRadioAccessTechnologyDidChange
}
NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)),
name: notificationName, object: nil)
}
@objc
private func cellularDidChange(_ notification: NSNotification) {
self.queue.async {
let newCellularName: String?
if #available(iOS 12.0, *), self.useNewCellMonitor {
let cellularKey = notification.object as? String
newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] }
} else {
newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology
}
if let newCellularName = newCellularName, self.cellularName != newCellularName {
self.cellularName = newCellularName
self.callback(State(lastChange: .cellularTechnology(technology: newCellularName),
isReachable: self.isReachable ?? false))
}
}
}
// MARK: - Reachability
private func startMonitoringReachability(_ reachability: SCNetworkReachability) {
let info = Unmanaged.passUnretained(self).toOpaque()
var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil,
release: nil, copyDescription: nil)
let callback: SCNetworkReachabilityCallBack = { _, flags, info in
let observer = info.map { Unmanaged<ClientNetworkMonitor>.fromOpaque($0).takeUnretainedValue() }
observer?.reachabilityDidChange(with: flags)
}
SCNetworkReachabilitySetCallback(reachability, callback, &context)
SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(),
CFRunLoopMode.commonModes.rawValue)
self.queue.async { [weak self] in
var flags = SCNetworkReachabilityFlags()
SCNetworkReachabilityGetFlags(reachability, &flags)
self?.reachabilityDidChange(with: flags)
}
}
private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) {
self.queue.async {
let isUsingWifi = !flags.contains(.isWWAN)
let isReachable = flags.contains(.reachable)
let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi
let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable
self.isUsingWifi = isUsingWifi
self.isReachable = isReachable
if notifyForWifi {
self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable))
}
if notifyForReachable {
self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable))
}
}
}
}
#endif