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.Geolocation; using TINK.View; namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler { public class ReservedDisconnected : Base, IRequestHandler { /// Provides info about the smart device (phone, tablet, ...) /// View model to be used for progress report and unlocking/ locking view. public ReservedDisconnected( IBikeInfoMutable selectedBike, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, Func viewUpdateManager, ISmartDevice smartDevice, IViewService viewService, IBikesViewModel bikesViewModel, IUser activeUser) : base( selectedBike, AppResources.ActionCancelRequest, // Copri button text: "Reservierung abbrechen" true, // Show button to enable canceling reservation. isConnectedDelegate, connectorFactory, geolocation, lockService, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser) { LockitButtonText = AppResources.ActionSearchLock; IsLockitButtonVisible = true; // Show "Öffnen" button to enable unlocking } /// Cancel reservation. public async Task HandleRequestOption1() => await CancelReservation(); /// Connect to reserved bike ask whether to book bike bike or not and if yes open lock. /// public async Task HandleRequestOption2() => await ConnectLockAndBook(); /// Cancel reservation. public async Task CancelReservation() { BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending. var alertResult = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionCancelReservation, SelectedBike.GetFullDisplayName()), AppResources.QuestionAnswerYes, AppResources.QuestionAnswerNo); if (alertResult == false) { // User aborted cancel process Log.ForContext().Information("User selected reserved bike {l_oId} in order to cancel reservation but action was canceled.", SelectedBike.Id); BikesViewModel.IsIdle = true; return this; } Log.ForContext().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id); // Stop polling before cancel request. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation; IsConnected = IsConnectedDelegate(); try { await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike); // If canceling bike succedes remove bike because it is not ready to be booked again IsRemoveBikeRequired = true; } catch (Exception l_oException) { BikesViewModel.ActionText = string.Empty; if (l_oException is InvalidAuthorizationResponseException) { // Copri response is invalid. Log.ForContext().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id); await ViewService.DisplayAlert( AppResources.MessageCancelReservationBikeErrorGeneralTitle, l_oException.Message, AppResources.MessageAnswerOk); } else if (l_oException is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id); await ViewService.DisplayAlert( AppResources.MessageCancelReservationBikeErrorConnectionTitle, string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected reserved bike {l_oId} but cancel reservation failed. {@l_oException}.", SelectedBike.Id, l_oException); await ViewService.DisplayAlert( AppResources.MessageCancelReservationBikeErrorGeneralTitle, l_oException.Message, "OK"); } 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, SmartDevice, ViewService, BikesViewModel, ActiveUser); } Log.ForContext().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id); 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, SmartDevice, ViewService, BikesViewModel, ActiveUser); } /// Connect to reserved bike ask whether to book bike bike or not and if yes open lock. /// public async Task ConnectLockAndBook() { BikesViewModel.IsIdle = false; Log.ForContext().Information("Request to search for {bike} detected.", SelectedBike); // Stop polling before getting new auth-values. BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopUpdatePeridically(); BikesViewModel.ActionText = AppResources.ActivityTextQuerryServer; IsConnected = IsConnectedDelegate(); try { // Repeat reservation to get a new seed/ k_user value. await ConnectorFactory(IsConnected).Command.CalculateAuthKeys(SelectedBike); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("User selected requested bike {l_oId} to connect to lock. (Copri server not reachable).", SelectedBike.Id); await ViewService.DisplayAlert( AppResources.MessageErrorConnectTitle, $"{AppResources.ErrorConnectLockReservedBikeNoWebMessage}\r\n{exception.Message}\r\n{WebConnectFailureException.GetHintToPossibleExceptionsReasons}", AppResources.MessageAnswerOk); } else { Log.ForContext().Error("User selected requested bike {l_oId} to scan for lock. {@l_oException}", SelectedBike.Id, exception); await ViewService.DisplayAlert( AppResources.MessageErrorConnectTitle, $"{AppResources.ErrorConnectLockGeneralErrorMessage}\r\n{exception.Message}", AppResources.MessageAnswerOk); } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return this; } // Connect to lock. LockInfoTdo result = null; var continueConnect = true; var retryCount = 1; while (continueConnect && 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(retryCount)); } catch (Exception exception) { BikesViewModel.ActionText = string.Empty; if (exception is ConnectBluetoothNotOnException) { continueConnect = false; await ViewService.DisplayAlert( AppResources.MessageErrorConnectTitle, AppResources.ErrorFindLockBluetoothNotOn, AppResources.MessageAnswerOk); } else if (exception is ConnectLocationPermissionMissingException) { continueConnect = false; await ViewService.DisplayAlert( AppResources.MessageConnectLockErrorTitle, AppResources.ErrorFindLockLocationPermissionMissing, AppResources.MessageAnswerOk); } else if (exception is ConnectLocationOffException) { continueConnect = false; await ViewService.DisplayAlert( AppResources.MessageConnectLockErrorTitle, AppResources.ErrorFindLockLocationOff, AppResources.MessageAnswerOk); } else if (exception is OutOfReachException) { Log.ForContext().Debug("Lock can not be found because out of reach.. {Exception}", exception); continueConnect = await ViewService.DisplayAlert( AppResources.MessageErrorConnectTitle, AppResources.ErrorFindLockReservedBikeOutOfReachMessage, AppResources.MessageAnswerRetry, AppResources.MessageAnswerCancel); } else { Log.ForContext().Error("Lock can not be found. {Exception}", exception); string message; if (retryCount < 2) { message = AppResources.ErrorReservedSearchMessage; } else { message = AppResources.ErrorReservedSearchMessageEscalationLevel1; } Log.ForContext().Error("Lock state can not be retrieved. {Exception}", exception); continueConnect = await ViewService.DisplayAdvancedAlert( AppResources.MessageErrorConnectTitle, message, "", // bool IsReportLevelVerbose ? exception.Message : string.Empty, // or use ActiveUser.DebugLevel.HasFlag(Permissions.ReportLevel) instead? AppResources.MessageAnswerRetry, AppResources.MessageAnswerCancel); } if (continueConnect) { retryCount++; continue; } // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return this; } } if (result?.State == null) { Log.ForContext().Information("Lock for bike {bike} not found.", SelectedBike); BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.MessageErrorConnectTitle, AppResources.ErrorFindLockReservedBikeNoStausMessage, AppResources.MessageAnswerOk); // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return this; } var state = result.State.Value.GetLockingState(); SelectedBike.LockInfo.State = state; SelectedBike.LockInfo.Guid = result?.Guid ?? new Guid(); Log.ForContext().Information($"State for bike {SelectedBike.Id} updated successfully. Value is {SelectedBike.LockInfo.State}."); BikesViewModel.ActionText = string.Empty; // Ask whether to really book bike? var alertResult = await ViewService.DisplayAlert( string.Empty, string.Format(AppResources.QuestionOpenLockAndBookBike, SelectedBike.GetFullDisplayName()), 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, SmartDevice, 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.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } // Unlock bike. BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock; ILockService btLock; try { btLock = LockService[SelectedBike.LockInfo.Id]; SelectedBike.LockInfo.State = (await btLock.OpenAsync())?.GetLockingState() ?? LockingState.UnknownDisconnected; } 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.ErrorOpenLockOutOfReachMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenBoldIsBlockedException) { Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, AppResources.ErrorOpenLockBoldBlockedMessage, AppResources.MessageAnswerOk); } else if (exception is CouldntOpenBoldWasBlockedException) { Log.ForContext().Debug("Lock can not be opened. Bold was or is blocked. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockStillOpenTitle, AppResources.ErrorOpenLockBoldWasBlockedMessage, AppResources.MessageAnswerOk); } 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, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("Lock can not be opened. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorOpenLockTitle, exception.Message, AppResources.MessageAnswerOk); } SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException ? stateAwareException.State : LockingState.UnknownDisconnected; 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, SmartDevice, ViewService, BikesViewModel, ActiveUser); } if (SelectedBike.LockInfo.State != LockingState.Open) { // Opening lock failed. 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, SmartDevice, ViewService, BikesViewModel, ActiveUser); } BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; try { SelectedBike.LockInfo.BatteryPercentage = await btLock.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; var versionTdo = btLock.VersionInfo; if (versionTdo != null) { SelectedBike.LockInfo.VersionInfo = new VersionInfo.Builder { FirmwareVersion = versionTdo.FirmwareVersion, HardwareVersion = versionTdo.HardwareVersion, LockVersion = versionTdo.LockVersion, }.Build(); } 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); // Restart polling again. BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; await ViewUpdateManager().StartUpdateAyncPeridically(); BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; // Unlock GUI return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser); } } }