using System; using System.Threading; using System.Threading.Tasks; using Serilog; using TINK.Repository.Request; using TINK.Services.BluetoothLock; using TINK.Services.Geolocation; namespace TINK.Model.Bikes.BikeInfoNS.BluetoothLock.Command { /// /// Provides functionality to get the locked bike location. /// public static class GetLockedLocationCommand { /// /// Possible steps of closing a lock. /// public enum Step { StartingQueryLocation, DisconnectingLockOnDisconnectedNoLocationError, } /// /// Possible steps of closing a lock. /// public enum State { DisconnetedNoLocationError, DisconnectError, QueryLocationSucceeded, QueryLocationFailed, } /// /// Interface to notify view model about steps/ state changes of closing process. /// public interface IGetLockedLocationCommandListener { /// /// 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); } /// /// Get current location. /// /// /// /// public static async Task InvokeAsync( IBikeInfoMutable bike, IGeolocationService geolocation, ILocksService lockService, Func dateTimeProvider = null, IGetLockedLocationCommandListener 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 //// Step: Start query geolocation data. InvokeCurrentStep(Step.StartingQueryLocation); // Get geolocation which was requested when closing lock. IGeolocation closingLockLocation = bike.LockInfo.Location; if (closingLockLocation != null) { // Location was available when closing bike. No further actions required. return 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(); } // Check if bike is around => geolocation information can be queried var deviceState = lockService[bike.LockInfo.Id].GetDeviceState(); if (deviceState != DeviceState.Connected) { // Geolocation can not be queried because bike is not around. Log.ForContext().Information("User selected booked bike {bikeId} but returning failed. There is no geolocation information available.", bike.Id); await InvokeCurrentStateAsync(State.DisconnetedNoLocationError, ""); //// Step: Disconnect lock. InvokeCurrentStep(Step.DisconnectingLockOnDisconnectedNoLocationError); try { bike.LockInfo.State = await lockService.DisconnectAsync(bike.LockInfo.Id, bike.LockInfo.Guid); Log.ForContext().Information("Lock from bike {bikeId} disconnected successfully.", bike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock from bike {bikeId} can not be disconnected. {@exception}", bike.Id, exception); await InvokeCurrentStateAsync(State.DisconnectError, exception.Message); } await InvokeCurrentStateAsync(State.QueryLocationSucceeded, ""); throw new Exception(); } // Query geolocation. var ctsLocation = new CancellationTokenSource(); try { closingLockLocation = await geolocation.GetAsync(ctsLocation.Token, DateTime.Now); Log.ForContext().Information("Query location of lock from bike {bikeId} successful."); } catch (Exception exception) { // No location information available. Log.ForContext().Information("Geolocation query failed. {@exception}", exception); await InvokeCurrentStateAsync(State.QueryLocationFailed, exception.Message); throw; } await InvokeCurrentStateAsync(State.QueryLocationSucceeded, string.Empty); // Update last lock state time // save geolocation data for sending to backend var currentLocationDto = closingLockLocation != null ? new LocationDto.Builder { Latitude = closingLockLocation.Latitude, Longitude = closingLockLocation.Longitude, Accuracy = closingLockLocation.Accuracy ?? double.NaN, Age = (dateTimeProvider != null ? dateTimeProvider() : DateTime.Now).Subtract(closingLockLocation.Timestamp.DateTime), }.Build() : null; return currentLocationDto; } } }