ClientNetworkMonitor.swift 6.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162
  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 callback: (State) -> Void
  30. private let reachability: SCNetworkReachability
  31. /// Instance of network info being used for obtaining cellular technology names.
  32. public let cellularInfo = CTTelephonyNetworkInfo()
  33. /// Whether the network is currently reachable. Backed by `SCNetworkReachability`.
  34. public private(set) var isReachable: Bool?
  35. /// Whether the device is currently using wifi (versus cellular).
  36. public private(set) var isUsingWifi: Bool?
  37. /// Name of the cellular technology being used (e.g., `CTRadioAccessTechnologyLTE`).
  38. public private(set) var cellularName: String?
  39. /// Represents a state of connectivity.
  40. public struct State: Equatable {
  41. /// The most recent change that was made to the state.
  42. public let lastChange: Change
  43. /// Whether this state is currently reachable/online.
  44. public let isReachable: Bool
  45. }
  46. /// A change in network condition.
  47. public enum Change: Equatable {
  48. /// Reachability changed (online <> offline).
  49. case reachability(isReachable: Bool)
  50. /// The device switched from cellular to wifi.
  51. case cellularToWifi
  52. /// The device switched from wifi to cellular.
  53. case wifiToCellular
  54. /// The cellular technology changed (e.g., 3G <> LTE).
  55. case cellularTechnology(technology: String)
  56. }
  57. /// Designated initializer for the network monitor. Initializer fails if reachability is unavailable.
  58. ///
  59. /// - Parameter host: Host to use for monitoring reachability.
  60. /// - Parameter queue: Queue on which to process and update network changes. Will create one if `nil`.
  61. /// Should always be used when accessing properties of this class.
  62. /// - Parameter callback: Closure to call whenever state changes.
  63. public init?(host: String = "google.com", queue: DispatchQueue? = nil, callback: @escaping (State) -> Void) {
  64. guard let reachability = SCNetworkReachabilityCreateWithName(nil, host) else {
  65. return nil
  66. }
  67. self.queue = queue ?? DispatchQueue(label: "SwiftGRPC.ClientNetworkMonitor.queue")
  68. self.callback = callback
  69. self.reachability = reachability
  70. self.startMonitoringReachability(reachability)
  71. self.startMonitoringCellular()
  72. }
  73. deinit {
  74. SCNetworkReachabilitySetCallback(self.reachability, nil, nil)
  75. SCNetworkReachabilityUnscheduleFromRunLoop(self.reachability, CFRunLoopGetMain(),
  76. CFRunLoopMode.commonModes.rawValue)
  77. NotificationCenter.default.removeObserver(self)
  78. }
  79. // MARK: - Cellular
  80. private func startMonitoringCellular() {
  81. let notificationName: Notification.Name
  82. if #available(iOS 12.0, *) {
  83. notificationName = .CTServiceRadioAccessTechnologyDidChange
  84. } else {
  85. notificationName = .CTRadioAccessTechnologyDidChange
  86. }
  87. NotificationCenter.default.addObserver(self, selector: #selector(self.cellularDidChange(_:)),
  88. name: notificationName, object: nil)
  89. }
  90. @objc
  91. private func cellularDidChange(_ notification: NSNotification) {
  92. self.queue.async {
  93. let newCellularName: String?
  94. if #available(iOS 12.0, *) {
  95. let cellularKey = notification.object as? String
  96. newCellularName = cellularKey.flatMap { self.cellularInfo.serviceCurrentRadioAccessTechnology?[$0] }
  97. } else {
  98. newCellularName = notification.object as? String ?? self.cellularInfo.currentRadioAccessTechnology
  99. }
  100. if let newCellularName = newCellularName, self.cellularName != newCellularName {
  101. self.cellularName = newCellularName
  102. self.callback(State(lastChange: .cellularTechnology(technology: newCellularName),
  103. isReachable: self.isReachable ?? false))
  104. }
  105. }
  106. }
  107. // MARK: - Reachability
  108. private func startMonitoringReachability(_ reachability: SCNetworkReachability) {
  109. let info = Unmanaged.passUnretained(self).toOpaque()
  110. var context = SCNetworkReachabilityContext(version: 0, info: info, retain: nil,
  111. release: nil, copyDescription: nil)
  112. let callback: SCNetworkReachabilityCallBack = { _, flags, info in
  113. let observer = info.map { Unmanaged<ClientNetworkMonitor>.fromOpaque($0).takeUnretainedValue() }
  114. observer?.reachabilityDidChange(with: flags)
  115. }
  116. SCNetworkReachabilitySetCallback(reachability, callback, &context)
  117. SCNetworkReachabilityScheduleWithRunLoop(reachability, CFRunLoopGetMain(),
  118. CFRunLoopMode.commonModes.rawValue)
  119. self.queue.async { [weak self] in
  120. var flags = SCNetworkReachabilityFlags()
  121. SCNetworkReachabilityGetFlags(reachability, &flags)
  122. self?.reachabilityDidChange(with: flags)
  123. }
  124. }
  125. private func reachabilityDidChange(with flags: SCNetworkReachabilityFlags) {
  126. self.queue.async {
  127. let isUsingWifi = !flags.contains(.isWWAN)
  128. let isReachable = flags.contains(.reachable)
  129. let notifyForWifi = self.isUsingWifi != nil && self.isUsingWifi != isUsingWifi
  130. let notifyForReachable = self.isReachable != nil && self.isReachable != isReachable
  131. self.isUsingWifi = isUsingWifi
  132. self.isReachable = isReachable
  133. if notifyForWifi {
  134. self.callback(State(lastChange: isUsingWifi ? .cellularToWifi : .wifiToCellular, isReachable: isReachable))
  135. }
  136. if notifyForReachable {
  137. self.callback(State(lastChange: .reachability(isReachable: isReachable), isReachable: isReachable))
  138. }
  139. }
  140. }
  141. }
  142. #endif