Version 3.0.381

This commit is contained in:
Anja 2024-04-09 12:53:23 +02:00
parent f963c0a219
commit 3a363acf3a
1525 changed files with 60589 additions and 125098 deletions

View file

@ -0,0 +1,133 @@
using System;
using System.ComponentModel;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.View;
using ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike.BC
{
/// <summary>
/// View model for a BC bike.
/// Provides functionality for views
/// - MyBikes
/// - BikesAtStation
/// </summary>
public class BikeViewModel : BikeViewModelBase, INotifyPropertyChanged
{
/// <summary>
/// Notifies GUI about changes.
/// </summary>
public override event PropertyChangedEventHandler PropertyChanged;
/// <summary> Holds object which manages requests. </summary>
private IRequestHandler RequestHandler { get; set; }
/// <summary> Raises events in order to update GUI.</summary>
public override void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs) => PropertyChanged?.Invoke(sender, eventArgs);
/// <summary>
/// Constructs a bike view model object.
/// </summary>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="selectedBike">Bike to be displayed.</param>
/// <param name="activeUser">Object holding logged in user or an empty user object.</param>
/// <param name="stateInfoProvider">Provides in use state information.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <param name="openUrlInBrowser">Delegate to open browser.</param>
public BikeViewModel(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Action<string> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser activeUser,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel,
Action<string> openUrlInBrowser) : base(isConnectedDelegate, connectorFactory, bikeRemoveDelegate, viewUpdateManager, smartDevice, viewService, selectedBike, activeUser, new ViewContext(PageContext.BikesAtStation), stateInfoProvider, bikesViewModel, openUrlInBrowser)
{
RequestHandler = activeUser.IsLoggedIn
? RequestHandlerFactory.Create(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
ActiveUser)
: new NotLoggedIn(
selectedBike.State.Value,
viewService,
bikesViewModel,
ActiveUser);
selectedBike.PropertyChanged += (sender, ev) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
}
/// <summary>
/// Handles BikeInfoMutable events.
/// Helper member to raise events. Maps model event change notification to view model events.
/// Todo: Check which events are received here and filter, to avoid event storm.
/// </summary>
/// <param name="p_strNameOfProp"></param>
public override void OnSelectedBikeStateChanged()
{
RequestHandler = RequestHandlerFactory.Create(
Bike,
IsConnectedDelegate,
ConnectorFactory,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(nameof(ButtonText)));
handler(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
}
}
/// <summary> Gets visibility of the copri command button. </summary>
public bool IsButtonVisible
=> RequestHandler.IsButtonVisible
&& Bike.DataSource == Model.Bikes.BikeInfoNS.BC.DataSource.Copri /* do not show button if data is from cache */ ;
/// <summary> Gets the text of the copri command button. </summary>
public string ButtonText => RequestHandler.ButtonText;
/// <summary> Processes request to perform a copri action (reserve bike and cancel reservation). </summary>
public System.Windows.Input.ICommand OnButtonClicked => new Xamarin.Forms.Command(async () =>
{
var lastHandler = RequestHandler;
var lastState = Bike.State.Value;
RequestHandler = await RequestHandler.HandleRequest();
CheckRemoveBike(Id, lastState);
if (lastHandler.GetType() == RequestHandler.GetType())
{
// No state change occurred.
return;
}
// State changed and instance of request handler was switched.
if (lastHandler.ButtonText != ButtonText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonText)));
}
if (lastHandler.IsButtonVisible != IsButtonVisible)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
}
});
}
}

View file

