[Android] Add NetworkChange implementation using PeriodicTimer (#80548)
authorŠimon Rozsíval <simon@rozsival.com>
Thu, 19 Jan 2023 20:09:50 +0000 (21:09 +0100)
committerGitHub <noreply@github.com>
Thu, 19 Jan 2023 20:09:50 +0000 (21:09 +0100)
* Add NetworkChange implementation using PeriodicTimer

* Code cleanup

* Rename functions

* Update csproj

src/libraries/System.Net.NetworkInformation/src/System.Net.NetworkInformation.csproj
src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/NetworkAddressChange.Android.cs [new file with mode: 0644]

index a779454..e8621ab 100644 (file)
     <Compile Include="System\Net\NetworkInformation\AndroidIPInterfaceProperties.cs" />
     <Compile Include="System\Net\NetworkInformation\AndroidIPv4InterfaceProperties.cs" />
     <Compile Include="System\Net\NetworkInformation\AndroidIPv6InterfaceProperties.cs" />
-    <Compile Include="System\Net\NetworkInformation\NetworkInterfacePal.Android.cs" />
     <Compile Include="System\Net\NetworkInformation\AndroidNetworkInterface.cs" />
+    <Compile Include="System\Net\NetworkInformation\NetworkInterfacePal.Android.cs" />
+    <Compile Include="System\Net\NetworkInformation\NetworkAddressChange.Android.cs" />
   </ItemGroup>
   <!-- OSX -->
   <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'osx' or '$(TargetPlatformIdentifier)' == 'ios' or '$(TargetPlatformIdentifier)' == 'tvos' or '$(TargetPlatformIdentifier)' == 'freebsd'">
   <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'freebsd'">
     <Compile Include="$(CommonPath)Interop\FreeBSD\Interop.Libraries.cs" Link="Common\Interop\FreeBSD\Interop.Libraries.cs" />
   </ItemGroup>
-  <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'linux' or '$(TargetPlatformIdentifier)' == 'android' or '$(TargetPlatformIdentifier)' == 'freebsd'">
+  <ItemGroup Condition="'$(TargetPlatformIdentifier)' == 'linux' or '$(TargetPlatformIdentifier)' == 'freebsd'">
     <Compile Include="System\Net\NetworkInformation\NetworkAddressChange.Unix.cs" />
     <Compile Include="$(CommonPath)Interop\Unix\System.Native\Interop.NetworkChange.cs" Link="Common\Interop\Unix\System.Native\Interop.NetworkChange.cs" />
   </ItemGroup>
diff --git a/src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/NetworkAddressChange.Android.cs b/src/libraries/System.Net.NetworkInformation/src/System/Net/NetworkInformation/NetworkAddressChange.Android.cs
new file mode 100644 (file)
index 0000000..3bca706
--- /dev/null
@@ -0,0 +1,251 @@
+// Licensed to the .NET Foundation under one or more agreements.
+// The .NET Foundation licenses this file to you under the MIT license.
+
+using System.Collections.Generic;
+using System.Diagnostics;
+using System.Runtime.Versioning;
+using System.Threading;
+using System.Threading.Tasks;
+
+namespace System.Net.NetworkInformation
+{
+    public partial class NetworkChange
+    {
+        private static readonly TimeSpan s_timerInterval = TimeSpan.FromSeconds(2);
+        private static readonly object s_lockObj = new();
+
+        private static Task? s_loopTask;
+        private static CancellationTokenSource? s_cancellationTokenSource;
+        private static IPAddress[]? s_lastIpAddresses;
+
+        [UnsupportedOSPlatform("illumos")]
+        [UnsupportedOSPlatform("solaris")]
+        public static event NetworkAddressChangedEventHandler? NetworkAddressChanged
+        {
+            add
+            {
+                if (value != null)
+                {
+                    lock (s_lockObj)
+                    {
+                        if (s_addressChangedSubscribers.Count == 0 &&
+                            s_availabilityChangedSubscribers.Count == 0)
+                        {
+                            CreateAndStartLoop();
+                        }
+                        else
+                        {
+                            Debug.Assert(s_loopTask is not null);
+                        }
+
+                        s_addressChangedSubscribers.TryAdd(value, ExecutionContext.Capture());
+                    }
+                }
+            }
+            remove
+            {
+                if (value != null)
+                {
+                    lock (s_lockObj)
+                    {
+                        bool hadAddressChangedSubscribers = s_addressChangedSubscribers.Count != 0;
+                        s_addressChangedSubscribers.Remove(value);
+
+                        if (hadAddressChangedSubscribers && s_addressChangedSubscribers.Count == 0 &&
+                            s_availabilityChangedSubscribers.Count == 0)
+                        {
+                            StopLoop();
+                        }
+                    }
+                }
+            }
+        }
+
+        [UnsupportedOSPlatform("illumos")]
+        [UnsupportedOSPlatform("solaris")]
+        public static event NetworkAvailabilityChangedEventHandler? NetworkAvailabilityChanged
+        {
+            add
+            {
+                if (value != null)
+                {
+                    lock (s_lockObj)
+                    {
+                        if (s_addressChangedSubscribers.Count == 0 &&
+                            s_availabilityChangedSubscribers.Count == 0)
+                        {
+                            CreateAndStartLoop();
+                        }
+                        else
+                        {
+                            Debug.Assert(s_loopTask is not null);
+                        }
+
+                        s_availabilityChangedSubscribers.TryAdd(value, ExecutionContext.Capture());
+                    }
+                }
+            }
+            remove
+            {
+                if (value != null)
+                {
+                    lock (s_lockObj)
+                    {
+                        bool hadSubscribers = s_addressChangedSubscribers.Count != 0 ||
+                                              s_availabilityChangedSubscribers.Count != 0;
+                        s_availabilityChangedSubscribers.Remove(value);
+
+                        if (hadSubscribers && s_addressChangedSubscribers.Count == 0 &&
+                            s_availabilityChangedSubscribers.Count == 0)
+                        {
+                            StopLoop();
+                        }
+                    }
+                }
+            }
+        }
+
+        private static void CreateAndStartLoop()
+        {
+            Debug.Assert(s_cancellationTokenSource is null);
+            Debug.Assert(s_loopTask is null);
+
+            s_cancellationTokenSource = new CancellationTokenSource();
+            s_loopTask = Task.Run(() => PeriodicallyCheckIfNetworkChanged(s_cancellationTokenSource.Token));
+        }
+
+        private static void StopLoop()
+        {
+            Debug.Assert(s_cancellationTokenSource is not null);
+            Debug.Assert(s_loopTask is not null);
+
+            s_cancellationTokenSource.Cancel();
+
+            s_loopTask = null;
+            s_cancellationTokenSource = null;
+            s_lastIpAddresses = null;
+        }
+
+        private static async Task PeriodicallyCheckIfNetworkChanged(CancellationToken cancellationToken)
+        {
+            using var timer = new PeriodicTimer(s_timerInterval);
+
+            try
+            {
+                while (await timer.WaitForNextTickAsync(cancellationToken).ConfigureAwait(false)
+                    && !cancellationToken.IsCancellationRequested)
+                {
+                    CheckIfNetworkChanged();
+                }
+            }
+            catch (OperationCanceledException)
+            {
+            }
+        }
+
+        private static void CheckIfNetworkChanged()
+        {
+            var newAddresses = GetIPAddresses();
+            if (s_lastIpAddresses is IPAddress[] oldAddresses && NetworkChanged(oldAddresses, newAddresses))
+            {
+                OnNetworkChanged();
+            }
+
+            s_lastIpAddresses = newAddresses;
+        }
+
+        private static IPAddress[] GetIPAddresses()
+        {
+            var addresses = new List<IPAddress>();
+
+            var networkInterfaces = NetworkInterface.GetAllNetworkInterfaces();
+            foreach (var networkInterface in networkInterfaces)
+            {
+                var properties = networkInterface.GetIPProperties();
+                foreach (var addressInformation in properties.UnicastAddresses)
+                {
+                    addresses.Add(addressInformation.Address);
+                }
+            }
+
+            return addresses.ToArray();
+        }
+
+        private static bool NetworkChanged(IPAddress[] oldAddresses, IPAddress[] newAddresses)
+        {
+            if (oldAddresses.Length != newAddresses.Length)
+            {
+                return true;
+            }
+
+            for (int i = 0; i < newAddresses.Length; i++)
+            {
+                if (Array.IndexOf(oldAddresses, newAddresses[i]) == -1)
+                {
+                    return true;
+                }
+            }
+
+            return false;
+        }
+
+        private static void OnNetworkChanged()
+        {
+            Dictionary<NetworkAddressChangedEventHandler, ExecutionContext?>? addressChangedSubscribers = null;
+            Dictionary<NetworkAvailabilityChangedEventHandler, ExecutionContext?>? availabilityChangedSubscribers = null;
+
+            lock (s_lockObj)
+            {
+                if (s_addressChangedSubscribers.Count > 0)
+                {
+                    addressChangedSubscribers = new Dictionary<NetworkAddressChangedEventHandler, ExecutionContext?>(s_addressChangedSubscribers);
+                }
+                if (s_availabilityChangedSubscribers.Count > 0)
+                {
+                    availabilityChangedSubscribers = new Dictionary<NetworkAvailabilityChangedEventHandler, ExecutionContext?>(s_availabilityChangedSubscribers);
+                }
+            }
+
+            if (addressChangedSubscribers != null)
+            {
+                foreach (KeyValuePair<NetworkAddressChangedEventHandler, ExecutionContext?>
+                    subscriber in addressChangedSubscribers)
+                {
+                    NetworkAddressChangedEventHandler handler = subscriber.Key;
+                    ExecutionContext? ec = subscriber.Value;
+
+                    if (ec == null) // Flow suppressed
+                    {
+                        handler(null, EventArgs.Empty);
+                    }
+                    else
+                    {
+                        ExecutionContext.Run(ec, s_runAddressChangedHandler, handler);
+                    }
+                }
+            }
+
+            if (availabilityChangedSubscribers != null)
+            {
+                bool isAvailable = NetworkInterface.GetIsNetworkAvailable();
+                NetworkAvailabilityEventArgs args = isAvailable ? s_availableEventArgs : s_notAvailableEventArgs;
+                ContextCallback callbackContext = isAvailable ? s_runHandlerAvailable : s_runHandlerNotAvailable;
+                foreach (KeyValuePair<NetworkAvailabilityChangedEventHandler, ExecutionContext?>
+                    subscriber in availabilityChangedSubscribers)
+                {
+                    NetworkAvailabilityChangedEventHandler handler = subscriber.Key;
+                    ExecutionContext? ec = subscriber.Value;
+
+                    if (ec == null) // Flow suppressed
+                    {
+                        handler(null, args);
+                    }
+                    else
+                    {
+                        ExecutionContext.Run(ec, callbackContext, handler);
+                    }
+                }
+            }
+        }
+    }
+}