Initial version.

This commit is contained in:
Oliver Hauff 2021-05-13 20:03:07 +02:00
parent 193aaa1a56
commit b72c67a53e
228 changed files with 25924 additions and 0 deletions

View file

@ -0,0 +1,124 @@
using System;
using System.ComponentModel;
using TINK.Model.Connector;
using TINK.Model.User;
using TINK.View;
using TINK.ViewModel.Bikes.Bike.BC.RequestHandler;
using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable;
namespace TINK.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="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>
public BikeViewModel(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Action<int> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser activeUser,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel) : base(isConnectedDelegate, connectorFactory, bikeRemoveDelegate, viewUpdateManager, viewService, selectedBike, activeUser, stateInfoProvider, bikesViewModel)
{
RequestHandler = activeUser.IsLoggedIn
? RequestHandlerFactory.Create(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
viewService,
bikesViewModel,
ActiveUser)
: new NotLoggedIn(
selectedBike.State.Value,
viewService,
bikesViewModel,
ActiveUser);
}
/// <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,
ViewService,
BikesViewModel,
ActiveUser);
var handler = PropertyChanged;
if (handler != null)
{
handler(this, new PropertyChangedEventArgs(nameof(ButtonText)));
handler(this, new PropertyChangedEventArgs(nameof(IsButtonVisible)));
}
}
/// <summary> Gets visiblity 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> 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;
RequestHandler = await RequestHandler.HandleRequest();
if (lastHandler.IsRemoveBikeRequired)
{
BikeRemoveDelegate(Id);
}
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,98 @@
using System;
using TINK.Model.Connector;
using TINK.Model.State;
using TINK.Model.User;
using TINK.View;
namespace TINK.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 the bike state. </summary>
public abstract InUseStateEnum State { get; }
/// <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>
/// Reference on view servcie to show modal notifications and to perform navigation.
/// </summary>
protected IViewService ViewService { get; }
/// <summary> Provides an 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> Gets if the bike has to be remvoed after action has been completed. </summary>
public bool IsRemoveBikeRequired { get; set; }
/// <summary>
/// Constructs the reqest handler base.
/// </summary>
/// <param name="selectedBike">Bike which is reserved or for which reservation is canceled.</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,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
ButtonText = buttonText;
IsButtonVisible = isCopriButtonVisible;
SelectedBike = selectedBike;
IsConnectedDelegate = isConnectedDelegate;
ConnectorFactory = connectorFactory;
ViewUpdateManager = viewUpdateManager;
ViewService = viewService;
ActiveUser = activeUser;
IsRemoveBikeRequired = false;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
}
}

View file

@ -0,0 +1,78 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Bikes.Bike.BC;
using TINK.Model.State;
using TINK.Model.User;
using TINK.MultilingualResources;
using TINK.View;
namespace TINK.ViewModel.Bikes.Bike.BC.RequestHandler
{
public class Booked : IRequestHandler
{
/// <summary> Gets the bike state. </summary>
public InUseStateEnum State => InUseStateEnum.Booked;
/// <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.ActionReturn; // "Miete beenden"
/// <summary>
/// Reference on view servcie 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 remvoed 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,116 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Repository.Exception;
using TINK.Model.State;
using TINK.Model.User;
using TINK.MultilingualResources;
using TINK.View;
using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable;
namespace TINK.ViewModel.Bikes.Bike.BC.RequestHandler
{
/// <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,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, selectedBike.State.Value.GetActionText(), true, isConnectedDelegate, connectorFactory, viewUpdateManager, viewService, bikesViewModel, activeUser)
{
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Disposable;
/// <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.GetDisplayName(), StateRequestedInfo.MaximumReserveTime.Minutes),
AppResources.MessageAnswerYes,
AppResources.MessageAnswerNo);
if (l_oResult == false)
{
// User aborted booking process
Log.ForContext<Disposable>().Information("User selected availalbe 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().StopUpdatePeridically();
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoReserve(SelectedBike);
}
catch (Exception l_oException)
{
if (l_oException is BookingDeclinedException)
{
// Too many bikes booked.
Log.ForContext<Disposable>().Information("Request declined because maximum count of bikes {l_oException.MaxBikesCount} already requested/ booked.", (l_oException as BookingDeclinedException).MaxBikesCount);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.MessageTitleHint,
string.Format(AppResources.MessageReservationBikeErrorTooManyReservationsRentals, SelectedBike.Id, (l_oException as BookingDeclinedException).MaxBikesCount),
AppResources.MessageAnswerOk);
}
else if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<Disposable>().Information("User selected availalbe bike {l_oId} but reserving failed (Copri server not reachable).", SelectedBike.Id);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Verbingungsfehler beim Reservieren des Rads!",
string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"OK");
}
else
{
Log.ForContext<Disposable>().Error("User selected availalbe bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert("Fehler beim Reservieren des Rads!", l_oException.Message, "OK");
}
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().StartUpdateAyncPeridically();
// 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, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,14 @@

using System.Threading.Tasks;
namespace TINK.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 Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.State;
using TINK.Model.User;
using TINK.View;
namespace TINK.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)
{
State = state;
IsIdle = true;
ViewService = viewService;
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
public InUseStateEnum State { get; }
public bool IsButtonVisible => true;
public bool IsIdle { get; private set; }
public string ButtonText => State.GetActionText();
public string ActionText { get => BikesViewModel.ActionText; private set => BikesViewModel.ActionText = value; }
/// <summary>
/// Reference on view servcie 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 map page
ViewService.ShowPage(ViewTypes.LoginPage);
}
catch (Exception p_oException)
{
Log.ForContext<BikesViewModel>().Error("Ein unerwarteter Fehler ist auf der Seite Anmelden aufgetreten. Kontext: Aufruf nach Reservierungsversuch ohne Anmeldung. {@Exception}", p_oException);
IsIdle = true;
return this;
}
IsIdle = true;
return this;
}
}
}

View file

