using System; using System.Threading.Tasks; using TINK.Model.Connector; using TINK.Model.Bike.BluetoothLock; using TINK.Model.State; using TINK.View; using TINK.Services.Geolocation; using TINK.Services.BluetoothLock; using Serilog; using TINK.Repository.Exception; using TINK.Services.BluetoothLock.Exception; using Xamarin.Essentials; using TINK.MultilingualResources; using TINK.Model.Bikes.Bike.BluetoothLock; using TINK.Model.User; using TINK.Repository.Request; using TINK.Model.Device; using System.Collections.Generic; using System.Threading; using TINK.Model; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { public class BookedOpen : Base, IRequestHandler { /// View model to be used for progress report and unlocking/ locking view. public BookedOpen( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionCloseAndReturn, // Copri button text: "Schloss schließen & Miete beenden" true, // Show button to allow user to return bike. isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = AppResources.ActionClose; // BT button text "Schließen". IsLockitButtonVisible = true; // Show button to allow user to lock bike. } /// Gets the bike state. public override InUseStateEnum State => InUseStateEnum.Disposable; /// Close lock and return bike. public async Task HandleRequestOption1() => await CloseLockAndReturnBike(); /// Close lock in order to pause ride and update COPRI lock state. public async Task HandleRequestOption2() => await CloseLock(); /// Close lock and return bike. public async Task CloseLockAndReturnBike() { // Prevent concurrent interaction BikesViewModel.IsIdle = false; // Start getting geolocation. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationStart; var ctsLocation = new CancellationTokenSource(); Task currentLocationTask = null; var timeStamp = DateTime.Now; try { currentLocationTask = Geolocation.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 = string.Empty; await ViewService.DisplayAlert( AppResources.MessageErrorQueryLocationStartTitle, $"{AppResources.MessageErrorQueryLocationMessage}\r\n{ex.Message}", AppResources.MessageAnswerOk); 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, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Ask whether to really return bike? var l_oResult = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionCloseLockAndReturnBike, SelectedBike.GetFullDisplayName()), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (l_oResult == false) { // User aborted closing and returning bike process Log.ForContext().Information("User selected booked bike {l_oId} in order to close and return but action was canceled.", SelectedBike.Id); // Cancel getting geolocation. ctsLocation.Cancel(); 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 abort returning opened bike failed. {Exception}", SelectedBike, ex); } BikesViewModel.IsIdle = true; return this; } // Unlock bike. Log.ForContext().Information("Request to return bike {bike} detected.", SelectedBike); // Stop polling before returning bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); // Notify COPRI about start reaturning bike BikesViewModel.ActionText = AppResources.ActivityTextStartReturningBike; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.StartReturningBike( SelectedBike); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected booked bike {bike} but returing failed (Copri server not reachable).", SelectedBike); await ViewService.DisplayAdvancedAlert( AppResources.ErrorReturnBikeNoWebTitle, string.Format("{0}\r\n{1}", AppResources.ErrorReturnBikeNoWebMessage, WebConnectFailureException.GetHintToPossibleExceptionsReasons), exception.Message, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected booked bike {bike} but returning failed. {@l_oException}", SelectedBike.Id, exception); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, exception.Message, AppResources.MessageAnswerOk); } 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); } // 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; SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; // Signal cts to cancel getting geolocation. ctsLocation.Cancel(); Task updateLockingStateTask = Task.CompletedTask; try { updateLockingStateTask = ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync( SelectedBike); } catch (Exception innerExceptionStartUpdateLockingState) { // No location information available/ updating state failed. Log.ForContext().Information("Start update locking state failed on lock operating error. {Exception}", SelectedBike, innerExceptionStartUpdateLockingState); } // Signal cts to cancel getting geolocation. ctsLocation.Cancel(); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, AppResources.ErrorCloseLockOutOfReachMessage, AppResources.MessageAnswerOk); } 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, 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); } // Wait until cancel getting geolocation has completed. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationCancelWait; try { await Task.WhenAll(new List { currentLocationTask ?? Task.CompletedTask, updateLockingStateTask }); } catch (Exception innerExWhenAll) { // No location information available/ updating state failed. Log.ForContext().Information("Canceling query location/ updating lock state failed on closing lock error. {Exception}", SelectedBike, innerExWhenAll); } // 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(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } if (SelectedBike.LockInfo.State != LockingState.Closed) { Log.ForContext().Error($"Lock can not be closed. Invalid locking state state {SelectedBike.LockInfo.State} detected."); // Signal cts to cancel getting geolocation. ctsLocation.Cancel(); BikesViewModel.ActionText = string.Empty; // Signal cts to cancel getting geolocation. ctsLocation.Cancel(); Task updateLockingStateTask = Task.CompletedTask; try { updateLockingStateTask = ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync( SelectedBike); } catch (Exception innerExceptionStartUpdateLockingState) { // No location information available/ updating state failed. Log.ForContext().Information("Start update locking state failed on unexpected state. {Exception}", SelectedBike, innerExceptionStartUpdateLockingState); } await ViewService.DisplayAlert( AppResources.ErrorCloseLockTitle, SelectedBike.LockInfo.State == LockingState.Open ? AppResources.ErrorCloseLockStillOpenMessage : string.Format(AppResources.ErrorCloseLockUnexpectedStateMessage, SelectedBike.LockInfo.State), AppResources.MessageAnswerOk); // Wait until cancel getting geolocation has completed. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationCancelWait; try { await Task.WhenAll(new List { currentLocationTask ?? Task.CompletedTask, updateLockingStateTask}); } catch (Exception innerExWhenAll) { // No location information available. Log.ForContext().Information("Canceling query location/ updating lock state failed failed on unexpected lock state failed. {Exception}", SelectedBike, innerExWhenAll); } 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, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation; Location currentLocation = null; try { var task = await Task.WhenAny(new List { currentLocationTask }); currentLocation = currentLocationTask.Result; } catch (Exception ex) { // No location information available. Log.ForContext().Information("Returning bike {Bike} is not possible. Query location failed. {Exception}", SelectedBike, ex); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAdvancedAlert( AppResources.MessageErrorQueryLocationTitle, AppResources.MessageErrorQueryLocationMessage, ex.GetErrorMessage(), AppResources.MessageAnswerOk); 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, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Lock list to avoid multiple taps while copri action is pending. BikesViewModel.ActionText = AppResources.ActivityTextReturningBike; IsConnected = IsConnectedDelegate(); var feedBackUri = SelectedBike?.OperatorUri; BookingFinishedModel bookingFinished; try { bookingFinished = await ConnectorFactory(IsConnected).Command.DoReturn( 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, SmartDevice); // If canceling bike succedes remove bike because it is not ready to be booked again IsRemoveBikeRequired = true; } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected booked bike {bike} but returing failed (Copri server not reachable).", SelectedBike); await ViewService.DisplayAdvancedAlert( AppResources.ErrorReturnBikeNoWebTitle, string.Format("{0}\r\n{1}", AppResources.ErrorReturnBikeNoWebMessage, WebConnectFailureException.GetHintToPossibleExceptionsReasons), exception.Message, AppResources.MessageAnswerOk); } else if (exception is NotAtStationException notAtStationException) { // COPRI returned an error. Log.ForContext().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, string.Format(AppResources.ErrorReturnBikeNotAtStationMessage, notAtStationException.StationNr, notAtStationException.Distance), AppResources.MessageAnswerOk); } else if (exception is NoGPSDataException) { // COPRI returned an error. Log.ForContext().Information("User selected booked bike {bike} but returing failed. COPRI returned an no GPS- data error.", SelectedBike); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, string.Format(AppResources.ErrorReturnBikeLockOpenNoGPSMessage), AppResources.MessageAnswerOk); } else if (exception is ResponseException copriException) { // Copri server is not reachable. Log.ForContext().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike); await ViewService.DisplayAdvancedAlert( "Statusfehler beim Zurückgeben des Rads!", copriException.Message, copriException.Response, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected booked bike {bike} but returning failed. {@l_oException}", SelectedBike.Id, exception); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, exception.Message, AppResources.MessageAnswerOk); } 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); } Log.ForContext().Information("User returned bike {bike} successfully.", SelectedBike); // 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; } #if !USERFEEDBACKDLG_OFF // Do get Feedback var feedback = await ViewService.DisplayUserFeedbackPopup(bookingFinished?.Co2Saving); try { await ConnectorFactory(IsConnected).Command.DoSubmitFeedback( new UserFeedbackDto { BikeId = SelectedBike.Id, IsBikeBroken = feedback.IsBikeBroken, Message = feedback.Message }, feedBackUri); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is ResponseException copriException) { // Copri server is not reachable. Log.ForContext().Information("Submitting feedback for bike {bike} failed. COPRI returned an error.", SelectedBike); } else { Log.ForContext().Error("Submitting feedback for bike {bike} failed. {@l_oException}", SelectedBike.Id, exception); } await ViewService.DisplayAlert( AppResources.ErrorReturnSubmitFeedbackTitle, AppResources.ErrorReturnSubmitFeedbackMessage, AppResources.MessageAnswerOk); 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); } #endif if (bookingFinished != null && bookingFinished.MiniSurvey.Questions.Count > 0) { await ViewService.PushModalAsync(ViewTypes.MiniSurvey); } 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); } /// 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 = Geolocation.GetAsync(ctsLocation.Token, timeStamp); } catch (Exception ex) { // No location information available. Log.ForContext().Information("Closing lock of bike {Bike} is not possible. Starting 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 CounldntCloseMovingException) { 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); } // 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, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Get geoposition. BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation; Location currentLocation = null; try { await Task.WhenAny(new List { currentLocationTask }); currentLocation = currentLocationTask.Result; } catch (Exception ex) { // No location information available. Log.ForContext().Information("Getting geolocation when closing lock of bike {Bike} failed. {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, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } } }