ClientNetworkMonitor.swift 7.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168
  1. /*
  2. * Copyright 2019, gRPC Authors All rights reserved.
  3. *
  4. * Licensed under the Apache License, Version 2.0 (the "License");
  5. * you may not use this file except in compliance with the License.
  6. * You may obtain a copy of the License at
  7. *
  8. * http://www.apache.org/licenses/LICENSE-2.0
  9. *
  10. * Unless required by applicable law or agreed to in writing, software
  11. * distributed under the License is distributed on an "AS IS" BASIS,
  12. * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
  13. * See the License for the specific language governing permissions and
  14. * limitations under the License.
  15. */
  16. #if os(iOS)
  17. import CoreTelephony
  18. import Dispatch
  19. import SystemConfiguration
  20. /// This class may be used to monitor changes on the device that can cause gRPC to silently disconnect (making
  21. /// it seem like active calls/connections are hanging), then manually shut down / restart gRPC channels as
  22. /// needed. The root cause of these problems is that the backing gRPC-Core doesn't get the optimizations
  23. /// made by iOS' networking stack when changes occur on the device such as switching from wifi to cellular,
  24. /// switching between 3G and LTE, enabling/disabling airplane mode, etc.
  25. /// Read more: https://github.com/grpc/grpc-swift/tree/master/README.md#known-issues
  26. /// Original issue: https://github.com/grpc/grpc-swift/issues/337
  27. open class ClientNetworkMonitor {
  28. private let queue: DispatchQueue
  29. private let useNewCellMonitor: Bool
  30. private let callback: (State) -> Void
  31. private let reachability: SCNetworkReachability
  32. /// Instance of network info being used for obtaining cellular technology names.
  33. public let cellularInfo = CTTelephonyNetworkInfo()
  34. /// Whether the network is currently reachable. Backed by `SCNetworkReachability`.
  35. public private(set) var isReachable: Bool?
  36. /// Whether the device is currently using wifi (versus cellular).
  37. public private(set) var isUsingWifi: Bool?
  38. /// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`).
  39. public private(set) var cellularName: String?
  40. /// Represents a state of connectivity.
  41. public struct State: Equatable {
  42. /// The most recent change that was made to the state.
  43. public let lastChange: Change
  44. /// Whether this state is currently reachable/online.
  45. public let isReachable: Bool
  46. }
  47. /// A change in network condition.
  48. public enum Change: Equatable {
  49. /// Reachability changed (online <> offline).
  50. case reachability(isReachable: Bool)
  51. /// The device switched from cellular to wifi.
  52. case cellularToWifi
  53. /// The device switched from wifi to cellular.
  54. case wifiToCellular
  55. /// The cellular technology changed (e.g., 3G <> LTE).
  56. case cellularTechnology(technology: String)
  57. }
  58. /// Designated initializer for the network monitor. Initializer fails if reachability is unavailable.
  59. ///
  60. /// - Parameter host: Host to use for monitoring reachability.
  61. /// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`.
  62. /// Should always be used when accessing properties of this class.
  63. /// - Parameter useNewCellMonitor: Whether to use the new cellular monitor introduced in iOS 12.
  64. /// Due to rdar://46873673 this defaults to false to prevent crashes.
  65. /// - Parameter callback: Closure to call whenever state changes.
  66. public init?(host: String = "google.com", queue: DispatchQueue? = nil, useNewCellMonitor: Bool = false,
  67. callback: @escaping (State) -> Void)
  68. {
  69. guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else {
  70. return nil
  71. }
  72. self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue")
  73. self.useNewCellMonitor = useNewCellMonitor
  74. self.callback = callback
  75. self.reachability = reachability
  76. self.startMonitoringReachability(reachability)
  77. self.startMonitoringCellular()
  78. }
  79. deinit {
  80. SCNetworkReachabilitySetCallback(self.reachability, nil, nil)
  81. SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(),
  82. CFRunLoopMode.commonModes.rawValue)
  83. NotificationCenter.default.removeObserver(self)
  84. }
  85. // MARK: - Cellular
  86. private func startMonitoringCellular() {
  87. let notificationName: Notification.Name
  88. if #available(iOS 12.0, *), self.useNewCellMonitor {
  89. notificationName = .CTServiceRadioAccessTechnologyDidChange
  90. } else {
  91. notificationName = .CTRadioAccessTechnologyDidChange
  92. }
  93. NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)),
  94. name: notificationName, object: nil)
  95. }
  96. @objc
  97. private func cellularDidChange(_ notification: NSNotification) {
  98. self.queue.async {
  99. let newCellularName: String?
  100. if #available(iOS 12.0, *), self.useNewCellMonitor {
  101. let cellularKey = notification.object as? String
  102. newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] }
  103. } else {
  104. newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology
  105. }
  106. if let newCellularName = newCellularName, self.cellularName != newCellularName {
  107. self.cellularName = newCellularName
  108. self.callback(State(lastChange: .cellularTechnology(technology: newCellularName),
  109. isReachable: self.isReachable ?? false))
  110. }
  111. }
  112. }
  113. // MARK: - Reachability
  114. private func startMonitoringReachability(_ reachability: SCNetworkReachability) {
  115. let info = Unmanaged.passUnretained(self).toOpaque()
  116. var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil,
  117. release: nil, copyDescription: nil)
  118. let callback: SCNetworkReachabilityCallBack = { _, flags, info in
  119. let observer = info.map { Unmanaged<ClientNetworkMonitor>.fromOpaque($0).takeUnretainedValue() }
  120. observer?.reachabilityDidChange(with: flags)
  121. }
  122. SCNetworkReachabilitySetCallback(reachability, callback, &context)
  123. SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(),
  124. CFRunLoopMode.commonModes.rawValue)
  125. self.queue.async { [weak self] in
  126. var flags = SCNetworkReachabilityFlags()
  127. SCNetworkReachabilityGetFlags(reachability, &flags)
  128. self?.reachabilityDidChange(with: flags)
  129. }
  130. }
  131. private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) {
  132. self.queue.async {
  133. let isUsingWifi = !flags.contains(.isWWAN)
  134. let isReachable = flags.contains(.reachable)
  135. let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi
  136. let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable
  137. self.isUsingWifi = isUsingWifi
  138. self.isReachable = isReachable
  139. if notifyForWifi {
  140. self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable))
  141. }
  142. if notifyForReachable {
  143. self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable))
  144. }
  145. }
  146. }
  147. }
  148. #endif