using System; using Serilog; using System.Threading.Tasks; using TINK.Model.Bike.BluetoothLock; using TINK.Model.Connector; using TINK.Model.State; using TINK.View; using TINK.Model.Repository.Exception; using TINK.Model.Services.Geolocation; using TINK.Services.BluetoothLock; using TINK.Services.BluetoothLock.Tdo; using TINK.MultilingualResources; using TINK.Model.Bikes.Bike.BluetoothLock; using TINK.Services.BluetoothLock.Exception; using TINK.Model.User; using TINK.Repository.Exception; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { public class DisposableDisconnected : Base, IRequestHandler { /// View model to be used for progress report and unlocking/ locking view. public DisposableDisconnected( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, Func viewUpdateManager, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionRequest, // Copri text: "Rad reservieren" true, // Show copri button to enable reserving and opening isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, viewService, bikesViewModel, activeUser) { LockitButtonText = GetType().Name; IsLockitButtonVisible = false; // If bike is not reserved/ booked app can not connect to lock } /// Gets the bike state. public override InUseStateEnum State => InUseStateEnum.Disposable; /// Reserve bike and connect to lock. public async Task HandleRequestOption1() { BikesViewModel.IsIdle = false; // Ask whether to really book bike? var alertResult = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionReserveBike, SelectedBike.GetDisplayName(), StateRequestedInfo.MaximumReserveTime.Minutes), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (alertResult == false) { // User aborted booking process Log.ForContext().Information("User selected availalbe bike {bike} in order to reserve but action was canceled.", SelectedBike); BikesViewModel.IsIdle = true; return this; } // Lock list to avoid multiple taps while copri action is pending. Log.ForContext().Information("Request to book and open lock for bike {bike} detected.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; // Stop polling before requesting bike. await ViewUpdateManager().StopUpdatePeridically(); BikesViewModel.ActionText = AppResources.ActivityTextReservingBike; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoReserve(SelectedBike); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is BookingDeclinedException) { // Too many bikes booked. Log.ForContext().Information("Request declined because maximum count of bikes {l_oException.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount); await ViewService.DisplayAlert( AppResources.MessageTitleHint, string.Format(AppResources.MessageReservationBikeErrorTooManyReservationsRentals, SelectedBike.Id, (exception as BookingDeclinedException).MaxBikesCount), AppResources.MessageAnswerOk); } else if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected availalbe bike {bike} but reserving failed (Copri server not reachable).", SelectedBike); await ViewService.DisplayAlert( "Verbingungsfehler beim Reservieren des Rads!", string.Format("{0}\r\n{1}", exception.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), "OK"); } else { Log.ForContext().Error("User selected availalbe bike {bike} but reserving failed. {@l_oException}", SelectedBike, exception); await ViewService.DisplayAlert( "Fehler beim Reservieren des Rads!", exception.Message, "OK"); } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); 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)); } catch (Exception exception) { // Do not display any messages here, because search is implicit. if (exception is OutOfReachException) { Log.ForContext().Debug("Lock state can not be retrieved, lock is out of reach. {Exception}", exception); BikesViewModel.ActionText = "Schloss außerhalb Reichweite"; } else { Log.ForContext().Error("Lock state can not be retrieved. {Exception}", exception); BikesViewModel.ActionText = "Schloss nicht gefunden"; } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } SelectedBike.LockInfo.State = result?.State?.GetLockingState() ?? LockingState.Disconnected; if (SelectedBike.LockInfo.State == LockingState.Disconnected) { // Do not display any messages here, because search is implicit. Log.ForContext().Information("Lock for bike {bike} not found.", SelectedBike); // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } SelectedBike.LockInfo.Guid = result?.Guid ?? new Guid(); Log.ForContext().Information("Lock found {bike} successfully.", SelectedBike); BikesViewModel.ActionText = string.Empty; // Ask whether to really book bike? alertResult = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.MessageOpenLockAndBookeBike, SelectedBike.GetDisplayName()), AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (alertResult == false) { // User aborted booking process Log.ForContext().Information("User selected recently requested bike {bike} in order to reserve but did deny to book bike.", SelectedBike); // Disconnect lock. BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; try { SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); } catch (Exception exception) { Log.ForContext().Error("Lock can not be disconnected. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } Log.ForContext().Information("User selected recently requested bike {bike} in order to book.", SelectedBike); // Book bike prior to opening lock. BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike); } catch (Exception l_oException) { BikesViewModel.ActionText = string.Empty; if (l_oException is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected recently requested bike {l_oId} but booking failed (Copri server not reachable).", SelectedBike.Id); await ViewService.DisplayAdvancedAlert( AppResources.MessageRentingBikeErrorConnectionTitle, WebConnectFailureException.GetHintToPossibleExceptionsReasons, l_oException.Message, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected recently requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException); await ViewService.DisplayAdvancedAlert( AppResources.MessageRentingBikeErrorGeneralTitle, string.Empty, l_oException.Message, AppResources.MessageAnswerOk); } BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } // Unlock bike. BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock; try { SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected; } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is OutOfReachException) { Log.ForContext().Debug("Lock can not be opened. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockOutOfReadMessage, "OK"); } else if (exception is CouldntOpenBoldBlockedException) { Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockMessage, "OK"); } else if (exception is CouldntOpenInconsistentStateExecption inconsistentState && inconsistentState.State == LockingState.Closed) { Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockStillClosedMessage, "OK"); } else { Log.ForContext().Error("Lock can not be opened. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, exception.Message, "OK"); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.Disconnected; BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } if (SelectedBike.LockInfo.State != LockingState.Open) { // Opening lock failed. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = ""; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync()); } catch (Exception exception) { if (exception is OutOfReachException) { Log.ForContext().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; } else { Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; } } // Lock list to avoid multiple taps while copri action is pending. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); } catch (Exception exception) { 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).", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; } 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. {response}.", SelectedBike, copriException.Response); BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; } else { Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; } } Log.ForContext().Information("User reserved bike {bike} successfully.", SelectedBike); BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); } public Task HandleRequestOption2() { throw new NotSupportedException(); } } }