mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-11-16 23:26:26 +01:00
284 lines
9.1 KiB
C#
284 lines
9.1 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Threading;
|
|
using System.Threading.Tasks;
|
|
using Serilog;
|
|
using TINK.Model.Connector;
|
|
using TINK.Repository.Exception;
|
|
using TINK.Repository.Request;
|
|
using TINK.Services.BluetoothLock;
|
|
using TINK.Services.BluetoothLock.Exception;
|
|
using TINK.Services.BluetoothLock.Tdo;
|
|
using TINK.Services.Geolocation;
|
|
|
|
namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command
|
|
{
|
|
public static class CloseCommand
|
|
{
|
|
/// <summary>
|
|
/// Possible steps of closing a lock.
|
|
/// </summary>
|
|
public enum Step
|
|
{
|
|
StartStopingPolling,
|
|
StartingQueryingLocation,
|
|
ClosingLock,
|
|
WaitStopPollingQueryLocation,
|
|
/// <summary>
|
|
/// Notifies back end about modified lock state.
|
|
/// </summary>
|
|
UpdateLockingState,
|
|
QueryLocationTerminated
|
|
}
|
|
|
|
/// <summary>
|
|
/// Possible states of closing a lock.
|
|
/// </summary>
|
|
public enum State
|
|
{
|
|
OutOfReachError,
|
|
CouldntCloseMovingError,
|
|
CouldntCloseBoltBlockedError,
|
|
GeneralCloseError,
|
|
StartGeolocationException,
|
|
WaitGeolocationException,
|
|
WebConnectFailed,
|
|
ResponseIsInvalid,
|
|
BackendUpdateFailed
|
|
}
|
|
|
|
/// <summary>
|
|
/// Interface to notify view model about steps/ state changes of closing process.
|
|
/// </summary>
|
|
public interface ICloseCommandListener
|
|
{
|
|
/// <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>
|
|
/// Closes the lock and updates copri.
|
|
/// </summary>
|
|
/// <param name="listener">Interface to notify view model about steps/ state changes of closing process.</param>
|
|
/// <param name="stopPolling">Task which stops polling.</param>
|
|
public static async Task InvokeAsync<T>(
|
|
IBikeInfoMutable bike,
|
|
IGeolocationService geolocation,
|
|
ILocksService lockService,
|
|
Func<bool> isConnectedDelegate,
|
|
Func<bool, IConnector> connectorFactor,
|
|
ICloseCommandListener listener,
|
|
Task stopPollingTask)
|
|
{
|
|
// Invokes member to notify about step being started.
|
|
void InvokeCurrentStep(Step step)
|
|
{
|
|
if (listener == null)
|
|
return;
|
|
|
|
try
|
|
{
|
|
listener.ReportStep(step);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
Log.ForContext<T>().Error("An exception {exception} was thrown invoking step- action for set {step} ", ex, 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 ex)
|
|
{
|
|
Log.ForContext<T>().Error("An exception {exception} was thrown invoking state- action for set {state} ", ex, state);
|
|
}
|
|
}
|
|
|
|
// Wait for geolocation and polling task to stop (on finished or on canceled).
|
|
async Task<IGeolocation> WaitForPendingTasks(Task<IGeolocation> locationTask)
|
|
{
|
|
// Step: Wait until getting geolocation has completed.
|
|
InvokeCurrentStep(Step.WaitStopPollingQueryLocation);
|
|
Log.ForContext<T>().Debug($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish...");
|
|
try
|
|
{
|
|
await Task.WhenAll(new List<Task> { locationTask, stopPollingTask ?? Task.CompletedTask });
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// No location information available.
|
|
Log.ForContext<T>().Information("Canceling query location/ wait for polling task to finish failed. {Exception}", ex);
|
|
await InvokeCurrentStateAsync(State.WaitGeolocationException, ex.Message);
|
|
InvokeCurrentStep(Step.QueryLocationTerminated);
|
|
return null;
|
|
}
|
|
|
|
Log.ForContext<T>().Debug($"Steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} finished.");
|
|
InvokeCurrentStep(Step.QueryLocationTerminated);
|
|
return locationTask.Result;
|
|
}
|
|
|
|
// Updates locking state
|
|
async Task UpdateLockingState(IGeolocation location, DateTime timeStamp)
|
|
{
|
|
// Step: Update backend.
|
|
InvokeCurrentStep(Step.UpdateLockingState);
|
|
try
|
|
{
|
|
await connectorFactor(true).Command.UpdateLockingStateAsync(
|
|
bike,
|
|
location != null
|
|
? new LocationDto.Builder
|
|
{
|
|
Latitude = location.Latitude,
|
|
Longitude = location.Longitude,
|
|
Accuracy = location.Accuracy ?? double.NaN,
|
|
Age = timeStamp.Subtract(location.Timestamp.DateTime),
|
|
}.Build()
|
|
: null);
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
//BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
|
|
if (exception is WebConnectFailureException)
|
|
{
|
|
// Copri server is not reachable.
|
|
Log.ForContext<T>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", bike);
|
|
await InvokeCurrentStateAsync(State.WebConnectFailed, exception.Message);
|
|
return;
|
|
}
|
|
else if (exception is ResponseException copriException)
|
|
{
|
|
// Copri server is not reachable.
|
|
Log.ForContext<T>().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", bike, copriException.Message, copriException.Response);
|
|
await InvokeCurrentStateAsync(State.ResponseIsInvalid, exception.Message);
|
|
return;
|
|
}
|
|
else
|
|
{
|
|
Log.ForContext<T>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", bike.Id, exception);
|
|
await InvokeCurrentStateAsync(State.BackendUpdateFailed, exception.Message);
|
|
return;
|
|
}
|
|
}
|
|
}
|
|
|
|
//// Start Action
|
|
//// Step: Start query geolocation data.
|
|
Log.ForContext<T>().Debug($"Starting step {Step.StartingQueryingLocation}...");
|
|
InvokeCurrentStep(Step.StartingQueryingLocation);
|
|
var ctsLocation = new CancellationTokenSource();
|
|
Task<IGeolocation> currentLocationTask = Task.FromResult<IGeolocation>(null);
|
|
var timeStampNow = DateTime.Now;
|
|
try
|
|
{
|
|
currentLocationTask = geolocation.GetAsync(ctsLocation.Token, timeStampNow);
|
|
}
|
|
catch (Exception ex)
|
|
{
|
|
// No location information available.
|
|
Log.ForContext<T>().Information("Starting query location failed. {Exception}", bike, ex);
|
|
await InvokeCurrentStateAsync(State.StartGeolocationException, ex.Message);
|
|
}
|
|
|
|
//// Step: Close lock.
|
|
IGeolocation currentLocation;
|
|
Log.ForContext<T>().Debug($"Starting step {Step.ClosingLock}...");
|
|
InvokeCurrentStep(Step.ClosingLock);
|
|
LockitLockingState? lockingState;
|
|
try
|
|
{
|
|
lockingState = await lockService[bike.LockInfo.Id].CloseAsync();
|
|
}
|
|
catch (Exception exception)
|
|
{
|
|
if (exception is OutOfReachException)
|
|
{
|
|
Log.ForContext<T>().Debug("Lock can not be closed. {Exception}", exception);
|
|
await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message);
|
|
}
|
|
else if (exception is CouldntCloseMovingException)
|
|
{
|
|
Log.ForContext<T>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
|
|
await InvokeCurrentStateAsync(State.CouldntCloseMovingError, exception.Message);
|
|
}
|
|
else if (exception is CouldntCloseBoltBlockedException)
|
|
{
|
|
Log.ForContext<T>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
|
|
await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message);
|
|
}
|
|
else
|
|
{
|
|
Log.ForContext<T>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
|
|
await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message);
|
|
}
|
|
|
|
Log.ForContext<T>().Error("Lock can not be closed. {Exception}", exception);
|
|
|
|
// Signal cts to cancel getting geolocation.
|
|
ctsLocation.Cancel();
|
|
|
|
// Wait until getting geolocation and stop polling has completed.
|
|
currentLocation = await WaitForPendingTasks(currentLocationTask);
|
|
|
|
// Update current state from exception
|
|
bike.LockInfo.State = exception is StateAwareException stateAwareException
|
|
? stateAwareException.State
|
|
: LockingState.UnknownDisconnected;
|
|
|
|
if (!isConnectedDelegate())
|
|
{
|
|
// Lock state can not be updated because there is no connected.
|
|
throw;
|
|
}
|
|
|
|
if (exception is OutOfReachException)
|
|
{
|
|
// Locking state can not be updated because lock is not connected.
|
|
throw;
|
|
}
|
|
|
|
// Update backend.
|
|
// Do this even if current lock state is open (lock state must not necessarily be open before try to open, i.e. something undefined between open and closed).
|
|
await UpdateLockingState(currentLocation, timeStampNow);
|
|
|
|
throw;
|
|
}
|
|
|
|
//// Step: Wait until getting geolocation and stop polling has completed.
|
|
currentLocation = await WaitForPendingTasks(currentLocationTask);
|
|
|
|
bike.LockInfo.State = lockingState?.GetLockingState() ?? LockingState.UnknownDisconnected;
|
|
|
|
// Keep geolocation where closing action occurred.
|
|
bike.LockInfo.Location = currentLocation;
|
|
|
|
if (!isConnectedDelegate())
|
|
{
|
|
return;
|
|
}
|
|
|
|
//// Step: Update backend.
|
|
await UpdateLockingState(currentLocation, timeStampNow);
|
|
}
|
|
}
|
|
}
|