using System; using System.Collections.Generic; using System.Threading; using System.Threading.Tasks; using Serilog; using TINK.Model; 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; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { public class BookedClosed : Base, IRequestHandler { /// Provides info about the smart device (phone, tablet, ...) /// View model to be used for progress report and unlocking/ locking view. public BookedClosed( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocationService geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionReturn, // Copri button text "Miete beenden" true, // Show button to enabled returning of bike. isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = AppResources.ActionOpenAndPause; IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike. } /// Return bike. public async Task HandleRequestOption1() => await ReturnBike(); /// Open bike and update COPRI lock state. public async Task HandleRequestOption2() => await OpenLock(); /// Return bike. public async Task ReturnBike() { BikesViewModel.IsIdle = false; var ctsLocation = new CancellationTokenSource(); Task currentLocationTask = null; // Try getting geolocation which was requested when closing lock. IGeolocation currentLocation = SelectedBike.LockInfo.Location; var lastConfimredLockStateTimeStamp = SelectedBike.LockInfo.LastLockingStateChange; // Check if bike is around. var deviceState = LockService[SelectedBike.LockInfo.Id].GetDeviceState(); // Check if // - geolocation is already available // - or if bike is in reach so that geolocation makes sense if (currentLocation == null && deviceState != DeviceState.Connected) { // Geolocation information is missing and can not be queried. Log.ForContext().Information("User selected booked bike {bike} but returning failed. COPRI returned an error.", SelectedBike); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, AppResources.Error_ReturnBike_Station_Location_Message, AppResources.MessageAnswerOk); // 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 = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Check if querying geolocation is required. if (currentLocation == null) { BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationStart; // Start getting geolocation. try { currentLocationTask = GeolocationService.GetAsync(ctsLocation.Token, DateTime.Now); } catch (Exception ex) { // No location information available. Log.ForContext().Information("Getting geolocation when returning bike {Bike} failed. {Exception}", SelectedBike, ex); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, AppResources.ErrorReturnBikeLockClosedStartGetGPSExceptionMessage, AppResources.MessageAnswerOk); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return this; } } // Ask whether to really return bike? var l_oResult = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionReturnBike, SelectedBike.GetFullDisplayName()), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (l_oResult == false) { // User aborted returning bike process Log.ForContext().Information("User selected booked bike {l_oId} in order to 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 closed bike failed. {Exception}", SelectedBike, ex); } BikesViewModel.IsIdle = true; return this; } // Lock list to avoid multiple taps while copri action is pending. Log.ForContext().Information("Request to return bike {bike} detected.", SelectedBike); // Stop polling before returning bike. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); // Get geolocation if // - geolocation was not available when closing lock // - bike is around (lock is connected via bluetooth) LocationDto currentLocationDto = null; if (currentLocation == null) { BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation; try { await Task.WhenAny(new List { currentLocationTask ?? Task.CompletedTask }); currentLocation = currentLocationTask?.Result ?? null; } catch (Exception ex) { // No location information available. Log.ForContext().Information("Returning closed bike {Bike} is not possible. Cancel geolocation query failed. {Exception}", SelectedBike, ex); await ViewService.DisplayAlert( AppResources.ErrorQueryGeolocation, AppResources.ErrorReturnBikeLockClosedGetGPSExceptionMessage, AppResources.MessageAnswerOk); BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return this; } lastConfimredLockStateTimeStamp = DateTime.Now; } currentLocationDto = currentLocation != null ? new LocationDto.Builder { Latitude = currentLocation.Latitude, Longitude = currentLocation.Longitude, Accuracy = currentLocation.Accuracy ?? double.NaN, Age = lastConfimredLockStateTimeStamp is DateTime lastLockState ? lastLockState.Subtract(currentLocation.Timestamp.DateTime) : TimeSpan.MaxValue, }.Build() : null; BikesViewModel.ActionText = AppResources.ActivityTextReturningBike; IsConnected = IsConnectedDelegate(); var feedBackUri = SelectedBike?.OperatorUri; BookingFinishedModel bookingFinished; try { bookingFinished = await ConnectorFactory(IsConnected).Command.DoReturn( SelectedBike, currentLocationDto); } 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 returning 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 returning failed. COPRI returned out of GEO fencing error. Position send to COPRI {@position}.", SelectedBike, currentLocationDto); 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 returning failed. COPRI returned an no GPS- data error.", SelectedBike); await ViewService.DisplayAlert( AppResources.ErrorReturnBikeTitle, string.Format(AppResources.ErrorReturnBikeLockClosedNoGPSMessage), AppResources.MessageAnswerOk); } else if (exception is ResponseException copriException) { // COPRI returned an error. Log.ForContext().Information("User selected booked bike {bike} but returning failed. COPRI returned an error.", SelectedBike); await ViewService.DisplayAdvancedAlert( "Statusfehler beim Zurückgeben des Rads!", copriException.Message, copriException.Response, "OK"); } 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, GeolocationService, 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(SelectedBike.Drive?.Battery, bookingFinished?.Co2Saving); IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoSubmitFeedback( new UserFeedbackDto { BikeId = SelectedBike.Id, CurrentChargeBars = feedback.CurrentChargeBars, 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, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } #endif if (bookingFinished != null && bookingFinished.MiniSurvey.Questions.Count > 0) { await ViewService.PushModalAsync(ViewTypes.MiniSurvey); } // Restart polling again. 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); } /// 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. Bold status is unknown. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockStillClosedTitle, AppResources.ErrorOpenLockBoldStatusIsUnknownMessage, "OK"); } 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, "OK"); } else { Log.ForContext().Error("Lock can not be opened. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, exception.Message, "OK"); } // 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); } } }