using System; using System.Threading.Tasks; using Serilog; using TINK.Model.Bikes.BikeInfoNS.BluetoothLock; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Model.User; using TINK.MultilingualResources; using TINK.Repository.Exception; using TINK.Services.BluetoothLock; using TINK.Services.BluetoothLock.Exception; using TINK.Services.BluetoothLock.Tdo; using TINK.Services.CopriApi.Exception; using TINK.Services.Geolocation; using TINK.View; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { public class DisposableDisconnected : Base, IRequestHandler { /// Provides info about the smart device (phone, tablet, ...). /// View model to be used for progress report and unlocking/ locking view. public DisposableDisconnected( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocationService geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionReserveBike, // Reserve Bike true, // Show button "Reserve Bike" isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = GetType().Name; IsLockitButtonVisible = false; // If bike is not reserved/ rented app can not connect to lock } /// Reserve bike, connect to lock, open lock and rent bike. public async Task HandleRequestOption1() => await ReserveRentBikeAndOpenLock(); public async Task HandleRequestOption2() => await UnsupportedRequest(); /// Reserve and rent bike. public async Task ReserveRentBikeAndOpenLock() { Log.ForContext().Information("User request to reserve bike {bikeId}.", SelectedBike.Id); BikesViewModel.IsIdle = false; // Ask whether to really reserve bike? var alertResult = await ViewService.DisplayAlert( string.Empty, string.Format( AppResources.QuestionReserveBike, SelectedBike.GetFullDisplayName(), SelectedBike.TariffDescription?.MaxReservationTimeSpan.TotalMinutes ?? 0), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (alertResult == false) { // User aborted booking process Log.ForContext().Information("User canceled request to reserve bike {bikeId}.", SelectedBike.Id); BikesViewModel.IsIdle = true; return this; } // Lock list to avoid multiple taps while copri action is pending. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; // Stop polling before requesting bike. await ViewUpdateManager().StopAsync(); BikesViewModel.ActionText = AppResources.ActivityTextReservingBike; IsConnected = IsConnectedDelegate(); // reserve bike try { await ConnectorFactory(IsConnected).Command.DoReserve(SelectedBike); Log.ForContext().Information("User reserved bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; Log.ForContext().Information("Request to reserve bike {bikeId} declined.", SelectedBike.Id); if (exception is BookingDeclinedException) { // Too many bikes booked. Log.ForContext().Error("Maximum count of bikes {exception.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount); await ViewService.DisplayAlert( AppResources.MessageHintTitle, string.Format(AppResources.ErrorReservingBikeTooManyReservationsRentals, SelectedBike.Id, (exception as BookingDeclinedException).MaxBikesCount), AppResources.MessageAnswerOk); } else if (exception is WebConnectFailureException || exception is RequestNotCachableException) { // Copri server is not reachable. Log.ForContext().Error("Copri server not reachable."); await ViewService.DisplayAlert( AppResources.ErrorNoConnectionTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("{@exception}", exception); await ViewService.DisplayAdvancedAlert( AppResources.ErrorReservingBikeTitle, exception.Message, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return this; } // Search for lock. LockInfoTdo result = null; BikesViewModel.ActionText = AppResources.ActivityTextSearchingLock; try { result = await LockService.ConnectAsync( new LockInfoAuthTdo.Builder { Id = SelectedBike.LockInfo.Id, Guid = SelectedBike.LockInfo.Guid, K_seed = SelectedBike.LockInfo.Seed, K_u = SelectedBike.LockInfo.UserKey }.Build(), LockService.TimeOut.GetSingleConnect(1)); Log.ForContext().Information("Connected to lock of bike {bikeId} successfully. Value is {lockState}.", SelectedBike.Id, SelectedBike.LockInfo.State); } catch (Exception exception) { Log.ForContext().Information("Connection to lock of bike {bikeId} failed."); // Do not display any messages here, because search is implicit. if (exception is OutOfReachException) { Log.ForContext().Error("Lock is out of reach."); BikesViewModel.ActionText = AppResources.ActivityTextLockIsOutOfReach; } else { Log.ForContext().Error("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextLockNotFound; } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // get current locking state SelectedBike.LockInfo.State = result?.State?.GetLockingState() ?? LockingState.UnknownDisconnected; if (SelectedBike.LockInfo.State == LockingState.UnknownDisconnected) { // Do not display any messages here, because search is implicit. Log.ForContext().Error("Lock is still not connected."); // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } SelectedBike.LockInfo.Guid = result?.Guid ?? new Guid(); // Ask whether to really book bike? alertResult = SelectedBike.LockInfo.State != LockingState.Open ? await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionOpenLockAndBookBike, SelectedBike.GetFullDisplayName()), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo) : await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionBookBike, SelectedBike.GetFullDisplayName()), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (alertResult == false) { // User aborted booking process Log.ForContext().Information("User request to not book reserved bike {bikeId}.", SelectedBike.Id); // Disconnect lock. BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; try { SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); Log.ForContext().Information("Disconnected from lock of bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock of bike {bikeId} can not be disconnected. {@exception}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } Log.ForContext().Information("User request to book reserved bike {bikeId}.", SelectedBike.Id); // Book bike prior to opening lock. BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; IsConnected = IsConnectedDelegate(); try { if (SelectedBike.LockInfo.State != LockingState.Open) { await ConnectorFactory(IsConnected).Command.DoBookAsync(SelectedBike, LockingAction.Open); } else { await ConnectorFactory(IsConnected).Command.DoBookAsync(SelectedBike); } Log.ForContext().Information("User booked bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; Log.ForContext().Information("Booking of bike {bikeId} failed.", SelectedBike.Id); if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Error("Copri server not reachable."); await ViewService.DisplayAlert( AppResources.ErrorNoConnectionTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("{@exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorRentingBikeTitle, exception.Message, AppResources.MessageAnswerOk); } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Unlock bike. ILockService btLock = LockService[SelectedBike.LockInfo.Id]; if (SelectedBike.LockInfo.State != LockingState.Open) { BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock; try { SelectedBike.LockInfo.State = (await btLock.OpenAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; Log.ForContext().Information("Lock from {bikeId} opened successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Lock from {bikeId} can not be opened.", SelectedBike.Id); BikesViewModel.ActionText = string.Empty; if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach."); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorLockOutOfReach, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenBoldIsBlockedException) { Log.ForContext().Debug("Bold is blocked."); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockBoldBlocked, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenBoldStatusIsUnknownException) { Log.ForContext().Debug("Bold status is unknown."); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockStatusUnknown, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenInconsistentStateExecption inconsistentState && inconsistentState.State == LockingState.Closed) { Log.ForContext().Debug("Lock reports that it is still closed."); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockStillClosed, AppResources.MessageAnswerOk); } else { Log.ForContext().Debug("{@exception}", exception); await ViewService.DisplayAdvancedAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorTryAgain, exception.Message, AppResources.MessageAnswerOk); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; } } // get current charging level BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = await btLock.GetBatteryPercentageAsync(); Log.ForContext().Information("Battery state of lock from {bikeId} read successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Battery state of lock from {bikeId} can not be read.", SelectedBike.Id); if (exception is OutOfReachException) { Log.ForContext().Debug("Lock is out of reach"); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; } else { Log.ForContext().Error("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; } } // Get lock infos. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; var versionTdo = btLock.VersionInfo; if (versionTdo != null) { SelectedBike.LockInfo.VersionInfo = new VersionInfo.Builder { FirmwareVersion = versionTdo.FirmwareVersion, HardwareVersion = versionTdo.HardwareVersion, LockVersion = versionTdo.LockVersion, }.Build(); } // update backend IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); Log.ForContext().Information("Backend updated for bike {bikeId} successfully.", SelectedBike.Id); } catch (Exception exception) { Log.ForContext().Information("Updating backend for bike {bikeId} failed.", SelectedBike.Id); if (exception is WebConnectFailureException) { // No web. Log.ForContext().Debug("Copri server not reachable. No web."); BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; } else if (exception is ResponseException copriException) { // Copri exception. Log.ForContext().Debug("{response}", copriException.Response); BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; } else { Log.ForContext().Debug("{@exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; } } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartAsync(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } /// Request is not supported, button should be disabled. /// public async Task UnsupportedRequest() { Log.ForContext().Error("Click of unsupported button click detected."); return await Task.FromResult(this); } } }