@ -0,0 +1,96 @@
using System;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler
{
public abstract class Base<T>
{
/// <summary>
/// View model to be used by subclasses for progress report and unlocking/ locking view.
/// </summary>
public IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// Gets a value indicating whether the button to reserve bike is visible or not.
/// </summary>
public bool IsButtonVisible { get; }
/// <summary>
/// Gets the name of the button when bike is disposable.
/// </summary>
public string ButtonText { get; }
/// <summary> Provides info about the smart device (phone, tablet, ...).</summary>
protected ISmartDevice SmartDevice;
/// <summary>
/// Reference on view service to show modal notifications and to perform navigation.
/// </summary>
protected IViewService ViewService { get; }
/// <summary> Provides a connector object.</summary>
protected Func<bool, IConnector> ConnectorFactory { get; }
/// <summary> Delegate to retrieve connected state. </summary>
protected Func<bool> IsConnectedDelegate { get; }
/// <summary> Object to manage update of view model objects from Copri.</summary>
protected Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Reference on the user </summary>
protected IUser ActiveUser { get; }
/// <summary> Bike to display. </summary>
protected T SelectedBike { get; }
/// <summary>Holds the is connected state. </summary>
private bool isConnected;
/// <summary>Gets the is connected state. </summary>
public bool IsConnected
{
get => isConnected;
set
{
if (value == isConnected)
return;
isConnected = value;
}
}
/// <summary>
/// Constructs the request handler base.
/// </summary>
/// <param name="selectedBike">Bike which is reserved or for which reservation is canceled.</param>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="bikesViewModel">View model to be used by subclasses for progress report and unlocking/ locking view.</param>
public Base(
T selectedBike,
string buttonText,
bool isCopriButtonVisible,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
ButtonText = buttonText;
IsButtonVisible = isCopriButtonVisible;
SelectedBike = selectedBike;
IsConnectedDelegate = isConnectedDelegate;
ConnectorFactory = connectorFactory;
ViewUpdateManager = viewUpdateManager;
SmartDevice = smartDevice;
ViewService = viewService;
ActiveUser = activeUser;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
}
}

View file

@ -0,0 +1,74 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BC;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler
{
public class Booked : IRequestHandler
{
/// <summary>
/// If a bike is booked unbooking can not be done by though app.
/// </summary>
public bool IsButtonVisible => false;
/// <summary>
/// Gets the name of the button when bike is cancel reservation.
/// </summary>
public string ButtonText => AppResources.ActionEndRental; // "Miete beenden"
/// <summary>
/// Reference on view service to show modal notifications and to perform navigation.
/// </summary>
protected IViewService ViewService { get; }
/// <summary>View model to be used for progress report and unlocking/ locking view.</summary>
public IBikesViewModel BikesViewModel { get; }
/// <summary> Executes user request to cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequest()
{
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
BikesViewModel.IsIdle = false;
Log.ForContext<BikesViewModel>().Error("User selected booked bike {l_oId}.", SelectedBike.Id);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
string.Empty,
"Rückgabe nur über Bordcomputer des Fahrrads durch Drücken der Taste 2 möglich!",
"Ok");
BikesViewModel.IsIdle = true;
return this;
}
/// <summary>
/// Bike to display.
/// </summary>
protected IBikeInfoMutable SelectedBike { get; }
/// <summary>Gets the is connected state. </summary>
public bool IsConnected { get; set; }
/// <summary> Gets if the bike has to be removed after action has been completed. </summary>
public bool IsRemoveBikeRequired => false;
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public Booked(
IBikeInfoMutable selectedBike,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
SelectedBike = selectedBike;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
}
}

View file

@ -0,0 +1,119 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.State;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.View;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public class Disposable : Base<BikeInfoMutable>, IRequestHandler
{
public Disposable(
BikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, selectedBike.State.Value.GetActionText(), true, isConnectedDelegate, connectorFactory, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser)
{
}
/// <summary> Request bike. </summary>
public async Task<IRequestHandler> HandleRequest()
{
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
BikesViewModel.IsIdle = false;
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
string.Format(
AppResources.QuestionReserveBike,
SelectedBike.GetFullDisplayName(),
SelectedBike.TariffDescription?.MaxReservationTimeSpan.TotalMinutes ?? 0),
AppResources.MessageAnswerYes,
AppResources.MessageAnswerNo);
if (l_oResult == false)
{
// User aborted booking process
Log.ForContext<Disposable>().Information("User selected centered bike {l_oId} in order to reserve but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
// Stop polling before requesting bike.
await ViewUpdateManager().StopAsync();
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoReserve(SelectedBike);
}
catch (Exception exception)
{
if (exception is BookingDeclinedException)
{
// Too many bikes booked.
Log.ForContext<Disposable>().Information("Request declined because maximum count of bikes {l_oException.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount);
BikesViewModel.ActionText = string.Empty;
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<Disposable>().Information("User selected centered bike {l_oId} but reserving failed (Copri server not reachable).", SelectedBike.Id);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<Disposable>().Error("User selected centered bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(AppResources.ErrorReservingBikeTitle, exception.Message, AppResources.MessageAnswerOk);
}
BikesViewModel.ActionText = string.Empty; // Todo: Remove this statement because in catch block ActionText is already set to empty above.
BikesViewModel.IsIdle = true;
return this;
}
finally
{
// Restart polling again.
await ViewUpdateManager().StartAsync();
// Update status text and unlock list of bikes because no more action is pending.
BikesViewModel.ActionText = string.Empty; // Todo: Move this statement in front of finally block because in catch block ActionText is already set to empty.
BikesViewModel.IsIdle = true;
}
Log.ForContext<Disposable>().Information("User reserved bike {l_oId} successfully.", SelectedBike.Id);
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,14 @@

using System.Threading.Tasks;
namespace ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler
{
public interface IRequestHandler : IRequestHandlerBase
{
/// <summary>
/// Performs the copri action to be executed when user presses the copri button managed by request handler.
/// </summary>
/// <returns>New handler object if action suceesed, same handler otherwise.</returns>
Task<IRequestHandler> HandleRequest();
}
}

View file

@ -0,0 +1,83 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.State;
using ShareeBike.Model.User;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler
{
public class NotLoggedIn : IRequestHandler
{
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public NotLoggedIn(
InUseStateEnum state,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
ButtonText = state.GetActionText();
IsIdle = true;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
public bool IsButtonVisible => true;
public bool IsIdle { get; private set; }
public string ButtonText { get; private set; }
public string ActionText { get => BikesViewModel.ActionText; private set => BikesViewModel.ActionText = value; }
/// <summary>
/// Reference on view service to show modal notifications and to perform navigation.
/// </summary>
protected IViewService ViewService { get; }
/// <summary>View model to be used for progress report and unlocking/ locking view.</summary>
public IBikesViewModel BikesViewModel { get; }
public bool IsConnected => throw new NotImplementedException();
/// <summary> Gets if the bike has to be removed after action has been completed. </summary>
public bool IsRemoveBikeRequired => false;
public async Task<IRequestHandler> HandleRequest()
{
Log.ForContext<NotLoggedIn>().Information("User selected bike but is not logged in.");
// User is not logged in
ActionText = string.Empty;
var l_oResult = await ViewService.DisplayAlert(
"Hinweis",
"Bitte anmelden vor Reservierung eines Fahrrads!\r\nAuf Anmeldeseite wechseln?",
"Ja",
"Nein");
if (l_oResult == false)
{
// User aborted booking process
IsIdle = true;
return this;
}
try
{
// Switch to login page
await ViewService.ShowPage("//LoginPage");
}
catch (Exception p_oException)
{
Log.ForContext<BikesViewModel>().Error("Ein unerwarteter Fehler ist in der Klasse NotLoggedIn aufgetreten. Kontext: Aufruf nach Reservierungsversuch ohne Anmeldung. {@Exception}", p_oException);
IsIdle = true;
return this;
}
IsIdle = true;
return this;
}
}
}

View file

@ -0,0 +1,112 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.View;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler
{
public class Reserved : Base<BikeInfoMutable>, IRequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public Reserved(
BikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, AppResources.ActionCancelReservation, true, isConnectedDelegate, connectorFactory, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser)
{
}
/// <summary> Executes user request to cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequest()
{
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = string.Empty;
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
string.Format("Reservierung für Fahrrad {0} aufheben?", SelectedBike.GetFullDisplayName()),
"Ja",
"Nein");
if (l_oResult == false)
{
// User aborted cancel process
Log.ForContext<Reserved>().Information("User selected reserved bike {l_oId} in order to cancel reservation but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
// Stop polling before cancel request.
await ViewUpdateManager().StopAsync();
try
{
IsConnected = IsConnectedDelegate();
await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike);
}
catch (Exception exception)
{
if (exception is InvalidAuthorizationResponseException)
{
// Copri response is invalid.
Log.ForContext<Reserved>().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id);
BikesViewModel.ActionText = String.Empty;
await ViewService.DisplayAlert("Fehler beim Stornieren der Buchung!", exception.Message, "OK");
BikesViewModel.IsIdle = true;
return this;
}
else if (exception is WebConnectFailureException
|| exception is RequestNotCachableException)
{
// Copri server is not reachable.
Log.ForContext<Reserved>().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id);
BikesViewModel.ActionText = String.Empty;
await ViewService.DisplayAlert(
"Verbingungsfehler beim Stornieren der Buchung!",
AppResources.ErrorNoWeb,
"OK");
BikesViewModel.IsIdle = true;
return this;
}
else
{
Log.ForContext<Reserved>().Error("User selected reserved bike {l_oId} but cancel reservation failed. {@l_oException}.", SelectedBike.Id, exception);
BikesViewModel.ActionText = String.Empty;
await ViewService.DisplayAlert("Fehler beim Stornieren der Buchung!", exception.Message, "OK");
BikesViewModel.IsIdle = true;
return this;
}
}
finally
{
// Restart polling again.
await ViewUpdateManager().StartAsync();
// Unlock list of bikes because no more action is pending.
BikesViewModel.ActionText = string.Empty; // Todo: Move this statement in front of finally block because in catch block ActionText is already set to empty.
}
Log.ForContext<Reserved>().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,61 @@
using System;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.View;
using ShareeBike.ViewModel.Bikes.Bike.BC.RequestHandler;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike.BC
{
public static class RequestHandlerFactory
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public static IRequestHandler Create(
BikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
switch (selectedBike.State.Value)
{
case Model.State.InUseStateEnum.Disposable:
// Bike can be booked.
return new Disposable(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case Model.State.InUseStateEnum.Reserved:
// Reservation can be canceled.
return new Reserved(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
default:
// No action using app possible.
return new Booked(
selectedBike,
viewService,
bikesViewModel,
activeUser);
}
}
}
}

View file

@ -0,0 +1,26 @@
using ShareeBike.Model.State;
namespace ShareeBike.ViewModel.Bikes.Bike.BC
{
public static class StateToText
{
/// <summary> Get button text for given copri state. </summary>
public static string GetActionText(this InUseStateEnum state)
{
switch (state)
{
case InUseStateEnum.Disposable:
return "Rad reservieren";
case InUseStateEnum.Reserved:
return "Reservierung aufheben";
case InUseStateEnum.Booked:
return "Miete beenden";
default:
return $"{state}";
}
}
}
}

View file

@ -0,0 +1,461 @@
using Serilog;
using System;
using System.ComponentModel;
using System.Text.RegularExpressions;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS;
using ShareeBike.Model.Bikes.BikeInfoNS.DriveNS.BatteryNS;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.State;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.View;
using Xamarin.Forms;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// Defines the type of BikesViewModel child items, i.e. BikesViewModel derives from ObservableCollection&ltBikeViewModelBase&gt.
/// Holds references to
/// - connection state services
/// - copri service
/// - view service
/// </summary>
public abstract class BikeViewModelBase
{
/// <summary>
/// Time format for text "Gebucht seit".
/// </summary>
public const string TIMEFORMAT = "dd. MMMM HH:mm";
/// <summary> Provides info about the smart device (phone, tablet, ...).</summary>
protected ISmartDevice SmartDevice;
/// <summary>
/// Reference on view service to show modal notifications and to perform navigation.
/// </summary>
protected IViewService ViewService { get; }
/// <summary> Provides a connect object.</summary>
protected Func<bool, IConnector> ConnectorFactory { get; }
/// <summary> Delegate to retrieve connected state. </summary>
protected Func<bool> IsConnectedDelegate { get; }
/// <summary> Removes bike from bikes view model. </summary>
protected Action<string> BikeRemoveDelegate { get; }
/// <summary> Object to manage update of view model objects from Copri.</summary>
public Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary>
/// Holds the bike to display.
/// </summary>
protected BikeInfoMutable Bike;
/// <summary> Reference on the user </summary>
protected IUser ActiveUser { get; }
/// <summary> Holds the view context in which bike view model is used.</summary>
protected ViewContext ViewContext { get; }
/// <summary>
/// Provides context related info.
/// </summary>
private IInUseStateInfoProvider StateInfoProvider { get; }
/// <summary>View model to be used for progress report and unlocking/ locking view.</summary>
protected IBikesViewModel BikesViewModel { get; }
/// <summary> Delegate to open browser. </summary>
private Action<string> OpenUrlInBrowser;
/// <summary>
/// Notifies GUI about changes.
/// </summary>
public abstract event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notifies children about changed bike state.
/// </summary>
public abstract void OnSelectedBikeStateChanged();
/// <summary> Raises events in order to update GUI.</summary>
public abstract void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs);
/// <summary>
/// Constructs a bike view model object.
/// </summary>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="selectedBike">Bike to be displayed.</param>
/// <param name="activeUser">Object holding logged in user or an empty user object.</param>
/// <param name="viewContext"> Holds the view context in which bike view model is used.</param>
/// <param name="stateInfoProvider">Provides in use state information.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <param name="openUrlInBrowser">Delegate to open browser.</param>
public BikeViewModelBase(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Action<string> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser activeUser,
ViewContext viewContext,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel,
Action<string> openUrlInBrowser)
{
IsConnectedDelegate = isConnectedDelegate;
ConnectorFactory = connectorFactory;
BikeRemoveDelegate = bikeRemoveDelegate;
ViewUpdateManager = viewUpdateManager;
SmartDevice = smartDevice;
ViewService = viewService;
Bike = selectedBike
?? throw new ArgumentException(string.Format("Can not construct {0}- object, bike object is null.", typeof(BikeViewModelBase)));
ActiveUser = activeUser
?? throw new ArgumentException(string.Format("Can not construct {0}- object, user object is null.", typeof(BikeViewModelBase)));
ViewContext = viewContext;
StateInfoProvider = stateInfoProvider
?? throw new ArgumentException(string.Format("Can not construct {0}- object, user object is null.", typeof(IInUseStateInfoProvider)));
selectedBike.PropertyChanged +=
(sender, eventargs) => OnSelectedBikePropertyChanged(eventargs.PropertyName);
var battery = selectedBike.Drive?.Battery;
if (battery != null)
{
battery.PropertyChanged += (_, args) =>
{
if (args.PropertyName == nameof(BatteryMutable.CurrentChargeBars))
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(CurrentChargeBars)));
}
};
}
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
OpenUrlInBrowser = openUrlInBrowser ?? (url => { Log.ForContext<BikeViewModelBase>().Error($"No browse service available to open {url}."); });
}
/// <summary>
/// Handles BikeInfoMutable events.
/// Helper member to raise events. Maps model event change notification to view model events.
/// </summary>
/// <param name="nameOfProp"></param>
private void OnSelectedBikePropertyChanged(string nameOfProp)
{
if (nameOfProp == nameof(State))
{
OnSelectedBikeStateChanged(); // Notify derived class about change of state.
}
var state = State;
if (LastState != state)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(State)));
LastState = state;
}
var stateText = StateText;
if (LastStateText != stateText)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(StateText)));
LastStateText = stateText;
}
var stateColor = StateColor;
if (LastStateColor != stateColor)
{
RaisePropertyChanged(this, new PropertyChangedEventArgs(nameof(StateColor)));
LastStateColor = stateColor;
}
}
/// <summary>
/// Gets the display name of the bike containing of bike id and type of bike..
/// </summary>
public string Name => Bike.GetDisplayName();
public string TypeOfBike => Bike.GetDisplayTypeOfBike();
/// <summary>
/// Gets whether bike is a AA bike (bike must be always returned a the same station) or AB bike (start and end stations can be different stations).
/// </summary>
public AaRideType? AaRideType => Bike.AaRideType;
public string WheelType => Bike.GetDisplayWheelType();
/// <summary>
/// Gets the unique Id of bike or an empty string, if no name is defined to avoid duplicate display of id.
/// </summary>
public string DisplayId => Bike.GetDisplayId();
/// <summary>
/// Gets the unique Id of bike used by derived model to determine which bike to remove.
/// </summary>
public string Id => Bike.Id;
public string StationId => $"Station {Bike.StationId}";
public string DisplayName => Bike.GetDisplayName();
public bool IsBikeWithCopriLock => Bike.LockModel == Model.Bikes.BikeInfoNS.BikeNS.LockModel.Sigo;
/// Returns if type of bike is a cargo pedelec bike.
public bool IsBatteryChargeVisible =>
Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec
&& (!Bike.Drive.Battery.IsHidden.HasValue /* no value means show battery level */ || Bike.Drive.Battery.IsHidden.Value == false);
/// Gets the image path for bike type City bike, CargoLong, Trike or Pedelec.
public string DisplayedBikeImageSourceString => $"bike_{Bike.TypeOfBike}_{Bike.Drive.Type}_{Bike.WheelType}.png";
/// <summary>
/// Gets the current charge level.
/// </summary>
public string CurrentChargeBars => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec
? Bike.Drive.Battery.CurrentChargeBars?.ToString() ?? string.Empty
: string.Empty;
/// <summary>
/// Gets the value if current charge level is low ( <= 1 ).
/// </summary>
public bool IsCurrentChargeLow => this.CurrentChargeBars == "1" || this.CurrentChargeBars == "0";
/// <summary>
/// Gets the current charge level.
/// </summary>
public string MaxChargeBars => Bike.Drive.Type == Model.Bikes.BikeInfoNS.DriveNS.DriveType.Pedelec
? Bike.Drive.Battery.MaxChargeBars?.ToString() ?? string.Empty
: string.Empty;
/// <summary>
/// Returns status of a bike as text (binds to GUI).
/// </summary>
/// <todo> Log invalid states for diagnose purposes.</todo>
public string StateText
{
get
{
switch (Bike.State.Value)
{
case InUseStateEnum.FeedbackPending:
return AppResources.StatusTextFeedbackPending;
case InUseStateEnum.Disposable:
return AppResources.StatusTextAvailable;
}
if (!ActiveUser.IsLoggedIn)
{
// Nobody is logged in.
switch (Bike.State.Value)
{
case InUseStateEnum.Reserved:
return GetReservedInfo(
Bike.State.RemainingTime,
Bike.StationId,
null); // Hide reservation code because no one but active user should see code
case InUseStateEnum.Booked:
return GetBookedInfo(
Bike.State.From,
Bike.StationId,
null); // Hide reservation code because no one but active user should see code
default:
return string.Format("Unbekannter status {0}.", Bike.State.Value);
}
}
switch (Bike.State.Value)
{
case InUseStateEnum.Reserved:
return Bike.State.MailAddress == ActiveUser.Mail
? GetReservedInfo(
Bike.State.RemainingTime,
Bike.StationId,
Bike.State.Code)
: "Fahrrad bereits reserviert durch anderen Nutzer.";
case InUseStateEnum.Booked:
return Bike.State.MailAddress == ActiveUser.Mail
? GetBookedInfo(
Bike.State.From,
Bike.StationId,
Bike.State.Code)
: "Fahrrad bereits gebucht durch anderen Nutzer.";
default:
return string.Format("Unbekannter status {0}.", Bike.State.Value);
}
}
}
/// <summary> Gets the value of property <see cref="StateColor"/> when PropertyChanged was fired. </summary>
private string LastStateText { get; set; }
/// <summary>
/// Gets reserved into display text.
/// </summary>
/// <todo>Log unexpected states.</todo>
/// <param name="p_oInUseState"></param>
/// <returns>Display text</returns>
private string GetReservedInfo(
TimeSpan? p_oRemainingTime,
string p_strStation = null,
string p_strCode = null)
{
return StateInfoProvider.GetReservedInfo(p_oRemainingTime, p_strStation, p_strCode);
}
/// <summary>
/// Gets booked into display text.
/// </summary>
/// <todo>Log unexpected states.</todo>
/// <param name="p_oInUseState"></param>
/// <returns>Display text</returns>
private string GetBookedInfo(
DateTime? p_oFrom,
string p_strStation = null,
string p_strCode = null)
{
return StateInfoProvider.GetBookedInfo(p_oFrom, p_strStation, p_strCode);
}
/// <summary>
/// Exposes the bike state.
/// </summary>
public InUseStateEnum State => Bike.State.Value;
/// <summary> Gets the value of property <see cref="State"/> when PropertyChanged was fired. </summary>
public InUseStateEnum LastState { get; set; }
/// <summary>
/// Gets the color which visualizes the state of bike in relation to logged in user.
/// </summary>
public Color StateColor
{
get
{
if (!ActiveUser.IsLoggedIn)
{
return Color.Default;
}
var l_oSelectedBikeState = Bike.State;
switch (l_oSelectedBikeState.Value)
{
case InUseStateEnum.Reserved:
return l_oSelectedBikeState.MailAddress == ActiveUser.Mail
? InUseStateEnum.Reserved.GetColor()
: Color.Red; // Bike is reserved by someone else
case InUseStateEnum.Booked:
return l_oSelectedBikeState.MailAddress == ActiveUser.Mail
? InUseStateEnum.Booked.GetColor()
: Color.Red; // Bike is booked by someone else
default:
return Color.Default;
}
}
}
/// <summary> Holds description about the tariff. </summary>
public TariffDescriptionViewModel TariffDescription => new TariffDescriptionViewModel(Bike.TariffDescription);
/// <summary> Gets the value of property <see cref="StateColor"/> when PropertyChanged was fired. </summary>
public Color LastStateColor { get; set; }
/// <summary> Gets first url from text.</summary>
/// <param name="htmlSource">url to extract text from.</param>
/// <returns>Gets first url or an empty string if on url is contained in text.</returns>
public static string GetUrlFirstOrDefault(string htmlSource)
{
if (string.IsNullOrEmpty(htmlSource))
return string.Empty;
try
{
var matches = new Regex(@"https://[-a-zA-Z0-9+&@#/%?=~_|!:, .;]*[-a-zA-Z0-9+&@#/%=~_|]").Matches(htmlSource);
return matches.Count > 0
? matches[0].Value
: string.Empty;
}
catch (Exception e)
{
Log.ForContext<BikeViewModelBase>().Error("Extracting URL failed. {Exception}", e);
return string.Empty;
}
}
/// <summary>
/// Check if bike has to be removed and if yes invoke remove delegate.
/// </summary>
/// <param name="Id">Id of bike to remove.</param>
/// <param name="lastState">Previous state used to decide whether to remove bike or not.</param>
public void CheckRemoveBike(string Id, InUseStateEnum lastState)
{
switch (ViewContext.Page)
{
case PageContext.MyBikes:
// Bike is shown on page My Bikes.
switch (Bike.State.Value)
{
case InUseStateEnum.FeedbackPending:
case InUseStateEnum.Reserved:
case InUseStateEnum.Booked:
// Bike has still to be shown at my bikes page to give feedback or manage bike.
break;
default:
BikeRemoveDelegate(Id);
break;
}
break;
case PageContext.BikesAtStation:
// Bike is shown on page Bike At Station.
switch (lastState != InUseStateEnum.Booked)
{
case true:
// Only remove bike if bike was rented before.
break;
default:
switch (ViewContext.StationId == Bike.StationId)
{
case true:
// Do not remove bike if bike is returned a current station.
break;
default:
BikeRemoveDelegate(Id);
break;
}
break;
}
break;
}
}
}
}

View file

@ -0,0 +1,72 @@
using System;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike
{
public static class BikeViewModelFactory
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="viewContext"> Holds the view context in which bike view model is used.</param>
/// <param name="stateInfoProvider">Provides in use state information.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <param name="openUrlInBrowser">Delegate to open browser.</param>
public static BikeViewModelBase Create(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocationService,
ILocksService lockService,
Action<string> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
Model.Bikes.BikeInfoNS.BC.BikeInfoMutable bikeInfo,
IUser activeUser,
ViewContext viewContext,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel,
Action<string> openUrlInBrowser)
{
if (bikeInfo is Model.Bikes.BikeInfoNS.BluetoothLock.BikeInfoMutable)
{
return new BluetoothLock.BikeViewModel(
isConnectedDelegate,
connectorFactory,
geolocationService,
lockService,
bikeRemoveDelegate,
viewUpdateManager,
smartDevice,
viewService,
bikeInfo as Model.Bikes.BikeInfoNS.BluetoothLock.BikeInfoMutable,
activeUser,
viewContext,
stateInfoProvider,
bikesViewModel,
openUrlInBrowser);
}
if (bikeInfo is Model.Bikes.BikeInfoNS.CopriLock.BikeInfoMutable)
{
return new CopriLock.BikeViewModel(
isConnectedDelegate,
connectorFactory,
bikeRemoveDelegate,
viewUpdateManager,
smartDevice,
viewService,
bikeInfo,
activeUser,
viewContext,
stateInfoProvider,
bikesViewModel,
openUrlInBrowser);
}
return null;
}
}
}

View file

