using System; using System.Threading.Tasks; using ShareeBike.Model.Connector; using ShareeBike.MultilingualResources; using ShareeBike.View; using Serilog; using StartReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand; using AuthCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand; using StartRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand; using ConnectAndGetStateCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand; using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand; using System.ComponentModel; using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock; namespace ShareeBike.ViewModel.Bikes.Bike { /// /// Return bike action. /// /// Type of owner. public class StartReservationOrRentalActionViewModel : StartReservationCommand.IStartReservationCommandListener, AuthCommand.IAuthCommandListener, StartRentalCommand.IStartRentalCommandListener, ConnectAndGetStateCommand.IConnectAndGetStateCommandListener, DisconnectCommand.IDisconnectCommandListener, INotifyPropertyChanged { /// Notifies view about changes. public event PropertyChangedEventHandler PropertyChanged; /// /// View model to be used for progress report and unlocking/ locking view. /// private IBikesViewModel BikesViewModel { get; set; } /// /// View service to show modal notifications. /// private IViewService ViewService { get; } /// Object to start or stop update of view model objects from Copri. private Func ViewUpdateManager { get; } /// Bike private IBikeInfoMutable SelectedBike { get; set; } /// Provides a connector object. protected Func ConnectorFactory { get; } /// /// Constructs the object. /// /// Bike to close. /// Object to start or stop update of view model objects from Copri. /// View service to show modal notifications. /// View model to be used for progress report and unlocking/ locking view. /// public StartReservationOrRentalActionViewModel( IBikeInfoMutable selectedBike, Func viewUpdateManager, IViewService viewService, IBikesViewModel bikesViewModel) { SelectedBike = selectedBike; ViewUpdateManager = viewUpdateManager; ViewService = viewService; BikesViewModel = bikesViewModel ?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel)}-object. {nameof(bikesViewModel)} must not be null."); // Set parameter for RentalProcess View to initial value. BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id) { State = CurrentRentalProcess.None, StepIndex = 0, Result = CurrentStepStatus.None }); } /// /// Processes the start reservation progress. /// /// Current step to process. public void ReportStep(StartReservationCommand.Step step) { switch (step) { case StartReservationCommand.Step.ReserveBike: BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeFirstStepRequest; BikesViewModel.ActionText = AppResources.ActivityTextReservingBike; break; } } /// /// Processes the start reservation state. /// /// State to process. /// Textual details describing current state. public async Task ReportStateAsync(StartReservationCommand.State state, string details) { switch (state) { case StartReservationCommand.State.TooManyBikesError: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.MessageHintTitle, string.Format(AppResources.ErrorReservingBikeTooManyReservationsRentals, SelectedBike.Id, details), AppResources.MessageAnswerOk); break; case StartReservationCommand.State.WebConnectFailed: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorNoConnectionTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); break; case StartReservationCommand.State.GeneralStartReservationError: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAdvancedAlert( AppResources.ErrorReservingBikeTitle, details, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); break; } } /// /// Processes the start reservation progress. /// /// Current step to process. public void ReportStep(AuthCommand.Step step) { switch (step) { case AuthCommand.Step.Authenticate: BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeFirstStepAuthenticateInfo; BikesViewModel.ActionText = AppResources.ActivityTextAuthenticate; break; } } /// /// Processes the authentication state. /// /// State to process. /// Textual details describing current state. public async Task ReportStateAsync(AuthCommand.State state, string details) { switch (state) { case AuthCommand.State.WebConnectFailed: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorNoConnectionTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); break; case AuthCommand.State.GeneralAuthError: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAdvancedAlert( AppResources.ErrorAccountInvalidAuthorization, details, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); break; } } /// /// Processes the authentication progress. /// /// Current step to process. public void ReportStep(StartRentalCommand.Step step) { switch (step) { case StartRentalCommand.Step.RentBike: BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeSecondStepStartRental; BikesViewModel.RentalProcess.ImportantStepInfoText = SelectedBike.LockInfo.State != LockingState.Open ? AppResources.MarkingRentalProcessRequestBikeSecondStepStartRentalWait : string.Empty; break; } } /// /// Processes the start rental state. /// /// State to process. /// Textual details describing current state. public async Task ReportStateAsync(StartRentalCommand.State state, string details) { switch (state) { case StartRentalCommand.State.WebConnectFailed: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorNoConnectionTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); break; case StartRentalCommand.State.GeneralStartRentalError: BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed; BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorRentingBikeTitle, details, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); break; } } /// /// Processes the connect to lock progress. /// /// Current step to process. public void ReportStep(ConnectAndGetStateCommand.Step step) { switch (step) { case ConnectAndGetStateCommand.Step.ConnectLock: BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeFirstStepConnect; BikesViewModel.ActionText = AppResources.ActivityTextSearchingLock; break; case ConnectAndGetStateCommand.Step.GetLockingState: break; } } /// /// Processes the connect to lock state. /// /// State to process. /// Textual details describing current state. public async Task ReportStateAsync(ConnectAndGetStateCommand.State state, string details) { if (StartedWithReservation == true) { switch (state) { case ConnectAndGetStateCommand.State.OutOfReachError: BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorConnectLockTitle, AppResources.ErrorLockOutOfReach, AppResources.MessageAnswerOk); break; case ConnectAndGetStateCommand.State.BluetoothOff: BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorConnectLockTitle, AppResources.ErrorLockBluetoothNotOn, AppResources.MessageAnswerOk); break; case ConnectAndGetStateCommand.State.NoLocationPermission: BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorConnectLockTitle, AppResources.ErrorNoLocationPermission, AppResources.MessageAnswerOk); break; case ConnectAndGetStateCommand.State.LocationServicesOff: BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorConnectLockTitle, AppResources.ErrorLockLocationOff, AppResources.MessageAnswerOk); break; case ConnectAndGetStateCommand.State.GeneralConnectLockError: BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAdvancedAlert( AppResources.ErrorConnectLockTitle, details, AppResources.ErrorTryAgain, AppResources.MessageAnswerOk); break; } } else { switch (state) { case ConnectAndGetStateCommand.State.OutOfReachError: BikesViewModel.ActionText = AppResources.ActivityTextLockIsOutOfReach; break; case ConnectAndGetStateCommand.State.BluetoothOff: BikesViewModel.ActionText = string.Empty; await ViewService.DisplayAlert( AppResources.ErrorConnectLockTitle, AppResources.ErrorLockBluetoothNotOn, AppResources.MessageAnswerOk); break; case ConnectAndGetStateCommand.State.NoLocationPermission: BikesViewModel.ActionText = AppResources.ActivityTextErrorQueryLocationQuery; break; case ConnectAndGetStateCommand.State.LocationServicesOff: BikesViewModel.ActionText = AppResources.ActivityTextErrorQueryLocationQuery; break; case ConnectAndGetStateCommand.State.GeneralConnectLockError: BikesViewModel.ActionText = AppResources.ErrorConnectLockTitle; break; } } } /// /// Processes the disconnect lock progress. /// /// Current step to process. public void ReportStep(DisconnectCommand.Step step) { switch (step) { case DisconnectCommand.Step.DisconnectLock: BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; break; } } /// /// Processes the disconnect lock state. /// /// State to process. /// Textual details describing current state. public Task ReportStateAsync(DisconnectCommand.State state, string details) { switch (state) { case DisconnectCommand.State.GeneralDisconnectError: BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; break; } return Task.CompletedTask; } /// Request bike. public async Task StartReservationOrRentalAsync() { Log.ForContext().Information("User request to end rental of bike {bikeId}.", SelectedBike.Id); // lock GUI BikesViewModel.IsIdle = false; // Stop Updater BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StopAsync(); // 1. Step // Parameter for RentalProcess View BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id) { State = CurrentRentalProcess.StartReservationOrRental, StepIndex = 1, Result = CurrentStepStatus.None }); // 1a.Step: Reserve bike or authenticate, if already reserved if(SelectedBike.State.Value != Model.State.InUseStateEnum.Reserved) { startedWithReservation = false; try { #if USELOCALINSTANCE var command = new StartReservationCommand(SelectedBike, ConnectorFactory, ViewUpdateManager); await command.Invoke(this); #else await SelectedBike.ReserveBikeAsync(this); #endif } catch (Exception exception) { Log.ForContext().Information("Reservation of bike {bikeId} failed. {@exception}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StartAsync(); BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return; } } else { startedWithReservation = true; try { #if USELOCALINSTANCE var command = new AuthCommand(SelectedBike, ConnectorFactory, ViewUpdateManager); await command.Invoke(this); #else await SelectedBike.AuthAsync(this); #endif } catch (Exception exception) { Log.ForContext().Information("User could not be authenticated for bike {bikeId}. {@exception}", SelectedBike.Id, exception); BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StartAsync(); BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return; } } BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; // 2. Step BikesViewModel.RentalProcess.StepIndex = 2; BikesViewModel.RentalProcess.Result = CurrentStepStatus.None; BikesViewModel.RentalProcess.ImportantStepInfoText = string.Empty; // 2a.Step: Get locking state try { #if USELOCALINSTANCE var command = new ConnectLockAndGetLockingStateCommand(SelectedBike, LockService, ConnectorFactory, ViewUpdateManager); await command.Invoke(this); #else await SelectedBike.ConnectAsync(this); #endif } catch (Exception exception) { Log.ForContext().Information("Lock of bike {bikeId} could not be connected. {@exception}", SelectedBike.Id, exception); } var isStartRentalRequested = false; switch(SelectedBike.LockInfo.State) { case LockingState.UnknownDisconnected: // Lock is not connected: Bike is reserved confirmation BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeSecondStepReserve; isStartRentalRequested = false; break; case LockingState.Closed: case LockingState.UnknownFromHardwareError: if (startedWithReservation == false) { // Lock is connected: Ask whether to start rental & open lock BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeSecondStepRent; isStartRentalRequested = await ViewService.DisplayAlert( AppResources.QuestionRentalProcessRentBikeTitle, (SelectedBike.AaRideType == Model.Bikes.BikeInfoNS.BikeNS.AaRideType.AaRide ? AppResources.QuestionRentalProcessRentAndOpenAaBikeText : AppResources.QuestionRentalProcessRentAndOpenBikeText), AppResources.QuestionRentalProcessRentBikeRentAndOpenAnswer, AppResources.ActionCancel); } else { isStartRentalRequested = true; } break; case LockingState.Open: isStartRentalRequested = true; break; } // User does not want to start rental -> only reserve if (isStartRentalRequested == false) { // Bikes is reserved confirmation BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeSecondStepReserve; BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; await ViewService.DisplayAlert( AppResources.MessageRentalProcessReserveBikeFinishedTitle, String.Format(AppResources.MessageRentalProcessReserveBikeFinishedText), AppResources.MessageAnswerOk); } else { // 2a.Step: Rent bike try { #if USELOCALINSTANCE var command = new StartRentalCommand(SelectedBike, LockService, ConnectorFactory, ViewUpdateManager); await command.Invoke(this); #else await SelectedBike.RentBikeAsync(this); #endif continueWithOpenLock = true; BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded; } catch (Exception exception) { Log.ForContext().Information("Rental of bike {bikeId} could not be started. {@exception}", SelectedBike.Id, exception); switch (SelectedBike.LockInfo.State) { case LockingState.Closed: // Disconnect lock. try { BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; #if USELOCALINSTANCE var command = new DisconnectCommand(SelectedBike, LockService); await command.Invoke(this); #else await SelectedBike.DisconnectAsync(this); Log.ForContext().Information("Lock of bike {bikeId} was disconnected successfully.", SelectedBike.Id); #endif } catch (Exception disconnectException) { Log.ForContext().Information("Lock of bike {bikeId} can not be disconnected. {@exception}", SelectedBike.Id, disconnectException); } break; case LockingState.Open: await ViewService.DisplayAlert( AppResources.ErrorRentingBikeTitle, String.Format(AppResources.ErrorRentingBikeQuestionIfCloseLock, SelectedBike.Id), AppResources.MessageAnswerOk); break; } BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StartAsync(); BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; continueWithOpenLock = false; return; } } BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; await ViewUpdateManager().StartAsync(); BikesViewModel.RentalProcess.State = CurrentRentalProcess.None; BikesViewModel.ActionText = string.Empty; BikesViewModel.IsIdle = true; return; } /// /// Default value of user request to open lock = false. /// private bool continueWithOpenLock = false; /// /// True if to continue with open lock. /// public bool ContinueWithOpenLock { get { return continueWithOpenLock; } set { continueWithOpenLock = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContinueWithOpenLock))); } } /// /// Default value of user started request with reservation = false. /// private bool startedWithReservation = false; /// /// True if user started request with reservation. /// public bool StartedWithReservation { get { return startedWithReservation; } set { startedWithReservation = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StartedWithReservation))); } } } }