Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -9,7 +9,6 @@ namespace Microsoft.Maui.Networking
partial class ConnectivityImplementation : IConnectivity
{
#if !(MACCATALYST || MACOS)
// TODO: Use NWPathMonitor on > iOS 12
#pragma warning disable BI1234, CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
static readonly Lazy<CTCellularData> cellularData = new Lazy<CTCellularData>(() => new CTCellularData());

Expand Down Expand Up @@ -41,7 +40,6 @@ public NetworkAccess NetworkAccess
{
var restricted = false;
#if !(MACCATALYST || MACOS)
// TODO: Use NWPathMonitor on > iOS 12
#pragma warning disable BI1234, CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
restricted = CellularData.RestrictedState == CTCellularDataRestrictedState.Restricted;
#pragma warning restore BI1234, CA1416
Expand Down
Original file line number Diff line number Diff line change
@@ -1,12 +1,12 @@
using System;
using System.Collections.Generic;
using System.Net;
using System.Threading;
using System.Threading.Tasks;
#if !(MACCATALYST || MACOS)
using CoreTelephony;
#endif
using CoreFoundation;
using SystemConfiguration;
using Network;

namespace Microsoft.Maui.Networking
{
Expand All @@ -19,144 +19,102 @@ enum NetworkStatus

static class Reachability
{
internal const string HostName = "www.microsoft.com";
static NWPathMonitor sharedMonitor;
static readonly object monitorLock = new object();
static readonly ManualResetEventSlim initEvent = new ManualResetEventSlim(false);

internal static NetworkStatus RemoteHostStatus()
static NWPathMonitor SharedMonitor
{
using (var remoteHostReachability = new NetworkReachability(HostName))
get
{
var reachable = remoteHostReachability.TryGetFlags(out var flags);

if (!reachable)
return NetworkStatus.NotReachable;

if (!IsReachableWithoutRequiringConnection(flags))
return NetworkStatus.NotReachable;

#if __IOS__
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
return NetworkStatus.ReachableViaCarrierDataNetwork;
#endif

return NetworkStatus.ReachableViaWiFiNetwork;
lock (monitorLock)
{
if (sharedMonitor == null)
{
sharedMonitor = new NWPathMonitor();
sharedMonitor.SnapshotHandler = _ => initEvent.Set();
sharedMonitor.SetQueue(DispatchQueue.DefaultGlobalQueue);
sharedMonitor.Start();
}
}
// Wait for the first path update to ensure CurrentPath is available.
// ManualResetEventSlim stays signaled once Set(), so subsequent calls return immediately.
initEvent.Wait(TimeSpan.FromSeconds(5));
return sharedMonitor;
}
}

internal static NetworkStatus InternetConnectionStatus()
static NWPath GetCurrentPath()
{
var status = NetworkStatus.NotReachable;
var monitor = SharedMonitor;
return monitor?.CurrentPath;
}

var defaultNetworkAvailable = IsNetworkAvailable(out var flags);
static NetworkStatus GetNetworkStatus()
{
var path = GetCurrentPath();
if (path == null || path.Status != NWPathStatus.Satisfied)
return NetworkStatus.NotReachable;

#if __IOS__
// If it's a WWAN connection..
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
status = NetworkStatus.ReachableViaCarrierDataNetwork;
if (path.UsesInterfaceType(NWInterfaceType.Cellular))
return NetworkStatus.ReachableViaCarrierDataNetwork;
#endif

// If the connection is reachable and no connection is required, then assume it's WiFi
if (defaultNetworkAvailable)
{
status = NetworkStatus.ReachableViaWiFiNetwork;
}
return NetworkStatus.ReachableViaWiFiNetwork;
}

// If the connection is on-demand or on-traffic and no user intervention
// is required, then assume WiFi.
if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) &&
(flags & NetworkReachabilityFlags.InterventionRequired) == 0)
{
status = NetworkStatus.ReachableViaWiFiNetwork;
}
// RemoteHostStatus and InternetConnectionStatus previously had different
// implementations (DNS probe to www.microsoft.com vs default route check).
// With NWPathMonitor they share the same underlying path status.
internal static NetworkStatus RemoteHostStatus() => GetNetworkStatus();

return status;
}
internal static NetworkStatus InternetConnectionStatus() => GetNetworkStatus();

internal static IEnumerable<NetworkStatus> GetActiveConnectionType()
{
var status = new List<NetworkStatus>();
var path = GetCurrentPath();

var defaultNetworkAvailable = IsNetworkAvailable(out var flags);
if (path == null || path.Status != NWPathStatus.Satisfied)
return status;

#if __IOS__
// If it's a WWAN connection.
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
if (path.UsesInterfaceType(NWInterfaceType.Cellular))
{
status.Add(NetworkStatus.ReachableViaCarrierDataNetwork);
}
else if (defaultNetworkAvailable)
else if (path.UsesInterfaceType(NWInterfaceType.Wifi) || path.UsesInterfaceType(NWInterfaceType.Wired))
#else
// If the connection is reachable and no connection is required, then assume it's WiFi
if (defaultNetworkAvailable)
if (path.UsesInterfaceType(NWInterfaceType.Wifi) || path.UsesInterfaceType(NWInterfaceType.Wired))
#endif
{
status.Add(NetworkStatus.ReachableViaWiFiNetwork);
}
else if (((flags & NetworkReachabilityFlags.ConnectionOnDemand) != 0 || (flags & NetworkReachabilityFlags.ConnectionOnTraffic) != 0) &&
(flags & NetworkReachabilityFlags.InterventionRequired) == 0)
{
// If the connection is on-demand or on-traffic and no user intervention
// is required, then assume WiFi.
status.Add(NetworkStatus.ReachableViaWiFiNetwork);
}

return status;
}

internal static bool IsNetworkAvailable(out NetworkReachabilityFlags flags)
internal static bool IsNetworkAvailable()
{
var ip = new IPAddress(0);
using (var defaultRouteReachability = new NetworkReachability(ip))
{
if (!defaultRouteReachability.TryGetFlags(out flags))
return false;

return IsReachableWithoutRequiringConnection(flags);
}
}

internal static bool IsReachableWithoutRequiringConnection(NetworkReachabilityFlags flags)
{
// Is it reachable with the current network configuration?
var isReachable = (flags & NetworkReachabilityFlags.Reachable) != 0;

// Do we need a connection to reach it?
var noConnectionRequired = (flags & NetworkReachabilityFlags.ConnectionRequired) == 0;

#if __IOS__
// Since the network stack will automatically try to get the WAN up,
// probe that
if ((flags & NetworkReachabilityFlags.IsWWAN) != 0)
noConnectionRequired = true;
#endif

return isReachable && noConnectionRequired;
var path = GetCurrentPath();
return path != null && path.Status == NWPathStatus.Satisfied;
}
}