@ -0,0 +1,280 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.BikeInfoMutable;
using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock
{
/// <summary>
/// View model for a ILockIt bike.
/// Provides functionality for views
/// - MyBikes
/// - BikesAtStation
/// </summary>
public class BikeViewModel : BikeViewModelBase, INotifyPropertyChanged, DisconnectCommand.IDisconnectCommandListener
{
public Xamarin.Forms.Command ShowTrackingInfoCommand { get; private set; }
public Xamarin.Forms.Command ShowRideTypeInfoCommand { get; private set; }
public Xamarin.Forms.Command ShowBikeIsBoundToCityInfoCommand { get; private set; }
/// <summary> Notifies GUI about changes. </summary>
public override event PropertyChangedEventHandler PropertyChanged;
private IGeolocationService GeolocationService { get; }
private ILocksService LockService { get; }
/// <summary> Holds object which manages requests. </summary>
private IRequestHandler RequestHandler { get; set; }
/// <summary> Raises events in order to update GUI.</summary>
public override void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs) => PropertyChanged?.Invoke(sender, eventArgs);
/// <summary> Raises events if property values changed in order to update GUI.</summary>
private void RaisePropertyChangedEvent(
IRequestHandler lastHandler,
string lastStateText = null,
Xamarin.Forms.Color? lastStateColor = null)
{
if (lastHandler.ButtonText != ButtonText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonText)));
}
if (lastHandler.IsButtonVisible != IsButtonVisible)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
}
if (lastHandler.LockitButtonText != LockitButtonText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LockitButtonText)));
}
if (lastHandler.IsLockitButtonVisible != IsLockitButtonVisible)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLockitButtonVisible)));
}
if (RequestHandler.ErrorText != lastHandler.ErrorText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ErrorText)));
}
if (!string.IsNullOrEmpty(lastStateText) && lastStateText != StateText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateText)));
}
if (lastStateColor != null && lastStateColor != StateColor)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateColor)));
}
}
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly DisconnectLockActionViewModel<BikeViewModel> _disconnectLockActionViewModel;
/// <summary>
/// Processes the renting a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(DisconnectCommand.Step step) => _disconnectLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the renting a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(DisconnectCommand.State state, string details) => await _disconnectLockActionViewModel.ReportStateAsync(state, details);
/// <summary>
/// Constructs a bike view model object.
/// </summary>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="selectedBike">Bike to be displayed.</param>
/// <param name="user">Object holding logged in user or an empty user object.</param>
/// <param name="viewContext"> Holds the view context in which bike view model is used.</param>
/// <param name="stateInfoProvider">Provides in use state information.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <param name="openUrlInBrowser">Delegate to open browser.</param>
public BikeViewModel(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocationService,
ILocksService lockService,
Action<string> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser user,
ViewContext viewContext,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel,
Action<string> openUrlInBrowser) : base(isConnectedDelegate, connectorFactory, bikeRemoveDelegate, viewUpdateManager, smartDevice, viewService, selectedBike, user, viewContext, stateInfoProvider, bikesViewModel, openUrlInBrowser)
{
ShowTrackingInfoCommand = new Xamarin.Forms.Command(async () => {
await ViewService.DisplayAlert(
"Tracking",
TariffDescription.TrackingInfoText,
AppResources.MessageAnswerOk);
});
ShowRideTypeInfoCommand = new Xamarin.Forms.Command(async () => {
await ViewService.DisplayAlert(
AppResources.MessageAaRideTypeInfoTitle,
TariffDescription.RideTypeText,
AppResources.MessageAnswerOk);
});
ShowBikeIsBoundToCityInfoCommand = new Xamarin.Forms.Command(async () => {
// later, if value comes from backend: message = TariffDescription.CityAreaType
await ViewService.DisplayAlert(
AppResources.MessageBikeIsBoundToCityInfoTitle,
String.Format(AppResources.MessageBikeIsBoundToCityInfoText,selectedBike.TypeOfBike),
AppResources.MessageAnswerOk);
});
_disconnectLockActionViewModel = new DisconnectLockActionViewModel<BikeViewModel>(
selectedBike,
viewUpdateManager,
bikesViewModel);
RequestHandler = user.IsLoggedIn
? RequestHandlerFactory.Create(
selectedBike,
isConnectedDelegate,
connectorFactory,
geolocationService,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
user)
: new NotLoggedIn(
selectedBike.State.Value,
viewService,
bikesViewModel);
GeolocationService = geolocationService
?? throw new ArgumentException($"Can not instantiate {this.GetType().Name}-object. Parameter {nameof(geolocationService)} can not be null.");
LockService = lockService
?? throw new ArgumentException($"Can not instantiate {this.GetType().Name}-object. Parameter {nameof(lockService)} can not be null.");
selectedBike.PropertyChanged += (sender, ev) =>
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLockitButtonVisible)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OnButtonClicked)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(OnLockitButtonClicked)));
};
}
/// <summary>
/// Handles BikeInfoMutable events.
/// Helper member to raise events. Maps model event change notification to view model events.
/// Todo: Check which events are received here and filter, to avoid event storm.
/// </summary>
public override async void OnSelectedBikeStateChanged()
{
if (Bike.State.Value == Model.State.InUseStateEnum.Disposable)
{
await _disconnectLockActionViewModel.DisconnectLockAsync();
}
var lastHandler = RequestHandler;
RequestHandler = RequestHandlerFactory.Create(
Bike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
RaisePropertyChangedEvent(lastHandler);
}
public bool IsBikeBoundToCity
=> Bike.AaRideType == ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.AaRideType.NoAaRide ? true : false;
/// <summary> Gets visibility of the copri command button. </summary>
public bool IsButtonVisible
=> RequestHandler.IsButtonVisible;
/// <summary> Gets the text of the copri command button. </summary>
public string ButtonText => RequestHandler.ButtonText;
/// <summary> Gets visibility of the ILockIt command button. </summary>
public bool IsLockitButtonVisible
=> RequestHandler.IsLockitButtonVisible;
/// <summary> Gets the text of the ILockIt command button. </summary>
public string LockitButtonText => RequestHandler.LockitButtonText;
/// <summary> Processes request to perform a copri action (reserve bike and cancel reservation).
/// Button only enabled if data is up to date = not from cache. </summary>
public System.Windows.Input.ICommand OnButtonClicked => new Xamarin.Forms.Command(async () => await ClickButton(RequestHandler.HandleRequestOption1()));
/// <summary> Processes request to perform a ILockIt action (unlock bike and lock bike).
/// Button only enabled if data is up to date = not from cache. </summary>
public System.Windows.Input.ICommand OnLockitButtonClicked => new Xamarin.Forms.Command(async () => await ClickButton(RequestHandler.HandleRequestOption2()));
/// <summary> Processes request to perform a copri action (reserve bike and cancel reservation). </summary>
private async Task ClickButton(Task<IRequestHandler> handleRequest)
{
var lastHandler = RequestHandler;
var lastState = Bike.State.Value;
var lastStateText = StateText;
var lastStateColor = StateColor;
RequestHandler = await handleRequest;
CheckRemoveBike(Id, lastState);
if (RuntimeHelpers.Equals(lastHandler, RequestHandler))
{
// No state change occurred (same instance is returned).
return;
}
RaisePropertyChangedEvent(
lastHandler,
lastStateText,
lastStateColor);
}
public string ErrorText => RequestHandler.ErrorText;
}
}

View file

@ -0,0 +1,414 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.Logging;
using ShareeBike.View;
using CloseCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using CancelReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using System.ComponentModel;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock
{
/// <summary>
/// View model for action close bluetooth lock.
/// </summary>
/// <typeparam name="T"></typeparam>
internal class CloseLockActionViewModel<T> : CloseCommand.ICloseCommandListener, CancelReservationCommand.ICancelReservationCommandListener, DisconnectCommand.IDisconnectCommandListener, INotifyPropertyChanged
{
/// <summary> Notifies view about changes. </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// View service to show modal notifications.
/// </summary>
private IViewService ViewService { get; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike close. </summary>
private Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable SelectedBike { get; }
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to close.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="viewService">View service to show modal notifications.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public CloseLockActionViewModel(
Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable selectedBike,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>
/// Processes the close lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(CloseCommand.Step step)
{
switch (step)
{
case CloseCommand.Step.StartStopingPolling:
break;
case CloseCommand.Step.StartingQueryingLocation:
// 1a.Step: Start query geolocation data.
BikesViewModel.ActionText = AppResources.ActivityTextQueryLocationStart;
break;
case CloseCommand.Step.ClosingLock:
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
break;
case CloseCommand.Step.WaitStopPollingQueryLocation:
BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation;
break;
case CloseCommand.Step.QueryLocationTerminated:
break;
case CloseCommand.Step.UpdateLockingState:
// 1b.Step: Sent info to backend
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessCloseLockStepUpload;
BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessCloseLockCheckLock;
break;
}
}
/// <summary>
/// Processes the close lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CloseCommand.State state, string details)
{
switch (state)
{
case CloseCommand.State.OutOfReachError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorLockOutOfReach,
AppResources.MessageAnswerOk);
break;
case CloseCommand.State.CouldntCloseMovingError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorLockMoving,
AppResources.MessageAnswerOk);
break;
case CloseCommand.State.CouldntCloseBoltBlockedError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoltBlocked,
AppResources.MessageAnswerOk);
break;
case CloseCommand.State.GeneralCloseError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
details,
AppResources.MessageAnswerOk);
break;
case CloseCommand.State.WebConnectFailed:
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
break;
case CloseCommand.State.ResponseIsInvalid:
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
break;
case CloseCommand.State.BackendUpdateFailed:
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
break;
}
}
/// <summary>
/// Processes the start reservation progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(CancelReservationCommand.Step step)
{
switch (step)
{
case CancelReservationCommand.Step.CancelReservation:
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessCancelReservation;
BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation;
break;
}
}
/// <summary>
/// Processes the start reservation state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CancelReservationCommand.State state, string details)
{
switch (state)
{
case CancelReservationCommand.State.InvalidResponse:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCancelReservationTitle,
AppResources.ErrorAccountInvalidAuthorization,
AppResources.MessageAnswerOk);
break;
case CancelReservationCommand.State.WebConnectFailed:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
break;
case CancelReservationCommand.State.GeneralCancelReservationError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAdvancedAlert(
AppResources.ErrorCancelReservationTitle,
details,
AppResources.ErrorTryAgain,
AppResources.MessageAnswerOk);
break;
}
}
/// <summary>
/// Processes the disconnect from lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(DisconnectCommand.Step step)
{
switch (step)
{
case DisconnectCommand.Step.DisconnectLock:
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessCloseLockStepUpload;
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
break;
}
}
/// <summary>
/// Processes the disconnect from lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(DisconnectCommand.State state, string details)
{
switch (state)
{
case DisconnectCommand.State.GeneralDisconnectError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAdvancedAlert(
AppResources.ErrorAccountInvalidAuthorization,
details,
AppResources.ErrorTryAgain,
AppResources.MessageAnswerOk);
break;
}
}
/// <summary> Close lock in order to pause ride and update COPRI lock state.</summary>
public async Task CloseLockAsync()
{
Log.ForContext<T>().Information("User request to close lock of bike {bikeId}.", SelectedBike.Id);
// lock GUI
BikesViewModel.IsIdle = false;
// Stop Updater
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
var stopPollingTask = ViewUpdateManager().StopAsync();
// Clear logging memory sink to avoid passing log data not related to returning of bike to backend.
// Log data is passed to backend when calling CopriCallsHttps.DoReturn().
MemoryStackSink.ClearMessages();
// 1. Step
// Parameter for RentalProcess View
switch(SelectedBike.State.Value)
{
case Model.State.InUseStateEnum.Reserved:
BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id)
{
State = CurrentRentalProcess.CloseLockAndCancelReservation,
StepIndex = 1,
Result = CurrentStepStatus.None
});
break;
case Model.State.InUseStateEnum.Disposable:
BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id)
{
State = CurrentRentalProcess.CloseDisposableLock,
StepIndex = 1,
Result = CurrentStepStatus.None
});
break;
default:
BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id)
{
State = CurrentRentalProcess.CloseLock,
StepIndex = 1,
Result = CurrentStepStatus.None
});
break;
}
// Close Lock
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessCloseLockStepCloseLock;
BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessCloseLockObserve;
try
{
#if USELOCALINSTANCE
var command = new CloseCommand(SelectedBike, GeolocationService, LockService, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.CloseLockAsync(this, stopPollingTask);
#endif
Log.ForContext<T>().Information("Lock of bike {bikeId} closed successfully.", SelectedBike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be closed. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
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;
if (SelectedBike.State.Value == Model.State.InUseStateEnum.Reserved)
{
// Cancel reservation
try
{
#if USELOCALINSTANCE
var command = new CancelReservationCommand(SelectedBike, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.CancelReservationAsync(this);
#endif
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Reservation of bike {bikeId} could not be canceled. {@exception}", SelectedBike.Id, exception);
}
}
// If bike is not reserved/booked, disconnect lock
if (SelectedBike.State.Value == Model.State.InUseStateEnum.Disposable)
{
try
{
#if USELOCALINSTANCE
var command = new DisconnectCommand(SelectedBike, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.DisconnectAsync(this);
#endif
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} could not be disconnected. {@exception}", SelectedBike.Id, exception);
}
}
else
{
// Question if park bike or end rental
IsEndRentalRequested = await ViewService.DisplayAlert(
AppResources.QuestionRentalProcessCloseLockEndOrContinueTitle,
AppResources.QuestionRentalProcessCloseLockEndOrContinueText,
AppResources.ActionEndRental,
AppResources.QuestionRentalProcessCloseLockContinueRentalAnswer);
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded;
// Message for parking bike
if (IsEndRentalRequested == false)
{
Log.ForContext<T>().Information("User request to park bike {bikeId}.", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.MessageRentalProcessCloseLockFinishedTitle,
AppResources.MessageRentalProcessCloseLockFinishedText,
AppResources.MessageAnswerOk);
}
}
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
BikesViewModel.IsIdle = true;
return;
}
/// <summary>
/// Default value of user request to end rental = false.
/// </summary>
private bool isEndRentalRequested = false;
/// <summary>
/// True if user requested End rental.
/// </summary>
public bool IsEndRentalRequested
{
get { return isEndRentalRequested; }
set
{
isEndRentalRequested = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsEndRentalRequested)));
}
}
}
}

View file

@ -0,0 +1,240 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.MultilingualResources;
using ShareeBike.View;
using Serilog;
using ConnectAndGetStateCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
using AuthCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.AuthCommand;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// Return bike action.
/// </summary>
/// <typeparam name="T">Type of owner.</typeparam>
public class ConnectLockActionViewModel<T> :
AuthCommand.IAuthCommandListener,
ConnectAndGetStateCommand.IConnectAndGetStateCommandListener
{
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// View service to show modal notifications.
/// </summary>
private IViewService ViewService { get; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike </summary>
private IBikeInfoMutable SelectedBike { get; set; }
/// <summary> Provides a connector object.</summary>
protected Func<bool, IConnector> ConnectorFactory { get; }
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to close.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="viewService">View service to show modal notifications.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public ConnectLockActionViewModel(
IBikeInfoMutable selectedBike,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel<T>)}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>
/// Processes the start reservation progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(AuthCommand.Step step)
{
switch (step)
{
case AuthCommand.Step.Authenticate:
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeFirstStepAuthenticateInfo;
BikesViewModel.ActionText = AppResources.ActivityTextAuthenticate;
break;
}
}
/// <summary>
/// Processes the authentication state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
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;
}
}
/// <summary>
/// Processes the connect to lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
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;
}
}
/// <summary>
/// Processes the connect to lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(ConnectAndGetStateCommand.State state, string details)
{
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;
}
}
/// <summary> Search and connect to lock. </summary>
public async Task ConnectLockAsync()
{
Log.ForContext<T>().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();
// 1a.Step: Authenticate
try
{
#if USELOCALINSTANCE
var command = new AuthCommand(SelectedBike, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.AuthAsync(this);
#endif
Log.ForContext<T>().Information("User authenticated for bike {bikeId} successfully.", SelectedBike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("User could not be authenticated for bike {bikeId}. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
}
// 1b.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
Log.ForContext<T>().Information("Lock of {bikeId} connected successfully.", SelectedBike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} could not be connected. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
}
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return;
}
}
}

