mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-10-06 13:26:28 +02:00
257 lines
8 KiB
C#
257 lines
8 KiB
C#
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
|
|
{
|
|
|
|
/// <summary>
|
|
/// Possible steps of returning a bike.
|
|
/// </summary>
|
|
public enum Step
|
|
{
|
|
GetLocation,
|
|
ReturnBike,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Possible steps of returning a bike.
|
|
/// </summary>
|
|
public enum State
|
|
{
|
|
GPSNotSupportedException,
|
|
GPSNotEnabledException,
|
|
NoGPSPermissionsException,
|
|
GeneralQueryLocationFailed,
|
|
|
|
WebConnectFailed,
|
|
NotAtStation,
|
|
NoGPSData,
|
|
ResponseException,
|
|
GeneralEndRentalError,
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface to notify view model about steps/ state changes of returning bike process.
|
|
/// </summary>
|
|
public interface IEndRentalCommandListener
|
|
{
|
|
/// <summary>
|
|
/// Reports current step.
|
|
/// </summary>
|
|
/// <param name="currentStep">Current step to report.</param>
|
|
void ReportStep(Step currentStep);
|
|
|
|
/// <summary>
|
|
/// Reports current state.
|
|
/// </summary>
|
|
/// <param name="currentState">Current state to report.</param>
|
|
/// <param name="message">Message describing the current state.</param>
|
|
/// <returns></returns>
|
|
Task ReportStateAsync(State currentState, string message);
|
|
}
|
|
|
|
/// <summary>
|
|
/// End rental.
|
|
/// </summary>
|
|
/// <param name="listener"></param>
|
|
/// <returns></returns>
|
|
/// <exception cref="Exception"></exception>
|
|
public static async Task<BookingFinishedModel> InvokeAsync<T>(
|
|
IBikeInfoMutable bike,
|
|
IGeolocationService geolocation,
|
|
ILocksService lockService,
|
|
Func<DateTime> dateTimeProvider = null,
|
|
Func<bool> isConnectedDelegate = null,
|
|
Func<bool, IConnector> 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<T>().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<T>().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<T>().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<T>().Information("Geolocation query failed.");
|
|
if (exception is FeatureNotSupportedException)
|
|
{
|
|
Log.ForContext<T>().Error("Location service are not supported on device.");
|
|
await InvokeCurrentStateAsync(State.GPSNotSupportedException, exception.Message);
|
|
}
|
|
if (exception is FeatureNotEnabledException)
|
|
{
|
|
Log.ForContext<T>().Error("Location service are off.");
|
|
await InvokeCurrentStateAsync(State.GPSNotEnabledException, exception.Message);
|
|
}
|
|
if (exception is PermissionException)
|
|
{
|
|
Log.ForContext<T>().Error("No location service permissions granted.");
|
|
await InvokeCurrentStateAsync(State.NoGPSPermissionsException, exception.Message);
|
|
}
|
|
else
|
|
{
|
|
Log.ForContext<T>().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<T>().Information("Rental of bike {bikeId} was terminated successfully.", bike.Id);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
Log.ForContext<T>().Information("Rental of bike {bikeId} can not be terminated.", bike.Id);
|
|
if (exception is WebConnectFailureException)
|
|
{
|
|
// No web.
|
|
Log.ForContext<T>().Error("Copri server not reachable. No web.");
|
|
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
|
}
|
|
else if (exception is NotAtStationException notAtStationException)
|
|
{
|
|
// not at station.
|
|
Log.ForContext<T>().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<T>().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<T>().Error("COPRI returned an error. {response}", copriException.Response);
|
|
await InvokeCurrentStateAsync(State.ResponseException, exception.Message);
|
|
}
|
|
else
|
|
{
|
|
Log.ForContext<T>().Error("{@exception}", exception);
|
|
await InvokeCurrentStateAsync(State.GeneralEndRentalError, exception.Message);
|
|
}
|
|
|
|
throw;
|
|
}
|
|
|
|
return bookingFinished;
|
|
|
|
}
|
|
}
|
|
}
|