@ -0,0 +1,115 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Repository.Exception;
using TINK.Model.State;
using TINK.Model.User;
using TINK.MultilingualResources;
using TINK.View;
using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable;
namespace TINK.ViewModel.Bikes.Bike.BC.RequestHandler
{
public class Reserved : Base<BikeInfoMutable>, IRequestHandler
{
/// <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,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, AppResources.ActionCancelRequest, true, isConnectedDelegate, connectorFactory, viewUpdateManager, viewService, bikesViewModel, activeUser)
{
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Reserved;
/// <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.GetDisplayName()),
"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().StopUpdatePeridically();
try
{
IsConnected = IsConnectedDelegate();
await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike);
// If canceling bike succedes remove bike because it is not ready to be booked again
IsRemoveBikeRequired = true;
}
catch (Exception l_oException)
{
if (l_oException 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!", l_oException.Message, "OK");
BikesViewModel.IsIdle = true;
return this;
}
else if (l_oException is WebConnectFailureException)
{
// 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!",
string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"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, l_oException);
BikesViewModel.ActionText = String.Empty;
await ViewService.DisplayAlert("Fehler beim Stornieren der Buchung!", l_oException.Message, "OK");
BikesViewModel.IsIdle = true;
return this;
}
}
finally
{
// Restart polling again.
await ViewUpdateManager().StartUpdateAyncPeridically();
// 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, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,56 @@
using System;
using TINK.Model.Connector;
using TINK.Model.User;
using TINK.View;
using TINK.ViewModel.Bikes.Bike.BC.RequestHandler;
using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable;
namespace TINK.ViewModel.Bikes.Bike.BC
{
public static class RequestHandlerFactory
{
/// <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,
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,
viewService,
bikesViewModel,
activeUser);
case Model.State.InUseStateEnum.Reserved:
// Reservation can be cancelled.
return new Reserved(
selectedBike,
isConnectedDelegate,
connectorFactory,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
default:
// No action using app possible.
return new Booked(
selectedBike,
viewService,
bikesViewModel,
activeUser);
}
}
}
}

View file

@ -0,0 +1,26 @@
using TINK.Model.State;
namespace TINK.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,309 @@
using System;
using System.ComponentModel;
using TINK.Model.Connector;
using TINK.Model.State;
using TINK.Model.User;
using TINK.MultilingualResources;
using TINK.View;
using Xamarin.Forms;
using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable;
namespace TINK.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>
/// Reference on view servcie to show modal notifications and to perform navigation.
/// </summary>
protected IViewService ViewService { get; }
/// <summary> Provides an connector 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<int> 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>
/// 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>
/// Notifies GUI about changes.
/// </summary>
public abstract event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Notfies childs 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="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>
public BikeViewModelBase(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
Action<int> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser activeUser,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel)
{
IsConnectedDelegate = isConnectedDelegate;
ConnectorFactory = connectorFactory;
BikeRemoveDelegate = bikeRemoveDelegate;
ViewUpdateManager = viewUpdateManager;
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)));
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);
BikesViewModel = bikesViewModel
?? throw new ArgumentException($"Can not construct {GetType().Name}-object. {nameof(bikesViewModel)} must not be null.");
}
/// <summary>
/// Handles BikeInfoMutable events.
/// Helper member to raise events. Maps model event change notification to view model events.
/// </summary>
/// <param name="p_strNameOfProp"></param>
private void OnSelectedBikePropertyChanged(string p_strNameOfProp)
{
if (p_strNameOfProp == 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
{
get
{
return bike.GetDisplayName();
}
}
/// <summary>
/// Gets the unique Id of bike used by derived model to determine which bike to remove.
/// </summary>
public int Id
{
get { return bike.Id; }
}
/// <summary>
/// Returns status of a bike as text.
/// </summary>
/// <todo> Log invalid states for diagnose purposes.</todo>
public string StateText
{
get
{
switch (bike.State.Value)
{
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.CurrentStation,
null); // Hide reservation code because no one but active user should see code
case InUseStateEnum.Booked:
return GetBookedInfo(
bike.State.From,
bike.CurrentStation,
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.CurrentStation,
bike.State.Code)
: "Fahrrad bereits reserviert durch anderen Nutzer.";
case InUseStateEnum.Booked:
return bike.State.MailAddress == ActiveUser.Mail
? GetBookedInfo(
bike.State.From,
bike.CurrentStation,
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 unexpeced states.</todo>
/// <param name="p_oInUseState"></param>
/// <returns>Display text</returns>
private string GetReservedInfo(
TimeSpan? p_oRemainingTime,
int? 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 unexpeced states.</todo>
/// <param name="p_oInUseState"></param>
/// <returns>Display text</returns>
private string GetBookedInfo(
DateTime? p_oFrom,
int? 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 tarif. </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; }
}
}

View file

@ -0,0 +1,52 @@
using System;
using TINK.Model.Connector;
using TINK.Services.BluetoothLock;
using TINK.Model.Services.Geolocation;
using TINK.Model.User;
using TINK.View;
namespace TINK.ViewModel.Bikes.Bike
{
public static class BikeViewModelFactory
{
/// <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>
public static BikeViewModelBase Create(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocation geolocation,
ILocksService lockService,
Action<int> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
Model.Bike.BC.BikeInfoMutable bikeInfo,
IUser activeUser,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel)
{
return bikeInfo as Model.Bikes.Bike.BluetoothLock.IBikeInfoMutable != null
? new BluetoothLock.BikeViewModel(
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
bikeRemoveDelegate,
viewUpdateManager,
viewService,
bikeInfo as Model.Bike.BluetoothLock.BikeInfoMutable,
activeUser,
stateInfoProvider,
bikesViewModel) as BikeViewModelBase
: new BC.BikeViewModel(
isConnectedDelegate,
connectorFactory,
bikeRemoveDelegate,
viewUpdateManager,
viewService,
bikeInfo,
activeUser,
stateInfoProvider,
bikesViewModel);
}
}
}

View file

@ -0,0 +1,188 @@
using System;
using System.ComponentModel;
using System.Runtime.CompilerServices;
using TINK.Model.Connector;
using TINK.Services.BluetoothLock;
using TINK.Model.Services.Geolocation;
using TINK.Model.User;
using TINK.View;
using BikeInfoMutable = TINK.Model.Bike.BluetoothLock.BikeInfoMutable;
using System.Threading.Tasks;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock
{
/// <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;
private IGeolocation Geolocation { 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>
/// Constructs a bike view model object.
/// </summary>
/// <param name="selectedBike">Bike to be displayed.</param>
/// <param name="user">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>
public BikeViewModel(
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocation geolocation,
ILocksService lockService,
Action<int> bikeRemoveDelegate,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
BikeInfoMutable selectedBike,
IUser user,
IInUseStateInfoProvider stateInfoProvider,
IBikesViewModel bikesViewModel) : base(isConnectedDelegate, connectorFactory, bikeRemoveDelegate, viewUpdateManager, viewService, selectedBike, user, stateInfoProvider, bikesViewModel)
{
RequestHandler = user.IsLoggedIn
? RequestHandlerFactory.Create(
selectedBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
user)
: new NotLoggedIn(
selectedBike.State.Value,
viewService,
bikesViewModel);
Geolocation = geolocation
?? throw new ArgumentException($"Can not instantiate {this.GetType().Name}-object. Parameter {nameof(geolocation)} can not be null.");
LockService = lockService
?? throw new ArgumentException($"Can not instantiate {this.GetType().Name}-object. Parameter {nameof(lockService)} can not be null.");
}
/// <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,
Geolocation,
LockService,
ViewUpdateManager,
ViewService,
BikesViewModel,
ActiveUser);
RaisePropertyChangedEvent(lastHandler);
}
/// <summary> Gets visiblity 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 visiblity 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). </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 lastStateText = StateText;
var lastStateColor = StateColor;
RequestHandler = await handleRequest;
if (lastHandler.IsRemoveBikeRequired)
{
BikeRemoveDelegate(Id);
}
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,51 @@
using System;
using TINK.Model.Connector;
using TINK.Services.BluetoothLock;
using TINK.Model.Services.Geolocation;
using TINK.Model.State;
using TINK.View;
using TINK.Model.User;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public abstract class Base : BC.RequestHandler.Base<Model.Bikes.Bike.BluetoothLock.IBikeInfoMutable>
{
/// <summary>
/// Constructs the reqest handler base.
/// </summary>
/// <param name="selectedBike">Bike which is reserved or for which reservation is canceled.</param>
/// <param name="bikesViewModel">View model to be used for progress report and unlocking/ locking view.</param>
public Base(
Model.Bikes.Bike.BluetoothLock.IBikeInfoMutable selectedBike,
string buttonText,
bool isCopriButtonVisible,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(selectedBike, buttonText, isCopriButtonVisible, isConnectedDelegate, connectorFactory, viewUpdateManager, viewService, bikesViewModel, activeUser)
{
Geolocation = 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.");
}
protected IGeolocation Geolocation { get; }
protected ILocksService LockService { get; }
/// <summary> Gets the bike state. </summary>
public abstract override InUseStateEnum State { get; }
public string LockitButtonText { get; protected set; }
public bool IsLockitButtonVisible { get; protected set; }
public string ErrorText => string.Empty;
}
}

View file

@ -0,0 +1,378 @@
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using Serilog;
using TINK.Model.Repository.Exception;
using TINK.Services.BluetoothLock.Exception;
using TINK.MultilingualResources;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.User;
using TINK.Repository.Exception;
using Xamarin.Essentials;
using TINK.Model.Repository.Request;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedClosed : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionReturn, // Copri button text "Miete beenden"
true, // Show button to enabled returning of bike.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenAndPause;
IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike.
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Booked;
/// <summary> Return bike. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
BikesViewModel.IsIdle = false;
// Ask whether to really return bike?
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
$"Fahrrad {SelectedBike.GetDisplayName()} zurückgeben?",
"Ja",
"Nein");
if (l_oResult == false)
{
// User aborted returning bike process
Log.ForContext<BookedClosed>().Information("User selected booked bike {l_oId} in order to return but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
// Lock list to avoid multiple taps while copri action is pending.
Log.ForContext<BookedClosed>().Information("Request to return bike {bike} detected.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
// Check if bike is around.
LocationDto currentLocationDto = null;
var deviceState = LockService[SelectedBike.LockInfo.Id].GetDeviceState();
if (deviceState == DeviceState.Connected)
{
// Bluetooth is in reach
// Get geoposition to pass when returning.
var timeStamp = DateTime.Now;
BikesViewModel.ActionText = "Abfrage Standort...";
Location currentLocation = null;
try
{
currentLocation = await Geolocation.GetAsync(timeStamp);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<BookedClosed>().Information("Returning bike {Bike} is not possible. {Exception}", SelectedBike, ex);
}
currentLocationDto = currentLocation != null
? new LocationDto.Builder
{
Latitude = currentLocation.Latitude,
Longitude = currentLocation.Longitude,
Accuracy = currentLocation.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(currentLocation.Timestamp.DateTime),
}.Build()
: null;
}
else
{
// Bluetooth out of reach. Lock state is no more known.
SelectedBike.LockInfo.State = LockingState.Disconnected;
}
BikesViewModel.ActionText = "Gebe Rad zurück...";
IsConnected = IsConnectedDelegate();
var feedBackUri = SelectedBike?.OperatorUri;
try
{
await ConnectorFactory(IsConnected).Command.DoReturn(
SelectedBike,
currentLocationDto);
// If canceling bike succedes remove bike because it is not ready to be booked again
IsRemoveBikeRequired = true;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedClosed>().Information("User selected booked bike {bike} but returing failed (Copri server not reachable).", SelectedBike);
await ViewService.DisplayAlert(
"Verbingungsfehler beim Zurückgeben des Rads!",
string.Format("{0}\r\n{1}\r\n{2}", "Internet muss erreichbar sein zum Zurückgeben des Rads.", exception.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"OK");
}
else if (exception is NotAtStationException notAtStationException)
{
// COPRI returned an error.
Log.ForContext<BookedClosed>().Information("User selected booked bike {bike} but returning failed. COPRI returned an not at station error.", SelectedBike);
await ViewService.DisplayAlert(
AppResources.ErrorReturnBikeNotAtStationTitle,
string.Format(AppResources.ErrorReturnBikeNotAtStationMessage, notAtStationException.StationNr, notAtStationException.Distance),
"OK");
}
else if (exception is NoGPSDataException)
{
// COPRI returned an error.
Log.ForContext<BookedClosed>().Information("User selected booked bike {bike} but returing failed. COPRI returned an no GPS- data error.", SelectedBike);
await ViewService.DisplayAlert(
AppResources.ErrorReturnBikeNotAtStationTitle,
string.Format(AppResources.ErrorReturnBikeLockClosedNoGPSMessage),
"OK");
}
else if (exception is ResponseException copriException)
{
// COPRI returned an error.
Log.ForContext<BookedClosed>().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike);
await ViewService.DisplayAdvancedAlert(
"Statusfehler beim Zurückgeben des Rads!",
copriException.Message,
copriException.Response,
"OK");
}
else
{
Log.ForContext<BookedClosed>().Error("User selected availalbe bike {bike} but reserving failed. {@l_oException}", SelectedBike.Id, exception);
await ViewService.DisplayAlert(
"Fehler beim Zurückgeben des Rads!",
exception.Message, "OK");
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<BookedClosed>().Information("User returned bike {bike} successfully.", SelectedBike);
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedClosed>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
#if !USERFEEDBACKDLG_OFF
// Do get Feedback
var feedback = await ViewService.DisplayUserFeedbackPopup();
try
{
await ConnectorFactory(IsConnected).Command.DoSubmitFeedback(
new UserFeedbackDto { IsBikeBroken = feedback.IsBikeBroken, Message = feedback.Message },
feedBackUri);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike);
}
else
{
Log.ForContext<BookedOpen>().Error("User selected availalbe bike {bike} but reserving failed. {@l_oException}", SelectedBike.Id, exception);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
#endif
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
// 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().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedClosed>().Debug("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockOutOfReadMessage,
"OK");
}
else if (exception is CouldntOpenBoldBlockedException)
{
Log.ForContext<BookedClosed>().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockMessage,
"OK");
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStillClosedMessage,
"OK");
}
else
{
Log.ForContext<BookedClosed>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
"OK");
}
// When bold is blocked lock is still closed even if exception occurres.
// In all other cases state is supposed to be unknown. Example: Lock is out of reach and no more bluetooth connected.
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<BookedClosed>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<BookedClosed>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedClosed>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedClosed>().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<BookedClosed>().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<BookedClosed>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,193 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.Connector;
using TINK.Model.Repository.Exception;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using TINK.Model.Services.Geolocation;
using TINK.Model.State;
using TINK.MultilingualResources;
using TINK.View;
using TINK.Model.User;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedDisconnected : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) :
base(
selectedBike,
nameof(BookedDisconnected),
false,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionSearchLock;
IsLockitButtonVisible = true;
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Booked;
public Task<IRequestHandler> HandleRequestOption1()
{
throw new NotSupportedException();
}
/// <summary> Scan for lock.</summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption2()
{
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.IsIdle = false;
Log.ForContext<BookedDisconnected>().Information("Request to search {bike} detected.", SelectedBike);
// Stop polling before getting new auth-values.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextQuerryServer;
IsConnected = IsConnectedDelegate();
try
{
// Repeat booking to get a new seed/ k_user value.
await ConnectorFactory(IsConnected).Command.CalculateAuthKeys(SelectedBike);
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<DisposableDisconnected>().Information("User selected booked bike {l_oId} to connect to lock. (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"Internet muss erreichbar sein um Verbindung mit Schloss für gemietetes Rad herzustellen.\r\n{l_oException.Message}\r\n{WebConnectFailureException.GetHintToPossibleExceptionsReasons}",
"OK");
}
else
{
Log.ForContext<DisposableDisconnected>().Error("User selected booked bike {l_oId} to connect to lock. {@l_oException}", SelectedBike.Id, l_oException);
await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"Kommunikationsfehler bei Schlosssuche.\r\n{l_oException.Message}",
"OK");
}
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true;
return this;
}
LockInfoTdo result = null;
var continueConnect = true;
var retryCount = 1;
while (continueConnect && result == null)
{
BikesViewModel.ActionText = AppResources.ActivityTextSearchingLock;
try
{
result = await LockService.ConnectAsync(
new LockInfoAuthTdo.Builder { Id = SelectedBike.LockInfo.Id, Guid = SelectedBike.LockInfo.Guid, K_seed = SelectedBike.LockInfo.Seed, K_u = SelectedBike.LockInfo.UserKey }.Build(),
LockService.TimeOut.GetSingleConnect(retryCount));
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedDisconnected>().Debug("Lock can not be found. {Exception}", exception);
continueConnect = await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
"Schloss kann erst gefunden werden, wenn gemietetes Rad in der Nähe ist.",
"Wiederholen",
"Abbrechen");
}
else
{
Log.ForContext<BookedDisconnected>().Error("Lock can not be found. {Exception}", exception);
continueConnect = await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"{AppResources.ErrorBookedSearchMessage}\r\nDetails:\r\n{exception.Message}",
"Wiederholen",
"Abbrechen");
}
if (continueConnect)
{
retryCount++;
continue;
}
// Quit and restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return this;
}
}
if (result?.State == null)
{
Log.ForContext<BookedDisconnected>().Information("Lock for bike {bike} not found.", SelectedBike);
BikesViewModel.ActionText = "";
await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"Schlossstatus des gemieteten Rads konnte nicht ermittelt werden.",
"OK");
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return this;
}
var state = result.State.Value.GetLockingState();
SelectedBike.LockInfo.State = state;
SelectedBike.LockInfo.Guid = result?.Guid ?? new Guid();
Log.ForContext<BookedDisconnected>().Information($"State for bike {SelectedBike.Id} updated successfully. Value is {SelectedBike.LockInfo.State}.");
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,456 @@
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using Serilog;
using TINK.Model.Repository.Exception;
using TINK.Services.BluetoothLock.Exception;
using Xamarin.Essentials;
using TINK.MultilingualResources;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.User;
using TINK.Model.Repository.Request;
using TINK.Repository.Exception;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedOpen : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCloseAndReturn, // Copri button text: "Schloss schließen & Miete beenden"
true, // Show button to allow user to return bike.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionClose; // BT button text "Schließen".
IsLockitButtonVisible = true; // Show button to allow user to lock bike.
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Disposable;
/// <summary> Close lock and return bike.</summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption1()
{
// Ask whether to really return bike?
BikesViewModel.IsIdle = false;
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
$"Fahrrad {SelectedBike.GetDisplayName()} abschließen und zurückgeben?",
"Ja",
"Nein");
if (l_oResult == false)
{
// User aborted closing and returning bike process
Log.ForContext<BookedOpen>().Information("User selected booked bike {l_oId} in order to close and return but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
// Unlock bike.
Log.ForContext<BookedOpen>().Information("Request to return bike {bike} detected.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockOutOfReachMessage,
"OK");
}
else if (exception is CounldntCloseMovingException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockMovingMessage,
"OK");
}
else if (exception is CouldntCloseBoldBlockedException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoldBlockedMessage,
"OK");
}
else
{
Log.ForContext<BookedOpen>().Error("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
if (SelectedBike.LockInfo.State != LockingState.Closed)
{
Log.ForContext<BookedOpen>().Error($"Lock can not be closed. Invalid locking state state {SelectedBike.LockInfo.State} detected.");
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
SelectedBike.LockInfo.State == LockingState.Open
? AppResources.ErrorCloseLockStillOpenMessage
: string.Format(AppResources.ErrorCloseLockUnexpectedStateMessage, SelectedBike.LockInfo.State),
"OK");
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Get geoposition.
var timeStamp = DateTime.Now;
BikesViewModel.ActionText = "Abfrage Standort...";
Location currentLocation;
try
{
currentLocation = await Geolocation.GetAsync(timeStamp);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<BookedOpen>().Information("Returning bike {Bike} is not possible. {Exception}", SelectedBike, ex);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler bei Standortabfrage!",
string.Format($"Schloss schließen und Miete beenden ist nicht möglich.\r\n{ex.Message}"),
"OK");
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = "Gebe Rad zurück...";
IsConnected = IsConnectedDelegate();
var feedBackUri = SelectedBike?.OperatorUri;
try
{
await ConnectorFactory(IsConnected).Command.DoReturn(
SelectedBike,
currentLocation != null
? new LocationDto.Builder
{
Latitude = currentLocation.Latitude,
Longitude = currentLocation.Longitude,
Accuracy = currentLocation.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(currentLocation.Timestamp.DateTime),
}.Build()
: null);
// If canceling bike succedes remove bike because it is not ready to be booked again
IsRemoveBikeRequired = true;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User selected booked bike {bike} but returing failed (Copri server not reachable).", SelectedBike);
await ViewService.DisplayAlert(
"Verbingungsfehler beim Zurückgeben des Rads!",
string.Format("{0}\r\n{1}\r\n{2}", "Internet muss erreichbar sein beim Zurückgeben des Rads.", exception.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"OK");
}
else if (exception is NotAtStationException notAtStationException)
{
// COPRI returned an error.
Log.ForContext<BookedOpen>().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike);
await ViewService.DisplayAlert(
AppResources.ErrorReturnBikeNotAtStationTitle,
string.Format(AppResources.ErrorReturnBikeNotAtStationMessage, notAtStationException.StationNr, notAtStationException.Distance),
"OK");
}
else if (exception is NoGPSDataException)
{
// COPRI returned an error.
Log.ForContext<BookedOpen>().Information("User selected booked bike {bike} but returing failed. COPRI returned an no GPS- data error.", SelectedBike);
await ViewService.DisplayAlert(
AppResources.ErrorReturnBikeNotAtStationTitle,
string.Format(AppResources.ErrorReturnBikeLockOpenNoGPSMessage),
"OK");
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike);
await ViewService.DisplayAdvancedAlert(
"Statusfehler beim Zurückgeben des Rads!",
copriException.Message,
copriException.Response,
"OK");
}
else
{
Log.ForContext<BookedOpen>().Error("User selected availalbe bike {bike} but reserving failed. {@l_oException}", SelectedBike.Id, exception);
await ViewService.DisplayAlert("Fehler beim Zurückgeben des Rads!", exception.Message, "OK");
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<BookedOpen>().Information("User returned bike {bike} successfully.", SelectedBike);
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedClosed>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
#if !USERFEEDBACKDLG_OFF
// Do get Feedback
var feedback = await ViewService.DisplayUserFeedbackPopup();
try
{
await ConnectorFactory(IsConnected).Command.DoSubmitFeedback(
new UserFeedbackDto { IsBikeBroken = feedback.IsBikeBroken, Message = feedback.Message },
feedBackUri);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike);
}
else
{
Log.ForContext<BookedOpen>().Error("User selected availalbe bike {bike} but reserving failed. {@l_oException}", SelectedBike.Id, exception);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
#endif
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Close lock in order to pause ride and update COPRI lock state.</summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
// Unlock bike.
BikesViewModel.IsIdle = false;
Log.ForContext<BookedOpen>().Information("User request to lock bike {bike} in order to pause ride.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockOutOfReachMessage,
"OK");
}
else if (exception is CounldntCloseMovingException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockMovingMessage,
"OK");
}
else if (exception is CouldntCloseBoldBlockedException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoldBlockedMessage,
"OK");
}
else
{
Log.ForContext<BookedOpen>().Error("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Get geoposition.
var timeStamp = DateTime.Now;
BikesViewModel.ActionText = "Abfrage Standort...";
Location currentLocation = null;
try
{
currentLocation = await Geolocation.GetAsync(timeStamp);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<BookedOpen>().Information("Returning bike {Bike} is not possible. {Exception}", SelectedBike, ex);
BikesViewModel.ActionText = "Keine Standortinformationen verfügbar.";
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(
SelectedBike,
currentLocation != null
? new LocationDto.Builder
{
Latitude = currentLocation.Latitude,
Longitude = currentLocation.Longitude,
Accuracy = currentLocation.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(currentLocation.Timestamp.DateTime),
}.Build()
: null);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedOpen>().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<BookedOpen>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<BookedOpen>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,319 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.Connector;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Repository.Exception;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.MultilingualResources;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.User;
using TINK.Repository.Exception;
using Xamarin.Essentials;
using TINK.Model.Repository.Request;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class BookedUnknown : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionOpenAndPause, // Schloss öffnen und Miete fortsetzen.
true, // Show button to enabled returning of bike.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionClose; // BT button text "Schließen".;
IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike.
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Booked;
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
// Unlock bike.
Log.ForContext<BookedUnknown>().Information("User request to unlock bike {bike}.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedUnknown>().Debug("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockOutOfReadMessage,
"OK");
}
else if (exception is CouldntOpenBoldBlockedException)
{
Log.ForContext<BookedUnknown>().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockMessage,
"OK");
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<BookedUnknown>().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStillClosedMessage,
"OK");
}
else
{
Log.ForContext<BookedUnknown>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
"OK");
}
// When bold is blocked lock is still closed even if exception occurres.
// In all other cases state is supposed to be unknown. Example: Lock is out of reach and no more bluetooth connected.
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<BookedUnknown>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<BookedUnknown>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<BookedUnknown>().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<BookedUnknown>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Close lock in order to pause ride and update COPRI lock state.</summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
// Unlock bike.
BikesViewModel.IsIdle = false;
Log.ForContext<BookedUnknown>().Information("User request to lock bike {bike} in order to pause ride.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedUnknown>().Debug("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockOutOfReachMessage,
"OK");
}
else if (exception is CounldntCloseMovingException)
{
Log.ForContext<BookedUnknown>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockMovingMessage,
"OK");
}
else if (exception is CouldntCloseBoldBlockedException)
{
Log.ForContext<BookedUnknown>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoldBlockedMessage,
"OK");
}
else
{
Log.ForContext<BookedUnknown>().Error("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Get geoposition.
var timeStamp = DateTime.Now;
BikesViewModel.ActionText = "Abfrage Standort...";
Location currentLocation = null;
try
{
currentLocation = await Geolocation.GetAsync(timeStamp);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<BookedUnknown>().Information("Returning bike {Bike} is not possible. {Exception}", SelectedBike, ex);
BikesViewModel.ActionText = "Keine Standortinformationen verfügbar.";
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(
SelectedBike,
currentLocation != null
? new LocationDto.Builder
{
Latitude = currentLocation.Latitude,
Longitude = currentLocation.Longitude,
Accuracy = currentLocation.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(currentLocation.Timestamp.DateTime),
}.Build()
: null);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<BookedUnknown>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<BookedUnknown>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

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

View file

@ -0,0 +1,274 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.Connector;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Repository.Exception;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.MultilingualResources;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.User;
namespace TINK.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
{
/// <summary> Bike is disposable, lock is open and can be reached via bluetooth. </summary>
/// <remarks>
/// This state should never occure 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="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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionBookOrClose,
true, // Show copri button to enable reserving
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = GetType().Name;
IsLockitButtonVisible = false;
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Disposable;
/// <summary>Books bike by reserving bike, opening lock and booking bike.</summary>
/// <returns>Next request handler.</returns>
public async Task<IRequestHandler> HandleRequestOption1()
{
BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending.
// Stop polling before requesting bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
// Ask whether to really book bike or close lock?
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
$"Fahrrad {SelectedBike.GetDisplayName()} mieten oder Schloss schließen?",
"Mieten",
"Schloss schließen");
if (l_oResult == false)
{
// Close lock
Log.ForContext<DisposableOpen>().Information("User selected disposable bike {bike} in order to close lock.", SelectedBike);
// Unlock bike.
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<DisposableOpen>().Debug("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockOutOfReachMessage,
"OK");
}
else if (exception is CounldntCloseMovingException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockMovingMessage,
"OK");
}
else if (exception is CouldntCloseBoldBlockedException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoldBlockedMessage,
"OK");
}
else
{
Log.ForContext<DisposableOpen>().Error("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedClosed>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Lock list to avoid multiple taps while copri action is pending.
Log.ForContext<DisposableOpen>().Information("Request to book bike {bike}.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<DisposableOpen>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<DisposableOpen>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Notify corpi about unlock action in order to start booking.
BikesViewModel.ActionText = AppResources.ActivityTextRentingBike;
try
{
await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike);
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<DisposableOpen>().Information("User selected requested bike {l_oId} but reserving failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.MessageRentingBikeErrorConnectionTitle,
string.Format(AppResources.MessageErrorLockIsClosedThreeLines, l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<DisposableOpen>().Error("User selected requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException);
await ViewService.DisplayAlert(
AppResources.MessageRentingBikeErrorGeneralTitle,
string.Format(AppResources.MessageErrorLockIsClosedTwoLines, l_oException.Message),
AppResources.MessageAnswerOk);
}
// If booking failed lock bike again because bike is only reserved.
BikesViewModel.ActionText = "Verschließe Schloss...";
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
Log.ForContext<DisposableOpen>().Error("Locking bike after booking failure failed. {Exception}", exception);
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
}
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedClosed>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
// 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 BikesViewModel.ActionText is already set to empty.
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<DisposableOpen>().Information("User reserved bike {bike} successfully.", SelectedBike);
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
// 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 BikesViewModel.ActionText is already set to empty.
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
public async Task<IRequestHandler> HandleRequestOption2()
{
Log.ForContext<DisposableOpen>().Error("Click of unsupported button detected.");
return await Task.FromResult<IRequestHandler>(this);
}
}
}

View file

@ -0,0 +1,26 @@
using System.Threading.Tasks;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock
{
public interface IRequestHandler : IRequestHandlerBase
{
/// <summary> Gets a value indicating whether the ILockIt button which is managed by request hadnler 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 suceeded, same handler otherwise.</returns>
Task<IRequestHandler> HandleRequestOption1();
Task<IRequestHandler> HandleRequestOption2();
/// <summary>
/// Holds error discription (invalid state).
/// </summary>
string ErrorText { get; }
}
}

View file

@ -0,0 +1,62 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.State;
namespace TINK.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.");
State = copriState;
ErrorText = errorText;
Log.Error($"{errorText}. Copri state is {State} 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 => this.GetType().Name;
public bool IsConnected => false;
public InUseStateEnum State { get; }
private LockingState LockingState { get; }
public bool IsButtonVisible => false;
public string ButtonText => this.GetType().Name;
/// <summary> Gets if the bike has to be remvoed 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,90 @@
using Serilog;
using System;
using System.ComponentModel;
using System.Threading.Tasks;
using TINK.Model.State;
using TINK.View;
namespace TINK.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)
{
State = 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 InUseStateEnum State { get; }
public bool IsButtonVisible => true;
public bool IsLockitButtonVisible => false;
public string ButtonText => BC.StateToText.GetActionText(State);
public string LockitButtonText => GetType().Name;
/// <summary>
/// Reference on view servcie 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 remvoed 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(
"Hinweis",
"Bitte anmelden vor Reservierung eines Fahrrads!\r\nAuf Anmeldeseite wechseln?",
"Ja",
"Nein");
if (l_oResult == false)
{
// User aborted booking process
BikesViewModel.IsIdle = true;
return this;
}
try
{
// Switch to map page
ViewService.ShowPage(ViewTypes.LoginPage);
}
catch (Exception p_oException)
{
Log.ForContext<BikesViewModel>().Error("Ein unerwarteter Fehler ist auf der Seite Anmelden 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,348 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Repository.Exception;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.State;
using TINK.View;
using IBikeInfoMutable = TINK.Model.Bikes.Bike.BluetoothLock.IBikeInfoMutable;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.MultilingualResources;
using TINK.Model.User;
using TINK.Repository.Exception;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
/// <summary> Bike is reserved, lock is closed and and connected to app. </summary>
/// <remarks>
/// Occures when
/// - biks was reserved out of reach and is in reach now
/// - bike is is reserved while in reach
/// </remarks>
public class ReservedClosed : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCancelRequest, // Copri button text: "Reservierung abbrechen"
true, // Show button to enable canceling reservation.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionOpenAndBook; // Button text: "Schloss öffnen & Rad mieten"
IsLockitButtonVisible = true; // Show "Öffnen" button to enable unlocking
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Reserved;
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending.
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
string.Format(AppResources.QuestionCancelReservation, SelectedBike.GetDisplayName()),
AppResources.QuestionAnswerYes,
AppResources.QuestionAnswerNo);
if (l_oResult == false)
{
// User aborted cancel process
Log.ForContext<ReservedClosed>().Information("User selected reserved bike {l_oId} in order to cancel reservation but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
Log.ForContext<ReservedClosed>().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id);
// Stop polling before cancel request.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike);
// If canceling bike succedes remove bike because it is not ready to be booked again
IsRemoveBikeRequired = true;
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is InvalidAuthorizationResponseException)
{
// Copri response is invalid.
Log.ForContext<BikesViewModel>().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id);
await ViewService.DisplayAlert(
"Fehler beim Aufheben der Reservierung!",
l_oException.Message,
"OK");
}
else if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BikesViewModel>().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
"Verbingungsfehler beim Aufheben der Reservierung!",
string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"OK");
}
else
{
Log.ForContext<BikesViewModel>().Error("User selected reserved bike {l_oId} but cancel reservation failed. {@l_oException}.", SelectedBike.Id, l_oException);
await ViewService.DisplayAlert(
"Fehler beim Aufheben der Reservierung!",
l_oException.Message,
"OK");
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<BikesViewModel>().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id);
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedClosed>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Open lock and book bike. </summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
BikesViewModel.IsIdle = false;
// Ask whether to really book bike?
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
string.Format(AppResources.MessageOpenLockAndBookeBike, SelectedBike.GetDisplayName()),
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 but action was canceled.", SelectedBike);
// Stop polling before cancel request.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
// Book bike prior to opening lock.
BikesViewModel.ActionText = AppResources.ActivityTextRentingBike;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike);
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedClosed>().Information("User selected requested bike {l_oId} but booking failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAdvancedAlert(
AppResources.MessageRentingBikeErrorConnectionTitle,
WebConnectFailureException.GetHintToPossibleExceptionsReasons,
l_oException.Message,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<ReservedClosed>().Error("User selected requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException);
await ViewService.DisplayAdvancedAlert(
AppResources.MessageRentingBikeErrorGeneralTitle,
string.Empty,
l_oException.Message,
AppResources.MessageAnswerOk);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Unlock bike.
BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<ReservedClosed>().Debug("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockOutOfReadMessage,
"OK");
}
else if (exception is CouldntOpenBoldBlockedException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockMessage,
"OK");
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStillClosedMessage,
"OK");
}
else
{
Log.ForContext<ReservedClosed>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
if (SelectedBike.LockInfo.State != LockingState.Open)
{
// Opening lock failed.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<ReservedClosed>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<ReservedClosed>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<BookedClosed>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<BookedClosed>().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<BookedClosed>().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<ReservedClosed>().Information("User reserved bike {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,465 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Repository.Exception;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.Services.BluetoothLock.Tdo;
using TINK.MultilingualResources;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.User;
using TINK.Repository.Exception;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class ReservedDisconnected : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionCancelRequest, // Copri button text: "Reservierung abbrechen"
true, // Show button to enable canceling reservation.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionSearchLock;
IsLockitButtonVisible = true; // Show "Öffnen" button to enable unlocking
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Reserved;
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending.
var alertResult = await ViewService.DisplayAlert(
string.Empty,
string.Format(AppResources.QuestionCancelReservation, SelectedBike.GetDisplayName()),
AppResources.QuestionAnswerYes,
AppResources.QuestionAnswerNo);
if (alertResult == false)
{
// User aborted cancel process
Log.ForContext<ReservedDisconnected>().Information("User selected reserved bike {l_oId} in order to cancel reservation but action was canceled.", SelectedBike.Id);
BikesViewModel.IsIdle = true;
return this;
}
Log.ForContext<ReservedDisconnected>().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id);
// Stop polling before cancel request.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike);
// If canceling bike succedes remove bike because it is not ready to be booked again
IsRemoveBikeRequired = true;
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is InvalidAuthorizationResponseException)
{
// Copri response is invalid.
Log.ForContext<ReservedDisconnected>().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id);
await ViewService.DisplayAlert("Fehler beim Aufheben der Reservierung!", l_oException.Message, "OK");
}
else if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedDisconnected>().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
"Verbingungsfehler beim Aufheben der Reservierung!",
string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"OK");
}
else
{
Log.ForContext<ReservedDisconnected>().Error("User selected reserved bike {l_oId} but cancel reservation failed. {@l_oException}.", SelectedBike.Id, l_oException);
await ViewService.DisplayAlert("Fehler beim Aufheben der Reservierung!", l_oException.Message, "OK");
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<ReservedDisconnected>().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Connect to reserved bike. </summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption2()
{
BikesViewModel.IsIdle = false;
Log.ForContext<ReservedDisconnected>().Information("Request to search for {bike} detected.", SelectedBike);
// Stop polling before getting new auth-values.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextQuerryServer;
IsConnected = IsConnectedDelegate();
try
{
// Repeat reservation to get a new seed/ k_user value.
await ConnectorFactory(IsConnected).Command.CalculateAuthKeys(SelectedBike);
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedDisconnected>().Information("User selected requested bike {l_oId} to connect to lock. (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"Internet muss erreichbar sein um Verbindung mit Schloss für reserviertes Rad herzustellen.\r\n{exception.Message}\r\n{WebConnectFailureException.GetHintToPossibleExceptionsReasons}",
"OK");
}
else
{
Log.ForContext<ReservedDisconnected>().Error("User selected requested bike {l_oId} to scan for lock. {@l_oException}", SelectedBike.Id, exception);
await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"Kommunikationsfehler bei Schlosssuche.\r\n{exception.Message}",
"OK");
}
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return this;
}
// Connect to lock.
LockInfoTdo result = null;
var continueConnect = true;
var retryCount = 1;
while (continueConnect && result == null)
{
BikesViewModel.ActionText = AppResources.ActivityTextSearchingLock;
try
{
result = await LockService.ConnectAsync(
new LockInfoAuthTdo.Builder
{
Id = SelectedBike.LockInfo.Id,
Guid = SelectedBike.LockInfo.Guid,
K_seed = SelectedBike.LockInfo.Seed,
K_u = SelectedBike.LockInfo.UserKey
}.Build(),
LockService.TimeOut.GetSingleConnect(retryCount));
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<ReservedDisconnected>().Debug("Lock state can not be retrieved. {Exception}", exception);
continueConnect = await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
"Schloss kann erst gefunden werden, wenn reserviertes Rad in der Nähe ist.",
"Wiederholen",
"Abbrechen");
}
else
{
Log.ForContext<ReservedDisconnected>().Error("Lock state can not be retrieved. {Exception}", exception);
continueConnect = await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"{AppResources.ErrorReservedSearchMessage}\r\nDetails:\r\n{exception.Message}",
"Wiederholen",
"Abbrechen");
}
if (continueConnect)
{
retryCount++;
continue;
}
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return this;
}
}
if (result?.State == null)
{
Log.ForContext<ReservedDisconnected>().Information("Lock for bike {bike} not found.", SelectedBike);
BikesViewModel.ActionText = "";
await ViewService.DisplayAlert(
"Fehler bei Verbinden mit Schloss!",
$"Schlossstatus des reservierten Rads konnte nicht ermittelt werden.",
"OK");
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return this;
}
var state = result.State.Value.GetLockingState();
SelectedBike.LockInfo.State = state;
SelectedBike.LockInfo.Guid = result?.Guid ?? new Guid();
Log.ForContext<ReservedDisconnected>().Information($"State for bike {SelectedBike.Id} updated successfully. Value is {SelectedBike.LockInfo.State}.");
BikesViewModel.ActionText = string.Empty;
// Ask whether to really book bike?
var alertResult = await ViewService.DisplayAlert(
string.Empty,
string.Format(AppResources.MessageOpenLockAndBookeBike, SelectedBike.GetDisplayName()),
AppResources.MessageAnswerYes,
AppResources.MessageAnswerNo);
if (alertResult == false)
{
// User aborted booking process
Log.ForContext<ReservedDisconnected>().Information("User selected recently requested bike {bike} in order to reserve but did deny to book bike.", SelectedBike);
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedDisconnected>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<ReservedDisconnected>().Information("User selected recently requested bike {bike} in order to book.", SelectedBike);
// Book bike prior to opening lock.
BikesViewModel.ActionText = AppResources.ActivityTextRentingBike;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike);
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedDisconnected>().Information("User selected recently requested bike {l_oId} but booking failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAdvancedAlert(
AppResources.MessageRentingBikeErrorConnectionTitle,
WebConnectFailureException.GetHintToPossibleExceptionsReasons,
l_oException.Message,
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<ReservedDisconnected>().Error("User selected recently requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException);
await ViewService.DisplayAdvancedAlert(
AppResources.MessageRentingBikeErrorGeneralTitle,
string.Empty,
l_oException.Message,
AppResources.MessageAnswerOk);
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Unlock bike.
BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<ReservedDisconnected>().Debug("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockOutOfReadMessage,
"OK");
}
else if (exception is CouldntOpenBoldBlockedException)
{
Log.ForContext<ReservedDisconnected>().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockMessage,
"OK");
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<ReservedDisconnected>().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStillClosedMessage,
"OK");
}
else
{
Log.ForContext<ReservedDisconnected>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
if (SelectedBike.LockInfo.State != LockingState.Open)
{
// Opening lock failed.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<ReservedDisconnected>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<ReservedDisconnected>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedDisconnected>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<ReservedDisconnected>().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<ReservedDisconnected>().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<ReservedDisconnected>().Information("User reserved bike {bike} successfully.", SelectedBike);
// Restart polling again.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,481 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Connector;
using TINK.Model.Repository.Exception;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.MultilingualResources;
using TINK.Model.User;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
/// <summary> Bike is reserved, lock is open and connected to app. </summary>
/// <remarks>
/// This state might occure 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 conect to same lock.
public class ReservedOpen : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
"Rad zurückgeben oder mieten",
true, // Show button to enable canceling reservation.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = "Alarm/ Sounds verwalten";
IsLockitButtonVisible = activeUser.DebugLevel > 0; // Will be visible in future version of user with leveraged privileges.
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Reserved;
/// <summary> Cancel reservation. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
BikesViewModel.IsIdle = false; // Lock list to avoid multiple taps while copri action is pending.
var l_oResult = await ViewService.DisplayAlert(
string.Empty,
string.Format("Rad {0} abschließen und zurückgeben oder Rad mieten?", SelectedBike.GetDisplayName()),
"Zurückgeben",
"Mieten");
// Stop polling before cancel request.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
if (l_oResult == false)
{
// User decided to book
Log.ForContext<ReservedOpen>().Information("User selected requested bike {bike} in order to book.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<ReservedOpen>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<ReservedOpen>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Notify corpi about unlock action in order to start booking.
BikesViewModel.ActionText = AppResources.ActivityTextRentingBike;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike);
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = string.Empty;
if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedOpen>().Information("User selected requested bike {l_oId} but booking failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
AppResources.MessageRentingBikeErrorConnectionTitle,
string.Format(AppResources.MessageErrorLockIsClosedThreeLines, l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
AppResources.MessageAnswerOk);
}
else
{
Log.ForContext<ReservedOpen>().Error("User selected requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException);
await ViewService.DisplayAlert(
AppResources.MessageRentingBikeErrorGeneralTitle,
string.Format(AppResources.MessageErrorLockIsClosedTwoLines, l_oException.Message),
AppResources.MessageAnswerOk);
}
// If booking failed lock bike again because bike is only reserved.
BikesViewModel.ActionText = "Wiederverschließe Schloss...";
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Locking bike after booking failure failed. {Exception}", exception);
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<ReservedOpen>().Information("User booked bike {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Close lock and cancel reservation.
Log.ForContext<ReservedClosed>().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id);
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockOutOfReachStateReservedMessage,
"OK");
}
else if (exception is CounldntCloseMovingException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock bike is moving. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockMovingMessage,
"OK");
}
else if (exception is CouldntCloseBoldBlockedException)
{
Log.ForContext<BookedOpen>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoldBlockedMessage,
"OK");
}
else
{
Log.ForContext<BookedOpen>().Error("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
string.Format(AppResources.ErrorCloseLockUnkErrorMessage, exception.Message),
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike);
// If canceling bike succedes remove bike because it is not ready to be booked again
IsRemoveBikeRequired = true;
}
catch (Exception l_oException)
{
BikesViewModel.ActionText = String.Empty;
if (l_oException is InvalidAuthorizationResponseException)
{
// Copri response is invalid.
Log.ForContext<ReservedOpen>().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id);
await ViewService.DisplayAlert("Fehler beim Aufheben der Reservierung!", l_oException.Message, "OK");
}
else if (l_oException is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedOpen>().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id);
await ViewService.DisplayAlert(
"Verbingungsfehler beim Aufheben der Reservierung!",
string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons),
"OK");
}
else
{
Log.ForContext<ReservedOpen>().Error("User selected reserved bike {l_oId} but cancel reservation failed. {@l_oException}.", SelectedBike.Id, l_oException);
await ViewService.DisplayAlert("Fehler beim Aufheben der Reservierung!", l_oException.Message, "OK");
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = "";
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
Log.ForContext<ReservedOpen>().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id);
// Disconnect lock.
BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock;
try
{
SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid);
}
catch (Exception exception)
{
Log.ForContext<ReservedClosed>().Error("Lock can not be disconnected. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect;
}
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Manage sound/ alarm settings. </summary>
/// <returns></returns>
public async Task<IRequestHandler> HandleRequestOption2()
{
// Stop polling before requesting bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
// Close lock
Log.ForContext<ReservedOpen>().Information("User selected disposable bike {bike} in order to manage sound/ alarm settings.", SelectedBike);
// Check current state.
BikesViewModel.ActionText = "Schlosseinstellung abfragen...";
bool isAlarmOff;
try
{
isAlarmOff = await LockService[SelectedBike.LockInfo.Id].GetIsAlarmOffAsync();
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not get lock alarm settings. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Abfragen der Alarmeinstellungen!",
"Schloss kann erst geschlossen werden, wenn Rad in der Nähe ist.",
"OK");
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not get lock alarm settings. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Abfragen der Alarmeinstellungen!",
exception.Message,
"OK");
return this;
}
if (isAlarmOff)
{
// Switch on sound.
BikesViewModel.ActionText = "Anschalten von Sounds...";
try
{
await LockService[SelectedBike.LockInfo.Id].SetSoundAsync(SoundSettings.AllOn);
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not turn on sounds. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Anschalten der Sounds!",
"Sounds können erst angeschalten werden, wenn Rad in der Nähe ist.",
"OK");
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not turn on sounds. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Anschalten der Sounds!",
exception.Message,
"OK");
return this;
}
// Switch off alarm.
BikesViewModel.ActionText = "Anschalten von Alarm...";
try
{
await LockService[SelectedBike.LockInfo.Id].SetIsAlarmOffAsync(true);
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not turn on alarm settings. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Anschalten des Alarms!",
"Alarm kann erst angeschalten werden, wenn Rad in der Nähe ist.",
"OK");
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not turn on alarm. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Anschalten des Alarms!",
exception.Message,
"OK");
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
finally
{
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
}
await ViewService.DisplayAlert(
"Hinweis",
"Alarm und Sounds erfolgreich aktiviert",
"OK");
return this;
}
// Switch off sound.
BikesViewModel.ActionText = "Abschalten der Sounds...";
try
{
await LockService[SelectedBike.LockInfo.Id].SetSoundAsync(SoundSettings.AllOff);
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not turn off sounds. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Abschalten der Sounds!",
"Sounds können erst abgeschalten werden, wenn Rad in der Nähe ist.",
"OK");
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not turn off sounds. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Abschalten der Sounds!",
exception.Message,
"OK");
return this;
}
// Switch off alarm.
BikesViewModel.ActionText = "Abschalten von Alarm...";
try
{
await LockService[SelectedBike.LockInfo.Id].SetIsAlarmOffAsync(false);
}
catch (OutOfReachException exception)
{
Log.ForContext<ReservedOpen>().Debug("Can not turn off alarm settings. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Abschalten des Alarms!",
"Alarm kann erst abgeschalten werden, wenn Rad in der Nähe ist.",
"OK");
return this;
}
catch (Exception exception)
{
Log.ForContext<ReservedOpen>().Error("Can not turn off alarm. {Exception}", exception);
BikesViewModel.ActionText = string.Empty;
await ViewService.DisplayAlert(
"Fehler beim Abschalten des Alarms!",
exception.Message,
"OK");
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
finally
{
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true; // Unlock GUI
await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again.
}
await ViewService.DisplayAlert(
"Hinweis",
"Alarm und Sounds erfolgreich abgeschalten.",
"OK");
return this;
}
}
}

