2022-08-30 15:42:25 +02:00
using System ;
using System.Collections.Generic ;
using System.Linq ;
using System.Threading ;
2021-05-13 17:25:46 +02:00
using System.Threading.Tasks ;
using Plugin.BLE ;
2022-08-30 15:42:25 +02:00
using Plugin.BLE.Abstractions ;
2021-05-13 17:25:46 +02:00
using Plugin.BLE.Abstractions.Contracts ;
using Serilog ;
using TINK.Model.Connector ;
using TINK.Model.Device ;
2022-08-30 15:42:25 +02:00
using TINK.Services.BluetoothLock.Exception ;
using TINK.Services.BluetoothLock.Tdo ;
using Xamarin.Essentials ;
2021-05-13 17:25:46 +02:00
namespace TINK.Services.BluetoothLock.BLE
{
/// <summary> Manages ILockIt- Locks.</summary>
public abstract class LockItByScanServiceBase : LockItServiceBase
{
2022-04-10 17:38:34 +02:00
/// <summary> Service to manage bluetooth stack. </summary>
private IBluetoothLE BluetoothService { get ; }
/// <summary> Returns true if location permission is required (Android) but on given. </summary>
2022-08-30 15:42:25 +02:00
private Func < Task < bool > > IsLocationPermissionMissingDelegate { get ; }
2022-04-10 17:38:34 +02:00
/// <summary> Returns true if location service is required (Android) and off. </summary>
private Func < bool > IsLocationRequiredAndOffDelegate { get ; }
2022-08-30 15:42:25 +02:00
private Func < IDevice , LockInfoAuthTdo , IAdapter , Task < LockItBase > > AuthenticateDelegate { get ; set ; }
2021-05-13 17:25:46 +02:00
public LockItByScanServiceBase (
ICipher cipher ,
2022-04-10 17:38:34 +02:00
Func < IDevice , LockInfoAuthTdo , IAdapter , Task < LockItBase > > authenticateDelegate ,
IBluetoothLE bluetoothLE ,
Func < Task < bool > > isLocationPermissionMissingDelegate ,
2022-08-30 15:42:25 +02:00
Func < bool > isLocationRequiredAndOffDelegate ) : base ( cipher )
2021-05-13 17:25:46 +02:00
{
2022-04-10 17:38:34 +02:00
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 ." ) ;
2021-05-13 17:25:46 +02:00
AuthenticateDelegate = authenticateDelegate ;
}
/// <summary> Connects to lock.</summary>
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks>
/// <param name="authInfo"> Info required to connect to lock.</param>
/// <param name="connectTimeout">Timeout for connect operation.</param>
public async Task < LockInfoTdo > ConnectAsync ( LockInfoAuthTdo authInfo , TimeSpan connectTimeout )
{
if ( DeviceInfo . Platform ! = DevicePlatform . Unknown & & MainThread . IsMainThread = = false )
{
2022-08-30 15:42:25 +02:00
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." ) ;
2021-05-13 17:25:46 +02:00
}
Log . ForContext < LockItByScanServiceBase > ( ) . 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
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( $"Nothing to do. Lock is already connected." ) ;
2021-05-13 17:25:46 +02:00
return await lockIt . GetLockStateAsync ( ) ;
}
if ( lockIt ! = null )
{
// Reconnect required
Log . ForContext < LockItByScanServiceBase > ( ) . 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.
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Verbose ( "Starting scan for new devices..." ) ;
2021-05-13 17:25:46 +02:00
var newlyDiscovertedDevice = await ScanForNewDevices ( new List < int > { authInfo . Id } , connectTimeout ) ;
var bleDevice = newlyDiscovertedDevice . FirstOrDefault ( x = > x . Name . GetBluetoothLockId ( ) = = authInfo . Id ) ;
if ( bleDevice = = null )
{
2022-04-10 17:38:34 +02:00
// Try to check why lock was not found.
var bluetoothState = await BluetoothService . GetBluetoothState ( ) ;
switch ( bluetoothState )
{
case BluetoothState . On :
break ;
case BluetoothState . Off :
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( "Lock probable not found because bluetooth is off." ) ;
2022-04-10 17:38:34 +02:00
throw new ConnectBluetoothNotOnException ( ) ;
default :
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( "Lock probable not found because unexpected bluetooth state {state} detected." , bluetoothState ) ;
2022-04-10 17:38:34 +02:00
throw new ConnectBluetoothNotOnException ( bluetoothState ) ;
}
if ( await IsLocationPermissionMissingDelegate ( ) )
{
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( "Lock probable not found because missing location permission." ) ;
2022-04-10 17:38:34 +02:00
throw new ConnectLocationPermissionMissingException ( ) ;
}
if ( IsLocationRequiredAndOffDelegate ( ) )
{
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( "Lock probable not found because location is off." ) ;
2022-04-10 17:38:34 +02:00
throw new ConnectLocationOffException ( ) ;
}
2021-05-13 17:25:46 +02:00
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( "Can not connect because device was not discovered. List of discovered devices {deviceList}." , String . Join ( ";" , newlyDiscovertedDevice ? . Select ( x = > x ? . Id ) ) ) ;
2021-05-13 17:25:46 +02:00
throw new OutOfReachException ( ) ;
}
var adapter = CrossBluetoothLE . Current . Adapter ;
var cts = new CancellationTokenSource ( connectTimeout ) ;
// Connect to device and authenticate.
Log . ForContext < LockItByScanServiceBase > ( ) . 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 )
{
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Error ( "Can not connect to device by name. {Exception}" , exception ) ;
2021-05-13 17:25:46 +02:00
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 < LockItByScanServiceBase > ( ) . 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 < LockInfoTdo > ( null ) ;
}
DeviceList . Add ( lockIt ) ;
return await lockIt . GetLockStateAsync ( ) ;
}
/// <summary> Checks for locks which have not yet been discoverted and connects them. </summary>
/// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be connected. </remarks>
/// <param name="locksInfo">Locks to reconnect.</param>
/// <param name="connectTimeout">Timeout for connect operation of a single lock.</param>
protected override async Task < IEnumerable < ILockService > > CheckConnectMissing ( IEnumerable < LockInfoAuthTdo > 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 < LockItBase > ( ) ;
}
// 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 < LockItBase > ( ) ;
// Connect to newly discovered devices.
foreach ( var lockInfo in locksInfoToScanFor )
{
2022-08-30 15:42:25 +02:00
var device = newlyDiscovertedDevicesList . FirstOrDefault ( x = > x . Name . GetBluetoothLockId ( ) = = lockInfo . Id ) ;
2021-05-13 17:25:46 +02:00
if ( device = = null )
{
// No device found for given lock.
continue ;
}
Log . ForContext < LockItByScanServiceBase > ( ) . 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 )
{
2022-08-30 15:42:25 +02:00
Log . ForContext < LockItByScanServiceBase > ( ) . Error ( "Can not connect to lock. {Exception}" , exception ) ;
2021-05-13 17:25:46 +02:00
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 < LockItByScanServiceBase > ( ) . Error ( $"Authentication failed. {exception.Message}" ) ;
continue ;
}
if ( lockIt = = null )
{
// connectiong to device succeded.
continue ;
}
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( $"Auth succeeded for device {device}." ) ;
locksList . Add ( lockIt ) ;
}
return locksList ;
}
/// <summary> Scans for a given set of devices.</summary>
/// <param name="targetLocks">Locks to scan for.</param>
/// <returns></returns>
private static async Task < IEnumerable < IDevice > > ScanForNewDevices (
IEnumerable < int > targetLocks ,
TimeSpan connectTimeout )
{
var adapter = CrossBluetoothLE . Current . Adapter ;
var newlyDiscovertedDevicesList = new List < IDevice > ( ) ;
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 < LockItByScanServiceBase > ( ) . Debug ( $"Lock without name discovered." ) ;
return ;
}
if ( targetLocks . Where ( x = > x ! = TextToLockItTypeHelper . INVALIDLOCKID ) . Contains ( name . GetBluetoothLockId ( ) ) )
{
Log . ForContext < LockItByScanServiceBase > ( ) . Debug ( $"New LOCKIT device {name} discovered." ) ;
newlyDiscovertedDevicesList . Add ( a . Device ) ;
if ( newlyDiscovertedDevicesList . Count ( ) > = targetLocks . Count ( ) )
{
cts . Cancel ( ) ;
}
return ;
}
Log . ForContext < LockItByScanServiceBase > ( ) . Verbose ( $"Device of unknown advertisement name {name} discovered." ) ;
} ;
try
{
await adapter . StartScanningForDevicesAsync ( cancellationToken : cts . Token ) ;
}
catch ( System . Exception exception )
{
Log . ForContext < LockItByScanServiceBase > ( ) . Error ( "Bluetooth scan failed. {Exception}" , exception ) ;
throw ;
}
finally
{
try
{
await adapter . StopScanningForDevicesAsync ( ) ;
}
catch ( System . Exception exception )
{
Log . ForContext < LockItByScanServiceBase > ( ) . Error ( "Stop scanning failed. {Excpetion}" , exception ) ;
throw ;
}
}
2022-08-30 15:42:25 +02:00
2021-05-13 17:25:46 +02:00
return newlyDiscovertedDevicesList ;
}
}
}