2023-08-31 12:20:06 +02:00
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>
2023-08-31 12:31:38 +02:00
/// Possible states of closing a lock.
2023-08-31 12:20:06 +02:00
/// </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 ;
}
}
}
2023-08-31 12:31:38 +02:00
//// Start Action
//// Step: Start query geolocation data.
2023-08-31 12:20:06 +02:00
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 ) ;
}
2023-08-31 12:31:38 +02:00
//// Step: Close lock.
2023-08-31 12:20:06 +02:00
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 ( ) ;
2023-08-31 12:31:38 +02:00
// Wait until getting geolocation and stop polling has completed.
2023-08-31 12:20:06 +02:00
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 ;
}
2023-08-31 12:31:38 +02:00
// Update backend.
2023-08-31 12:20:06 +02:00
// 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 ;
}
2023-08-31 12:31:38 +02:00
//// Step: Wait until getting geolocation and stop polling has completed.
2023-08-31 12:20:06 +02:00
currentLocation = await WaitForPendingTasks ( currentLocationTask ) ;
bike . LockInfo . State = lockingState ? . GetLockingState ( ) ? ? LockingState . UnknownDisconnected ;
// Keep geolocation where closing action occurred.
bike . LockInfo . Location = currentLocation ;
if ( ! isConnectedDelegate ( ) )
{
return ;
}
2023-08-31 12:31:38 +02:00
//// Step: Update backend.
2023-08-31 12:20:06 +02:00
await UpdateLockingState ( currentLocation , timeStampNow ) ;
}
}
}