View file

@ -0,0 +1,319 @@
using Serilog;
using System;
using System.Threading.Tasks;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.Connector;
using TINK.Model.State;
using TINK.View;
using TINK.Model.Repository.Exception;
using TINK.Model.Services.Geolocation;
using TINK.Services.BluetoothLock;
using TINK.Services.BluetoothLock.Exception;
using TINK.MultilingualResources;
using TINK.Model.Bikes.Bike.BluetoothLock;
using TINK.Model.User;
using TINK.Repository.Exception;
using Xamarin.Essentials;
using TINK.Model.Repository.Request;
namespace TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler
{
public class ReservedUnknown : Base, IRequestHandler
{
/// <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,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser) : base(
selectedBike,
AppResources.ActionOpenAndBook, // BT button text "Schloss öffnen und Rad mieten."
false, // Show button to enabled returning of bike.
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser)
{
LockitButtonText = AppResources.ActionClose; // BT button text "Schließen"
IsLockitButtonVisible = true; // Show button to enable opening lock in case user took a pause and does not want to return the bike.
}
/// <summary> Gets the bike state. </summary>
public override InUseStateEnum State => InUseStateEnum.Reserved;
/// <summary> Open bike and update COPRI lock state. </summary>
public async Task<IRequestHandler> HandleRequestOption1()
{
// Unlock bike.
Log.ForContext<ReservedUnknown>().Information("User request to unlock bike {bike}.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.IsIdle = false;
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<ReservedUnknown>().Debug("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockOutOfReadMessage,
"OK");
}
else if (exception is CouldntOpenBoldBlockedException)
{
Log.ForContext<ReservedUnknown>().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockMessage,
"OK");
}
else if (exception is CouldntOpenInconsistentStateExecption inconsistentState
&& inconsistentState.State == LockingState.Closed)
{
Log.ForContext<ReservedUnknown>().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
AppResources.ErrorOpenLockStillClosedMessage,
"OK");
}
else
{
Log.ForContext<ReservedUnknown>().Error("Lock can not be opened. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorOpenLockTitle,
exception.Message,
"OK");
}
// When bold is blocked lock is still closed even if exception occurres.
// In all other cases state is supposed to be unknown. Example: Lock is out of reach and no more bluetooth connected.
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel;
try
{
SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync());
}
catch (Exception exception)
{
if (exception is OutOfReachException)
{
Log.ForContext<ReservedUnknown>().Debug("Akkustate can not be read, bike out of range. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach;
}
else
{
Log.ForContext<ReservedUnknown>().Error("Akkustate can not be read. {Exception}", exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral;
}
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<ReservedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<ReservedUnknown>().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<ReservedUnknown>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
/// <summary> Close lock in order to pause ride and update COPRI lock state.</summary>
public async Task<IRequestHandler> HandleRequestOption2()
{
// Unlock bike.
BikesViewModel.IsIdle = false;
Log.ForContext<ReservedUnknown>().Information("User request to lock bike {bike} in order to pause ride.", SelectedBike);
// Stop polling before returning bike.
BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease;
await ViewUpdateManager().StopUpdatePeridically();
BikesViewModel.ActionText = AppResources.ActivityTextClosingLock;
try
{
SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].CloseAsync())?.GetLockingState() ?? LockingState.Disconnected;
}
catch (Exception exception)
{
BikesViewModel.ActionText = string.Empty;
if (exception is OutOfReachException)
{
Log.ForContext<ReservedUnknown>().Debug("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockOutOfReachMessage,
"OK");
}
else if (exception is CounldntCloseMovingException)
{
Log.ForContext<ReservedUnknown>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockMovingMessage,
"OK");
}
else if (exception is CouldntCloseBoldBlockedException)
{
Log.ForContext<ReservedUnknown>().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
AppResources.ErrorCloseLockBoldBlockedMessage,
"OK");
}
else
{
Log.ForContext<ReservedUnknown>().Error("Lock can not be closed. {Exception}", exception);
await ViewService.DisplayAlert(
AppResources.ErrorCloseLockTitle,
exception.Message,
"OK");
}
SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException
? stateAwareException.State
: LockingState.Disconnected;
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
// Get geoposition.
var timeStamp = DateTime.Now;
BikesViewModel.ActionText = "Abfrage Standort...";
Location currentLocation = null;
try
{
currentLocation = await Geolocation.GetAsync(timeStamp);
}
catch (Exception ex)
{
// No location information available.
Log.ForContext<ReservedUnknown>().Information("Returning bike {Bike} is not possible. {Exception}", SelectedBike, ex);
BikesViewModel.ActionText = "Keine Standortinformationen verfügbar.";
}
// Lock list to avoid multiple taps while copri action is pending.
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState;
IsConnected = IsConnectedDelegate();
try
{
await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(
SelectedBike,
currentLocation != null
? new LocationDto.Builder
{
Latitude = currentLocation.Latitude,
Longitude = currentLocation.Longitude,
Accuracy = currentLocation.Accuracy ?? double.NaN,
Age = timeStamp.Subtract(currentLocation.Timestamp.DateTime),
}.Build()
: null);
}
catch (Exception exception)
{
if (exception is WebConnectFailureException)
{
// Copri server is not reachable.
Log.ForContext<ReservedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate;
}
else if (exception is ResponseException copriException)
{
// Copri server is not reachable.
Log.ForContext<ReservedUnknown>().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response);
BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate;
}
else
{
Log.ForContext<ReservedUnknown>().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception);
BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate;
}
}
Log.ForContext<ReservedUnknown>().Information("User paused ride using {bike} successfully.", SelectedBike);
BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater;
await ViewUpdateManager().StartUpdateAyncPeridically();
BikesViewModel.ActionText = string.Empty;
BikesViewModel.IsIdle = true;
return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser);
}
}
}

