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);
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);
}
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;
}
}
}