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; using IBikeInfoMutable = TINK.Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable; 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, AppResources.ActionCloseLock, true, // Show button "Close Lock" isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = activeUser.DebugLevel.HasFlag(Model.User.Account.Permissions.ManageAlarmAndSounds) ? "Alarm/ Sounds verwalten" : AppResources.ActionRentBike; IsLockitButtonVisible = true; // Only users with special permissions are allowed to set alarm. Regular user gets options "Rent Bike" } /// Close lock. public async Task HandleRequestOption1() => await CloseLock(); /// Manage sound/ alarm settings or Rent bike. /// public async Task HandleRequestOption2() => ActiveUser.DebugLevel.HasFlag(Model.User.Account.Permissions.ManageAlarmAndSounds) ? await ManageLockSettings() : await RentBike(); /// Rent bike. public async Task RentBike() { BikesViewModel.IsIdle = false; Log.ForContext().Information("User request to book bike {bikeId}.", SelectedBike.Id); // Stop polling before requesting bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopAsync(); // Book bike BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoBookAsync(SelectedBike, LockingAction.Open); Log.ForContext().Information("User booked bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; Log.ForContext().Information("Booking of bike {bikeId} failed.", SelectedBike.Id); if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Error("Copri server not reachable."); await ViewService.DisplayAlert( AppResources.ErrorNoConnectionTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("{@exception}", exception); await ViewService.DisplayAdvancedAlert( AppResources.ErrorRentingBikeTitle, exception.Message, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); } } // get current charging level ILockService btLock = LockService[SelectedBike.LockInfo.Id]; BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = await btLock.GetBatteryPercentageAsync(); Log.ForContext().Information("Battery state of lock from {bikeId} read successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Debug("Battery state of lock from {bikeId} can not be read.", SelectedBike.Id); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach."); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; } else { Log.ForContext().Error("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; } } // get lock infos. 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(); } // update backend IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); Log.ForContext().Information("Backend updated for bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Updating backend for bike {bikeId} failed.", SelectedBike.Id); if (exception is WebConnectFailureException) { // No web. Log.ForContext().Debug("Copri server not reachable. No web."); BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; } else if (exception is ResponseException copriException) { // Copri exception. Log.ForContext().Debug("{response}", copriException.Response); BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; } else { Log.ForContext().Debug("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; } } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); // 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().StopAsync(); // Close lock Log.ForContext().Information("User selected bike {bikeId} in order to manage sound/ alarm settings.", SelectedBike.Id); // 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.", AppResources.MessageAnswerOk); 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, AppResources.MessageAnswerOk); 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.", AppResources.MessageAnswerOk); 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, AppResources.MessageAnswerOk); 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.", AppResources.MessageAnswerOk); 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, AppResources.MessageAnswerOk); return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } finally { BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI } await ViewService.DisplayAlert( AppResources.MessageHintTitle, "Alarm und Sounds erfolgreich abgeschalten.", AppResources.MessageAnswerOk); return this; } public async Task CloseLock() { BikesViewModel.IsIdle = false; // Stop polling before requesting bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopAsync(); Log.ForContext().Information("User request to close lock of bike {bikeId}.", SelectedBike.Id); BikesViewModel.ActionText = AppResources.ActivityTextClosingLock; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; Log.ForContext().Information("Lock of bike {bikeId} closed successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock of bike {bikeId} can not be closed.", SelectedBike.Id); BikesViewModel.ActionText = string.Empty; if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach."); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorLockOutOfReach, AppResources.MessageAnswerOk); } else if (exception is CouldntCloseMovingException) { Log.ForContext().Debug("Lock is moving."); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorLockMoving, AppResources.MessageAnswerOk); } else if (exception is CouldntCloseBoltBlockedException) { Log.ForContext().Debug("Bold is blocked."); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockBoltBlocked, AppResources.MessageAnswerOk); } else { Log.ForContext().Debug("{@exception}", exception); await ViewService.DisplayAdvancedAlert( AppResources.ErrorCloseLockTitle, exception.Message, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; } // get current charging level ILockService btLock = LockService[SelectedBike.LockInfo.Id]; BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = await btLock.GetBatteryPercentageAsync(); Log.ForContext().Information("Battery state of lock from {bikeId} read successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Battery state of lock from {bikeId} can not be read.", SelectedBike.Id); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach."); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; } else { Log.ForContext().Debug("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; } } // get lock infos. 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(); } // update backend IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); Log.ForContext().Information("Backend updated for bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Updating backend for bike {bikeId} failed .", SelectedBike.Id); if (exception is WebConnectFailureException) { // No web. Log.ForContext().Information("Copri server not reachable. No web."); BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; } else if (exception is ResponseException copriException) { // Copri exception. Log.ForContext().Debug("{response}.", copriException.Response); BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; } else { Log.ForContext().Debug("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; } } // Ask whether to cancel reservation var result = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionCancelReservation, SelectedBike.GetFullDisplayName()), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (result == true) { Log.ForContext().Information("User request to cancel reservation of bike {bikeId}.", SelectedBike.Id); BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike); Log.ForContext().Information("User canceled reservation of bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; Log.ForContext().Information("Canceling reservation of bike {bikeId} failed.", SelectedBike.Id); if (exception is InvalidAuthorizationResponseException) { // Copri response is invalid. Log.ForContext().Debug("Invalid auth. response."); await ViewService.DisplayAlert( AppResources.ErrorCancelReservationTitle, AppResources.ErrorAccountInvalidAuthorization, AppResources.MessageAnswerOk); } else if (exception is WebConnectFailureException || exception is RequestNotCachableException) { // No web. Log.ForContext().Debug("Copri server not reachable. No web."); await ViewService.DisplayAlert( AppResources.ErrorCancelReservationTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); } else { Log.ForContext().Debug("{@exception}.", exception); await ViewService.DisplayAdvancedAlert( AppResources.ErrorCancelReservationTitle, exception.Message, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); } } } // Disconnect lock. if (SelectedBike.LockInfo.State == LockingState.Closed) { BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; try { SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); Log.ForContext().Information("Lock of bike {bikeId} disconnected successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock of bike {bikeId} can not be disconnected. {@exception}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; } } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } } }