using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Serilog; using TINK.Model.Bikes.BikeInfoNS.BluetoothLock; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Model.User; using TINK.MultilingualResources; using TINK.Repository.Exception; using TINK.Repository.Request; using TINK.Services.BluetoothLock; using TINK.Services.BluetoothLock.Exception; using TINK.Services.Geolocation; using TINK.View; using Xamarin.Essentials; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { public class ReservedUnknown : Base, IRequestHandler { /// Provides info about the smart device (phone, tablet, ...) /// View model to be used for progress report and unlocking/ locking view. public ReservedUnknown( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocationService geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionOpenAndBook, // BT button text "Schloss öffnen und Rad mieten." false, // Show button to enabled returning of bike. isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = AppResources.ActionClose; // BT button text "Schließen" IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike. } /// Open bike and update COPRI lock state. public async Task HandleRequestOption1() => await OpenLock(); /// Open bike and update COPRI lock state. public async Task OpenLock() { // Unlock bike. Log.ForContext().Information("User request to unlock bike {bike}.", SelectedBike); // Stop polling before returning bike. BikesViewModel.IsIdle = false; BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock; ILockService btLock; try { btLock = LockService[SelectedBike.LockInfo.Id]; SelectedBike.LockInfo.State = (await btLock.OpenAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is OutOfReachException) { Log.ForContext().Debug("Lock can not be opened. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockOutOfReachMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenBoldIsBlockedException) { Log.ForContext().Debug("Lock can not be opened. bold is blocked. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockBoldIsBlockedMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenBoldStatusIsUnknownException) { Log.ForContext().Debug("Lock can not be opened. lock reports state unkwnown. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockStillClosedTitle, AppResources.ErrorOpenLockBoldStatusIsUnknownMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenInconsistentStateExecption inconsistentState && inconsistentState.State == LockingState.Closed) { Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockStillClosedMessage, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("Lock can not be opened. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, exception.Message, AppResources.MessageAnswerOk); } // When bold is blocked lock is still closed even if exception occurs. // In all other cases state is supposed to be unknown. Example: Lock is out of reach and no more bluetooth connected. SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = await btLock.GetBatteryPercentageAsync(); } catch (Exception exception) { if (exception is OutOfReachException) { Log.ForContext().Debug("Battery state can not be read, bike out of range. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; } else { Log.ForContext().Error("Battery state can not be read. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; } } // Lock list to avoid multiple taps while copri action is pending. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; var versionTdo = btLock.VersionInfo; if (versionTdo != null) { SelectedBike.LockInfo.VersionInfo = new VersionInfo.Builder { FirmwareVersion = versionTdo.FirmwareVersion, HardwareVersion = versionTdo.HardwareVersion, LockVersion = versionTdo.LockVersion, }.Build(); } IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); } catch (Exception exception) { if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; } else if (exception is ResponseException copriException) { // Copri server is not reachable. Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; } else { Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; } } Log.ForContext().Information("User paused ride using {bike} successfully.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } /// Close lock in order to pause ride and update COPRI lock state. public async Task HandleRequestOption2() => await CloseLock(); /// Close lock in order to pause ride and update COPRI lock state. public async Task CloseLock() { // Unlock bike. BikesViewModel.IsIdle = false; Log.ForContext().Information("User request to lock bike {bike} in order to pause ride.", SelectedBike); // Start getting geolocation. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationStart; var ctsLocation = new CancellationTokenSource(); Task currentLocationTask = null; var timeStamp = DateTime.Now; try { currentLocationTask = GeolocationService.GetAsync(ctsLocation.Token, timeStamp); } catch (Exception ex) { // No location information available. Log.ForContext().Information("Returning bike {Bike} is not possible. Start query location failed. {Exception}", SelectedBike, ex); BikesViewModel.ActionText = AppResources.ActivityTextErrorQueryLocationQuery; } // Stop polling before returning bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); // Close lock BikesViewModel.ActionText = AppResources.ActivityTextClosingLock; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; // Signal cts to cancel getting geolocation. ctsLocation.Cancel(); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock can not be closed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockOutOfReachMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntCloseMovingException) { Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockMovingMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntCloseBoldBlockedException) { Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockBoldBlockedMessage, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("Lock can not be closed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, exception.Message, AppResources.MessageAnswerOk); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; // Wait until cancel getting geolocation has completed. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationCancelWait; try { await Task.WhenAny(new List { currentLocationTask ?? Task.CompletedTask }); } catch (Exception ex) { // No location information available. Log.ForContext().Information("Canceling query location failed on closing lock error. {Exception}", SelectedBike, ex); } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Get geolocation. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation; IGeolocation currentLocation = null; try { await Task.WhenAny(new List { currentLocationTask ?? Task.CompletedTask }); currentLocation = currentLocationTask?.Result ?? null; } catch (Exception ex) { // No location information available. Log.ForContext().Information("Returning bike {Bike} is not possible. {Exception}", SelectedBike, ex); BikesViewModel.ActionText = AppResources.ActivityTextErrorQueryLocationWhenAny; } // Lock list to avoid multiple taps while copri action is pending. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync( SelectedBike, currentLocation != null ? new LocationDto.Builder { Latitude = currentLocation.Latitude, Longitude = currentLocation.Longitude, Accuracy = currentLocation.Accuracy ?? double.NaN, Age = timeStamp.Subtract(currentLocation.Timestamp.DateTime), }.Build() : null); } catch (Exception exception) { if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; } else if (exception is ResponseException copriException) { // Copri server is not reachable. Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response); BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; } else { Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; } } Log.ForContext().Information("User paused ride using {bike} successfully.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } } }