using Serilog; using System; using System.Threading.Tasks; using TINK.Model.Bike.BluetoothLock; using TINK.Model.Connector; using TINK.Model.State; using TINK.View; using TINK.Repository.Exception; using TINK.Model.Services.Geolocation; using TINK.Services.BluetoothLock; using TINK.Services.BluetoothLock.Exception; using TINK.MultilingualResources; using TINK.Model.Bikes.Bike.BluetoothLock; using TINK.Model.User; using TINK.Model.Device; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { /// Bike is disposable, lock is open and connected to app. /// /// This state can not be occur because /// - app does not allow to return bike/ cancel reservation when lock is not closed /// - as long as app is connected to lock /// - lock can not be opened manually /// - no other device can access lock /// public class DisposableOpen : Base, IRequestHandler { /// Bike is disposable, lock is open and can be reached via bluetooth. /// /// This state should never occure because as long as a ILOCKIT is connected it /// - cannot be closed manually /// - no other device can access lock /// - app itself should never event attempt to open a lock which is not rented. /// /// Provides info about the smart device (phone, tablet, ...) /// View model to be used for progress report and unlocking/ locking view. public DisposableOpen( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionBookOrClose, true, // Show copri button to enable reserving isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = GetType().Name; IsLockitButtonVisible = false; } /// Gets the bike state. public override InUseStateEnum State => InUseStateEnum.Disposable; /// Books bike by reserving bike, opening lock and booking bike. /// Next request handler. public async Task HandleRequestOption1() => await DoBookOrClose(); public async Task HandleRequestOption2() => await UnsupportedRequest(); /// Books bike by reserving bike, opening lock and booking bike. /// Next request handler. public async Task DoBookOrClose() { BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending. // Stop polling before requesting bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); // Ask whether to really book bike or close lock? var l_oResult = await ViewService.DisplayAlert( string.Empty, $"Fahrrad {SelectedBike.GetFullDisplayName()} mieten oder Schloss schließen?", "Mieten", "Schloss schließen"); if (l_oResult == false) { // Close lock Log.ForContext().Information("User selected disposable bike {bike} in order to close lock.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextClosingLock; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected; } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is OutOfReachException) { Log.ForContext().Debug("Lock can not be closed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockOutOfReachMessage, "OK"); } else if (exception is CounldntCloseMovingException) { Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockMovingMessage, "OK"); } 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, "OK"); } else { Log.ForContext().Error("Lock can not be closed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, exception.Message, "OK"); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.Disconnected; BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Disconnect lock. BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; try { SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); } catch (Exception exception) { Log.ForContext().Error("Lock can not be disconnected. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Lock list to avoid multiple taps while copri action is pending. Log.ForContext().Information("Request to book bike {bike}.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync(); } catch (Exception exception) { if (exception is OutOfReachException) { Log.ForContext().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; } else { Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; } } // Notify corpi about unlock action in order to start booking. BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; try { await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike); } catch (Exception l_oException) { BikesViewModel.ActionText = string.Empty; if (l_oException is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected requested bike {l_oId} but reserving failed (Copri server not reachable).", SelectedBike.Id); await ViewService.DisplayAlert( AppResources.MessageRentingBikeErrorConnectionTitle, string.Format(AppResources.MessageErrorLockIsClosedThreeLines, l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException); await ViewService.DisplayAlert( AppResources.MessageRentingBikeErrorGeneralTitle, string.Format(AppResources.MessageErrorLockIsClosedTwoLines, l_oException.Message), AppResources.MessageAnswerOk); } // If booking failed lock bike again because bike is only reserved. BikesViewModel.ActionText = "Verschließe Schloss..."; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected; } catch (Exception exception) { Log.ForContext().Error("Locking bike after booking failure failed. {Exception}", exception); SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.Disconnected; } // Disconnect lock. BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; try { SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); } catch (Exception exception) { Log.ForContext().Error("Lock can not be disconnected. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Update status text and unlock list of bikes because no more action is pending. BikesViewModel.ActionText = string.Empty; // Todo: Move this statement in front of finally block because in catch block BikesViewModel.ActionText is already set to empty. BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } Log.ForContext().Information("User reserved bike {bike} successfully.", SelectedBike); // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Update status text and unlock list of bikes because no more action is pending. BikesViewModel.ActionText = string.Empty; // Todo: Move this statement in front of finally block because in catch block BikesViewModel.ActionText is already set to empty. BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } /// Requst is not supported, button should be disabled. /// public async Task UnsupportedRequest() { Log.ForContext().Error("Click of unsupported button click detected."); return await Task.FromResult(this); } } }