using System; using System.Threading.Tasks; using Serilog; using ShareeBike.Repository.Exception; using ShareeBike.Model.Connector; using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock; using ShareeBike.Repository.Request; using System.Threading; using ShareeBike.Services.BluetoothLock; using ShareeBike.Services.Geolocation; using ShareeBike.MultilingualResources; using Xamarin.Essentials; namespace ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command { public static class EndRentalCommand { /// /// Possible steps of returning a bike. /// public enum Step { GetLocation, ReturnBike, } /// /// Possible steps of returning a bike. /// public enum State { GPSNotSupportedException, GPSNotEnabledException, NoGPSPermissionsException, GeneralQueryLocationFailed, WebConnectFailed, NotAtStation, NoGPSData, ResponseException, GeneralEndRentalError, } /// /// Interface to notify view model about steps/ state changes of returning bike process. /// public interface IEndRentalCommandListener { /// /// Reports current step. /// /// Current step to report. void ReportStep(Step currentStep); /// /// Reports current state. /// /// Current state to report. /// Message describing the current state. /// Task ReportStateAsync(State currentState, string message); } /// /// End rental. /// /// /// /// public static async Task InvokeAsync( IBikeInfoMutable bike, IGeolocationService geolocation, ILocksService lockService, Func dateTimeProvider = null, Func isConnectedDelegate = null, Func connectorFactory = null, IEndRentalCommandListener listener = null) { // Invokes member to notify about step being started. void InvokeCurrentStep(Step step) { if (listener == null) return; try { listener.ReportStep(step); } catch (Exception exception) { Log.ForContext().Error("An exception {@exception} was thrown invoking step-action for step {step} ", exception, step); } } // Invokes member to notify about state change. async Task InvokeCurrentStateAsync(State state, string message) { if (listener == null) return; try { await listener.ReportStateAsync(state, message); } catch (Exception exception) { Log.ForContext().Error("An exception {@exception} was thrown invoking state-action for state {state} ", exception, state); } } //// Start Action // Get Location //// Step: Start query geolocation data. InvokeCurrentStep(Step.GetLocation); // Get geolocation which was requested when closing lock. var closingLockLocation = bike.LockInfo.Location; LocationDto endRentalLocation; if (closingLockLocation != null) { // Location was available when closing bike. No further actions required. endRentalLocation = new LocationDto.Builder { Latitude = closingLockLocation.Latitude, Longitude = closingLockLocation.Longitude, Accuracy = closingLockLocation.Accuracy ?? double.NaN, Age = bike.LockInfo.LastLockingStateChange is DateTime lastLockState1 ? lastLockState1.Subtract(closingLockLocation.Timestamp.DateTime) : TimeSpan.MaxValue, }.Build(); } else { IGeolocation newLockLocation = null; // Check if bike is around var deviceState = lockService[bike.LockInfo.Id].GetDeviceState(); // Geolocation can not be queried because bike is not around. if (deviceState != DeviceState.Connected) { Log.ForContext().Information("There is no geolocation information available since lock of bike {bikeId} is not connected", bike.Id); // no GPS data exception. //NoGPSDataException.IsNoGPSData("There is no geolocation information available since lock is not connected", out NoGPSDataException exception); await InvokeCurrentStateAsync(State.NoGPSData, "There is no geolocation information available since lock is not connected"); return null; // return empty BookingFinishedModel } else { // Bike is around -> Query geolocation. var ctsLocation = new CancellationTokenSource(); try { newLockLocation = await geolocation.GetAsync(ctsLocation.Token, DateTime.Now); } catch (Exception exception) { // No location information available. Log.ForContext().Information("Geolocation query failed."); if (exception is FeatureNotSupportedException) { Log.ForContext().Error("Location service are not supported on device."); await InvokeCurrentStateAsync(State.GPSNotSupportedException, exception.Message); } if (exception is FeatureNotEnabledException) { Log.ForContext().Error("Location service are off."); await InvokeCurrentStateAsync(State.GPSNotEnabledException, exception.Message); } if (exception is PermissionException) { Log.ForContext().Error("No location service permissions granted."); await InvokeCurrentStateAsync(State.NoGPSPermissionsException, exception.Message); } else { Log.ForContext().Information("{@exception}", exception); await InvokeCurrentStateAsync(State.GeneralQueryLocationFailed, exception.Message); } throw; } } // Update last lock state time // save geolocation data for sending to backend endRentalLocation = newLockLocation != null ? new LocationDto.Builder { Latitude = newLockLocation.Latitude, Longitude = newLockLocation.Longitude, Accuracy = newLockLocation.Accuracy ?? double.NaN, Age = (dateTimeProvider != null ? dateTimeProvider() : DateTime.Now).Subtract(newLockLocation.Timestamp.DateTime), }.Build() : null; } // Return bike InvokeCurrentStep(Step.ReturnBike); BookingFinishedModel bookingFinished; try { bookingFinished = await connectorFactory(true).Command.DoReturn( bike, endRentalLocation); Log.ForContext().Information("Rental of bike {bikeId} was terminated successfully.", bike.Id); } catch (Exception exception) { Log.ForContext().Information("Rental of bike {bikeId} can not be terminated.", bike.Id); if (exception is WebConnectFailureException) { // No web. Log.ForContext().Error("Copri server not reachable. No web."); await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message); } else if (exception is NotAtStationException notAtStationException) { // not at station. Log.ForContext().Error("COPRI returned out of GEO fencing error. Position send to COPRI {position}.", endRentalLocation); await InvokeCurrentStateAsync(State.NotAtStation, string.Format(AppResources.ErrorEndRentalNotAtStation, notAtStationException.StationNr, notAtStationException.Distance)); // reset location -> at next try query new location data bike.LockInfo.Location = null; } else if (exception is NoGPSDataException) { // no GPS data. Log.ForContext().Error("COPRI returned a no-GPS-data error."); await InvokeCurrentStateAsync(State.NoGPSData, exception.Message); // reset location -> at next try query new location data bike.LockInfo.Location = null; } else if (exception is ResponseException copriException) { // COPRI exception. Log.ForContext().Error("COPRI returned an error. {response}", copriException.Response); await InvokeCurrentStateAsync(State.ResponseException, exception.Message); } else { Log.ForContext().Error("{@exception}", exception); await InvokeCurrentStateAsync(State.GeneralEndRentalError, exception.Message); } throw; } return bookingFinished; } } }