Path: blob/main/SignalServiceKit/Network/ReachabilityManager.swift
1 views
//
// Copyright 2018 Signal Messenger, LLC
// SPDX-License-Identifier: AGPL-3.0-only
//
import Foundation
import SystemConfiguration
public enum ReachabilityType {
case any
case wifi
case cellular
}
// MARK: -
public class SSKReachability {
// This notification is only fired if the app is ready.
public static let owsReachabilityDidChange = Notification.Name("owsReachabilityDidChange")
}
// MARK: -
public protocol SSKReachabilityManager {
var isReachable: Bool { get }
func isReachable(via reachabilityType: ReachabilityType) -> Bool
}
public extension SSKReachabilityManager {
var currentReachabilityString: String {
guard isReachable else { return "No Connection" }
if isReachable(via: .wifi) { return "WiFi" }
if isReachable(via: .cellular) { return "Cellular" }
return "Unknown (but online)"
}
func isReachable(with configuration: NetworkInterfaceSet) -> Bool {
NetworkInterface.allCases.contains { interface in
configuration.isSuperset(of: interface.singleItemSet) && isReachable(via: interface.reachabilityType)
}
}
}
// MARK: -
public class SSKReachabilityManagerImpl: SSKReachabilityManager {
private struct Token {
let isReachable: Bool
let isReachableViaWiFi: Bool
let isReachableViaWWAN: Bool
static var empty: Token {
Token(isReachable: false, isReachableViaWiFi: false, isReachableViaWWAN: false)
}
}
private let backgroundSession = OWSURLSession(
securityPolicy: OWSURLSession.signalServiceSecurityPolicy,
configuration: .background(withIdentifier: "SSKReachabilityManagerImpl"),
canUseSignalProxy: false,
)
private let reachability: SCNetworkReachability
private let token = AtomicValue<Token>(.empty, lock: .sharedGlobal)
public init(appReadiness: AppReadiness) {
AssertIsOnMainThread()
// Set up a check for connecting to IPv4 0.0.0.0, meaning "the IPv4 internet in general".
var sockAddrRepresentingIPv4InGeneral = sockaddr_in()
sockAddrRepresentingIPv4InGeneral.sin_len = numericCast(MemoryLayout<sockaddr_in>.size)
sockAddrRepresentingIPv4InGeneral.sin_family = numericCast(AF_INET)
self.reachability = withUnsafePointer(to: sockAddrRepresentingIPv4InGeneral) {
$0.withMemoryRebound(to: sockaddr.self, capacity: 1) {
SCNetworkReachabilityCreateWithAddress(nil, $0)!
}
}
appReadiness.runNowOrWhenAppDidBecomeReadySync {
self.configure()
}
}
// MARK: -
public var isReachable: Bool {
isReachable(via: .any)
}
public func isReachable(via reachabilityType: ReachabilityType) -> Bool {
switch reachabilityType {
case .any:
return token.get().isReachable
case .wifi:
return token.get().isReachableViaWiFi
case .cellular:
return token.get().isReachableViaWWAN
}
}
// MARK: -
func reachabilityChanged(newState: SCNetworkReachabilityFlags) {
AssertIsOnMainThread()
updateToken(newState)
Logger.info("New preferred network: \(currentReachabilityString)")
NotificationCenter.default.post(name: SSKReachability.owsReachabilityDidChange, object: self)
scheduleWakeupRequestIfNecessary()
}
private func updateToken(_ rawFlags: SCNetworkReachabilityFlags) {
AssertIsOnMainThread()
// This logic was originally taken from the Reachability pod.
// Credit to Tony Million.
token.set(Token(
// Don't count [.connectionRequired, .transientConnection] because (historically, according to the Reachability pod) it can happen when you toggle airplane mode on and off.
isReachable: rawFlags.contains(.reachable) && !(rawFlags.contains(.connectionRequired) && rawFlags.contains(.transientConnection)),
isReachableViaWiFi: rawFlags.contains(.reachable) && !rawFlags.contains(.isWWAN),
isReachableViaWWAN: rawFlags.contains(.reachable) && rawFlags.contains(.isWWAN),
))
}
private func configure() {
AssertIsOnMainThread()
/// A retain-cycle breaker we can pass to `SCNetworkReachabilitySetCallback`.
class WeakWrapper {
weak var manager: SSKReachabilityManagerImpl?
init(manager: SSKReachabilityManagerImpl) {
self.manager = manager
}
}
let weakWrapper = WeakWrapper(manager: self)
var weakWrapperContext = SCNetworkReachabilityContext(
version: 0,
info: Unmanaged.passRetained(weakWrapper).toOpaque(),
retain: { UnsafeRawPointer(Unmanaged<WeakWrapper>.fromOpaque($0).retain().toOpaque()) },
release: { Unmanaged<WeakWrapper>.fromOpaque($0).release() },
copyDescription: nil,
)
guard
SCNetworkReachabilitySetDispatchQueue(reachability, .main),
SCNetworkReachabilitySetCallback(reachability, { reachability, currentState, weakWrapperPointer in
let weakWrapper = Unmanaged<WeakWrapper>.fromOpaque(weakWrapperPointer!).takeUnretainedValue()
guard let manager = weakWrapper.manager else { return }
manager.reachabilityChanged(newState: currentState)
}, &weakWrapperContext)
else {
owsFailDebug("failed to start notifier")
return
}
var initialState = SCNetworkReachabilityFlags()
if SCNetworkReachabilityGetFlags(reachability, &initialState) {
// Send an initial notification to anyone who may have registered *before* the app was ready.
reachabilityChanged(newState: initialState)
}
scheduleWakeupRequestIfNecessary()
}
private func scheduleWakeupRequestIfNecessary() {
AssertIsOnMainThread()
// Start a background session to wake the app when the network
// becomes available. We start this immediately when we lose
// connectivity rather than waiting until the app is backgrounded,
// because if started while backgrounded when the app is woken up
// will be at the OSes discretion.
guard !isReachable else { return }
Logger.info("Scheduling wakeup request for pending message sends.")
Task { [backgroundSession] in
do {
_ = try await backgroundSession.performDownload(TSConstants.mainServiceURL, method: .get)
Logger.info("Finished wakeup request.")
} catch {
Logger.warn("Failed wakeup request \(error)")
}
}
}
}
// MARK: -
private extension NetworkInterface {
var reachabilityType: ReachabilityType {
switch self {
case .cellular: return .cellular
case .wifi: return .wifi
}
}
}
// MARK: -
#if TESTABLE_BUILD
public class MockSSKReachabilityManager: SSKReachabilityManager {
public var isReachableViaWifi: Bool = false
public var isReachableViaCellular: Bool = false
public var isReachable: Bool {
return isReachableViaCellular || isReachableViaWifi
}
public func isReachable(via reachabilityType: ReachabilityType) -> Bool {
switch reachabilityType {
case .wifi: return isReachableViaWifi
case .cellular: return isReachableViaCellular
case .any: return isReachable
}
}
}
#endif