using System.Collections.Generic; using System.Threading.Tasks; using System.Linq; using TINK.Services.BluetoothLock.Tdo; using TINK.Model.Connector; using System; using Serilog; using TINK.Model.Device; using Xamarin.Essentials; using TINK.Model.Bike.BluetoothLock; namespace TINK.Services.BluetoothLock.BLE { public abstract class LockItServiceBase { /// <summary>Constructs base object.</summary> /// <param name="cipher">Encrpyting/ decrypting object.</param> /// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock.</param> public LockItServiceBase(ICipher cipher) { Cipher = cipher; } /// <summary> Holds timeout values for series of connecting attemps to a lock or multiple locks. </summary> public ITimeOutProvider TimeOut { get; set; } protected ICipher Cipher { get; } /// <summary> List of available or connected bluetooth devices. </summary> protected List<ILockService> DeviceList = new List<ILockService>(); /// <summary> Reconnect locks of interest if required. </summary> /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence for each lock to be reconnected. </remarks> /// <param name="locksInfo">Locks to reconnect.</param> /// <param name="connectTimeout">Timeout for connect operation.</param> public static async Task CheckReconnect( IEnumerable<ILockService> deviceList, IEnumerable<LockInfoAuthTdo> locksInfo, TimeSpan connectTimeout) { // Get list of target locks without invalid entries. var validLocksInfo = locksInfo .Where(x => x.IsIdValid || x.IsGuidValid) .Where(x => x.K_seed.Length > 0 && x.K_u.Length > 0) .ToList(); foreach (var device in deviceList) { if (device.GetDeviceState() == DeviceState.Connected) { // No need to reconnect device because device is already connected. continue; } var lockInfo = validLocksInfo.FirstOrDefault(x => x.Id == device. Name.GetBluetoothLockId() || x.Guid == device.Guid); if (lockInfo == null) { // Current lock from deviceList is not of interest detected, no need to reconnect. continue; } try { await device.ReconnectAsync(lockInfo, connectTimeout); } catch (System.Exception exception) { // Member is called for background update of missing devices. // Do not display any error messages. Log.ForContext<LockItServiceBase>().Error($"Reconnect failed. {exception.Message}"); continue; } } } /// <summary> Gets the state for locks of interest.</summary> /// <remarks> /// Might require a /// - connect to lock /// - reconnect to lock /// </remarks> /// <param name="locksInfo">Locks toget info for.</param> /// <param name="connectTimeout">Timeout for connect operation of a single lock.</param> public async Task<IEnumerable<LockInfoTdo>> GetLocksStateAsync( IEnumerable<LockInfoAuthTdo> locksInfo, TimeSpan connectTimeout) { if (locksInfo.Count() == 0) { // Nothing to do. return new List<LockInfoTdo>(); } // Reconnect locks. await CheckReconnect(DeviceList, locksInfo, connectTimeout); // Connect to locks which were not yet discovered. DeviceList.AddRange(await CheckConnectMissing(locksInfo, connectTimeout)); // Get devices for which to update state. var locksInfoState = new List<LockInfoTdo>(); foreach (var device in DeviceList) { var lockInfoAuth = locksInfo.FirstOrDefault(x => x.Guid == device.Guid || x.Id == device.Id); if (lockInfoAuth == null) { // Device is not of interest. continue; } var state = await device.GetLockStateAsync(); locksInfoState.Add(new LockInfoTdo.Builder { Id = lockInfoAuth.Id, Guid = lockInfoAuth.IsGuidValid ? lockInfoAuth.Guid : device.Guid, State = state?.State }.Build()); } return locksInfoState; } /// <remarks> Consists of a bluetooth connect plus invocation of an authentication sequence. </remarks> /// <param name="locksInfo">Locks to reconnect.</param> /// <param name="connectTimeout">Timeout for connect operation of a single lock.</param> protected abstract Task<IEnumerable<ILockService>> CheckConnectMissing( IEnumerable<LockInfoAuthTdo> locksInfo, TimeSpan connectTimeout); /// <summary>Gets a lock by bike Id.</summary> /// <param name="bikeId"></param> /// <returns>Lock object</returns> public ILockService this[int bikeId] { get { var device = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId); if (device == null) { return new NullLock(bikeId); } return device; } } /// <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<LockingState> DisconnectAsync(int bikeId, Guid bikeGuid) { if (DeviceInfo.Platform != DevicePlatform.Unknown && MainThread.IsMainThread == false) { throw new System.Exception("Can not disconnect from lock. Bluetooth code must be run on main thread"); } Log.ForContext<LockItByScanServiceBase>().Debug($"Request to disconnect from device {bikeId}/ {bikeGuid}."); var lockIt = DeviceList.FirstOrDefault(x => x.Name.GetBluetoothLockId() == bikeId || x.Guid == bikeGuid); if (lockIt == null) { // Nothing to do return LockingState.Disconnected; } DeviceList.Remove(lockIt); if (lockIt.GetDeviceState() == DeviceState.Disconnected) { // Nothing to do return LockingState.Disconnected; } await lockIt.Disconnect(); return LockingState.Disconnected; } } }