View file

@ -0,0 +1,229 @@
using System;
using TINK.Model.Bike.BluetoothLock;
using TINK.Model.Connector;
using TINK.Services.BluetoothLock;
using TINK.Model.Services.Geolocation;
using TINK.View;
using TINK.ViewModel.Bikes.Bike.BluetoothLock.RequestHandler;
using TINK.Model.User;
using TINK.MultilingualResources;
using Serilog;
namespace TINK.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="bikeRemoveDelegate"></param>
/// <param name="viewUpdateManager"></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 IRequestHandler Create(
Model.Bikes.Bike.BC.IBikeInfoMutable selectedBike,
Func<bool> isConnectedDelegate,
Func<bool, IConnector> connectorFactory,
IGeolocation geolocation,
ILocksService lockService,
Func<IPollingUpdateTaskManager> viewUpdateManager,
IViewService viewService,
IBikesViewModel bikesViewModel,
IUser activeUser)
{
if (!(selectedBike is Model.Bikes.Bike.BluetoothLock.IBikeInfoMutable selectedBluetoothLockBike))
return null;
switch (selectedBluetoothLockBike.State.Value)
{
case Model.State.InUseStateEnum.Disposable:
// Bike is reserved, selecte action depending on lock state.
switch (selectedBluetoothLockBike.LockInfo.State)
{
case LockingState.Closed:
// Unexepected state detected.
// This state is unexpected because connection is closed
// - when reservation is canceled or
// - when bike is returned.
Log.Error("Unexpected state {BookingState}/ {LockingState} detected.", selectedBluetoothLockBike.State.Value, selectedBluetoothLockBike.LockInfo.State);
return new InvalidState(
bikesViewModel,
selectedBluetoothLockBike.State.Value,
selectedBluetoothLockBike.LockInfo.State,
string.Format(AppResources.MarkingBikeInfoErrorStateDisposableClosedDetected, selectedBluetoothLockBike.Description));
case LockingState.Open:
case LockingState.Unknown:
// Unexepected 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
/// Nevetheless 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,
viewService,
bikesViewModel,
activeUser);
default:
// Do not allow interaction with lock before reserving bike.
return new DisposableDisconnected(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
}
case Model.State.InUseStateEnum.Reserved:
// Bike is reserved, selecte 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,
viewService,
bikesViewModel,
activeUser);
case LockingState.Disconnected:
return new ReservedDisconnected(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
case LockingState.Open:
// Unwanted state detected.
/// This state might occure 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,
viewService,
bikesViewModel,
activeUser);
case LockingState.Unknown:
// User wants to return bike/ pause ride.
return new ReservedUnknown(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
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, selecte action depending on lock state.
switch (selectedBluetoothLockBike.LockInfo.State)
{
case LockingState.Closed:
// Ride was paused.
return new BookedClosed(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
case LockingState.Open:
// User wants to return bike/ pause ride.
return new BookedOpen(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
case LockingState.Unknown:
// User wants to return bike/ pause ride.
return new BookedUnknown(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
default:
// Invalid state detected.
// If bike is booked lock state must be querried before creating view model.
return new BookedDisconnected(
selectedBluetoothLockBike,
isConnectedDelegate,
connectorFactory,
geolocation,
lockService,
viewUpdateManager,
viewService,
bikesViewModel,
activeUser);
}
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,33 @@
using TINK.Model.State;
namespace TINK.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 the bike state. </summary>
InUseStateEnum State { get; }
/// <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; }
/// <summary> Gets if the bike has to be remvoed after action has been completed. </summary>
bool IsRemoveBikeRequired { get; }
}
}

View file

@ -0,0 +1,66 @@
using System;
using TINK.Model.Bikes.Bike;
using TINK.MultilingualResources;
namespace TINK.ViewModel.Bikes.Bike
{
/// <summary>
/// View model for displaying tariff info.
/// </summary>
public class TariffDescriptionViewModel
{
private TariffDescription Tariff { get; }
public TariffDescriptionViewModel(TariffDescription tariff)
{
Tariff = tariff;
}
public string Header
{
get
{
if (string.IsNullOrEmpty(FeeEuroPerHour)
&& string.IsNullOrEmpty(AboEuroPerMonth)
&& string.IsNullOrEmpty(FreeTimePerSession)
&& string.IsNullOrEmpty(MaxFeeEuroPerDay))
// No tariff description details available.
return string.Empty;
return string.Format(AppResources.MessageBikesManagementTariffDescriptionTariffHeader, Tariff?.Name ?? "-", Tariff?.Number != null ? Tariff.Number : "-");
}
}
/// <summary>
/// Costs per hour in euro.
/// </summary>
public string FeeEuroPerHour
=> !double.IsNaN(Tariff.FeeEuroPerHour)
? string.Format("{0} {1}", Tariff.FeeEuroPerHour.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerHour)
: string.Empty;
/// <summary>
/// Costs of the abo per month.
/// </summary>
public string AboEuroPerMonth
=> !double.IsNaN(Tariff.AboEuroPerMonth)
? string.Format("{0} {1}", Tariff.AboEuroPerMonth.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerHour)
: string.Empty;
/// <summary>
/// Free use time.
/// </summary>
public string FreeTimePerSession
=> Tariff.FreeTimePerSession != TimeSpan.Zero
? string.Format("{0} {1}", Tariff.FreeTimePerSession.TotalHours, AppResources.MessageBikesManagementTariffDescriptionHour)
: string.Empty;
/// <summary>
/// Max costs per day in euro.
/// </summary>
public string MaxFeeEuroPerDay
=> !double.IsNaN(Tariff.FeeEuroPerHour)
? string.Format("{0} {1}", Tariff.MaxFeeEuroPerDay.ToString("0.00"), AppResources.MessageBikesManagementMaxFeeEuroPerDay)
: string.Empty;
}
}