View file

@ -0,0 +1,117 @@
using System;
using System.Threading.Tasks;
using ShareeBike.MultilingualResources;
using Serilog;
using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// Return bike action.
/// </summary>
/// <typeparam name="T">Type of owner.</typeparam>
public class DisconnectLockActionViewModel<T> :
DisconnectCommand.IDisconnectCommandListener
{
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike </summary>
private IBikeInfoMutable SelectedBike { get; set; }
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to close.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public DisconnectLockActionViewModel(
IBikeInfoMutable selectedBike,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
ViewUpdateManager = viewUpdateManager;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>
/// Processes the disconnect lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(DisconnectCommand.Step step)
{
switch (step)
{
case DisconnectCommand.Step.DisconnectLock:
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
break;
}
}
/// <summary>
/// Processes the disconnect lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public Task ReportStateAsync(DisconnectCommand.State state, string details)
{
switch (state)
{
case DisconnectCommand.State.GeneralDisconnectError:
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
break;
}
return Task.CompletedTask;
}
/// <summary> Disconnect lock. </summary>
public async Task DisconnectLockAsync()
{
Log.ForContext<T>().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();
try
{
#if USELOCALINSTANCE
var command = new DisonnectCommand(SelectedBike, LockService);
await command.Invoke(this);
#else
await SelectedBike.DisconnectAsync(this);
#endif
Log.ForContext<T>().Information("Lock of {bikeId} disconnected successfully.", SelectedBike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} could not be disconnected. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
}
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return;
}
}
}

View file

@ -0,0 +1,224 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.Logging;
using ShareeBike.View;
using static ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using System.ComponentModel;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock
{
/// <summary>
/// View model for action open bluetooth lock.
/// </summary>
/// <typeparam name="T"></typeparam>
internal class OpenLockActionViewModel<T> : IOpenCommandListener
{
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// View service to show modal notifications.
/// </summary>
private IViewService ViewService { get; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike open. </summary>
private Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable SelectedBike { get; }
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to open.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="viewService">View service to show modal notifications.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public OpenLockActionViewModel(
Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable selectedBike,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>
/// Processes the open lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(Step step)
{
switch (step)
{
case Step.OpeningLock:
// 1a.Step: Open lock
BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock;
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessOpenLockStepOpenLock;
BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessOpenLockObserve;
break;
case Step.GetLockInfos:
// 1b.Step: Get lock infos
BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessOpenLockWait;
break;
case Step.WaitStopPolling:
// 1c.Step: Wait for polling to be stopped
break;
case Step.UpdateLockingState:
// 1c.Step: Sent info to backend
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessOpenLockStepUpload;
BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessOpenLockWait;
break;
}
}
/// <summary>
/// Processes the open lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(State state, string details)
{
switch (state)
{
case State.OutOfReachError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorLockOutOfReach,
AppResources.MessageAnswerOk);
break;
case State.CouldntOpenBoldStatusIsUnknownError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStatusUnknown,
AppResources.MessageAnswerOk);
break;
case State.CouldntOpenBoldIsBlockedError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockBoldBlocked,
AppResources.MessageAnswerOk);
break;
case State.CouldntOpenInconsistentStateError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStillClosed,
AppResources.MessageAnswerOk);
break;
case State.GeneralOpenError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
details,
AppResources.MessageAnswerOk);
break;
case State.StopPollingFailed:
break;
case State.WebConnectFailed:
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
break;
case State.ResponseIsInvalid:
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
break;
case State.BackendUpdateFailed:
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
break;
}
}
/// <summary> Open lock in order to pause ride and update COPRI lock state.</summary>
public async Task OpenLockAsync()
{
Log.ForContext<T>().Information("User request to open lock of bike {bikeId}.", SelectedBike.Id);
// lock GUI
BikesViewModel.IsIdle = false;
// Stop Updater
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
var stopPollingTask = ViewUpdateManager().StopAsync();
// Clear logging memory sink to avoid passing log data not related to returning of bike to backend.
// Log data is passed to backend when calling CopriCallsHttps.DoReturn().
MemoryStackSink.ClearMessages();
// 1. Step
// Parameter for RentalProcess View
BikesViewModel.StartRentalProcess(new RentalProcessViewModel(SelectedBike.Id)
{
State = CurrentRentalProcess.OpenLock,
StepIndex = 1,
Result = CurrentStepStatus.None
});
try
{
#if USELOCALINSTANCE
var command = new OpenCommand(SelectedBike, GeolocationService, LockService, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.OpenLockAsync(this, stopPollingTask);
#endif
Log.ForContext<T>().Information("Lock of bike {bikeId} opened successfully.", SelectedBike.Id);
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be opened. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
BikesViewModel.IsIdle = true;
return;
}
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
//Confirmation message
Log.ForContext<T>().Information("User request to park bike {bikeId}.", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.MessageRentalProcessOpenLockFinishedTitle,
AppResources.MessageRentalProcessOpenLockFinishedText,
AppResources.MessageAnswerOk);
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
BikesViewModel.IsIdle = true;
return;
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public abstract class Base : BC.RequestHandler.Base<Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable>
{
/// <summary>
/// Constructs the request handler base.
/// </summary>
/// <param name="selectedBike">Bike which is reserved or for which reservation is canceled.</param>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public Base(
Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable selectedBike,
string buttonText,
bool isCopriButtonVisible,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, buttonText, isCopriButtonVisible, isConnectedDelegate, connectorFactory, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser)
{
GeolocationService = geolocation
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. Parameter {nameof(geolocation)} must not be null.");
LockService = lockService
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. Parameter {nameof(lockService)} must not be null.");
}
/// <summary>
/// Service to query geolocation information.
/// </summary>
protected IGeolocationService GeolocationService { get; }
/// <summary>
/// Service to control locks.
/// </summary>
protected ILocksService LockService { get; }
public string LockitButtonText { get; protected set; }
public bool IsLockitButtonVisible { get; protected set; }
public string ErrorText => string.Empty;
}
}

View file

@ -0,0 +1,134 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using EndRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
using OpenCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedClosed : Base, IRequestHandler, EndRentalCommand.IEndRentalCommandListener, OpenCommand.IOpenCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public BookedClosed(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionEndRental, // End Rental
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenLock; // Open Lock
IsLockitButtonVisible = true; // Show button
_openLockActionViewModel = new OpenLockActionViewModel<BookedClosed>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_endRentalActionViewModel = new EndRentalActionViewModel<BookedClosed>(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for end rental action.
/// </summary>
private readonly EndRentalActionViewModel<BookedClosed> _endRentalActionViewModel;
/// <summary>
/// Processes the get lock location progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(EndRentalCommand.Step step) => _endRentalActionViewModel.ReportStep(step);
/// <summary>
/// Processes the get lock location state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(EndRentalCommand.State state, string details) => await _endRentalActionViewModel.ReportStateAsync(state, details);
/// <summary> End Rental. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _endRentalActionViewModel.EndRentalAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly OpenLockActionViewModel<BookedClosed> _openLockActionViewModel;
/// <summary>
/// Processes the open lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(OpenCommand.Step step) => _openLockActionViewModel.ReportStep(step);
/// <summary>
/// Processes the open lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(OpenCommand.State state, string details) => await _openLockActionViewModel.ReportStateAsync(state, details);
/// <summary> Open lock. </summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _openLockActionViewModel.OpenLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,109 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using ConnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.ConnectAndGetStateCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedDisconnected : Base, IRequestHandler, ConnectCommand.IConnectAndGetStateCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public BookedDisconnected(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) :
base(
selectedBike,
AppResources.ActionSearchLock, // Connect Lock
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = GetType().Name;
IsLockitButtonVisible = false;
_connectLockActionViewModel = new ConnectLockActionViewModel<BookedDisconnected>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for connecting to lock action.
/// </summary>
private readonly ConnectLockActionViewModel<BookedDisconnected> _connectLockActionViewModel;
/// <summary>
/// Processes the connect to lock progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(ConnectCommand.Step step) => _connectLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the connect to lock state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(ConnectCommand.State state, string details) => await _connectLockActionViewModel.ReportStateAsync(state, details);
public async Task<IRequestHandler> HandleRequestOption2() => await UnsupportedRequest();
/// <summary> Scan for lock.</summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _connectLockActionViewModel.ConnectLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary> Request is not supported, button should be disabled. </summary>
/// <returns></returns>
public async Task<IRequestHandler> UnsupportedRequest()
{
Log.ForContext<BookedDisconnected>().Error("Click of unsupported button click detected.");
return await Task.FromResult<IRequestHandler>(this);
}
}
}

View file

@ -0,0 +1,264 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using CloseCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
using ShareeBike.Services.BluetoothLock.Exception;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedOpen : Base, IRequestHandler, CloseCommand.ICloseCommandListener
{
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public BookedOpen(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCloseLock, // Close Lock
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = activeUser.DebugLevel.HasFlag(Model.User.Account.Permissions.ManageAlarmAndSounds) ? AppResources.ActionLockSettings : GetType().Name;
IsLockitButtonVisible = activeUser.DebugLevel.HasFlag(Model.User.Account.Permissions.ManageAlarmAndSounds);
_closeLockActionViewModel = new CloseLockActionViewModel<BookedOpen>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly CloseLockActionViewModel<BookedOpen> _closeLockActionViewModel;
/// <summary>
/// Processes the close lock progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CloseCommand.Step step) => _closeLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the close lock state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CloseCommand.State state, string details) => await _closeLockActionViewModel.ReportStateAsync(state, details);
/// <summary> Close lock (and return bike).</summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _closeLockActionViewModel.CloseLockAsync();
if(_closeLockActionViewModel.IsEndRentalRequested == false)
{
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
var _endRentalActionViewModel = new EndRentalActionViewModel<BookedClosed>(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
ViewUpdateManager,
ViewService,
BikesViewModel);
await _endRentalActionViewModel.EndRentalAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
public async Task<IRequestHandler> HandleRequestOption2()
{
if (ActiveUser.DebugLevel.HasFlag(Model.User.Account.Permissions.ManageAlarmAndSounds))
{
await ManageLockSettings();
}
else
{
throw new InvalidOperationException();
}
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary> Manage sound/ alarm settings. </summary>
/// <returns></returns>
public async Task<IRequestHandler> ManageLockSettings()
{
// Stop polling before requesting bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
// Go to lock settings
Log.ForContext<ReservedOpen>().Information("User selected bike {bikeId} in order to manage sound/ alarm settings.", SelectedBike.Id);
// Alarm settings
var resultAlarm = await ViewService.DisplayAlert(
"Alarm einstellen",
"Alarm auf Einstellung 'niedrige Sensitivität' setzen oder Alarm ausschalten?",
"Auf niedrige Sensitivität",
"Alarm aus");
try
{
if (resultAlarm == true)
{
// Lower alarm sensitivity.
BikesViewModel.ActionText = "Setzen Alarm-Einstellungen...";
await LockService[SelectedBike.LockInfo.Id].SetAlarmSettingsAsync(AlarmSettings.SmallSensivitySilent);
}
else
{
// Switch off alarm.
BikesViewModel.ActionText = "Alarm ausschalten...";
await LockService[SelectedBike.LockInfo.Id].SetIsAlarmOffAsync(true);
}
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not set alarm settings. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Setzen der Alarm-Einstellungen!",
"Alarm kann erst eingestellt werden, wenn Rad in der Nähe ist.",
AppResources.MessageAnswerOk);
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not set alarm settings. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Setzen der Alarms-Einstellungen!",
exception.Message,
AppResources.MessageAnswerOk);
return this;
}
// Sound settings
var resultSound = await ViewService.DisplayAlert(
"Sounds einstellen",
"Sounds auf Einstellung 'Warnung' setzen oder alle Sounds ausschalten?",
"Auf Warnung",
"Alles aus");
try
{
if (resultSound == true)
{
BikesViewModel.ActionText = "Sounds einstellen auf 'Warnung'...";
await LockService[SelectedBike.LockInfo.Id].SetSoundAsync(SoundSettings.Warn);
}
else
{
BikesViewModel.ActionText = "Alle Sounds ausschalten...";
await LockService[SelectedBike.LockInfo.Id].SetSoundAsync(SoundSettings.AllOff);
}
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not set sounds. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Einstellen der Sounds!",
"Sounds können erst eingestellt werden, wenn Rad in der Nähe ist.",
AppResources.MessageAnswerOk);
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not set sounds. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Einstellen der Sounds!",
exception.Message,
AppResources.MessageAnswerOk);
return Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return this;
}
/// <summary> Request is not supported, button should be disabled. </summary>
/// <returns></returns>
public async Task<IRequestHandler> UnsupportedRequest()
{
Log.ForContext<DisposableDisconnected>().Error("Click of unsupported button click detected.");
return await Task.FromResult<IRequestHandler>(this);
}
}
}

View file

