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 { /// /// Possible steps of closing a lock. /// public enum Step { StartStopingPolling, StartingQueryingLocation, ClosingLock, WaitStopPollingQueryLocation, /// /// Notifies back end about modified lock state. /// UpdateLockingState, QueryLocationTerminated } /// /// Possible states of closing a lock. /// public enum State { OutOfReachError, CouldntCloseMovingError, CouldntCloseBoltBlockedError, GeneralCloseError, StartGeolocationException, WaitGeolocationException, WebConnectFailed, ResponseIsInvalid, BackendUpdateFailed } /// /// Interface to notify view model about steps/ state changes of closing process. /// public interface ICloseCommandListener { /// /// 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); } /// /// Closes the lock and updates copri. /// /// Interface to notify view model about steps/ state changes of closing process. /// Task which stops polling. public static async Task InvokeAsync( IBikeInfoMutable bike, IGeolocationService geolocation, ILocksService lockService, Func isConnectedDelegate, Func 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().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().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 WaitForPendingTasks(Task locationTask) { // Step: Wait until getting geolocation has completed. InvokeCurrentStep(Step.WaitStopPollingQueryLocation); Log.ForContext().Debug($"Waiting on steps {Step.StartingQueryingLocation} and {Step.StartStopingPolling} to finish..."); try { await Task.WhenAll(new List { locationTask, stopPollingTask ?? Task.CompletedTask }); } catch (Exception ex) { // No location information available. Log.ForContext().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().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().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().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().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().Debug($"Starting step {Step.StartingQueryingLocation}..."); InvokeCurrentStep(Step.StartingQueryingLocation); var ctsLocation = new CancellationTokenSource(); Task currentLocationTask = Task.FromResult(null); var timeStampNow = DateTime.Now; try { currentLocationTask = geolocation.GetAsync(ctsLocation.Token, timeStampNow); } catch (Exception ex) { // No location information available. Log.ForContext().Information("Starting query location failed. {Exception}", bike, ex); await InvokeCurrentStateAsync(State.StartGeolocationException, ex.Message); } //// Step: Close lock. IGeolocation currentLocation; Log.ForContext().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().Debug("Lock can not be closed. {Exception}", exception); await InvokeCurrentStateAsync(State.OutOfReachError, exception.Message); } else if (exception is CouldntCloseMovingException) { Log.ForContext().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); await InvokeCurrentStateAsync(State.CouldntCloseBoltBlockedError, exception.Message); } else { Log.ForContext().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); await InvokeCurrentStateAsync(State.GeneralCloseError, exception.Message); } Log.ForContext().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); } } }