using System; 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.Services.BluetoothLock; using TINK.Services.BluetoothLock.Exception; using TINK.Services.CopriApi.Exception; using TINK.Services.Geolocation; using TINK.View; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { /// Bike is reserved, lock is open and connected to app. /// /// This state might occur when a ILOCKIT was manually opened (color code) and app connects afterwards. /// This should never during ILOCKIT is connected to app because /// - manually opening lock is not possible when lock is connected /// - two devices can not simultaneously connect to same lock. public class ReservedOpen : Base, IRequestHandler { /// Provides info about the smart device (phone, tablet, ...) /// View model to be used for progress report and unlocking/ locking view. public ReservedOpen( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocationService geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, "Rad zurückgeben oder mieten", true, // Show button to enable canceling reservation. isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = "Alarm/ Sounds verwalten"; IsLockitButtonVisible = activeUser.DebugLevel > 0; // Will be visible in future version of user with leveraged privileges. } /// Cancel reservation. public async Task HandleRequestOption1() => await CloseLockOrDoBook(); /// Manage sound/ alarm settings. /// public async Task HandleRequestOption2() => await ManageLockSettings(); /// Cancel reservation. public async Task CloseLockOrDoBook() { BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending. var l_oResult = await ViewService.DisplayAlert( string.Empty, string.Format("Rad {0} abschließen und zurückgeben oder Rad mieten?", SelectedBike.GetFullDisplayName()), "Zurückgeben", "Mieten"); // Stop polling before cancel request. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); if (l_oResult == false) { // User decided to book Log.ForContext().Information("User selected requested bike {bike} in order to book.", 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("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; } } // Notify copri about unlock action in order to start booking. BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoBookAsync(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 booking 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 = "Wiederverschließe Schloss..."; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; } catch (Exception exception) { Log.ForContext().Error("Locking bike after booking failure failed. {Exception}", exception); SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } Log.ForContext().Information("User booked bike {bike} successfully.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Close lock and cancel reservation. Log.ForContext().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id); BikesViewModel.ActionText = AppResources.ActivityTextClosingLock; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; } 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.ErrorCloseLockOutOfReachStateReservedMessage, "OK"); } else if (exception is CouldntCloseMovingException) { Log.ForContext().Debug("Lock can not be closed. Lock bike is moving. {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, string.Format(AppResources.ErrorCloseLockUnkErrorMessage, exception.Message), "OK"); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike); // If canceling bike succeeds remove bike because it is not ready to be booked again IsRemoveBikeRequired = true; } catch (Exception exception) { BikesViewModel.ActionText = String.Empty; if (exception is InvalidAuthorizationResponseException) { // Copri response is invalid. Log.ForContext().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id); await ViewService.DisplayAlert( AppResources.MessageCancelReservationBikeErrorGeneralTitle, exception.Message, AppResources.MessageAnswerOk); } else if (exception is WebConnectFailureException || exception is RequestNotCachableException) { // Copri server is not reachable. Log.ForContext().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id); await ViewService.DisplayAlert( AppResources.MessageCancelReservationBikeErrorConnectionTitle, string.Format("{0}\r\n{1}", exception.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected reserved bike {l_oId} but cancel reservation failed. {@l_oException}.", SelectedBike.Id, exception); await ViewService.DisplayAlert( AppResources.MessageCancelReservationBikeErrorGeneralTitle, exception.Message, "OK"); } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } Log.ForContext().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id); // 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(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } /// Manage sound/ alarm settings. /// public async Task ManageLockSettings() { // Stop polling before requesting bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); // Close lock Log.ForContext().Information("User selected disposable bike {bike} in order to manage sound/ alarm settings.", SelectedBike); // Alarm and sounds are on, toggle to off. // Switch off sound. BikesViewModel.ActionText = "Abschalten der Sounds..."; try { await LockService[SelectedBike.LockInfo.Id].SetSoundAsync(SoundSettings.Warn); } catch (OutOfReachException exception) { Log.ForContext().Debug("Can not turn off sounds. {Exception}", exception); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( "Fehler beim Abschalten der Sounds!", "Sounds können erst abgeschalten werden, wenn Rad in der Nähe ist.", "OK"); return this; } catch (Exception exception) { Log.ForContext().Error("Can not turn off sounds. {Exception}", exception); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( "Fehler beim Abschalten der Sounds!", exception.Message, "OK"); return this; } // Lower alarm sensivity. BikesViewModel.ActionText = "Setzen Alarm-Einstellungen..."; try { await LockService[SelectedBike.LockInfo.Id].SetAlarmSettingsAsync(AlarmSettings.SmallSensivitySilent); } catch (OutOfReachException exception) { Log.ForContext().Debug("Can not set alarm settings. {Exception}", exception); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( "Fehler beim Setzen der Alarm-Einstellungen!", "Alarm kann erst eingestellt werden, wenn Rad in der Nähe ist.", "OK"); return this; } catch (Exception exception) { Log.ForContext().Error("Can not set alarm settings. {Exception}", exception); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( "Fehler beim Setzen der Alarms-Einstellungen!", exception.Message, "OK"); return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Switch off alarm. BikesViewModel.ActionText = "Abschalten von Alarm..."; try { await LockService[SelectedBike.LockInfo.Id].SetIsAlarmOffAsync(true); } catch (OutOfReachException exception) { Log.ForContext().Debug("Can not turn off alarm settings. {Exception}", exception); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( "Fehler beim Abschalten des Alarms!", "Alarm kann erst abgeschalten werden, wenn Rad in der Nähe ist.", "OK"); return this; } catch (Exception exception) { Log.ForContext().Error("Can not turn off alarm. {Exception}", exception); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( "Fehler beim Abschalten des Alarms!", exception.Message, "OK"); return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } finally { BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI } await ViewService.DisplayAlert( "Hinweis", "Alarm und Sounds erfolgreich abgeschalten.", "OK"); return this; } } }