@ -0,0 +1,163 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using CloseCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using OpenCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.OpenCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedUnknown : Base, IRequestHandler, CloseCommand.ICloseCommandListener, OpenCommand.IOpenCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public BookedUnknown(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionOpenLock, // Open Lock
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionCloseLock; // Close Lock
IsLockitButtonVisible = true; // Show button
_openLockActionViewModel = new OpenLockActionViewModel<BookedUnknown>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_closeLockActionViewModel = new CloseLockActionViewModel<BookedUnknown>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly OpenLockActionViewModel<BookedUnknown> _openLockActionViewModel;
/// <summary>
/// Processes the open lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(OpenCommand.Step step) => _openLockActionViewModel.ReportStep(step);
/// <summary>
/// Processes the open lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(OpenCommand.State state, string details) => await _openLockActionViewModel.ReportStateAsync(state, details);
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _openLockActionViewModel.OpenLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly CloseLockActionViewModel<BookedUnknown> _closeLockActionViewModel;
/// <summary>
/// Processes the close lock progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CloseCommand.Step step) => _closeLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the close lock state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CloseCommand.State state, string details) => await _closeLockActionViewModel.ReportStateAsync(state, details);
/// <summary> Close lock in order to pause ride and update COPRI lock state.</summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _closeLockActionViewModel.CloseLockAsync();
if (_closeLockActionViewModel.IsEndRentalRequested == false)
{
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
var _endRentalActionViewModel = new EndRentalActionViewModel<BookedClosed>(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
ViewUpdateManager,
ViewService,
BikesViewModel);
await _endRentalActionViewModel.EndRentalAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,130 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using StartReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class DisposableDisconnected : Base, IRequestHandler, StartReservationCommand.IStartReservationCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public DisposableDisconnected(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionRequestBike, // Reserve / Rent Bike
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = GetType().Name;
IsLockitButtonVisible = false;
_startReservationOrRentalActionViewModel = new StartReservationOrRentalActionViewModel<DisposableDisconnected>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly StartReservationOrRentalActionViewModel<DisposableDisconnected> _startReservationOrRentalActionViewModel;
/// <summary>
/// Processes the reserving a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartReservationCommand.Step step) => _startReservationOrRentalActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the reserving a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(StartReservationCommand.State state, string details) => await _startReservationOrRentalActionViewModel.ReportStateAsync(state, details);
/// <summary>Reserve bike, connect to lock, open lock and rent bike.</summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _startReservationOrRentalActionViewModel.StartReservationOrRentalAsync();
if (_startReservationOrRentalActionViewModel.ContinueWithOpenLock == false)
{
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
var _openLockActionViewModel = new OpenLockActionViewModel<BookedClosed>(
SelectedBike,
ViewUpdateManager,
ViewService,
BikesViewModel);
await _openLockActionViewModel.OpenLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
public async Task<IRequestHandler> HandleRequestOption2() => await UnsupportedRequest();
/// <summary> Request is not supported, button should be disabled. </summary>
/// <returns></returns>
public async Task<IRequestHandler> UnsupportedRequest()
{
Log.ForContext<DisposableDisconnected>().Error("Click of unsupported button click detected.");
return await Task.FromResult<IRequestHandler>(this);
}
}
}

View file

@ -0,0 +1,164 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using StartReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartReservationCommand;
using CloseCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
/// <summary> Bike is disposable, lock is open and connected to app. </summary>
/// <remarks>
/// This state can not be occur because
/// - app does not allow to return bike/ cancel reservation when lock is not closed
/// - as long as app is connected to lock
/// - lock can not be opened manually
/// - no other device can access lock
/// </remarks>
public class DisposableOpen : Base, IRequestHandler, StartReservationCommand.IStartReservationCommandListener, CloseCommand.ICloseCommandListener
{
/// <summary> Bike is disposable, lock is open and can be reached via bluetooth. </summary>
/// <remarks>
/// This state should never occur because as long as a ILOCKIT is connected it
/// - cannot be closed manually
/// - no other device can access lock
/// - app itself should never event attempt to open a lock which is not rented.
/// </remarks>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public DisposableOpen(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionRequestBike, // Reserve / Rent Bike
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionCloseLock; // Close Lock
IsLockitButtonVisible = true; // Show button
_startReservationOrRentalActionViewModel = new StartReservationOrRentalActionViewModel<DisposableOpen>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_closeLockActionViewModel = new CloseLockActionViewModel<DisposableOpen>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly StartReservationOrRentalActionViewModel<DisposableOpen> _startReservationOrRentalActionViewModel;
/// <summary>
/// Processes the reserving a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartReservationCommand.Step step) => _startReservationOrRentalActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the reserving a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(StartReservationCommand.State state, string details) => await _startReservationOrRentalActionViewModel.ReportStateAsync(state, details);
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly CloseLockActionViewModel<DisposableOpen> _closeLockActionViewModel;
/// <summary>
/// Processes the close lock progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CloseCommand.Step step) => _closeLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the close lock state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CloseCommand.State state, string details) => await _closeLockActionViewModel.ReportStateAsync(state, details);
/// <summary>Rents bike by reserving bike and renting bike.</summary>
/// <returns>Next request handler.</returns>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _startReservationOrRentalActionViewModel.StartReservationOrRentalAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary>Closes Lock.</summary>
/// <returns>Next request handler.</returns>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _closeLockActionViewModel.CloseLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,26 @@
using System.Threading.Tasks;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock
{
public interface IRequestHandler : IRequestHandlerBase
{
/// <summary> Gets a value indicating whether the ILockIt button which is managed by request handler is visible or not. </summary>
bool IsLockitButtonVisible { get; }
/// <summary> Gets the text of the ILockIt button which is managed by request handler. </summary>
string LockitButtonText { get; }
/// <summary>
/// Performs the copri action to be executed when user presses the copri button managed by request handler.
/// </summary>
/// <returns>New handler object if action succeeded, same handler otherwise.</returns>
Task<IRequestHandler> HandleRequestOption1();
Task<IRequestHandler> HandleRequestOption2();
/// <summary>
/// Holds error description (invalid state).
/// </summary>
string ErrorText { get; }
}
}

View file

@ -0,0 +1,56 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.State;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class InvalidState : IRequestHandler
{
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public InvalidState(
IBikesViewModel bikesViewModel,
InUseStateEnum copriState,
LockingState lockingState,
string errorText)
{
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
ErrorText = errorText;
Log.Error($"{errorText}. Copri state is {copriState} and lock state is {lockingState}.");
}
/// <summary>View model to be used for progress report and unlocking/ locking view.</summary>
public IBikesViewModel BikesViewModel { get; }
public bool IsLockitButtonVisible => false;
public string LockitButtonText => GetType().Name;
public bool IsConnected => false;
public bool IsButtonVisible => false;
public string ButtonText => GetType().Name;
/// <summary> Gets if the bike has to be removed after action has been completed. </summary>
public bool IsRemoveBikeRequired => false;
public string ErrorText { get; }
public async Task<IRequestHandler> HandleRequestOption2()
{
Log.ForContext<InvalidState>().Error($"Click of unsupported button {nameof(HandleRequestOption2)} detected.");
return await Task.FromResult<IRequestHandler>(this);
}
public async Task<IRequestHandler> HandleRequestOption1()
{
Log.ForContext<InvalidState>().Error($"Click of unsupported button {nameof(HandleRequestOption1)} detected.");
return await Task.FromResult<IRequestHandler>(this);
}
}
}

View file

@ -0,0 +1,88 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.State;
using ShareeBike.MultilingualResources;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock
{
public class NotLoggedIn : IRequestHandler
{
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public NotLoggedIn(
InUseStateEnum state,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
ButtonText = BC.StateToText.GetActionText(state);
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>View model to be used for progress report and unlocking/ locking view.</summary>
public IBikesViewModel BikesViewModel { get; }
public bool IsButtonVisible => true;
public bool IsLockitButtonVisible => false;
public string ButtonText { get; private set; }
public string LockitButtonText => GetType().Name;
/// <summary>
/// Reference on view service to show modal notifications and to perform navigation.
/// </summary>
private IViewService ViewService { get; }
public bool IsConnected => throw new NotImplementedException();
/// <summary> Gets if the bike has to be removed after action has been completed. </summary>
public bool IsRemoveBikeRequired => false;
public async Task<IRequestHandler> HandleRequestOption1()
{
Log.ForContext<BikesViewModel>().Information("User selected bike but is not logged in.");
// User is not logged in
BikesViewModel.ActionText = string.Empty;
var l_oResult = await ViewService.DisplayAlert(
AppResources.QuestionLogInTitle,
AppResources.QuestionLogIn,
AppResources.MessageAnswerYes,
AppResources.MessageAnswerNo);
if (l_oResult == false)
{
// User aborted booking process
BikesViewModel.IsIdle = true;
return this;
}
try
{
// Switch to map page
await ViewService.ShowPage("//LoginPage");
}
catch (Exception p_oException)
{
Log.ForContext<BikesViewModel>().Error("Ein unerwarteter Fehler ist in der Klasse NotLoggedIn aufgetreten. Kontext: Aufruf nach Reservierungsversuch ohne Anmeldung. {@Exception}", p_oException);
BikesViewModel.IsIdle = true;
return this;
}
BikesViewModel.IsIdle = true;
return this;
}
public async Task<IRequestHandler> HandleRequestOption2()
{
Log.ForContext<NotLoggedIn>().Error("Click of unsupported button detected.");
return await Task.FromResult(this);
}
public string ErrorText => string.Empty;
}
}

View file

@ -0,0 +1,173 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using IBikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable;
using CancelReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using StartRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
/// <summary> Bike is reserved, lock is closed and connected to app. </summary>
/// <remarks>
/// Occurs when
/// - bike was reserved out of reach and is in reach now
/// - bike is reserved while in reach
/// </remarks>
public class ReservedClosed : Base, IRequestHandler, CancelReservationCommand.ICancelReservationCommandListener, StartRentalCommand.IStartRentalCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public ReservedClosed(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCancelReservation, // Cancel Reservation
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenLockAndRentBike; // Rent Bike
IsLockitButtonVisible = true; // Show button
_cancelReservationActionViewModel = new CancelReservationActionViewModel<ReservedClosed>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_startRentalActionViewModel = new StartReservationOrRentalActionViewModel<ReservedClosed>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly CancelReservationActionViewModel<ReservedClosed> _cancelReservationActionViewModel;
/// <summary>
/// Processes the canceling of reservation progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CancelReservationCommand.Step step) => _cancelReservationActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the canceling of reservation state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CancelReservationCommand.State state, string details) => await _cancelReservationActionViewModel.ReportStateAsync(state, details);
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly StartReservationOrRentalActionViewModel<ReservedClosed> _startRentalActionViewModel;
/// <summary>
/// Processes the renting a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartRentalCommand.Step step) => _startRentalActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the renting a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(StartRentalCommand.State state, string details) => await _startRentalActionViewModel.ReportStateAsync(state, details);
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _cancelReservationActionViewModel.CancelReservationAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary> Open lock and rent bike. </summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _startRentalActionViewModel.StartReservationOrRentalAsync();
if (_startRentalActionViewModel.ContinueWithOpenLock == false)
{
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
var _openLockActionViewModel = new OpenLockActionViewModel<BookedClosed>(
SelectedBike,
ViewUpdateManager,
ViewService,
BikesViewModel);
await _openLockActionViewModel.OpenLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,167 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using CancelReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using StartRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class ReservedDisconnected : Base, IRequestHandler, CancelReservationCommand.ICancelReservationCommandListener, StartRentalCommand.IStartRentalCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public ReservedDisconnected(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCancelReservation, // Cancel Reservation
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionRentBike; // Rent bike
IsLockitButtonVisible = true; // Show button
_cancelReservationlActionViewModel = new CancelReservationActionViewModel<ReservedDisconnected>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_startRentalActionViewModel = new StartReservationOrRentalActionViewModel<ReservedDisconnected>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly CancelReservationActionViewModel<ReservedDisconnected> _cancelReservationlActionViewModel;
/// <summary>
/// Processes the canceling of reservation progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CancelReservationCommand.Step step) => _cancelReservationlActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the canceling of reservation state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CancelReservationCommand.State state, string details) => await _cancelReservationlActionViewModel.ReportStateAsync(state, details);
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _cancelReservationlActionViewModel.CancelReservationAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly StartReservationOrRentalActionViewModel<ReservedDisconnected> _startRentalActionViewModel;
/// <summary>
/// Processes the renting a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartRentalCommand.Step step) => _startRentalActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the renting a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(StartRentalCommand.State state, string details) => await _startRentalActionViewModel.ReportStateAsync(state, details);
/// <summary> Connect to reserved bike ask whether to rent bike or not and if yes open lock. </summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _startRentalActionViewModel.StartReservationOrRentalAsync();
if (_startRentalActionViewModel.ContinueWithOpenLock == false)
{
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
var _openLockActionViewModel = new OpenLockActionViewModel<BookedClosed>(
SelectedBike,
ViewUpdateManager,
ViewService,
BikesViewModel);
await _openLockActionViewModel.OpenLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,152 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using IBikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable;
using CloseCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using StartRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
/// <summary> Bike is reserved, lock is open and connected to app. </summary>
/// <remarks>
/// This state might occur when a ILOCKIT was manually opened (color code) and app connects afterwards.
/// This should never during ILOCKIT is connected to app because
/// - manually opening lock is not possible when lock is connected
/// - two devices can not simultaneously connect to same lock.
public class ReservedOpen : Base, IRequestHandler, CloseCommand.ICloseCommandListener, StartRentalCommand.IStartRentalCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public ReservedOpen(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCloseLock, // Close Lock
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionRentBike; // Rent bike
IsLockitButtonVisible = true; // Show button
_closeLockActionViewModel = new CloseLockActionViewModel<ReservedOpen>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_startRentalActionViewModel = new StartReservationOrRentalActionViewModel<ReservedOpen>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly CloseLockActionViewModel<ReservedOpen> _closeLockActionViewModel;
/// <summary>
/// Processes the close lock progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CloseCommand.Step step) => _closeLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the close lock state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CloseCommand.State state, string details) => await _closeLockActionViewModel.ReportStateAsync(state, details);
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly StartReservationOrRentalActionViewModel<ReservedOpen> _startRentalActionViewModel;
/// <summary>
/// Processes the renting a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartRentalCommand.Step step) => _startRentalActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the renting a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(StartRentalCommand.State state, string details) => await _startRentalActionViewModel.ReportStateAsync(state, details);
/// <summary> Close lock. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _closeLockActionViewModel.CloseLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary> Start Rental. </summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _startRentalActionViewModel.StartReservationOrRentalAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,165 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using CloseCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.CloseCommand;
using StartRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.StartRentalCommand;
using static ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandlerFactory;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class ReservedUnknown : Base, IRequestHandler, CloseCommand.ICloseCommandListener, StartRentalCommand.IStartRentalCommandListener
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public ReservedUnknown(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCloseLock, // Close Lock
true, // Show button
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionRentBike; // Rent Bike
IsLockitButtonVisible = true; // Show button
_closeLockActionViewModel = new CloseLockActionViewModel<ReservedUnknown>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
_startRentalActionViewModel = new StartReservationOrRentalActionViewModel<ReservedUnknown>(
selectedBike,
viewUpdateManager,
viewService,
bikesViewModel);
}
/// <summary>
/// Holds the view model for close action.
/// </summary>
private readonly CloseLockActionViewModel<ReservedUnknown> _closeLockActionViewModel;
/// <summary>
/// Processes the close lock progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(CloseCommand.Step step) => _closeLockActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the close lock state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CloseCommand.State state, string details) => await _closeLockActionViewModel.ReportStateAsync(state, details);
/// <summary>
/// Holds the view model for requesting a bike action.
/// </summary>
private readonly StartReservationOrRentalActionViewModel<ReservedUnknown> _startRentalActionViewModel;
/// <summary>
/// Processes the renting a bike progress.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartRentalCommand.Step step) => _startRentalActionViewModel?.ReportStep(step);
/// <summary>
/// Processes the renting a bike state.
/// </summary>
/// <remarks>
/// Only used for testing.
/// </remarks>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(StartRentalCommand.State state, string details) => await _startRentalActionViewModel.ReportStateAsync(state, details);
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
await _startRentalActionViewModel.StartReservationOrRentalAsync();
if (_startRentalActionViewModel.ContinueWithOpenLock == false)
{
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
var _openLockActionViewModel = new OpenLockActionViewModel<BookedClosed>(
SelectedBike,
ViewUpdateManager,
ViewService,
BikesViewModel);
await _openLockActionViewModel.OpenLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
/// <summary> Close lock (and return bike).</summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
await _closeLockActionViewModel.CloseLockAsync();
return Create(
SelectedBike,
IsConnectedDelegate,
ConnectorFactory,
GeolocationService,
LockService,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
}
}
}

View file

@ -0,0 +1,240 @@
using System;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Services.BluetoothLock;
using ShareeBike.Services.Geolocation;
using ShareeBike.View;
using ShareeBike.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
namespace ShareeBike.ViewModel.Bikes.Bike.BluetoothLock
{
public static class RequestHandlerFactory
{
/// <summary> Creates a request handler.</summary>
/// <param name="selectedBike"></param>
/// <param name="isConnectedDelegate"></param>
/// <param name="connectorFactory"></param>
/// <param name="geolocation"></param>
/// <param name="viewUpdateManager"></param>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="viewService"></param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <param name="context">Specifies the context (last action performed).</param>
/// <returns>Request handler.</returns>
public static IRequestHandler Create(
Model.Bikes.BikeInfoNS.BC.IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocationService geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
if (!(selectedBike is IBikeInfoMutable selectedBluetoothLockBike))
return null;
switch (selectedBluetoothLockBike.State.Value)
{
case Model.State.InUseStateEnum.Disposable:
// Bike is reserved, select action depending on lock state.
switch (selectedBluetoothLockBike.LockInfo.State)
{
case LockingState.Open:
case LockingState.UnknownFromHardwareError:
// Unexpected state detected.
/// This state is unexpected because
/// - app does not allow to return bike/ cancel reservation when lock is closed
/// - as long as app is connected to lock
/// - lock can not be opened manually
/// - no other device can access lock
/// Nevertheless this state is not expected let user either
/// - close lock or
/// - rent bike
/// </remarks>
Log.Error("Unexpected state {BookingState}/ {LockingState} detected.", selectedBluetoothLockBike.State.Value, selectedBluetoothLockBike.LockInfo.State);
return new DisposableOpen(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.Closed:
case LockingState.UnknownDisconnected:
// Do not allow interaction with lock before reserving bike.
return new DisposableDisconnected(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
default:
// Invalid state detected. Lock must never be open if bike is reserved.
throw new ArgumentException();
}
case Model.State.InUseStateEnum.Reserved:
// Bike is reserved, select action depending on lock state.
switch (selectedBluetoothLockBike.LockInfo.State)
{
case LockingState.Closed:
// Lock could not be opened after reserving bike.
return new ReservedClosed(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.UnknownDisconnected:
return new ReservedDisconnected(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.Open:
// Unwanted state detected.
/// This state might occur when a ILOCKIT was manually opened (color code) and app connects afterwards.
Log.Error("Unwanted state {BookingState}/ {LockingState} detected.", selectedBluetoothLockBike.State.Value, selectedBluetoothLockBike.LockInfo.State);
return new ReservedOpen(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.UnknownFromHardwareError:
// User wants to return bike/ pause ride.
return new ReservedUnknown(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
default:
// Invalid state detected. Lock must never be open if bike is reserved.
throw new ArgumentException();
}
case Model.State.InUseStateEnum.Booked:
// Bike is booked, select action depending on lock state.
switch (selectedBluetoothLockBike.LockInfo.State)
{
case LockingState.Closed:
// User wants to close lock.
return new BookedClosed(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.Open:
// User wants to return bike/ pause ride.
return new BookedOpen(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.UnknownFromHardwareError:
// User wants to return bike/ pause ride.
return new BookedUnknown(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case LockingState.UnknownDisconnected:
// Invalid state detected.
// If bike is booked lock state must be queried before creating view model.
return new BookedDisconnected(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
default:
// Invalid state detected. Lock must never be open if bike is reserved.
throw new ArgumentException();
}
default:
// Unexpected copri state detected.
Log.Error("Unexpected locking {BookingState}/ {LockingState} detected.", selectedBluetoothLockBike.State.Value, selectedBluetoothLockBike.LockInfo.State);
return new InvalidState(
bikesViewModel,
selectedBluetoothLockBike.State.Value,
selectedBluetoothLockBike.LockInfo.State,
string.Format(AppResources.MarkingBikeInfoErrorStateUnknownDetected, selectedBluetoothLockBike.Description));
}
}
}
}

View file

@ -0,0 +1,200 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.MultilingualResources;
using ShareeBike.View;
using Serilog;
using CancelReservationCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.CancelReservationCommand;
using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock;
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// Return bike action.
/// </summary>
/// <typeparam name="T">Type of owner.</typeparam>
public class CancelReservationActionViewModel<T> :
CancelReservationCommand.ICancelReservationCommandListener,
DisconnectCommand.IDisconnectCommandListener
{
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// View service to show modal notifications.
/// </summary>
private IViewService ViewService { get; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike close. </summary>
private IBikeInfoMutable SelectedBike { get; }
/// <summary> Provides a connector object.</summary>
protected Func<bool, IConnector> ConnectorFactory { get; }
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to close.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="viewService">View service to show modal notifications.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public CancelReservationActionViewModel(
IBikeInfoMutable selectedBike,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel<T>)}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>
/// Processes the start reservation progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(CancelReservationCommand.Step step)
{
switch (step)
{
case CancelReservationCommand.Step.CancelReservation:
BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation;
break;
}
}
/// <summary>
/// Processes the start reservation state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(CancelReservationCommand.State state, string details)
{
switch (state)
{
case CancelReservationCommand.State.InvalidResponse:
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCancelReservationTitle,
AppResources.ErrorAccountInvalidAuthorization,
AppResources.MessageAnswerOk);
break;
case CancelReservationCommand.State.WebConnectFailed:
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
break;
case CancelReservationCommand.State.GeneralCancelReservationError:
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAdvancedAlert(
AppResources.ErrorCancelReservationTitle,
details,
AppResources.ErrorTryAgain,
AppResources.MessageAnswerOk);
break;
}
}
/// <summary>
/// Processes the disconnect from lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(DisconnectCommand.Step step)
{
switch (step)
{
case DisconnectCommand.Step.DisconnectLock:
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
break;
}
}
/// <summary>
/// Processes the disconnect from lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(DisconnectCommand.State state, string details)
{
switch (state)
{
case DisconnectCommand.State.GeneralDisconnectError:
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAdvancedAlert(
AppResources.ErrorAccountInvalidAuthorization,
details,
AppResources.ErrorTryAgain,
AppResources.MessageAnswerOk);
break;
}
}
/// <summary> Cancel reservation. </summary>
public async Task CancelReservationAsync()
{
// lock GUI
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
// 1.Step: Cancel reservation
try
{
#if USELOCALINSTANCE
var command = new CancelReservationCommand(SelectedBike, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.CancelReservationAsync(this);
#endif
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Reservation of bike {bikeId} could not be canceled. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return;
}
// 2. Step: Disconnect Lock
if (SelectedBike.LockInfo.State != LockingState.UnknownDisconnected)
{
try
{
#if USELOCALINSTANCE
var command = new DisconnectCommand(SelectedBike, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.DisconnectAsync(this);
#endif
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} could not be disconnected. {@exception}", SelectedBike.Id, exception);
}
}
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return;
}
}
}

View file

@ -0,0 +1,191 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.View;
using BikeInfoMutable = ShareeBike.Model.Bikes.BikeInfoNS.BC.BikeInfoMutable;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock
{
using IRequestHandler = BluetoothLock.IRequestHandler;
/// <summary>
/// View model for a ILockIt bike.
/// Provides functionality for views
/// - MyBikes
/// - BikesAtStation
/// </summary>
public class BikeViewModel : BikeViewModelBase, INotifyPropertyChanged
{
/// <summary> Notifies GUI about changes. </summary>
public override event PropertyChangedEventHandler PropertyChanged;
/// <summary> Holds object which manages requests. </summary>
private IRequestHandler RequestHandler { get; set; }
/// <summary> Raises events in order to update GUI.</summary>
public override void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs) => PropertyChanged?.Invoke(sender, eventArgs);
/// <summary> Raises events if property values changed in order to update GUI.</summary>
private void RaisePropertyChangedEvent(
IRequestHandler lastHandler,
string lastStateText = null,
Xamarin.Forms.Color? lastStateColor = null)
{
if (lastHandler.ButtonText != ButtonText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ButtonText)));
}
if (lastHandler.IsButtonVisible != IsButtonVisible)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
}
if (lastHandler.LockitButtonText != LockitButtonText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(LockitButtonText)));
}
if (lastHandler.IsLockitButtonVisible != IsLockitButtonVisible)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLockitButtonVisible)));
}
if (RequestHandler.ErrorText != lastHandler.ErrorText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ErrorText)));
}
if (!string.IsNullOrEmpty(lastStateText) && lastStateText != StateText)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateText)));
}
if (lastStateColor != null && lastStateColor != StateColor)
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StateColor)));
}
}
/// <summary>
/// Constructs a bike view model object.
/// </summary>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="selectedBike">Bike to be displayed.</param>
/// <param name="user">Object holding logged in user or an empty user object.</param>
/// <param name="viewContext"> Holds the view context in which bike view model is used.</param>
/// <param name="stateInfoProvider">Provides in use state information.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <param name="openUrlInBrowser">Delegate to open browser.</param>
public BikeViewModel(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Action<string> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser user,
ViewContext viewContext,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel,
Action<string> openUrlInBrowser) : base(isConnectedDelegate, connectorFactory, bikeRemoveDelegate, viewUpdateManager, smartDevice, viewService, selectedBike, user, viewContext, stateInfoProvider, bikesViewModel, openUrlInBrowser)
{
RequestHandler = user.IsLoggedIn
? RequestHandlerFactory.Create(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
user)
: new BluetoothLock.NotLoggedIn(
selectedBike.State.Value,
viewService,
bikesViewModel);
selectedBike.PropertyChanged += (sender, ev) =>
{
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLockitButtonVisible)));
};
}
/// <summary>
/// Handles BikeInfoMutable events.
/// Helper member to raise events. Maps model event change notification to view model events.
/// Todo: Check which events are received here and filter, to avoid event storm.
/// </summary>
public override void OnSelectedBikeStateChanged()
{
var lastHandler = RequestHandler;
RequestHandler = RequestHandlerFactory.Create(
Bike,
IsConnectedDelegate,
ConnectorFactory,
ViewUpdateManager,
SmartDevice,
ViewService,
BikesViewModel,
ActiveUser);
RaisePropertyChangedEvent(lastHandler);
}
/// <summary> Gets visibility of the copri command button. </summary>
public bool IsButtonVisible
=> RequestHandler.IsButtonVisible
&& Bike.DataSource == Model.Bikes.BikeInfoNS.BC.DataSource.Copri /* do not show button if data is from cache */ ;
/// <summary> Gets the text of the copri command button. </summary>
public string ButtonText => RequestHandler.ButtonText;
/// <summary> Gets visibility of the ILockIt command button. </summary>
public bool IsLockitButtonVisible
=> RequestHandler.IsLockitButtonVisible
&& Bike.DataSource == Model.Bikes.BikeInfoNS.BC.DataSource.Copri /* do not show button if data is from cache */ ;
/// <summary> Gets the text of the ILockIt command button. </summary>
public string LockitButtonText => RequestHandler.LockitButtonText;
/// <summary> Processes request to perform a copri action (reserve bike and cancel reservation). </summary>
public System.Windows.Input.ICommand OnButtonClicked =>
new Xamarin.Forms.Command(async () => await ClickButton(RequestHandler.HandleRequestOption1()));
/// <summary> Processes request to perform a ILockIt action (unlock bike and lock bike). </summary>
public System.Windows.Input.ICommand OnLockitButtonClicked =>
new Xamarin.Forms.Command(async () => await ClickButton(RequestHandler.HandleRequestOption2()));
/// <summary> Processes request to perform a copri action (reserve bike and cancel reservation). </summary>
private async Task ClickButton(Task<IRequestHandler> handleRequest)
{
var lastHandler = RequestHandler;
var lastState = Bike.State.Value;
var lastStateText = StateText;
var lastStateColor = StateColor;
RequestHandler = await handleRequest;
CheckRemoveBike(Id, lastState);
if (RuntimeHelpers.Equals(lastHandler, RequestHandler))
{
// No state change occurred (same instance is returned).
return;
}
RaisePropertyChangedEvent(
lastHandler,
lastStateText,
lastStateColor);
}
public string ErrorText => RequestHandler.ErrorText;
}
}