class ReachabilityListener : IDisposable
{
NetworkReachability defaultRouteReachability;
NetworkReachability remoteHostReachability;
// Delay to allow connection status to stabilize before notifying listeners
const int ConnectionStatusChangeDelayMs = 100;

NWPathMonitor pathMonitor;

internal ReachabilityListener()
{
var ip = new IPAddress(0);
defaultRouteReachability = new NetworkReachability(ip);
#pragma warning disable CA1422 // obsolete in MacCatalyst 15, iOS 13
defaultRouteReachability.SetNotification(OnChange);
defaultRouteReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault);
#pragma warning restore CA1422

remoteHostReachability = new NetworkReachability(Reachability.HostName);

// Need to probe before we queue, or we wont get any meaningful values
// this only happens when you create NetworkReachability from a hostname
remoteHostReachability.TryGetFlags(out var flags);

#pragma warning disable CA1422 // obsolete in MacCatalyst 15, iOS 13
remoteHostReachability.SetNotification(OnChange);
remoteHostReachability.Schedule(CFRunLoop.Main, CFRunLoop.ModeDefault);
#pragma warning restore CA1422
pathMonitor = new NWPathMonitor();
pathMonitor.SnapshotHandler = OnPathUpdate;
pathMonitor.SetQueue(DispatchQueue.DefaultGlobalQueue);
pathMonitor.Start();

#if !(MACCATALYST || MACOS)
#pragma warning disable BI1234, CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
Expand All @@ -171,10 +129,13 @@ internal ReachabilityListener()

internal void Dispose()
{
defaultRouteReachability?.Dispose();
defaultRouteReachability = null;
remoteHostReachability?.Dispose();
remoteHostReachability = null;
if (pathMonitor != null)
{
pathMonitor.SnapshotHandler = null;
pathMonitor.Cancel();
pathMonitor.Dispose();
pathMonitor = null;
}

#if !(MACCATALYST || MACOS)
#pragma warning disable CA1416 // Analyzer bug https://github.com/dotnet/roslyn-analyzers/issues/5938
Expand All @@ -183,6 +144,20 @@ internal void Dispose()
#endif
}

async void OnPathUpdate(NWPath path)
{
try
{
// Add in artificial delay so the connection status has time to change
await Task.Delay(ConnectionStatusChangeDelayMs);
ReachabilityChanged?.Invoke();
}
catch (Exception ex)
{
System.Diagnostics.Debug.WriteLine($"ReachabilityListener handler failed: {ex}");
}
}

#if !(MACCATALYST || MACOS)
#pragma warning disable BI1234
void OnRestrictedStateChanged(CTCellularDataRestrictedState state)
Expand All @@ -191,14 +166,5 @@ void OnRestrictedStateChanged(CTCellularDataRestrictedState state)
}
#pragma warning restore BI1234
#endif

async void OnChange(NetworkReachabilityFlags flags)
{
// Add in artifical delay so the connection status has time to change
// else it will return true no matter what.
await Task.Delay(100);

ReachabilityChanged?.Invoke();
}
}
}
Loading