diff --git a/InTheHand.BluetoothLE/Platforms/Android/RemoteGattServer.android.cs b/InTheHand.BluetoothLE/Platforms/Android/RemoteGattServer.android.cs index 6c2b5ba..ff3783d 100644 --- a/InTheHand.BluetoothLE/Platforms/Android/RemoteGattServer.android.cs +++ b/InTheHand.BluetoothLE/Platforms/Android/RemoteGattServer.android.cs @@ -179,6 +179,11 @@ namespace InTheHand.Bluetooth _gatt.Disconnect(); } + void PlatformCleanup() + { + // Android has no explicit cleanup 🤪 + } + async Task PlatformGetPrimaryService(BluetoothUuid service) { await WaitForServiceDiscovery(); diff --git a/InTheHand.BluetoothLE/Platforms/Apple/RemoteGattServer.unified.cs b/InTheHand.BluetoothLE/Platforms/Apple/RemoteGattServer.unified.cs index c4868fe..1d999e6 100644 --- a/InTheHand.BluetoothLE/Platforms/Apple/RemoteGattServer.unified.cs +++ b/InTheHand.BluetoothLE/Platforms/Apple/RemoteGattServer.unified.cs @@ -102,6 +102,10 @@ namespace InTheHand.Bluetooth { } + void PlatformCleanup() + { + } + Task PlatformGetPrimaryService(BluetoothUuid service) { return Task.Run(() => diff --git a/InTheHand.BluetoothLE/Platforms/Standard/RemoteGattServer.standard.cs b/InTheHand.BluetoothLE/Platforms/Standard/RemoteGattServer.standard.cs index 7b8ae98..54bfe59 100644 --- a/InTheHand.BluetoothLE/Platforms/Standard/RemoteGattServer.standard.cs +++ b/InTheHand.BluetoothLE/Platforms/Standard/RemoteGattServer.standard.cs @@ -29,6 +29,10 @@ namespace InTheHand.Bluetooth { } + void PlatformCleanup() + { + } + Task PlatformGetPrimaryService(BluetoothUuid service) { return Task.FromResult((GattService)null); diff --git a/InTheHand.BluetoothLE/Platforms/Windows/BluetoothDevice.windows.cs b/InTheHand.BluetoothLE/Platforms/Windows/BluetoothDevice.windows.cs index 283434d..e90d03a 100644 --- a/InTheHand.BluetoothLE/Platforms/Windows/BluetoothDevice.windows.cs +++ b/InTheHand.BluetoothLE/Platforms/Windows/BluetoothDevice.windows.cs @@ -6,6 +6,8 @@ //----------------------------------------------------------------------- using System; +using System.Collections.Concurrent; +using System.Collections.Generic; using System.Threading.Tasks; using Windows.Devices.Bluetooth; @@ -14,15 +16,101 @@ namespace InTheHand.Bluetooth partial class BluetoothDevice { internal BluetoothLEDevice NativeDevice; + internal readonly ConcurrentDictionary NativeDisposeList = new ConcurrentDictionary(); + private string _cachedId; + private string _cachedName; + internal ulong LastKnownAddress; internal BluetoothDevice(BluetoothLEDevice device) { NativeDevice = device; + AddDisposableObject(this, device); + + + if (device == null) return; + + LastKnownAddress = device.BluetoothAddress; + + // this will cache the Id and Name for use in cases where 'NativeDevice' has been disposed. + GetId(); + GetName(); } ~BluetoothDevice() { - NativeDevice.Dispose(); + DisposeAllNativeObjects(); + } + + /// Adds a native (IDisposable) object to the dispose list + /// This is generally the managed object that contains the native object but it can be anything that can serve as a unique key. + /// The native object that we will disposed when the user requests a disconnect + internal void AddDisposableObject(object container, IDisposable disposableObject) + { + if (NativeDisposeList.TryGetValue(container.GetHashCode(), out IDisposable existingValue)) + { + NativeDisposeList.TryUpdate(container.GetHashCode(), disposableObject, existingValue); + } + else + { + NativeDisposeList.TryAdd(container.GetHashCode(), disposableObject); + } + } + + /// Called in RemoteServer.PlatformCleanup to dispose all of the native object that have been collected. + internal void DisposeAllNativeObjects() + { + Dictionary itemsDisposed = new Dictionary(); + foreach (var kv in NativeDisposeList) + { + kv.Value?.Dispose(); + itemsDisposed.Add(kv.Key, kv.Value); + } + + foreach (var kv in itemsDisposed) + { + NativeDisposeList.TryUpdate(kv.Key, null, kv.Value); + } + } + + /// + /// Used to Recreate 'NativeDevice' if it is in a disposed state. + /// + /// The bluetooth device address + /// True if 'NativeDevice' was recreated + internal async Task CreateNativeInstance() + { + if (LastKnownAddress == 0) return false; + + if (NativeDisposeList.TryGetValue(GetHashCode(), out IDisposable existingItem)) + { + if (existingItem == null) + { + // The native object was disposed as the result of a call to RemoteGattServer.Disconnect. + // we need to create another one. + BluetoothLEDevice nativeDevice = await BluetoothLEDevice.FromBluetoothAddressAsync(LastKnownAddress); + if (nativeDevice != null) + { + AddDisposableObject(this, nativeDevice); + NativeDevice = nativeDevice; + return true; + } + } + } + + return false; + } + + /// Checks if the native object for this container has been disposed. + /// This is generally the managed object that contains the native object but it can be anything that can serve as a unique key. + /// True if the container exists and it's native object has been disposed. + internal bool IsDisposedItem(object container) + { + if (NativeDisposeList.TryGetValue(container.GetHashCode(), out IDisposable existingItem)) + { + return existingItem == null; + } + + return false; } public static implicit operator BluetoothLEDevice(BluetoothDevice device) @@ -63,24 +151,34 @@ namespace InTheHand.Bluetooth return device; } - string GetId() + internal string GetId() { - return NativeDevice.BluetoothAddress.ToString("X6"); + if (IsDisposedItem(this)) return _cachedId; + _cachedId = NativeDevice.BluetoothAddress.ToString("X6"); + return _cachedId; } - string GetName() + internal string GetName() { - if(NativeDevice.Name.StartsWith("Bluetooth ")) + if (IsDisposedItem(this)) return _cachedName; + if (NativeDevice.Name.StartsWith("Bluetooth ")) { - return NativeDevice.DeviceInformation.Name; + _cachedName = NativeDevice.DeviceInformation.Name; + } + else + { + _cachedName = NativeDevice.Name; } - return NativeDevice.Name; + return _cachedName; } + private RemoteGattServer _remoteGattServer; RemoteGattServer GetGatt() { - return new RemoteGattServer(this); + if (_remoteGattServer == null) + _remoteGattServer = new RemoteGattServer(this); + return _remoteGattServer; } /*bool GetWatchingAdvertisements() diff --git a/InTheHand.BluetoothLE/Platforms/Windows/GattService.windows.cs b/InTheHand.BluetoothLE/Platforms/Windows/GattService.windows.cs index e99ecfe..05ea107 100644 --- a/InTheHand.BluetoothLE/Platforms/Windows/GattService.windows.cs +++ b/InTheHand.BluetoothLE/Platforms/Windows/GattService.windows.cs @@ -22,6 +22,7 @@ namespace InTheHand.Bluetooth { _service = service; _isPrimary = isPrimary; + device.AddDisposableObject(this,service); } public static implicit operator WBluetooth.GattDeviceService(GattService service) diff --git a/InTheHand.BluetoothLE/Platforms/Windows/RemoteGattServer.windows.cs b/InTheHand.BluetoothLE/Platforms/Windows/RemoteGattServer.windows.cs index c35c5a2..c4a5f35 100644 --- a/InTheHand.BluetoothLE/Platforms/Windows/RemoteGattServer.windows.cs +++ b/InTheHand.BluetoothLE/Platforms/Windows/RemoteGattServer.windows.cs @@ -27,20 +27,35 @@ namespace InTheHand.Bluetooth bool GetConnected() { + if (Device.IsDisposedItem(Device)) return false; return Device.NativeDevice.ConnectionStatus == Windows.Devices.Bluetooth.BluetoothConnectionStatus.Connected; } async Task PlatformConnect() { + // Ensure that our native objects have not been disposed. + // If they have, re-create the native device object. + if (await Device.CreateNativeInstance()) PlatformInit(); + var status = await Device.NativeDevice.RequestAccessAsync(); if(status == Windows.Devices.Enumeration.DeviceAccessStatus.Allowed) { + Device.LastKnownAddress = Device.NativeDevice.BluetoothAddress; var session = await Windows.Devices.Bluetooth.GenericAttributeProfile.GattSession.FromDeviceIdAsync(Device.NativeDevice.BluetoothDeviceId); if(session != null) + { + // Even though this is a local variable, we still want to add it to our dispose list so + // we don't have to rely on the GC to clean it up. + Device.AddDisposableObject(this, session); session.MaintainConnection = true; + } // need to request something to force a connection - await Device.NativeDevice.GetGattServicesAsync(cacheMode: Windows.Devices.Bluetooth.BluetoothCacheMode.Uncached); + var services = await Device.NativeDevice.GetGattServicesAsync(cacheMode: Windows.Devices.Bluetooth.BluetoothCacheMode.Uncached); + foreach (var service in services.Services) + { + service.Dispose(); + } } else { @@ -53,8 +68,25 @@ namespace InTheHand.Bluetooth // Windows has no explicit disconnect 🤪 } + void PlatformCleanup() + { + // The user has explicitly called the Disconnect method so unhook ConnectionStatusChanged + // and dispose all of the native windows bluetooth objects. This will release the device + // so that it can be used by another application or re-connected by the current + // application. + if (Device.NativeDisposeList.TryGetValue(Device.GetHashCode(), out IDisposable existingDevice)) + { + if (existingDevice != null) + { + Device.NativeDevice.ConnectionStatusChanged -= NativeDevice_ConnectionStatusChanged; + Device.DisposeAllNativeObjects(); + } + } + } + async Task PlatformGetPrimaryService(BluetoothUuid service) { + if (await Device.CreateNativeInstance()) PlatformInit(); var result = await Device.NativeDevice.GetGattServicesForUuidAsync(service, Windows.Devices.Bluetooth.BluetoothCacheMode.Uncached); if (result == null || result.Services.Count == 0) @@ -65,6 +97,7 @@ namespace InTheHand.Bluetooth async Task> PlatformGetPrimaryServices(BluetoothUuid? service) { + if (await Device.CreateNativeInstance()) PlatformInit(); var services = new List(); var nativeDevice = Device.NativeDevice; Windows.Devices.Bluetooth.BluetoothCacheMode cacheMode = nativeDevice.DeviceInformation.Pairing.IsPaired ? Windows.Devices.Bluetooth.BluetoothCacheMode.Cached : Windows.Devices.Bluetooth.BluetoothCacheMode.Uncached; diff --git a/InTheHand.BluetoothLE/RemoteGattServer.cs b/InTheHand.BluetoothLE/RemoteGattServer.cs index 8652a63..4a21824 100644 --- a/InTheHand.BluetoothLE/RemoteGattServer.cs +++ b/InTheHand.BluetoothLE/RemoteGattServer.cs @@ -56,6 +56,7 @@ namespace InTheHand.Bluetooth { PlatformDisconnect(); Device.OnGattServerDisconnected(); + PlatformCleanup(); } ///