View file

@ -0,0 +1,37 @@
using System;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler
{
public abstract class Base : BC.RequestHandler.Base<Model.Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable>
{
/// <summary>
/// Constructs the request handler base.
/// </summary>
/// <param name="selectedBike">Bike which is reserved or for which reservation is canceled.</param>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public Base(
Model.Bikes.BikeInfoNS.CopriLock.IBikeInfoMutable selectedBike,
string buttonText,
bool isCopriButtonVisible,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, buttonText, isCopriButtonVisible, isConnectedDelegate, connectorFactory, viewUpdateManager, smartDevice, viewService, bikesViewModel, activeUser)
{
}
public string LockitButtonText { get; protected set; }
public bool IsLockitButtonVisible { get; protected set; }
public string ErrorText => string.Empty;
}
}

View file

@ -0,0 +1,113 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.CopriLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler
{
using IRequestHandler = BluetoothLock.IRequestHandler;
public class BookedClosed : Base, IRequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public BookedClosed(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
nameof(BookedClosed),
false, // Lock can only be closed manually and returning is performed by placing bike into the station.
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenLock;
IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike.
}
/// <summary> Return bike. </summary>
public async Task<IRequestHandler> HandleRequestOption1() => await UnsupportedRequest();
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption2() => await OpenLock();
/// <summary> Request is not supported, button should be disabled. </summary>
public async Task<IRequestHandler> UnsupportedRequest()
{
Log.ForContext<BookedClosed>().Error("Click of unsupported button click detected.");
return await Task.FromResult<IRequestHandler>(this);
}
/// <summary> Open bike. </summary>
public async Task<IRequestHandler> OpenLock()
{
// Unlock bike.
Log.ForContext<BookedClosed>().Information("User request to unlock bike {bike}.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
BikesViewModel.ActionText = AppResources.ActivityTextOpeningCopriLock;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.OpenLockAsync(SelectedBike);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedClosed>().Information("User selected bike {id} but opening lock failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<BookedClosed>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
AppResources.MessageAnswerOk);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<BookedClosed>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,114 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.CopriLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler
{
using IRequestHandler = BluetoothLock.IRequestHandler;
public class BookedOpen : Base, IRequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public BookedOpen(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
nameof(BookedOpen),
false, // Lock can only be closed manually and returning is performed by placing bike into the station.
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenLock; // Lock is open but show button anyway to be less prone to errors.
IsLockitButtonVisible = true;
}
/// <summary> Close lock and return bike.</summary>
public async Task<IRequestHandler> HandleRequestOption1() => await UnsupportedRequest();
/// <summary> Close lock in order to pause ride and update COPRI lock state.</summary>
public async Task<IRequestHandler> HandleRequestOption2() => await OpenLock();
/// <summary> Request is not supported, button should be disabled. </summary>
public async Task<IRequestHandler> UnsupportedRequest()
{
Log.ForContext<BookedOpen>().Error("Click of unsupported button click detected.");
return await Task.FromResult<IRequestHandler>(this);
}
/// <summary> Open bike. </summary>
public async Task<IRequestHandler> OpenLock()
{
// Unlock bike.
Log.ForContext<BookedOpen>().Information("User request to unlock bike {bike}. For locking state {state} this request is unexpected.", SelectedBike, SelectedBike?.LockInfo?.State);
// Stop polling before returning bike.
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
BikesViewModel.ActionText = AppResources.ActivityTextOpeningCopriLock;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.OpenLockAsync(SelectedBike);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User selected bike {id} but opening lock failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<BookedOpen>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
AppResources.MessageAnswerOk);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<BookedOpen>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,213 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.CopriLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.State;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler
{
using IRequestHandler = BluetoothLock.IRequestHandler;
public class DisposableClosed : Base, IRequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public DisposableClosed(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionOpenLockAndRentBike, // Button text: "Schloss öffnen & Rad mieten"
true, // Show copri button to enable booking and opening
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionReserveBike; // Copri text: "Rad reservieren"
IsLockitButtonVisible = true;
}
/// <summary>Reserve bike and connect to lock.</summary>
public async Task<IRequestHandler> HandleRequestOption1() => await BookAndRelease();
public async Task<IRequestHandler> HandleRequestOption2() => await Reserve();
/// <summary>Book bike and open lock.</summary>
public async Task<IRequestHandler> BookAndRelease()
{
BikesViewModel.IsIdle = false;
// 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<DisposableClosed>().Information("User selected bike {bike} in order to book and release bike from station but action was canceled.", SelectedBike);
BikesViewModel.IsIdle = true;
return this;
}
// Book and release bike from station.
Log.ForContext<DisposableClosed>().Information("User selected bike {bike} in order to book and release bike from station.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
// Book bike prior to opening lock.
BikesViewModel.ActionText = AppResources.ActivityTextRentingBike;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.BookAndOpenAync(SelectedBike);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<DisposableClosed>().Information("User selected bike {id} but booking failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<DisposableClosed>().Error("User selected bike {id} but booking failed. {@exception}", SelectedBike.Id, 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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<DisposableClosed>().Information("User booked and released bike {bike} successfully.", SelectedBike);
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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
/// <summary>Reserve bike.</summary>
public async Task<IRequestHandler> Reserve()
{
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<DisposableClosed>().Information("User selected centered bike {bike} in order to reserve but action was canceled.", SelectedBike);
BikesViewModel.IsIdle = true;
return this;
}
Log.ForContext<DisposableClosed>().Information("Request to reserve for bike {bike} detected.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
// Stop polling before requesting bike.
await ViewUpdateManager().StopAsync();
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<DisposableClosed>().Information("Request declined because maximum count of bikes {l_oException.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount);
await ViewService.DisplayAlert(
AppResources.MessageHintTitle,
string.Format(AppResources.ErrorReservingBikeTooManyReservationsRentals, SelectedBike.GetFullDisplayName(), (exception as BookingDeclinedException).MaxBikesCount),
AppResources.MessageAnswerOk);
}
else if (exception is WebConnectFailureException
|| exception is RequestNotCachableException)
{
// Copri server is not reachable.
Log.ForContext<DisposableClosed>().Information("User selected centered bike {bike} but reserving failed (Copri server not reachable).", SelectedBike);
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<DisposableClosed>().Error("User selected centered bike {bike} but reserving failed. {@exception}", SelectedBike, exception);
await ViewService.DisplayAlert(
AppResources.ErrorReservingBikeTitle,
exception.Message,
AppResources.MessageAnswerOk);
}
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return this;
}
Log.ForContext<DisposableClosed>().Information("User reserved bike {bike} successfully.", SelectedBike);
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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,132 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.CopriLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler
{
using IRequestHandler = BluetoothLock.IRequestHandler;
public class FeedbackPending : Base, IRequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public FeedbackPending(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
nameof(FeedbackPending),
false, // No other action but give feedback allowed.
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionGiveFeedback;
IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike.
}
/// <summary> Return bike. </summary>
public async Task<IRequestHandler> HandleRequestOption1() => await UnsupportedRequest();
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption2() => await GiveFeedback();
/// <summary> Request is not supported, button should be disabled. </summary>
/// <returns></returns>
public async Task<IRequestHandler> UnsupportedRequest()
{
Log.ForContext<BookedClosed>().Error("Click of unsupported button click detected.");
return await Task.FromResult<IRequestHandler>(this);
}
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> GiveFeedback()
{
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
// Do get Feedback
var battery = SelectedBike.Drive?.Battery;
var feedback = await ViewService.DisplayUserFeedbackPopup(
battery);
if (battery != null
&& feedback.CurrentChargeBars != null)
{
SelectedBike.Drive.Battery.CurrentChargeBars = feedback.CurrentChargeBars;
}
BikesViewModel.ActionText = AppResources.ActivityTextSubmittingFeedback;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoSubmitFeedback(
new UserFeedbackDto
{
BikeId = SelectedBike.Id,
CurrentChargeBars = feedback.CurrentChargeBars,
IsBikeBroken = feedback.IsBikeBroken,
Message = feedback.Message
},
SelectedBike?.OperatorUri);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<FeedbackPending>().Information("Submitting feedback for bike {bike} failed. COPRI returned an error.", SelectedBike);
}
else
{
Log.ForContext<FeedbackPending>().Error("Submitting feedback for bike {bike} failed. {@Exception}", SelectedBike.Id, exception);
}
await ViewService.DisplayAlert(
AppResources.ErrorSubmitFeedbackTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
// Feedback was given successfully.
// Set state from FeedbackPending to Available.
SelectedBike.State.Load(Model.State.InUseStateEnum.Disposable);
if (SelectedBike?.BookingFinishedModel?.MiniSurvey?.Questions?.Count > 0)
{
// No need to restart polling again because different page is shown.
await ViewService.PushModalAsync(ViewTypes.MiniSurvey);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,210 @@
using System;
using System.Threading.Tasks;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.CopriLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.Services.CopriApi.Exception;
using ShareeBike.View;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler
{
using IRequestHandler = BluetoothLock.IRequestHandler;
/// <summary> Bike is reserved, lock is closed and connected to app. </summary>
/// <remarks>
/// Occurs when
/// - bike was reserved out of reach and is in reach now
/// - bike is reserved while in reach
/// </remarks>
public class ReservedClosed : Base, IRequestHandler
{
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public ReservedClosed(
IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCancelReservation, // Copri button text: "Reservierung abbrechen"
true, // Show button to enable canceling reservation.
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenLockAndRentBike; // Button text: "Schloss öffnen & Rad mieten"
IsLockitButtonVisible = true; // Show "Öffnen" button to enable unlocking
}
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequestOption1() => await CancelReservation();
/// <summary> Open lock and book bike. </summary>
public async Task<IRequestHandler> HandleRequestOption2() => await OpenLockAndDoBook();
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> CancelReservation()
{
BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending.
var result = await ViewService.DisplayAlert(
string.Empty,
string.Format(AppResources.QuestionCancelReservation, SelectedBike.GetFullDisplayName()),
AppResources.MessageAnswerYes,
AppResources.MessageAnswerNo);
if (result == false)
{
// User aborted cancel process
Log.ForContext<ReservedClosed>().Information("User selected reserved bike {Id} in order to cancel reservation but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
Log.ForContext<ReservedClosed>().Information("User selected reserved bike {Id} in order to cancel reservation.", SelectedBike.Id);
// Stop polling before cancel request.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is InvalidAuthorizationResponseException)
{
// Copri response is invalid.
Log.ForContext<BikesViewModel>().Error("User selected reserved bike {Id} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.ErrorCancelReservationTitle,
exception.Message,
AppResources.MessageAnswerOk);
}
else if (exception is WebConnectFailureException
|| exception is RequestNotCachableException)
{
// Copri server is not reachable.
Log.ForContext<BikesViewModel>().Information("User selected reserved bike {Id} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.ErrorCancelReservationTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<BikesViewModel>().Error("User selected reserved bike {Id} but cancel reservation failed. {@Exception}.", SelectedBike.Id, exception);
await ViewService.DisplayAlert(
AppResources.ErrorCancelReservationTitle,
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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<BikesViewModel>().Information("User canceled reservation of bike {Id} successfully.", SelectedBike.Id);
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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Open lock and book bike. </summary>
public async Task<IRequestHandler> OpenLockAndDoBook()
{
BikesViewModel.IsIdle = false;
// Ask whether to really book bike?
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
string.Format(AppResources.QuestionOpenLockAndBookBike, SelectedBike.GetFullDisplayName()),
AppResources.MessageAnswerYes,
AppResources.MessageAnswerNo);
if (l_oResult == false)
{
// User aborted booking process
Log.ForContext<ReservedClosed>().Information("User selected requested bike {bike} in order to book but action was canceled.", SelectedBike);
BikesViewModel.IsIdle = true;
return this;
}
Log.ForContext<ReservedClosed>().Information("User selected requested bike {bike} in order to book.", SelectedBike);
// Stop polling before cancel request.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopAsync();
// Book bike prior to opening lock.
BikesViewModel.ActionText = AppResources.ActivityTextRentingBike;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.BookAndOpenAync(SelectedBike);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedClosed>().Information("User selected requested bike {Id} but booking failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.ErrorNoConnectionTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<ReservedClosed>().Error("User selected requested bike {Id} but reserving failed. {@Exception}", SelectedBike.Id, exception);
await ViewService.DisplayAdvancedAlert(
AppResources.ErrorRentingBikeTitle,
exception.Message,
string.Empty,
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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<ReservedClosed>().Information("User booked and opened bike {bike} successfully.", SelectedBike.Id);
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, ViewUpdateManager, SmartDevice, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,129 @@
using System;
using Serilog;
using ShareeBike.Model.Bikes.BikeInfoNS.CopriLock;
using ShareeBike.Model.Connector;
using ShareeBike.Model.Device;
using ShareeBike.Model.User;
using ShareeBike.View;
using ShareeBike.ViewModel.Bikes.Bike.CopriLock.RequestHandler;
namespace ShareeBike.ViewModel.Bikes.Bike.CopriLock
{
public static class RequestHandlerFactory
{
/// <summary> Creates a request handler.</summary>
/// <param name="selectedBike"></param>
/// <param name="isConnectedDelegate"></param>
/// <param name="connectorFactory"></param>
/// <param name="bikeRemoveDelegate"></param>
/// <param name="viewUpdateManager"></param>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param>
/// <param name="viewService"></param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <returns>Request handler.</returns>
public static BluetoothLock.IRequestHandler Create(
Model.Bikes.BikeInfoNS.BC.IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
ISmartDevice smartDevice,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
if (!(selectedBike is IBikeInfoMutable selectedCopriLock))
{
Log.Error("Bike of unexpected type {type} detected.", selectedBike?.GetType());
return null;
}
switch (selectedCopriLock.State.Value)
{
// User has rented this bike before. Bike is now available but feedback was not yet given.
case Model.State.InUseStateEnum.FeedbackPending:
return new FeedbackPending(
selectedCopriLock,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case Model.State.InUseStateEnum.Disposable:
// Bike is reserved, selected action depending on lock state.
return new DisposableClosed(
selectedCopriLock,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case Model.State.InUseStateEnum.Reserved:
// Lock could not be opened after reserving bike.
return new ReservedClosed(
selectedCopriLock,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
case Model.State.InUseStateEnum.Booked:
// Bike is booked, selected action depending on lock state.
var lockState = selectedCopriLock.LockInfo.State;
switch (lockState)
{
case LockingState.Closed:
// Frame lock is closed. User paused ride and closed lock.
return new BookedClosed(
selectedCopriLock,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
default:
if (lockState != LockingState.Open)
Log.Error("Unexpected locking {LockingState} of a rented bike with copri lock bike detected.", selectedCopriLock.LockInfo.State);
return new BookedOpen(
selectedCopriLock,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
}
default:
// Unexpected copri state detected.
Log.Error("Unexpected locking {BookingState}/ {LockingState} of a copri lock bike detected.", selectedCopriLock.State.Value, selectedCopriLock.LockInfo.State);
return new DisposableClosed(
selectedCopriLock,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
smartDevice,
viewService,
bikesViewModel,
activeUser);
}
}
}
}

View file

@ -0,0 +1,380 @@
using System;
using System.Threading.Tasks;
using ShareeBike.Model.Connector;
using ShareeBike.Model;
using ShareeBike.MultilingualResources;
using ShareeBike.Repository.Exception;
using ShareeBike.View;
using EndRentalCommand = ShareeBike.Model.Bikes.BikeInfoNS.BikeNS.Command.EndRentalCommand;
using DisconnectCommand = ShareeBike.Model.Bikes.BikeInfoNS.BluetoothLock.Command.DisconnectCommand;
using Serilog;
using ShareeBike.Services.BluetoothLock;
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// Return bike action.
/// </summary>
/// <typeparam name="T">Type of owner.</typeparam>
public class EndRentalActionViewModel<T> : EndRentalCommand.IEndRentalCommandListener, DisconnectCommand.IDisconnectCommandListener
{
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// View service to show modal notifications.
/// </summary>
private IViewService ViewService { get; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike close. </summary>
private Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable SelectedBike { get; }
/// <summary> Provides a connector object.</summary>
protected Func<bool, IConnector> ConnectorFactory { get; }
/// <summary> Delegate to retrieve connected state. </summary>
private Func<bool> IsConnectedDelegate { get; }
/// <summary>Gets the is connected state. </summary>
bool IsConnected;
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to close.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="viewService">View service to show modal notifications.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public EndRentalActionViewModel(
Model.Bikes.BikeInfoNS.BluetoothLock.IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
IsConnectedDelegate = isConnectedDelegate;
ConnectorFactory = connectorFactory;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel<T>)}-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
});
}
/// <summary>
/// Processes the end rental progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(EndRentalCommand.Step step)
{
switch (step)
{
case EndRentalCommand.Step.GetLocation:
// 1a.Step: Geolocation data
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepGPS;
BikesViewModel.ActionText = AppResources.ActivityTextQueryLocation;
break;
case EndRentalCommand.Step.ReturnBike:
// 1b.Step: Return bike
BikesViewModel.ActionText = AppResources.ActivityTextReturningBike;
break;
}
}
/// <summary>
/// Processes the get lock location state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public async Task ReportStateAsync(EndRentalCommand.State state, string details)
{
switch (state)
{
case EndRentalCommand.State.GeneralQueryLocationFailed:
case EndRentalCommand.State.GPSNotSupportedException:
case EndRentalCommand.State.NoGPSData:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
AppResources.ErrorEndRentalUnknownLocation,
AppResources.MessageAnswerOk);
break;
case EndRentalCommand.State.GPSNotEnabledException:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
AppResources.ErrorLockLocationOff,
AppResources.MessageAnswerOk);
break;
case EndRentalCommand.State.NoGPSPermissionsException:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
AppResources.ErrorNoLocationPermission,
AppResources.MessageAnswerOk);
break;
case EndRentalCommand.State.ResponseException:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
AppResources.ActivityTextErrorInvalidResponseException,
AppResources.MessageAnswerOk);
break;
case EndRentalCommand.State.WebConnectFailed:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
AppResources.ErrorNoWeb,
AppResources.MessageAnswerOk);
break;
case EndRentalCommand.State.NotAtStation:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
details,
AppResources.MessageAnswerOk);
break;
case EndRentalCommand.State.GeneralEndRentalError:
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Failed;
await ViewService.DisplayAlert(
AppResources.ErrorEndRentalTitle,
AppResources.ErrorTryAgain,
AppResources.MessageAnswerOk);
break;
}
}
/// <summary>
/// Processes the disconnect lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(DisconnectCommand.Step step)
{
switch (step)
{
case DisconnectCommand.Step.DisconnectLock:
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
break;
}
}
/// <summary>
/// Processes the disconnect lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public Task ReportStateAsync(DisconnectCommand.State state, string details)
{
switch (state)
{
case DisconnectCommand.State.GeneralDisconnectError:
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
break;
}
return Task.CompletedTask;
}
/// <summary> Return bike. </summary>
public async Task EndRentalAsync()
{
Log.ForContext<T>().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.EndRental,
StepIndex = 1,
Result = CurrentStepStatus.None
});
BookingFinishedModel bookingFinished = null;
try
{
#if USELOCALINSTANCE
var command = new EndRentalCommand(SelectedBike, GeolocationService, LockService, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
bookingFinished = await SelectedBike.ReturnBikeAsync(this);
Log.ForContext<T>().Information("Rental of bike {bikeId} ended successfully.", SelectedBike.Id);
#endif
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Rental of bike {bikeId} could not be terminated. {@exception}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
BikesViewModel.IsIdle = true;
return;
}
if (bookingFinished != null)
{
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded;
// Disconnect lock.
try
{
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
#if USELOCALINSTANCE
var command = new DisconnectCommand(SelectedBike, GeolocationService, LockService, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager);
await command.Invoke(this);
#else
await SelectedBike.DisconnectAsync(this);
Log.ForContext<T>().Information("Lock of bike {bikeId} was disconnected successfully.", SelectedBike.Id);
#endif
}
catch (Exception exception)
{
Log.ForContext<T>().Information("Lock of bike {bikeId} can not be disconnected. {@exception}", SelectedBike.Id, exception);
}
// 2.Step: User feedback on bike condition
BikesViewModel.RentalProcess.StepIndex = 2;
BikesViewModel.RentalProcess.Result = CurrentStepStatus.None;
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepFeedback;
BikesViewModel.RentalProcess.ImportantStepInfoText = string.Empty;
var feedBackUri = SelectedBike?.OperatorUri;
var battery = SelectedBike.Drive?.Battery;
var feedback = await ViewService.DisplayUserFeedbackPopup(
battery);
if (battery != null
&& feedback.CurrentChargeBars != null)
{
SelectedBike.Drive.Battery.CurrentChargeBars = feedback.CurrentChargeBars;
}
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded;
// 3.Step
// Send user feedback to backend
BikesViewModel.RentalProcess.StepIndex = 3;
BikesViewModel.RentalProcess.Result = CurrentStepStatus.None;
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessEndRentalStepUpload;
BikesViewModel.RentalProcess.ImportantStepInfoText = AppResources.MarkingRentalProcessEndRentalWait;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoSubmitFeedback(
new UserFeedbackDto
{
BikeId = SelectedBike.Id,
CurrentChargeBars = feedback.CurrentChargeBars,
IsBikeBroken = feedback.IsBikeBroken,
Message = feedback.Message
},
feedBackUri);
Log.ForContext<T>().Information("Feedback for bike {bikeId} was submitted successfully.", SelectedBike.Id);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
Log.ForContext<T>().Information("Feedback for bike {bikeId} can not be submitted.", SelectedBike.Id);
if (exception is ResponseException copriException)
{
// Copri exception.
Log.ForContext<T>().Debug("COPRI returned an error. {response}", copriException.Response);
}
else
{
Log.ForContext<T>().Debug("{@exception}", exception);
}
await ViewService.DisplayAlert(
AppResources.ErrorSubmitFeedbackTitle,
AppResources.ErrorSubmitFeedback,
AppResources.MessageAnswerOk);
}
BikesViewModel.RentalProcess.Result = CurrentStepStatus.Succeeded;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StartAsync();
BikesViewModel.ActionText = string.Empty;
// Confirmation message that rental is ended
Log.ForContext<T>().Information("Rental of bike {bikeId} was terminated successfully.", SelectedBike.Id);
await ViewService.DisplayAlert(
string.Format(AppResources.MessageRentalProcessEndRentalFinishedTitle, SelectedBike.Id),
string.Format(
"{0}{1}{2}{3}{4}",
!string.IsNullOrWhiteSpace(bookingFinished?.Distance) ?
$"{string.Format(AppResources.MessageRentalProcessEndRentalFinishedDistanceText, bookingFinished?.Distance)}\r\n"
: string.Empty,
!string.IsNullOrWhiteSpace(bookingFinished?.Co2Saving) ?
$"{string.Format(AppResources.MessageRentalProcessEndRentalFinishedCO2SavingText, bookingFinished?.Co2Saving)}\r\n"
: string.Empty,
!string.IsNullOrWhiteSpace(bookingFinished?.Duration) ?
$"{string.Format(AppResources.MessageRentalProcessEndRentalFinishedDurationText, bookingFinished?.Duration)}\r\n"
: $"{string.Empty}",
!string.IsNullOrWhiteSpace(bookingFinished?.RentalCosts) ?
$"{string.Format(AppResources.MessageRentalProcessEndRentalFinishedRentalCostsText, bookingFinished?.RentalCosts)}\r\n"
: $"{AppResources.MessageRentalProcessEndRentalFinishedNoRentalCostsText}\r\n",
AppResources.MessageRentalProcessEndRentalFinishedText
),
AppResources.MessageAnswerOk
);
// Mini survey
if (bookingFinished != null && bookingFinished.MiniSurvey.Questions.Count > 0)
{
await ViewService.PushModalAsync(ViewTypes.MiniSurvey);
}
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return;
}
else
{
BikesViewModel.RentalProcess.State = CurrentRentalProcess.None;
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return;
}
}
}
}

View file

@ -0,0 +1,25 @@
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// Base interface for Copri and ILockIt request handler.
/// Copri communication is used by both handlers.
/// </summary>
public interface IRequestHandlerBase
{
/// <summary>
/// Gets a value indicating whether the copri button which is managed by request handler is visible or not.
/// </summary>
bool IsButtonVisible { get; }
/// <summary>View model to be used for progress report and unlocking/ locking view.</summary>
IBikesViewModel BikesViewModel { get; }
/// <summary>
/// Gets the text of the copri button which is managed by request handler.
/// </summary>
string ButtonText { get; }
/// <summary>Gets the is connected state. </summary>
bool IsConnected { get; }
}
}

View file

@ -0,0 +1,615 @@
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
{
/// <summary>
/// Return bike action.
/// </summary>
/// <typeparam name="T">Type of owner.</typeparam>
public class StartReservationOrRentalActionViewModel<T> :
StartReservationCommand.IStartReservationCommandListener,
AuthCommand.IAuthCommandListener, StartRentalCommand.IStartRentalCommandListener,
ConnectAndGetStateCommand.IConnectAndGetStateCommandListener,
DisconnectCommand.IDisconnectCommandListener,
INotifyPropertyChanged
{
/// <summary> Notifies view about changes. </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// View model to be used for progress report and unlocking/ locking view.
/// </summary>
private IBikesViewModel BikesViewModel { get; set; }
/// <summary>
/// View service to show modal notifications.
/// </summary>
private IViewService ViewService { get; }
/// <summary>Object to start or stop update of view model objects from Copri.</summary>
private Func<IPollingUpdateTaskManager> ViewUpdateManager { get; }
/// <summary> Bike </summary>
private IBikeInfoMutable SelectedBike { get; set; }
/// <summary> Provides a connector object.</summary>
protected Func<bool, IConnector> ConnectorFactory { get; }
/// <summary>
/// Constructs the object.
/// </summary>
/// <param name="selectedBike">Bike to close.</param>
/// <param name="viewUpdateManager">Object to start or stop update of view model objects from Copri.</param>
/// <param name="viewService">View service to show modal notifications.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
/// <exception cref="ArgumentException"></exception>
public StartReservationOrRentalActionViewModel(
IBikeInfoMutable selectedBike,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel)
{
SelectedBike = selectedBike;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {typeof(EndRentalActionViewModel<T>)}-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
});
}
/// <summary>
/// Processes the start reservation progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(StartReservationCommand.Step step)
{
switch (step)
{
case StartReservationCommand.Step.ReserveBike:
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeFirstStepRequest;
BikesViewModel.ActionText = AppResources.ActivityTextReservingBike;
break;
}
}
/// <summary>
/// Processes the start reservation state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
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;
}
}
/// <summary>
/// Processes the start reservation progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(AuthCommand.Step step)
{
switch (step)
{
case AuthCommand.Step.Authenticate:
BikesViewModel.RentalProcess.StepInfoText = AppResources.MarkingRentalProcessRequestBikeFirstStepAuthenticateInfo;
BikesViewModel.ActionText = AppResources.ActivityTextAuthenticate;
break;
}
}
/// <summary>
/// Processes the authentication state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
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;
}
}
/// <summary>
/// Processes the authentication progress.
/// </summary>
/// <param name="step">Current step to process.</param>
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;
}
}
/// <summary>
/// Processes the start rental state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
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;
}
}
/// <summary>
/// Processes the connect to lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
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;
}
}
/// <summary>
/// Processes the connect to lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
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;
}
}
}
/// <summary>
/// Processes the disconnect lock progress.
/// </summary>
/// <param name="step">Current step to process.</param>
public void ReportStep(DisconnectCommand.Step step)
{
switch (step)
{
case DisconnectCommand.Step.DisconnectLock:
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
break;
}
}
/// <summary>
/// Processes the disconnect lock state.
/// </summary>
/// <param name="state">State to process.</param>
/// <param name="details">Textual details describing current state.</param>
public Task ReportStateAsync(DisconnectCommand.State state, string details)
{
switch (state)
{
case DisconnectCommand.State.GeneralDisconnectError:
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
break;
}
return Task.CompletedTask;
}
/// <summary> Request bike. </summary>
public async Task StartReservationOrRentalAsync()
{
Log.ForContext<T>().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<T>().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<T>().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<T>().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<T>().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<T>().Information("Lock of bike {bikeId} was disconnected successfully.", SelectedBike.Id);
#endif
}
catch (Exception disconnectException)
{
Log.ForContext<T>().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;
}
/// <summary>
/// Default value of user request to open lock = false.
/// </summary>
private bool continueWithOpenLock = false;
/// <summary>
/// True if to continue with open lock.
/// </summary>
public bool ContinueWithOpenLock
{
get { return continueWithOpenLock; }
set
{
continueWithOpenLock = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ContinueWithOpenLock)));
}
}
/// <summary>
/// Default value of user started request with reservation = false.
/// </summary>
private bool startedWithReservation = false;
/// <summary>
/// True if user started request with reservation.
/// </summary>
public bool StartedWithReservation
{
get { return startedWithReservation; }
set
{
startedWithReservation = value;
PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StartedWithReservation)));
}
}
}
}

