using System; using System.Collections.Generic; using System.Linq; using System.Threading; using System.Threading.Tasks; using Plugin.BLE; using Plugin.BLE.Abstractions; using Plugin.BLE.Abstractions.Contracts; using Serilog; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Services.BluetoothLock.Exception; using TINK.Services.BluetoothLock.Tdo; using Xamarin.Essentials; namespace TINK.Services.BluetoothLock.BLE { /// Manages ILockIt- Locks. public abstract class LockItByScanServiceBase : LockItServiceBase { /// Service to manage bluetooth stack. private IBluetoothLE BluetoothService { get; } /// Returns true if location permission is required (Android) but on given. private Func> IsLocationPermissionMissingDelegate { get; } /// Returns true if location service is required (Android) and off. private Func IsLocationRequiredAndOffDelegate { get; } private Func> AuthenticateDelegate { get; set; } public LockItByScanServiceBase( ICipher cipher, Func> authenticateDelegate, IBluetoothLE bluetoothLE, Func> isLocationPermissionMissingDelegate, Func isLocationRequiredAndOffDelegate) : base(cipher) { BluetoothService = bluetoothLE ?? throw new ArgumentException($"Can not instantiate {nameof(LockItByScanServiceBase)}- object. No bluetooth service available."); IsLocationPermissionMissingDelegate = isLocationPermissionMissingDelegate ?? throw new ArgumentException($"Can not instantiate {nameof(LockItByScanServiceBase)}- object. No location permission missing delegate available."); IsLocationRequiredAndOffDelegate = isLocationRequiredAndOffDelegate ?? throw new ArgumentException($"Can not instantiate {nameof(LockItByScanServiceBase)}- object. No location ."); AuthenticateDelegate = authenticateDelegate; } /// Connects to lock. /// Consists of a bluetooth connect plus invocation of an authentication sequence. /// Info required to connect to lock. /// Timeout for connect operation. public async Task ConnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout) { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not connect to lock by name. Platform must not be unknown and bluetooth code must be run on main thread."); } Log.ForContext().Debug($"Request to connect to device {authInfo.Id}."); var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id || x.Guid == authInfo.Guid); if (lockIt != null && lockIt.GetDeviceState() == DeviceState.Connected) { // Nothing to do Log.ForContext().Debug($"Nothing to do. Lock is already connected."); return await lockIt.GetLockStateAsync(); } if (lockIt != null) { // Reconnect required Log.ForContext().Debug($"Device {lockIt.Name} has been already connected but needs reconnect."); await lockIt.ReconnectAsync(authInfo, connectTimeout); return await lockIt.GetLockStateAsync(); } // Device has not yet been discovered. Log.ForContext().Verbose("Starting scan for new devices..."); var newlyDiscovertedDevice = await ScanForNewDevices(new List { authInfo.Id }, connectTimeout); var bleDevice = newlyDiscovertedDevice.FirstOrDefault(x => x.Name.GetBluetoothLockId() == authInfo.Id); if (bleDevice == null) { // Try to check why lock was not found. var bluetoothState = await BluetoothService.GetBluetoothState(); switch (bluetoothState) { case BluetoothState.On: break; case BluetoothState.Off: Log.ForContext().Debug("Lock probable not found because bluetooth is off."); throw new ConnectBluetoothNotOnException(); default: Log.ForContext().Debug("Lock probable not found because unexpected bluetooth state {state} detected.", bluetoothState); throw new ConnectBluetoothNotOnException(bluetoothState); } if (await IsLocationPermissionMissingDelegate()) { Log.ForContext().Debug("Lock probable not found because missing location permission."); throw new ConnectLocationPermissionMissingException(); } if (IsLocationRequiredAndOffDelegate()) { Log.ForContext().Debug("Lock probable not found because location is off."); throw new ConnectLocationOffException(); } Log.ForContext().Debug("Can not connect because device was not discovered. List of discovered devices {deviceList}.", String.Join(";", newlyDiscovertedDevice?.Select(x => x?.Id))); throw new OutOfReachException(); } var adapter = CrossBluetoothLE.Current.Adapter; var cts = new CancellationTokenSource(connectTimeout); // Connect to device and authenticate. Log.ForContext().Debug($"Device sucessfully discovered. Request connect to device {bleDevice?.Name}. Connect state is {bleDevice?.State}."); try { await adapter.ConnectToDeviceAsync( bleDevice, new ConnectParameters(forceBleTransport: true /* Force BLE transport */), cts.Token); } catch (System.Exception exception) { Log.ForContext().Error("Can not connect to device by name. {Exception}", exception); if (exception is TaskCanceledException) { // A timeout occurred. throw new System.Exception($"Can not connect to lock by name.\r\nTimeout is {connectTimeout.TotalMilliseconds} [ms].\r\n{exception.Message}", exception); } Log.ForContext().Error("Bluetooth connect to known device request failed. {Exception}", exception); throw new System.Exception($"Can not connect to lock by name. {exception.Message}", exception); } lockIt = await AuthenticateDelegate(bleDevice, authInfo, CrossBluetoothLE.Current.Adapter); if (lockIt == null) { await adapter.DisconnectDeviceAsync(bleDevice); return await Task.FromResult(null); } DeviceList.Add(lockIt); await lockIt.GetVersionInfoAsync(); return await lockIt.GetLockStateAsync(); } /// Checks for locks which have not yet been discoverted and connects them. /// Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be connected. /// Locks to reconnect. /// Timeout for connect operation of a single lock. protected override async Task> CheckConnectMissing(IEnumerable locksInfo, TimeSpan connectTimeout) { // Get list of target locks without invalid entries. var validLocksInfo = locksInfo .Where(x => x.IsIdValid) .Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0) .ToList(); // Get Locks to scan for var locksInfoToScanFor = validLocksInfo.Where(x => !DeviceList.Any(y => y.Name.GetBluetoothLockId() == x.Id)).ToList(); // Check if bluetooth scan is required if (locksInfoToScanFor.Count <= 0) { // No Scan required. return new List(); } // Do bluetooth scan var scanTargets = locksInfoToScanFor.Select(x => x.Id).ToList(); var newlyDiscovertedDevicesList = await ScanForNewDevices(scanTargets, TimeSpan.FromSeconds(connectTimeout.TotalSeconds * scanTargets.Count)); var locksList = new List(); // Connect to newly discovered devices. foreach (var lockInfo in locksInfoToScanFor) { var device = newlyDiscovertedDevicesList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == lockInfo.Id); if (device == null) { // No device found for given lock. continue; } Log.ForContext().Debug($"Request connect to device {device?.Name}. Connect state is {device?.State}."); var cts = new CancellationTokenSource(connectTimeout); try { // Connect to device and authenticate. await CrossBluetoothLE.Current.Adapter.ConnectToDeviceAsync( device, new ConnectParameters(forceBleTransport: true), // Force BLE transport cts.Token); } catch (System.Exception exception) { Log.ForContext().Error("Can not connect to lock. {Exception}", exception); continue; } LockItBase lockIt = null; try { lockIt = await AuthenticateDelegate(device, lockInfo, CrossBluetoothLE.Current.Adapter); } catch (System.Exception exception) { // Member is called for background update of missing devices. // Do not display any error messages. Log.ForContext().Error($"Authentication failed. {exception.Message}"); continue; } if (lockIt == null) { // connectiong to device succeded. continue; } Log.ForContext().Debug($"Auth succeeded for device {device}."); locksList.Add(lockIt); await lockIt.GetVersionInfoAsync(); } return locksList; } /// Scans for a given set of devices. /// Locks to scan for. /// private static async Task> ScanForNewDevices( IEnumerable targetLocks, TimeSpan connectTimeout) { var adapter = CrossBluetoothLE.Current.Adapter; var newlyDiscovertedDevicesList = new List(); var cts = new CancellationTokenSource(connectTimeout); // Not all devices already connected. // Start scanning adapter.DeviceDiscovered += (s, a) => { var name = a.Device.Name; if (string.IsNullOrEmpty(name)) { // Lock without name detected. Log.ForContext().Debug($"Ble decvice without name discovered. RSSI={a.Device.Rssi}, GUID={a.Device.Id}, State={a.Device.State}."); return; } if (targetLocks.Where(x => x != TextToLockItTypeHelper.INVALIDLOCKID).Contains(name.GetBluetoothLockId())) { Log.ForContext().Debug($"New LOCKIT device {name} discovered. RSSI={a.Device.Rssi}, GUID={a.Device.Id}, State={a.Device.State}."); newlyDiscovertedDevicesList.Add(a.Device); if (newlyDiscovertedDevicesList.Count() >= targetLocks.Count()) { cts.Cancel(); } return; } Log.ForContext().Verbose($"Device of unknown advertisement name {name} discovered. RSSI={a.Device.Rssi}, GUID={a.Device.Id}, State={a.Device.State}."); }; try { await adapter.StartScanningForDevicesAsync(cancellationToken: cts.Token); } catch (System.Exception exception) { Log.ForContext().Error("Bluetooth scan failed. {Exception}", exception); throw; } finally { try { await adapter.StopScanningForDevicesAsync(); } catch (System.Exception exception) { Log.ForContext().Error("Stop scanning failed. {Excpetion}", exception); throw; } } return newlyDiscovertedDevicesList; } } }