View file

@ -0,0 +1,79 @@
using System.Collections.ObjectModel;
using System.Linq;
using ShareeBike.Model.Bikes.BikeInfoNS;
namespace ShareeBike.ViewModel.Bikes.Bike
{
/// <summary>
/// View model for displaying tariff info.
/// </summary>
public class TariffDescriptionViewModel
{
private const string TRACKINGKEY = "TRACKING";
private const string RIDETYPEKEY = "AAFAHRTEN";
public TariffDescriptionViewModel(IRentalDescription tariff)
{
Name = tariff?.Name ?? string.Empty;
TariffEntries = tariff != null && tariff?.TariffEntries != null
? new ObservableCollection<RentalDescription.TariffElement>(tariff.TariffEntries.OrderBy(x => x.Key).Select(x => x.Value))
: new ObservableCollection<RentalDescription.TariffElement>();
// Add all entires except the known entries which are kept as properties.
InfoEntries = tariff != null && tariff?.InfoEntries != null
? new ObservableCollection<string>(tariff.InfoEntries
.Where(x => x.Value.Key.ToUpper() != TRACKINGKEY
&& x.Value.Key.ToUpper() != RIDETYPEKEY)
.OrderBy(x => x.Key)
.Select(x => x.Value.Value))
: new ObservableCollection<string>();
RideTypeText = tariff?.InfoEntries != null ? tariff?.InfoEntries?.FirstOrDefault(x => x.Value.Key.ToUpper() == RIDETYPEKEY).Value?.Value ?? string.Empty : string.Empty;
TrackingInfoText = tariff?.InfoEntries != null ? tariff?.InfoEntries?.FirstOrDefault(x => x.Value.Key.ToUpper() == TRACKINGKEY).Value?.Value ?? string.Empty : string.Empty;
}
/// <summary>
/// Holds the name of the tariff.
/// </summary>
public string Name { get; set; }
/// <summary>
/// Holds the tariff entries to display.
/// </summary>
public ObservableCollection<RentalDescription.TariffElement> TariffEntries { get; private set; }
/// <summary>
/// Holds the info entries to display.
/// </summary>
public ObservableCollection<string> InfoEntries { get; private set; }
/// <summary>
/// Holds the tracking info text or empty if not applicable.
/// </summary>
public string RideTypeText { get; private set; }
/// <summary>
/// Holds the tracking info text or empty if not applicable.
/// </summary>
public string TrackingInfoText { get; private set; }
public RentalDescription.TariffElement TarifEntry1 => TariffEntries.Count > 0 ? TariffEntries[0] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry2 => TariffEntries.Count > 1 ? TariffEntries[1] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry3 => TariffEntries.Count > 2 ? TariffEntries[2] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry4 => TariffEntries.Count > 3 ? TariffEntries[3] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry5 => TariffEntries.Count > 4 ? TariffEntries[4] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry6 => TariffEntries.Count > 5 ? TariffEntries[5] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry7 => TariffEntries.Count > 6 ? TariffEntries[6] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry8 => TariffEntries.Count > 7 ? TariffEntries[7] : new RentalDescription.TariffElement();
public RentalDescription.TariffElement TarifEntry9 => TariffEntries.Count > 8 ? TariffEntries[8] : new RentalDescription.TariffElement();
public string InfoEntry1 => InfoEntries.Count > 0 ? InfoEntries[0] : string.Empty;
public string InfoEntry2 => InfoEntries.Count > 1 ? InfoEntries[1] : string.Empty;
public string InfoEntry3 => InfoEntries.Count > 2 ? InfoEntries[2] : string.Empty;
public string InfoEntry4 => InfoEntries.Count > 3 ? InfoEntries[3] : string.Empty;
public string InfoEntry5 => InfoEntries.Count > 4 ? InfoEntries[4] : string.Empty;
}
}