diff --git a/TINKLib/CSharp9.cs b/TINKLib/CSharp9.cs new file mode 100644 index 0000000..604ad5d --- /dev/null +++ b/TINKLib/CSharp9.cs @@ -0,0 +1,14 @@ +using System.ComponentModel; + +// ReSharper disable once CheckNamespace +namespace System.Runtime.CompilerServices +{ + /// + /// Reserved to be used by the compiler for tracking metadata. + /// This class should not be used by developers in source code. + /// + [EditorBrowsable(EditorBrowsableState.Never)] + internal static class IsExternalInit + { + } +} \ No newline at end of file diff --git a/TINKLib/Model/Bikes/Bike/BC/BikeInfo.cs b/TINKLib/Model/Bikes/Bike/BC/BikeInfo.cs new file mode 100644 index 0000000..c552060 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BC/BikeInfo.cs @@ -0,0 +1,216 @@ +using System; +using System.Collections.Generic; +using TINK.Model.Bikes.Bike; +using TINK.Model.State; + +namespace TINK.Model.Bike.BC +{ + public class BikeInfo : IBikeInfo + { + /// Default value of demo property. + public const bool DEFAULTVALUEISDEMO = false; + + /// Holds the info about the bike state. + private readonly IStateInfo m_oStateInfo; + + /// + /// Holds the bike object. + /// + private Bike Bike { get; } + + /// Constructs a bike object. + protected BikeInfo( + IStateInfo stateInfo, + int id, + bool? isDemo = DEFAULTVALUEISDEMO, + IEnumerable group = null, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null, + int? currentStationId = null, + Uri operatorUri = null, + TariffDescription tariffDescription = null) + { + Bike = new Bike(id, wheelType, typeOfBike, description); + + m_oStateInfo = stateInfo; + + IsDemo = isDemo ?? DEFAULTVALUEISDEMO; + Group = group ?? new List(); + CurrentStation = currentStationId; + OperatorUri = operatorUri; + TariffDescription = tariffDescription; + } + + public BikeInfo(BikeInfo bikeInfo) : this( + bikeInfo?.State, + bikeInfo?.Id ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source must not be null."), + bikeInfo.IsDemo, + bikeInfo.Group, + bikeInfo.WheelType, + bikeInfo.TypeOfBike, + bikeInfo.Description, + bikeInfo.CurrentStation, + bikeInfo.OperatorUri, + bikeInfo.TariffDescription) { } + + /// + /// Constructs a bike info object for a available bike. + /// + /// Unique id of bike. + /// Id of station where bike is located. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// + public BikeInfo( + int id, + int? currentStationId, + Uri operatorUri = null, + TariffDescription tariffDescription = null, + bool? isDemo = DEFAULTVALUEISDEMO, + IEnumerable group = null, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null) : this( + new StateInfo(), + id, + isDemo, + group, + wheelType, + typeOfBike, + description, + currentStationId, + operatorUri, + tariffDescription) + { + } + + /// + /// Constructs a bike info object for a requested bike. + /// + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + /// + /// Unique id of bike. + /// Name of station where bike is located, null if bike is on the road. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// Date time when bike was requested + /// Mail address of user which requested bike. + /// Booking code. + /// Date time provider to calculate reaining time. + public BikeInfo( + int id, + bool? isDemo, + IEnumerable group, + WheelType? wheelType, + TypeOfBike? typeOfBike, + string description, + int? stationId, + Uri operatorUri, + TariffDescription tariffDescription, + DateTime requestedAt, + string mailAddress, + string code, + Func dateTimeProvider = null) : this( + new StateInfo( + dateTimeProvider, + requestedAt, + mailAddress, + code), + id, + isDemo, + group, + wheelType, + typeOfBike, + description, + stationId, + operatorUri, + tariffDescription) + { + } + + /// + /// Constructs a bike info object for a booked bike. + /// + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + /// + /// Unique id of bike. + /// Name of station where bike is located, null if bike is on the road. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// Date time when bike was booked + /// Mail address of user which booked bike. + /// Booking code. + public BikeInfo( + int id, + bool? isDemo, + IEnumerable group, + WheelType? wheelType, + TypeOfBike? typeOfBike, + string description, + int? currentStationId, + Uri operatorUri, + TariffDescription tariffDescription, + DateTime bookedAt, + string mailAddress, + string code) : this( + new StateInfo( + bookedAt, + mailAddress, + code), + id, + isDemo, + group, + wheelType, + typeOfBike, + description, + currentStationId, + operatorUri, + tariffDescription) + { + } + + /// True if device is demo device, false otherwise. + public bool IsDemo { get; } + + /// Returns the group (TINK, Konrad, ...). + public IEnumerable Group { get; } + + /// + /// Station a which bike is located, null otherwise. + /// + public int? CurrentStation { get; } + + /// Holds description about the tarif. + public TariffDescription TariffDescription { get; } + + + /// Holds the rent state of the bike. + /// + public IStateInfo State + { + get { return m_oStateInfo; } + } + + public int Id => Bike.Id; + + public WheelType? WheelType => Bike.WheelType; + + public TypeOfBike? TypeOfBike => Bike.TypeOfBike; + + public string Description => Bike.Description; + + /// + /// Uri of the operator or null, in case of single operator setup. + /// + public Uri OperatorUri { get; } + + /// + /// Converts the instance to text. + /// + public new string ToString() + { + return $"Id={Bike.Id}{(Bike.WheelType != null ? $", wheel(s)={Bike.WheelType}" : string.Empty)}{(Bike.TypeOfBike != null ? $"type={Bike.TypeOfBike}" : "")}, state={State}, location={(CurrentStation.HasValue ? $"Station {CurrentStation}" : "On the road")}, is demo={IsDemo}."; + } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BC/BikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BC/BikeInfoMutable.cs new file mode 100644 index 0000000..80b9447 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BC/BikeInfoMutable.cs @@ -0,0 +1,124 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Runtime.Serialization; +using TINK.Model.Bikes.Bike; +using TINK.Model.Bikes.Bike.BC; +using TINK.Model.State; + +namespace TINK.Model.Bike.BC +{ + [DataContract] + public class BikeInfoMutable : IBikeInfoMutable, INotifyPropertyChanged + { + /// Holds the bike. + private readonly Bike m_oBike; + + /// Holds the state info of the bike. + private readonly StateInfoMutable m_oStateInfo; + + /// + /// Constructs a bike. + /// + /// Unique id of bike. + /// True if device is demo device, false otherwise. + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + /// + /// Name of station where bike is located, null if bike is on the road. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// Bike state info. + protected BikeInfoMutable( + int id, + bool isDemo = BikeInfo.DEFAULTVALUEISDEMO, + IEnumerable group = null, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null, + int? currentStationId = null, + Uri operatorUri = null, + TariffDescription tariffDescription = null, + Func dateTimeProvider = null, + IStateInfo stateInfo = null) + { + IsDemo = isDemo; + Group = group; + m_oBike = new Bike(id, wheelType, typeOfBike, description); + m_oStateInfo = new StateInfoMutable(dateTimeProvider, stateInfo); + m_oStateInfo.PropertyChanged += (sender, eventargs) => PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(eventargs.PropertyName)); + CurrentStation = currentStationId; + OperatorUri = operatorUri; + TariffDescription = tariffDescription; + } + + /// Constructs a bike object from source. + public BikeInfoMutable(IBikeInfo p_oBike) : this( + p_oBike.Id, + p_oBike.IsDemo, + p_oBike.Group, + p_oBike.WheelType, + p_oBike.TypeOfBike, + p_oBike.Description, + p_oBike.CurrentStation, + p_oBike.OperatorUri, + p_oBike.TariffDescription, + null, + p_oBike.State) + { + } + + /// + /// Station a which bike is located, null otherwise. + /// + [DataMember] + public int? CurrentStation { get; } + + /// Holds description about the tarif. + [DataMember] + public TariffDescription TariffDescription { get; private set; } + + /// + /// Holds the rent state of the bike. + /// + [DataMember] + public StateInfoMutable State + { + get { return m_oStateInfo; } + } + + /// + /// Uri of the operator or null, in case of single operator setup. + /// + public Uri OperatorUri { get; } + + /// Unused member. + IStateInfoMutable IBikeInfoMutable.State => m_oStateInfo; + + public int Id => m_oBike.Id; + + public bool IsDemo { get; } + + /// Returns the group (TINK, Konrad, ...). + public IEnumerable Group { get; } + + public WheelType? WheelType => m_oBike.WheelType; + + public TypeOfBike? TypeOfBike => m_oBike.TypeOfBike; + + public string Description => m_oBike.Description; + + /// + /// Fired whenever property of bike changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Converts the instance to text. + /// + /// + public new string ToString() + { + return $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $", type={TypeOfBike}" : "")}, demo={IsDemo}, state={State.ToString()}, location={(CurrentStation.HasValue ? $"Station {CurrentStation}" : "On the road")}."; + } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BC/IBikeInfo.cs b/TINKLib/Model/Bikes/Bike/BC/IBikeInfo.cs new file mode 100644 index 0000000..bcb6d13 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BC/IBikeInfo.cs @@ -0,0 +1,55 @@ +using System; +using System.Collections.Generic; +using TINK.Model.Bikes.Bike; +using TINK.Model.State; + +namespace TINK.Model.Bike.BC +{ + /// + /// Allows to access bike info. + /// + public interface IBikeInfo + { + /// + /// Holds the unique id of the bike; + /// + int Id { get; } + + /// True if bike is a demo bike. + bool IsDemo { get; } + + /// Returns the group (TINK, Konrad, ...). + IEnumerable Group { get; } + + /// + /// Holds the count of wheels. + /// + WheelType? WheelType { get; } + + /// + /// Holds the type of bike. + /// + TypeOfBike? TypeOfBike { get; } + + /// Holds the description of the bike. + string Description { get; } + + /// + /// Station a which bike is located, null otherwise. + /// + int? CurrentStation { get; } + + /// + /// Uri of the operator or null, in case of single operator setup. + /// + Uri OperatorUri { get; } + + /// Holds description about the tarif. + TariffDescription TariffDescription { get; } + + /// + /// Holds the rent state of the bike. + /// + IStateInfo State { get; } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BC/IBikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BC/IBikeInfoMutable.cs new file mode 100644 index 0000000..ede2168 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BC/IBikeInfoMutable.cs @@ -0,0 +1,61 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using TINK.Model.Bike; +using TINK.Model.State; + +namespace TINK.Model.Bikes.Bike.BC +{ + public interface IBikeInfoMutable + { + /// + /// Holds the unique id of the bike; + /// + int Id { get; } + + /// True if bike is a demo bike. + bool IsDemo { get; } + + /// Returns the group (TINK, Konrad, ...). + IEnumerable Group { get; } + + /// + /// Holds the count of wheels. + /// + WheelType? WheelType { get; } + + /// + /// Holds the type of bike. + /// + TypeOfBike? TypeOfBike { get; } + + /// Holds the description of the bike. + string Description { get; } + + /// + /// Station a which bike is located, null otherwise. + /// + int? CurrentStation { get; } + + /// + /// Holds the rent state of the bike. + /// + IStateInfoMutable State { get; } + + /// + /// Uri of the operator or null, in case of single operator setup. + /// + Uri OperatorUri { get; } + + event PropertyChangedEventHandler PropertyChanged; + } + + public enum NotifyPropertyChangedLevel + { + /// Notify about all property changes. + All, + + /// Notify about no property changes. + None + } +} diff --git a/TINKLib/Model/Bikes/Bike/Bike.cs b/TINKLib/Model/Bikes/Bike/Bike.cs new file mode 100644 index 0000000..51fcb70 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/Bike.cs @@ -0,0 +1,126 @@ +using System; +using System.Collections.Generic; + +namespace TINK.Model.Bike +{ + /// Count of wheels. + public enum WheelType + { + Mono = 0, + Two = 1, + Trike = 2, + } + + /// Type of bike. + public enum TypeOfBike + { + Allround = 0, + Cargo = 1, + Citybike = 2, + } + + public class Bike : IEquatable + { + /// + /// Constructs a bike. + /// + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + /// + /// Unique id of bike. + /// Name of station where bike is located, null if bike is on the road. + public Bike( + int p_iId, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null) + { + WheelType = wheelType; + TypeOfBike = typeOfBike; + Id = p_iId; + Description = description; + } + + /// + /// Holds the unique id of the bike; + /// + public int Id { get; } + + /// + /// Holds the count of wheels. + /// + public WheelType? WheelType { get; } + + /// + /// Holds the type of bike. + /// + public TypeOfBike? TypeOfBike { get; } + + /// Holds the description of the bike. + public string Description { get; } + + + /// Compares two bike object. + /// Object to compare with. + /// True if bikes are equal. + public override bool Equals(object obj) + { + var l_oBike = obj as Bike; + if (l_oBike == null) + { + return false; + } + + return Equals(l_oBike); + } + + /// Converts the instance to text. + public new string ToString() + { + return WheelType == null || TypeOfBike == null + ? $"Id={Id}{(!string.IsNullOrEmpty(Description) ? $", {Description}" : "")}" + : $"Id={Id}{(WheelType != null ? $", wheel(s)={WheelType}" : string.Empty)}{(TypeOfBike != null ? $"type={TypeOfBike}" : "")}."; + } + + /// Compares two bike object. + /// Object to compare with. + /// True if bikes are equal. + public bool Equals(Bike other) + { + return other != null && + Id == other.Id && + WheelType == other.WheelType && + TypeOfBike == other.TypeOfBike + && Description == other.Description; + } + + /// Compares two bike object. + /// Object to compare with. + /// True if bikes are equal. + public static bool operator ==(Bike bike1, Bike bike2) + { + return EqualityComparer.Default.Equals(bike1, bike2); + } + + /// Compares two bike object. + /// Object to compare with. + /// True if bikes are equal. + public static bool operator !=(Bike bike1, Bike bike2) + { + return !(bike1 == bike2); + } + + /// + /// Generates hash code for bike object. + /// + /// + public override int GetHashCode() + { + var hashCode = -390870100; + hashCode = hashCode * -1521134295 + Id.GetHashCode(); + hashCode = hashCode * -1521134295 + WheelType?.GetHashCode() ?? 0; + hashCode = hashCode * -1521134295 + TypeOfBike?.GetHashCode() ?? 0; + hashCode = hashCode * -1521134295 + Description?.GetHashCode() ?? 0; + return hashCode; + } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfo.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfo.cs new file mode 100644 index 0000000..2978e71 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfo.cs @@ -0,0 +1,151 @@ +using System; +using System.Collections.Generic; +using TINK.Model.Bikes.Bike; +using TINK.Model.State; + +namespace TINK.Model.Bike.BluetoothLock +{ + public class BikeInfo : BC.BikeInfo, IBikeInfo + { + /// + /// Constructs a bike info object for a available bike. + /// + /// Unique id of bike. + /// Id of the lock. + /// GUID specifying the lock. + /// Id of station where bike is located. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// Trike, two wheels, mono, .... + public BikeInfo( + int bikeId, + int lockId, + Guid lockGuid, + int? currentStationId, + Uri operatorUri = null, + TariffDescription tariffDescription = null, + bool? isDemo = DEFAULTVALUEISDEMO, + IEnumerable group = null, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null) : base( + new StateInfo(), + bikeId, + isDemo, + group, + wheelType, + typeOfBike, + description, + currentStationId, + operatorUri, + tariffDescription) + { + LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid }.Build(); + } + + /// + /// Constructs a bike info object for a requested bike. + /// + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + /// Unique id of bike. + /// Id of the lock. + /// GUID specifying the lock. + /// Date time when bike was requested + /// Mail address of user which requested bike. + /// Name of station where bike is located, null if bike is on the road. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// Date time provider to calculate reaining time. + /// + public BikeInfo( + int id, + int lockId, + Guid lockGuid, + byte[] userKey, + byte[] adminKey, + byte[] seed, + DateTime requestedAt, + string mailAddress, + int? currentStationId, + Uri operatorUri, + TariffDescription tariffDescription, + Func dateTimeProvider, + bool? isDemo = DEFAULTVALUEISDEMO, + IEnumerable group = null, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null) : base( + new StateInfo( + dateTimeProvider, + requestedAt, + mailAddress, + ""), + id, + isDemo, + group, + wheelType, + typeOfBike, + description, + currentStationId, + operatorUri, + tariffDescription) + { + LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build(); + } + + /// + /// Constructs a bike info object for a booked bike. + /// + /// Unique id of bike. + /// Id of the lock. + /// GUID specifying the lock. + /// Date time when bike was booked + /// Mail address of user which booked bike. + /// Name of station where bike is located, null if bike is on the road. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Hold tariff description of bike. + /// + public BikeInfo( + int id, + int lockId, + Guid lockGuid, + byte[] userKey, + byte[] adminKey, + byte[] seed, + DateTime bookedAt, + string mailAddress, + int? currentStationId, + Uri operatorUri, + TariffDescription tariffDescription = null, + bool? isDemo = DEFAULTVALUEISDEMO, + IEnumerable group = null, + WheelType? wheelType = null, + TypeOfBike? typeOfBike = null, + string description = null) : base( + new StateInfo( + bookedAt, + mailAddress, + ""), + id, + isDemo, + group, + wheelType, + typeOfBike, + description, + currentStationId, + operatorUri, + tariffDescription) + { + LockInfo = new LockInfo.Builder { Id = lockId, Guid = lockGuid, UserKey = userKey, AdminKey = adminKey, Seed = seed }.Build(); + } + + public BikeInfo(BC.BikeInfo bikeInfo, LockInfo lockInfo) : base( + bikeInfo ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source bike info must not be null.")) + { + LockInfo = lockInfo + ?? throw new ArgumentException($"Can not copy-construct {typeof(BikeInfo).Name}-object. Source lock object must not be null."); + } + + public LockInfo LockInfo { get; private set; } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfoMutable.cs new file mode 100644 index 0000000..fbc49cd --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/BikeInfoMutable.cs @@ -0,0 +1,36 @@ +using System; +using TINK.Model.Bikes.Bike; +using TINK.Model.Bikes.Bike.BluetoothLock; + +namespace TINK.Model.Bike.BluetoothLock +{ + public class BikeInfoMutable : BC.BikeInfoMutable, IBikeInfoMutable + { + /// Constructs a bike object from source. + public BikeInfoMutable(BikeInfo bike) : base( + bike.Id, + bike.IsDemo, + bike.Group, + bike.WheelType, + bike.TypeOfBike, + bike.Description, + bike.CurrentStation, + bike.OperatorUri, + bike.TariffDescription, + () => DateTime.Now, + bike.State) + { + LockInfo = new LockInfoMutable( + bike.LockInfo.Id, + bike.LockInfo.Guid, + bike.LockInfo.UserKey, + bike.LockInfo.AdminKey, + bike.LockInfo.Seed, + bike.LockInfo.State); + } + + public LockInfoMutable LockInfo { get; } + + ILockInfoMutable IBikeInfoMutable.LockInfo => LockInfo; + } +} diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfo.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfo.cs new file mode 100644 index 0000000..e0664db --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfo.cs @@ -0,0 +1,6 @@ +namespace TINK.Model.Bike.BluetoothLock +{ + public interface IBikeInfo : BC.IBikeInfo + { + } +} diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfoMutable.cs new file mode 100644 index 0000000..5467ec7 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/IBikeInfoMutable.cs @@ -0,0 +1,7 @@ +namespace TINK.Model.Bikes.Bike.BluetoothLock +{ + public interface IBikeInfoMutable : BC.IBikeInfoMutable + { + ILockInfoMutable LockInfo { get; } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/ILockInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/ILockInfoMutable.cs new file mode 100644 index 0000000..109aa30 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/ILockInfoMutable.cs @@ -0,0 +1,24 @@ +using System; +using TINK.Model.Bike.BluetoothLock; + +namespace TINK.Model.Bikes.Bike.BluetoothLock +{ + public interface ILockInfoMutable + { + /// Identification number of bluetooth lock. + int Id { get; } + + /// Gets the user key. + byte[] UserKey { get; } + + LockingState State { get; set; } + + /// Holds the percentage of lock battery. + double BatteryPercentage { get; set; } + + /// Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock. + Guid Guid { get; set; } + + byte[] Seed { get; } + } +} diff --git a/TINKLib/Model/Bikes/Bike/BluetoothLock/LockInfoMutable.cs b/TINKLib/Model/Bikes/Bike/BluetoothLock/LockInfoMutable.cs new file mode 100644 index 0000000..c53e0f6 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/BluetoothLock/LockInfoMutable.cs @@ -0,0 +1,54 @@ +using System; +using TINK.Model.Bikes.Bike.BluetoothLock; + +namespace TINK.Model.Bike.BluetoothLock +{ + public class LockInfoMutable : ILockInfoMutable + { + /// Lock info object. + private LockInfo LockInfo { get; set; } + + /// Constructs a bluetooth lock info object. + /// Id of lock must always been known when constructing an lock info object. + public LockInfoMutable( + int id, + Guid guid, + byte[] userKey, + byte[] adminKey, + byte[] seed, + LockingState state) + { + LockInfo = new LockInfo.Builder() { Id = id, Guid = guid, UserKey = userKey, AdminKey = adminKey, Seed = seed, State = state }.Build(); + } + + public int Id => LockInfo.Id; + + /// Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock. + public Guid Guid + { + get => LockInfo.Guid; + set => LockInfo = new LockInfo.Builder(LockInfo) { Guid = value }.Build(); + } + + public byte[] Seed => LockInfo.Seed; + + public byte[] UserKey => LockInfo.UserKey; + + public byte[] AdminKey => LockInfo.AdminKey; + + public LockingState State + { + get => LockInfo.State; + set => LockInfo = new LockInfo.Builder(LockInfo) { State = value }.Build(); + } + + /// Holds the percentage of lock battery. + public double BatteryPercentage { get; set; } = double.NaN; + + /// Loads lock info object from values. + public void Load(int id, Guid guid, byte[] seed, byte[] userKey, byte[] adminKey) + { + LockInfo = new LockInfo.Builder(LockInfo) { Id = id, Guid = guid, Seed = seed, UserKey = userKey, AdminKey = adminKey}.Build(); + } + } +} diff --git a/TINKLib/Model/Bikes/Bike/TariffDescription.cs b/TINKLib/Model/Bikes/Bike/TariffDescription.cs new file mode 100644 index 0000000..beeb6d2 --- /dev/null +++ b/TINKLib/Model/Bikes/Bike/TariffDescription.cs @@ -0,0 +1,40 @@ +using System; + +namespace TINK.Model.Bikes.Bike +{ + /// + /// Holds tariff info for a single bike. + /// + public record TariffDescription + { + /// + /// Name of the tariff. + /// + public string Name { get; init; } + + /// + /// Number of the tariff. + /// + public int? Number { get; init; } + + /// + /// Costs per hour in euro. + /// + public double FeeEuroPerHour { get; init; } + + /// + /// Costs of the abo per month. + /// + public double AboEuroPerMonth { get; init; } + + /// + /// Costs per hour in euro. + /// + public TimeSpan FreeTimePerSession { get; init; } + + /// + /// Max. costs per day in euro. + /// + public double MaxFeeEuroPerDay { get; init; } + } +} diff --git a/TINKLib/Model/Bikes/BikeCollection.cs b/TINKLib/Model/Bikes/BikeCollection.cs new file mode 100644 index 0000000..18486e3 --- /dev/null +++ b/TINKLib/Model/Bikes/BikeCollection.cs @@ -0,0 +1,54 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +using System.Linq; + +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; + +namespace TINK.Model.Bike +{ + public class BikeCollection : IBikeDictionary + { + /// Holds the bike dictionary object. + private Dictionary BikeDictionary { get; } + + /// Constructs an empty bike info dictionary object. + public BikeCollection() + { + BikeDictionary = new Dictionary(); + } + + /// Constructs a bike collection object. + /// + public BikeCollection(Dictionary bikeDictionary) + { + BikeDictionary = bikeDictionary ?? + throw new ArgumentNullException(nameof(bikeDictionary), "Can not construct BikeCollection object."); + } + + /// Gets a bike by its id. + /// Id of the bike to get. + /// + public BikeInfo GetById(int p_iId) + { + return BikeDictionary.FirstOrDefault(x => x.Key == p_iId).Value; + } + + /// Gets the count of bikes. + public int Count => BikeDictionary.Count; + + /// Gets if a bike with given id exists. + /// Id of bike. + /// True if bike is contained, false otherwise. + public bool ContainsKey(int p_iId) => BikeDictionary.Keys.Contains(p_iId); + + /// Gets the enumerator. + /// Enumerator object. + public IEnumerator GetEnumerator() => BikeDictionary.Values.GetEnumerator(); + + /// Gets the enumerator. + /// Enumerator object. + IEnumerator IEnumerable.GetEnumerator() => GetEnumerator(); + } +} diff --git a/TINKLib/Model/Bikes/BikeCollectionFilter.cs b/TINKLib/Model/Bikes/BikeCollectionFilter.cs new file mode 100644 index 0000000..7dc62f0 --- /dev/null +++ b/TINKLib/Model/Bikes/BikeCollectionFilter.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using System.Linq; +using TINK.Model.Bike; + +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; + +namespace TINK.Model +{ + public static class BikeCollectionFilter + { + /// Filters bikes by station. + /// Bikes available, requested and/ or occupied bikes to filter. + /// Id of station, might be null + /// BikeCollection holding bikes at given station or empty BikeCollection, if there are no bikes. + public static BikeCollection GetAtStation( + this BikeCollection bikesAtAnyStation, + int? selectedStation) + { + return new BikeCollection(bikesAtAnyStation? + .Where(bike => selectedStation.HasValue && bike.CurrentStation == selectedStation.Value) + .ToDictionary(x => x.Id) ?? new Dictionary()); + } + + /// Filters bikes by bike type. + /// Bikes available, requested and/ or occupied bikes to filter. + /// BikeCollection holding LockIt-bikes empty BikeCollection, if there are no LockIt-bikes. + public static BikeCollection GetLockIt(this BikeCollection bcAndLockItBikes) + { + return new BikeCollection(bcAndLockItBikes? + .Where(bike => bike is Bike.BluetoothLock.BikeInfo) + .ToDictionary(x => x.Id) ?? new Dictionary()); + } + } +} diff --git a/TINKLib/Model/Bikes/BikeCollectionMutable.cs b/TINKLib/Model/Bikes/BikeCollectionMutable.cs new file mode 100644 index 0000000..99f299a --- /dev/null +++ b/TINKLib/Model/Bikes/BikeCollectionMutable.cs @@ -0,0 +1,145 @@ +using System; +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; + +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; +using BikeInfoMutable = TINK.Model.Bike.BC.BikeInfoMutable; + +namespace TINK.Model.Bike +{ + /// Holds entity of bikes. + public class BikeCollectionMutable : ObservableCollection, IBikeDictionaryMutable + { + /// Constructs a mutable bike collection object. + public BikeCollectionMutable() + { + SelectedBike = null; + } + + /// + /// Updates bikes dictionary from bikes response, i.e. + /// - removes bikes which are no more contained in bikes response + /// - updates state of all bikes from state contained in bikes response + /// + /// Object holding bikes info from copri to update from. + /// Provices date time information. + public void Update( + IEnumerable bikesAll) + { + // Get list of current bikes by state(s) to update. + // Needed to remove bikes which switched state and have to be removed from collection. + var bikesToBeRemoved = this.Select(x => x.Id).ToList(); + + foreach (var bikeInfo in (bikesAll ?? new List())) + { + /// Check if bike has to be added to list of existing station. + if (ContainsKey(bikeInfo.Id) == false) + { + // Bike does not yet exist in list of bikes. + Add(BikeInfoMutableFactory.Create(bikeInfo)); + continue; + } + + // Update bike. + GetById(bikeInfo.Id).State.Load(bikeInfo.State); + + if (bikesToBeRemoved.Contains(bikeInfo.Id)) + { + // Remove list from obsolete list. + bikesToBeRemoved.Remove(bikeInfo.Id); + } + } + + // Remove obsolete bikes. + foreach (var l_oId in bikesToBeRemoved) + { + RemoveById(l_oId); + } + } + + /// + /// Adds a new bike to collecion of bike. + /// + /// New bike to add. + /// Thrown if bike is not unique. + public new void Add(BikeInfoMutable p_oNewBike) + { + // Ensure that bike id of new bike is is unique + foreach (BikeInfoMutable l_oBike in Items) + { + if (l_oBike.Id == p_oNewBike.Id) + { + throw new Exception(string.Format("Can not add bike with {0} to collection ob bike. Id is not unnique.", p_oNewBike)); + } + } + + base.Add(p_oNewBike); + } + + /// + /// Bike selected by user for regerving or cancel reservation. + /// + public BikeInfoMutable SelectedBike + { + get; + private set; + } + + public void SetSelectedBike(int p_intId) + { + SelectedBike = GetById(p_intId); + } + + /// + /// Gets a bike by its id. + /// + /// + /// + public BikeInfoMutable GetById(int p_iId) + { + { + return this.FirstOrDefault(bike => bike.Id == p_iId); + } + } + + /// + /// Deteermines whether a bike by given key exists. + /// + /// Key to check. + /// True if bike exists. + public bool ContainsKey(int p_iId) + { + return GetById(p_iId) != null; + } + + /// + /// Removes a bike by its id. + /// + /// Id of bike to be removed. + public void RemoveById(int p_iId) + { + var l_oBike = GetById(p_iId); + if (l_oBike == null) + { + // Nothing to do if bike does not exists. + return; + } + + Remove(l_oBike); + } + + /// + /// Create mutable objects from immutable objects. + /// + private static class BikeInfoMutableFactory + { + public static BikeInfoMutable Create(BikeInfo bikeInfo) + { + return (bikeInfo is BluetoothLock.BikeInfo bluetoothLockBikeInfo) + ? new BluetoothLock.BikeInfoMutable(bluetoothLockBikeInfo) + : new BikeInfoMutable(bikeInfo); + } + } + } +} diff --git a/TINKLib/Model/Bikes/BikeCollectionUpdater.cs b/TINKLib/Model/Bikes/BikeCollectionUpdater.cs new file mode 100644 index 0000000..edf7b4d --- /dev/null +++ b/TINKLib/Model/Bikes/BikeCollectionUpdater.cs @@ -0,0 +1,38 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model.Bike +{ + public static class BikeCollectionUpdater + { + /// Updates bikes lock info with the latest lock info from bluetooth service. + /// bikes to be updated. + /// locks info to be used for updating bikes. + /// + public static BikeCollection UpdateLockInfo( + this BikeCollection bikes, + IEnumerable locksInfo) + { + + var updatedBikesCollection = new Dictionary(); + + foreach (var bikeInfo in bikes) + { + if (!(bikeInfo is BluetoothLock.BikeInfo bluetoothBikeInfo)) + { + // No processing needed because bike is not a bluetooth bike + updatedBikesCollection.Add(bikeInfo.Id, bikeInfo); + continue; + } + + // Check if to update current bike lock info using state from bluetooth service or not. + var currentLockInfo = locksInfo.FirstOrDefault(x => x.Id == bluetoothBikeInfo.LockInfo.Id) // Update bike info with latest info from bluethooth service if available + ?? bluetoothBikeInfo.LockInfo; // Use lock info state object from copri which holds a lock id and a state of value unknown. + + updatedBikesCollection.Add(bluetoothBikeInfo.Id, new BluetoothLock.BikeInfo(bluetoothBikeInfo, currentLockInfo)); + } + + return new BikeCollection(updatedBikesCollection); + } + } +} diff --git a/TINKLib/Model/Bikes/IBikeCollection.cs b/TINKLib/Model/Bikes/IBikeCollection.cs new file mode 100644 index 0000000..3e2aa6d --- /dev/null +++ b/TINKLib/Model/Bikes/IBikeCollection.cs @@ -0,0 +1,35 @@ +using System.Collections.Generic; + +namespace TINK.Model.Bike +{ + public interface IBikeDictionary : IReadOnlyCollection + { + /// + /// Gets a bike by its id. + /// + /// + /// + T GetById(int p_iId); + + /// + /// Deteermines whether a bike by given key exists. + /// + /// Key to check. + /// True if bike exists. + bool ContainsKey(int p_iId); + } + public interface IBikeDictionaryMutable : IBikeDictionary + { + /// + /// Removes a bike by its id. + /// + /// Id of bike to be removed. + void RemoveById(int p_iId); + + /// + /// Adds a new element to dictinary. + /// + /// New element to add. + void Add(T p_oNewElement); + } +} diff --git a/TINKLib/Model/Connector/Command/Command.cs b/TINKLib/Model/Connector/Command/Command.cs new file mode 100644 index 0000000..b81ba6d --- /dev/null +++ b/TINKLib/Model/Connector/Command/Command.cs @@ -0,0 +1,141 @@ +using Serilog; +using System; +using System.Threading.Tasks; +using TINK.Model.Repository; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Model.User.Account; + +namespace TINK.Model.Connector +{ + public class Command : Base, ICommand + { + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => CopriServer.IsConnected; + + /// No user is logged in. + public string SessionCookie => null; + + /// Is raised whenever login state has changed. + public event LoginStateChangedEventHandler LoginStateChanged; + + /// Constructs a copri query object. + /// Server which implements communication. + public Command( + ICopriServerBase p_oCopriServer) : base(p_oCopriServer) + { + } + + /// + /// Logs user in. + /// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device). + /// If log in fails (password modified) session cookie is set to empty. + /// If communication fails an exception is thrown. + /// + public async Task DoLogin( + string p_strMail, + string p_strPassword, + string p_strDeviceId) + { + if (string.IsNullOrEmpty(p_strMail)) + { + throw new ArgumentNullException("Can not loging user. Mail address must not be null or empty."); + } + + if (string.IsNullOrEmpty(p_strPassword)) + { + throw new ArgumentNullException("Can not loging user. Password must not be null or empty."); + } + + if (string.IsNullOrEmpty(p_strDeviceId)) + { + throw new ArgumentNullException("Can not loging user. Device not be null or empty."); + } + + AuthorizationResponse l_oResponse; + try + { + l_oResponse = (await CopriServer.DoAuthorizationAsync(p_strMail, p_strPassword, p_strDeviceId)).GetIsResponseOk(p_strMail); + } + catch (System.Exception) + { + throw; + } + + var l_oAccount = l_oResponse.GetAccount(MerchantId, p_strMail, p_strPassword); + + // Log in state changes. Notify parent object to update. + LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs(l_oAccount.SessionCookie, l_oAccount.Mail)); + + return l_oAccount; + } + + /// Logs user out. + public async Task DoLogout() + { + Log.ForContext().Error("Unexpected log out request detected. No user logged in."); + await Task.CompletedTask; + } + + /// + /// Request to reserve a bike. + /// + /// Bike to book. + public async Task DoReserve( + Bikes.Bike.BC.IBikeInfoMutable p_oBike) + { + Log.ForContext().Error("Unexpected booking request detected. No user logged in."); + await Task.CompletedTask; + } + + /// Request to cancel a reservation. + /// Bike to book. + public async Task DoCancelReservation(Bikes.Bike.BC.IBikeInfoMutable bike) + { + Log.ForContext().Error("Unexpected cancel reservation request detected. No user logged in."); + await Task.CompletedTask; + } + + /// Get authentication keys. + /// Bike to book. + public async Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike) + { + Log.ForContext().Error("Unexpected request to get authenticatin keys detected. No user logged in."); + await Task.CompletedTask; + } + + /// Updates COPRI lock state for a booked bike. + /// Bike to update locking state for. + /// Location where lock was opened/ changed. + /// Response on updating locking state. + public async Task UpdateLockingStateAsync(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location) + { + Log.ForContext().Error("Unexpected request to update locking state detected. No user logged in."); + await Task.CompletedTask; + } + + public async Task DoBook(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike) + { + Log.ForContext().Error("Unexpected booking request detected. No user logged in."); + await Task.CompletedTask; + } + + public async Task DoReturn( + Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, + LocationDto location) + { + Log.ForContext().Error("Unexpected returning request detected. No user logged in."); + await Task.CompletedTask; + } + + /// + /// Submits feedback to copri server. + /// + /// Feedback to submit. + public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri) + { + Log.ForContext().Error("Unexpected submit feedback request detected. No user logged in."); + await Task.CompletedTask; + } + } +} \ No newline at end of file diff --git a/TINKLib/Model/Connector/Command/CommandLoggedIn.cs b/TINKLib/Model/Connector/Command/CommandLoggedIn.cs new file mode 100644 index 0000000..dc5bc95 --- /dev/null +++ b/TINKLib/Model/Connector/Command/CommandLoggedIn.cs @@ -0,0 +1,279 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Bike.BluetoothLock; +using TINK.Model.Repository; +using TINK.Model.Repository.Exception; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Model.User.Account; + +namespace TINK.Model.Connector +{ + public class CommandLoggedIn : BaseLoggedIn, ICommand + { + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => CopriServer.IsConnected; + + /// Is raised whenever login state has changed. + public event LoginStateChangedEventHandler LoginStateChanged; + + /// Constructs a copri query object. + /// Server which implements communication. + public CommandLoggedIn(ICopriServerBase p_oCopriServer, + string p_strSessionCookie, + string p_strMail, + Func p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider) + { + } + + /// + /// Logs user in. + /// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device). + /// If log in fails (password modified) session cookie is set to empty. + /// If communication fails an TINK.Model.Repository.Exception is thrown. + /// + /// Account to use for login. + public Task DoLogin(string p_strMail, string p_strPassword, string p_strDeviceId) + { + if (string.IsNullOrEmpty(p_strMail)) + { + throw new ArgumentNullException("Can not loging user. Mail address must not be null or empty."); + } + + throw new Exception($"Fehler beim Anmelden von unter {p_strMail}. Benutzer {Mail} ist bereits angemeldet."); + } + + /// Logs user out. + public async Task DoLogout() + { + AuthorizationoutResponse l_oResponse = null; + try + { + l_oResponse = (await CopriServer.DoAuthoutAsync()).GetIsResponseOk(); + } + catch (AuthcookieNotDefinedException) + { + // Cookie is no more defined, i.e. no need to logout user at copri because user is already logged out. + // Just ignore this error. + // User logged out, log in state changed. Notify parent object to update. + LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs()); + return; + } + catch (Exception) + { + throw; + } + + // User logged out, log in state changed. Notify parent object to update. + LoginStateChanged?.Invoke(this, new LoginStateChangedEventArgs()); + } + + /// + /// Request to reserve a bike. + /// + /// Bike to book. + public async Task DoReserve(Bikes.Bike.BC.IBikeInfoMutable bike) + { + if (bike == null) + { + throw new ArgumentNullException("Can not reserve bike. No bike object available."); + } + + BikeInfoReservedOrBooked response; + try + { + response = (await CopriServer.DoReserveAsync(bike.Id, bike.OperatorUri)).GetIsReserveResponseOk(bike.Id); + } + catch (Exception) + { + // Exception was not expected or too many subsequent excepitons detected. + throw; + } + + bike.Load(response, Mail, DateTimeProvider, Bikes.Bike.BC.NotifyPropertyChangedLevel.None); + } + + /// Request to cancel a reservation. + /// Bike to cancel reservation. + public async Task DoCancelReservation( + Bikes.Bike.BC.IBikeInfoMutable bike) + { + if (bike == null) + { + throw new ArgumentNullException("Can not cancel reservation of bike. No bike object available."); + } + + ReservationCancelReturnResponse response; + try + { + response = (await CopriServer.DoCancelReservationAsync(bike.Id, bike.OperatorUri)).GetIsCancelReservationResponseOk(bike.Id); + } + catch (Exception) + { + // Exception was not expected or too many subsequent excepitons detected. + throw; + } + + bike.Load(Bikes.Bike.BC.NotifyPropertyChangedLevel.None); + } + + /// Get authentication keys. + /// Bike to get new keys for. + public async Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike) + { + if (bike == null) + { + throw new ArgumentNullException("Can not calculate auth keys. No bike object available."); + } + + switch (bike.State.Value) + { + case State.InUseStateEnum.Reserved: + case State.InUseStateEnum.Booked: + break; + + default: + throw new ArgumentNullException($"Can not calculate auth keys. Unexpected bike state {bike.State.Value} detected."); + } + + BikeInfoReservedOrBooked response; + Guid guid = (bike is BikeInfoMutable btBike) ? btBike.LockInfo.Guid : new Guid(); + try + { + response = (await CopriServer.CalculateAuthKeysAsync(bike.Id, bike.OperatorUri)).GetIsBookingResponseOk(bike.Id); + } + catch (Exception) + { + // Exception was not expected or too many subsequent excepitons detected. + throw; + } + + UpdaterJSON.Load( + bike, + response, + Mail, + DateTimeProvider, + Bikes.Bike.BC.NotifyPropertyChangedLevel.None); + } + + /// Updates COPRI lock state for a booked bike. + /// Bike to update locking state for. + /// Response on updating locking state. + public async Task UpdateLockingStateAsync( + Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location) + { + if (bike == null) + { + throw new ArgumentNullException("Can not book bike. No bike object available."); + } + + if (bike.State.Value != State.InUseStateEnum.Booked) + { + throw new ArgumentNullException($"Can not update locking state of bike. Unexpected booking state {bike.State} detected."); + } + + lock_state? state = null; + switch (bike.LockInfo.State) + { + case LockingState.Open: + state = lock_state.unlocked; + break; + + case LockingState.Closed: + state = lock_state.locked; + break; + } + + if (!state.HasValue) + { + throw new ArgumentNullException($"Can not update locking state of bike. Unexpected locking state {bike.LockInfo.State} detected."); + } + + try + { + (await CopriServer.UpdateLockingStateAsync( + bike.Id, + location, + state.Value, + bike.LockInfo.BatteryPercentage, + bike.OperatorUri)).GetIsBookingResponseOk(bike.Id); + } + catch (Exception) + { + // Exception was not expected or too many subsequent excepitons detected. + throw; + } + } + + + /// Request to book a bike. + /// Bike to book. + public async Task DoBook( + Bikes.Bike.BluetoothLock.IBikeInfoMutable bike) + { + if (bike == null) + { + throw new ArgumentNullException(nameof(bike), "Can not book bike. No bike object available."); + } + + BikeInfoReservedOrBooked response; + var btBike = bike as BikeInfoMutable; + Guid guid = btBike != null ? btBike.LockInfo.Guid : new Guid(); + double batteryPercentage = btBike != null ? btBike.LockInfo.BatteryPercentage : double.NaN; + try + { + response = (await CopriServer.DoBookAsync( + bike.Id, + guid, + batteryPercentage, + bike.OperatorUri)).GetIsBookingResponseOk(bike.Id); + } + catch (Exception) + { + // Exception was not expected or too many subsequent excepitons detected. + throw; + } + + bike.Load( + response, + Mail, + DateTimeProvider, + Bikes.Bike.BC.NotifyPropertyChangedLevel.None); + } + + /// Request to return a bike. + /// Latitude of the bike. + /// Longitude of the bike. + /// Bike to return. + public async Task DoReturn( + Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, + LocationDto location) + { + if (bike == null) + { + throw new ArgumentNullException("Can not return bike. No bike object available."); + } + + ReservationCancelReturnResponse l_oResponse; + try + { + l_oResponse = (await CopriServer.DoReturn(bike.Id, location, bike.OperatorUri)).GetIsReturnBikeResponseOk(bike.Id); + } + catch (Exception) + { + // Exception was not expected or too many subsequent exceptions detected. + throw; + } + + bike.Load(Bikes.Bike.BC.NotifyPropertyChangedLevel.None); + } + + /// + /// Submits feedback to copri server. + /// + /// Feedback to submit. + public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri) + => await CopriServer.DoSubmitFeedback(userFeedback.Message, userFeedback.IsBikeBroken, opertorUri); + + } +} \ No newline at end of file diff --git a/TINKLib/Model/Connector/Command/ICommand.cs b/TINKLib/Model/Connector/Command/ICommand.cs new file mode 100644 index 0000000..b2f8be4 --- /dev/null +++ b/TINKLib/Model/Connector/Command/ICommand.cs @@ -0,0 +1,99 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Repository.Request; +using TINK.Model.User.Account; + +namespace TINK.Model.Connector +{ + public interface ICommand + { + /// Is raised whenever login state has changed. + event LoginStateChangedEventHandler LoginStateChanged; + + /// + /// Logs user in. + /// If log in succeeds either and session might be updated if it was no more valid (logged in by an different device). + /// If log in fails (password modified) session cookie is set to empty. + /// If communication fails an exception is thrown. + /// + Task DoLogin(string p_strMail, string p_strPassword, string p_strDeviceId); + + /// Logs user out. + Task DoLogout(); + + /// Request to reserve a bike. + /// Bike to book. + Task DoReserve(Bikes.Bike.BC.IBikeInfoMutable p_oBike); + + /// Request to cancel a reservation. + /// Bike to book. + Task DoCancelReservation(Bikes.Bike.BC.IBikeInfoMutable p_oBike); + + /// Get authentication keys to connect to lock. + /// Bike to book. + Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike); + + /// Updates COPRI lock state for a booked bike. + /// Id of the bike to update locking state for. + /// Geolocation of lock when returning bike. + /// Response on updating locking state. + Task UpdateLockingStateAsync(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location = null); + + /// Request to book a bike. + /// Bike to book. + Task DoBook(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike); + + /// Request to return a bike. + /// Geolocation of lock when returning bike. + /// Bike to return. + Task DoReturn(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto geolocation = null); + + /// True if connector has access to copri server, false if cached values are used. + bool IsConnected { get; } + + /// True if user is logged in false if not. + string SessionCookie { get; } + + Task DoSubmitFeedback(IUserFeedback userFeedback, Uri opertorUri); + + /// + /// Feedback given by user when returning bike. + /// + public interface IUserFeedback + { + /// + /// Holds whether bike is broken or not. + /// + bool IsBikeBroken { get; } + + /// + /// Holds either + /// - general feedback + /// - error description of broken bike + /// or both. + /// + string Message { get; } + } + } + + /// Defines delegate to be raised whenever login state changes. + /// Holds session cookie and mail address if user logged in successfully. + public delegate void LoginStateChangedEventHandler(object p_oSender, LoginStateChangedEventArgs p_oEventArgs); + + /// Event arguments to notify about changes of logged in state. + public class LoginStateChangedEventArgs : EventArgs + { + public LoginStateChangedEventArgs() : this(string.Empty, string.Empty) + { } + + public LoginStateChangedEventArgs(string p_strSessionCookie, string p_strMail) + { + SessionCookie = p_strSessionCookie; + Mail = p_strMail; + } + + public string SessionCookie { get; } + + public string Mail { get; } + } +} diff --git a/TINKLib/Model/Connector/Command/UserFeedbackDto.cs b/TINKLib/Model/Connector/Command/UserFeedbackDto.cs new file mode 100644 index 0000000..b9c15ff --- /dev/null +++ b/TINKLib/Model/Connector/Command/UserFeedbackDto.cs @@ -0,0 +1,9 @@ + +namespace TINK.Model.Connector +{ + public record UserFeedbackDto : ICommand.IUserFeedback + { + public bool IsBikeBroken { get; init; } + public string Message { get; init; } + } +} diff --git a/TINKLib/Model/Connector/Connector.cs b/TINKLib/Model/Connector/Connector.cs new file mode 100644 index 0000000..f2c6b5a --- /dev/null +++ b/TINKLib/Model/Connector/Connector.cs @@ -0,0 +1,57 @@ +using System; +using TINK.Model.Services.CopriApi; +using TINK.Model.Repository; + +namespace TINK.Model.Connector +{ + /// + /// Connects tink app to copri by getting data from copri and updating tink app model (i.e. bikes, user, ...) + /// + public class Connector : IConnector + { + /// Constructs a copri connector object. + /// Uri to connect to. + /// Holds the name and version of the TINKApp. + /// /// Holds the session cookie. + /// Mail of user. + /// Timespan which holds value after which cache expires. + /// Provides cached addess to copri. + public Connector( + Uri activeUri, + string userAgent, + string sessionCookie, + string mail, + TimeSpan? expiresAfter = null, + ICachedCopriServer server = null ) + { + Command = GetCommand( + server ?? new CopriProviderHttps(activeUri, TinkApp.MerchantId, userAgent, sessionCookie), + sessionCookie, + mail); + + Query = GetQuery( + server ?? new CopriProviderHttps(activeUri, TinkApp.MerchantId, userAgent, sessionCookie, expiresAfter), + sessionCookie, + mail); + } + + /// Object for queriying stations and bikes. + public ICommand Command { get; private set; } + + /// Object for queriying stations and bikes. + public IQuery Query { get; private set; } + + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => Command.IsConnected; + + /// Gets a command object to perform copri commands. + public static ICommand GetCommand(ICopriServerBase copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie) + ? new Command(copri) + : new CommandLoggedIn(copri, sessioncookie, mail, () => DateTime.Now) as ICommand; + + /// Gets a command object to perform copri queries. + private static IQuery GetQuery(ICachedCopriServer copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie) + ? new CachedQuery(copri) as IQuery + : new CachedQueryLoggedIn(copri, sessioncookie, mail, () => DateTime.Now); + } +} diff --git a/TINKLib/Model/Connector/ConnectorCache.cs b/TINKLib/Model/Connector/ConnectorCache.cs new file mode 100644 index 0000000..48b03a7 --- /dev/null +++ b/TINKLib/Model/Connector/ConnectorCache.cs @@ -0,0 +1,47 @@ +using System; +using TINK.Model.Services.CopriApi; +using TINK.Model.Repository; + +namespace TINK.Model.Connector +{ + /// + /// Connects tink app to copri by getting data from copri and updating tink app model (i.e. bikes, user, ...) + /// + public class ConnectorCache : IConnector + { + /// Constructs a copri connector object. + /// Holds the session cookie. + /// Mail of user. + /// Provides addess to copri. + public ConnectorCache( + string sessionCookie, + string mail, + ICopriServer server = null) + { + + Command = Connector.GetCommand( + server ?? new CopriProviderMonkeyStore(TinkApp.MerchantId, sessionCookie), + sessionCookie, + mail); + + Query = GetQuery( + server ?? new CopriProviderMonkeyStore(TinkApp.MerchantId, sessionCookie), + sessionCookie, + mail); + } + + /// Object for queriying stations and bikes. + public ICommand Command { get; private set; } + + /// Object for queriying stations and bikes. + public IQuery Query { get; private set; } + + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => Command.IsConnected; + + /// Gets a command object to perform copri queries. + private static IQuery GetQuery(ICopriServer copri, string sessioncookie, string mail) => string.IsNullOrEmpty(sessioncookie) + ? new Query(copri) as IQuery + : new QueryLoggedIn(copri, sessioncookie, mail, () => DateTime.Now); + } +} diff --git a/TINKLib/Model/Connector/ConnectorFactory.cs b/TINKLib/Model/Connector/ConnectorFactory.cs new file mode 100644 index 0000000..2526580 --- /dev/null +++ b/TINKLib/Model/Connector/ConnectorFactory.cs @@ -0,0 +1,19 @@ +using System; + +namespace TINK.Model.Connector +{ + public class ConnectorFactory + { + /// + /// Gets a connector object depending on whether beein onlin or offline. + /// + /// True if online, false if offline + /// + public static IConnector Create(bool isConnected, Uri activeUri, string userAgent, string sessionCookie, string mail, TimeSpan? expiresAfter = null) + { + return isConnected + ? new Connector(activeUri, userAgent, sessionCookie, mail, expiresAfter: expiresAfter) as IConnector + : new ConnectorCache(sessionCookie, mail); + } + } +} diff --git a/TINKLib/Model/Connector/Filter/GroupFilterFactory.cs b/TINKLib/Model/Connector/Filter/GroupFilterFactory.cs new file mode 100644 index 0000000..12362c8 --- /dev/null +++ b/TINKLib/Model/Connector/Filter/GroupFilterFactory.cs @@ -0,0 +1,13 @@ +using System.Collections; +using System.Collections.Generic; + +namespace TINK.Model.Connector.Filter +{ + public static class GroupFilterFactory + { + public static IGroupFilter Create(IEnumerable group) + { + return group != null ? (IGroupFilter) new IntersectGroupFilter(group) : new NullGroupFilter(); + } + } +} diff --git a/TINKLib/Model/Connector/Filter/IGroupFilter.cs b/TINKLib/Model/Connector/Filter/IGroupFilter.cs new file mode 100644 index 0000000..f32e4ca --- /dev/null +++ b/TINKLib/Model/Connector/Filter/IGroupFilter.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace TINK.Model.Connector.Filter +{ + public interface IGroupFilter + { + IEnumerable DoFilter(IEnumerable filter); + } +} diff --git a/TINKLib/Model/Connector/Filter/IntersectGroupFilter.cs b/TINKLib/Model/Connector/Filter/IntersectGroupFilter.cs new file mode 100644 index 0000000..6500fb9 --- /dev/null +++ b/TINKLib/Model/Connector/Filter/IntersectGroupFilter.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model.Connector.Filter +{ + /// Filters to enumerations of string by intersecting. + public class IntersectGroupFilter : IGroupFilter + { + private IEnumerable Group { get; set; } + + public IntersectGroupFilter(IEnumerable group) => Group = group ?? new List(); + + /// Applies filtering. + /// Enumeration of filter values to filter with or null if no filtering has to be applied. + /// + public IEnumerable DoFilter(IEnumerable filter) => filter != null + ? Group.Intersect(filter) + : Group; + } +} diff --git a/TINKLib/Model/Connector/Filter/NullGroupFilter.cs b/TINKLib/Model/Connector/Filter/NullGroupFilter.cs new file mode 100644 index 0000000..e7ddb36 --- /dev/null +++ b/TINKLib/Model/Connector/Filter/NullGroupFilter.cs @@ -0,0 +1,9 @@ +using System.Collections.Generic; + +namespace TINK.Model.Connector.Filter +{ + public class NullGroupFilter : IGroupFilter + { + public IEnumerable DoFilter(IEnumerable filter) => filter; + } +} diff --git a/TINKLib/Model/Connector/FilterHelper.cs b/TINKLib/Model/Connector/FilterHelper.cs new file mode 100644 index 0000000..58a2565 --- /dev/null +++ b/TINKLib/Model/Connector/FilterHelper.cs @@ -0,0 +1,11 @@ +namespace TINK.Model.Connector +{ + public static class FilterHelper + { + /// Holds the Konrad group (city bikes). + public const string FILTERKONRAD = "Konrad"; + + /// Holds the tink group (Lastenräder). + public const string FILTERTINKGENERAL = "TINK"; + } +} diff --git a/TINKLib/Model/Connector/FilteredConnector.cs b/TINKLib/Model/Connector/FilteredConnector.cs new file mode 100644 index 0000000..cbecfbf --- /dev/null +++ b/TINKLib/Model/Connector/FilteredConnector.cs @@ -0,0 +1,115 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Connector.Filter; +using TINK.Model.Services.CopriApi; +using TINK.Model.Station; + +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; + +namespace TINK.Model.Connector +{ + /// Filters connector respones. + /// Former name: Filter + public class FilteredConnector : IFilteredConnector + { + /// Constructs a filter object. + /// Filter group. + /// Connector object. + public FilteredConnector( + IEnumerable group, + IConnector connector) + { + Connector = connector; + + if (Connector == null) + { + throw new ArgumentException("Can not construct filter object. Connector- and command objects must not be null."); + } + + Query = new QueryProvider(Connector.Query, GroupFilterFactory.Create(group)); + } + + /// Inner connector object. + public IConnector Connector { get; } + + /// Command object. + public ICommand Command => Connector.Command; + + /// Object to query information. + public IQuery Query { get; } + + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => Connector.IsConnected; + + /// Object to perform filtered queries. + private class QueryProvider : IQuery + { + /// Holds the filter. + private IGroupFilter Filter { get; } + + /// Holds the reference to object which performs copry queries. + private IQuery m_oInnerQuery; + + /// Constructs a query object. + /// + /// + public QueryProvider(IQuery innerQuerry, IGroupFilter filter) + { + m_oInnerQuery = innerQuerry; + Filter = filter; + } + + /// Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. + public async Task> GetBikesAsync() + { + var result = await m_oInnerQuery.GetBikesAsync(); + return new Result( + result.Source, + new BikeCollection(DoFilter(result.Response, Filter)), + result.Exception); + } + + /// Gets bikes occupied if a user is logged in. + public async Task> GetBikesOccupiedAsync() + { + var result = await m_oInnerQuery.GetBikesOccupiedAsync(); + return new Result( + result.Source, + new BikeCollection(result.Response.ToDictionary(x => x.Id)), + result.Exception); + } + + /// Gets all station applying filter rules. + /// + public async Task> GetBikesAndStationsAsync() + { + var result = await m_oInnerQuery.GetBikesAndStationsAsync(); + + return new Result( + result.Source, + new StationsAndBikesContainer( + new StationDictionary(result.Response.StationsAll.CopriVersion, DoFilter(result.Response.StationsAll, Filter)), + new BikeCollection(DoFilter(result.Response.Bikes, Filter))), + result.Exception); + } + + /// Filter bikes by group. + /// Bikes to filter. + /// Filtered bikes. + private static Dictionary DoFilter(BikeCollection p_oBikes, IGroupFilter filter) + { + return p_oBikes.Where(x => filter.DoFilter(x.Group).Count() > 0).ToDictionary(x => x.Id); + } + + /// Filter stations by broup. + /// + private static Dictionary DoFilter(StationDictionary p_oStations, IGroupFilter filter) + { + return p_oStations.Where(x => filter.DoFilter(x.Group).Count() > 0).ToDictionary((x => x.Id)); + } + } + } +} diff --git a/TINKLib/Model/Connector/FilteredConnectorFactory.cs b/TINKLib/Model/Connector/FilteredConnectorFactory.cs new file mode 100644 index 0000000..8005f01 --- /dev/null +++ b/TINKLib/Model/Connector/FilteredConnectorFactory.cs @@ -0,0 +1,16 @@ +using System.Collections.Generic; + +namespace TINK.Model.Connector +{ + public static class FilteredConnectorFactory + { + /// Creates a filter object. + /// + public static IFilteredConnector Create(IEnumerable group, IConnector connector) + { + return group != null + ? (IFilteredConnector) new FilteredConnector(group, connector) + : new NullFilterConnector(connector); + } + } +} diff --git a/TINKLib/Model/Connector/IConnector.cs b/TINKLib/Model/Connector/IConnector.cs new file mode 100644 index 0000000..7cebd34 --- /dev/null +++ b/TINKLib/Model/Connector/IConnector.cs @@ -0,0 +1,14 @@ +namespace TINK.Model.Connector +{ + public interface IConnector + { + /// Object for queriying stations and bikes. + ICommand Command { get; } + + /// Object for queriying stations and bikes. + IQuery Query { get; } + + /// True if connector has access to copri server, false if cached values are used. + bool IsConnected { get; } + } +} diff --git a/TINKLib/Model/Connector/IFilteredConnector.cs b/TINKLib/Model/Connector/IFilteredConnector.cs new file mode 100644 index 0000000..d58149d --- /dev/null +++ b/TINKLib/Model/Connector/IFilteredConnector.cs @@ -0,0 +1,7 @@ +namespace TINK.Model.Connector +{ + public interface IFilteredConnector : IConnector + { + IConnector Connector { get; } + } +} diff --git a/TINKLib/Model/Connector/NullFilterConnector.cs b/TINKLib/Model/Connector/NullFilterConnector.cs new file mode 100644 index 0000000..9cce17f --- /dev/null +++ b/TINKLib/Model/Connector/NullFilterConnector.cs @@ -0,0 +1,108 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Services.CopriApi; +using TINK.Model.Station; + +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; + +namespace TINK.Model.Connector +{ + /// Filters connector respones. + public class NullFilterConnector : IFilteredConnector + { + /// Constructs a filter object. + /// Filter group. + /// Connector object. + public NullFilterConnector( + IConnector connector) + { + Connector = connector; + + if (Connector == null) + { + throw new ArgumentException("Can not construct filter object. Connector- and command objects must not be null."); + } + + Query = new QueryProvider(Connector.Query); + } + + /// Inner connector object. + public IConnector Connector { get; } + + /// Command object. + public ICommand Command => Connector.Command; + + /// Object to query information. + public IQuery Query { get; } + + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => Connector.IsConnected; + + /// Object to perform filtered queries. + private class QueryProvider : IQuery + { + /// Holds the reference to object which performs copry queries. + private IQuery m_oInnerQuery; + + /// Constructs a query object. + /// + /// + public QueryProvider(IQuery p_oInnerQuery) + { + m_oInnerQuery = p_oInnerQuery; + } + + /// Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. + public async Task> GetBikesAsync() + { + var result = await m_oInnerQuery.GetBikesAsync(); + return new Result( + result.Source, + new BikeCollection(result.Response.ToDictionary(x => x.Id)), + result.Exception); + } + + /// Gets bikes occupied if a user is logged in. + public async Task> GetBikesOccupiedAsync() + { + var result = await m_oInnerQuery.GetBikesOccupiedAsync(); + return new Result( + result.Source, + new BikeCollection(result.Response.ToDictionary(x => x.Id)), + result.Exception); + } + + /// Gets all station applying filter rules. + /// + public async Task> GetBikesAndStationsAsync() + { + var result = await m_oInnerQuery.GetBikesAndStationsAsync(); + + return new Result( + result.Source, + new StationsAndBikesContainer( + new StationDictionary(result.Response.StationsAll.CopriVersion, result.Response.StationsAll.ToDictionary(x => x.Id)), + new BikeCollection(result.Response.Bikes.ToDictionary(x => x.Id))), + result.Exception); + } + + /// Filter bikes by group. + /// Bikes to filter. + /// Filtered bikes. + public static Dictionary DoFilter(BikeCollection p_oBikes, IEnumerable p_oFilter) + { + return p_oBikes.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary(x => x.Id); + } + + /// Filter stations by broup. + /// + public static Dictionary DoFilter(StationDictionary p_oStations, IEnumerable p_oFilter) + { + return p_oStations.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary((x => x.Id)); + } + } + } +} diff --git a/TINKLib/Model/Connector/Query/Base.cs b/TINKLib/Model/Connector/Query/Base.cs new file mode 100644 index 0000000..62daab0 --- /dev/null +++ b/TINKLib/Model/Connector/Query/Base.cs @@ -0,0 +1,27 @@ +using System; +using TINK.Model.Repository; + +namespace TINK.Model.Connector +{ + /// + /// Provides information required for copri commands/ query operations. + /// + public class Base + { + /// Reference to object which provides access to copri server. + protected ICopriServerBase CopriServer { get; } + + /// Gets the merchant id. + protected string MerchantId => CopriServer.MerchantId; + + /// Constructs a query base object. + /// Server which implements communication. + /// Object which hold communication objects. + protected Base( + ICopriServerBase p_oCopriServer) + { + CopriServer = p_oCopriServer + ?? throw new ArgumentException("Can not instantiate command/ query base- object. Copri server object must never be null or emtpy."); + } + } +} diff --git a/TINKLib/Model/Connector/Query/BaseLoggedIn.cs b/TINKLib/Model/Connector/Query/BaseLoggedIn.cs new file mode 100644 index 0000000..604b138 --- /dev/null +++ b/TINKLib/Model/Connector/Query/BaseLoggedIn.cs @@ -0,0 +1,39 @@ +using System; +using TINK.Model.Repository; + +namespace TINK.Model.Connector +{ + /// Holds user infromation required for copri related commands/ query operations. + public class BaseLoggedIn : Base + { + /// Session cookie used to sign in to copri. + public string SessionCookie { get; } + + /// Mail address of the user. + protected string Mail { get; } + + /// Object which provides date time info. + protected readonly Func DateTimeProvider; + + /// Constructs a copri query object. + /// Server which implements communication. + public BaseLoggedIn(ICopriServerBase p_oCopriServer, + string p_strSessionCookie, + string p_strMail, + Func p_oDateTimeProvider) : base(p_oCopriServer) + { + if (string.IsNullOrEmpty(p_strSessionCookie)) + throw new ArgumentException("Can not instantiate query object- object. Session cookie must never be null or emtpy."); + + if (string.IsNullOrEmpty(p_strMail)) + throw new ArgumentException("Can not instantiate query object- object. Mail address must never be null or emtpy."); + + DateTimeProvider = p_oDateTimeProvider + ?? throw new ArgumentException("Can not instantiate connector- object. No date time provider object available."); + + SessionCookie = p_strSessionCookie; + + Mail = p_strMail; + } + } +} diff --git a/TINKLib/Model/Connector/Query/CachedQuery.cs b/TINKLib/Model/Connector/Query/CachedQuery.cs new file mode 100644 index 0000000..aab461b --- /dev/null +++ b/TINKLib/Model/Connector/Query/CachedQuery.cs @@ -0,0 +1,86 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Services.CopriApi; +using TINK.Model.Repository; +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; + +namespace TINK.Model.Connector +{ + public class CachedQuery : Base, IQuery + { + /// Cached copri server. + private readonly ICachedCopriServer server; + + /// Constructs a copri query object. + /// Server which implements communication. + public CachedQuery( + ICopriServerBase p_oCopriServer) : base(p_oCopriServer) + { + server = p_oCopriServer as ICachedCopriServer; + if (server == null) + { + throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}."); + } + } + + /// Gets all stations including postions and bikes. + public async Task> GetBikesAndStationsAsync() + { + var resultStations = await server.GetStations(); + + if (resultStations.Source == typeof(CopriCallsMonkeyStore)) + { + // Communication with copri in order to get stations failed. + return new Result( + resultStations.Source, + new StationsAndBikesContainer( + resultStations.Response.GetStationsAllMutable(), + (await server.GetBikesAvailable(true)).Response.GetBikesAvailable()), + resultStations.Exception); + } + + var resultBikes = await server.GetBikesAvailable(); + if (resultBikes.Source == typeof(CopriCallsMonkeyStore)) + { + // Communication with copri in order to get bikes failed. + return new Result( + resultBikes.Source, + new StationsAndBikesContainer( + (await server.GetStations(true)).Response.GetStationsAllMutable(), + resultBikes.Response.GetBikesAvailable()), + resultBikes.Exception); + } + + // Communicatin with copri succeeded. + server.AddToCache(resultStations); + server.AddToCache(resultBikes); + + return new Result( + resultStations.Source, + new StationsAndBikesContainer(resultStations.Response.GetStationsAllMutable(), resultBikes.Response.GetBikesAvailable())); + } + + /// Gets bikes occupied. + /// Collection of bikes. + public async Task> GetBikesOccupiedAsync() + { + Log.ForContext().Error("Unexpected call to get be bikes occpied detected. No user is logged in."); + return new Result( + typeof(CopriCallsMonkeyStore), + await Task.Run(() => new BikeCollection(new Dictionary())), + new System.Exception("Abfrage der reservierten/ gebuchten Räder nicht möglich. Kein Benutzer angemeldet.")); + } + + /// Gets bikes available. + /// Collection of bikes. + public async Task> GetBikesAsync() + { + var result = await server.GetBikesAvailable(); + server.AddToCache(result); + return new Result(result.Source, result.Response.GetBikesAvailable(), result.Exception); + } + } +} diff --git a/TINKLib/Model/Connector/Query/CachedQueryLoggedIn.cs b/TINKLib/Model/Connector/Query/CachedQueryLoggedIn.cs new file mode 100644 index 0000000..7f03d6b --- /dev/null +++ b/TINKLib/Model/Connector/Query/CachedQueryLoggedIn.cs @@ -0,0 +1,159 @@ +using MonkeyCache.FileStore; +using System; +using System.Linq; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Services.CopriApi; +using TINK.Model.Repository; + +namespace TINK.Model.Connector +{ + /// Provides query functionality for a logged in user. + public class CachedQueryLoggedIn : BaseLoggedIn, IQuery + { + /// Cached copri server. + private readonly ICachedCopriServer server; + + /// Constructs a copri query object. + /// Server which implements communication. + public CachedQueryLoggedIn(ICopriServerBase p_oCopriServer, + string p_strSessionCookie, + string p_strMail, + Func p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider) + { + server = p_oCopriServer as ICachedCopriServer; + if (server == null) + { + throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}."); + } + } + + /// Gets all stations including postions. + public async Task> GetBikesAndStationsAsync() + { + var resultStations = await server.GetStations(); + + if (resultStations.Source == typeof(CopriCallsMonkeyStore) + || resultStations.Exception != null) + { + // Stations were read from cache ==> get bikes availbalbe and occupied from cache as well to avoid inconsistencies + return new Result( + resultStations.Source, + new StationsAndBikesContainer( + resultStations.Response.GetStationsAllMutable(), + UpdaterJSON.GetBikesAll( + (await server.GetBikesAvailable(true)).Response, + (await server.GetBikesOccupied(true)).Response, + Mail, + DateTimeProvider)), + resultStations.Exception); + } + + var l_oBikesAvailableResponse = await server.GetBikesAvailable(); + if (l_oBikesAvailableResponse.Source == typeof(CopriCallsMonkeyStore) + || l_oBikesAvailableResponse.Exception != null) + { + // Bikes avilable were read from cache ==> get bikes occupied from cache as well to avoid inconsistencies + return new Result( + l_oBikesAvailableResponse.Source, + new StationsAndBikesContainer( + (await server.GetStations(true)).Response.GetStationsAllMutable(), + UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse.Response, + (await server.GetBikesOccupied(true)).Response, + Mail, + DateTimeProvider)), + l_oBikesAvailableResponse.Exception); + } + + var l_oBikesOccupiedResponse = await server.GetBikesOccupied(); + if (l_oBikesOccupiedResponse.Source == typeof(CopriCallsMonkeyStore) + || l_oBikesOccupiedResponse.Exception != null) + { + // Bikes occupied were read from cache ==> get bikes available from cache as well to avoid inconsistencies + return new Result( + l_oBikesOccupiedResponse.Source, + new StationsAndBikesContainer( + (await server.GetStations(true)).Response.GetStationsAllMutable(), + UpdaterJSON.GetBikesAll( + (await server.GetBikesAvailable(true)).Response, + l_oBikesOccupiedResponse.Response, + Mail, + DateTimeProvider)), + l_oBikesOccupiedResponse.Exception); + } + + // Both types bikes could read from copri => update cache + server.AddToCache(resultStations); + server.AddToCache(l_oBikesAvailableResponse); + server.AddToCache(l_oBikesOccupiedResponse); + + var exceptions = new[] { resultStations?.Exception, l_oBikesAvailableResponse?.Exception, l_oBikesOccupiedResponse?.Exception }.Where(x => x != null).ToArray(); + + return new Result( + resultStations.Source, + new StationsAndBikesContainer( + resultStations.Response.GetStationsAllMutable(), + UpdaterJSON.GetBikesAll( + l_oBikesAvailableResponse.Response, + l_oBikesOccupiedResponse.Response, + Mail, + DateTimeProvider)), + exceptions.Length > 0 ? new AggregateException(exceptions) : null); + } + + /// Gets bikes occupied. + /// Collection of bikes. + public async Task> GetBikesOccupiedAsync() + { + var result = await server.GetBikesOccupied(); + server.AddToCache(result); + return new Result(result.Source, result.Response.GetBikesOccupied(Mail, DateTimeProvider), result.Exception); + } + + /// Gets bikes available and bikes occupied. + /// Collection of bikes. + public async Task> GetBikesAsync() + { + var l_oBikesAvailableResponse = await server.GetBikesAvailable(); + + if (l_oBikesAvailableResponse.Source == typeof(CopriCallsMonkeyStore) + || l_oBikesAvailableResponse.Exception != null) + { + // Bikes avilable were read from cache ==> get bikes occupied from cache as well to avoid inconsistencies + return new Result( + l_oBikesAvailableResponse.Source, + UpdaterJSON.GetBikesAll( + l_oBikesAvailableResponse.Response, + (await server.GetBikesOccupied(true)).Response, + Mail, + DateTimeProvider), + l_oBikesAvailableResponse.Exception); + } + + var l_oBikesOccupiedResponse = await server.GetBikesOccupied(); + if (l_oBikesOccupiedResponse.Source == typeof(CopriCallsMonkeyStore) + || l_oBikesOccupiedResponse.Exception != null) + { + // Bikes occupied were read from cache ==> get bikes available from cache as well to avoid inconsistencies + return new Result( + l_oBikesOccupiedResponse.Source, + UpdaterJSON.GetBikesAll( + (await server.GetBikesAvailable(true)).Response, + l_oBikesOccupiedResponse.Response, + Mail, + DateTimeProvider), + l_oBikesOccupiedResponse.Exception); + + } + + // Both types bikes could read from copri => update cache + server.AddToCache(l_oBikesAvailableResponse); + server.AddToCache(l_oBikesOccupiedResponse); + + return new Result( + l_oBikesAvailableResponse.Source, + UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse.Response, l_oBikesOccupiedResponse.Response, Mail, DateTimeProvider), + l_oBikesAvailableResponse.Exception != null || l_oBikesOccupiedResponse.Exception != null ? new AggregateException(new[] { l_oBikesAvailableResponse.Exception, l_oBikesOccupiedResponse.Exception }) : null); + } + } +} diff --git a/TINKLib/Model/Connector/Query/IQuery.cs b/TINKLib/Model/Connector/Query/IQuery.cs new file mode 100644 index 0000000..4aed98b --- /dev/null +++ b/TINKLib/Model/Connector/Query/IQuery.cs @@ -0,0 +1,20 @@ +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Services.CopriApi; + +namespace TINK.Model.Connector +{ + public interface IQuery + { + /// Gets all stations including postions. + Task> GetBikesAndStationsAsync(); + + /// Gets bikes occupied is a user is logged in. + /// Collection of bikes. + Task> GetBikesOccupiedAsync(); + + /// Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. + /// Collection of bikes. + Task> GetBikesAsync(); + } +} diff --git a/TINKLib/Model/Connector/Query/Query.cs b/TINKLib/Model/Connector/Query/Query.cs new file mode 100644 index 0000000..c645cfd --- /dev/null +++ b/TINKLib/Model/Connector/Query/Query.cs @@ -0,0 +1,60 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Services.CopriApi; +using TINK.Model.Repository; +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; + +namespace TINK.Model.Connector +{ + /// Provides query functionality without login. + public class Query : Base, IQuery + { + /// Cached copri server. + private readonly ICopriServer server; + + /// Constructs a copri query object. + /// Server which implements communication. + public Query(ICopriServerBase p_oCopriServer) : base(p_oCopriServer) + { + server = p_oCopriServer as ICopriServer; + if (server == null) + { + throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}."); + } + } + + /// Gets all stations including postions. + public async Task> GetBikesAndStationsAsync() + { + var stationsAllResponse = await server.GetStationsAsync(); + var bikesAvailableResponse = await server.GetBikesAvailableAsync(); + + return new Result( + typeof(CopriCallsMonkeyStore), + new StationsAndBikesContainer( stationsAllResponse.GetStationsAllMutable(), bikesAvailableResponse.GetBikesAvailable())); + } + + /// Gets bikes occupied. + /// Collection of bikes. + public async Task> GetBikesOccupiedAsync() + { + Log.ForContext().Error("Unexpected call to get be bikes occpied detected. No user is logged in."); + return new Result( + typeof(CopriCallsMonkeyStore), + await Task.Run(() => new BikeCollection(new Dictionary())), + new System.Exception("Abfrage der reservierten/ gebuchten Räder fehlgeschlagen. Kein Benutzer angemeldet.")); + } + + /// Gets bikes occupied. + /// Collection of bikes. + public async Task> GetBikesAsync() + { + return new Result( + typeof(CopriCallsMonkeyStore), + (await server.GetBikesAvailableAsync()).GetBikesAvailable()); + } + } +} diff --git a/TINKLib/Model/Connector/Query/QueryLoggedIn.cs b/TINKLib/Model/Connector/Query/QueryLoggedIn.cs new file mode 100644 index 0000000..a8d6b39 --- /dev/null +++ b/TINKLib/Model/Connector/Query/QueryLoggedIn.cs @@ -0,0 +1,65 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Services.CopriApi; +using TINK.Model.Repository; + +namespace TINK.Model.Connector +{ + /// Provides query functionality for a logged in user. + public class QueryLoggedIn : BaseLoggedIn, IQuery + { + /// Cached copri server. + private readonly ICopriServer server; + + /// Constructs a copri query object. + /// Server which implements communication. + public QueryLoggedIn(ICopriServerBase p_oCopriServer, + string p_strSessionCookie, + string p_strMail, + Func p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider) + { + server = p_oCopriServer as ICopriServer; + if (server == null) + { + throw new ArgumentException($"Copri server is not of expected typ. Type detected is {p_oCopriServer.GetType()}."); + } + + server = p_oCopriServer as ICopriServer; + } + + /// Gets all stations including postions. + public async Task> GetBikesAndStationsAsync() + { + var stationResponse = await server.GetStationsAsync(); + var bikesAvailableResponse = await server.GetBikesAvailableAsync(); + var bikesOccupiedResponse = await server.GetBikesOccupiedAsync(); + + return new Result( + typeof(CopriCallsMonkeyStore), + new StationsAndBikesContainer( + stationResponse.GetStationsAllMutable(), + UpdaterJSON.GetBikesAll(bikesAvailableResponse, bikesOccupiedResponse, Mail, DateTimeProvider))); + } + + /// Gets bikes occupied. + /// Collection of bikes. + public async Task> GetBikesOccupiedAsync() + { + return new Result( + typeof(CopriCallsMonkeyStore), + (await server.GetBikesOccupiedAsync()).GetBikesOccupied(Mail, DateTimeProvider)); + } + /// Gets bikes available and bikes occupied. + /// Collection of bikes. + public async Task> GetBikesAsync() + { + var l_oBikesAvailableResponse = await server.GetBikesAvailableAsync(); + var l_oBikesOccupiedResponse = await server.GetBikesOccupiedAsync(); + + return new Result( + typeof(CopriCallsMonkeyStore), + UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse, l_oBikesOccupiedResponse, Mail, DateTimeProvider)); + } + } +} diff --git a/TINKLib/Model/Connector/TextToTypeHelper.cs b/TINKLib/Model/Connector/TextToTypeHelper.cs new file mode 100644 index 0000000..99468d2 --- /dev/null +++ b/TINKLib/Model/Connector/TextToTypeHelper.cs @@ -0,0 +1,358 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.Globalization; +using System.Linq; +using System.Text.RegularExpressions; +using TINK.Model.Bike; +using TINK.Model.Repository.Exception; +using TINK.Model.Repository.Response; +using TINK.Model.Services.CopriApi.ServerUris; +using TINK.Model.State; + +namespace TINK.Model.Connector +{ + /// + /// Conversion helper functionality. + /// + public static class TextToTypeHelper + { + /// Holds the text for demo bikes. + private const string DEMOBIKEMARKER = "DEMO"; + + /// Part text denoting two wheel cargo bike.. + private const string TWOWHEELCARGOMARKERFRAGMENT = "LONG"; + + /// + /// Gets the position from StationInfo object. + /// + /// Object to get information from. + /// Position information. + public static Station.Position GetPosition(this StationsAllResponse.StationInfo p_oStationInfo) + { + return GetPosition(p_oStationInfo.gps); + } + + /// Gets the position from StationInfo object. + /// Object to get information from. + /// Position information. + public static IEnumerable GetGroup(this AuthorizationResponse p_oAuthorizationResponse) + { + try + { + return p_oAuthorizationResponse.user_group.GetGroup(); + } + catch (Exception l_oException) + { + throw new Exception($"Can not get group of user from text \"{p_oAuthorizationResponse.user_group}\".", l_oException); + } + } + + /// Gets the position from StationInfo object. + /// Object to get information from. + /// Position information. + public static IEnumerable GetGroup(this string p_oGroup) + { + if (string.IsNullOrEmpty(p_oGroup)) + { + throw new ArgumentException("Can not get goup form string. Group text can not be null."); + } + + return new HashSet(p_oGroup.Split(',')).ToList(); + } + + /// Gets the position from StationInfo object. + /// Object to get information from. + /// Position information. + public static string GetGroup(this IEnumerable p_oGroup) + { + return string.Join(",", p_oGroup); + } + + /// Gets the position from StationInfo object. + /// Object to get information from. + /// Position information. + public static IEnumerable GetGroup(this StationsAllResponse.StationInfo p_oStationInfo) + { + try + { + return p_oStationInfo.station_group.GetGroup(); + } + catch (System.Exception l_oException) + { + throw new System.Exception($"Can not get group of stations from text \"{p_oStationInfo.station_group}\".", l_oException); + } + } + + /// + /// Gets the position from StationInfo object. + /// + /// Object to get information from. + /// Position information. + public static Station.Position GetPosition(this BikeInfoAvailable p_oBikeInfo) + { + return GetPosition(p_oBikeInfo.gps); + } + + /// + /// Gets the position from StationInfo object. + /// + /// Object to get information from. + /// Position information. + public static InUseStateEnum GetState(this BikeInfoBase p_oBikeInfo) + { + var l_oState = p_oBikeInfo.state; + + if (string.IsNullOrEmpty(l_oState)) + { + throw new InvalidResponseException( + string.Format("Unknown reservation state detected. Member {0}.{1}.", typeof(BikeInfoBase), nameof(BikeInfoBase.state)), + p_oBikeInfo); + } + + if (l_oState == "available") + { + return InUseStateEnum.Disposable; + } + else if (l_oState == "reserved" || + l_oState == "requested") + { + return InUseStateEnum.Reserved; + } + else if (l_oState == "booked" || + l_oState == "occupied") + { + return InUseStateEnum.Booked; + } + + throw new CommunicationException(string.Format("Unknown bike state detected. State is {0}.", l_oState)); + } + + /// + /// Gets the from date information from JSON. + /// + /// JSON to get information from.. + /// From information. + public static DateTime GetFrom(this BikeInfoReservedOrBooked p_oBikeInfo) + { + return DateTime.Parse(p_oBikeInfo.start_time); + } + + /// + /// Gets whether the bike is a trike or not. + /// + /// JSON to get information from.. + /// From information. + public static bool? GetIsDemo(this BikeInfoBase p_oBikeInfo) + { + return p_oBikeInfo?.description != null + ? p_oBikeInfo.description.ToUpper().Contains(DEMOBIKEMARKER) + : (bool?) null; + } + + /// + /// Gets whether the bike is a trike or not. + /// + /// JSON to get information from.. + /// From information. + public static IEnumerable GetGroup(this BikeInfoBase p_oBikeInfo) + { + try + { + return p_oBikeInfo?.bike_group?.GetGroup()?.ToList() ?? new List(); + } + catch (System.Exception l_oException) + { + throw new System.Exception($"Can not get group of user from text \"{p_oBikeInfo.bike_group}\".", l_oException); + } + } + + /// Gets whether the bike has a bord computer or not. + /// JSON to get information from. + /// From information. + public static bool GetIsManualLockBike(this BikeInfoBase p_oBikeInfo) + { + return !string.IsNullOrEmpty(p_oBikeInfo.system) + && p_oBikeInfo.system.ToUpper().StartsWith("LOCK"); + } + + /// Gets whether the bike has a bord computer or not. + /// JSON to get information from.. + /// From information. + public static bool GetIsBluetoothLockBike(this BikeInfoBase p_oBikeInfo) + { + return !string.IsNullOrEmpty(p_oBikeInfo.system) + && p_oBikeInfo.system.ToUpper().StartsWith("ILOCKIT"); + } + + /// Gets whether the bike has a bord computer or not. + /// JSON to get information from.. + /// From information. + public static int GetBluetoothLockId(this BikeInfoAvailable bikeInfo) + { + return TextToLockItTypeHelper.GetBluetoothLockId(bikeInfo?.Ilockit_ID); + } + + /// Gets whether the bike has a bord computer or not. + /// JSON to get information from.. + /// From information. + public static Guid GetBluetoothLockGuid(this BikeInfoAvailable bikeInfo) + { + // return new Guid("00000000-0000-0000-0000-e57e6b9aee16"); + return Guid.TryParse(bikeInfo?.Ilockit_GUID, out Guid lockGuid) + ? lockGuid + : TextToLockItTypeHelper.INVALIDLOCKGUID; + } + + public static byte[] GetUserKey(this BikeInfoReservedOrBooked bikeInfo) + { + return GetKey(bikeInfo.K_u); + } + + public static byte[] GetAdminKey(this BikeInfoReservedOrBooked bikeInfo) + { + return GetKey(bikeInfo.K_a); + } + + public static byte[] GetSeed(this BikeInfoReservedOrBooked bikeInfo) + { + return GetKey(bikeInfo.K_seed); + } + + /// + /// Get array of keys from string of format "[12, -9, 5]" + /// + /// + /// + private static byte[] GetKey(string keyArrayText) + { + try + { + if (string.IsNullOrEmpty(keyArrayText)) + return new byte[0]; + + return Regex.Replace(keyArrayText, @"\[(.*)\]", "$1").Split(',').Select(x => (byte)sbyte.Parse(x)).ToArray(); + } + catch (Exception exception) + { + Log.Error("Can not extract K_u/ K_a/ or K_seed. Key {ArrayText} does not is not of expected format. {Exception}", keyArrayText, exception); + return new byte[0]; + } + } + + /// + /// Gets whether the bike is a trike or not. + /// + /// JSON to get information from.. + /// From information. + public static WheelType? GetWheelType(this BikeInfoBase bikeInfo) + { + var l_oDescription = bikeInfo.description; + + if (l_oDescription == null) + { + // Can not get type of wheel if description text is empty. + return null; + } + + foreach (WheelType l_oWheelType in Enum.GetValues(typeof(WheelType))) + { + + if (l_oDescription.ToUpper().Contains(l_oWheelType.ToString().ToUpper())) + { + return l_oWheelType; + } + } + + // Check for custom value "Long". + if (l_oDescription.ToUpper().Contains(TWOWHEELCARGOMARKERFRAGMENT)) + { + return WheelType.Two; + } + + // Check for Stadrad. + if (GetTypeOfBike(bikeInfo) == TypeOfBike.Citybike) + { + return WheelType.Two; + } + + return null; + } + + /// + /// Gets the type of bike. + /// + /// Object to get bike type from. + /// Type of bike. + public static TypeOfBike? GetTypeOfBike(this BikeInfoBase bikeInfo) + { + var l_oDescription = bikeInfo?.description; + + if (l_oDescription == null) + { + // Can not get type of wheel if description text is empty. + return null; + } + + foreach (TypeOfBike l_oTypeOfBike in Enum.GetValues(typeof(TypeOfBike))) + { + if (l_oDescription.ToUpper().Contains(l_oTypeOfBike.GetCopriText().ToUpper())) + { + return l_oTypeOfBike; + } + } + + return null; + } + + /// + /// Get position from a ,- separated string. + /// + /// Text to extract positon from. + /// Position object. + public static Station.Position GetPosition(string p_strGps) + { + if (p_strGps == null) + { + return null; + } + + var l_oPosition = p_strGps.Split(','); + + if (l_oPosition.Length != 2) + return null; + + double l_oLatitude; + if (!double.TryParse(l_oPosition[0], NumberStyles.Float, CultureInfo.InvariantCulture, out l_oLatitude)) + return null; + + double l_oLongitude; + if (!double.TryParse(l_oPosition[1], NumberStyles.Float, CultureInfo.InvariantCulture, out l_oLongitude)) + return null; + + return new Station.Position(l_oLatitude, l_oLongitude); + } + + /// Gets text of bike from. + /// Type to get text for. + /// + public static string GetCopriText(this TypeOfBike p_eType) + { + switch (p_eType) + { + case TypeOfBike.Citybike: + return "Stadtrad"; + + default: + return p_eType.ToString(); + } + } + + public static Uri GetOperatorUri(this BikeInfoBase bikeInfo) + { + return bikeInfo?.uri_operator != null && !string.IsNullOrEmpty(bikeInfo?.uri_operator) + ? new Uri($"{bikeInfo.uri_operator}/{CopriServerUriList.REST_RESOURCE_ROOT}") + : null; + } + } +} diff --git a/TINKLib/Model/Connector/Updater/UpdaterJSON.cs b/TINKLib/Model/Connector/Updater/UpdaterJSON.cs new file mode 100644 index 0000000..3e4fd21 --- /dev/null +++ b/TINKLib/Model/Connector/Updater/UpdaterJSON.cs @@ -0,0 +1,496 @@ +using System; +using TINK.Model.Bike; +using TINK.Model.Station; +using TINK.Model.Repository.Response; +using TINK.Model.User.Account; +using System.Collections.Generic; +using TINK.Model.State; +using TINK.Model.Repository.Exception; +using Serilog; + +using BikeInfo = TINK.Model.Bike.BC.BikeInfo; +using IBikeInfoMutable = TINK.Model.Bikes.Bike.BC.IBikeInfoMutable; +using System.Globalization; + +namespace TINK.Model.Connector +{ + /// + /// Connects TINK app to copri using JSON as input data format. + /// + /// Rename to UpdateFromCopri. + public static class UpdaterJSON + { + /// Loads a bike object from copri server cancel reservation/ booking update request. + /// Bike object to load response into. + /// Controls whether notify property changed events are fired or not. + public static void Load( + this IBikeInfoMutable bike, + Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel) + { + + bike.State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel); + } + /// + /// Gets all statsion for station provider and add them into station list. + /// + /// List of stations to update. + public static StationDictionary GetStationsAllMutable(this StationsAllResponse p_oStationsAllResponse) + { + // Get stations from Copri/ file/ memory, .... + if (p_oStationsAllResponse == null + || p_oStationsAllResponse.stations == null) + { + // Latest list of stations could not be retrieved from provider. + return new StationDictionary(); + } + + Version.TryParse(p_oStationsAllResponse.copri_version, out Version l_oCopriVersion); + + var l_oStations = new StationDictionary(p_oVersion: l_oCopriVersion); + + foreach (var l_oStation in p_oStationsAllResponse.stations) + { + if (l_oStations.GetById(l_oStation.Value.station) != null) + { + // Can not add station to list of station. Id is not unique. + throw new InvalidResponseException( + string.Format("Station id {0} is not unique.", l_oStation.Value.station), p_oStationsAllResponse); + } + + l_oStations.Add(new Station.Station( + l_oStation.Value.station, + l_oStation.Value.GetGroup(), + l_oStation.Value.GetPosition(), + l_oStation.Value.description)); + } + + return l_oStations; + } + + /// Gets account object from login response. + /// Needed to extract cookie from autorization response. + /// Response to get session cookie and debug level from. + /// Mail address needed to construct a complete account object (is not part of response). + /// Password needed to construct a complete account object (is not part of response). + public static IAccount GetAccount( + this AuthorizationResponse loginResponse, + string merchantId, + string mail, + string password) + { + if (loginResponse == null) + { + throw new ArgumentNullException("p_oLoginResponse"); + } + + return new Account( + mail, + password, + loginResponse.authcookie?.Replace(merchantId, ""), + loginResponse.GetGroup(), + loginResponse.debuglevel == 1 + ? Permissions.All : + (Permissions)loginResponse.debuglevel) ; + } + + /// Load bike object from booking response. + /// Bike object to load from response. + /// Booking response. + /// Mail address of user which books bike. + /// Session cookie of user which books bike. + /// Controls whether notify property changed events are fired or not. + public static void Load( + this IBikeInfoMutable bike, + BikeInfoReservedOrBooked bikeInfo, + string mailAddress, + Func dateTimeProvider, + Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel = Bikes.Bike.BC.NotifyPropertyChangedLevel.All) + { + + var l_oDateTimeProvider = dateTimeProvider != null + ? dateTimeProvider + : () => DateTime.Now; + + if (bike is Bike.BluetoothLock.BikeInfoMutable btBikeInfo) + { + btBikeInfo.LockInfo.Load( + bikeInfo.GetBluetoothLockId(), + bikeInfo.GetBluetoothLockGuid(), + bikeInfo.GetSeed(), + bikeInfo.GetUserKey(), + bikeInfo.GetAdminKey()); + } + + var l_oState = bikeInfo.GetState(); + switch (l_oState) + { + case InUseStateEnum.Disposable: + bike.State.Load( + InUseStateEnum.Disposable, + notifyLevel: notifyLevel); + break; + + case InUseStateEnum.Reserved: + bike.State.Load( + InUseStateEnum.Reserved, + bikeInfo.GetFrom(), + mailAddress, + bikeInfo.timeCode, + notifyLevel); + break; + + case InUseStateEnum.Booked: + bike.State.Load( + InUseStateEnum.Booked, + bikeInfo.GetFrom(), + mailAddress, + bikeInfo.timeCode, + notifyLevel); + break; + + default: + throw new Exception(string.Format("Unexpected bike state detected. state is {0}.", l_oState)); + } + } + + /// Gets bikes available from copri server response. + /// Response to create collection from. + /// New collection of available bikes. + public static BikeCollection GetBikesAvailable( + this BikesAvailableResponse p_oBikesAvailableResponse) + { + return GetBikesAll( + p_oBikesAvailableResponse, + new BikesReservedOccupiedResponse(), // There are no occupied bikes. + string.Empty, + () => DateTime.Now); + } + + /// Gets bikes occupied from copri server response. + /// Response to create bikes from. + /// New collection of occupied bikes. + public static BikeCollection GetBikesOccupied( + this BikesReservedOccupiedResponse p_oBikesOccupiedResponse, + string p_strMail, + Func p_oDateTimeProvider) + { + return GetBikesAll( + new BikesAvailableResponse(), + p_oBikesOccupiedResponse, + p_strMail, + p_oDateTimeProvider); + } + + /// Gets bikes occupied from copri server response. + /// Response to create bikes from. + /// New collection of occupied bikes. + public static BikeCollection GetBikesAll( + BikesAvailableResponse p_oBikesAvailableResponse, + BikesReservedOccupiedResponse p_oBikesOccupiedResponse, + string p_strMail, + Func p_oDateTimeProvider) + { + var l_oBikesDictionary = new Dictionary(); + var l_oDuplicates = new Dictionary(); + + // Get bikes from Copri/ file/ memory, .... + if (p_oBikesAvailableResponse != null + && p_oBikesAvailableResponse.bikes != null) + { + foreach (var bikeInfoResponse in p_oBikesAvailableResponse.bikes.Values) + { + var l_oBikeInfo = BikeInfoFactory.Create(bikeInfoResponse); + if (l_oBikeInfo == null) + { + // Response is not valid. + continue; + } + + if (l_oBikesDictionary.ContainsKey(l_oBikeInfo.Id)) + { + // Duplicates are not allowed. + Log.Error($"Duplicate bike with id {l_oBikeInfo.Id} detected evaluating bikes available. Bike status is {l_oBikeInfo.State.Value}."); + + if (!l_oDuplicates.ContainsKey(l_oBikeInfo.Id)) + { + l_oDuplicates.Add(l_oBikeInfo.Id, l_oBikeInfo); + } + + continue; + } + + l_oBikesDictionary.Add(l_oBikeInfo.Id, l_oBikeInfo); + } + } + + // Get bikes from Copri/ file/ memory, .... + if (p_oBikesOccupiedResponse != null + && p_oBikesOccupiedResponse.bikes_occupied != null) + { + foreach (var l_oBikeInfoResponse in p_oBikesOccupiedResponse.bikes_occupied.Values) + { + BikeInfo l_oBikeInfo = BikeInfoFactory.Create( + l_oBikeInfoResponse, + p_strMail, + p_oDateTimeProvider); + + if (l_oBikeInfo == null) + { + continue; + } + + if (l_oBikesDictionary.ContainsKey(l_oBikeInfo.Id)) + { + // Duplicates are not allowed. + Log.Error($"Duplicate bike with id {l_oBikeInfo.Id} detected evaluating bikes occupied. Bike status is {l_oBikeInfo.State.Value}."); + if (!l_oDuplicates.ContainsKey(l_oBikeInfo.Id)) + { + l_oDuplicates.Add(l_oBikeInfo.Id, l_oBikeInfo); + } + + continue; + } + + + l_oBikesDictionary.Add(l_oBikeInfo.Id, l_oBikeInfo); + } + } + + // Remove entries which are not unique. + foreach (var l_oDuplicate in l_oDuplicates) + { + l_oBikesDictionary.Remove(l_oDuplicate.Key); + } + + return new BikeCollection(l_oBikesDictionary); + } + } + + + /// + /// Constructs bike info instances/ bike info derived instances. + /// + public static class BikeInfoFactory + { + public static BikeInfo Create(BikeInfoAvailable bikeInfo) + { + if (bikeInfo.GetIsManualLockBike()) + { + // Manual lock bikes are no more supported. + Log.Error( + $"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " + + "Manual lock bikes are no more supported." + + $"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $"station number {bikeInfo.station}" : string.Empty)}." + ); + return null; + } + + switch (bikeInfo.GetState()) + { + case InUseStateEnum.Disposable: + break; + + default: + Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected."); + return null; + } + + if (bikeInfo.station == null) + { + // Bike available must always have a station id because bikes can only be returned at a station. + Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. No station info set."); + return null; + } + + try + { + return !bikeInfo.GetIsBluetoothLockBike() + ? new BikeInfo( + bikeInfo.bike, + bikeInfo.station, + bikeInfo.GetOperatorUri(), +#if !NOTARIFFDESCRIPTION + Create(bikeInfo.tariff_description), +#else + Create((TINK.Repository.Response.TariffDescription) null), +#endif + bikeInfo.GetIsDemo(), + bikeInfo.GetGroup(), + bikeInfo.GetWheelType(), + bikeInfo.GetTypeOfBike(), + bikeInfo.description) + : new Bike.BluetoothLock.BikeInfo( + bikeInfo.bike, + bikeInfo.GetBluetoothLockId(), + bikeInfo.GetBluetoothLockGuid(), + bikeInfo.station, + bikeInfo.GetOperatorUri(), +#if !NOTARIFFDESCRIPTION + Create(bikeInfo.tariff_description), +#else + Create((TINK.Repository.Response.TariffDescription)null), +#endif + bikeInfo.GetIsDemo(), + bikeInfo.GetGroup(), + bikeInfo.GetWheelType(), + bikeInfo.GetTypeOfBike(), + bikeInfo.description); + + } + catch (ArgumentException ex) + { + // Contructor reported invalid arguemts (missing lock id, ....). + Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Invalid response detected. Available bike with id {bikeInfo.bike} skipped. {ex.Message}"); + return null; + } + } + + /// Creates a bike info object from copri response. + /// Copri response. + /// Mail address of user. + /// Date and time provider function. + /// + public static BikeInfo Create( + BikeInfoReservedOrBooked bikeInfo, + string mailAddress, + Func dateTimeProvider) + { + if (bikeInfo.GetIsManualLockBike()) + { + // Manual lock bikes are no more supported. + Log.Error( + $"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. " + + "Manual lock bikes are no more supported." + + $"Bike number: {bikeInfo.bike}{(bikeInfo.station != null ? $", station number {bikeInfo.station}" : string.Empty)}." + ); + return null; + } + + // Check if bike is a bluetooth lock bike. + var isBluetoothBike = bikeInfo.GetIsBluetoothLockBike(); + int lockSerial = bikeInfo.GetBluetoothLockId(); + Guid lockGuid = bikeInfo.GetBluetoothLockGuid(); + + switch (bikeInfo.GetState()) + { + case InUseStateEnum.Reserved: + try + { + return !isBluetoothBike + ? new BikeInfo( + bikeInfo.bike, + bikeInfo.GetIsDemo(), + bikeInfo.GetGroup(), + bikeInfo.GetWheelType(), + bikeInfo.GetTypeOfBike(), + bikeInfo.description, + bikeInfo.station, + bikeInfo.GetOperatorUri(), + +#if !NOTARIFFDESCRIPTION + Create(bikeInfo.tariff_description), +#else + Create((TINK.Repository.Response.TariffDescription)null), +#endif + bikeInfo.GetFrom(), + mailAddress, + bikeInfo.timeCode, + dateTimeProvider) + : new Bike.BluetoothLock.BikeInfo( + bikeInfo.bike, + lockSerial, + lockGuid, + bikeInfo.GetUserKey(), + bikeInfo.GetAdminKey(), + bikeInfo.GetSeed(), + bikeInfo.GetFrom(), + mailAddress, + bikeInfo.station, + bikeInfo.GetOperatorUri(), +#if !NOTARIFFDESCRIPTION + Create(bikeInfo.tariff_description), +#else + Create((TINK.Repository.Response.TariffDescription)null), +#endif + dateTimeProvider, + bikeInfo.GetIsDemo(), + bikeInfo.GetGroup(), + bikeInfo.GetWheelType(), + bikeInfo.GetTypeOfBike()); + } + catch (ArgumentException ex) + { + // Contructor reported invalid arguemts (missing lock id, ....). + Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Reserved bike with id {bikeInfo.bike} skipped. {ex.Message}"); + return null; + } + + case InUseStateEnum.Booked: + try + { + return !isBluetoothBike + ? new BikeInfo( + bikeInfo.bike, + bikeInfo.GetIsDemo(), + bikeInfo.GetGroup(), + bikeInfo.GetWheelType(), + bikeInfo.GetTypeOfBike(), + bikeInfo.description, + bikeInfo.station, + bikeInfo.GetOperatorUri(), +#if !NOTARIFFDESCRIPTION + Create(bikeInfo.tariff_description), +#else + Create((TINK.Repository.Response.TariffDescription)null), +#endif + bikeInfo.GetFrom(), + mailAddress, + bikeInfo.timeCode) + : new Bike.BluetoothLock.BikeInfo( + bikeInfo.bike, + lockSerial, + bikeInfo.GetBluetoothLockGuid(), + bikeInfo.GetUserKey(), + bikeInfo.GetAdminKey(), + bikeInfo.GetSeed(), + bikeInfo.GetFrom(), + mailAddress, + bikeInfo.station, + bikeInfo.GetOperatorUri(), +#if !NOTARIFFDESCRIPTION + Create(bikeInfo.tariff_description), +#else + Create((TINK.Repository.Response.TariffDescription)null), +#endif + bikeInfo.GetIsDemo(), + bikeInfo.GetGroup(), + bikeInfo.GetWheelType(), + bikeInfo.GetTypeOfBike()); + } + catch (ArgumentException ex) + { + // Contructor reported invalid arguemts (missing lock id, ....). + Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoReservedOrBooked)} argument. Invalid response detected. Booked bike with id {bikeInfo.bike} skipped. {ex.Message}"); + return null; + } + + default: + Log.Error($"Can not create new {nameof(BikeInfo)}-object from {nameof(BikeInfoAvailable)} argument. Unexpected state {bikeInfo.GetState()} detected."); + return null; + } + } + + public static Bikes.Bike.TariffDescription Create(this TINK.Repository.Response.TariffDescription tariffDesciption) + { + return new Bikes.Bike.TariffDescription + { + Name = tariffDesciption?.name, + Number = int.TryParse(tariffDesciption?.number, out int number) ? number : null, + FreeTimePerSession = double.TryParse(tariffDesciption?.free_hours, NumberStyles.Any, CultureInfo.InvariantCulture, out double freeHours) ? TimeSpan.FromHours(freeHours) : TimeSpan.Zero, + FeeEuroPerHour = double.TryParse(tariffDesciption?.eur_per_hour, NumberStyles.Any, CultureInfo.InvariantCulture, out double euroPerHour) ? euroPerHour : double.NaN, + AboEuroPerMonth = double.TryParse(tariffDesciption?.abo_eur_per_month, NumberStyles.Any, CultureInfo.InvariantCulture, out double aboEuroPerMonth) ? aboEuroPerMonth : double.NaN, + MaxFeeEuroPerDay = double.TryParse(tariffDesciption?.max_eur_per_day, NumberStyles.Any, CultureInfo.InvariantCulture, out double maxEuroPerDay) ? maxEuroPerDay : double.NaN, + }; + } + } +} diff --git a/TINKLib/Model/Device/IAppInfo.cs b/TINKLib/Model/Device/IAppInfo.cs new file mode 100644 index 0000000..77b0759 --- /dev/null +++ b/TINKLib/Model/Device/IAppInfo.cs @@ -0,0 +1,14 @@ +using System; +namespace TINK.Model.Device +{ + /// Interface to get version info. + public interface IAppInfo + { + /// Gets the app version to display to user. + Version Version { get; } + + /// Gets the URL to the app store. + /// The store URL. + string StoreUrl { get; } + } +} diff --git a/TINKLib/Model/Device/IDevice.cs b/TINKLib/Model/Device/IDevice.cs new file mode 100644 index 0000000..787dce5 --- /dev/null +++ b/TINKLib/Model/Device/IDevice.cs @@ -0,0 +1,9 @@ +namespace TINK.Model.Device +{ + public interface IDevice + { + /// Gets unitque device identifier. + /// Gets the identifies specifying device. + string GetIdentifier(); + } +} diff --git a/TINKLib/Model/Device/IExternalBrowserService.cs b/TINKLib/Model/Device/IExternalBrowserService.cs new file mode 100644 index 0000000..4a7a9ae --- /dev/null +++ b/TINKLib/Model/Device/IExternalBrowserService.cs @@ -0,0 +1,9 @@ +namespace TINK.Model.Device +{ + public interface IExternalBrowserService + { + /// Opens an external browser. + /// Url to open. + void OpenUrl(string url); + } +} diff --git a/TINKLib/Model/Device/IGeolodationDependent.cs b/TINKLib/Model/Device/IGeolodationDependent.cs new file mode 100644 index 0000000..736b437 --- /dev/null +++ b/TINKLib/Model/Device/IGeolodationDependent.cs @@ -0,0 +1,7 @@ +namespace TINK.Model.Device +{ + public interface IGeolodationDependent + { + bool IsGeolcationEnabled { get; } + } +} diff --git a/TINKLib/Model/Device/ISpecialFolder.cs b/TINKLib/Model/Device/ISpecialFolder.cs new file mode 100644 index 0000000..ed6ac85 --- /dev/null +++ b/TINKLib/Model/Device/ISpecialFolder.cs @@ -0,0 +1,15 @@ +namespace TINK.Model.Device +{ + public interface ISpecialFolder + { + /// + /// Get the folder name of external folder to write to. + /// + /// External directory. + string GetExternalFilesDir(); + + /// Gets the folder name of the personal data folder dir on internal storage. + /// Directory name. + string GetInternalPersonalDir(); + } +} diff --git a/TINKLib/Model/Device/IWebView.cs b/TINKLib/Model/Device/IWebView.cs new file mode 100644 index 0000000..bda2202 --- /dev/null +++ b/TINKLib/Model/Device/IWebView.cs @@ -0,0 +1,8 @@ +namespace TINK.Model.Device +{ + public interface IWebView + { + /// Clears the cookie cache for all web views. + void ClearCookies(); + } +} diff --git a/TINKLib/Model/FileOperationException.cs b/TINKLib/Model/FileOperationException.cs new file mode 100644 index 0000000..909c8ac --- /dev/null +++ b/TINKLib/Model/FileOperationException.cs @@ -0,0 +1,11 @@ +using System; + +namespace TINK.Model +{ + /// Operation fired when a file operation fails. + public class FileOperationException : Exception + { + public FileOperationException(string message, Exception innerException) : base(message, innerException) + { } + } +} diff --git a/TINKLib/Model/FilterCollectionStore.cs b/TINKLib/Model/FilterCollectionStore.cs new file mode 100644 index 0000000..ff9bca0 --- /dev/null +++ b/TINKLib/Model/FilterCollectionStore.cs @@ -0,0 +1,14 @@ +using Newtonsoft.Json; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model +{ + public static class FilterCollectionStore + { + /// Writes filter collection state to string. + /// Filter collection to write to string. + /// P o dictionary. + public static string ToString(this IDictionary p_oDictionary) => "{" + string.Join(", ", p_oDictionary.Select(x => "(" + x.Key + "= " + x.Value + ")")) + "}"; + } +} diff --git a/TINKLib/Model/GroupFilterHelper.cs b/TINKLib/Model/GroupFilterHelper.cs new file mode 100644 index 0000000..665ac59 --- /dev/null +++ b/TINKLib/Model/GroupFilterHelper.cs @@ -0,0 +1,45 @@ +using System.Collections.Generic; +using TINK.Model.Connector; +using TINK.ViewModel.Map; +using TINK.ViewModel.Settings; + +namespace TINK.Model +{ + /// Holds collecion of filters to filter options (TINK, Konrad, ....). + /// Former name: FilterCollection. + public static class GroupFilterHelper + { + /// Gets default filter set. + public static IGroupFilterSettings GetSettingsFilterDefaults + { + get + { + return new GroupFilterSettings(new Dictionary { + { FilterHelper.FILTERTINKGENERAL, FilterState.On }, + {FilterHelper.FILTERKONRAD, FilterState.On } + }); + } + } + + /// Gets default filter set. + public static IGroupFilterMapPage GetMapPageFilterDefaults + { + get + { + return new ViewModel.Map.GroupFilterMapPage(new Dictionary { + { FilterHelper.FILTERTINKGENERAL, FilterState.On }, + {FilterHelper.FILTERKONRAD, FilterState.Off } + }); + } + } + } + + /// Holds value whether filter (on TINK, Konrad, ....) is on or off. + public enum FilterState + { + /// Option (TINK, Konrad, ....) is available. + On, + /// Option is off + Off, + } +} diff --git a/TINKLib/Model/ITinkApp.cs b/TINKLib/Model/ITinkApp.cs new file mode 100644 index 0000000..67ba1dc --- /dev/null +++ b/TINKLib/Model/ITinkApp.cs @@ -0,0 +1,98 @@ +using Plugin.Permissions.Abstractions; +using Serilog.Events; +using System; +using System.Threading; +using TINK.Model.Connector; +using TINK.Model.Device; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.CopriApi.ServerUris; +using TINK.Model.Services.Geolocation; +using TINK.Settings; +using TINK.ViewModel.Map; +using TINK.ViewModel.Settings; +using TINK.Services; + +namespace TINK.Model +{ + public interface ITinkApp + { + /// Update connector from depending on whether user is logged in or not. + void UpdateConnector(); + + /// Saves object to file. + void Save(); + + /// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. + IGroupFilterMapPage GroupFilterMapPage { get; set; } + + /// Holds the user of the app. + User.User ActiveUser { get; } + + /// Sets flag whats new page was already shown to true. + void SetWhatsNewWasShown(); + + /// Holds the system to copri. + IFilteredConnector GetConnector(bool isConnected); + + /// Name of the station which is selected. + int? SelectedStation { get; set; } + + /// Polling periode. + PollingParameters Polling { get; set; } + + TimeSpan ExpiresAfter { get; set; } + + /// Holds status about whants new page. + WhatsNew WhatsNew { get; } + + /// Gets whether device is connected to internet or not. + bool GetIsConnected(); + + /// Action to post to GUI thread. + Action PostAction { get; } + + /// Holds the uri which is applied after restart. + Uri NextActiveUri { get; set; } + + /// Holds the filters loaded from settings. + IGroupFilterSettings FilterGroupSetting { get; set; } + + /// Value indicating whether map is centerted to current position or not. + bool CenterMapToCurrentLocation { get; set; } + + bool LogToExternalFolder { get; set; } + + bool IsSiteCachingOn { get; set; } + + /// Gets the minimum logging level. + LogEventLevel MinimumLogEventLevel { get; set; } + + /// Updates logging level. + /// New level to set. + void UpdateLoggingLevel(LogEventLevel p_oNewLevel); + + /// Holds uris of copri servers. + CopriServerUriList Uris { get; } + + /// Holds the different lock service implementations. + LocksServicesContainerMutable LocksServices { get; } + + /// Holds the different geo location service implementations. + ServicesContainerMutable GeolocationServices { get; } + + /// Holds available app themes. + ServicesContainerMutable Themes { get; } + + /// Reference of object which provides device information. + IDevice Device { get; } + + /// Os permission. + IPermissions Permissions { get; } + + /// Holds the folder where settings files are stored. + string SettingsFileFolder { get; } + + /// Holds the external path. + string ExternalFolder { get; } + } +} diff --git a/TINKLib/Model/Logging/EmptyDirectoryLoggingManger.cs b/TINKLib/Model/Logging/EmptyDirectoryLoggingManger.cs new file mode 100644 index 0000000..41bc718 --- /dev/null +++ b/TINKLib/Model/Logging/EmptyDirectoryLoggingManger.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace TINK.Model.Logging +{ + public class EmptyDirectoryLoggingManger : ILoggingDirectoryManager + { + public void DeleteObsoleteLogs() { } + + /// Gets the log file name. + public string LogFileName { get { return string.Empty; } } + + /// Gets all log files in logging directory. + /// List of log files. + public IList GetLogFiles() { return new List(); } + + /// Gets path where log files are located. + /// Path to log files. + public string LogFilePath { get { return string.Empty; } } + } +} diff --git a/TINKLib/Model/Logging/ILoggingDirectoryManager.cs b/TINKLib/Model/Logging/ILoggingDirectoryManager.cs new file mode 100644 index 0000000..ea1188a --- /dev/null +++ b/TINKLib/Model/Logging/ILoggingDirectoryManager.cs @@ -0,0 +1,21 @@ +using System.Collections.Generic; + +namespace TINK.Model.Logging +{ + public interface ILoggingDirectoryManager + { + /// Deletes files which are out of retainment scope. + void DeleteObsoleteLogs(); + + /// Gets the log file name. + string LogFileName { get; } + + /// Gets all log files in logging directory. + /// List of log files. + IList GetLogFiles(); + + /// Gets path where log files are located. + /// Path to log files. + string LogFilePath { get; } + } +} diff --git a/TINKLib/Model/Logging/LogEntryClassifyHelper.cs b/TINKLib/Model/Logging/LogEntryClassifyHelper.cs new file mode 100644 index 0000000..ed8ae23 --- /dev/null +++ b/TINKLib/Model/Logging/LogEntryClassifyHelper.cs @@ -0,0 +1,35 @@ +using Serilog; +using System; +using TINK.Model.Repository.Exception; + +namespace TINK.Model.Logging +{ + public static class LogEntryClassifyHelper + { + /// Classifies exception and logs information or error depending on result of classification. + /// Type of first message parameter. + /// Type of second message parameter. + /// Object to use for logging. + /// Templated used to output message. + /// First message parameter. + /// Second message parameter. + /// Exception to classify. + public static void InformationOrError(this ILogger p_oLogger, string messageTemplate, T0 propertyValue0, T1 propertyValue1, Exception p_oException) + { + if (p_oException == null) + { + p_oLogger.Error(messageTemplate, propertyValue0, propertyValue1, string.Empty); + return; + } + + if (p_oException.GetIsConnectFailureException()) + { + // Expected exception (LAN or mobile data off/ not reachable, proxy, ...) + p_oLogger.Information(messageTemplate, propertyValue0, propertyValue1, p_oException); + return; + } + + p_oLogger.Error(messageTemplate, propertyValue0, propertyValue1, p_oException); + } + } +} diff --git a/TINKLib/Model/Logging/LoggerConfigurationHelper.cs b/TINKLib/Model/Logging/LoggerConfigurationHelper.cs new file mode 100644 index 0000000..49a6c66 --- /dev/null +++ b/TINKLib/Model/Logging/LoggerConfigurationHelper.cs @@ -0,0 +1,103 @@ +using Serilog; +using Serilog.Configuration; +using Serilog.Formatting.Json; +using System; +using System.Collections.Generic; +using System.IO; + +namespace TINK.Model.Logging +{ + /// Holds new logging levels. + public enum RollingInterval + { + /// Create a new log file for each session (start of app). + Session, + } + + /// Provides logging file name helper functionality. + public static class LoggerConfigurationHelper + { + /// Holds the log file name. + private static ILoggingDirectoryManager m_oDirectoryManager = new EmptyDirectoryLoggingManger(); + + /// Sets up logging to file. + /// Object to set up logging with. + /// Object to get file informaton from. + /// Specifies rolling type. + /// Count of file being retained. + /// Logger object. + public static LoggerConfiguration File( + this LoggerSinkConfiguration p_oLoggerConfiguration, + string p_strLogFileFolder, + RollingInterval p_oRollingInterval = RollingInterval.Session, + int p_iRetainedFilesCountLimit = 10) + { + if (m_oDirectoryManager is EmptyDirectoryLoggingManger) + { + // Roll file only once per app session. + try + { + m_oDirectoryManager = new LoggingDirectoryManager( + Directory.GetFiles, + Directory.Exists, + (path) => Directory.CreateDirectory(path), + System.IO.File.Delete, + p_strLogFileFolder, + Path.DirectorySeparatorChar, + p_iRetainedFilesCountLimit); + } + catch (Exception l_oException) + { + Log.Error("Log directory manager could not be instanciated successfully. {@l_oException}", l_oException); + m_oDirectoryManager = new EmptyDirectoryLoggingManger(); + } + } + + try + { + m_oDirectoryManager.DeleteObsoleteLogs(); + } + catch (Exception l_oException) + { + Log.Error("Not all obsolte log files could be deleted successfully. {@l_oException}", l_oException); + } + + if (p_oLoggerConfiguration == null) + { + return null; + } + + return p_oLoggerConfiguration.File( + new JsonFormatter(), + m_oDirectoryManager.LogFileName, + /*shared: true, // Leads to exception if activated.*/ + rollingInterval: Serilog.RollingInterval.Infinite, + retainedFileCountLimit: p_iRetainedFilesCountLimit); + } + + /// Gets all log files in logging directory. + /// + /// List of log files. + public static IList GetLogFiles(this ILogger p_oLogger) + { + try + { + return m_oDirectoryManager.GetLogFiles(); + } + catch (Exception l_oException) + { + Log.Error("Getting list of log files failed. Empty list is returned instead. {@l_oException}", l_oException); + return new List(); + } + } + + /// Gets path where log files are located. + /// + /// List of log files. + public static string GetLogFilePath( + this ILogger p_oLogger) + { + return m_oDirectoryManager.LogFilePath; + } + } +} diff --git a/TINKLib/Model/Logging/LoggingDirectoryManager.cs b/TINKLib/Model/Logging/LoggingDirectoryManager.cs new file mode 100644 index 0000000..d14b5f6 --- /dev/null +++ b/TINKLib/Model/Logging/LoggingDirectoryManager.cs @@ -0,0 +1,147 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model.Logging +{ + public class LoggingDirectoryManager : ILoggingDirectoryManager + { + /// Name of logging subdirectory. + private const string LOGDIRECTORYTITLE = "Log"; + + /// Prevents an invalid instance to be created. + private LoggingDirectoryManager() { } + + public LoggingDirectoryManager( + Func> p_oFileListProvider, + Func p_oDirectoryExistsChecker, + Action p_oDirectoryCreator, + Action p_oFileEraser, + string p_oLogFilePath, + char p_strDirectorySeparatorChar, + int p_iRetainedFilesCountLimit) + { + m_oFileListProvider = p_oFileListProvider ?? throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. File list provider delegate can not be null."); + + if (p_oDirectoryExistsChecker == null) + { + throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Directory existance checker delegate can not be null."); + } + + if (p_oDirectoryCreator == null) + { + throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Directory creator delegate can not be null."); + } + + m_oFileEraser = p_oFileEraser ?? throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. File eraser delegate can not be null."); + + if (string.IsNullOrEmpty(p_oLogFilePath)) + { + throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Log file path can not be null or empty."); + } + + if (string.IsNullOrEmpty(p_strDirectorySeparatorChar.ToString())) + { + throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Directory separtor character can not be null or empty."); + } + + if (p_iRetainedFilesCountLimit < 1) + { + throw new ArgumentException($"Can not instantiate {nameof(LoggingDirectoryManager)}- object. Count of retained log files is {p_iRetainedFilesCountLimit} but must be equal or larger one."); + } + + DirectorySeparatorChar = p_strDirectorySeparatorChar.ToString(); + + LogFilePath = $"{p_oLogFilePath}{DirectorySeparatorChar}{LOGDIRECTORYTITLE}"; + + + m_iRetainedFilesCountLimit = p_iRetainedFilesCountLimit; + + if (string.IsNullOrEmpty(LogFileTitle)) + { + LogFileTitle = $"{ DateTime.Now:yyyy_MM_dd_HH_mm_ss}.jsnl"; + } + + // Create directory if direcotry does not exist. + if (p_oDirectoryExistsChecker(LogFilePath) == false) + { + try + { + p_oDirectoryCreator(LogFilePath); + } + catch (Exception l_oException) + { + + throw new FileOperationException($"Logging directory {LogFilePath} could not be created successfully.", l_oException); + } + + } + } + + /// Deletes files which are out of retainment scope. + public void DeleteObsoleteLogs() + { + var l_oExceptions = new List(); + var l_oSortedFileArray = m_oFileListProvider(LogFilePath).OrderByDescending(x => x).ToArray(); + + for (int l_iIndex = l_oSortedFileArray.Length - 1; + l_iIndex >= m_iRetainedFilesCountLimit - 1; /* files remaining count must be m_iRetainedFilesCountLimit - 1 because new log file will be added afterwards */ + l_iIndex --) + { + try + { + m_oFileEraser(l_oSortedFileArray[l_iIndex]); + } + catch (Exception l_oExpetion) + { + // Getting list of log files found. + l_oExceptions.Add(l_oExpetion); + } + } + + if (l_oExceptions.Count <= 0) + { + return; + } + + throw new AggregateException("Deleting obsolete log files failed.", l_oExceptions.ToArray()); + } + + /// Gets all log files in logging directory. + /// + /// List of log files. + public IList GetLogFiles() + { + try + { + return m_oFileListProvider(LogFilePath).OrderBy(x => x).ToList(); + } + catch (Exception l_oExpetion) + { + // Getting list of log files found. + throw new FileOperationException("Getting list of log files failed.", l_oExpetion); + } + } + + /// Holds delegate to provide file names. + private readonly Func> m_oFileListProvider; + + /// Holds delegate to delete files. + private readonly Action m_oFileEraser; + + /// Holds delegate to provide file names. + private int m_iRetainedFilesCountLimit; + + /// Holds the log file name. + private string LogFileTitle { get; } + + /// Holds the log file name. + public string LogFilePath { get; } + + /// Holds the directory separator character. + private string DirectorySeparatorChar { get; } + + /// Holds the log file name. + public string LogFileName { get { return $"{LogFilePath}{DirectorySeparatorChar}{LogFileTitle}"; } } + } +} diff --git a/TINKLib/Model/Settings/GroupFilterSettings.cs b/TINKLib/Model/Settings/GroupFilterSettings.cs new file mode 100644 index 0000000..a209037 --- /dev/null +++ b/TINKLib/Model/Settings/GroupFilterSettings.cs @@ -0,0 +1,75 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using TINK.Model; +using TINK.Model.Connector.Filter; + +namespace TINK.ViewModel.Settings +{ + public class GroupFilterSettings : IGroupFilterSettings + { + + public GroupFilterSettings(IDictionary filterDictionary = null) + { + FilterDictionary = filterDictionary ?? new Dictionary(); + + Filter = filterDictionary != null + ? (IGroupFilter) new IntersectGroupFilter(FilterDictionary.Where(x => x.Value == FilterState.On).Select(x => x.Key)) + : new NullGroupFilter(); + } + + private IDictionary FilterDictionary { get; set; } + + private IGroupFilter Filter { get; } + + /// Performs filtering on response-group. + public IEnumerable DoFilter(IEnumerable filter = null) => Filter.DoFilter(filter); + + public FilterState this[string key] { get => FilterDictionary[key]; set => FilterDictionary[key] = value; } + + public ICollection Keys => FilterDictionary.Keys; + + public ICollection Values => FilterDictionary.Values; + + public int Count => FilterDictionary.Count; + + public bool IsReadOnly => true; + + public void Add(string key, FilterState value) + { + throw new System.NotImplementedException(); + } + + public void Add(KeyValuePair item) + { + throw new System.NotImplementedException(); + } + + public void Clear() + { + throw new System.NotImplementedException(); + } + + public bool Contains(KeyValuePair item) => FilterDictionary.Contains(item); + + public bool ContainsKey(string key) => FilterDictionary.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => FilterDictionary.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => FilterDictionary.GetEnumerator(); + + public bool Remove(string key) + { + throw new System.NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new System.NotImplementedException(); + } + + public bool TryGetValue(string key, out FilterState value) => FilterDictionary.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => FilterDictionary.GetEnumerator(); + } +} diff --git a/TINKLib/Model/Settings/IGroupFilterSettings.cs b/TINKLib/Model/Settings/IGroupFilterSettings.cs new file mode 100644 index 0000000..e2231a9 --- /dev/null +++ b/TINKLib/Model/Settings/IGroupFilterSettings.cs @@ -0,0 +1,11 @@ +using System.Collections.Generic; +using TINK.Model; + +namespace TINK.ViewModel.Settings +{ + public interface IGroupFilterSettings : IDictionary + { + /// Performs filtering on response-group. + IEnumerable DoFilter(IEnumerable filter = null); + } +} diff --git a/TINKLib/Model/Settings/JsonSettingsDictionary.cs b/TINKLib/Model/Settings/JsonSettingsDictionary.cs new file mode 100644 index 0000000..ab78b39 --- /dev/null +++ b/TINKLib/Model/Settings/JsonSettingsDictionary.cs @@ -0,0 +1,515 @@ +using Newtonsoft.Json; +using Newtonsoft.Json.Converters; +using Serilog.Events; +using System; +using System.Collections.Generic; +using System.Linq; +using TINK.Model.Connector; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.CopriApi.ServerUris; +using TINK.Model.Services.Geolocation; +using TINK.Settings; +using TINK.ViewModel.Map; +using TINK.ViewModel.Settings; + +namespace TINK.Model.Settings +{ + public static class JsonSettingsDictionary + { + /// Title of the settings file. + public const string SETTINGSFILETITLE = "Setting.Json"; + + /// Key of the app version entry. + public const string APPVERIONKEY = "AppVersion"; + + /// Key of the app version entry. + public const string SHOWWHATSNEWKEY = "ShowWhatsNew"; + + /// Key of the app version entry. + public const string EXPIRESAFTER = "ExpiresAfter"; + + /// Key of the connect timeout. + public const string CONNECTTIMEOUT = "ConnectTimeout"; + + /// Key of the logging level entry. + public const string MINLOGGINGLEVELKEY = "MinimumLoggingLevel"; + + /// Key of the center to ... entry. + public const string CENTERMAPTOCURRENTLOCATION = "CenterMapToCurrentLocation"; + + public const string LOGTOEXTERNALFOLDER = "LogToExternalFolder"; + + public const string THEMEKEY = "Theme"; + + public const string ISSITECACHINGON = "IsSiteCachingOn"; + + /// Gets a nullable value. + /// Dictionary to get value from. + public static T? GetNullableEntry( + string keyName, + Dictionary settingsJSON) where T : struct + { + if (!settingsJSON.TryGetValue(keyName, out string boolText) + || string.IsNullOrEmpty(boolText)) + { + // File holds no entry. + return null; + } + + return JsonConvert.DeserializeObject(boolText); + } + + /// Gets a value to type class. + /// Dictionary to get value from. + public static T GetEntry( + string keyName, + Dictionary settingsJSON) where T : class + { + if (!settingsJSON.TryGetValue(keyName, out string boolText) + || string.IsNullOrEmpty(boolText)) + { + // File holds no entry. + return null; + } + + return JsonConvert.DeserializeObject(boolText); + } + + /// Sets a nullable. + /// Entry to add to dictionary. + /// Key to use for value. + /// Dictionary to set value to. + public static Dictionary SetEntry(T entry, string keyName, IDictionary targetDictionary) + { + // Set value. + if (targetDictionary == null) + throw new Exception($"Writing entry value {keyName} to dictionary failed. Dictionary must not be null."); + + return targetDictionary.Union(new Dictionary + { + { keyName, JsonConvert.SerializeObject(entry) } + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Sets the timeout to apply when connecting to bluetooth lock. + /// Dictionary to write information to. + /// Connect timeout value. + public static Dictionary SetConnectTimeout( + this IDictionary targetDictionary, + TimeSpan connectTimeout) + { + if (targetDictionary == null) + throw new Exception("Writing conntect timeout info failed. Dictionary must not be null."); + + return targetDictionary.Union(new Dictionary + { + { CONNECTTIMEOUT, JsonConvert.SerializeObject(connectTimeout, new JavaScriptDateTimeConverter()) } + }).ToDictionary(key => key.Key, value => value.Value); + } + /// Sets the uri of the active copri host. + /// Dictionary holding parameters from JSON. + public static Dictionary SetCopriHostUri(this IDictionary p_oTargetDictionary, string p_strNextActiveUriText) + { + if (p_oTargetDictionary == null) + throw new Exception("Writing copri host uri to dictionary failed. Dictionary must not be null."); + + return p_oTargetDictionary.Union(new Dictionary + { + { typeof(CopriServerUriList).ToString(), JsonConvert.SerializeObject(p_strNextActiveUriText) }, + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Gets the timeout to apply when connecting to bluetooth lock. + /// Dictionary to get information from. + /// Connect timeout value. + public static TimeSpan? GetConnectTimeout(Dictionary p_oSettingsJSON) + { + if (!p_oSettingsJSON.TryGetValue(CONNECTTIMEOUT, out string connectTimeout) + || string.IsNullOrEmpty(connectTimeout)) + { + // File holds no entry. + return null; + } + + return JsonConvert.DeserializeObject(connectTimeout, new JavaScriptDateTimeConverter()); + } + /// Gets the logging level. + /// Dictionary to get logging level from. + /// Logging level + public static Uri GetCopriHostUri(this IDictionary settingsJSON) + { + // Get uri of corpi server. + if (!settingsJSON.TryGetValue(typeof(CopriServerUriList).ToString(), out string uriText) + || string.IsNullOrEmpty(uriText)) + { + // File holds no entry. + return null; + } + + if (uriText.ToUpper().ToUpper().Contains("copri-bike.de".ToUpper())) + return new Uri(CopriServerUriList.SHAREE_DEVEL); + + if (uriText.ToUpper().ToUpper().Contains("copri.eu".ToUpper())) + return new Uri(CopriServerUriList.SHAREE_LIVE); + + return JsonConvert.DeserializeObject(uriText); + } + + /// Sets the version of the app. + /// Dictionary holding parameters from JSON. + public static Dictionary SetAppVersion(this IDictionary p_oTargetDictionary, Version p_strAppVersion) + { + if (p_oTargetDictionary == null) + throw new Exception("Writing copri host uri to dictionary failed. Dictionary must not be null."); + + return p_oTargetDictionary.Union(new Dictionary + { + {APPVERIONKEY , JsonConvert.SerializeObject(p_strAppVersion, new VersionConverter()) }, + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Gets the app versions. + /// Dictionary to get logging level from. + /// Logging level + public static Version GetAppVersion(this IDictionary p_oSettingsJSON) + { + // Get the version of the app which wrote the settings file. + if (!p_oSettingsJSON.TryGetValue(APPVERIONKEY, out string l_oAppVersion) + || string.IsNullOrEmpty(l_oAppVersion)) + { + // File holds no entry. + return null; + } + + return l_oAppVersion.TrimStart().StartsWith("\"") + ? JsonConvert.DeserializeObject(l_oAppVersion, new VersionConverter()) + : Version.Parse(l_oAppVersion); // Format used up to version 3.0.0.115 + } + + + /// Sets whether polling is on or off and the periode if polling is on. + /// Dictionary to write entries to. + public static Dictionary SetPollingParameters(this IDictionary p_oTargetDictionary, PollingParameters p_oPollingParameter) + { + if (p_oTargetDictionary == null) + throw new Exception("Writing polling parameters to dictionary failed. Dictionary must not be null."); + + return p_oTargetDictionary.Union(new Dictionary + { + { $"{typeof(PollingParameters).Name}_{typeof(TimeSpan).Name}", JsonConvert.SerializeObject(p_oPollingParameter.Periode) }, + { $"{typeof(PollingParameters).Name}_{typeof(bool).Name}", JsonConvert.SerializeObject(p_oPollingParameter.IsActivated) }, + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Get whether polling is on or off and the periode if polling is on. + /// Dictionary holding parameters from JSON. + /// Polling parameters. + public static PollingParameters GetPollingParameters(this IDictionary p_oSettingsJSON) + { + // Check if dictionary contains entry for periode. + if (p_oSettingsJSON.TryGetValue($"{typeof(PollingParameters).Name}_{typeof(TimeSpan).Name}", out string l_strPeriode) + && p_oSettingsJSON.TryGetValue($"{typeof(PollingParameters).Name}_{typeof(bool).Name}", out string l_strIsActive) + && !string.IsNullOrEmpty(l_strPeriode) + && !string.IsNullOrEmpty(l_strIsActive)) + { + return new PollingParameters( + JsonConvert.DeserializeObject(l_strPeriode), + JsonConvert.DeserializeObject(l_strIsActive)); + + } + + return null; + } + + /// Saves object to file. + /// Settings to save. + public static void Serialize(string p_strSettingsFileFolder, IDictionary p_SettingsList) + { + // Save settings to file. + var l_oText = JsonConvert.SerializeObject(p_SettingsList, Formatting.Indented); + var l_oFolder = p_strSettingsFileFolder; + System.IO.File.WriteAllText($"{l_oFolder}{System.IO.Path.DirectorySeparatorChar}{SETTINGSFILETITLE}", l_oText); + } + + /// Gets TINK app settings form xml- file. + /// Directory to read settings from. + /// Dictionary of settings. + public static Dictionary Deserialize(string p_strSettingsDirectory) + { + var l_oFileName = $"{p_strSettingsDirectory}{System.IO.Path.DirectorySeparatorChar}{SETTINGSFILETITLE}"; + + if (!System.IO.File.Exists(l_oFileName)) + { + // File is empty. Nothing to read. + return new Dictionary(); ; + } + + var l_oJSONFile = System.IO.File.ReadAllText(l_oFileName); + + if (string.IsNullOrEmpty(l_oJSONFile)) + { + // File is empty. Nothing to read. + return new Dictionary(); + } + + // Load setting file. + return JsonConvert.DeserializeObject>(l_oJSONFile); + } + + /// Gets the logging level. + /// Dictionary to get logging level from. + /// Logging level. + public static LogEventLevel? GetMinimumLoggingLevel( + Dictionary p_oSettingsJSON) + { + // Get logging level. + if (!p_oSettingsJSON.TryGetValue(MINLOGGINGLEVELKEY, out string l_strLevel) + || string.IsNullOrEmpty(l_strLevel)) + { + // File holds no entry. + return null; + } + + return (LogEventLevel)int.Parse(JsonConvert.DeserializeObject(l_strLevel)); + } + + /// Sets the logging level. + /// Dictionary to get logging level from. + public static Dictionary SetMinimumLoggingLevel(this IDictionary p_oTargetDictionary, LogEventLevel p_oLevel) + { + // Set logging level. + if (p_oTargetDictionary == null) + throw new Exception("Writing logging level to dictionary failed. Dictionary must not be null."); + + return p_oTargetDictionary.Union(new Dictionary + { + { MINLOGGINGLEVELKEY, JsonConvert.SerializeObject((int)p_oLevel) } + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Gets the version of app when whats new was shown. + /// Dictionary to get logging level from. + /// Version of the app. + public static Version GetWhatsNew(Dictionary p_oSettingsJSON) + { + // Get logging level. + if (!p_oSettingsJSON.TryGetValue(SHOWWHATSNEWKEY, out string l_strWhatsNewVersion) + || string.IsNullOrEmpty(l_strWhatsNewVersion)) + { + // File holds no entry. + return null; + } + + return JsonConvert.DeserializeObject(l_strWhatsNewVersion, new VersionConverter()); + } + + /// Sets the version of app when whats new was shown. + /// Dictionary to get information from. + public static Dictionary SetWhatsNew(this IDictionary p_oTargetDictionary, Version p_oAppVersion) + { + // Set logging level. + if (p_oTargetDictionary == null) + throw new Exception("Writing WhatsNew info failed. Dictionary must not be null."); + + return p_oTargetDictionary.Union(new Dictionary + { + { SHOWWHATSNEWKEY, JsonConvert.SerializeObject(p_oAppVersion, new VersionConverter()) } + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Gets the expires after value. + /// Dictionary to get expries after value from. + /// Expires after value. + public static TimeSpan? GetExpiresAfter(Dictionary p_oSettingsJSON) + { + if (!p_oSettingsJSON.TryGetValue(EXPIRESAFTER, out string expiresAfter) + || string.IsNullOrEmpty(expiresAfter)) + { + // File holds no entry. + return null; + } + + return JsonConvert.DeserializeObject(expiresAfter, new JavaScriptDateTimeConverter()); + } + + /// Sets the the expiration time. + /// Dictionary to write information to. + public static Dictionary SetExpiresAfter(this IDictionary p_oTargetDictionary, TimeSpan expiresAfter) + { + if (p_oTargetDictionary == null) + throw new Exception("Writing ExpiresAfter info failed. Dictionary must not be null."); + + return p_oTargetDictionary.Union(new Dictionary + { + { EXPIRESAFTER, JsonConvert.SerializeObject(expiresAfter, new JavaScriptDateTimeConverter()) } + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Sets the active lock service name. + /// Dictionary holding parameters from JSON. + public static Dictionary SetActiveLockService(this IDictionary targetDictionary, string activeLockService) + { + if (targetDictionary == null) + throw new Exception("Writing active lock service name to dictionary failed. Dictionary must not be null."); + + return targetDictionary.Union(new Dictionary + { + { typeof(ILocksService).Name, activeLockService }, + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Gets the active lock service name. + /// Dictionary to get logging level from. + /// Active lock service name. + public static string GetActiveLockService(this IDictionary settingsJSON) + { + // Get uri of corpi server. + if (!settingsJSON.TryGetValue(typeof(ILocksService).Name, out string activeLockService) + || string.IsNullOrEmpty(activeLockService)) + { + // File holds no entry. + return null; + } + + if (activeLockService == "TINK.Services.BluetoothLock.BLE.LockItByScanService") + { + // Name of this service was switched. + return typeof(TINK.Services.BluetoothLock.BLE.LockItByScanServicePolling).FullName; + } + + return activeLockService; + } + + /// Sets the active Geolocation service name. + /// Dictionary holding parameters from JSON. + public static Dictionary SetActiveGeolocationService( + this IDictionary targetDictionary, + string activeGeolocationService) + { + if (targetDictionary == null) + throw new Exception("Writing active geolocation service name to dictionary failed. Dictionary must not be null."); + + return targetDictionary.Union(new Dictionary + { + { typeof(IGeolocation).Name, activeGeolocationService }, + }).ToDictionary(key => key.Key, value => value.Value); + } + + /// Gets the active Geolocation service name. + /// Dictionary to get name of geolocation service from. + /// Active lock service name. + public static string GetActiveGeolocationService(this IDictionary settingsJSON) + { + // Get uri of corpi server. + if (!settingsJSON.TryGetValue(typeof(IGeolocation).Name, out string activeGeolocationService) + || string.IsNullOrEmpty(activeGeolocationService)) + { + // File holds no entry. + return null; + } + + return activeGeolocationService; + } + + /// Gets a value indicating whether to center the map to location or not. + /// Dictionary to get value from. + public static bool? GetCenterMapToCurrentLocation(Dictionary settingsJSON) => GetNullableEntry(CENTERMAPTOCURRENTLOCATION, settingsJSON); + + /// Sets a value indicating whether to center the map to location or not. + /// Dictionary to get value from. + public static Dictionary SetCenterMapToCurrentLocation(this IDictionary targetDictionary, bool centerMapToCurrentLocation) + => SetEntry(centerMapToCurrentLocation, CENTERMAPTOCURRENTLOCATION, targetDictionary); + + /// Gets whether to store logging data on SD card or not. + /// Dictionary to get value from. + public static bool? GetLogToExternalFolder(Dictionary settingsJSON) => GetNullableEntry(LOGTOEXTERNALFOLDER, settingsJSON); + + /// Gets full class name of active theme. + /// Dictionary to get value from. + public static string GetActiveTheme(Dictionary settingsJSON) => GetEntry(THEMEKEY, settingsJSON); + + /// Gets a value indicating whether site caching is on or off. + /// Dictionary to get value from. + public static bool? GetIsSiteCachingOn(Dictionary settingsJSON) => GetNullableEntry(ISSITECACHINGON, settingsJSON); + + /// Sets whether to store logging data on SD card or not. + /// Dictionary to get value from. + public static Dictionary SetLogToExternalFolder(this IDictionary targetDictionary, bool useSdCard) => SetEntry(useSdCard, LOGTOEXTERNALFOLDER, targetDictionary); + + /// Sets active theme. + /// Dictionary to set value to. + public static Dictionary SetActiveTheme(this IDictionary targetDictionary, string theme) => SetEntry(theme, THEMEKEY, targetDictionary); + + /// Sets whether site caching is on or off. + /// Dictionary to get value from. + public static Dictionary SetIsSiteCachingOn(this IDictionary targetDictionary, bool useSdCard) => SetEntry(useSdCard, ISSITECACHINGON, targetDictionary); + + /// Gets the map page filter. + /// Settings objet to load from. + public static IGroupFilterMapPage GetGroupFilterMapPage(this IDictionary settings) + { + var l_oKeyName = "FilterCollection_MapPageFilter"; + if (settings == null || !settings.ContainsKey(l_oKeyName)) + { + return null; + } + + return new GroupFilterMapPage(JsonConvert.DeserializeObject>(settings[l_oKeyName])); + } + + public static IDictionary SetGroupFilterMapPage( + this IDictionary settings, + IDictionary p_oFilterCollection) + { + if (settings == null + || p_oFilterCollection == null + || p_oFilterCollection.Count < 1) + { + return settings; + } + + settings["FilterCollection_MapPageFilter"] = JsonConvert.SerializeObject(p_oFilterCollection); + return settings; + } + + /// Gets the settings filter. + /// Settings objet to load from. + public static IGroupFilterSettings GetGoupFilterSettings(this IDictionary settings) + { + var l_oKeyName = "FilterCollection"; + if (settings == null || !settings.ContainsKey(l_oKeyName)) + { + return null; + } + + var legacyFilterCollection = new GroupFilterSettings(JsonConvert.DeserializeObject>(settings[l_oKeyName])); + + // Process legacy entries. + var updatedFilterCollection = legacyFilterCollection.Where(x => x.Key.ToUpper() != "TINK.SMS" && x.Key.ToUpper() != "TINK.COPRI").ToDictionary(x => x.Key, x => x.Value); + if (legacyFilterCollection.Count() <= updatedFilterCollection.Count()) + return legacyFilterCollection; + var list = updatedFilterCollection.ToList(); + + updatedFilterCollection.Add( + FilterHelper.FILTERTINKGENERAL, + legacyFilterCollection.Any(x => x.Key.ToUpper() == "TINK.COPRI") ? legacyFilterCollection.FirstOrDefault(x => x.Key.ToUpper() == "TINK.COPRI").Value : FilterState.Off); + + return new GroupFilterSettings(updatedFilterCollection); + } + + public static IDictionary SetGroupFilterSettings( + this IDictionary settings, + IDictionary p_oFilterCollection) + { + if (settings == null + || p_oFilterCollection == null + || p_oFilterCollection.Count < 1) + { + return settings; + } + + settings["FilterCollection"] = JsonConvert.SerializeObject(p_oFilterCollection); + return settings; + } + } +} diff --git a/TINKLib/Model/Settings/PollingParameters.cs b/TINKLib/Model/Settings/PollingParameters.cs new file mode 100644 index 0000000..04967d1 --- /dev/null +++ b/TINKLib/Model/Settings/PollingParameters.cs @@ -0,0 +1,100 @@ +using System; + +namespace TINK.Settings +{ + /// Holds polling parameters. + public sealed class PollingParameters : IEquatable + { + /// Holds default polling parameters. + public static PollingParameters Default { get; } = new PollingParameters( + new TimeSpan(0, 0, 0, 10 /*secs*/, 0),// Default polling interval. + true); + + /// Holds polling parameters which represent polling off (empty polling object). + public static PollingParameters NoPolling { get; } = new PollingParameters( + TimeSpan.MaxValue,// Very long intervall which should never be used because polling IsActivated property is set to false. + false); + + /// Constructs a polling parameter object. + /// Polling periode. + /// True if polling is activated. + public PollingParameters(TimeSpan p_oPeriode, bool p_bIsActivated) + { + Periode = p_oPeriode; // Can not be null because is a struct. + IsActivated = p_bIsActivated; + } + + /// Holds the polling periode. + public TimeSpan Periode { get; } + + /// Holds value whether polling is activated or not. + public bool IsActivated { get; } + + /// Checks equallity. + /// Object to compare with. + /// True if objects are equal. + public bool Equals(PollingParameters other) + { + return this == other; + } + + /// Checks equallity. + /// Object to compare with. + /// True if objects are equal. + public override bool Equals(object obj) + { + var l_oParameters = obj as PollingParameters; + if (l_oParameters == null) + { + return false; + } + + return this == l_oParameters; + } + + /// Gets the has code of object. + /// + public override int GetHashCode() + { + return Periode.GetHashCode() ^ IsActivated.GetHashCode(); + } + + /// Gets the string representation of the object. + /// + public override string ToString() + { + return $"Polling is on={IsActivated}, polling interval={Periode.TotalSeconds}[sec]."; + } + + /// Defines equality of thwo polling parameter objects. + /// First object to compare. + /// Second object to compare. + /// True if objects are equal + public static bool operator ==(PollingParameters p_oSource, PollingParameters p_oTarget) + { + if (p_oSource is null && p_oTarget is null) + { + // Both object are null + return true; + } + + if (p_oSource is null ^ p_oTarget is null) + { + // Only one object is null. + return false; + } + + return p_oSource.Periode == p_oTarget.Periode + && p_oSource.IsActivated == p_oTarget.IsActivated; + } + + /// Defines equality of thwo polling parameter objects. + /// First object to compare. + /// Second object to compare. + /// True if objects are equal + public static bool operator !=(PollingParameters p_oSource, PollingParameters p_oTarget) + { + return (p_oSource == p_oTarget) == false; + } + } +} diff --git a/TINKLib/Model/Settings/Settings.cs b/TINKLib/Model/Settings/Settings.cs new file mode 100644 index 0000000..6c7a244 --- /dev/null +++ b/TINKLib/Model/Settings/Settings.cs @@ -0,0 +1,104 @@ +using Serilog.Events; +using System; +using TINK.Model.Services.Geolocation; +using TINK.Services.BluetoothLock; +using TINK.Services.CopriApi.ServerUris; +using TINK.Settings; +using TINK.ViewModel.Map; +using TINK.ViewModel.Settings; + + +namespace TINK.Model.Settings +{ + /// Holds settings which are persisted. + public class Settings + { + public const LogEventLevel DEFAULTLOGGINLEVEL = LogEventLevel.Error; + + // Default value of the expires after entry. Controls the expiration time of the cache values. + private TimeSpan DEFAULTEXPIRESAFTER = TimeSpan.FromSeconds(1); + + /// Constructs settings object. + /// filter which is applied on the map view. Either TINK or Konrad stations are displayed. + /// + /// + /// + /// Minimum logging level to be applied. + /// Holds the expires after value. + /// Gets the name of the lock service to use. + /// Timeout to apply when connecting to bluetooth lock + /// Full class name of active app theme. + public Settings( + IGroupFilterMapPage groupFilterMapPage = null, + IGroupFilterSettings groupFilterSettings = null, + Uri activeUri = null, + PollingParameters pollingParameters = null, + LogEventLevel? minimumLogEventLevel = null, + TimeSpan? expiresAfter = null, + string activeLockService = null, + TimeSpan? connectTimeout = null, + string activeGeolocationService = null, + bool? centerMapToCurrentLocation = null, + bool? logToExternalFolder = null, + bool? isSiteCachingOn = null, + string activeTheme = null) + { + GroupFilterMapPage = groupFilterMapPage ?? GroupFilterHelper.GetMapPageFilterDefaults; + GroupFilterSettings = groupFilterSettings ?? GroupFilterHelper.GetSettingsFilterDefaults; + ActiveUri = GetActiveUri(activeUri); + PollingParameters = pollingParameters ?? PollingParameters.Default; + MinimumLogEventLevel = minimumLogEventLevel ?? DEFAULTLOGGINLEVEL; + ExpiresAfter = expiresAfter ?? DEFAULTEXPIRESAFTER; + ActiveLockService = activeLockService ?? LocksServicesContainerMutable.DefaultLocksservice; + ConnectTimeout = connectTimeout ?? new TimeSpan(0, 0, TimeOutProvider.DEFAULT_BLUETOOTHCONNECT_TIMEOUTSECONDS); // Try one sec. to connect. + ActiveGeolocationService = activeGeolocationService ?? typeof(LastKnownGeolocationService).Name; + CenterMapToCurrentLocation = centerMapToCurrentLocation ?? GetCenterMapToCurrentLocation(activeUri); + LogToExternalFolder = logToExternalFolder ?? false; + IsSiteCachingOn = isSiteCachingOn ?? true; + ActiveTheme = activeTheme ?? typeof(Themes.ShareeBike).FullName; + } + + /// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. + public IGroupFilterMapPage GroupFilterMapPage { get; } + + /// Holds the filters loaded from settings. + public IGroupFilterSettings GroupFilterSettings { get; } + + /// Holds the uri to connect to. + public Uri ActiveUri { get; } + + /// Holds the polling parameters. + public PollingParameters PollingParameters { get; } + + /// Gets the minimum logging level. + public LogEventLevel MinimumLogEventLevel { get; } + + /// Gets the expires after value. + public TimeSpan ExpiresAfter { get; } + + /// Gets the lock service to use. + public string ActiveLockService { get; private set; } + + /// Timeout to apply when connecting to bluetooth lock. + public TimeSpan ConnectTimeout { get; } + + /// Gets the geolocation service to use. + public string ActiveGeolocationService { get; } + + public bool CenterMapToCurrentLocation { get; } + + public bool LogToExternalFolder { get; } + + public bool IsSiteCachingOn { get; } + + public string ActiveTheme { get; } + + public static Uri GetActiveUri(Uri activeUri) => activeUri ?? Services.CopriApi.ServerUris.CopriServerUriList.DefaultActiveUri; + + public static bool GetCenterMapToCurrentLocation(Uri activeUri) + { + // TINK does not require acess to current location. Deactivate center map to current location for this reason. + return !GetActiveUri(activeUri).Host.GetIsCopri(); + } + } +} diff --git a/TINKLib/Model/State/BaseState.cs b/TINKLib/Model/State/BaseState.cs new file mode 100644 index 0000000..d9afc90 --- /dev/null +++ b/TINKLib/Model/State/BaseState.cs @@ -0,0 +1,20 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.State +{ + /// + /// Base type for serialization purposes. + /// + [DataContract] + [KnownType(typeof(StateAvailableInfo))] + [KnownType(typeof(StateRequestedInfo))] + [KnownType(typeof(StateOccupiedInfo))] + public abstract class BaseState + { + /// Constructor for Json serialization. + /// State value. + protected BaseState(InUseStateEnum p_eValue) {} + + public abstract InUseStateEnum Value { get; } + } +} diff --git a/TINKLib/Model/State/IBaseState.cs b/TINKLib/Model/State/IBaseState.cs new file mode 100644 index 0000000..a2659cf --- /dev/null +++ b/TINKLib/Model/State/IBaseState.cs @@ -0,0 +1,11 @@ + +namespace TINK.Model.State +{ + /// + /// Base state information. + /// + public interface IBaseState + { + InUseStateEnum Value { get; } + } +} diff --git a/TINKLib/Model/State/INotAvailableState.cs b/TINKLib/Model/State/INotAvailableState.cs new file mode 100644 index 0000000..d453cb2 --- /dev/null +++ b/TINKLib/Model/State/INotAvailableState.cs @@ -0,0 +1,14 @@ +using System; + +namespace TINK.Model.State +{ + /// + /// State of bikes which are either reserved or booked. + /// + public interface INotAvailableState : IBaseState + { + DateTime From { get; } + string MailAddress { get; } + string Code { get; } + } +} diff --git a/TINKLib/Model/State/IStateInfo.cs b/TINKLib/Model/State/IStateInfo.cs new file mode 100644 index 0000000..b1c6fe0 --- /dev/null +++ b/TINKLib/Model/State/IStateInfo.cs @@ -0,0 +1,16 @@ +using System; + +namespace TINK.Model.State +{ + /// + /// Interface to access informations about bike information. + /// + public interface IStateInfo : IBaseState + { + string MailAddress { get; } + + DateTime? From { get; } + + string Code { get; } + } +} diff --git a/TINKLib/Model/State/IStateInfoMutable.cs b/TINKLib/Model/State/IStateInfoMutable.cs new file mode 100644 index 0000000..0bfa0d0 --- /dev/null +++ b/TINKLib/Model/State/IStateInfoMutable.cs @@ -0,0 +1,23 @@ +using System; + +namespace TINK.Model.State +{ + public interface IStateInfoMutable + { + InUseStateEnum Value { get; } + + /// Updates state from webserver. + /// State of the bike. + /// Date time when bike was reserved/ booked. + /// Lenght of time span for which bike remains booked. + /// Mailaddress of the one which reserved/ booked. + /// Booking code if bike is booked or reserved. + /// Controls whether notify property changed events are fired or not. + void Load( + InUseStateEnum p_oState, + DateTime? p_oFrom = null, + string p_strMailAddress = null, + string p_strCode = null, + Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel = Bikes.Bike.BC.NotifyPropertyChangedLevel.All); + } +} diff --git a/TINKLib/Model/State/StateAvailableInfo.cs b/TINKLib/Model/State/StateAvailableInfo.cs new file mode 100644 index 0000000..ad97fdd --- /dev/null +++ b/TINKLib/Model/State/StateAvailableInfo.cs @@ -0,0 +1,39 @@ +using Newtonsoft.Json; +using System; +using System.Runtime.Serialization; + +namespace TINK.Model.State +{ + /// + /// Represents the state available. + /// + [DataContract] + public sealed class StateAvailableInfo : BaseState, IBaseState + { + /// + /// Constructs state info object representing state available. + /// + public StateAvailableInfo() : base(InUseStateEnum.Disposable) + { + } + + /// Constructor for Json serialization. + /// Unused value. + [JsonConstructor] + private StateAvailableInfo (InUseStateEnum p_eValue) : base(InUseStateEnum.Disposable) + { + } + + /// + /// Gets the info that state is disposable. + /// Setter exists only for serialization purposes. + /// + public override InUseStateEnum Value + { + get + { + return InUseStateEnum.Disposable; + } + } + } +} diff --git a/TINKLib/Model/State/StateInfo.cs b/TINKLib/Model/State/StateInfo.cs new file mode 100644 index 0000000..7843611 --- /dev/null +++ b/TINKLib/Model/State/StateInfo.cs @@ -0,0 +1,161 @@ +using System; + +namespace TINK.Model.State +{ + /// + /// Types of rent states + /// + public enum InUseStateEnum + { + /// + /// Bike is not in use. Corresponding COPRI state is "available". + /// + Disposable, + + /// + /// Bike is reserved. Corresponding COPRI state is "requested". + /// + Reserved, + + /// + /// Bike is booked. Corresponding COPRI statie is "occupied". + /// + Booked + } + + /// + /// Manages the state of a bike. + /// + public class StateInfo : IStateInfo + { + // Holds the current disposable state value + private readonly BaseState m_oInUseState; + + /// + /// Constructs a state info object when state is available. + /// + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + public StateInfo() + { + m_oInUseState = new StateAvailableInfo(); + } + + /// + /// Constructs a state info object when state is requested. + /// + /// Date time when bike was requested + /// Mail address of user which requested bike. + /// Booking code. + /// Date time provider to calculate reaining time. + public StateInfo( + Func p_oDateTimeNowProvider, + DateTime p_oRequestedAt, + string p_strMailAddress, + string p_strCode) + { + // Todo: Handle p_oFrom == null here. + // Todo: Handle p_oDuration == null here. + m_oInUseState = new StateRequestedInfo( + p_oDateTimeNowProvider ?? (() => DateTime.Now), + p_oRequestedAt, + p_strMailAddress, + p_strCode); + } + + /// + /// Constructs a state info object when state is booked. + /// + /// Date time when bike was booked + /// Mail address of user which booked bike. + /// Booking code. + public StateInfo( + DateTime p_oBookedAt, + string p_strMailAddress, + string p_strCode) + { + // Todo: Handle p_oFrom == null here. + // Todo: Clearify question: What to do if code changes form one value to another? This should never happen. + // Todo: Clearify question: What to do if from time changes form one value to another? This should never happen. + m_oInUseState = new StateOccupiedInfo( + p_oBookedAt, + p_strMailAddress, + p_strCode); + } + + /// + /// Gets the state value of object. + /// + public InUseStateEnum Value + { + get { return m_oInUseState.Value; } + } + + /// + /// Member for serialization purposes. + /// + internal BaseState StateInfoObject + { + get { return m_oInUseState; } + } + /// Transforms object to string. + /// + /// + public new string ToString() + { + return m_oInUseState.Value.ToString("g"); + } + + /// + /// Date of request/ bookeing action. + /// + public DateTime? From + { + get + { + var l_oNotDisposableInfo = m_oInUseState as INotAvailableState; + return l_oNotDisposableInfo != null ? l_oNotDisposableInfo.From : (DateTime?)null; + } + } + + /// + /// Mail address. + /// + public string MailAddress + { + get + { + var l_oNotDisposableInfo = m_oInUseState as INotAvailableState; + return l_oNotDisposableInfo?.MailAddress; + } + } + + /// + /// Reservation code. + /// + public string Code + { + get + { + var l_oNotDisposableInfo = m_oInUseState as INotAvailableState; + return l_oNotDisposableInfo?.Code; + } + } + + /// + /// Tries update + /// + /// True if reservation span has not exeeded and state remains reserved, false otherwise. + /// Implement logging of time stamps. + public bool GetIsStillReserved(out TimeSpan? p_oRemainingTime) + { + var l_oReservedInfo = m_oInUseState as StateRequestedInfo; + if (l_oReservedInfo == null) + { + p_oRemainingTime = null; + return false; + } + + return l_oReservedInfo.GetIsStillReserved(out p_oRemainingTime); + } + } +} diff --git a/TINKLib/Model/State/StateInfoMutable.cs b/TINKLib/Model/State/StateInfoMutable.cs new file mode 100644 index 0000000..e193a52 --- /dev/null +++ b/TINKLib/Model/State/StateInfoMutable.cs @@ -0,0 +1,299 @@ +using System; + +namespace TINK.Model.State +{ + using System.ComponentModel; + using System.Runtime.Serialization; + + /// + /// Manges the state of a bike. + /// + [DataContract] + public class StateInfoMutable : INotifyPropertyChanged, IStateInfoMutable + { + /// + /// Provider for current date time to calculate remainig time on demand for state of type reserved. + /// + private readonly Func m_oDateTimeNowProvider; + + // Holds the current disposable state value + private StateInfo m_oStateInfo; + + /// + /// Backs up remaining time of child object. + /// + private TimeSpan? m_oRemainingTime = null; + + /// Notifies clients about state changes. + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// Constructs a state object from source. + /// + /// State info to load from. + public StateInfoMutable( + Func p_oDateTimeNowProvider = null, + IStateInfo p_oState = null) + { + // Back up date time provider to be able to pass this to requested- object if state changes to requested. + m_oDateTimeNowProvider = p_oDateTimeNowProvider != null + ? p_oDateTimeNowProvider + : () => DateTime.Now; + + m_oStateInfo = Create(p_oState, p_oDateTimeNowProvider); + } + + /// + /// Loads state from immutable source. + /// + /// State to load from. + public void Load(IStateInfo p_oState) + { + if (p_oState == null) + { + throw new ArgumentException("Can not load state info, object must not be null."); + } + + // Back up last state value and remaining time value + // to be able to check whether an event has to be fired or not. + var l_oLastState = Value; + var l_oLastRemainingTime = m_oRemainingTime; + + // Create new state info object from source. + m_oStateInfo = Create(p_oState, m_oDateTimeNowProvider); + + // Update remaining time value. + m_oStateInfo.GetIsStillReserved(out m_oRemainingTime); + + if (l_oLastState == m_oStateInfo.Value + && l_oLastRemainingTime == m_oRemainingTime) + { + return; + } + + // State has changed, notify clients. + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); + } + + /// + /// Creates a state info object. + /// + /// State to load from. + private static StateInfo Create( + IStateInfo p_oState, + Func p_oDateTimeNowProvider) + { + switch (p_oState != null ? p_oState.Value : InUseStateEnum.Disposable) + { + case InUseStateEnum.Disposable: + return new StateInfo(); + + case InUseStateEnum.Reserved: + // Todo: Handle p_oFrom == null here. + // Todo: Handle p_oDuration == null here. + return new StateInfo( + p_oDateTimeNowProvider, + p_oState.From.Value, + p_oState.MailAddress, + p_oState.Code); + + + case InUseStateEnum.Booked: + // Todo: Handle p_oFrom == null here. + // Todo: Clearify question: What to do if code changes form one value to another? This should never happen. + // Todo: Clearify question: What to do if from time changes form one value to another? This should never happen. + return new StateInfo( + p_oState.From.Value, + p_oState.MailAddress, + p_oState.Code); + + default: + // Todo: New state Busy has to be defined. + throw new Exception(string.Format("Can not create new state info object. Unknown state {0} detected.", p_oState.Value)); + } + } + + /// + /// Gets the state value of object. + /// + public InUseStateEnum Value + { + get { return m_oStateInfo.Value; } + } + + /// + /// Member for serialization purposes. + /// + [DataMember] + private BaseState StateInfoObject + { + get { return m_oStateInfo.StateInfoObject; } + set + { + var l_oStateOccupied = value as StateOccupiedInfo; + if (l_oStateOccupied != null) + { + m_oStateInfo = new StateInfo(l_oStateOccupied.From, l_oStateOccupied.MailAddress, l_oStateOccupied.Code); + return; + } + + var l_oStateRequested = value as StateRequestedInfo; + if (l_oStateRequested != null) + { + m_oStateInfo = new StateInfo(m_oDateTimeNowProvider, l_oStateRequested.From, l_oStateRequested.MailAddress, l_oStateRequested.Code); + return; + } + + m_oStateInfo = new StateInfo(); + + } + } + /// Transforms object to string. + /// + /// + public new string ToString() + { + return m_oStateInfo.ToString(); + } + + /// + /// Checks and updates state if required. + /// + /// Value indicating wheter state has changed + public void UpdateOnTimeElapsed() + { + switch (m_oStateInfo.Value ) + { + // State is disposable or booked. No need to update "OnTimeElapsed" + case InUseStateEnum.Disposable: + case InUseStateEnum.Booked: + return; + } + + // Check if maximum reserved time has elapsed. + if (!m_oStateInfo.GetIsStillReserved(out m_oRemainingTime)) + { + // Time has elapsed, switch state to disposable and notfiy client + m_oStateInfo = new StateInfo(); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); + return; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(RemainingTime))); + } + + /// Updates state from webserver. + /// State of the bike. + /// Date time when bike was reserved/ booked. + /// Lenght of time span for which bike remains booked. + /// Mailaddress of the one which reserved/ booked. + /// Booking code if bike is booked or reserved. + /// Controls whether notify property changed events are fired or not. + public void Load( + InUseStateEnum p_oState, + DateTime? p_oFrom = null, + string p_strMailAddress = null, + string p_strCode = null, + Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel = Bikes.Bike.BC.NotifyPropertyChangedLevel.All) + { + var l_oLastState = m_oStateInfo.Value; + + switch (p_oState) + { + case InUseStateEnum.Disposable: + m_oStateInfo = new StateInfo(); + + // Set value to null. Otherwise potentially obsolete value will be taken remaining time. + m_oRemainingTime = null; + break; + + case InUseStateEnum.Reserved: + // Todo: Handle p_oFrom == null here. + // Todo: Handle p_oDuration == null here. + m_oStateInfo = new StateInfo( + m_oDateTimeNowProvider, + p_oFrom.Value, + p_strMailAddress, + p_strCode); + + // Set value to null. Otherwise potentially obsolete value will be taken remaining time. + m_oRemainingTime = null; + break; + + case InUseStateEnum.Booked: + // Todo: Handle p_oFrom == null here. + // Todo: Clearify question: What to do if code changes form one value to another? This should never happen. + // Todo: Clearify question: What to do if from time changes form one value to another? This should never happen. + m_oStateInfo = new StateInfo( + p_oFrom.Value, + p_strMailAddress, + p_strCode); + + // Set value to null. Otherwise potentially obsolete value will be taken remaining time. + m_oRemainingTime = null; + break; + + default: + // Todo: New state Busy has to be defined. + break; + } + + if (l_oLastState != m_oStateInfo.Value + && notifyLevel == Bikes.Bike.BC.NotifyPropertyChangedLevel.All) + { + // State has changed, notify clients. + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(State))); + } + } + + /// + /// If bike is reserved time raimaining while bike stays reserved, null otherwise. + /// + public TimeSpan? RemainingTime + { + get + { + switch (m_oStateInfo.Value) + { + // State is either available or occupied. + case InUseStateEnum.Disposable: + case InUseStateEnum.Booked: + return null; + } + + if (m_oRemainingTime.HasValue == false) + { + // Value was not yet querried. + // Do querry before returning object. + m_oStateInfo.GetIsStillReserved(out m_oRemainingTime); + } + + return m_oRemainingTime; + } + } + + public DateTime? From + { + get + { + return m_oStateInfo.From; + } + } + + public string MailAddress + { + get + { + return m_oStateInfo.MailAddress; + } + } + + public string Code + { + get + { + return m_oStateInfo.Code; + } + } + } +} diff --git a/TINKLib/Model/State/StateOccupiedInfo.cs b/TINKLib/Model/State/StateOccupiedInfo.cs new file mode 100644 index 0000000..c62acd0 --- /dev/null +++ b/TINKLib/Model/State/StateOccupiedInfo.cs @@ -0,0 +1,80 @@ +using Newtonsoft.Json; +using System; +using System.Runtime.Serialization; + +namespace TINK.Model.State +{ + /// + /// Manages state booked. + /// + [DataContract] + public sealed class StateOccupiedInfo : BaseState, IBaseState, INotAvailableState + { + /// + /// Prevents an invalid instance to be created. + /// + private StateOccupiedInfo() : base(InUseStateEnum.Booked) + { + } + + /// + /// Constructs an object holding booked state info. + /// + /// Date time when bike was booked + /// + /// + public StateOccupiedInfo( + DateTime p_oFrom, + string p_strMailAddress, + string p_strCode) : base(InUseStateEnum.Booked) + { + From = p_oFrom; + MailAddress = p_strMailAddress; + Code = p_strCode; + } + + /// Constructor for Json serialization. + /// Unused value. + /// Date time when bike was booked + /// + /// + [JsonConstructor] + private StateOccupiedInfo( + InUseStateEnum Value, + DateTime From, + string MailAddress, + string Code) : this(From, MailAddress, Code) + { + } + + /// + /// Gets the info that state is reserved. + /// Setter exists only for serialization purposes. + /// + public override InUseStateEnum Value + { + get + { + return InUseStateEnum.Booked; + } + } + + /// + /// Prevents an invalid instance to be created. + /// + [DataMember] + public DateTime From { get; } + + /// + /// Mail address of user who bookec the bike. + /// + [DataMember] + public string MailAddress { get; } + + /// + /// Booking code. + /// + [DataMember] + public string Code { get; } + } +} diff --git a/TINKLib/Model/State/StateRequestedInfo.cs b/TINKLib/Model/State/StateRequestedInfo.cs new file mode 100644 index 0000000..0efff7e --- /dev/null +++ b/TINKLib/Model/State/StateRequestedInfo.cs @@ -0,0 +1,114 @@ +using Newtonsoft.Json; +using System; +using System.Runtime.Serialization; + +namespace TINK.Model.State +{ + /// + /// Manages state reserved. + /// + [DataContract] + public sealed class StateRequestedInfo : BaseState, IBaseState, INotAvailableState + { + // Maximum time while reserving request is kept. + public static readonly TimeSpan MaximumReserveTime = new TimeSpan(0, 15, 0); // 15 mins + + // Reference to date time provider. + private Func m_oDateTimeNowProvider; + + /// + /// Prevents an invalid instance to be created. + /// Used by serializer only. + /// + private StateRequestedInfo() : base(InUseStateEnum.Reserved) + { + // Is called in context of JSON deserialization. + m_oDateTimeNowProvider = () => DateTime.Now; + } + + /// + /// Reservation performed with other device/ before start of app. + /// Date time info when bike was reserved has been received from webserver. + /// + /// Time span which holds duration how long bike still will be reserved. + [JsonConstructor] + private StateRequestedInfo( + InUseStateEnum Value, + DateTime From, + string MailAddress, + string Code) : this(() => DateTime.Now, From, MailAddress, Code) + { + } + + /// + /// Reservation performed with other device/ before start of app. + /// Date time info when bike was reserved has been received from webserver. + /// + /// + /// Used to to provide current date time information for potential calls of . + /// Not used to calculate remaining time because this duration whould always be shorter as the one received from webserver. + /// + /// Time span which holds duration how long bike still will be reserved. + public StateRequestedInfo( + Func p_oDateTimeNowProvider, + DateTime p_oFrom, + string p_strMailAddress, + string p_strCode) : base(InUseStateEnum.Reserved) + { + m_oDateTimeNowProvider = p_oDateTimeNowProvider ?? (() => DateTime.Now); + From = p_oFrom; + MailAddress = p_strMailAddress; + Code = p_strCode; + } + + /// + /// Tries update + /// + /// True if reservation span has not exeeded and state remains reserved, false otherwise. + /// Implement logging of time stamps. + public bool GetIsStillReserved(out TimeSpan? p_oRemainingTime) + { + var l_oTimeReserved = m_oDateTimeNowProvider().Subtract(From); + if (l_oTimeReserved > MaximumReserveTime) + + { + // Reservation has elapsed. To not update remaining time. + p_oRemainingTime = null; + return false; + } + + p_oRemainingTime = MaximumReserveTime - l_oTimeReserved; + return true; + } + + /// + /// State reserved. + /// Setter exists only for serialization purposes. + /// + public override InUseStateEnum Value + { + get + { + return InUseStateEnum.Reserved; + } + } + + /// + /// Date time when bike was reserved. + /// + [DataMember] + public DateTime From { get; } + + /// + /// Mail address of user who reserved the bike. + /// + [DataMember] + public string MailAddress { get; } + + /// + /// Booking code. + /// + [DataMember] + public string Code { get; } + } +} diff --git a/TINKLib/Model/Station/IStation.cs b/TINKLib/Model/Station/IStation.cs new file mode 100644 index 0000000..05cda1d --- /dev/null +++ b/TINKLib/Model/Station/IStation.cs @@ -0,0 +1,19 @@ +using System.Collections.Generic; + +namespace TINK.Model.Station +{ + public interface IStation + { + /// Holds the unique id of the station.c + int Id { get; } + + /// Holds the group to which the station belongs. + IEnumerable Group { get; } + + /// Gets the name of the station. + string StationName { get; } + + /// Holds the gps- position of the station. + Position Position { get; } + } +} diff --git a/TINKLib/Model/Station/NullStation.cs b/TINKLib/Model/Station/NullStation.cs new file mode 100644 index 0000000..f782971 --- /dev/null +++ b/TINKLib/Model/Station/NullStation.cs @@ -0,0 +1,20 @@ +using System.Collections.Generic; + +namespace TINK.Model.Station +{ + /// Holds object representing null station. + public class NullStation : IStation + { + /// Holds the unique id of the station.c + public int Id => -1; + + /// Holds the group to which the station belongs. + public IEnumerable Group => new List(); + + /// Gets the name of the station. + public string StationName => string.Empty; + + /// Holds the gps- position of the station. + public Position Position => new Position(double.NaN, double.NaN); + } +} diff --git a/TINKLib/Model/Station/Position.cs b/TINKLib/Model/Station/Position.cs new file mode 100644 index 0000000..1728bfe --- /dev/null +++ b/TINKLib/Model/Station/Position.cs @@ -0,0 +1,49 @@ +using System; + +namespace TINK.Model.Station +{ + public class Position + { + private const double PRECISSION_LATITUDE_LONGITUDE = 0.000000000000001; + + public Position() + { + } + + public Position(double p_dLatitude, double p_dLongitude) + { + Latitude = p_dLatitude; + Longitude = p_dLongitude; + } + + public double Latitude { get; private set; } + + public double Longitude { get; private set; } + + /// + /// Compares position with a target position. + /// + /// Target position to compare with. + /// True if positions are equal. + public override bool Equals(object p_oTarget) + { + var l_oTarget = p_oTarget as Position; + if (l_oTarget is null) + { + return false; + } + + return Math.Abs(Latitude - l_oTarget.Latitude) < PRECISSION_LATITUDE_LONGITUDE + && Math.Abs(Longitude - l_oTarget.Longitude) < PRECISSION_LATITUDE_LONGITUDE; + + } + + public override int GetHashCode() + { + var hashCode = -1416534245; + hashCode = hashCode * -1521134295 + Latitude.GetHashCode(); + hashCode = hashCode * -1521134295 + Longitude.GetHashCode(); + return hashCode; + } + } +} diff --git a/TINKLib/Model/Station/Station.cs b/TINKLib/Model/Station/Station.cs new file mode 100644 index 0000000..f35e595 --- /dev/null +++ b/TINKLib/Model/Station/Station.cs @@ -0,0 +1,38 @@ +using System; +using System.Collections.Generic; + +namespace TINK.Model.Station +{ + /// Holds station info. + public class Station : IStation + { + /// Constructs a station object. + /// Id of the station. + /// Group (TINK, Konrad) to which station is related. + /// GPS- position of the station. + /// Name of the station. + public Station( + int p_iId, + IEnumerable p_oGroup, + Position p_oPosition, + string p_strStationName = "") + { + Id = p_iId; + Group = p_oGroup ?? throw new ArgumentException("Can not construct station object. Group of stations must not be null."); + Position = p_oPosition; + StationName = p_strStationName ?? string.Empty; + } + + /// Holds the unique id of the station.c + public int Id { get; } + + /// Holds the group to which the station belongs. + public IEnumerable Group { get; } + + /// Gets the name of the station. + public string StationName { get; } + + /// Holds the gps- position of the station. + public Position Position { get; } + } +} diff --git a/TINKLib/Model/Station/StationCollection.cs b/TINKLib/Model/Station/StationCollection.cs new file mode 100644 index 0000000..c32f7f5 --- /dev/null +++ b/TINKLib/Model/Station/StationCollection.cs @@ -0,0 +1,97 @@ +using System; +using System.Collections; +using System.Collections.Generic; + +namespace TINK.Model.Station +{ + public class StationDictionary : IEnumerable + { + /// Holds the list of stations. + private readonly IDictionary m_oStationDictionary; + + /// Count of stations. + public int Count { get { return m_oStationDictionary.Count; } } + + public Version CopriVersion { get; } + + /// Constructs a station dictionary object. + /// Version of copri- service. + public StationDictionary(Version p_oVersion = null, IDictionary p_oStations = null) + { + m_oStationDictionary = p_oStations ?? new Dictionary(); + + CopriVersion = p_oVersion != null + ? new Version(p_oVersion.Major, p_oVersion.Minor, p_oVersion.Revision, p_oVersion.Build) + : new Version(0, 0, 0, 0); + } + + public IEnumerator GetEnumerator() + { + return m_oStationDictionary.Values.GetEnumerator(); + } + + /// + /// Deteermines whether a station by given key exists. + /// + /// Key to check. + /// True if station exists. + public bool ContainsKey(int p_strKey) + { + return m_oStationDictionary.ContainsKey(p_strKey); + } + + /// + /// Remove a station by station id. + /// + /// + public void RemoveById(int p_iId) + { + if (!m_oStationDictionary.ContainsKey(p_iId)) + { + // Nothing to do if there is no station with given name. + return; + } + + m_oStationDictionary.Remove(p_iId); + } + + /// + /// Remove a station by station name. + /// + /// + public Station GetById(int p_iId) + { + if (!m_oStationDictionary.ContainsKey(p_iId)) + { + // Nothing to do if there is no station with given name. + return null; + } + + return m_oStationDictionary[p_iId]; + } + + /// + /// Adds a station to dictionary of stations. + /// + /// + public void Add(Station p_oStation) + { + if (p_oStation == null) + { + throw new ArgumentException("Can not add empty station to collection of stations."); + } + + if (m_oStationDictionary.ContainsKey(p_oStation.Id)) + { + throw new ArgumentException(string.Format("Can not add station {0} to collection of stations. A station with given name already exists.", p_oStation.Id)); + } + + m_oStationDictionary.Add(p_oStation.Id, p_oStation); + } + + IEnumerator IEnumerable.GetEnumerator() + { + return m_oStationDictionary.Values.GetEnumerator(); + } + } +} diff --git a/TINKLib/Model/TinkApp.cs b/TINKLib/Model/TinkApp.cs new file mode 100644 index 0000000..ef9cfaf --- /dev/null +++ b/TINKLib/Model/TinkApp.cs @@ -0,0 +1,423 @@ +using System; +using System.Collections.Generic; +using System.Runtime.Serialization; +using TINK.Model.Connector; +using TINK.Model.Device; +using TINK.Settings; +using TINK.Model.User.Account; +using TINK.Model.Settings; +using TINK.Model.Logging; +using Serilog.Events; +using Serilog.Core; +using Serilog; +using Plugin.Connectivity; +using System.Threading; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.Geolocation; +using TINK.Model.Services.CopriApi.ServerUris; +using Plugin.Permissions.Abstractions; +using TINK.Services.BluetoothLock.Crypto; +using TINK.ViewModel.Map; +using TINK.ViewModel.Settings; +using TINK.Services; +using TINK.Services.BluetoothLock.BLE; +using Xamarin.Forms; + +namespace TINK.Model +{ + [DataContract] + public class TinkApp : ITinkApp + { + /// Delegate used by login view to commit user name and password. + /// Mail address used as id login. + /// Password for login. + /// True if setting credentials succeeded. + public delegate bool SetCredentialsDelegate(string p_strMailAddress, string p_strPassword); + + /// Returns the id of the app to be identified by copri. + public static string MerchantId => "oiF2kahH"; + + /// + /// Holds status about whants new page. + /// + public WhatsNew WhatsNew { get; private set; } + + /// Sets flag whats new page was already shown to true. + public void SetWhatsNewWasShown() => WhatsNew = WhatsNew.SetWasShown(); + + /// Holds uris of copri servers. + public CopriServerUriList Uris { get; } + + /// Holds the filters loaded from settings. + public IGroupFilterSettings FilterGroupSetting { get; set; } + + /// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. + private IGroupFilterMapPage m_oFilterDictionaryMapPage; + + /// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. + public IGroupFilterMapPage GroupFilterMapPage + { + get => m_oFilterDictionaryMapPage; + set => m_oFilterDictionaryMapPage = value ?? new GroupFilterMapPage(); + } + + /// Value indicating whether map is centerted to current position or not. + public bool CenterMapToCurrentLocation { get; set; } + + /// Gets the minimum logging level. + public LogEventLevel MinimumLogEventLevel { get; set; } + + /// Holds the uri which is applied after restart. + public Uri NextActiveUri { get; set; } + + /// Saves object to file. + public void Save() + => JsonSettingsDictionary.Serialize( + SettingsFileFolder, + new Dictionary() + .SetGroupFilterMapPage(GroupFilterMapPage) + .SetCopriHostUri(NextActiveUri.AbsoluteUri) + .SetPollingParameters(Polling) + .SetGroupFilterSettings(FilterGroupSetting) + .SetAppVersion(AppVersion) + .SetMinimumLoggingLevel(MinimumLogEventLevel) + .SetExpiresAfter(ExpiresAfter) + .SetWhatsNew(AppVersion) + .SetActiveLockService(LocksServices.Active.GetType().FullName) + .SetActiveGeolocationService(GeolocationServices.Active.GetType().FullName) + .SetCenterMapToCurrentLocation(CenterMapToCurrentLocation) + .SetLogToExternalFolder(LogToExternalFolder) + .SetConnectTimeout(LocksServices.Active.TimeOut.MultiConnect) + .SetIsSiteCachingOn(IsSiteCachingOn) + .SetActiveTheme(Themes.Active.GetType().FullName)); + + /// + /// Update connector from filters when + /// - login state changes + /// - view is toggled (TINK to Kornrad and vice versa) + /// + public void UpdateConnector() + { + // Create filtered connector. + m_oConnector = FilteredConnectorFactory.Create( + FilterGroupSetting.DoFilter(ActiveUser.DoFilter(GroupFilterMapPage.DoFilter())), + m_oConnector.Connector); + } + + /// Polling periode. + public PollingParameters Polling { get; set; } + + public TimeSpan ExpiresAfter { get; set; } + + /// Holds the version of the app. + public Version AppVersion { get; } + + /// + /// Holds the default polling value. + /// + public TimeSpan DefaultPolling => new TimeSpan(0, 0, 10); + + /// Constructs TinkApp object. + /// + /// + /// + /// + /// Null in productive context. Service to querry geoloation for testing purposes. Parameter can be made optional. + /// Null in productive context. Service to control locks/ get locks information for testing proposes. Parameter can be made optional. + /// Object allowing platform specific operations. + /// + /// + /// True if connector has access to copri server, false if cached values are used. + /// Version of the app. If null version is set to a fixed dummy value (3.0.122) for testing purposes. + /// Version of app which was used before this session. + /// Holds + /// - the version when whats new info was shown last or + /// - version of application used last if whats new functionality was not implemented in this version or + /// - null if app is installed for the first time. + /// /// + public TinkApp( + Settings.Settings settings, + IStore accountStore, + Func connectorFactory, + IGeolocation geolocationService, + IGeolodationDependent geolodationServiceDependent, + ILocksService locksService, + IDevice device, + ISpecialFolder specialFolder, + ICipher cipher, + IPermissions permissions = null, + object arendiCentral = null, + Func isConnectedFunc = null, + Action postAction = null, + Version currentVersion = null, + Version lastVersion = null, + Version whatsNewShownInVersion = null) + { + PostAction = postAction + ?? ((d, obj) => d(obj)); + + ConnectorFactory = connectorFactory + ?? throw new ArgumentException("Can not instantiate TinkApp- object. No connector factory object available."); + + Cipher = cipher ?? new Cipher(); + + var locksServices = locksService != null + ? new HashSet { locksService } + : new HashSet { + new LockItByScanServiceEventBased(Cipher), + new LockItByScanServicePolling(Cipher), + new LockItByGuidService(Cipher), +#if BLUETOOTHLE // Requires LockItBluetoothle library. + new Bluetoothle.LockItByGuidService(Cipher), +#endif +#if ARENDI // Requires LockItArendi library. + new Arendi.LockItByGuidService(Cipher, arendiCentral), + new Arendi.LockItByScanService(Cipher, arendiCentral), +#endif + new LocksServiceInReach(), + new LocksServiceOutOfReach(), + }; + + LocksServices = new LocksServicesContainerMutable( + lastVersion >= new Version(3, 0, 173) ? settings.ActiveLockService : LocksServicesContainerMutable.DefaultLocksservice, + locksServices); + + LocksServices.SetTimeOut(settings.ConnectTimeout); + + Themes = new ServicesContainerMutable( + new HashSet { new Themes.Konrad() , new Themes.ShareeBike() }, + settings.ActiveTheme); + + GeolocationServices = new ServicesContainerMutable( + geolocationService == null + ? new HashSet { new LastKnownGeolocationService(geolodationServiceDependent), new SimulatedGeolocationService(geolodationServiceDependent), new GeolocationService(geolodationServiceDependent) } + : new HashSet { geolocationService }, + geolocationService == null + ? (lastVersion >= new Version(3, 0, 173) ? settings.ActiveGeolocationService : typeof(LastKnownGeolocationService).FullName) + : geolocationService.GetType().FullName); + + // Load filters from settings or apply defaults if no settings are available + var l_oAccount = accountStore.Load(); + + if (settings.ActiveUri == new Uri(CopriServerUriList.TINK_LIVE) || + settings.ActiveUri == new Uri(CopriServerUriList.TINK_DEVEL)) + { + FilterGroupSetting = settings.GroupFilterSettings; + GroupFilterMapPage = settings.GroupFilterMapPage; + //} else if (settings.ActiveUri == new Uri(CopriServerUriList.SHAREE_LIVE) || + // settings.ActiveUri == new Uri(CopriServerUriList.SHAREE_DEVEL)) + //{ + // FilterGroupSetting = new GroupFilterSettings(new Dictionary { { "300001", FilterState.On }, { "300029", FilterState.On } }); + // FilterGroupMapPage = new GroupFilterMapPage(); + } else + { + FilterGroupSetting = new GroupFilterSettings(); + GroupFilterMapPage = new GroupFilterMapPage(); + } + + CenterMapToCurrentLocation = settings.CenterMapToCurrentLocation; + + Device = device + ?? throw new ArgumentException("Can not instantiate TinkApp- object. No device information provider available."); + + if (specialFolder == null) + { + throw new ArgumentException("Can not instantiate TinkApp- object. No special folder provider available."); + } + + // Set logging level. + Level.MinimumLevel = settings.MinimumLogEventLevel; + + LogToExternalFolder = settings.LogToExternalFolder; + + IsSiteCachingOn = settings.IsSiteCachingOn; + + ExternalFolder = specialFolder.GetExternalFilesDir(); + + SettingsFileFolder = specialFolder.GetInternalPersonalDir(); + + SelectedStation = null; + + ActiveUser = new User.User( + accountStore, + l_oAccount, + device.GetIdentifier()); + + this.isConnectedFunc = isConnectedFunc ?? (() => CrossConnectivity.Current.IsConnected); + + ExpiresAfter = settings.ExpiresAfter; + + // Create filtered connector for offline mode. + m_oConnector = FilteredConnectorFactory.Create( + FilterGroupSetting.DoFilter(l_oAccount.DoFilter(GroupFilterMapPage.DoFilter())), + ConnectorFactory(GetIsConnected(), settings.ActiveUri, ActiveUser.SessionCookie, ActiveUser.Mail, ExpiresAfter)); + + // Get uris from file. + // Initialize all settings to defaults + // Process uris. + Uris = new CopriServerUriList(settings.ActiveUri); + + NextActiveUri = Uris.ActiveUri; + + Polling = settings.PollingParameters ?? + throw new ArgumentException("Can not instantiate TinkApp- object. Polling parameters must never be null."); + + AppVersion = currentVersion ?? new Version(3, 0, 122); + + MinimumLogEventLevel = settings.MinimumLogEventLevel; + + Permissions = permissions ?? + throw new ArgumentException("Can not instantiate TinkApp- object. Permissions object must never be null."); + + WhatsNew = new WhatsNew(AppVersion, lastVersion, whatsNewShownInVersion); + + if (Themes.Active.GetType().FullName == typeof(Themes.ShareeBike).FullName) + return; + + // Set active app theme + ICollection mergedDictionaries = Application.Current.Resources.MergedDictionaries; + if (mergedDictionaries == null) + { + Log.ForContext().Error("No merged dictionary available."); + return; + } + + mergedDictionaries.Clear(); + + if (Themes.Active.GetType().FullName == typeof(Themes.Konrad).FullName) + { + mergedDictionaries.Add(new Themes.Konrad()); + } + else + { + Log.ForContext().Debug($"No theme {Themes.Active} found."); + } + } + + /// Holds the user of the app. + [DataMember] + public User.User ActiveUser { get; } + + /// Reference of object which provides device information. + public IDevice Device { get; } + + /// Os permission. + public IPermissions Permissions { get; } + + /// Holds delegate to determine whether device is connected or not. + private Func isConnectedFunc; + + /// Gets whether device is connected to internet or not. + public bool GetIsConnected() => isConnectedFunc(); + + /// Holds the folder where settings files are stored. + public string SettingsFileFolder { get; } + + /// Holds folder parent of the folder where log files are stored. + public string LogFileParentFolder => LogToExternalFolder && !string.IsNullOrEmpty(ExternalFolder) ? ExternalFolder : SettingsFileFolder; + + /// Holds a value indicating whether to log to external or internal folder. + public bool LogToExternalFolder { get; set; } + + /// Holds a value indicating whether Site caching is on or off. + public bool IsSiteCachingOn { get; set; } + + /// External folder. + public string ExternalFolder { get; } + + public ICipher Cipher { get; } + + /// Name of the station which is selected. + public int? SelectedStation { get; set; } + + /// Action to post to GUI thread. + public Action PostAction { get; } + + /// Function which creates a connector depending on connected status. + private Func ConnectorFactory { get; } + + /// Holds the object which provides offline data. + private IFilteredConnector m_oConnector; + + /// Holds the system to copri. + public IFilteredConnector GetConnector(bool isConnected) + { + if (m_oConnector.IsConnected == isConnected + && m_oConnector.Command.SessionCookie == ActiveUser.SessionCookie) + { + // Neither connection nor logged in stated changed. + return m_oConnector; + } + + // Connected state changed. New connection object has to be created. + m_oConnector = FilteredConnectorFactory.Create( + FilterGroupSetting.DoFilter(ActiveUser.DoFilter(GroupFilterMapPage.DoFilter())), + ConnectorFactory( + isConnected, + Uris.ActiveUri, + ActiveUser.SessionCookie, + ActiveUser.Mail, + ExpiresAfter)); + + return m_oConnector; + } + + /// Query geolocation. + public IGeolocation Geolocation => GeolocationServices.Active; + + /// Manages the different types of LocksService objects. + public LocksServicesContainerMutable LocksServices { get; set; } + + /// Holds available app themes. + public ServicesContainerMutable GeolocationServices { get; } + + /// Manages the different types of LocksService objects. + public ServicesContainerMutable Themes { get; } + + /// Object to switch logging level. + private LoggingLevelSwitch m_oLoggingLevelSwitch; + + /// + /// Object to allow swithing logging level + /// + public LoggingLevelSwitch Level + { + get + { + if (m_oLoggingLevelSwitch == null) + { + m_oLoggingLevelSwitch = new LoggingLevelSwitch + { + + // Set warning level to error. + MinimumLevel = Settings.Settings.DEFAULTLOGGINLEVEL + }; + } + + return m_oLoggingLevelSwitch; + } + } + + /// Updates logging level. + /// New level to set. + public void UpdateLoggingLevel(LogEventLevel p_oNewLevel) + { + if (Level.MinimumLevel == p_oNewLevel) + { + // Nothing to do. + return; + } + + Log.CloseAndFlush(); // Close before modifying logger configuration. Otherwise a sharing vialation occurs. + + Level.MinimumLevel = p_oNewLevel; + + // Update logging + Log.Logger = new LoggerConfiguration() + .MinimumLevel.ControlledBy(Level) + .WriteTo.Debug() + .WriteTo.File(LogFileParentFolder, Logging.RollingInterval.Session) + .CreateLogger(); + } + } +} diff --git a/TINKLib/Model/User/Account/Account.cs b/TINKLib/Model/User/Account/Account.cs new file mode 100644 index 0000000..031ddf1 --- /dev/null +++ b/TINKLib/Model/User/Account/Account.cs @@ -0,0 +1,91 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model.User.Account +{ + /// Specifies extra user permissions. + [Flags] + public enum Permissions + { + None = 0, // No extra permissions. + PickCopriServer = 2, // Allows user to switch COPRI server. + ManageCopriCacheExpiration = 4, // Allows to manage the livetime of COPRI cache entries. + ManagePolling = 8, // Turn polling off or on and set pollig frequency. + PickLockServiceImplementation = 16, // Allows to pick the implementation which controls bluetooth lock mangement. + PickLocationServiceImplementation = 32, // Allows to pick the implementation which gets location information. + PickLoggingLevel = 64, // Allows to select the logging level. + ShowDiagnostics = 128, // Turns on display of diagnostics. + SwitchNoSiteCaching = 1024, // Allows to turn off/ on caching of sites displayed in app hosted by COPRI + All = PickCopriServer + + ManageCopriCacheExpiration + + ManagePolling + + PickLockServiceImplementation + + PickLocationServiceImplementation + + PickLoggingLevel + + ShowDiagnostics + + SwitchNoSiteCaching, + } + + /// + /// Specifies parts of account data. + /// + /// + /// Usage: Account can be valid (user and password set) partly valid or completely invalid. + /// + [Flags] + public enum Elements + { + None = 0, + Mail = 1, + Password = 2, + Account = Mail + Password + } + + /// + /// Holds account data. + /// + public class Account : IAccount + { + /// Constructs an account object. + /// Mail addresss. + /// Password. + /// Session cookie from copri. + /// Group holdig info about Group (TINK, Konrad, ...) + /// Flag which controls display of debug settings. + public Account( + string p_oMail, + string p_Pwd, + string p_oSessionCookie, + IEnumerable p_strGroup, + Permissions debugLevel = Permissions.None) + { + Mail = p_oMail; + Pwd = p_Pwd; + SessionCookie = p_oSessionCookie; + DebugLevel = debugLevel; + Group = p_strGroup != null + ? new HashSet(p_strGroup).ToList() + : throw new ArgumentException("Can not instantiate account object. Reference to group list must not be empty."); + } + + public Account(IAccount p_oSource) : this(p_oSource?.Mail, p_oSource?.Pwd, p_oSource?.SessionCookie, p_oSource?.Group, p_oSource?.DebugLevel ?? Permissions.None) + { + } + + /// Mail address. + public string Mail { get; } + + /// Password of the account. + public string Pwd { get; } + + /// Session cookie used to sign in to copri. + public string SessionCookie { get; } + + /// Debug level used to determine which features are available. + public Permissions DebugLevel { get; } + + /// Holds the group of the bike (TINK, Konrad, ...). + public IEnumerable Group { get; } + } +} diff --git a/TINKLib/Model/User/Account/AccountExtensions.cs b/TINKLib/Model/User/Account/AccountExtensions.cs new file mode 100644 index 0000000..6920435 --- /dev/null +++ b/TINKLib/Model/User/Account/AccountExtensions.cs @@ -0,0 +1,34 @@ +using System.Collections.Generic; +using TINK.Model.Connector.Filter; + +namespace TINK.Model.User.Account +{ + public static class AccountExtensions + { + /// Gets information whether user is logged in or not from account object. + /// Object to get information from. + /// True if user is logged in, false if not. + public static bool GetIsLoggedIn(this IAccount p_oAccount) + { + return !string.IsNullOrEmpty(p_oAccount.Mail) + && !string.IsNullOrEmpty(p_oAccount.SessionCookie); + } + + /// + /// Filters bike groups depending on whether user has access to all groups of bikes. + /// Some user may be "TINK"- user only, some "Konrad" and some may be "TINK" and "Konrad" users. + /// + /// Account to filter with. + /// Groups to filter. + /// Filtered bike groups. + public static IEnumerable DoFilter( + this IAccount account, + IEnumerable filter) + { + + return GetIsLoggedIn(account) + ? GroupFilterFactory.Create(account.Group).DoFilter(filter) // Filter if user is logged in. + : new NullGroupFilter().DoFilter(filter); // Do not filter if no user is logged in. + } + } +} diff --git a/TINKLib/Model/User/Account/AccountMutable.cs b/TINKLib/Model/User/Account/AccountMutable.cs new file mode 100644 index 0000000..3323eea --- /dev/null +++ b/TINKLib/Model/User/Account/AccountMutable.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; + +namespace TINK.Model.User.Account +{ + /// + /// Holds email address and password. + /// + public class AccountMutable : IAccount + { + /// + /// Holds the account data. + /// + private Account m_oAccount; + + /// Prevents an invalid instance to be created. + private AccountMutable() + { + } + + public AccountMutable(IAccount p_oSource) + { + m_oAccount = new Account(p_oSource); + } + + public void Copy(IAccount p_oSource) + { + m_oAccount = new Account(p_oSource); + } + + /// + /// Mail address. + /// + public string Mail + { + get { return m_oAccount.Mail; } + set { m_oAccount = new Account(value, m_oAccount.Pwd, m_oAccount.SessionCookie, m_oAccount.Group, m_oAccount.DebugLevel); } + } + + /// + /// Password of the account. + /// + public string Pwd + { + get { return m_oAccount.Pwd; } + set { m_oAccount = new Account(m_oAccount.Mail, value, m_oAccount.SessionCookie, m_oAccount.Group, m_oAccount.DebugLevel); } + } + + /// + /// Session cookie used to sign in to copri. + /// + public string SessionCookie + { + get { return m_oAccount.SessionCookie; } + set { m_oAccount = new Account(m_oAccount.Mail, m_oAccount.Pwd, value, m_oAccount.Group, m_oAccount.DebugLevel); } + } + + /// + /// Holds the group of the bike (TINK, Konrad, ...). + /// + public IEnumerable Group + { + get { return m_oAccount.Group; } + } + + /// + /// Debug level used to determine which features are available. + /// + public Permissions DebugLevel + { + get { return m_oAccount.DebugLevel; } + set { m_oAccount = new Account(m_oAccount.Mail, m_oAccount.Pwd, m_oAccount.SessionCookie, m_oAccount.Group, value); } + } + } +} diff --git a/TINKLib/Model/User/Account/EmptyAccount.cs b/TINKLib/Model/User/Account/EmptyAccount.cs new file mode 100644 index 0000000..80a2745 --- /dev/null +++ b/TINKLib/Model/User/Account/EmptyAccount.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; + +namespace TINK.Model.User.Account +{ + /// Represents an empty account. + public class EmptyAccount : IAccount + { + public string Mail => null; + + public string Pwd => null; + + public string SessionCookie => null; + + public Permissions DebugLevel => Permissions.None; + + public IEnumerable Group => new List(); + } +} diff --git a/TINKLib/Model/User/Account/IAccount.cs b/TINKLib/Model/User/Account/IAccount.cs new file mode 100644 index 0000000..ed826d6 --- /dev/null +++ b/TINKLib/Model/User/Account/IAccount.cs @@ -0,0 +1,25 @@ +using System.Collections.Generic; + +namespace TINK.Model.User.Account +{ + /// + /// Holds account data. + /// + public interface IAccount + { + /// Mail address. + string Mail { get; } + + /// Password of the account. + string Pwd { get; } + + /// Session cookie used to sign in to copri. + string SessionCookie { get; } + + /// Debug level used to determine which features are available. + Permissions DebugLevel { get; } + + /// Holds the group of the bike (TINK, Konrad, ...). + IEnumerable Group { get; } + } +} diff --git a/TINKLib/Model/User/Account/IStore.cs b/TINKLib/Model/User/Account/IStore.cs new file mode 100644 index 0000000..22be440 --- /dev/null +++ b/TINKLib/Model/User/Account/IStore.cs @@ -0,0 +1,26 @@ +namespace TINK.Model.User.Account +{ + /// + /// Interface to manage an account store. + /// + public interface IStore + { + /// + /// Reads mail address and password from account store. + /// + /// + IAccount Load(); + + /// + /// Writes mail address and password to account store. + /// + /// + void Save(IAccount p_oMailAndPwd); + + /// + /// Deletes mail address and password from account store. + /// + /// Empty account instance if deleting succeeded. + IAccount Delete(IAccount p_oMailAndPwd); + } +} diff --git a/TINKLib/Model/User/Account/Validator.cs b/TINKLib/Model/User/Account/Validator.cs new file mode 100644 index 0000000..392d15e --- /dev/null +++ b/TINKLib/Model/User/Account/Validator.cs @@ -0,0 +1,167 @@ +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model.User.Account +{ + /// + /// Holds the state of mail and password and information about some invalid parts if there are. + /// + public class State + { + /// + /// Consider state to be invalid after construction. + /// + private Elements m_eElements = Elements.None; + + private Dictionary m_oDescription = new Dictionary(); + + /// + /// Constructs object to state all entries are valid. + /// + public State() + { + m_eElements = Elements.Account; + + m_oDescription = new Dictionary + { + { Elements.None, string.Empty }, + { Elements.Account, string.Empty } + }; + } + + /// + /// Constructs object to state some/ all elements are invalid. + /// + /// Specifies the parts which are invalid. + /// Description of invalid parts. + public State(Elements p_oValidParts, Dictionary p_oDescription) + { + m_eElements = p_oValidParts; + + m_oDescription = p_oDescription ?? new Dictionary(); + + // Ensure consistency + foreach (Elements l_oElement in Enum.GetValues(typeof(Elements))) + { + if (!m_oDescription.ContainsKey(l_oElement)) + { + switch (l_oElement) + { + case Elements.Account: + case Elements.None: + continue; + + } + + m_oDescription.Add(l_oElement, string.Empty); + } + } + } + + /// + /// True if account is valid. + /// + public bool IsValid { get { return ValidElement == Elements.Account; } } + + /// + /// Specifies if both mail and password are valid, one of them or none. + /// + public Elements ValidElement + { + get + { + return m_eElements; + } + } + + /// + /// Holds the message about invalid elements. + /// + public Dictionary Description + { + get + { + var l_oUserFriendlyDescription = new Dictionary(); + foreach (Elements l_oElement in Enum.GetValues(typeof(Elements))) + { + switch (l_oElement) + { + case Elements.Account: + case Elements.None: + continue; + + } + + l_oUserFriendlyDescription.Add( + l_oElement, + m_oDescription.ContainsKey(l_oElement) ? m_oDescription[l_oElement] : string.Empty); + } + + l_oUserFriendlyDescription.Add( + Elements.Account, + string.Join(";", l_oUserFriendlyDescription.Where(x => x.Value.Length > 0).Select(x => x.Value).ToArray())); + + return l_oUserFriendlyDescription ; + } + } + } + + /// + /// Verifies if a password is valid or not. + /// + /// + /// + /// + public delegate State PasswordValidator(string p_strMail, string p_strPassword); + + public static class Validator + { + public static State ValidateMailAndPasswordDelegate(string p_strMail, string p_strPassword) + { + var l_oElements = Elements.None; + var l_oDescription = new Dictionary(); + + // Validate mail address. + if (string.IsNullOrEmpty(p_strMail)) + { + l_oDescription.Add(Elements.Mail, "Email Addresse darf nicht leer sein."); + } + else if (p_strMail.ToString().Split('@').Length < 2) + { + l_oDescription.Add(Elements.Mail, "Email Adresse mit Zeichen \"@\" enthalten."); + } + else if (p_strMail.ToString().Split('@')[0].Length <= 0) + { + l_oDescription.Add(Elements.Mail, "Benutzername in Email Adresse darf nicht leer sein."); + } + else if (p_strMail.ToString().Split('@')[1].Length <= 0) + { + // Data has been entered + l_oDescription.Add(Elements.Mail, "Domain- Name in Email Adresse darf nicht leer sein."); + } + else + { + // Input mail address is ok + l_oElements = Elements.Mail; + l_oDescription.Add(Elements.Mail, string.Empty); + } + + // Validate password. + if (string.IsNullOrEmpty(p_strPassword) || p_strPassword.Length < 8) + { + // Data has been entered + l_oDescription.Add(Elements.Password, "Passwort is zu kurz."); + } + else + { + // Password is ok + l_oElements |= Elements.Password; + l_oDescription.Add(Elements.Password, string.Empty); + } + + return new State(l_oElements, l_oDescription); + } + } + +} diff --git a/TINKLib/Model/User/IUser.cs b/TINKLib/Model/User/IUser.cs new file mode 100644 index 0000000..eaa0fba --- /dev/null +++ b/TINKLib/Model/User/IUser.cs @@ -0,0 +1,16 @@ +using TINK.Model.User.Account; + +namespace TINK.Model.User +{ + public interface IUser + { + /// Holds a value indicating whether user is logged in or not. + bool IsLoggedIn { get; } + + /// Holds the mail address. + string Mail { get; } + + /// Holds the debug level. + Permissions DebugLevel { get; } + } +} diff --git a/TINKLib/Model/User/User.cs b/TINKLib/Model/User/User.cs new file mode 100644 index 0000000..8cfccb0 --- /dev/null +++ b/TINKLib/Model/User/User.cs @@ -0,0 +1,154 @@ +using System; +using System.Collections.Generic; +using TINK.Model.User.Account; + +namespace TINK.Model.User +{ + public delegate void LoginStateChangedDelegate(object p_oSender, EventArgs p_oEventArgs); + + + /// + /// Manages user of the app. + /// + public class User : IUser + { + /// + /// Holds account data. + /// + private readonly AccountMutable m_oAccount; + + /// + /// Provides storing functionality. + /// + private IStore m_oStore; + + /// Holds the id of the device. + public string DeviceId { get; } + + /// Loads user name and passwort from account store. + /// + /// Object to use for loading and saving user data. + public User( + IStore p_oAccountStore, + IAccount p_oAccount, + string p_strDeviceId) + { + m_oStore = p_oAccountStore + ?? throw new ArgumentException("Can not instantiate user- object. No store functionality available."); + DeviceId = p_strDeviceId; + m_oAccount = new AccountMutable(p_oAccount); + } + + /// Is fired wheneverlogin state changes. + public event LoginStateChangedDelegate StateChanged; + + /// + /// Holds a value indicating whether user is logged in or not. + /// + public bool IsLoggedIn { + get + { + return m_oAccount.GetIsLoggedIn(); + } + } + + /// + /// Holds the mail address. + /// + public string Mail + { + get { return m_oAccount.Mail; } + } + + /// + /// Gets the sessiong cookie. + /// + public string SessionCookie + { + get { return m_oAccount.SessionCookie; } + } + /// + /// Holds the password. + /// + public string Password + { + get { return m_oAccount.Pwd; } + } + + /// Holds the debug level. + public Permissions DebugLevel + { + get { return m_oAccount.DebugLevel; } + } + + /// Holds the group of the bike (TINK, Konrad, ...). + public IEnumerable Group { get { return m_oAccount.Group; } } + + /// Logs in user. + /// Account to use for login. + /// Holds the Id to identify the device. + /// True if connector has access to copri server, false if cached values are used. + public void CheckIsPasswordValid(string mail, string password) + { + if (IsLoggedIn) + { + throw new Exception($"Can not log in user {mail} because user {m_oAccount} is already logged in."); + } + + // Check if password might be valid before connecting to copri. + var l_oResult = Validator.ValidateMailAndPasswordDelegate(mail, password); + + if (!l_oResult.IsValid) + { + // Password is not valid. + throw new ArgumentException(l_oResult.Description[Elements.Account]); + } + } + + /// Logs in user. + /// Account to use for login. + /// Holds the Id to identify the device. + /// True if connector has access to copri server, false if cached values are used. + public void Login(IAccount account) + { + // Update account instance from copri data. + m_oAccount.Copy(account); + + // Save data to store. + m_oStore.Save(m_oAccount); + + // Nothing to do because state did not change. + StateChanged?.Invoke(this, new EventArgs()); + } + + /// Logs in user + /// + public void Logout() + { + var l_oPreviousState = IsLoggedIn; + + m_oAccount.Copy(m_oStore.Delete(m_oAccount)); + + if (IsLoggedIn == l_oPreviousState) + { + // Nothing to do because state did not change. + return; + } + + StateChanged?.Invoke(this, new EventArgs()); + } + + /// + /// Filters bike groups depending on whether user has access to all groups of bikes. + /// Some user may be "TINK"- user only, some "Konrad" and some may be "TINK" and "Konrad" users. + /// + /// Account to filter with. + /// Groups to filter.. + /// Filtered bike groups. + public IEnumerable DoFilter(IEnumerable p_oSource = null) + { + return m_oAccount.DoFilter(p_oSource); + } + } +} + diff --git a/TINKLib/Model/User/UsernamePasswordInvalidException.cs b/TINKLib/Model/User/UsernamePasswordInvalidException.cs new file mode 100644 index 0000000..1e7b916 --- /dev/null +++ b/TINKLib/Model/User/UsernamePasswordInvalidException.cs @@ -0,0 +1,11 @@ +using System; + +namespace TINK.Model.User +{ + public class UsernamePasswordInvalidException : Exception + { + public UsernamePasswordInvalidException() : base("Benutzername und/ oder Passwort sind ungültig. Cookie ist leer.") + { + } + } +} diff --git a/TINKLib/Model/WhatsNew.cs b/TINKLib/Model/WhatsNew.cs new file mode 100644 index 0000000..0ecf0d5 --- /dev/null +++ b/TINKLib/Model/WhatsNew.cs @@ -0,0 +1,454 @@ +using System; +using System.Collections.Generic; +using TINK.MultilingualResources; + +namespace TINK.Model +{ + /// Holds information about TINKApp development. + public class WhatsNew + { + private static readonly Version AGBMODIFIEDBUILD = new Version(3, 0, 131); + + /// Change of of think App. + private Dictionary WhatsNewMessages = new Dictionary + { + { + new Version(3, 0, 0, 115), + "Benutzeroberfläche verbessert.\r\n\r\n" + }, + { + new Version(3, 0, 120), + "Verbesserung: Keine Fehler mehr beim schnellen Tippen.\r\n" + + "Offlineanzeige Stationen/ Räderinfo.\r\n\r\n" + }, + { + AGBMODIFIEDBUILD, + "Neue Seiten eingebaut\r\n" + + "-zum erstmaligem Registrieren\r\n" + + "-zur Verwaltung des Benutzerkontos\r\n" + + "-zum Zurücksetzen des Passworts\r\n" + + "\r\n" + + "Anzeige Verbindungsstatus auf den Seiten\r\n" + + "-Kartenansicht Fahrradstandorte\r\n" + + "-Fahrräder an Station\r\n" + + "-Meine Fahrräder\r\n\r\n" + }, + { + new Version(3, 0, 137), + "Verschiedene kleine Verbesserungen und Korrekturen.\r\n" + }, + { + new Version(3, 0, 141), + "Erste I LOCK IT Unterstützung.\r\n" + + "Erweiterte Optionen: Zwei Schlosssimulationen.\r\n" + }, + { + new Version(3, 0, 142), + "Sharee Server verfügbar.\r\n" + }, + { + new Version(3, 0, 143), + "Geolocation wird am Gerät abgefragt.\r\n" + + "Erweiterte Optionen: Genauer Standort kann abgefragt werden, Standortsimulation verfügbar.\r\n" + }, + { + new Version(3, 0, 144), + "Diverse Fehler behoben.\r\n" + + "Erweiterte Optionen: Texte Auswahlboxen für Copri-Server, Schlosssteuerung und Geolocation verständlicher gemacht.\r\n" + }, + { + new Version(3, 0, 145), + "Kleine Fehler behoben.\r\n" + + "Erweiterte Option ein/ ausschalten \"Karte auf aktuelle Position ausrichten\" hinzugefügt.\r\n" + + "Für gemietete Räder ausserhalb der Reichweite wird Knof \"Schloss suchen\" angezeigt." + }, + { + new Version(3, 0, 146), + "Fehler behoben: Aktion Schloss schließen wird jetzt durchgeführt.\r\n" + + "Benennung: \"Miete weiterführen\" -> \"Miete fortsetzen\".\r\n" + }, + { + new Version(3, 0, 147), + "Erste prototypische Unterstützung des ILOCKIT-Schlosses.\r\n" + }, + { + new Version(3, 0, 148), + "Schloss-Guid wird an CORI bei Buchung übermittelt.\r\n" + }, + { + new Version(3, 0, 149), + "Schlösser mit neuem Advertisement-Name ISHAREIT+XXXXXXX unterstützt.\r\n" + }, + { + new Version(3, 0, 150), + "Verbesserung: Schlossstatus wird nach Öffnen/ Schließen abgefragt.\r\n" + }, + { + new Version(3, 0, 151), + "Verbesserungen:\r\n" + + "Erweiterte Optionen: Auswahl, ob log-Dateien auf internem Speicher oder SD-Karte abgelegt werden, ist konfigurierbar.\r\n" + + "Leistung: Suche nach Bluetooth Schlössern deutlich beschleunigt.\r\n" + + "Kleine Textkorrekturen.\r\n." + + "Fehlerbehebung: Schloss kann direkt nach Reservierung geöffnet werden.\r\n" + }, + { + new Version(3, 0, 152), + "Verbesserungen: Aufforderung zum Aktivieren von Bluetooth beim Öffnen der Seiten Meine Räder und Räder an Station implementiert.\r\n" + + "Fehlerbehebung: Seite Meine Räder kann auch geöffnet werden, ohne dass Räder reserviert oder gemietet sein müssen.\r\n" + }, + { + new Version(3, 0, 153), + "Stabilität erhöht.\r\n" + }, + { + new Version(3, 0, 154), + "Stationen in Freiburg werden angezeigt.\r\n" + }, + { + new Version(3, 0, 155), + "Schlosssuche verbessert.\r\n" + }, + { + new Version(3, 0, 156), + "Abschalten von Sounds und Alarm für offene, reservierte Räder hinzugefügt.\r\n" + + "Kleine Fehler behoben.\r\n" + }, + { + new Version(3, 0, 157), + "Versenden von Mail mit Diagnoseinformation funktioniert wieder.\r\n" + + "Stationen werden nicht mehr fälschlicherweise ausgeblendet nach Verlassen von Einstellungsseite.\r\n" + + "Absturz bei minimieren von App behoben.\r\n" + + "Stabilität Bluetoothverbindung bei erstmaligem Verbinden verbessert.\r\n" + + "Stabilität Bluetoothverbindung bei wiederholtem Verbinden verbessert.\r\n" + + "Absturz bei Drehen von Smartdevice behoben.\r\n" + + "Absturz bei minimieren von App behoben.\r\n" + }, + { + new Version(3, 0, 158), + "Bugfix: Auf Endgerät mit deutscher Sprache werden Texte wieder auf deutsch angezeigt.\r\n" + + "Erweiterung: Räderinfo für TINK-Räder werden nur noch bei Anmeldung mit TINK-Konto angezeigt.\r\n" + + "Erweiterung: Anwendergruppeninfotext \"TINK\" bzw. \"Konrad\" wird nur noch angezeigt, wenn Konto ein TINK- bzw. Konradrechte hat.\r\n" + }, + { + new Version(3, 0, 159), + "Bugfix: Asynchrone Bluetooth Aktualisierung für Android entfernt, da nicht unterstützt.\r\n" + }, + { + new Version(3, 0, 162), + "App umbenannt von TINKApp in sharee.bike.\r\n" + }, + { + new Version(3, 0, 163), + "Schlossstatus wird an COPRI übermittelt.\r\n" + }, + { + new Version(3, 0, 164), + "Wechsel Standard Lock-Umsetzung: GUID-Verbindungsaufbau wird statt Scan benutzt.\r\n" + + "Datenquellen für \"Passwort vergessen\", \"Persönliche Daten Verwalten\", \"Datenschutz\" und \"AGB\" aktualisiert.\r\n" + }, + { + new Version(3, 0, 165), + "Menüstruktur überarbeitet.\r\n" + }, + { + new Version(3, 0, 167), + "Standardeinstellung geändert: Kartenansicht wird per Default auf aktuelle Position zentriert.\r\n" + }, + { + new Version(3, 0, 168), + "Konfigurierbaren Connect Timeout eingebaut.\r\n" + + "Impressum, Radinfo und Tarifinfo wird von Server geladen.\r\n" + + "Adressen für share.bike erweitert.\r\n" + }, + { + new Version(3, 0, 169), + "Verschiedene Fehler behoben.\r\n" + }, + { + new Version(3, 0, 170), + "Fehler behoben: nach Sequenz Rad Zurückgeben-Resverieren-Mieten ist wieder Verbindung zu Schloss möglich.\r\n" + + "Zielplatform Android 10-Q" + }, + { + new Version(3, 0, 171), + "Fehler behoben: nach Sequenz von Homescreen wieder App aktivieren wenn Meine Räder offen ist für nicht mehr zu Crash.\r\n" + + "Fehlermeldung verbessert für den Fall, dass Bluetooth abgeschaltet ist." + }, + { + new Version(3, 0, 172), + "Spezielle Fehler Schloss blockiert beim Öffnen/ Schließen und Fahrrad in Bewegung beim Schließen werden in Alert angezeigt.\r\n" + + "Fehlerzustände werden detaillierter in Alerts angezeigt.\r\n" + + "Überprüfung, ob von COPRI gelieferte GUID gültig ist für bekannte Schlösser.\r\n" + + "Überprüfung, dass Seed nur einmalig verwendet werden.\r\n" + + "Verschiedene kleinere Verbesserungen." + }, + { + new Version(3, 0, 173), + "Fehlerzustände werden detaillierter in Alerts angezeigt.\r\n" + + "Aktualisierung auf Android 10." + }, + { + new Version(3, 0, 174), + "Fehlerkorrketur: GPS-Korrdianten werden länderinvariant übertragen. \r\n" + + "Nutzer mit erweiterten Rechten: Alarm- und Soundeinstellungen können verwaltet werden." + }, + { + new Version(3, 0, 175), + "Fehlerkorrektur: Nach Bluetooth-Wiederverbindung kann Schloss wieder geöffnet und geschlossen werden." + }, + { + new Version(3, 0, 176), + "Wiederholen-/ Abbrechen-Schleife beim Verbinden mit Schlössern umgesetzt. Beim Wiederholen wird die Timeoutzeit jeweils verdoppelt bis zum Faktor vier.\r\n" + + "Kein Neustart mehr notwendig nach Änderung der Timeouts." + }, + { + new Version(3, 0, 177), + "Beim Schließen des Schlosses wird geänderter Zustand an COPRI übermittelt.\r\n" + + "Beim Miete beenden, ohne dass unmittelbar voher das Schloss zu geschlossen wurde, werden keine Koordinaten an COPRI übermittelt.\r\n" + + "Meldungen zu nichkritische Fehlern werden in der Statuszeile angezeigt. " + }, + { + new Version(3, 0, 178), + "Bei Fehlern bei Radrückgabe Fehlermeldung verbessert.\r\n" + + "Activity Indicator (Sanduhr) eingebaut.\r\n" + + "Timeout von 3 auf 5 Sekunden erhöht insbesonders für standard Lock-Umsetzung Live-Scan." + }, + { + new Version(3, 0, 179), + "Verbesserte Fehlermeldungen bei Statusaktualisierung und Radrückgabe.\r\n" + + "Fehlerbehebung: Alter Geoloationinformation wird korrekt übertragen.\r\n" + + "Optimierung Benutzung Geolocationcache." + }, + { + new Version(3, 0, 180), + "Akkufüllstand wir an COPRI übermittelt beim Schloss öffnen." + }, + { + new Version(3, 0, 190), + "Erste Version für iOS." + }, + { + new Version(3, 0, 191), + "Für Seiten \"Fahrradstandorte\", \"Meine Räder\" und \"Räder an Station\":\r\n" + + "- Activity Indicator (Sanduhr) eingebaut\r\n" + + "- Statusmeldungen eingebaut\r\n" + + "Karte wird initial auf Verleistationen zentriert.\r\n" + + "Geschwindikgeitsverbesserungen und Fehler behoben." + }, + { + new Version(3, 0, 192), + "Erweiterung: Benutzerfreundliche Fehlermeldung für Szenario\r\n" + + "-Rückgabe außerhalb von Station\r\n" + + "-Rückgabe ohne GPS-Info\r\n" + + "Standortabfrage bei Radrückgabe von bereits verschlossenem Rad, wenn Schloss in Bluetoothreichweite ist.\r\n" + + "Fehlerbehebung: Passwort-Vergessen Funktionaliät wieder verfügbar.\r\n" + + "Kontakt-Seite aktualisiert (Telefonnummer, Mail, ...)." + }, + { + new Version(3, 0, 193), + "Erweiterte Benutzerrechte können selektiv akiviert werden.\r\n" + + "Verschiedene Umbenennungen." + }, + { + new Version(3, 0, 194), + "Fehlerkorrektur: Wenn kein Benutzer angemeldet ist werden nur noch öffentliche Stationen angezeigt.\r\n" + + "Master-Detail Elemente werden in sharee.bike- Farbe angezeigt." + }, + { + new Version(3, 0, 195), + "Fehlerkorrektur: Android action bar an sharee.bike- Farbenschema angepasst." + }, + { + new Version(3, 0, 196), + "Fehlerkorrektur: Registrieren-Link korrigiert.\r\n" + + "\"Kontakt\"-Seite überarbeitet." + }, + { + new Version(3, 0, 197), + "Android: App ist nicht mehr verfügbar Geräte ohne BluetoothLE/ ohne GPS.\r\n" + + "iOS:\r\n" + + "- Bugfix: Nicht mehr benötigtes Recht \"Standort im Hintergrund\" entfernt.\r\n" + + "- Schreibfehler behoben.\r\n" + + "Fehlerhandling bei Benutzung von mehr als acht Geräte verbessert." + }, + { + new Version(3, 0, 198), + "Fehlermeldungen angepasst.\r\n" + }, + { + new Version(3, 0, 199), + "Radbeschreibung auf sharee.bike angepasst.\r\n" + + "iOS: Berechtigungsfehler behoben." + }, + { + new Version(3, 0, 200), + "Titel von Seite Fahrradstandort verbessert.\r\n" + + "Statusmeldungen verbessert." + }, + { + new Version(3, 0, 201), + "iOS: Darstellung verbessert.\r\n" + + "Weitere Teile der App englischsprachig verfügbar." + }, + { + new Version(3, 0, 202), + "Kleinere Verbesserrungen bezüglich Stabilität und Benutzbarkeit.\r\n" + }, + { + new Version(3, 0, 203), + AppResources.ChangeLog3_0_203 + }, + { + new Version(3, 0, 204), + AppResources.ChangeLog3_0_204 + }, + { + new Version(3, 0, 205), + AppResources.ChangeLog3_0_205 + }, + { + new Version(3, 0, 206), + AppResources.ChangeLog3_0_206 + }, + { + new Version(3, 0, 207), + AppResources.ChangeLog3_0_207 + }, + { + new Version(3, 0, 208), + AppResources.ChangeLog3_0_208 + }, + { + new Version(3, 0, 209), + AppResources.ChangeLog3_0_209 + }, + { + new Version(3, 0, 214), + AppResources.ChangeLog3_0_214 + }, + { + new Version(3, 0, 215), + AppResources.ChangeLog3_0_215 + }, + { + new Version(3, 0, 216), + AppResources.ChangeLog3_0_216 + }, + { + new Version(3, 0, 217), + AppResources.ChangeLog3_0_217 + }, + { + new Version(3, 0, 218), + AppResources.ChangeLog3_0_218 + }, + { + new Version(3, 0, 219), + AppResources.ChangeLog3_0_219 + }, + { + new Version(3, 0, 220), + AppResources.ChangeLog3_0_220 + }, + { + new Version(3, 0, 222), + AppResources.ChangeLog3_0_222 + } + + }; + + /// Manges the whats new information. + /// Current version of the app. + /// Null or version in which whats new dialog was shown last. + public WhatsNew( + Version currentVersion, + Version lastVersion, + Version shownInVersion) + { + WasShownVersion = shownInVersion; + LastVersion = lastVersion; + CurrentVersion = currentVersion; + } + + /// Retruns a new WhatsNew object with property was shown set to true. + /// + public WhatsNew SetWasShown() + { + return new WhatsNew(CurrentVersion, LastVersion, CurrentVersion); + } + + /// Holds the information in which version of the app the whats new dialog has been shown. + private Version WasShownVersion { get; } + + /// Holds the information in which version of the app the whats new dialog has been shown. + private Version CurrentVersion { get; } + + private Version LastVersion; + + /// Holds information whether whats new page was already shown or not. + public bool IsShowRequired + { + get + { + if (CurrentVersion == null) + { + // Unexpected state detected. + return false; + } + + if (LastVersion == null) + { + // Initial install detected. + return false; + } + + return (WasShownVersion ?? LastVersion) < CurrentVersion; + } + } + + /// True if info about modified agb has to be displayed. + public bool IsShowAgbRequired => (WasShownVersion ?? AGBMODIFIEDBUILD) < AGBMODIFIEDBUILD; + + /// Get the whats new text depening of version gap. + public string WhatsNewText + { + get + { + if (CurrentVersion == null) + { + // Unexpected state detected. + return string.Empty; + } + + if (LastVersion == null) + { + // Initial install detected. All is new. + return string.Empty; + } + + var effectiveWasShownVersion = WasShownVersion + ?? LastVersion; // Upgrade from version without whats new dialog detected. + + var whatsNew = string.Empty; + foreach (var l_oInfo in WhatsNewMessages) + { + if (effectiveWasShownVersion >= l_oInfo.Key) + { + // What new info for this version entry was already shown. + continue; + } + + if (l_oInfo.Key > CurrentVersion) + { + // This whats new info entry does not yet apply to current version. + continue; + } + + whatsNew += $"

{l_oInfo.Key}
{l_oInfo.Value}

"; + } + + return whatsNew; + } + } + } +} diff --git a/TINKLib/MultilingualResources/AppResources.Designer.cs b/TINKLib/MultilingualResources/AppResources.Designer.cs new file mode 100644 index 0000000..628b74b --- /dev/null +++ b/TINKLib/MultilingualResources/AppResources.Designer.cs @@ -0,0 +1,1594 @@ +//------------------------------------------------------------------------------ +// +// This code was generated by a tool. +// Runtime Version:4.0.30319.42000 +// +// Changes to this file may cause incorrect behavior and will be lost if +// the code is regenerated. +// +//------------------------------------------------------------------------------ + +namespace TINK.MultilingualResources { + using System; + + + /// + /// A strongly-typed resource class, for looking up localized strings, etc. + /// + // This class was auto-generated by the StronglyTypedResourceBuilder + // class via a tool like ResGen or Visual Studio. + // To add or remove a member, edit your .ResX file then rerun ResGen + // with the /str option, or rebuild your VS project. + [global::System.CodeDom.Compiler.GeneratedCodeAttribute("System.Resources.Tools.StronglyTypedResourceBuilder", "16.0.0.0")] + [global::System.Diagnostics.DebuggerNonUserCodeAttribute()] + [global::System.Runtime.CompilerServices.CompilerGeneratedAttribute()] + public class AppResources { + + private static global::System.Resources.ResourceManager resourceMan; + + private static global::System.Globalization.CultureInfo resourceCulture; + + [global::System.Diagnostics.CodeAnalysis.SuppressMessageAttribute("Microsoft.Performance", "CA1811:AvoidUncalledPrivateCode")] + internal AppResources() { + } + + /// + /// Returns the cached ResourceManager instance used by this class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Resources.ResourceManager ResourceManager { + get { + if (object.ReferenceEquals(resourceMan, null)) { + global::System.Resources.ResourceManager temp = new global::System.Resources.ResourceManager("TINK.MultilingualResources.AppResources", typeof(AppResources).Assembly); + resourceMan = temp; + } + return resourceMan; + } + } + + /// + /// Overrides the current thread's CurrentUICulture property for all + /// resource lookups using this strongly typed resource class. + /// + [global::System.ComponentModel.EditorBrowsableAttribute(global::System.ComponentModel.EditorBrowsableState.Advanced)] + public static global::System.Globalization.CultureInfo Culture { + get { + return resourceCulture; + } + set { + resourceCulture = value; + } + } + + /// + /// Looks up a localized string similar to Rent bike or close lock. + /// + public static string ActionBookOrClose { + get { + return ResourceManager.GetString("ActionBookOrClose", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel bike reservation. + /// + public static string ActionCancelRequest { + get { + return ResourceManager.GetString("ActionCancelRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close lock. + /// + public static string ActionClose { + get { + return ResourceManager.GetString("ActionClose", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Close lock & return bike. + /// + public static string ActionCloseAndReturn { + get { + return ResourceManager.GetString("ActionCloseAndReturn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Submit rating. + /// + public static string ActionContactRate { + get { + return ResourceManager.GetString("ActionContactRate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login. + /// + public static string ActionLoginLogin { + get { + return ResourceManager.GetString("ActionLoginLogin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password forgotten?. + /// + public static string ActionLoginPasswordForgotten { + get { + return ResourceManager.GetString("ActionLoginPasswordForgotten", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Register. + /// + public static string ActionLoginRegister { + get { + return ResourceManager.GetString("ActionLoginRegister", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open lock. + /// + public static string ActionOpen { + get { + return ResourceManager.GetString("ActionOpen", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open lock & rent bike. + /// + public static string ActionOpenAndBook { + get { + return ResourceManager.GetString("ActionOpenAndBook", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Open lock & continue renting. + /// + public static string ActionOpenAndPause { + get { + return ResourceManager.GetString("ActionOpenAndPause", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reserve bike. + /// + public static string ActionRequest { + get { + return ResourceManager.GetString("ActionRequest", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Return bike. + /// + public static string ActionReturn { + get { + return ResourceManager.GetString("ActionReturn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Search lock. + /// + public static string ActionSearchLock { + get { + return ResourceManager.GetString("ActionSearchLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loading bikes located at station.... + /// + public static string ActivityTextBikesAtStationGetBikes { + get { + return ResourceManager.GetString("ActivityTextBikesAtStationGetBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Canceling reservation.... + /// + public static string ActivityTextCancelingReservation { + get { + return ResourceManager.GetString("ActivityTextCancelingReservation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Centering map.... + /// + public static string ActivityTextCenterMap { + get { + return ResourceManager.GetString("ActivityTextCenterMap", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Closing lock.... + /// + public static string ActivityTextClosingLock { + get { + return ResourceManager.GetString("ActivityTextClosingLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Disconnecting lock.... + /// + public static string ActivityTextDisconnectingLock { + get { + return ResourceManager.GetString("ActivityTextDisconnectingLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error on updating locking status.. + /// + public static string ActivityTextErrorConnectionUpdateingLockstate { + get { + return ResourceManager.GetString("ActivityTextErrorConnectionUpdateingLockstate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error: Deserialization failed.. + /// + public static string ActivityTextErrorDeserializationException { + get { + return ResourceManager.GetString("ActivityTextErrorDeserializationException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error occurred disconnecting. + /// + public static string ActivityTextErrorDisconnect { + get { + return ResourceManager.GetString("ActivityTextErrorDisconnect", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection interrupted.. + /// + public static string ActivityTextErrorException { + get { + return ResourceManager.GetString("ActivityTextErrorException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error, invalid server response.. + /// + public static string ActivityTextErrorInvalidResponseException { + get { + return ResourceManager.GetString("ActivityTextErrorInvalidResponseException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No web error on updating locking status.. + /// + public static string ActivityTextErrorNoWebUpdateingLockstate { + get { + return ResourceManager.GetString("ActivityTextErrorNoWebUpdateingLockstate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Battery status cannot be read.. + /// + public static string ActivityTextErrorReadingChargingLevelGeneral { + get { + return ResourceManager.GetString("ActivityTextErrorReadingChargingLevelGeneral", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Battery status can only be read when bike is nearby.. + /// + public static string ActivityTextErrorReadingChargingLevelOutOfReach { + get { + return ResourceManager.GetString("ActivityTextErrorReadingChargingLevelOutOfReach", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Status error on updating lock state.. + /// + public static string ActivityTextErrorStatusUpdateingLockstate { + get { + return ResourceManager.GetString("ActivityTextErrorStatusUpdateingLockstate", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection interrupted, server unreachable.. + /// + public static string ActivityTextErrorWebConnectFailureException { + get { + return ResourceManager.GetString("ActivityTextErrorWebConnectFailureException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error. Code: {0}.. + /// + public static string ActivityTextErrorWebException { + get { + return ResourceManager.GetString("ActivityTextErrorWebException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection interrupted, server busy.. + /// + public static string ActivityTextErrorWebForbiddenException { + get { + return ResourceManager.GetString("ActivityTextErrorWebForbiddenException", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loading Stations and Bikes.... + /// + public static string ActivityTextMapLoadingStationsAndBikes { + get { + return ResourceManager.GetString("ActivityTextMapLoadingStationsAndBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Check Bluetooth state and location permissions.... + /// + public static string ActivityTextMyBikesCheckBluetoothState { + get { + return ResourceManager.GetString("ActivityTextMyBikesCheckBluetoothState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Loading reserved/ booked bikes.... + /// + public static string ActivityTextMyBikesLoadingBikes { + get { + return ResourceManager.GetString("ActivityTextMyBikesLoadingBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to One moment please.... + /// + public static string ActivityTextOneMomentPlease { + get { + return ResourceManager.GetString("ActivityTextOneMomentPlease", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening lock.... + /// + public static string ActivityTextOpeningLock { + get { + return ResourceManager.GetString("ActivityTextOpeningLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Request server.... + /// + public static string ActivityTextQuerryServer { + get { + return ResourceManager.GetString("ActivityTextQuerryServer", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reading charging level.... + /// + public static string ActivityTextReadingChargingLevel { + get { + return ResourceManager.GetString("ActivityTextReadingChargingLevel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Renting bike.... + /// + public static string ActivityTextRentingBike { + get { + return ResourceManager.GetString("ActivityTextRentingBike", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reserving bike.... + /// + public static string ActivityTextReservingBike { + get { + return ResourceManager.GetString("ActivityTextReservingBike", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Searching locks.... + /// + public static string ActivityTextSearchBikes { + get { + return ResourceManager.GetString("ActivityTextSearchBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Searching lock.... + /// + public static string ActivityTextSearchingLock { + get { + return ResourceManager.GetString("ActivityTextSearchingLock", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updating.... + /// + public static string ActivityTextStartingUpdater { + get { + return ResourceManager.GetString("ActivityTextStartingUpdater", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updating lock state.... + /// + public static string ActivityTextStartingUpdatingLockingState { + get { + return ResourceManager.GetString("ActivityTextStartingUpdatingLockingState", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Updated to latest lock firmware.. + /// + public static string ChangeLog3_0_203 { + get { + return ResourceManager.GetString("ChangeLog3_0_203", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bluetooth communication inproved.. + /// + public static string ChangeLog3_0_204 { + get { + return ResourceManager.GetString("ChangeLog3_0_204", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Nicer station markers for iOS.. + /// + public static string ChangeLog3_0_205 { + get { + return ResourceManager.GetString("ChangeLog3_0_205", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bluetooth and geolocation functionality improved.. + /// + public static string ChangeLog3_0_206 { + get { + return ResourceManager.GetString("ChangeLog3_0_206", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minor fixes related to renting functionality. + ///Software packages updated. + ///Targets Android 11.. + /// + public static string ChangeLog3_0_207 { + get { + return ResourceManager.GetString("ChangeLog3_0_207", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minor fixes.. + /// + public static string ChangeLog3_0_208 { + get { + return ResourceManager.GetString("ChangeLog3_0_208", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minor fix: Bikes are disconnected as soon as becoming disposable.. + /// + public static string ChangeLog3_0_209 { + get { + return ResourceManager.GetString("ChangeLog3_0_209", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Multiple operators support.. + /// + public static string ChangeLog3_0_214 { + get { + return ResourceManager.GetString("ChangeLog3_0_214", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Layout of "Whats New"-dialog improved :-). + /// + public static string ChangeLog3_0_215 { + get { + return ResourceManager.GetString("ChangeLog3_0_215", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GUI layout improved.. + /// + public static string ChangeLog3_0_216 { + get { + return ResourceManager.GetString("ChangeLog3_0_216", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Packages updated.. + /// + public static string ChangeLog3_0_217 { + get { + return ResourceManager.GetString("ChangeLog3_0_217", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minor fixes.. + /// + public static string ChangeLog3_0_218 { + get { + return ResourceManager.GetString("ChangeLog3_0_218", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Icons added to flyout menu.. + /// + public static string ChangeLog3_0_219 { + get { + return ResourceManager.GetString("ChangeLog3_0_219", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GUI layout improved.. + /// + public static string ChangeLog3_0_220 { + get { + return ResourceManager.GetString("ChangeLog3_0_220", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to GUI layout improved.. + /// + public static string ChangeLog3_0_221 { + get { + return ResourceManager.GetString("ChangeLog3_0_221", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Minor bugfix and improvements.. + /// + public static string ChangeLog3_0_222 { + get { + return ResourceManager.GetString("ChangeLog3_0_222", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock of rented bike can not be found.. + /// + public static string ErrorBookedSearchMessage { + get { + return ResourceManager.GetString("ErrorBookedSearchMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock is blocked. Please ensure that no spoke or any other obstacle prevents the lock from closing and try again.. + /// + public static string ErrorCloseLockBoldBlockedMessage { + get { + return ResourceManager.GetString("ErrorCloseLockBoldBlockedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock can only be closed if bike is not moving. Please park bike and try again.. + /// + public static string ErrorCloseLockMovingMessage { + get { + return ResourceManager.GetString("ErrorCloseLockMovingMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock cannot be closed until bike is near.. + /// + public static string ErrorCloseLockOutOfReachMessage { + get { + return ResourceManager.GetString("ErrorCloseLockOutOfReachMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock cannot be closed until bike is near. + ///Please try again to close bike or report bike to support!. + /// + public static string ErrorCloseLockOutOfReachStateReservedMessage { + get { + return ResourceManager.GetString("ErrorCloseLockOutOfReachStateReservedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to After try to close lock state open is reported.. + /// + public static string ErrorCloseLockStillOpenMessage { + get { + return ResourceManager.GetString("ErrorCloseLockStillOpenMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock can not be closed!. + /// + public static string ErrorCloseLockTitle { + get { + return ResourceManager.GetString("ErrorCloseLockTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock reports state "{0}".. + /// + public static string ErrorCloseLockUnexpectedStateMessage { + get { + return ResourceManager.GetString("ErrorCloseLockUnexpectedStateMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please try to lock again or report bike to support! + ///{0}. + /// + public static string ErrorCloseLockUnkErrorMessage { + get { + return ResourceManager.GetString("ErrorCloseLockUnkErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock is blocked. Please ensure that no obstacle prevents lock from opening and try again.. + /// + public static string ErrorOpenLockMessage { + get { + return ResourceManager.GetString("ErrorOpenLockMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock cannot be opened until bike is near.. + /// + public static string ErrorOpenLockOutOfReadMessage { + get { + return ResourceManager.GetString("ErrorOpenLockOutOfReadMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to After try to open lock state closed is reported.. + /// + public static string ErrorOpenLockStillClosedMessage { + get { + return ResourceManager.GetString("ErrorOpenLockStillClosedMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock can not be opened!. + /// + public static string ErrorOpenLockTitle { + get { + return ResourceManager.GetString("ErrorOpenLockTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Lock of reserved bike can not be found.. + /// + public static string ErrorReservedSearchMessage { + get { + return ResourceManager.GetString("ErrorReservedSearchMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returning bike at an unknown location is not possible. + ///Bike can be returned if + ///- location information is available when closing lock + ///- bike is in reach and location information is available when pressing button "Return bike". + /// + public static string ErrorReturnBikeLockClosedNoGPSMessage { + get { + return ResourceManager.GetString("ErrorReturnBikeLockClosedNoGPSMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returning bike at an unknown location is not possible. + ///Bike can only be returned if bike is in reach and location information is available.. + /// + public static string ErrorReturnBikeLockOpenNoGPSMessage { + get { + return ResourceManager.GetString("ErrorReturnBikeLockOpenNoGPSMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Returning bike outside of station is not possible. Distance to station {0} is {1} m.. + /// + public static string ErrorReturnBikeNotAtStationMessage { + get { + return ResourceManager.GetString("ErrorReturnBikeNotAtStationMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error returning bike!. + /// + public static string ErrorReturnBikeNotAtStationTitle { + get { + return ResourceManager.GetString("ErrorReturnBikeNotAtStationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attachment could not be created.. + /// + public static string ErrorSupportmailCreateAttachment { + get { + return ResourceManager.GetString("ErrorSupportmailCreateAttachment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening mail app failed.. + /// + public static string ErrorSupportmailMailingFailed { + get { + return ResourceManager.GetString("ErrorSupportmailMailingFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Opening phone app failed.. + /// + public static string ErrorSupportmailPhoningFailed { + get { + return ResourceManager.GetString("ErrorSupportmailPhoningFailed", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The rental of bike No. {0} has failed.. + /// + public static string ExceptionTextRentingBikeFailedGeneral { + get { + return ResourceManager.GetString("ExceptionTextRentingBikeFailedGeneral", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The rental of bike No. {0} has failed, the bike is currently unavailable.{1}. + /// + public static string ExceptionTextRentingBikeFailedUnavailalbe { + get { + return ResourceManager.GetString("ExceptionTextRentingBikeFailedUnavailalbe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The reservation of bike no. {0} has failed.. + /// + public static string ExceptionTextReservationBikeFailedGeneral { + get { + return ResourceManager.GetString("ExceptionTextReservationBikeFailedGeneral", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to The reservation of bike No. {0} has failed, the bike is currently unavailable.{1}. + /// + public static string ExceptionTextReservationBikeFailedUnavailalbe { + get { + return ResourceManager.GetString("ExceptionTextReservationBikeFailedUnavailalbe", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to About {0}. + /// + public static string MarkingAbout { + get { + return ResourceManager.GetString("MarkingAbout", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Account. + /// + public static string MarkingAccount { + get { + return ResourceManager.GetString("MarkingAccount", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Invalid state for bike {0} detected. + ///Bike should always be disconnected when not reserved or rented. + ///Please restart app in order to get bike info.. + /// + public static string MarkingBikeInfoErrorStateDisposableClosedDetected { + get { + return ResourceManager.GetString("MarkingBikeInfoErrorStateDisposableClosedDetected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Unknown status for bike {0} detected.. + /// + public static string MarkingBikeInfoErrorStateUnknownDetected { + get { + return ResourceManager.GetString("MarkingBikeInfoErrorStateUnknownDetected", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bike Location {0}. + /// + public static string MarkingBikesAtStationTitle { + get { + return ResourceManager.GetString("MarkingBikesAtStationTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Contact. + /// + public static string MarkingFeedbackAndContact { + get { + return ResourceManager.GetString("MarkingFeedbackAndContact", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Instructions. + /// + public static string MarkingFeesAndBikes { + get { + return ResourceManager.GetString("MarkingFeesAndBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logged in as {0}.. + /// + public static string MarkingLoggedInStateInfoLoggedIn { + get { + return ResourceManager.GetString("MarkingLoggedInStateInfoLoggedIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Logged in as {0} at {1}.. + /// + public static string MarkingLoggedInStateInfoLoggedInGroup { + get { + return ResourceManager.GetString("MarkingLoggedInStateInfoLoggedInGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No user logged in.. + /// + public static string MarkingLoggedInStateInfoNotLoggedIn { + get { + return ResourceManager.GetString("MarkingLoggedInStateInfoNotLoggedIn", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login. + /// + public static string MarkingLogin { + get { + return ResourceManager.GetString("MarkingLogin", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to E-mail address. + /// + public static string MarkingLoginEmailAddressLabel { + get { + return ResourceManager.GetString("MarkingLoginEmailAddressLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to E-mail address. + /// + public static string MarkingLoginEmailAddressPlaceholder { + get { + return ResourceManager.GetString("MarkingLoginEmailAddressPlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Instructions TINK bikes. + /// + public static string MarkingLoginInstructions { + get { + return ResourceManager.GetString("MarkingLoginInstructions", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to If you already have a "konrad" or "TINK" account, you can stop using both rental bike systems with your existing account! Simply confirm the corresponding terms and conditions.. + /// + public static string MarkingLoginInstructionsTinkKonradMessage { + get { + return ResourceManager.GetString("MarkingLoginInstructionsTinkKonradMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to For your information! + ///. + /// + public static string MarkingLoginInstructionsTinkKonradTitle { + get { + return ResourceManager.GetString("MarkingLoginInstructionsTinkKonradTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password, minimum length 8 characters. + /// + public static string MarkingLoginPasswordLabel { + get { + return ResourceManager.GetString("MarkingLoginPasswordLabel", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Password. + /// + public static string MarkingLoginPasswordPlaceholder { + get { + return ResourceManager.GetString("MarkingLoginPasswordPlaceholder", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bike Locations. + /// + public static string MarkingMapPage { + get { + return ResourceManager.GetString("MarkingMapPage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to My Bikes. + /// + public static string MarkingMyBikes { + get { + return ResourceManager.GetString("MarkingMyBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Settings. + /// + public static string MarkingSettings { + get { + return ResourceManager.GetString("MarkingSettings", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Instructions. + /// + public static string MarkingTabBikes { + get { + return ResourceManager.GetString("MarkingTabBikes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Pricing. + /// + public static string MarkingTabFees { + get { + return ResourceManager.GetString("MarkingTabFees", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No. + /// + public static string MessageAnswerNo { + get { + return ResourceManager.GetString("MessageAnswerNo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to OK. + /// + public static string MessageAnswerOk { + get { + return ResourceManager.GetString("MessageAnswerOk", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + public static string MessageAnswerYes { + get { + return ResourceManager.GetString("MessageAnswerYes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to This version of the {0} App is outdated. Please update to the latest version.. + /// + public static string MessageAppVersionIsOutdated { + get { + return ResourceManager.GetString("MessageAppVersionIsOutdated", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please enable Bluetooth to manage bike lock/locks.. + /// + public static string MessageBikesManagementBluetoothActivation { + get { + return ResourceManager.GetString("MessageBikesManagementBluetoothActivation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please activate location so that bike lock can be found!. + /// + public static string MessageBikesManagementLocationActivation { + get { + return ResourceManager.GetString("MessageBikesManagementLocationActivation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please allow location sharing so that bike lock/locks can be managed.. + /// + public static string MessageBikesManagementLocationPermission { + get { + return ResourceManager.GetString("MessageBikesManagementLocationPermission", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please allow location sharing so that bike lock/locks can be managed. + ///Open sharing dialog?. + /// + public static string MessageBikesManagementLocationPermissionOpenDialog { + get { + return ResourceManager.GetString("MessageBikesManagementLocationPermissionOpenDialog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to €/day. + /// + public static string MessageBikesManagementMaxFeeEuroPerDay { + get { + return ResourceManager.GetString("MessageBikesManagementMaxFeeEuroPerDay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Subscription price. + /// + public static string MessageBikesManagementTariffDescriptionAboEuroPerMonth { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionAboEuroPerMonth", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to €/hour. + /// + public static string MessageBikesManagementTariffDescriptionEuroPerHour { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionEuroPerHour", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rental fees. + /// + public static string MessageBikesManagementTariffDescriptionFeeEuroPerHour { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionFeeEuroPerHour", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Free use. + /// + public static string MessageBikesManagementTariffDescriptionFreeTimePerSession { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionFreeTimePerSession", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to hour(s)/day. + /// + public static string MessageBikesManagementTariffDescriptionHour { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionHour", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Max. fee. + /// + public static string MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Tariff {0}, nr. {1}. + /// + public static string MessageBikesManagementTariffDescriptionTariffHeader { + get { + return ResourceManager.GetString("MessageBikesManagementTariffDescriptionTariffHeader", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please allow location sharing so that map can be centered. + ///Open sharing dialog?. + /// + public static string MessageCenterMapLocationPermissionOpenDialog { + get { + return ResourceManager.GetString("MessageCenterMapLocationPermissionOpenDialog", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Questions? Remarks? Criticism?. + /// + public static string MessageContactMail { + get { + return ResourceManager.GetString("MessageContactMail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attention: Lock is closed! + ///{0} + ///{1}. + /// + public static string MessageErrorLockIsClosedThreeLines { + get { + return ResourceManager.GetString("MessageErrorLockIsClosedThreeLines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attention: Lock is closed! + ///{0}. + /// + public static string MessageErrorLockIsClosedTwoLines { + get { + return ResourceManager.GetString("MessageErrorLockIsClosedTwoLines", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Login cookie must not be empty. {0}. + /// + public static string MessageLoginConnectionErrorMessage { + get { + return ResourceManager.GetString("MessageLoginConnectionErrorMessage", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error during registration!. + /// + public static string MessageLoginConnectionErrorTitle { + get { + return ResourceManager.GetString("MessageLoginConnectionErrorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error during login!. + /// + public static string MessageLoginErrorTitle { + get { + return ResourceManager.GetString("MessageLoginErrorTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please connect to Internet to recover the password.. + /// + public static string MessageLoginRecoverPassword { + get { + return ResourceManager.GetString("MessageLoginRecoverPassword", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Please connect to Internet to register.. + /// + public static string MessageLoginRegisterNoNet { + get { + return ResourceManager.GetString("MessageLoginRegisterNoNet", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to User {0} successfully logged in.. + /// + public static string MessageLoginWelcome { + get { + return ResourceManager.GetString("MessageLoginWelcome", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome!. + /// + public static string MessageLoginWelcomeTitle { + get { + return ResourceManager.GetString("MessageLoginWelcomeTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Welcome to {0}!. + /// + public static string MessageLoginWelcomeTitleGroup { + get { + return ResourceManager.GetString("MessageLoginWelcomeTitleGroup", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Session has expired. + ///Either there are more than 8 devices in use or the user's account is no longer valid. + ///Use of app is restricted to maximu 8 devices per account. + ///Please login to app once again. In case this fails please check on website if the account is still valid.. + /// + public static string MessageMapPageErrorAuthcookieUndefined { + get { + return ResourceManager.GetString("MessageMapPageErrorAuthcookieUndefined", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rent bike {0} and open lock?. + /// + public static string MessageOpenLockAndBookeBike { + get { + return ResourceManager.GetString("MessageOpenLockAndBookeBike", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Urgent question related to {0}? (Monday-Friday: 10:00 18:00). + /// + public static string MessagePhoneMail { + get { + return ResourceManager.GetString("MessagePhoneMail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Are you enjoying the {0}-App?. + /// + public static string MessageRateMail { + get { + return ResourceManager.GetString("MessageRateMail", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Connection error when renting the bike!. + /// + public static string MessageRentingBikeErrorConnectionTitle { + get { + return ResourceManager.GetString("MessageRentingBikeErrorConnectionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Error when renting the bike!. + /// + public static string MessageRentingBikeErrorGeneralTitle { + get { + return ResourceManager.GetString("MessageRentingBikeErrorGeneralTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A rental of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made.. + /// + public static string MessageRentingBikeErrorTooManyReservationsRentals { + get { + return ResourceManager.GetString("MessageRentingBikeErrorTooManyReservationsRentals", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to A reservation of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made.. + /// + public static string MessageReservationBikeErrorTooManyReservationsRentals { + get { + return ResourceManager.GetString("MessageReservationBikeErrorTooManyReservationsRentals", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Hint. + /// + public static string MessageTitleHint { + get { + return ResourceManager.GetString("MessageTitleHint", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Warning. + /// + public static string MessageWaring { + get { + return ResourceManager.GetString("MessageWaring", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to No. + /// + public static string QuestionAnswerNo { + get { + return ResourceManager.GetString("QuestionAnswerNo", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Yes. + /// + public static string QuestionAnswerYes { + get { + return ResourceManager.GetString("QuestionAnswerYes", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Cancel reservation for bike {0}?. + /// + public static string QuestionCancelReservation { + get { + return ResourceManager.GetString("QuestionCancelReservation", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Reserve bike {0} free of charge for {1} min?. + /// + public static string QuestionReserveBike { + get { + return ResourceManager.GetString("QuestionReserveBike", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} app request. + /// + public static string QuestionSupportmailAnswerApp { + get { + return ResourceManager.GetString("QuestionSupportmailAnswerApp", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to {0} request. + /// + public static string QuestionSupportmailAnswerOperator { + get { + return ResourceManager.GetString("QuestionSupportmailAnswerOperator", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Attach file containing diagnosis information to mail?. + /// + public static string QuestionSupportmailAttachment { + get { + return ResourceManager.GetString("QuestionSupportmailAttachment", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Does your request/ comment relate to the {0}-app or to a more general subject?. + /// + public static string QuestionSupportmailSubject { + get { + return ResourceManager.GetString("QuestionSupportmailSubject", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Question. + /// + public static string QuestionTitle { + get { + return ResourceManager.GetString("QuestionTitle", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Available.. + /// + public static string StatusTextAvailable { + get { + return ResourceManager.GetString("StatusTextAvailable", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Bike is rented.. + /// + public static string StatusTextBooked { + get { + return ResourceManager.GetString("StatusTextBooked", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code {0}, location {1}, rented since {2}.. + /// + public static string StatusTextBookedCodeLocationSince { + get { + return ResourceManager.GetString("StatusTextBookedCodeLocationSince", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code {0}, rented since {1}.. + /// + public static string StatusTextBookedCodeSince { + get { + return ResourceManager.GetString("StatusTextBookedCodeSince", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Rented since {0}.. + /// + public static string StatusTextBookedSince { + get { + return ResourceManager.GetString("StatusTextBookedSince", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code {0}, location {1}, max. reservation time of {2} minutes expired.. + /// + public static string StatusTextReservationExpiredCodeLocationMaxReservationTime { + get { + return ResourceManager.GetString("StatusTextReservationExpiredCodeLocationMaxReservationTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code {0}, location {1}, still {2} minutes reserved.. + /// + public static string StatusTextReservationExpiredCodeLocationReservationTime { + get { + return ResourceManager.GetString("StatusTextReservationExpiredCodeLocationReservationTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code {0}, max. reservation time of {1} minutes expired.. + /// + public static string StatusTextReservationExpiredCodeMaxReservationTime { + get { + return ResourceManager.GetString("StatusTextReservationExpiredCodeMaxReservationTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Code {0}, still {1} minutes reserved.. + /// + public static string StatusTextReservationExpiredCodeRemaining { + get { + return ResourceManager.GetString("StatusTextReservationExpiredCodeRemaining", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location {0}, max. reservation time of {1} minutes expired.. + /// + public static string StatusTextReservationExpiredLocationMaxReservationTime { + get { + return ResourceManager.GetString("StatusTextReservationExpiredLocationMaxReservationTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Location {0}, still {1} minutes reserved.. + /// + public static string StatusTextReservationExpiredLocationReservationTime { + get { + return ResourceManager.GetString("StatusTextReservationExpiredLocationReservationTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Max. reservation time of {0} minutes expired.. + /// + public static string StatusTextReservationExpiredMaximumReservationTime { + get { + return ResourceManager.GetString("StatusTextReservationExpiredMaximumReservationTime", resourceCulture); + } + } + + /// + /// Looks up a localized string similar to Still {0} minutes reserved.. + /// + public static string StatusTextReservationExpiredRemaining { + get { + return ResourceManager.GetString("StatusTextReservationExpiredRemaining", resourceCulture); + } + } + } +} diff --git a/TINKLib/MultilingualResources/AppResources.de.resx b/TINKLib/MultilingualResources/AppResources.de.resx new file mode 100644 index 0000000..bb76d3b --- /dev/null +++ b/TINKLib/MultilingualResources/AppResources.de.resx @@ -0,0 +1,546 @@ + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Rad mieten oder Schloss schließen + + + Reservierung aufheben + + + Schloss schließen + + + Schloss schließen & Miete beenden + + + Schloss öffnen + + + Schloss öffnen & Rad mieten + + + Schloss öffnen & Miete fortsetzen + + + Rad reservieren + + + Miete beenden + + + Fahrradstandorte + + + Benutzer {0} erfolgreich angemeldet. + + + Willkommen! + + + Wilkommen bei {0}! + + + Angemeldet als {0}. + + + Angemeldet als {0} bei {1}. + + + Kein Benutzer angemeldet. + + + Diese version der {0} App ist veraltet. Bitte auf aktuelle Version aktualisieren. + + + Betrifft die Anfrage/ Anmerkung die {0}-App oder ein allgemeines Thema? + + + {0}-App Anfrage + + + {0} Anfrage + + + Über {0} + + + Konto + + + Kontakt + + + Anmelden + + + Meine Räder + + + Bedienung + + + Einstellungen + + + Bedienung + + + Tarife + + + Schloss ist blockiert. Bitte Ursache von Blockierung beheben und Vorgang wiederholen. + + + Schloss ist blockiert. Bitte sicherstellen, dass keine Speiche oder ein anderer Gegenstand das Schloss blockiert und Vorgang wiederholen. + + + Schloss kann erst geschlossen werden, wenn Rad nicht mehr bewegt wird. Bitte Rad abstellen und Vorgang wiederholen. + + + Schloss des gemieteten Rads kann nicht gefunden werden. + + + Schloss des reservierten Rads kann nicht gefunden werden. + + + Lade Räder an Station... + + + Lade meine Räder... + + + Suche Schlösser... + + + Prüfe Berechtigungen... + + + Zentriere Karte... + + + Lade Stationen und Räder... + + + Rückgabe ausserhalb von Station nicht möglich. Entfernung zur Station {0} ist {1} m. + + + Fehler bei Radrückgabe! + + + Fahrradrückgabe an unbekanntem Standort nicht möglich. +Eine Radrückgabe ist möglich, wenn +- beim Schliessen des Schlosses Standortinformation verfügbar ist +- beim Drücken von "Rad zurückgeben" das Rad in Reichweite ist und Standortinformation verfügbar ist. + + + Fahrradrückgabe an unbekanntem Standort nicht möglich. +Eine Radrückgabe ist nur möglich, wenn das Rad in Reichweite ist und Standortinformation verfügbar ist. + + + Mailanhang konnte nich erzeugt werden. + + + Mailapp konnte nicht geöffnet werden. + + + Telefonapp konnte nicht geöffnet werden. + + + OK + + + Fragen? Hinweise? Kritik? + + + Eilige Frage rund um {0}? (Montag-Freitag: 10:00 18:00) + + + Gefällt die {0}-App? + + + Warnung + + + Nein + + + Ja + + + Soll der Mail eine Datei mit Diagnoseinformationen angehängt werden? + + + Frage + + + Sitzung ist abgelaufen. +Entweder es sind mehr als 8 Geräte in Benutzung oder das Konto ist nicht mehr gültig. +Die Nutzung der App ist auf maximal 8 Geräten pro Konto möglich. +Bitte erneut in App anmelden. Sollte dies fehlschlagen bitte auf Website prüfen, ob das Konto noch gültig ist. + + + Fahrradstandort {0} + + + Code ist {0}, max. Reservierungszeit von {1} Min. abgelaufen. + + + + Code ist {0}, noch {1} Minuten reserviert. + + + + Rad ist gemietet. + + + Gemietet seit {0}. + + + Noch {0} Minuten reserviert. + + + Max. Reservierungszeit von {0} Min. abgelaufen. + + + Code ist {0}, gemietet seit {1}. + + + Standort {0}, max. Reservierungszeit von {1} Min. abgelaufen. + + + + Frei. + + + Code {0}, Standort {1}, gemietet seit {2}. + + + + Code ist {0}, Standort {1}, max. Reservierungszeit von {2} Min. abgelaufen. + + + + Code ist {0}, Standort {1}, noch {2} Minuten reserviert. + + + + Standort {0}, noch {1} Minuten reserviert. + + + + Anmelden + + + Passwort vergessen? + + + Registrieren + + + E-Mail Adresse + + + E-Mail Adresse + + + Passwort, Mindestlänge 8 Zeichen + + + Passwort + + + Bewertung abgeben + + + Anleitung TINK Räder + + + Falls Sie bereits einen "konrad" oder "TINK" Account besitzen, können Sie mit Ihren bestehendem Account die Nutzung beider Mietradsysteme einstellen! Einfach dazu die entsprechende AGB bestätigen. + + + Zur Information! + + + Anmeldungskeks darf nicht leer sein. {0} + + + Verbindungsfehler beim Registrieren! + + + Fehler bei der Anmeldung! + + + Bitte mit dem Internet verbinden zum Wiederherstellen des Passworts. + + + Bitte mit dem Internet verbinden zum Registrieren. + + + Hinweis + + + Nach Versuch Schloss zu öffnen wird Status geschlossen zurückgemeldet. + + + Schloss kann erst geöffnet werden, wenn Rad in der Nähe ist. + + + Schloss kann nicht geöffnet werden! + + + Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist. + + + Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist. +Bitte Schließen nochmals versuchen oder Rad dem Support melden! + + + Schloss kann nicht geschlossen werden! + + + Nach Versuch Schloss zu schließen wird Status geöffnet zurückgemeldet. + + + Schloss meldet Status "{0}". + + + Bitte schließen nochmals versuchen oder Rad dem Support melden! +{0} + + + Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können. + + + Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können. +Freigabedialog öffen? + + + Bitte Standortfreigabe erlauben, damit Karte zentriert werden werden kann. +Freigabedialog öffen? + + + Bitte Standort aktivieren, damit Fahrradschloss gefunden werden kann! + + + Nein + + + Ja + + + Bitte Bluetooth aktivieren, damit Fahrradschloss/ Schlösser verwaltet werden können. + + + Schloss suchen + + + Einen Moment bitte... + + + Öffne Schloss... + + + Starte Aktualisierung... + + + Aktualisiere Schlossstatus... + + + Lese Akkustatus... + + + Statusfehler beim Aktualisieren des Schlossstatusses. + + + Verbingungsfehler beim Aktualisieren des Schlossstatusses. + + + Kein Netz beim Aktualisieren des Schlossstatusses. + + + Schließe Schloss... + + + Aktualisierrt auf aktuelle Schloss-Firmware. + + + Bluetooth Kommunikation verbessert. + + + Stationssymbole verbessert. + + + Bluetooth- und Geolocation-Funktionalität verbessert. + + + Kleinere Fehlerbehebungen. +Software Pakete aktualisiert. +Zielplatform Android 11. + + + Fahrrad {0} mieten und Schloss öffnen? + + + Reserviere Rad... + + + Akkustatus kann nicht gelesen werden. + + + Akkustatus kann erst gelesen werden, wenn Rad in der Nähe ist. + + + Miete Rad... + + + Verbingungsfehler beim Mieten des Rads! + + + Fehler beim Mieten des Rads! + + + Eine Miete des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden. + + + Eine Reservierung des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden. + + + Achtung: Schloss wird geschlossen! +{0} +{1} + + + Achtung: Schloss wird geschlossen! +{0} + + + Die Miete des Fahrads Nr. {0} ist fehlgeschlagen. + + + Die Miete des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar. + + + Die Reservierung des Fahrads Nr. {0} ist fehlgeschlagen. + + + Die Reservierung des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar. + + + Kleinere Fehlerbehebungen. + + + Trenne Schloss... + + + Fehler beim Trennen... + + + Ungülitiger Status von Rad {0} erkannt. +Ein nicht reserviertes oder gemietetes Rad sollte immer getrennt sein . +Bitte App neu starten um Rad Infos zu bekommen. + + + Ungültiger Mietstatus von Rad {0} erkannt. + + + + + Reservierung aufheben... + + + Reservierung für Fahrrad {0} aufheben? + + + Kleinere Fehlerbehebung: Verbindung wird getrennt, sobald Rad verfügbar ist. + + + Fahrrad {0} kostenlos für {1} Min. reservieren? + + + Verbindungsfehler: Deserialisierung fehlgeschlagen. + + + Verbindung unterbrochen. + + + Verbindungsfehler, ungülige Serverantwort. + + + Verbindung unterbrochen, Server nicht erreichbar. + + + Verbindungsfehler. Code: {0}. + + + Verbindung unterbrochen, Server beschäftigt. + + + Abo-Preis + + + Mietgebühren + + + Gratis Nutzung + + + Max. Gebühr + + + €/Std. + + + Std./Tag + + + Tarif {0}, Nr. {1} + + + Anfrage Server... + + + Suche Schloss... + + + Mehrbetreiber Unterstützung. + + + Layout des "Whats New"-Dialos verbessert :-) + + + €/Tag + + + Oberflächenlayout verbessert. + + + Pakete aktualisiert. + + + Kleine Verbesserungen. + + + Icons zum Flyout-Menü hinzugefügt. + + + Oberflächenlayout verbessert. + + + Oberflächenlayout verbessert. + + + Kleine Fehlerbehebungen und Verbesserungen. + + \ No newline at end of file diff --git a/TINKLib/MultilingualResources/AppResources.resx b/TINKLib/MultilingualResources/AppResources.resx new file mode 100644 index 0000000..601c72b --- /dev/null +++ b/TINKLib/MultilingualResources/AppResources.resx @@ -0,0 +1,643 @@ + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + text/microsoft-resx + + + 2.0 + + + System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089 + + + Rent bike or close lock + + + Cancel bike reservation + + + Close lock + + + Close lock & return bike + + + Open lock + + + Open lock & rent bike + + + Open lock & continue renting + + + Reserve bike + + + Return bike + + + Loading Stations and Bikes... + + + Lock of rented bike can not be found. + + + Lock is blocked. Please ensure that no spoke or any other obstacle prevents the lock from closing and try again. + + + Lock can only be closed if bike is not moving. Please park bike and try again. + + + Lock is blocked. Please ensure that no obstacle prevents lock from opening and try again. + + + Lock of reserved bike can not be found. + + + Returning bike at an unknown location is not possible. +Bike can be returned if +- location information is available when closing lock +- bike is in reach and location information is available when pressing button "Return bike" + + + Returning bike at an unknown location is not possible. +Bike can only be returned if bike is in reach and location information is available. + + + Returning bike outside of station is not possible. Distance to station {0} is {1} m. + + + Error returning bike! + + + Attachment could not be created. + + + Opening mail app failed. + + + Opening phone app failed. + + + About {0} + + + Account + + + Bike Location {0} + + + Contact + + + Instructions + + + Logged in as {0}. + + + Logged in as {0} at {1}. + + + No user logged in. + + + Login + + + Bike Locations + + + My Bikes + + + Settings + + + Instructions + + + Pricing + + + OK + + + This version of the {0} App is outdated. Please update to the latest version. + + + Questions? Remarks? Criticism? + + + User {0} successfully logged in. + + + Welcome! + + + Welcome to {0}! + + + Session has expired. +Either there are more than 8 devices in use or the user's account is no longer valid. +Use of app is restricted to maximu 8 devices per account. +Please login to app once again. In case this fails please check on website if the account is still valid. + + + Urgent question related to {0}? (Monday-Friday: 10:00 18:00) + + + Are you enjoying the {0}-App? + + + Warning + + + No + + + Yes + + + {0} app request + + + {0} request + + + Attach file containing diagnosis information to mail? + + + Does your request/ comment relate to the {0}-app or to a more general subject? + + + Question + + + Loading bikes located at station... + + + Centering map... + + + Check Bluetooth state and location permissions... + + + Loading reserved/ booked bikes... + + + Searching locks... + + + Code {0}, rented since {1}. + + + Bike is rented. + + + Rented since {0}. + + + Max. reservation time of {0} minutes expired. + + + Code {0}, still {1} minutes reserved. + + + Still {0} minutes reserved. + + + Code {0}, max. reservation time of {1} minutes expired. + + + Available. + + + Code {0}, location {1}, rented since {2}. + + + Code {0}, location {1}, max. reservation time of {2} minutes expired. + + + Code {0}, location {1}, still {2} minutes reserved. + + + Location {0}, max. reservation time of {1} minutes expired. + + + Location {0}, still {1} minutes reserved. + + + Submit rating + + + Login + + + Password forgotten? + + + Register + + + E-mail address + + + E-mail address + + + Instructions TINK bikes + + + If you already have a "konrad" or "TINK" account, you can stop using both rental bike systems with your existing account! Simply confirm the corresponding terms and conditions. + + + For your information! + + + + Password, minimum length 8 characters + + + Password + + + Login cookie must not be empty. {0} + + + Connection error during registration! + + + Error during login! + + + Please connect to Internet to recover the password. + + + Please connect to Internet to register. + + + Hint + + + After try to open lock state closed is reported. + + + Lock cannot be opened until bike is near. + + + Lock can not be opened! + + + Lock cannot be closed until bike is near. + + + Lock cannot be closed until bike is near. +Please try again to close bike or report bike to support! + + + Lock can not be closed! + + + After try to close lock state open is reported. + + + Lock reports state "{0}". + + + Please try to lock again or report bike to support! +{0} + + + Please allow location sharing so that bike lock/locks can be managed. + + + Please allow location sharing so that bike lock/locks can be managed. +Open sharing dialog? + + + Please allow location sharing so that map can be centered. +Open sharing dialog? + + + No + + + Yes + + + Please enable Bluetooth to manage bike lock/locks. + + + Please activate location so that bike lock can be found! + + + Search lock + + + One moment please... + + + Opening lock... + + + Updating... + + + Updating lock state... + + + Reading charging level... + + + Status error on updating lock state. + + + Connection error on updating locking status. + + + No web error on updating locking status. + + + Closing lock... + + + Updated to latest lock firmware. + + + Bluetooth communication inproved. + + + Nicer station markers for iOS. + + + Bluetooth and geolocation functionality improved. + + + Minor fixes related to renting functionality. +Software packages updated. +Targets Android 11. + + + Reserving bike... + + + Rent bike {0} and open lock? + + + Battery status cannot be read. + + + Battery status can only be read when bike is nearby. + + + Renting bike... + + + Connection error when renting the bike! + + + Error when renting the bike! + + + A rental of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made. + + + A reservation of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made. + + + Attention: Lock is closed! +{0} +{1} + + + Attention: Lock is closed! +{0} + + + The rental of bike No. {0} has failed. + + + The rental of bike No. {0} has failed, the bike is currently unavailable.{1} + + + The reservation of bike no. {0} has failed. + + + The reservation of bike No. {0} has failed, the bike is currently unavailable.{1} + + + Minor fixes. + + + Disconnecting lock... + + + Error occurred disconnecting + + + Invalid state for bike {0} detected. +Bike should always be disconnected when not reserved or rented. +Please restart app in order to get bike info. + + + Unknown status for bike {0} detected. + + + Canceling reservation... + + + Cancel reservation for bike {0}? + + + Minor fix: Bikes are disconnected as soon as becoming disposable. + + + Reserve bike {0} free of charge for {1} min? + + + Connection error: Deserialization failed. + + + Connection interrupted. + + + Connection error, invalid server response. + + + Connection interrupted, server unreachable. + + + Connection error. Code: {0}. + + + Connection interrupted, server busy. + + + Subscription price + + + €/hour + + + Rental fees + + + Free use + + + hour(s)/day + + + Max. fee + + + Tariff {0}, nr. {1} + + + Request server... + + + Searching lock... + + + Multiple operators support. + + + Layout of "Whats New"-dialog improved :-) + + + €/day + + + GUI layout improved. + + + Packages updated. + + + Minor fixes. + + + Icons added to flyout menu. + + + GUI layout improved. + + + GUI layout improved. + + + Minor bugfix and improvements. + + \ No newline at end of file diff --git a/TINKLib/MultilingualResources/TINKLib.de.xlf b/TINKLib/MultilingualResources/TINKLib.de.xlf new file mode 100644 index 0000000..bbe5646 --- /dev/null +++ b/TINKLib/MultilingualResources/TINKLib.de.xlf @@ -0,0 +1,730 @@ + + + +
+ +
+ + + + Rent bike or close lock + Rad mieten oder Schloss schließen + + + Cancel bike reservation + Reservierung aufheben + + + Close lock + Schloss schließen + + + Close lock & return bike + Schloss schließen & Miete beenden + + + Open lock + Schloss öffnen + + + Open lock & rent bike + Schloss öffnen & Rad mieten + + + Open lock & continue renting + Schloss öffnen & Miete fortsetzen + + + Reserve bike + Rad reservieren + + + Return bike + Miete beenden + + + Bike Locations + Fahrradstandorte + + + User {0} successfully logged in. + Benutzer {0} erfolgreich angemeldet. + + + Welcome! + Willkommen! + + + Welcome to {0}! + Wilkommen bei {0}! + + + Logged in as {0}. + Angemeldet als {0}. + + + Logged in as {0} at {1}. + Angemeldet als {0} bei {1}. + + + No user logged in. + Kein Benutzer angemeldet. + + + This version of the {0} App is outdated. Please update to the latest version. + Diese version der {0} App ist veraltet. Bitte auf aktuelle Version aktualisieren. + + + Does your request/ comment relate to the {0}-app or to a more general subject? + Betrifft die Anfrage/ Anmerkung die {0}-App oder ein allgemeines Thema? + + + {0} app request + {0}-App Anfrage + + + {0} request + {0} Anfrage + + + About {0} + Über {0} + + + Account + Konto + + + Contact + Kontakt + + + Login + Anmelden + + + My Bikes + Meine Räder + + + Instructions + Bedienung + + + Settings + Einstellungen + + + Instructions + Bedienung + + + Pricing + Tarife + + + Lock is blocked. Please ensure that no obstacle prevents lock from opening and try again. + Schloss ist blockiert. Bitte Ursache von Blockierung beheben und Vorgang wiederholen. + + + Lock is blocked. Please ensure that no spoke or any other obstacle prevents the lock from closing and try again. + Schloss ist blockiert. Bitte sicherstellen, dass keine Speiche oder ein anderer Gegenstand das Schloss blockiert und Vorgang wiederholen. + + + Lock can only be closed if bike is not moving. Please park bike and try again. + Schloss kann erst geschlossen werden, wenn Rad nicht mehr bewegt wird. Bitte Rad abstellen und Vorgang wiederholen. + + + Lock of rented bike can not be found. + Schloss des gemieteten Rads kann nicht gefunden werden. + + + Lock of reserved bike can not be found. + Schloss des reservierten Rads kann nicht gefunden werden. + + + Loading bikes located at station... + Lade Räder an Station... + + + Loading reserved/ booked bikes... + Lade meine Räder... + + + Searching locks... + Suche Schlösser... + + + Check Bluetooth state and location permissions... + Prüfe Berechtigungen... + + + Centering map... + Zentriere Karte... + + + Loading Stations and Bikes... + Lade Stationen und Räder... + + + Returning bike outside of station is not possible. Distance to station {0} is {1} m. + Rückgabe ausserhalb von Station nicht möglich. Entfernung zur Station {0} ist {1} m. + + + Error returning bike! + Fehler bei Radrückgabe! + + + Returning bike at an unknown location is not possible. +Bike can be returned if +- location information is available when closing lock +- bike is in reach and location information is available when pressing button "Return bike" + Fahrradrückgabe an unbekanntem Standort nicht möglich. +Eine Radrückgabe ist möglich, wenn +- beim Schliessen des Schlosses Standortinformation verfügbar ist +- beim Drücken von "Rad zurückgeben" das Rad in Reichweite ist und Standortinformation verfügbar ist. + + + Returning bike at an unknown location is not possible. +Bike can only be returned if bike is in reach and location information is available. + Fahrradrückgabe an unbekanntem Standort nicht möglich. +Eine Radrückgabe ist nur möglich, wenn das Rad in Reichweite ist und Standortinformation verfügbar ist. + + + Attachment could not be created. + Mailanhang konnte nich erzeugt werden. + + + Opening mail app failed. + Mailapp konnte nicht geöffnet werden. + + + Opening phone app failed. + Telefonapp konnte nicht geöffnet werden. + + + OK + OK + + + Questions? Remarks? Criticism? + Fragen? Hinweise? Kritik? + + + Urgent question related to {0}? (Monday-Friday: 10:00 18:00) + Eilige Frage rund um {0}? (Montag-Freitag: 10:00 18:00) + + + Are you enjoying the {0}-App? + Gefällt die {0}-App? + + + Warning + Warnung + + + No + Nein + + + Yes + Ja + + + Attach file containing diagnosis information to mail? + Soll der Mail eine Datei mit Diagnoseinformationen angehängt werden? + + + Question + Frage + + + Session has expired. +Either there are more than 8 devices in use or the user's account is no longer valid. +Use of app is restricted to maximu 8 devices per account. +Please login to app once again. In case this fails please check on website if the account is still valid. + Sitzung ist abgelaufen. +Entweder es sind mehr als 8 Geräte in Benutzung oder das Konto ist nicht mehr gültig. +Die Nutzung der App ist auf maximal 8 Geräten pro Konto möglich. +Bitte erneut in App anmelden. Sollte dies fehlschlagen bitte auf Website prüfen, ob das Konto noch gültig ist. + + + Bike Location {0} + Fahrradstandort {0} + + + Code {0}, max. reservation time of {1} minutes expired. + Code ist {0}, max. Reservierungszeit von {1} Min. abgelaufen. + + + + Code {0}, still {1} minutes reserved. + Code ist {0}, noch {1} Minuten reserviert. + + + + Bike is rented. + Rad ist gemietet. + + + Rented since {0}. + Gemietet seit {0}. + + + Still {0} minutes reserved. + Noch {0} Minuten reserviert. + + + Max. reservation time of {0} minutes expired. + Max. Reservierungszeit von {0} Min. abgelaufen. + + + Code {0}, rented since {1}. + Code ist {0}, gemietet seit {1}. + + + Location {0}, max. reservation time of {1} minutes expired. + Standort {0}, max. Reservierungszeit von {1} Min. abgelaufen. + + + + Available. + Frei. + + + Code {0}, location {1}, rented since {2}. + Code {0}, Standort {1}, gemietet seit {2}. + + + + Code {0}, location {1}, max. reservation time of {2} minutes expired. + Code ist {0}, Standort {1}, max. Reservierungszeit von {2} Min. abgelaufen. + + + + Code {0}, location {1}, still {2} minutes reserved. + Code ist {0}, Standort {1}, noch {2} Minuten reserviert. + + + + Location {0}, still {1} minutes reserved. + Standort {0}, noch {1} Minuten reserviert. + + + + Login + Anmelden + + + Password forgotten? + Passwort vergessen? + + + Register + Registrieren + + + E-mail address + E-Mail Adresse + + + E-mail address + E-Mail Adresse + + + Password, minimum length 8 characters + Passwort, Mindestlänge 8 Zeichen + + + Password + Passwort + + + Submit rating + Bewertung abgeben + + + Instructions TINK bikes + Anleitung TINK Räder + + + If you already have a "konrad" or "TINK" account, you can stop using both rental bike systems with your existing account! Simply confirm the corresponding terms and conditions. + Falls Sie bereits einen "konrad" oder "TINK" Account besitzen, können Sie mit Ihren bestehendem Account die Nutzung beider Mietradsysteme einstellen! Einfach dazu die entsprechende AGB bestätigen. + + + For your information! + + Zur Information! + + + Login cookie must not be empty. {0} + Anmeldungskeks darf nicht leer sein. {0} + + + Connection error during registration! + Verbindungsfehler beim Registrieren! + + + Error during login! + Fehler bei der Anmeldung! + + + Please connect to Internet to recover the password. + Bitte mit dem Internet verbinden zum Wiederherstellen des Passworts. + + + Please connect to Internet to register. + Bitte mit dem Internet verbinden zum Registrieren. + + + Hint + Hinweis + + + After try to open lock state closed is reported. + Nach Versuch Schloss zu öffnen wird Status geschlossen zurückgemeldet. + + + Lock cannot be opened until bike is near. + Schloss kann erst geöffnet werden, wenn Rad in der Nähe ist. + + + Lock can not be opened! + Schloss kann nicht geöffnet werden! + + + Lock cannot be closed until bike is near. + Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist. + + + Lock cannot be closed until bike is near. +Please try again to close bike or report bike to support! + Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist. +Bitte Schließen nochmals versuchen oder Rad dem Support melden! + + + Lock can not be closed! + Schloss kann nicht geschlossen werden! + + + After try to close lock state open is reported. + Nach Versuch Schloss zu schließen wird Status geöffnet zurückgemeldet. + + + Lock reports state "{0}". + Schloss meldet Status "{0}". + + + Please try to lock again or report bike to support! +{0} + Bitte schließen nochmals versuchen oder Rad dem Support melden! +{0} + + + Please allow location sharing so that bike lock/locks can be managed. + Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können. + + + Please allow location sharing so that bike lock/locks can be managed. +Open sharing dialog? + Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können. +Freigabedialog öffen? + + + Please allow location sharing so that map can be centered. +Open sharing dialog? + Bitte Standortfreigabe erlauben, damit Karte zentriert werden werden kann. +Freigabedialog öffen? + + + Please activate location so that bike lock can be found! + Bitte Standort aktivieren, damit Fahrradschloss gefunden werden kann! + + + No + Nein + + + Yes + Ja + + + Please enable Bluetooth to manage bike lock/locks. + Bitte Bluetooth aktivieren, damit Fahrradschloss/ Schlösser verwaltet werden können. + + + Search lock + Schloss suchen + + + One moment please... + Einen Moment bitte... + + + Opening lock... + Öffne Schloss... + + + Updating... + Starte Aktualisierung... + + + Updating lock state... + Aktualisiere Schlossstatus... + + + Reading charging level... + Lese Akkustatus... + + + Status error on updating lock state. + Statusfehler beim Aktualisieren des Schlossstatusses. + + + Connection error on updating locking status. + Verbingungsfehler beim Aktualisieren des Schlossstatusses. + + + No web error on updating locking status. + Kein Netz beim Aktualisieren des Schlossstatusses. + + + Closing lock... + Schließe Schloss... + + + Updated to latest lock firmware. + Aktualisierrt auf aktuelle Schloss-Firmware. + + + Bluetooth communication inproved. + Bluetooth Kommunikation verbessert. + + + Nicer station markers for iOS. + Stationssymbole verbessert. + + + Bluetooth and geolocation functionality improved. + Bluetooth- und Geolocation-Funktionalität verbessert. + + + Minor fixes related to renting functionality. +Software packages updated. +Targets Android 11. + Kleinere Fehlerbehebungen. +Software Pakete aktualisiert. +Zielplatform Android 11. + + + Rent bike {0} and open lock? + Fahrrad {0} mieten und Schloss öffnen? + + + Reserving bike... + Reserviere Rad... + + + Battery status cannot be read. + Akkustatus kann nicht gelesen werden. + + + Battery status can only be read when bike is nearby. + Akkustatus kann erst gelesen werden, wenn Rad in der Nähe ist. + + + Renting bike... + Miete Rad... + + + Connection error when renting the bike! + Verbingungsfehler beim Mieten des Rads! + + + Error when renting the bike! + Fehler beim Mieten des Rads! + + + A rental of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made. + Eine Miete des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden. + + + A reservation of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made. + Eine Reservierung des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden. + + + Attention: Lock is closed! +{0} +{1} + Achtung: Schloss wird geschlossen! +{0} +{1} + + + Attention: Lock is closed! +{0} + Achtung: Schloss wird geschlossen! +{0} + + + The rental of bike No. {0} has failed. + Die Miete des Fahrads Nr. {0} ist fehlgeschlagen. + + + The rental of bike No. {0} has failed, the bike is currently unavailable.{1} + Die Miete des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar. + + + The reservation of bike no. {0} has failed. + Die Reservierung des Fahrads Nr. {0} ist fehlgeschlagen. + + + The reservation of bike No. {0} has failed, the bike is currently unavailable.{1} + Die Reservierung des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar. + + + Minor fixes. + Kleinere Fehlerbehebungen. + + + Disconnecting lock... + Trenne Schloss... + + + Error occurred disconnecting + Fehler beim Trennen... + + + Invalid state for bike {0} detected. +Bike should always be disconnected when not reserved or rented. +Please restart app in order to get bike info. + Ungülitiger Status von Rad {0} erkannt. +Ein nicht reserviertes oder gemietetes Rad sollte immer getrennt sein . +Bitte App neu starten um Rad Infos zu bekommen. + + + Unknown status for bike {0} detected. + Ungültiger Mietstatus von Rad {0} erkannt. + + + + + Canceling reservation... + Reservierung aufheben... + + + Cancel reservation for bike {0}? + Reservierung für Fahrrad {0} aufheben? + + + Minor fix: Bikes are disconnected as soon as becoming disposable. + Kleinere Fehlerbehebung: Verbindung wird getrennt, sobald Rad verfügbar ist. + + + Reserve bike {0} free of charge for {1} min? + Fahrrad {0} kostenlos für {1} Min. reservieren? + + + Connection error: Deserialization failed. + Verbindungsfehler: Deserialisierung fehlgeschlagen. + + + Connection interrupted. + Verbindung unterbrochen. + + + Connection error, invalid server response. + Verbindungsfehler, ungülige Serverantwort. + + + Connection interrupted, server unreachable. + Verbindung unterbrochen, Server nicht erreichbar. + + + Connection error. Code: {0}. + Verbindungsfehler. Code: {0}. + + + Connection interrupted, server busy. + Verbindung unterbrochen, Server beschäftigt. + + + Subscription price + Abo-Preis + + + Rental fees + Mietgebühren + + + Free use + Gratis Nutzung + + + Max. fee + Max. Gebühr + + + €/hour + €/Std. + + + hour(s)/day + Std./Tag + + + Tariff {0}, nr. {1} + Tarif {0}, Nr. {1} + + + Request server... + Anfrage Server... + + + Searching lock... + Suche Schloss... + + + Multiple operators support. + Mehrbetreiber Unterstützung. + + + Layout of "Whats New"-dialog improved :-) + Layout des "Whats New"-Dialos verbessert :-) + + + €/day + €/Tag + + + GUI layout improved. + Oberflächenlayout verbessert. + + + Packages updated. + Pakete aktualisiert. + + + Minor fixes. + Kleine Verbesserungen. + + + Icons added to flyout menu. + Icons zum Flyout-Menü hinzugefügt. + + + GUI layout improved. + Oberflächenlayout verbessert. + + + GUI layout improved. + Oberflächenlayout verbessert. + + + Minor bugfix and improvements. + Kleine Fehlerbehebungen und Verbesserungen. + + + +
+
\ No newline at end of file diff --git a/TINKLib/Repository/CopriCallsHttps.cs b/TINKLib/Repository/CopriCallsHttps.cs new file mode 100644 index 0000000..0f8b202 --- /dev/null +++ b/TINKLib/Repository/CopriCallsHttps.cs @@ -0,0 +1,785 @@ + +using Serilog; +using System; +using System.IO; +using System.Net; +using System.Text; +using System.Threading.Tasks; +using TINK.Model.Repository.Exception; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Model.Logging; +using TINK.Repository.Response; + +namespace TINK.Model.Repository +{ + /// Object which manages calls to copri. + public class CopriCallsHttps : ICopriServer + { + /// Builds requests. + private IRequestBuilder requestBuilder; + + /// Initializes a instance of the copri calls https object. + /// Host to connect to. + /// Id of the merchant. + /// Holds the name and version of the TINKApp. + /// Session cookie if user is logged in, null otherwise. + public CopriCallsHttps( + Uri p_oCopriHost, + string p_strMerchantId, + string userAgent, + string sessionCookie = null) + { + m_oCopriHost = p_oCopriHost + ?? throw new System.Exception($"Can not construct {GetType().ToString()}- object. Uri of copri host must not be null."); + + UserAgent = !string.IsNullOrEmpty(userAgent) + ? userAgent + : throw new System.Exception($"Can not construct {GetType().ToString()}- object. User agent must not be null or empty."); + + requestBuilder = string.IsNullOrEmpty(sessionCookie) + ? new RequestBuilder(p_strMerchantId) as IRequestBuilder + : new RequestBuilderLoggedIn(p_strMerchantId, sessionCookie); + } + + /// Holds the URL for rest calls. + private Uri m_oCopriHost; + + /// Spacifies name and version of app. + private string UserAgent { get; } + + /// Returns true because value requested form copri server are returned. + public bool IsConnected => true; + + /// Gets the merchant id. + public string MerchantId => requestBuilder.MerchantId; + + /// Gets the session cookie if user is logged in, an empty string otherwise. + public string SessionCookie => requestBuilder.SessionCookie; + + /// Logs user in. + /// Mailaddress of user to log in. + /// Password to log in. + /// Id specifying user and hardware. + /// Response which holds auth cookie + public async Task DoAuthorizationAsync( + string mailAddress, + string password, + string deviceId) + { + return await DoAuthorizationAsync( + m_oCopriHost.AbsoluteUri, + requestBuilder.DoAuthorization(mailAddress, password, deviceId), + () => requestBuilder.DoAuthorization(mailAddress, "********", deviceId), + UserAgent); + } + + /// Logs user out. + /// Response which holds auth cookie + public async Task DoAuthoutAsync() + { + return await DoAuthoutAsync(m_oCopriHost.AbsoluteUri, requestBuilder.DoAuthout(), UserAgent); + } + + /// Gets bikes available. + /// Response holding list of bikes. + public async Task GetBikesAvailableAsync() + { + return await GetBikesAvailableAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesAvailable(), UserAgent); + } + + /// Gets a list of bikes reserved/ booked by acctive user. + /// Response holding list of bikes. + public async Task GetBikesOccupiedAsync() + { + try + { + return await GetBikesOccupiedAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesOccupied(), UserAgent); + } + catch (NotSupportedException) + { + // No user logged in. + await Task.CompletedTask; + return ResponseHelper.GetBikesOccupiedNone(); + } + } + + /// Get list of stations. + /// List of files. + public async Task GetStationsAsync() + { + return await GetStationsAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetStations(), UserAgent); + } + + /// Get authentication keys. + /// Id of the bike to get keys for. + /// Response holding authentication keys. + public async Task GetAuthKeys(int bikeId) + => await GetAuthKeysAsync(m_oCopriHost.AbsoluteUri, requestBuilder.CalculateAuthKeys(bikeId), UserAgent); + + + /// Gets booking request response. + /// Id of the bike to book. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Booking response. + public async Task DoReserveAsync( + int bikeId, + Uri operatorUri) + { + return await DoReserveAsync( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.DoReserve(bikeId), + UserAgent); + } + + /// Gets canel booking request response. + /// Id of the bike to book. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on cancel booking request. + public async Task DoCancelReservationAsync( + int bikeId, + Uri operatorUri) + { + return await DoCancelReservationAsync( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.DoCancelReservation(bikeId), + UserAgent); + } + + /// Get authentication keys. + /// Id of the bike to get keys for. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response holding authentication keys. + public async Task CalculateAuthKeysAsync( + int bikeId, + Uri operatorUri) + => await GetAuthKeysAsync( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.CalculateAuthKeys(bikeId), + UserAgent); + + /// Updates lock state for a booked bike. + /// Id of the bike to update locking state for. + /// Geolocation of lock. + /// New locking state. + /// Holds the filling level percentage of the battery. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on updating locking state. + public async Task UpdateLockingStateAsync( + int bikeId, + LocationDto location, + lock_state state, + double batteryLevel, + Uri operatorUri) + { + return await DoUpdateLockingStateAsync( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.UpateLockingState(bikeId, location, state, batteryLevel), + UserAgent); + } + + /// Gets booking request request. + /// Id of the bike to book. + /// Used to publish GUID from app to copri. Used for initial setup of bike in copri. + /// Holds the filling level percentage of the battery. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Requst on booking request. + public async Task DoBookAsync( + int bikeId, + Guid guid, + double batteryPercentage, + Uri operatorUri) + { + return await DoBookAsync( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.DoBook(bikeId, guid, batteryPercentage), + UserAgent); + } + + /// Returns a bike. + /// Id of the bike to return. + /// Geolocation of lock. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on returning request. + public async Task DoReturn( + int bikeId, + LocationDto location, + Uri operatorUri) + { + return await DoReturn( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.DoReturn(bikeId, location), + UserAgent); + } + + /// Submits feedback to copri server. + /// True if bike is broken. + /// General purpose message or error description. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on submitting feedback request. + public async Task DoSubmitFeedback( + string message, + bool isBikeBroken, + Uri operatorUri) => + await DoSubmitFeedback( + operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri, + requestBuilder.DoSubmitFeedback(message, isBikeBroken), + UserAgent); + + /// Logs user in. + /// Host to connect to. + /// Command to log user in. + /// Response which holds auth cookie + public static async Task DoAuthorizationAsync( + string copriHost, + string command, + Func displayCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + /// Extract session cookie from response. + string l_oResponseText = string.Empty; + try + { + l_oResponseText = await PostAsync( + copriHost, + command, + userAgent, + displayCommand); // Do not include password into exception output when an error occurres. + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Login fehlgeschlagen aufgrund eines Netzwerkfehlers.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Login fehlgeschlagen aufgrund eines Netzwerkfehlers.", l_oException); + } + + throw; + } + + return JsonConvert.DeserializeObject>(l_oResponseText)?.tinkjson; +#else + return null; +#endif + } + + /// Logs user out. + /// Host to connect to. + /// Command to log user out. + public static async Task DoAuthoutAsync( + string p_strCopriHost, + string p_oCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oLogoutResponse; + try + { + l_oLogoutResponse = await PostAsync(p_strCopriHost, p_oCommand, userAgent); + } + + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Login fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Login fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + throw; + } + + /// Extract session cookie from response. + return JsonConvert.DeserializeObject>(l_oLogoutResponse)?.tinkjson; +#else + return null; +#endif + } + + /// + /// Get list of stations from file. + /// + /// URL of the copri host to connect to. + /// Command to get stations. + /// List of files. + public static async Task GetStationsAsync( + string p_strCopriHost, + string p_oCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oStationsAllResponse; + try + { + l_oStationsAllResponse = await PostAsync(p_strCopriHost, p_oCommand, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oStationsAllResponse)?.tinkjson; +#else + return null; +#endif + } + + /// Gets a list of bikes from Copri. + /// URL of the copri host to connect to. + /// Command to get bikes. + /// Response holding list of bikes. + public static async Task GetBikesAvailableAsync( + string p_strCopriHost, + string l_oCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(p_strCopriHost, l_oCommand, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Abfage der verfügbaren Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return CopriCallsStatic.DeserializeBikesAvailableResponse(l_oBikesAvaialbeResponse); +#else + return null; +#endif + } + + /// Gets a list of bikes reserved/ booked by acctive user from Copri. + /// URL of the copri host to connect to. + /// Command to post. + /// Response holding list of bikes. + public static async Task GetBikesOccupiedAsync( + string p_strCopriHost, + string p_oCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oBikesOccupiedResponse; + try + { + l_oBikesOccupiedResponse = await PostAsync(p_strCopriHost, p_oCommand, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Abfage der reservierten/ gebuchten Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Abfage der reservierten/ gebuchten Räder fehlgeschlagen wegen Netzwerkfehler.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return CopriCallsStatic.DeserializeBikesOccupiedResponse(l_oBikesOccupiedResponse); +#else + return null; +#endif + } + + /// Get auth keys from COPRI. + /// Host to connect to. + /// Command to log user in. + /// Response on booking request. + public static async Task GetAuthKeysAsync( + string p_strCopriHost, + string p_oCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(p_strCopriHost, p_oCommand, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Schlosssuche wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Schlosssuche wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oBikesAvaialbeResponse)?.tinkjson; +#else + return null; +#endif + } + /// Gets booking request response. + /// Host to connect to. + /// Command to log user in. + /// Response on booking request. + public static async Task DoReserveAsync( + string p_strCopriHost, + string p_oCommand, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(p_strCopriHost, p_oCommand, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Reservierung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Reservierung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oBikesAvaialbeResponse)?.tinkjson; +#else + return null; +#endif + } + + /// Gets canel booking request response. + /// Host to connect to. + /// Command to log user in. + /// Response on cancel booking request. + public static async Task DoCancelReservationAsync( + string copriHost, + string command, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Reservierung des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Reservierung des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oBikesAvaialbeResponse)?.tinkjson; +#else + return null; +#endif + } + + public static async Task DoUpdateLockingStateAsync( + string copriHost, + string command, + string agent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(copriHost, command, agent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Aktualisierung des Schlossstatuses wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Aktualisierung des Schlossstatuses wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oBikesAvaialbeResponse)?.tinkjson; +#else + return null; +#endif + } + + public static async Task DoBookAsync( + string copriHost, + string command, + string agent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(copriHost, command, agent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Buchung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Buchung des Fahrrads wegen Netzwerkfehler fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oBikesAvaialbeResponse)?.tinkjson; +#else + return null; +#endif + } + + public static async Task DoReturn( + string copriHost, + string command, + string userAgent = null) + { +#if !WINDOWS_UWP + string l_oBikesAvaialbeResponse; + try + { + l_oBikesAvaialbeResponse = await PostAsync(copriHost, command, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Rückgabe des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Rückgabe des Fahrrads aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(l_oBikesAvaialbeResponse)?.tinkjson; +#else + return null; +#endif + } + + public async Task DoSubmitFeedback( + string copriHost, + string command, + string userAgent = null) + { +#if !WINDOWS_UWP + string userFeedbackResponse; + try + { + userFeedbackResponse = await PostAsync(copriHost, command, userAgent); + } + catch (System.Exception l_oException) + { + if (l_oException.GetIsConnectFailureException()) + { + throw new WebConnectFailureException("Senden der Rückmeldung aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); + } + + if (l_oException.GetIsForbiddenException()) + { + throw new WebForbiddenException("Senden der Rückmeldung aufgrund eines Netzwerkfehlers fehlgeschlagen.", l_oException); + } + + throw; + } + + // Extract bikes from response. + return JsonConvert.DeserializeObject>(userFeedbackResponse)?.tinkjson; +#else + return null; +#endif + } + + /// http get- request. + /// Ulr to get info from. + /// response from server + public static async Task Get(string Url) + { + string result = string.Empty; + HttpWebRequest myRequest = (HttpWebRequest)WebRequest.Create(Url); + myRequest.Method = "GET"; + using (var myResponse = await myRequest.GetResponseAsync()) + { + using (var sr = new StreamReader(myResponse.GetResponseStream(), Encoding.UTF8)) + { + result = sr.ReadToEnd(); + } + } + + return result; + } + + /// http- post request. + /// Command to send. + /// Command to display/ log used for error handling. + /// Address of server to communicate with. + /// Response as text. + /// An unused member PostAsyncHttpClient using HttpClient for posting was removed 2020-04-02. + private static async Task PostAsync( + string uRL, + string p_strCommand, + string userAgent = null, + Func p_oDisplayCommand = null) + { + if (string.IsNullOrEmpty(p_strCommand)) + { + Log.ForContext().Fatal("Can not post command. Command must not be null or empty."); + + throw new ArgumentException("Can not post command. Command must not be null or empty."); + } + + if (string.IsNullOrEmpty(uRL)) + { + Log.ForContext().Fatal("Can not post command. Host must not be null or empty."); + + throw new ArgumentException("Can not post command. Host must not be null or empty."); + } + + // Get display version of command to used for display/ logging (password should never be included in output) + Func displayCommandFunc = p_oDisplayCommand ?? delegate () { return p_strCommand; }; + + try + { +#if !WINDOWS_UWP + var l_strHost = uRL; + + // Returns a http request. + var l_oRequest = WebRequest.CreateHttp(l_strHost); + + l_oRequest.Method = "POST"; + l_oRequest.ContentType = "application/x-www-form-urlencoded"; + l_oRequest.UserAgent = userAgent; + + // Workaround for issue https://bugzilla.xamarin.com/show_bug.cgi?id=57705 + // If not KeepAlive is set to true Stream.Write leads arbitrarily to an object disposed exception. + l_oRequest.KeepAlive = true; + + byte[] l_oPostData = Encoding.UTF8.GetBytes(p_strCommand); + + l_oRequest.ContentLength = l_oPostData.Length; + + // Get the request stream. + using (Stream l_oDataStream = await l_oRequest.GetRequestStreamAsync()) + { + // Write the data to the request stream. + await l_oDataStream.WriteAsync(l_oPostData, 0, l_oPostData.Length); + } + + // Get the response. + var l_oResponse = await l_oRequest.GetResponseAsync() as HttpWebResponse; + + if (l_oResponse == null) + { + throw new System.Exception(string.Format("Reserve request failed. Response form from server was not of expected type.")); + } + + if (l_oResponse.StatusCode != HttpStatusCode.OK) + { + throw new CommunicationException(string.Format( + "Posting request {0} failed. Expected status code is {1} but was {2}.", + displayCommandFunc(), + HttpStatusCode.OK, + l_oResponse.StatusCode)); + } + + string response = string.Empty; + + // Get the request stream. + using (Stream l_oDataStream = l_oResponse.GetResponseStream()) + using (StreamReader l_oReader = new StreamReader(l_oDataStream)) + { + // Read the content. + response = l_oReader.ReadToEnd(); + + // Display the content. + Console.WriteLine(response); + + // Clean up the streams. + l_oResponse.Close(); + } + + Log.ForContext().Verbose("Post command {DisplayCommand} to host {URL} received {ResponseText:j}.", displayCommandFunc(), uRL, response); + + return response; +#else + return null; +#endif + } + catch (System.Exception l_oException) + { + Log.ForContext().InformationOrError("Posting command {DisplayCommand} to host {URL} failed. {Exception}.", displayCommandFunc(), uRL, l_oException); + throw; + } + } + } +} diff --git a/TINKLib/Repository/CopriCallsMonkeyStore.cs b/TINKLib/Repository/CopriCallsMonkeyStore.cs new file mode 100644 index 0000000..9bf4d1f --- /dev/null +++ b/TINKLib/Repository/CopriCallsMonkeyStore.cs @@ -0,0 +1,281 @@ +using MonkeyCache.FileStore; +using System; +using System.Threading.Tasks; +using TINK.Model.Connector; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Model.Services.CopriApi; +using TINK.Repository.Response; + +namespace TINK.Model.Repository +{ + public class CopriCallsMonkeyStore : ICopriCache + { + /// Prevents concurrent communictation. + private object monkeyLock = new object(); + + /// Builds requests. + private IRequestBuilder requestBuilder; + + public const string BIKESAVAILABLE = @"{ + ""copri_version"" : ""3.0.0.0"", + ""bikes"" : {}, + ""response_state"" : ""OK"", + ""apiserver"" : ""https://app.tink-konstanz.de"", + ""authcookie"" : """", + ""response"" : ""bikes_available"" + }"; + + public const string BIKESOCCUPIED = @"{ + ""debuglevel"" : ""1"", + ""user_id"" : """", + ""response"" : ""user_bikes_occupied"", + ""user_group"" : ""Konrad,TINK"", + ""authcookie"" : """", + ""response_state"" : ""OK"", + ""bikes_occupied"" : {}, + ""copri_version"" : ""3.0.0.0"", + ""apiserver"" : ""https://app.tink-konstanz.de"" + }"; + + public const string STATIONS = @"{ + ""apiserver"" : ""https://app.tink-konstanz.de"", + ""authcookie"" : """", + ""response"" : ""stations_all"", + ""copri_version"" : ""3.0.0.0"", + ""stations"" : {}, + ""response_state"" : ""OK"" + }"; + + /// + /// Holds the seconds after which station and bikes info is considered to be invalid. + /// Default value 1s. + /// + private TimeSpan ExpiresAfter { get; } + + /// Returns false because cached values are returned. + public bool IsConnected => false; + + /// Gets the merchant id. + public string MerchantId => requestBuilder.MerchantId; + + /// Gets the merchant id. + public string SessionCookie => requestBuilder.SessionCookie; + + /// Initializes a instance of the copri monkey store object. + /// Id of the merchant. + /// Session cookie if user is logged in, null otherwise. + public CopriCallsMonkeyStore( + string merchantId, + string sessionCookie = null, + TimeSpan? expiresAfter = null) + { + ExpiresAfter = expiresAfter ?? TimeSpan.FromSeconds(1); + + requestBuilder = string.IsNullOrEmpty(sessionCookie) + ? new RequestBuilder(merchantId) as IRequestBuilder + : new RequestBuilderLoggedIn(merchantId, sessionCookie); + + // Ensure that store holds valid entries. + if (!Barrel.Current.Exists(requestBuilder.GetBikesAvailable())) + { + AddToCache(JsonConvert.DeserializeObject(BIKESAVAILABLE), new TimeSpan(0)); + } + + // Do not query bikes occupied if no user is logged in (leads to not implemented exception) + if (!string.IsNullOrEmpty(sessionCookie) && !Barrel.Current.Exists(requestBuilder.GetBikesOccupied())) + { + AddToCache(JsonConvert.DeserializeObject(BIKESOCCUPIED), new TimeSpan(0)); + } + + if (!Barrel.Current.Exists(requestBuilder.GetStations())) + { + AddToCache(JsonConvert.DeserializeObject(STATIONS), new TimeSpan(0)); + } + } + + public Task DoReserveAsync(int bikeId, Uri operatorUri) + { + throw new System.Exception("Reservierung im Offlinemodus nicht möglich!"); + } + + public Task DoCancelReservationAsync(int p_iBikeId, Uri operatorUri) + { + throw new System.Exception("Abbrechen einer Reservierung im Offlinemodus nicht möglich!"); + } + + public Task CalculateAuthKeysAsync(int bikeId, Uri operatorUri) + => throw new System.Exception("Schlosssuche im Offlinemodus nicht möglich!"); + + public Task UpdateLockingStateAsync( + int bikeId, + LocationDto geolocation, + lock_state state, + double batteryLevel, + Uri operatorUri) + => throw new System.Exception("Aktualisierung des Schlossstatuses im Offlinemodus nicht möglich!"); + + public Task DoBookAsync(int bikeId, Guid guid, double batteryPercentage, Uri operatorUri) + { + throw new System.Exception("Buchung im Offlinemodus nicht möglich!"); + } + + public Task DoReturn(int bikeId, LocationDto geolocation, Uri operatorUri) + { + throw new System.Exception("Rückgabe im Offlinemodus nicht möglich!"); + } + + public Task DoSubmitFeedback(string message, bool isBikeBroken, Uri operatorUri) => + throw new System.Exception("Übermittlung von Feedback im Offlinemodus nicht möglich!"); + + public Task DoAuthorizationAsync(string p_strMailAddress, string p_strPassword, string p_strDeviceId) + { + throw new System.Exception("Anmelden im Offlinemodus nicht möglich!"); + } + + public Task DoAuthoutAsync() + { + throw new System.Exception("Abmelden im Offlinemodus nicht möglich!"); + } + + public async Task GetBikesAvailableAsync() + { + var l_oBikesAvailableTask = new TaskCompletionSource(); + lock (monkeyLock) + { + l_oBikesAvailableTask.SetResult(Barrel.Current.Get(requestBuilder.GetBikesAvailable())); + } + return await l_oBikesAvailableTask.Task; + } + + public async Task GetBikesOccupiedAsync() + { + try + { + var l_oBikesOccupiedTask = new TaskCompletionSource(); + lock (monkeyLock) + { + l_oBikesOccupiedTask.SetResult(Barrel.Current.Get(requestBuilder.GetBikesOccupied())); + } + + return await l_oBikesOccupiedTask.Task; + + } + catch (NotSupportedException) + { + // No user logged in. + await Task.CompletedTask; + return ResponseHelper.GetBikesOccupiedNone(); + } + } + + public async Task GetStationsAsync() + { + var l_oStationsAllTask = new TaskCompletionSource(); + lock (monkeyLock) + { + l_oStationsAllTask.SetResult(Barrel.Current.Get(requestBuilder.GetStations())); + } + return await l_oStationsAllTask.Task; + } + + /// Gets a value indicating whether stations are expired or not. + public bool IsStationsExpired + { + get + { + lock (monkeyLock) + { + return Barrel.Current.IsExpired(requestBuilder.GetStations()); + } + } + } + + /// Adds a stations all response to cache. + /// Stations to add. + public void AddToCache(StationsAllResponse stations) + { + AddToCache(stations, ExpiresAfter); + } + + /// Adds a stations all response to cache. + /// Stations to add. + /// Time after which anser is considered to be expired. + private void AddToCache(StationsAllResponse stations, TimeSpan expiresAfter) + { + lock (monkeyLock) + { + Barrel.Current.Add( + requestBuilder.GetStations(), + JsonConvert.SerializeObject(stations), + expiresAfter); + } + } + + /// Gets a value indicating whether stations are expired or not. + public bool IsBikesAvailableExpired + { + get + { + lock (monkeyLock) + { + return Barrel.Current.IsExpired(requestBuilder.GetBikesAvailable()); + } + } + } + + /// Adds a bikes response to cache. + /// Bikes to add. + public void AddToCache(BikesAvailableResponse bikes) + { + AddToCache(bikes, ExpiresAfter); + } + + /// Adds a bikes response to cache. + /// Bikes to add. + /// Time after which anser is considered to be expired. + private void AddToCache(BikesAvailableResponse bikes, TimeSpan expiresAfter) + { + lock (monkeyLock) + { + Barrel.Current.Add( + requestBuilder.GetBikesAvailable(), + JsonConvert.SerializeObject(bikes), + expiresAfter); + } + } + + /// Gets a value indicating whether stations are expired or not. + public bool IsBikesOccupiedExpired + { + get + { + lock (monkeyLock) + { + return Barrel.Current.IsExpired(requestBuilder.GetBikesOccupied()); + } + } + } + + /// Adds a bikes response to cache. + /// Bikes to add. + public void AddToCache(BikesReservedOccupiedResponse bikes) + { + AddToCache(bikes, ExpiresAfter); + + } + /// Adds a bikes response to cache. + /// Bikes to add. + /// Time after which anser is considered to be expired. + private void AddToCache(BikesReservedOccupiedResponse bikes, TimeSpan expiresAfter) + { + lock (monkeyLock) + { + Barrel.Current.Add( + requestBuilder.GetBikesOccupied(), + JsonConvert.SerializeObject(bikes), + expiresAfter); + } + } + } +} diff --git a/TINKLib/Repository/CopriCallsStatic.cs b/TINKLib/Repository/CopriCallsStatic.cs new file mode 100644 index 0000000..7fef62f --- /dev/null +++ b/TINKLib/Repository/CopriCallsStatic.cs @@ -0,0 +1,28 @@ +using TINK.Model.Repository.Response; +using TINK.Repository.Response; + +namespace TINK.Model.Repository +{ + public static class CopriCallsStatic + { + /// + /// Deserializes JSON from response string. + /// + /// Response to deserialize. + /// + public static BikesAvailableResponse DeserializeBikesAvailableResponse(string p_strResponse) + { + return JsonConvert.DeserializeObject>(p_strResponse)?.tinkjson; + } + + /// + /// Deserializes JSON from response string. + /// + /// Response to deserialize. + /// + public static BikesReservedOccupiedResponse DeserializeBikesOccupiedResponse(string p_strResponse) + { + return JsonConvert.DeserializeObject>(p_strResponse)?.tinkjson; + } + } +} diff --git a/TINKLib/Repository/Exception/AuthcookieNotDefinedException.cs b/TINKLib/Repository/Exception/AuthcookieNotDefinedException.cs new file mode 100644 index 0000000..9aa5b0e --- /dev/null +++ b/TINKLib/Repository/Exception/AuthcookieNotDefinedException.cs @@ -0,0 +1,48 @@ +namespace TINK.Model.Repository.Exception +{ + /// + /// Is fired with reqest used a cookie which is not defined. + /// Reasons for cookie to be not defined might be + /// - user used more thant 8 different devices (copri invalidates cookies in this case) + /// - user account has been deleted? + /// + public class AuthcookieNotDefinedException : InvalidResponseException + { + /// Constructs a authorization exceptions. + /// Text describing request which is shown if validation fails. + public AuthcookieNotDefinedException(string p_strTextOfAction, Response.ResponseBase response) : + base($"{p_strTextOfAction}\r\nDie Sitzung ist abgelaufen. Bitte neu anmelden.", response) + { + } + + public static bool IsAuthcookieNotDefined( + Response.ResponseBase reponse, + string actionText, + out AuthcookieNotDefinedException exception) + { + if (!reponse.response_state.ToUpper().Contains(AUTH_FAILURE_QUERY_AUTHCOOKIENOTDEFIED.ToUpper()) + && !reponse.response_state.ToUpper().Contains(AUTH_FAILURE_BOOK_AUTICOOKIENOTDEFIED.ToUpper()) + && !reponse.response_state.ToUpper().Contains(AUTH_FAILURE_BIKESOCCUPIED_AUTICOOKIENOTDEFIED.ToUpper()) + && !reponse.response_state.ToUpper().Contains(AUTH_FAILURE_LOGOUT_AUTHCOOKIENOTDEFIED.ToUpper())) + { + exception = null; + return false; + } + + exception = new AuthcookieNotDefinedException(actionText, reponse); + return true; + } + + /// Holds error description if session expired. From COPRI 4.0.0.0 1001 is the only authcookie not defined error. + private const string AUTH_FAILURE_QUERY_AUTHCOOKIENOTDEFIED = "Failure 1001: authcookie not defined"; + + /// Holds error description if session expired (Applies to COPRI < 4.0.0.0) + private const string AUTH_FAILURE_BOOK_AUTICOOKIENOTDEFIED = "Failure 1002: authcookie not defined"; + + /// Holds error description if session expired (Applies to COPRI < 4.0.0.0) + private const string AUTH_FAILURE_BIKESOCCUPIED_AUTICOOKIENOTDEFIED = "Failure 1003: authcookie not defined"; + + /// Holds error description if session expired. (Applies to COPRI < 4.0.0.0) + private const string AUTH_FAILURE_LOGOUT_AUTHCOOKIENOTDEFIED = "Failure 1004: authcookie not defined"; + } +} diff --git a/TINKLib/Repository/Exception/AuthorizationResponseException.cs b/TINKLib/Repository/Exception/AuthorizationResponseException.cs new file mode 100644 index 0000000..aec2163 --- /dev/null +++ b/TINKLib/Repository/Exception/AuthorizationResponseException.cs @@ -0,0 +1,15 @@ +namespace TINK.Model.Repository.Exception +{ + public class InvalidAuthorizationResponseException : InvalidResponseException + { + /// Constructs a authorization exceptions. + /// Mail address to create a detailed error message. + public InvalidAuthorizationResponseException(string p_strMail, Response.ResponseBase p_oResponse) : + base(string.Format("Kann Benutzer {0} nicht anmelden. Mailadresse unbekannt oder Passwort ungültig.", p_strMail), p_oResponse) + { + } + + /// Holds error description if user/ password combination is not valid. + public const string AUTH_FAILURE_STATUS_MESSAGE_UPPERCASE = "FAILURE: CANNOT GENERATE AUTHCOOKIE"; + } +} diff --git a/TINKLib/Repository/Exception/BookingDeclinedException.cs b/TINKLib/Repository/Exception/BookingDeclinedException.cs new file mode 100644 index 0000000..98982aa --- /dev/null +++ b/TINKLib/Repository/Exception/BookingDeclinedException.cs @@ -0,0 +1,42 @@ +using System.Text.RegularExpressions; + +namespace TINK.Model.Repository.Exception +{ + /// Handles booking request which fail due to too many bikes requested/ booked. + public class BookingDeclinedException : InvalidResponseException + { + /// Holds error description if user/ password combination is not valid. + public const string BOOKING_FAILURE_STATUS_MESSAGE_UPPERCASE = "(OK: BOOKING_REQUEST DECLINED\\. MAX COUNT OF )([0-9]+)( OCCUPIED BIKES HAS BEEN REACHED)"; + + /// Prevents invalid use of exception. + private BookingDeclinedException() : base(typeof(BookingDeclinedException).Name) + { + } + + /// Prevents invalid use of exception. + public BookingDeclinedException(int maxBikesCount) : base(typeof(BookingDeclinedException).Name) + { + MaxBikesCount = maxBikesCount; + } + + public static bool IsBookingDeclined(string responseState, out BookingDeclinedException exception) + { + // Check if there are too many bikes requested/ booked. + var match = Regex.Match( + responseState.ToUpper(), + BOOKING_FAILURE_STATUS_MESSAGE_UPPERCASE); + if (match.Groups.Count!= 4 + || !int.TryParse(match.Groups[2].ToString(), out int maxBikesCount)) + { + exception = null; + return false; + } + + exception = new BookingDeclinedException(maxBikesCount); + return true; + } + + /// Holds the maximum count of bikes allowed to reserve/ book. + public int MaxBikesCount { get; private set; } + } +} diff --git a/TINKLib/Repository/Exception/CallNotRequiredException.cs b/TINKLib/Repository/Exception/CallNotRequiredException.cs new file mode 100644 index 0000000..ccaa218 --- /dev/null +++ b/TINKLib/Repository/Exception/CallNotRequiredException.cs @@ -0,0 +1,6 @@ +namespace TINK.Model.Repository.Exception +{ + public class CallNotRequiredException : System.Exception + { + } +} diff --git a/TINKLib/Repository/Exception/CommunicationException.cs b/TINKLib/Repository/Exception/CommunicationException.cs new file mode 100644 index 0000000..fcc645a --- /dev/null +++ b/TINKLib/Repository/Exception/CommunicationException.cs @@ -0,0 +1,28 @@ +namespace TINK.Model.Repository.Exception +{ + public class CommunicationException : System.Exception + { + /// + /// Constructs a communication exception object. + /// + public CommunicationException() + { } + + /// + /// Constructs a communication exeption object. + /// + /// Error message. + public CommunicationException(string p_strMessage) : base(p_strMessage) + { + } + + /// + /// Constructs a communication exeption object. + /// + /// Error message. + /// Inner exceptions. + public CommunicationException(string p_strMessage, System.Exception p_oException) : base(p_strMessage, p_oException) + { + } + } +} diff --git a/TINKLib/Repository/Exception/DeserializationException.cs b/TINKLib/Repository/Exception/DeserializationException.cs new file mode 100644 index 0000000..c6f9c49 --- /dev/null +++ b/TINKLib/Repository/Exception/DeserializationException.cs @@ -0,0 +1,11 @@ +using TINK.Model.Repository.Exception; + +namespace TINK.Repository.Exception +{ + public class DeserializationException : CommunicationException + { + public DeserializationException(System.Exception ex) : base(ex.Message, ex) + { + } + } +} diff --git a/TINKLib/Repository/Exception/InvalidResponseException.cs b/TINKLib/Repository/Exception/InvalidResponseException.cs new file mode 100644 index 0000000..b129c78 --- /dev/null +++ b/TINKLib/Repository/Exception/InvalidResponseException.cs @@ -0,0 +1,34 @@ +namespace TINK.Model.Repository.Exception +{ + public class InvalidResponseException : InvalidResponseException + { + /// Constructs an invalid response Exception. + /// Describes the action which failed. + /// Response from copri. + public InvalidResponseException(string p_strActionWhichFailed, T p_oResponse) + : base(string.Format( + "{0}{1}", + p_strActionWhichFailed, + p_oResponse == null ? string.Format(" Response des Typs {0} ist null.", typeof(T).Name.ToString()) : string.Empty)) + { + Response = p_oResponse; + } + + public T Response { get; private set; } + } + + /// + /// Base exception for all generic invalid response exceptions. + /// + public class InvalidResponseException : CommunicationException + { + /// Prevents an invalid instance to be created. + private InvalidResponseException() + { } + + /// Constructs a invalid response execption. + /// Exception. + public InvalidResponseException(string p_strMessage) : base(p_strMessage) + { } + } +} diff --git a/TINKLib/Repository/Exception/NoGPSDataException.cs b/TINKLib/Repository/Exception/NoGPSDataException.cs new file mode 100644 index 0000000..042fca8 --- /dev/null +++ b/TINKLib/Repository/Exception/NoGPSDataException.cs @@ -0,0 +1,28 @@ +using TINK.Model.Repository.Exception; + +namespace TINK.Repository.Exception +{ + public class NoGPSDataException : InvalidResponseException + { + /// COPRI response status. + public const string RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE = "FAILURE 2245: NO GPS DATA, STATE CHANGE FORBIDDEN."; + + /// Prevents invalid use of exception. + private NoGPSDataException() : base(typeof(NoGPSDataException).Name) + { + } + + public static bool IsNoGPSData(string responseState, out NoGPSDataException exception) + { + // Check if there are too many bikes requested/ booked. + if (!responseState.Trim().ToUpper().StartsWith(RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE)) + { + exception = null; + return false; + } + + exception = new NoGPSDataException(); + return true; + } + } +} diff --git a/TINKLib/Repository/Exception/NotAtStationException.cs b/TINKLib/Repository/Exception/NotAtStationException.cs new file mode 100644 index 0000000..f0b12c3 --- /dev/null +++ b/TINKLib/Repository/Exception/NotAtStationException.cs @@ -0,0 +1,39 @@ +using System.Text.RegularExpressions; + +namespace TINK.Model.Repository.Exception +{ + public class NotAtStationException : InvalidResponseException + { + /// COPRI response status regular expression. + public const string RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE = "(FAILURE 2178: BIKE [0-9]+ OUT OF GEO FENCING\\. )([0-9]+)( METER DISTANCE TO NEXT STATION )([0-9]+)"; + + /// Prevents invalid use of exception. + private NotAtStationException() : base(typeof(NotAtStationException).Name) + { + } + + public static bool IsNotAtStation(string responseState, out NotAtStationException exception) + { + // Check if there are too many bikes requested/ booked. + var match = Regex.Match( + responseState.ToUpper(), + RETURNBIKE_FAILURE_STATUS_MESSAGE_UPPERCASE); + if (match.Groups.Count != 5 + || !int.TryParse(match.Groups[2].ToString(), out int meters) + || !int.TryParse(match.Groups[4].ToString(), out int stationNr)) + { + exception = null; + return false; + } + + exception = new NotAtStationException { Distance = meters, StationNr = stationNr }; + return true; + } + + /// Holds the maximum count of bikes allowed to reserve/ book. + public int Distance { get; private set; } + + /// Holds the maximum count of bikes allowed to reserve/ book. + public int StationNr { get; private set; } + } +} diff --git a/TINKLib/Repository/Exception/ResponseException.cs b/TINKLib/Repository/Exception/ResponseException.cs new file mode 100644 index 0000000..a4e3122 --- /dev/null +++ b/TINKLib/Repository/Exception/ResponseException.cs @@ -0,0 +1,15 @@ +using TINK.Model.Repository.Response; + +namespace TINK.Repository.Exception +{ + public class ResponseException : System.Exception + { + private readonly ResponseBase _response; + public ResponseException(ResponseBase response, string message) : base(message) + { + _response = response; + } + + public string Response => _response.response_text; + } +} diff --git a/TINKLib/Repository/Exception/ReturnBikeException.cs b/TINKLib/Repository/Exception/ReturnBikeException.cs new file mode 100644 index 0000000..31bc119 --- /dev/null +++ b/TINKLib/Repository/Exception/ReturnBikeException.cs @@ -0,0 +1,10 @@ +using TINK.Model.Repository.Response; + +namespace TINK.Repository.Exception +{ + public class ReturnBikeException : ResponseException + { + public ReturnBikeException(ReservationCancelReturnResponse response, string message) : base(response, message) + { } + } +} diff --git a/TINKLib/Repository/Exception/WebConnectFailureException.cs b/TINKLib/Repository/Exception/WebConnectFailureException.cs new file mode 100644 index 0000000..a16096a --- /dev/null +++ b/TINKLib/Repository/Exception/WebConnectFailureException.cs @@ -0,0 +1,24 @@ +namespace TINK.Model.Repository.Exception +{ + public class WebConnectFailureException : CommunicationException + { + /// + /// Returns a hint to fix communication problem. + /// + public static string GetHintToPossibleExceptionsReasons + { + get + { + return "Ist WLAN verfügbar/ Mobilfunknetz vefügbar und mobile Daten aktiviert / ... ?"; + } + } + /// + /// Constructs a communication exeption object. + /// + /// + /// + public WebConnectFailureException(string p_strMessage, System.Exception p_oException) : base(p_strMessage, p_oException) + { + } + } +} diff --git a/TINKLib/Repository/Exception/WebExceptionHelper.cs b/TINKLib/Repository/Exception/WebExceptionHelper.cs new file mode 100644 index 0000000..e88c24c --- /dev/null +++ b/TINKLib/Repository/Exception/WebExceptionHelper.cs @@ -0,0 +1,42 @@ +using System.Net; + +namespace TINK.Model.Repository.Exception +{ + public static class WebExceptionHelper + { + /// Gets if a exception is caused by an error connecting to copri (LAN or mobile data off/ not reachable, proxy, ...). + /// Expection to check. + /// True if exception if caused by an connection error. + public static bool GetIsConnectFailureException(this System.Exception p_oException) + { + var l_oException = p_oException as WebException; + if (l_oException == null) + { + return false; + } + + return l_oException.Status == WebExceptionStatus.ConnectFailure // Happens if WLAN and mobile data is off/ Router denies internet access/ ... + || l_oException.Status == WebExceptionStatus.NameResolutionFailure // Happens sometimes when not WLAN and no mobil connection are available (bad connection in lift). + || l_oException.Status == WebExceptionStatus.ReceiveFailure; // Happened when modile was connected to WLAN + } + + /// Gets if a exception is caused by clicking too fast. + /// Expection to check. + /// True if exception if caused by a fast click sequence. + public static bool GetIsForbiddenException(this System.Exception p_oException) + { + if (!(p_oException is WebException l_oException)) + { + return false; + } + + if (!(l_oException?.Response is HttpWebResponse l_oResponse)) + { + return false; + } + + return l_oException.Status == WebExceptionStatus.ProtocolError + && l_oResponse.StatusCode == HttpStatusCode.Forbidden; + } + } +} diff --git a/TINKLib/Repository/Exception/WebForbiddenException.cs b/TINKLib/Repository/Exception/WebForbiddenException.cs new file mode 100644 index 0000000..6c95c2f --- /dev/null +++ b/TINKLib/Repository/Exception/WebForbiddenException.cs @@ -0,0 +1,14 @@ +namespace TINK.Model.Repository.Exception +{ + public class WebForbiddenException : CommunicationException + { + /// + /// Constructs a communication exeption object. + /// + /// + /// + public WebForbiddenException(string p_strMessage, System.Exception p_oException) : base($"{p_strMessage}\r\nSchnell getippt?\r\nBitte die App etwas langsamer bedienen...", p_oException) + { + } + } +} diff --git a/TINKLib/Repository/ICopriServer.cs b/TINKLib/Repository/ICopriServer.cs new file mode 100644 index 0000000..399142a --- /dev/null +++ b/TINKLib/Repository/ICopriServer.cs @@ -0,0 +1,121 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Repository.Response; + +namespace TINK.Model.Repository +{ + /// Interface to communicate with copri server. + public interface ICopriServerBase + { + /// Logs user in. + /// Mailaddress of user to log in. + /// Password to log in. + /// Id specifying user and hardware. + /// Response which holds auth cookie + Task DoAuthorizationAsync( + string mailAddress, + string password, + string deviceId); + + /// Logs user out. + /// Response which holds auth cookie + Task DoAuthoutAsync(); + + /// Reserves bike. + /// Id of the bike to reserve. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on reserving request. + Task DoReserveAsync( + int bikeId, + Uri operatorUri); + + /// Cancels reservation of bik. + /// Id of the bike to reserve. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on cancel reservation request. + Task DoCancelReservationAsync( + int bikeId, + Uri operatorUri); + + /// Get authentication keys. + /// Id of the bike to get keys for. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response holding authentication keys. + Task CalculateAuthKeysAsync( + int bikeId, + Uri operatorUri); + + /// Updates COPRI lock state for a booked bike. + /// Id of the bike to update locking state for. + /// Geolocation of lock. + /// New locking state. + /// Holds the filling level percentage of the battery. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on updating locking state. + Task UpdateLockingStateAsync( + int bikeId, + LocationDto location, + lock_state state, + double batteryPercentage, + Uri operatorUri); + + /// Books a bike. + /// Id of the bike to book. + /// Used to publish GUID from app to copri. Used for initial setup of bike in copri. + /// Holds the filling level percentage of the battery. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on booking request. + Task DoBookAsync( + int bikeId, + Guid guid, + double batteryPercentage, + Uri operatorUri); + + /// Returns a bike. + /// Id of the bike to return. + /// Geolocation of lock. + /// Holds the uri of the operator or null, in case of single operator setup. + /// Response on returning request. + Task DoReturn( + int bikeId, + LocationDto location, + Uri operatorUri); + + /// + /// Submits feedback to copri server. + /// + /// True if bike is broken. + /// General purpose message or error description. + Task DoSubmitFeedback( + string message, + bool isBikeBroken, + Uri operatorUri); + + /// True if connector has access to copri server, false if cached values are used. + bool IsConnected { get; } + + /// Gets the session cookie if user is logged in, an empty string otherwise. + string SessionCookie { get; } + + /// Holds the id of the merchant. + string MerchantId { get; } + } + + /// Interface to communicate with copri server. + public interface ICopriServer : ICopriServerBase + { + /// Get list of stations. + /// List of all stations. + Task GetStationsAsync(); + + /// Gets a list of bikes from Copri. + /// Response holding list of bikes. + Task GetBikesAvailableAsync(); + + /// Gets a list of bikes reserved/ booked by acctive user from Copri. + /// Response holding list of bikes. + Task GetBikesOccupiedAsync(); + } +} diff --git a/TINKLib/Repository/Request/IRequestBuilder.cs b/TINKLib/Repository/Request/IRequestBuilder.cs new file mode 100644 index 0000000..e0bf258 --- /dev/null +++ b/TINKLib/Repository/Request/IRequestBuilder.cs @@ -0,0 +1,122 @@ +using System; + +namespace TINK.Model.Repository.Request +{ + /// Defines members to create requests. + public interface IRequestBuilder + { + /// Holds the id denoting the merchant (TINK app). + string MerchantId { get; } + + /// Gets the session cookie if user is logged in, an empty string otherwise. + string SessionCookie { get; } + + /// Gets request to log user in. + /// Mailaddress of user to log in. + /// Password to log in. + /// Id specifying user and hardware. + /// Requst which holds auth cookie + string DoAuthorization( + string mailAddress, + string password, + string deviceId); + + /// Logs user out. + /// Id of the merchant. + /// Cookie which identifies user. + string DoAuthout(); + + /// Get list of stations from file. + /// Request to query list of station. + string GetStations(); + + /// Gets bikes available. + /// Request to query list of bikes available. + string GetBikesAvailable(); + + /// Gets a list of bikes reserved/ booked by acctive user from Copri. + /// Request to query list of bikes occupied. + string GetBikesOccupied(); + + /// Gets reservation request (synonym: reservation == request == reservieren). + /// Id of the bike to reserve. + /// Requst to reserve bike. + string DoReserve(int bikeId); + + /// Gets request to cancel reservation. + /// Id of the bike to cancel reservation for. + /// Requst on cancel booking request. + string DoCancelReservation(int bikeId); + + /// Request to get keys. + /// Id of the bike to get keys for. + /// Request to get keys. + string CalculateAuthKeys(int bikeId); + + /// Gets the request for updating lock state for a booked bike. + /// Id of the bike to update locking state for. + /// Geolocation of lock when state change occurred. + /// New locking state. + /// Request to update locking state. + string UpateLockingState( + int bikeId, + LocationDto location, + lock_state state, + double batteryPercentage); + + /// Gets booking request request (synonym: booking == renting == mieten). + /// Id of the bike to book. + /// Used to publish GUID from app to copri. Used for initial setup of bike in copri. + /// Holds the filling level percentage of the battery. + /// Request to booking bike. + string DoBook(int bikeId, Guid guid, double batteryPercentage); + + /// Gets request for returning the bike. + /// Id of the bike to return. + /// Geolocation of lock when returning bike. + /// Requst on returning request. + string DoReturn(int bikeId, LocationDto location); + + /// + /// Gets request for submiting feedback to copri server. + /// + /// General purpose message or error description. + /// True if bike is broken. + string DoSubmitFeedback(string message = null, bool isBikeBroken = false); + } + + /// Copri locking states + public enum lock_state + { + locked, + unlocked + } + + public class LocationDto + { + public double Latitude { get; private set; } + + public double Longitude { get; private set; } + + /// Accuracy of location in meters. + public double? Accuracy { get; private set; } + + public TimeSpan Age { get; private set; } + + public class Builder + { + public double Latitude { get; set; } + + public double Longitude { get; set; } + + public double? Accuracy { get; set; } + + public TimeSpan Age { get; set; } + + public LocationDto Build() + { + return new LocationDto { Latitude = Latitude, Longitude = Longitude, Accuracy = Accuracy, Age = Age }; + } + } + } +} diff --git a/TINKLib/Repository/Request/RequestBuilder.cs b/TINKLib/Repository/Request/RequestBuilder.cs new file mode 100644 index 0000000..a7de790 --- /dev/null +++ b/TINKLib/Repository/Request/RequestBuilder.cs @@ -0,0 +1,112 @@ +using System; +using System.Net; +using TINK.Model.Repository.Exception; + +namespace TINK.Model.Repository.Request +{ + /// Creates requests if no user is logged in. + public class RequestBuilder : IRequestBuilder + { + /// Constructs a object for building requests. + /// + public RequestBuilder( + string merchantId) + { + MerchantId = !string.IsNullOrEmpty(merchantId) + ? merchantId + : throw new ArgumentException("Merchant id must not be null.", nameof(merchantId)); + } + + /// Holds the id denoting the merchant (TINK app). + public string MerchantId { get; } + + /// Holds the session cookie if a user is logged in. + public string SessionCookie => string.Empty; + + /// Gets request to log user in. + /// Mailaddress of user to log in. + /// Password to log in. + /// Id specifying user and hardware. + /// Response which holds auth cookie + public string DoAuthorization( + string mailAddress, + string password, + string deviceId) + { + return string.Format( + "request=authorization&merchant_id={0}&user_id={1}&user_pw={2}&hw_id={3}", + MerchantId, + WebUtility.UrlEncode(mailAddress), + WebUtility.UrlEncode(password), + deviceId); + } + + /// Logs user out. + public string DoAuthout() + { + throw new CallNotRequiredException(); + } + + /// Gets bikes available. + /// Request to query list of bikes available. + public string GetBikesAvailable() + { + return GetBikesAvailable(MerchantId); + } + + /// Gets bikes available. + /// Request to query list of bikes available. + public static string GetBikesAvailable(string merchantId, string sessionCookie = null) + { + return $"request=bikes_available&system=all&authcookie={sessionCookie ?? string.Empty}{merchantId}"; + } + + /// Get list of stations from file. + /// Request to query list of station. + public string GetStations() + { + return GetStations(MerchantId); + } + + /// Get list of stations from file. + /// Request to query list of station. + public static string GetStations(string merchantId, string sessionCookie = null) + { + return $"request=stations_available&authcookie={sessionCookie ?? string.Empty}{merchantId}"; + } + + /// Gets a list of bikes reserved/ booked by acctive user from Copri. + /// Request to query list of bikes occupied. + public string GetBikesOccupied() => throw new NotSupportedException(); + + /// Gets booking request response. + /// Id of the bike to book. + /// Response on booking request. + public string DoReserve(int p_iBikeId) => throw new NotSupportedException(); + + /// Gets cancel booking request response. + /// Id of the bike to book. + /// Response on cancel booking request. + public string DoCancelReservation(int p_iBikeId) => throw new NotSupportedException(); + + /// Request to calculate authentication keys. + /// Id of the bike to get keys for. + /// Response on request. + public string CalculateAuthKeys(int bikeId) => throw new NotSupportedException(); + + public string UpateLockingState(int bikeId, LocationDto geolocation, lock_state state, double batteryPercentage) + => throw new NotSupportedException(); + + public string DoBook(int bikeId, Guid guid, double batteryPercentage) => throw new NotSupportedException(); + + public string DoReturn(int bikeId, LocationDto geolocation) => throw new NotSupportedException(); + + /// Gets submit feedback request. + /// General purpose message or error description. + /// True if bike is broken. + /// Submit feedback request. + public string DoSubmitFeedback( + string message = null, + bool isBikeBroken = false) => throw new NotSupportedException(); + } +} diff --git a/TINKLib/Repository/Request/RequestBuilderLoggedIn.cs b/TINKLib/Repository/Request/RequestBuilderLoggedIn.cs new file mode 100644 index 0000000..8b74a10 --- /dev/null +++ b/TINKLib/Repository/Request/RequestBuilderLoggedIn.cs @@ -0,0 +1,170 @@ +using System; +using System.Globalization; +using System.Net; +using TINK.Model.Repository.Exception; + +namespace TINK.Model.Repository.Request +{ + /// Creates requests if a user is logged in. + public class RequestBuilderLoggedIn : IRequestBuilder + { + /// Constructs a object for building requests. + /// + public RequestBuilderLoggedIn( + string merchantId, + string sessionCookie) + { + MerchantId = !string.IsNullOrEmpty(merchantId) + ? merchantId + : throw new ArgumentException("Merchant id must not be null.", nameof(merchantId)); + + SessionCookie = !string.IsNullOrEmpty(sessionCookie) + ? sessionCookie + : throw new ArgumentException("Session cookie must not be null.", nameof(sessionCookie)); + } + + /// Holds the id denoting the merchant (TINK app). + public string MerchantId { get; } + + /// Holds the session cookie if a user is logged in. + public string SessionCookie { get; } + + /// Gets request to log user in. + /// Mailaddress of user to log in. + /// Password to log in. + /// Id specifying user and hardware. + /// Response which holds auth cookie + public string DoAuthorization( + string mailAddress, + string password, + string deviceId) + { + throw new CallNotRequiredException(); + } + + /// Logs user out. + public string DoAuthout() + { + return $"request=authout&authcookie={SessionCookie}{MerchantId}"; + } + + /// Gets bikes available. + /// Request to query list of bikes available. + public string GetBikesAvailable() + { + return RequestBuilder.GetBikesAvailable(MerchantId, SessionCookie); + } + + /// Gets a list of bikes reserved/ booked by acctive user from Copri. + /// Request to query list of bikes occupied. + public string GetBikesOccupied() + { + return !string.IsNullOrEmpty(SessionCookie) + ? $"request=user_bikes_occupied&system=all&genkey=1&authcookie={SessionCookie}{MerchantId}" + : "request=bikes_available"; + } + + /// Get list of stations from file. + /// Request to query list of station. + public string GetStations() + { + return $"request=stations_available&authcookie={SessionCookie ?? string.Empty}{MerchantId}"; + } + + /// Gets reservation request (synonym: reservation == request == reservieren). + /// Operator specific call. + /// Id of the bike to reserve. + /// Requst to reserve bike. + public string DoReserve(int bikeId) + => $"request=booking_request&bike={bikeId}&authcookie={SessionCookie}{MerchantId}"; + + /// Gets request to cancel reservation. + /// Operator specific call. + /// Id of the bike to cancel reservation for. + /// Requst on cancel booking request. + public string DoCancelReservation(int p_iBikeId) + => $"request=booking_cancel&bike={p_iBikeId}&authcookie={SessionCookie}{MerchantId}"; + + /// Request to get keys. + /// Operator specific call. + /// Id of the bike to get keys for. + /// Request to get keys. + public string CalculateAuthKeys(int bikeId) + => $"request=booking_update&bike={bikeId}&authcookie={SessionCookie}{MerchantId}&genkey=1"; + + /// Gets the request for updating lock state for a booked bike. + /// Operator specific call. + /// Id of the bike to update locking state for. + /// New locking state. + /// Request to update locking state. + public string UpateLockingState(int bikeId, LocationDto geolocation, lock_state state, double batteryPercentage) + { + return $"request=booking_update&bike={bikeId}{GetLocationKey(geolocation)}&lock_state={state}{GetBatteryPercentageKey(batteryPercentage)}&authcookie={SessionCookie}{MerchantId}"; + } + + /// Gets booking request request (synonym: booking == renting == mieten). + /// Operator specific call. + /// Id of the bike to book. + /// Used to publish GUID from app to copri. Used for initial setup of bike in copri. + /// Holds the filling level percentage of the battery. + /// Request to booking bike. + public string DoBook(int bikeId, Guid guid, double batteryPercentage) + => $"request=booking_update&bike={bikeId}&authcookie={SessionCookie}{MerchantId}&Ilockit_GUID={guid}&state=occupied&lock_state=unlocked{GetBatteryPercentageKey(batteryPercentage)}"; + + /// Gets request for returning the bike. + /// Operator specific call. + /// Id of bike to return. + /// Geolocation of lock when returning bike. + /// Requst on returning request. + public string DoReturn(int bikeId, LocationDto geolocation) + { + return $"request=booking_update&bike={bikeId}&authcookie={SessionCookie}{MerchantId}&state=available{GetLocationKey(geolocation)}&lock_state=locked"; + } + + /// Gets submit feedback request. + /// General purpose message or error description. + /// True if bike is broken. + /// Submit feedback request. + public string DoSubmitFeedback( + string message = null, + bool isBikeBroken = false) + { + if (string.IsNullOrEmpty(message) && !isBikeBroken) + { + // User just acknoledged biked returned message. + return "request=user_feedback"; + } + + if (isBikeBroken == false) + { + // Bike is ok and user entered a feedback message. + return $"request=user_feedback&message={WebUtility.UrlEncode(message)}"; + } + + + if (string.IsNullOrEmpty(message)) + { + // User just marked bike as broken without comment. + return $"request=user_feedback&bike_broken=1"; + } + + // Bike is marked as broken and user added a comment. + return $"request=user_feedback&bike_broken=1&message={WebUtility.UrlEncode(message)}"; + } + + private string GetBatteryPercentageKey(double batteryPercentage) => !double.IsNaN(batteryPercentage) + ? $"&voltage={batteryPercentage.ToString(CultureInfo.InvariantCulture)}" + : string.Empty; + + private string GetLocationKey(LocationDto geolocation) + { + if (geolocation == null) + return string.Empty; + + if (geolocation.Accuracy == null) + return $"&gps={geolocation.Latitude.ToString(CultureInfo.InvariantCulture)},{ geolocation.Longitude.ToString(CultureInfo.InvariantCulture)}&gps_age={geolocation.Age.TotalSeconds}"; + + return $"&gps={geolocation.Latitude.ToString(CultureInfo.InvariantCulture)},{geolocation.Longitude.ToString(CultureInfo.InvariantCulture)}&gps_accuracy={geolocation.Accuracy.Value.ToString(CultureInfo.InvariantCulture)}&gps_age={geolocation.Age.TotalSeconds}"; + } + } +} diff --git a/TINKLib/Repository/Response/AuthorizationResponse.cs b/TINKLib/Repository/Response/AuthorizationResponse.cs new file mode 100644 index 0000000..60a7a7f --- /dev/null +++ b/TINKLib/Repository/Response/AuthorizationResponse.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + [DataContract] + public class AuthorizationResponse : ResponseBase + { + [DataMember] + public int debuglevel { get; private set; } + + /// Holds the group of the bike (TINK, Konrad, ...). + [DataMember] + public string user_group { get; private set; } + } +} diff --git a/TINKLib/Repository/Response/AuthorizationoutResponse.cs b/TINKLib/Repository/Response/AuthorizationoutResponse.cs new file mode 100644 index 0000000..5288756 --- /dev/null +++ b/TINKLib/Repository/Response/AuthorizationoutResponse.cs @@ -0,0 +1,9 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + [DataContract] + public class AuthorizationoutResponse : ResponseBase + { + } +} diff --git a/TINKLib/Repository/Response/BikeInfoAvailable.cs b/TINKLib/Repository/Response/BikeInfoAvailable.cs new file mode 100644 index 0000000..49e940b --- /dev/null +++ b/TINKLib/Repository/Response/BikeInfoAvailable.cs @@ -0,0 +1,22 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + [DataContract] + public class BikeInfoAvailable : BikeInfoBase + { + /// + /// Position of the bike. + /// + [DataMember] + public string gps { get; private set; } + + [DataMember] + /// Full advertisement name. + public string Ilockit_ID { get; private set; } + + [DataMember] + /// Full advertisement name. + public string Ilockit_GUID { get; private set; } + } +} diff --git a/TINKLib/Repository/Response/BikeInfoBase.cs b/TINKLib/Repository/Response/BikeInfoBase.cs new file mode 100644 index 0000000..b52f91a --- /dev/null +++ b/TINKLib/Repository/Response/BikeInfoBase.cs @@ -0,0 +1,76 @@ +using System.Runtime.Serialization; +using TINK.Repository.Response; + +namespace TINK.Model.Repository.Response +{ + /// + /// Holds info about a single bike. + /// + [DataContract] + public class BikeInfoBase + { + /// + /// Id of the bike. + /// + [DataMember] + public int bike { get; private set; } + + /// + /// Id of the station. + /// + [DataMember] + public int? station { get; private set; } + + /// + /// Holds the localized (german) description of the bike. + /// + [DataMember] + public string description { get; private set; } + + /// Holds the group of the bike. + /// + /// Copri returns values "TINK", "Konrad". + /// + [DataMember] + public string bike_group { get; private set; } + + /// + /// Rental state. + /// + [DataMember] + public string state { get; private set; } + + /// + /// Holds the uri where to reserve/ rent the bike. + /// + [DataMember] + public string uri_operator { get; private set; } + + /// Holds whether bike is equipped with computer or if bike is a lock bike. + /// + /// + /// + /// + /// + /// + ///
Value Type of bike Member to extract info.
LOCK Bike with manual lock. TextToTypeHelper.GetIsNonBikeComputerBike
Bike with a bord computer.
? Bike with a bluetooth lock.
+ ///
+ [DataMember] + public string system { get; private set; } + +#if !NOTARIFFDESCRIPTION + /// Holds the tariff information for a bike. + [DataMember] + public TariffDescription tariff_description { get; private set; } +#endif + /// + /// Textual description of response. + /// + /// Object as text. + public new string ToString() + { + return $"Bike {bike}{(station != null ? $", at station {station}" : string.Empty)}{(!string.IsNullOrEmpty(description) ? $", {description}" : string.Empty)}{(!string.IsNullOrEmpty(state) ? $", status={state}" : string.Empty)}."; + } + + } +} diff --git a/TINKLib/Repository/Response/BikeInfoReservedBooked.cs b/TINKLib/Repository/Response/BikeInfoReservedBooked.cs new file mode 100644 index 0000000..bd82b9f --- /dev/null +++ b/TINKLib/Repository/Response/BikeInfoReservedBooked.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + [DataContract] + public class BikeInfoReservedOrBooked : BikeInfoAvailable + { + /// + /// Date from when bike was reserved from/ booked from. + /// Format: 2017-11-28 11:01:51.637747+01 + /// + [DataMember] + public string start_time { get; private set; } + + /// Booking code if bike is BC-bike. + [DataMember] + public string timeCode { get; private set; } + + [DataMember] + /// Seed used to generate key for connecting to bluetooth lock. + public string K_seed { get; private set; } + + [DataMember] + /// Key for connect to bluetooth lock as user. + public string K_u { get; private set; } + + [DataMember] + /// Key for connect to bluetooth lock as admin. + public string K_a { get; private set;} + + } +} diff --git a/TINKLib/Repository/Response/BikesAvailableResponse.cs b/TINKLib/Repository/Response/BikesAvailableResponse.cs new file mode 100644 index 0000000..d293445 --- /dev/null +++ b/TINKLib/Repository/Response/BikesAvailableResponse.cs @@ -0,0 +1,18 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + /// + /// Holds the information about all bikes and is used for deserialization of copri answer. + /// + [DataContract] + public class BikesAvailableResponse : ResponseBase + { + /// + /// Dictionary of bikes. + /// + [DataMember] + public Dictionary bikes { get; private set; } + } +} diff --git a/TINKLib/Repository/Response/BikesReservedOccupiedResponse.cs b/TINKLib/Repository/Response/BikesReservedOccupiedResponse.cs new file mode 100644 index 0000000..3ed5715 --- /dev/null +++ b/TINKLib/Repository/Response/BikesReservedOccupiedResponse.cs @@ -0,0 +1,14 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + public class BikesReservedOccupiedResponse : ResponseBase + { + /// + /// Dictionary of bikes. + /// + [DataMember] + public Dictionary bikes_occupied { get; private set; } + } +} diff --git a/TINKLib/Repository/Response/JsonConvert.cs b/TINKLib/Repository/Response/JsonConvert.cs new file mode 100644 index 0000000..0777bc6 --- /dev/null +++ b/TINKLib/Repository/Response/JsonConvert.cs @@ -0,0 +1,27 @@ +using TINK.Repository.Exception; + +namespace TINK.Repository.Response +{ + public static class JsonConvert + { + /// + /// Deserializes COPRI responses in a consitent way for entire app. + /// + /// Type of object to serialize to. + /// JSON to deserialize. + /// Deserialized object. + public static T DeserializeObject(string response) + { + try + { + return Newtonsoft.Json.JsonConvert.DeserializeObject(response); + } + catch (System.Exception ex) + { + throw new DeserializationException(ex); + } + } + + public static string SerializeObject(object value) => Newtonsoft.Json.JsonConvert.SerializeObject(value); + } +} diff --git a/TINKLib/Repository/Response/ReservationBookingResponse.cs b/TINKLib/Repository/Response/ReservationBookingResponse.cs new file mode 100644 index 0000000..d2b9547 --- /dev/null +++ b/TINKLib/Repository/Response/ReservationBookingResponse.cs @@ -0,0 +1,15 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + /// + /// Holds the information about a booking request and is used for deserialization of copri answer. + /// + [DataContract] + public class ReservationBookingResponse : BikesReservedOccupiedResponse + { + /// Booking code for BC- bikes. + [DataMember] + public string timeCode { get; private set; } + } +} diff --git a/TINKLib/Repository/Response/ReservationCancelReturnResponse.cs b/TINKLib/Repository/Response/ReservationCancelReturnResponse.cs new file mode 100644 index 0000000..67f5ba7 --- /dev/null +++ b/TINKLib/Repository/Response/ReservationCancelReturnResponse.cs @@ -0,0 +1,10 @@ + +namespace TINK.Model.Repository.Response +{ + /// + /// Holds the information about a cancel booking request and is used for deserialization of copri answer. + /// + public class ReservationCancelReturnResponse : BikesReservedOccupiedResponse + { + } +} diff --git a/TINKLib/Repository/Response/ResponseBase.cs b/TINKLib/Repository/Response/ResponseBase.cs new file mode 100644 index 0000000..ba4adff --- /dev/null +++ b/TINKLib/Repository/Response/ResponseBase.cs @@ -0,0 +1,32 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + [DataContract] + public class ResponseBase + { + [DataMember] + public string response_state { get; private set; } + + [DataMember] + public string response { get; private set; } + + [DataMember] + public string response_text { get; private set; } + + [DataMember] + public string authcookie { get; private set; } + + [DataMember] + public string copri_version { get; private set; } + + /// Textual description of response. + public new string ToString() + { + return $"Response state is \"{response_state ?? string.Empty}\", " + + $"auth cookie is \"{authcookie ?? string.Empty}\" and response is \"{response_text ?? string.Empty}\", " + + $"code \"{response ?? string.Empty}\"" + + $"response text \"{response_text ?? string.Empty}\"."; + } + } +} diff --git a/TINKLib/Repository/Response/ResponseContainer.cs b/TINKLib/Repository/Response/ResponseContainer.cs new file mode 100644 index 0000000..29fef86 --- /dev/null +++ b/TINKLib/Repository/Response/ResponseContainer.cs @@ -0,0 +1,25 @@ +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + [DataContract] + public class ResponseContainer + { + [DataMember] + public T tinkjson { get; private set; } + + /// + /// Serializes object to string. + /// + /// + public override string ToString() + { + if (tinkjson == null) + { + return "Response container does not hold no entry."; + } + + return tinkjson.ToString(); + } + } +} diff --git a/TINKLib/Repository/Response/ResponseHelper.cs b/TINKLib/Repository/Response/ResponseHelper.cs new file mode 100644 index 0000000..120ad38 --- /dev/null +++ b/TINKLib/Repository/Response/ResponseHelper.cs @@ -0,0 +1,240 @@ +using System.Linq; +using TINK.Model.Repository.Exception; +using TINK.MultilingualResources; +using TINK.Repository.Exception; + +namespace TINK.Model.Repository.Response +{ + public static class ResponseHelper + { + public const string RESPONSE_OK = "OK"; + + /// Holds the description of the action logout. + public const string BIKES_LOGOUT_ACTIONTEXT = "Abmeldung fehlgeschlagen."; + + /// Holds the description of the action get stations available. + public const string STATIONS_AVAILABLE_ACTIONTEXT = "Abfrage der verfügbaren Stationen fehlgeschlagen."; + + /// Holds the description of the action get bikes available. + public const string BIKES_AVAILABLE_ACTIONTEXT = "Abfrage der verfügbaren Fahrräder fehlgeschlagen."; + + /// Holds the description of the action get bikes occupied. + public const string BIKES_OCCUPIED_ACTIONTEXT = "Abfrage der reservierten/ gebuchten Fahrräder fehlgeschlagen."; + + /// Holds the description of the action cancel reservation. + public const string BIKES_CANCELREQUEST_ACTIONTEXT = "Aufheben der Reservierung fehlgeschlagen."; + + /// Holds the description of the action return bike. + public const string BIKES_RETURNBIKE_ACTIONTEXT = "Rückgabe des Rads fehlgeschlagen."; + + /// + /// Checks if log in response is ok. + /// + /// Response to check whether it is valid. + /// Mail addess used to create details error message in case log in response is invalid. + /// + public static AuthorizationResponse GetIsResponseOk(this AuthorizationResponse response, string mail) + { + if (response == null) + { + throw new InvalidResponseException("Anmeldung fehlgeschlagen.", null); + } + + if (response.response_state.ToUpper() == InvalidAuthorizationResponseException.AUTH_FAILURE_STATUS_MESSAGE_UPPERCASE) + { + throw new InvalidAuthorizationResponseException(mail, response); + } + else if (!response.response_state.Trim().ToUpper().StartsWith(RESPONSE_OK)) + { + throw new InvalidResponseException($"Anmeldung {mail} fehlgeschlagen.\r\nServer Antwort: {response.response_text}", response); + } + + return response; + } + + /// + /// Checks if log out response is ok. + /// + /// Response to check whether it is valid. + /// + public static AuthorizationoutResponse GetIsResponseOk(this AuthorizationoutResponse p_oResponse) + { + if (AuthcookieNotDefinedException.IsAuthcookieNotDefined(p_oResponse, BIKES_LOGOUT_ACTIONTEXT, out AuthcookieNotDefinedException exception)) + { + throw exception; + } + + GetIsResponseOk(p_oResponse, BIKES_LOGOUT_ACTIONTEXT); + + if (p_oResponse.authcookie != "1") + { + throw new InvalidResponseException( + BIKES_LOGOUT_ACTIONTEXT, + p_oResponse); + } + + return p_oResponse; + } + + /// Gets if a call to reserve bike succeeded or not by checking a booking response. + /// Id of bike which should be booked. + /// Sessiong cookie of logged in user. + /// Response to check. + /// + public static BikeInfoReservedOrBooked GetIsReserveResponseOk( + this ReservationBookingResponse bookingResponse, + int bikeId) + { + GetIsResponseOk(bookingResponse, string.Format(AppResources.ExceptionTextReservationBikeFailedGeneral, bikeId)); + + if (BookingDeclinedException.IsBookingDeclined(bookingResponse.response_state, out BookingDeclinedException exception)) + { + throw exception; + } + + // Get bike which has to be booked. + var bikeInfoRequestedOccupied = bookingResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId); + if (bikeInfoRequestedOccupied == null) + { + throw new System.Exception(string.Format( + AppResources.ExceptionTextReservationBikeFailedUnavailalbe, + bikeId, + !string.IsNullOrWhiteSpace(bookingResponse?.response_text) ? $"\r\n{bookingResponse.response_text}" : string.Empty)); + } + + return bikeInfoRequestedOccupied; + } + + /// Gets if a booking call succeeded or not by checking a booking response. + /// Id of bike which should be booked. + /// Response to check. + /// + public static BikeInfoReservedOrBooked GetIsBookingResponseOk( + this ReservationBookingResponse bookingResponse, + int bikeId) + { + GetIsResponseOk(bookingResponse, string.Format(AppResources.ExceptionTextRentingBikeFailedGeneral, bikeId)); + + // Get bike which has to be booked. + var bikeInfoRequestedOccupied = bookingResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId); + if (bikeInfoRequestedOccupied == null) + { + throw new System.Exception(string.Format( + AppResources.ExceptionTextRentingBikeFailedUnavailalbe, + bikeId, + !string.IsNullOrWhiteSpace(bookingResponse?.response_text) ? $"\r\n{bookingResponse.response_text}" : string.Empty)); + } + + return bikeInfoRequestedOccupied; + } + + /// Gets if request is ok. + /// Response to verify. + /// Text describing request which is shown if validation fails. + /// Verified response. + public static T GetIsResponseOk(this T response, string textOfAction) where T : ResponseBase + { + if (response == null) + { + throw new InvalidResponseException(textOfAction, null); + } + + if (AuthcookieNotDefinedException.IsAuthcookieNotDefined(response, textOfAction, out AuthcookieNotDefinedException exception)) + { + throw exception; + } + + if (!response.response_state.Trim().ToUpper().StartsWith(RESPONSE_OK)) + { + throw new InvalidResponseException( + $"{textOfAction}\r\nServer Antwort: {response.response_text}", + response); + } + + return response; + } + + /// Gets if cancel or booking request is ok. + /// Response to verify. + /// Text describing request which is shown if validation fails. + /// Id of bike. + /// Verified response. + public static ReservationCancelReturnResponse GetIsCancelReservationResponseOk( + this ReservationCancelReturnResponse cancelBookingResponse, + int bikeId) + { + GetIsResponseOk(cancelBookingResponse, BIKES_CANCELREQUEST_ACTIONTEXT); + + // Get bike which has to be booked. + var l_oBikeInfo = cancelBookingResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId); + if (l_oBikeInfo != null) + { + throw new ReturnBikeException( + cancelBookingResponse, + $"{BIKES_CANCELREQUEST_ACTIONTEXT} Aufruf wurde erfolgreich ausgeführt, aber Rad ist noch in Liste der reservierten Räder enthalten."); + } + + return cancelBookingResponse; + } + + /// Gets if return bike request is ok. + /// Response to verify. + /// Text describing request which is shown if validation fails. + /// Id of bike. + /// Verified response. + public static ReservationCancelReturnResponse GetIsReturnBikeResponseOk( + this ReservationCancelReturnResponse returnBikeResponse, + int bikeId) + { + // Check if bike is at station. + if (NotAtStationException.IsNotAtStation(returnBikeResponse.response_state.ToUpper(), out NotAtStationException notAtStationException)) + { + throw notAtStationException; + } + + // Check if GPS data was send to copri. + if (NoGPSDataException.IsNoGPSData(returnBikeResponse.response_state.ToUpper(), out NoGPSDataException noGPSDataException)) + { + throw noGPSDataException; + } + + GetIsResponseOk(returnBikeResponse, BIKES_RETURNBIKE_ACTIONTEXT); + + // Get bike which has to be booked. + var l_oBikeInfo = returnBikeResponse?.bikes_occupied?.Values?.FirstOrDefault(x => x.bike == bikeId); + if (l_oBikeInfo != null) + { + throw new ReturnBikeException( + returnBikeResponse, + $"{BIKES_RETURNBIKE_ACTIONTEXT} Aufruf wurde erfolgreich ausgeführt, aber Rad ist noch in Liste der gemieteten Räder enthalten."); + } + + return returnBikeResponse; + } + + /// + /// Gets the response for bikes occupied request with no bikes reserved. + /// + /// + /// + public static BikesReservedOccupiedResponse GetBikesOccupiedNone(string p_strSesstionCookie = null) + { + var l_oJson = BIKES_OCCUPIED_REQUEST_NONE_FILE.Replace(@"""authcookie"": """"", @"""authcookie"": """ + (p_strSesstionCookie ?? string.Empty) + @""""); + return CopriCallsStatic.DeserializeBikesOccupiedResponse(l_oJson); + } + + /// + /// Holds an empty bikes occupied response. + /// + private const string BIKES_OCCUPIED_REQUEST_NONE_FILE = @" + { + ""tinkjson"": { + ""response_state"": ""OK"", + ""bikes_occupied"": { }, + ""authcookie"": """", + ""response"": ""user_bikes_occupied"", + ""apiserver"": ""https://tinkwwp.copri-bike.de"" + } + }"; + } +} diff --git a/TINKLib/Repository/Response/StationsAllResponse.cs b/TINKLib/Repository/Response/StationsAllResponse.cs new file mode 100644 index 0000000..696dd21 --- /dev/null +++ b/TINKLib/Repository/Response/StationsAllResponse.cs @@ -0,0 +1,43 @@ +using System.Collections.Generic; +using System.Runtime.Serialization; + +namespace TINK.Model.Repository.Response +{ + /// + /// Holds the information about all stations and is used for deserialization of copri answer. + /// + [DataContract] + public class StationsAllResponse : ResponseBase + { + /// + /// Holds info about a single station. + /// + [DataContract] + public class StationInfo + { + /// + /// Unique id of the station. + /// + [DataMember] + public int station { get; private set; } + + [DataMember] + public string station_group { get; private set; } + + [DataMember] + public string description { get; private set; } + + /// + /// Position of the station. + /// + [DataMember] + public string gps { get; private set; } + } + + /// + /// Dictionary of bikes. + /// + [DataMember] + public Dictionary stations { get; private set; } + } +} diff --git a/TINKLib/Repository/Response/SubmitFeedbackResponse.cs b/TINKLib/Repository/Response/SubmitFeedbackResponse.cs new file mode 100644 index 0000000..006e197 --- /dev/null +++ b/TINKLib/Repository/Response/SubmitFeedbackResponse.cs @@ -0,0 +1,8 @@ +using TINK.Model.Repository.Response; + +namespace TINK.Repository.Response +{ + public class SubmitFeedbackResponse : ResponseBase + { + } +} diff --git a/TINKLib/Repository/Response/TariffDescription.cs b/TINKLib/Repository/Response/TariffDescription.cs new file mode 100644 index 0000000..620c98a --- /dev/null +++ b/TINKLib/Repository/Response/TariffDescription.cs @@ -0,0 +1,47 @@ +using System.Runtime.Serialization; + +namespace TINK.Repository.Response +{ + /// + /// Holds tariff info for a single bike. + /// + [DataContract] + public record TariffDescription + { + /// + /// Name of the tariff. + /// + [DataMember] + public string name { get; private set; } + + /// + /// Number of the tariff. + /// + [DataMember] + public string number { get; private set; } + + /// + /// Costs per hour in euro. + /// + [DataMember] + public string eur_per_hour { get; private set; } + + /// + /// Costs of the abo per month. + /// + [DataMember] + public string abo_eur_per_month { get; private set; } + + /// + /// Costs per hour in euro. + /// + [DataMember] + public string free_hours { get; private set; } + + /// + /// Maximum fee per day. + /// + [DataMember] + public string max_eur_per_day { get; private set; } + } +} diff --git a/TINKLib/Services/BluetoothLock/ILocksServiceFake.cs b/TINKLib/Services/BluetoothLock/ILocksServiceFake.cs new file mode 100644 index 0000000..715df68 --- /dev/null +++ b/TINKLib/Services/BluetoothLock/ILocksServiceFake.cs @@ -0,0 +1,9 @@ +using TINK.Model.Bike; + +namespace TINK.Services.BluetoothLock +{ + public interface ILocksServiceFake : ILocksService + { + void UpdateSimulation(BikeCollection bikes); + } +} diff --git a/TINKLib/Services/BluetoothLock/LocksServiceInReach.cs b/TINKLib/Services/BluetoothLock/LocksServiceInReach.cs new file mode 100644 index 0000000..39c2d4b --- /dev/null +++ b/TINKLib/Services/BluetoothLock/LocksServiceInReach.cs @@ -0,0 +1,157 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Bike.BluetoothLock; +using TINK.Services.BluetoothLock.Tdo; +using TINK.Model.State; +using System; + +namespace TINK.Services.BluetoothLock +{ + /// + /// Facke locks service implementation which simulates locks which are in reach. + /// + public class LocksServiceInReach : ILocksServiceFake + { + private IEnumerable LocksInfo { get; set; } = new List(); + + /// Holds timeout values for series of connecting attemps to a lock or multiple locks. + public ITimeOutProvider TimeOut { get; set; } + + /// Connects to lock. + /// Info required to connect to lock. + /// Timeout for connect operation. + public async Task ConnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout) + { + return await Task.FromResult(new LockInfoTdo.Builder { Id = authInfo.Id, Guid = authInfo.Guid, State = LockitLockingState.Closed }.Build()); + } + + /// Timeout for connect operation of a single lock. + public async Task> GetLocksStateAsync(IEnumerable locksInfo, TimeSpan connectTimeout) + { + return await Task.FromResult(LocksInfo); + } + + public void UpdateSimulation(BikeCollection bikes) + { + var locksInfo = new List (); + + // Add and process locks info object + foreach (var bikeInfo in bikes.OfType()) + { + var lockInfo = bikeInfo.LockInfo; + + switch (bikeInfo.State.Value) + { + case InUseStateEnum.Disposable: + switch (lockInfo.State ) + { + case LockingState.Open: + case LockingState.Disconnected: + case LockingState.Unknown: + // Open bikes are never disposable because as soon as a they are open they get booked. + if (LocksInfo.FirstOrDefault(x => x.Id == lockInfo.Id) != null) + { + continue; // Lock was already added. + } + locksInfo.Add(new LockInfoTdo.Builder { Id = lockInfo.Id, State = LockitLockingState.Closed }.Build()); + break; + } + break; + + case InUseStateEnum.Reserved: + switch (lockInfo.State) + { + case LockingState.Open: + case LockingState.Disconnected: + case LockingState.Unknown: + // Closed bikes are never reserved because as soon as they are open they get booked. + if (LocksInfo.FirstOrDefault(x => x.Id == lockInfo.Id) != null) + { + continue; // Lock was already added. + } + locksInfo.Add(new LockInfoTdo.Builder { Id = lockInfo.Id, State = LockitLockingState.Closed }.Build()); + break; + } + break; + + case InUseStateEnum.Booked: + switch (lockInfo.State) + { + case LockingState.Disconnected: + case LockingState.Unknown: + if (LocksInfo.FirstOrDefault(x => x.Id == lockInfo.Id) != null) + { + continue; // Lock was already added. + } + locksInfo.Add(new LockInfoTdo.Builder { Id = lockInfo.Id, State = LockitLockingState.Closed }.Build()); + break; + } + break; + } + + LocksInfo = locksInfo; + } + } + + /// Opens a lock. + /// Id of lock to open. + public async Task OpenAsync(int bikeId, byte[] copriKey) + { + return await Task.FromResult(LockitLockingState.Open); + } + + /// Closes a lock. + /// Id of lock to close. + public async Task CloseAsync(int bikeId, byte[] copriKey) + { + return await Task.FromResult(LockitLockingState.Closed); + } + + /// Set sound settings. + /// Id of lock to set sound settings. + public async Task SetSoundAsync(int lockId, SoundSettings settings) + { + return await Task.FromResult(true); + } + + /// Sets whether alarm is on or off. + /// Id of lock to get info from. + public async Task SetIsAlarmOffAsync(int lockId, bool activated) + { + await Task.FromResult(true); + } + + /// Gets battery percentage. + /// Id of lock to get info for. + public Task GetBatteryPercentageAsync(int lockId) + { + throw new NotSupportedException(); + } + + /// Gets whether alarm is on or off. + /// Id of lock to get info for. + public async Task GetIsAlarmOffAsync(int lockId) + { + return await Task.FromResult(true); + } + + /// Gets a lock by bike Id. + /// + /// Lock object + public ILockService this[int bikeId] + { + get + { + return null; + } + } + + + /// Disconnects lock. + /// Id of lock to disconnect. + /// Guid of lock to disconnect. + public async Task DisconnectAsync(int bikeId, Guid bikeGuid) => await Task.FromResult(LockingState.Disconnected); + } +} diff --git a/TINKLib/Services/BluetoothLock/LocksServiceOutOfReach.cs b/TINKLib/Services/BluetoothLock/LocksServiceOutOfReach.cs new file mode 100644 index 0000000..2ea81ed --- /dev/null +++ b/TINKLib/Services/BluetoothLock/LocksServiceOutOfReach.cs @@ -0,0 +1,65 @@ +using System.Collections.Generic; +using System.Linq; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Bike.BluetoothLock; +using TINK.Services.BluetoothLock.Tdo; +using System; + +namespace TINK.Services.BluetoothLock +{ + /// + /// Facke locks service implementation which simulates locks which are out of reach. + /// + public class LocksServiceOutOfReach : ILocksServiceFake + { + private IEnumerable LocksInfo { get; set; } = new List(); + + /// Holds timeout values for series of connecting attemps to a lock or multiple locks. + public ITimeOutProvider TimeOut { get; set; } + + /// Connects to lock. + /// Info required to connect to lock. + /// Timeout for connect operation. + public async Task ConnectAsync(LockInfoAuthTdo authInfo, TimeSpan connectTimeout) + { + return await Task.FromResult(new LockInfoTdo.Builder { Id = authInfo.Id, Guid = authInfo.Guid, State = null }.Build()); + } + + /// No info availalbe because no lock is in reach. + /// Timeout for connect operation of a single lock. + /// Empty collection. + public async Task> GetLocksStateAsync(IEnumerable locksInfo, TimeSpan connectTimeout) => await Task.FromResult(LocksInfo); + + + /// No changes required because lock state is unknown. + public void UpdateSimulation(BikeCollection bikes) + { + var locksInfo = new List(); + + // Add and process locks info object + foreach (var bikeInfo in bikes.OfType()) + { + locksInfo.Add(new LockInfoTdo.Builder { Id = bikeInfo.LockInfo.Id, State = null }.Build()); + } + + LocksInfo = locksInfo; + } + + /// Gets a lock by bike Id. + /// + /// Lock object + public ILockService this[int bikeId] + { + get + { + return null; + } + } + + /// Disconnects lock. + /// Id of lock to disconnect. + /// Guid of lock to disconnect. + public async Task DisconnectAsync(int bikeId, Guid bikeGuid) => await Task.FromResult(LockingState.Disconnected); + } +} diff --git a/TINKLib/Services/BluetoothLock/LocksServicesContainerMutable.cs b/TINKLib/Services/BluetoothLock/LocksServicesContainerMutable.cs new file mode 100644 index 0000000..6d12f4c --- /dev/null +++ b/TINKLib/Services/BluetoothLock/LocksServicesContainerMutable.cs @@ -0,0 +1,49 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Services.BluetoothLock +{ + /// Manages Locks services and related configuration parameter. + public class LocksServicesContainerMutable : IEnumerable + { + /// Manages the different types of LocksService objects. + private ServicesContainerMutable LocksServices { get; } + + /// Holds the name of the default locks service to use. + public static string DefaultLocksservice => typeof(BLE.LockItByScanServicePolling).FullName; + + /// + /// Name of active lock service implementation to use. + /// Null for production (set of lock service implentations and some fake implementation will created) hash set of services for testing purposes. + public LocksServicesContainerMutable( + string activeLockService, + HashSet locksServices) + { + LocksServices = new ServicesContainerMutable( + locksServices, + activeLockService); + } + + /// Active locks service. + public ILocksService Active => LocksServices.Active; + + /// Sets a lock service as active locks service by name. + /// Name of the new locks service. + public void SetActive(string active) => LocksServices.SetActive(active); + + IEnumerator IEnumerable.GetEnumerator() + => LocksServices.Select(x => x.GetType().FullName).GetEnumerator(); + IEnumerator IEnumerable.GetEnumerator() + => LocksServices.GetEnumerator(); + + public void SetTimeOut(TimeSpan connectTimeout) + { + foreach (var locksService in LocksServices) + { + locksService.TimeOut = new TimeOutProvider(new List { connectTimeout }); + } + } + } +} diff --git a/TINKLib/Services/BluetoothLock/StateChecker.cs b/TINKLib/Services/BluetoothLock/StateChecker.cs new file mode 100644 index 0000000..2a51740 --- /dev/null +++ b/TINKLib/Services/BluetoothLock/StateChecker.cs @@ -0,0 +1,41 @@ +using Plugin.BLE.Abstractions.Contracts; +using System; +using System.Threading.Tasks; + +namespace TINK.Services.BluetoothLock +{ + public static class StateChecker + { + /// + /// Get current bluetooth state + /// + /// See https://github.com/xabre/xamarin-bluetooth-le/issues/112#issuecomment-380994887. + /// Crossplatform bluetooth implementation object + /// BluetoothState + public static Task GetBluetoothState(this IBluetoothLE ble) + { + var tcs = new TaskCompletionSource(); + + if (ble.State != BluetoothState.Unknown) + { + // If we can detect state out of box just returning in + tcs.SetResult(ble.State); + } + else + { + // Otherwise let's setup dynamic event handler and wait for first state update + EventHandler handler = null; + handler = (o, e) => + { + ble.StateChanged -= handler; + // and return it as our state + // we can have an 'Unknown' check here, but in normal situation it should never occur + tcs.SetResult(e.NewState); + }; + ble.StateChanged += handler; + } + + return tcs.Task; + } + } +} diff --git a/TINKLib/Services/CopriApi/CopriProviderHttps.cs b/TINKLib/Services/CopriApi/CopriProviderHttps.cs new file mode 100644 index 0000000..5344efe --- /dev/null +++ b/TINKLib/Services/CopriApi/CopriProviderHttps.cs @@ -0,0 +1,246 @@ +using Serilog; +using System; +using System.Threading.Tasks; +using TINK.Model.Repository; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Repository.Response; + +namespace TINK.Model.Services.CopriApi +{ + /// Object which manages calls to copri in a thread safe way inclding cache functionality. + public class CopriProviderHttps : ICachedCopriServer + { + /// Object which manages stored copri answers. + private ICopriCache CacheServer { get; } + + /// Communicates whith copri server. + private ICopriServer HttpsServer { get; } + + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => HttpsServer.IsConnected; + + /// Gets the session cookie if user is logged in, an empty string otherwise. + public string SessionCookie => HttpsServer.SessionCookie; + + /// Gets the merchant id. + public string MerchantId => HttpsServer.MerchantId; + + /// Constructs copri provider object to connet to https using a cache objet. + /// + /// + /// Holds the name and version of the TINKApp. + /// Cookie of user if a user is logged in, false otherwise. + /// Timespan which holds value after which cache expires. + /// Delegate which returns if cache conted is out of date or not. + public CopriProviderHttps( + Uri copriHost, + string merchantId, + string userAgent, + string sessionCookie = null, + TimeSpan? expiresAfter = null, + ICopriCache cacheServer = null, + ICopriServer httpsServer = null) + { + CacheServer = cacheServer ?? new CopriCallsMonkeyStore(merchantId, sessionCookie, expiresAfter); + HttpsServer = httpsServer ?? new CopriCallsHttps(copriHost, merchantId, userAgent, sessionCookie); + } + + /// Gets bikes available. + /// Id of the merchant. + /// Auto cookie of user if user is logged in. + /// Response holding list of bikes. + public async Task> GetBikesAvailable(bool fromCache = false) + { + Log.ForContext().Debug($"Request to get bikes available{(fromCache ? " from cache" : "")}..."); + if (!CacheServer.IsBikesAvailableExpired + || fromCache) + { + // No need to query because previous answer is not yet outdated. + Log.ForContext().Debug($"Returning bikes available from cache."); + return new Result(typeof(CopriCallsMonkeyStore), await CacheServer.GetBikesAvailableAsync()); + } + + try + { + Log.ForContext().Debug($"Querrying bikes available from copri."); + return new Result( + typeof(CopriCallsHttps), + (await HttpsServer.GetBikesAvailableAsync()).GetIsResponseOk("Abfrage der verfügbaren Räder fehlgeschlagen.")); + + } + catch (Exception exception) + { + // Return response from cache. + Log.ForContext().Debug("An error occurred querrying bikes available. {Exception}.", exception); + return new Result(typeof(CopriCallsMonkeyStore), await CacheServer.GetBikesAvailableAsync(), exception); + } + } + + /// Gets a list of bikes reserved/ booked by acctive user. + /// Cookie to authenticate user. + /// Response holding list of bikes. + public async Task> GetBikesOccupied(bool fromCache = false) + { + Log.ForContext().Debug($"Request to get bikes occupied{(fromCache ? " from cache" : "")}..."); + if (!CacheServer.IsBikesOccupiedExpired + || fromCache) + { + // No need to query because previous answer is not yet outdated. + Log.ForContext().Debug($"Returning bikes occupied from cache."); + return new Result(typeof(CopriCallsMonkeyStore), await CacheServer.GetBikesOccupiedAsync()); + } + + try + { + Log.ForContext().Debug($"Querrying bikes occupied from copri."); + return new Result( + typeof(CopriCallsHttps), + (await HttpsServer.GetBikesOccupiedAsync()).GetIsResponseOk("Abfrage der reservierten/ gebuchten Räder fehlgeschlagen.")); + + } + catch (Exception exception) + { + // Return response from cache. + Log.ForContext().Debug("An error occurred querrying bikes occupied. {Exception}.", exception); + return new Result(typeof(CopriCallsMonkeyStore), await CacheServer.GetBikesOccupiedAsync(), exception); + } + } + + /// Get list of stations. + /// List of files. + public async Task> GetStations(bool fromCache = false) + { + Log.ForContext().Debug($"Request to get stations{(fromCache ? " from cache" : "")}..."); + if (!CacheServer.IsStationsExpired + || fromCache) + { + // No need to query because previous answer is not yet outdated. + Log.ForContext().Debug($"Returning stations from cache."); + return new Result(typeof(CopriCallsMonkeyStore), await CacheServer.GetStationsAsync()); + } + + try + { + Log.ForContext().Debug($"Querrying stations from copri."); + return new Result( + typeof(CopriCallsHttps), + (await HttpsServer.GetStationsAsync()).GetIsResponseOk("Abfrage der Stationen fehlsgeschlagen.")); + } + catch (Exception exception) + { + // Return response from cache. + Log.ForContext().Debug("An error occurred querrying stations. {Exception}.", exception); + return new Result(typeof(CopriCallsMonkeyStore), await CacheServer.GetStationsAsync(), exception); + } + } + + /// Adds https--response to cache if response is ok. + /// Response to add to cache. + /// + public void AddToCache(Result result) + { + Log.ForContext().Debug($"Request to add stations all response to cache..."); + if (result.Source == typeof(CopriCallsMonkeyStore) + || result.Exception != null) + { + // Do not add responses form cache or invalid responses to cache. + return; + } + + Log.ForContext().Debug($"Add bikes available response to cache."); + CacheServer.AddToCache(result.Response); + } + + /// Adds https--response to cache if response is ok. + /// Response to add to cache. + /// + public void AddToCache(Result result) + { + Log.ForContext().Debug($"Request to add bikes available response to cache..."); + if (result.Source == typeof(CopriCallsMonkeyStore) + || result.Exception != null) + { + // Do not add responses form cache or invalid responses to cache. + return; + } + + Log.ForContext().Debug($"Add bikes available response to cache."); + CacheServer.AddToCache(result.Response); + + } + + /// Adds https--response to cache if response is ok. + /// Response to add to cache. + /// + public void AddToCache(Result result) + { + Log.ForContext().Debug($"Request to add bikes occupied response to cache..."); + if (result.Source == typeof(CopriCallsMonkeyStore) + || result.Exception != null) + { + // Do not add responses form cache or invalid responses to cache. + return; + } + + Log.ForContext().Debug($"Add bikes occupied response to cache."); + CacheServer.AddToCache(result.Response); + } + + public async Task DoAuthorizationAsync(string p_strMailAddress, string p_strPassword, string p_strDeviceId) + { + return await HttpsServer.DoAuthorizationAsync(p_strMailAddress, p_strPassword, p_strDeviceId); + } + + public async Task DoAuthoutAsync() + { + return await HttpsServer.DoAuthoutAsync(); + } + + public async Task DoReserveAsync(int p_iBikeId, Uri operatorUri) + { + return await HttpsServer.DoReserveAsync(p_iBikeId, operatorUri); + } + + public async Task DoCancelReservationAsync(int p_iBikeId, Uri operatorUri) + { + return await HttpsServer.DoCancelReservationAsync(p_iBikeId, operatorUri); + } + + public async Task CalculateAuthKeysAsync(int bikeId, Uri operatorUri) + { + return await HttpsServer.CalculateAuthKeysAsync(bikeId, operatorUri); + } + + public async Task UpdateLockingStateAsync( + int bikeId, + LocationDto location, + lock_state state, + double batteryLevel, + Uri operatorUri) + => await HttpsServer.UpdateLockingStateAsync(bikeId, location, state, batteryLevel, operatorUri); + + /// Books a bike. + /// Id of the bike to book. + /// Used to publish GUID from app to copri. Used for initial setup of bike in copri. + /// Holds the filling level percentage of the battery. + /// Response on booking request. + public async Task DoBookAsync(int bikeId, Guid guid, double batteryPercentage, Uri operatorUri) + { + return await HttpsServer.DoBookAsync(bikeId, guid, batteryPercentage, operatorUri); + } + + public async Task DoReturn(int bikeId, LocationDto location, Uri operatorUri) + { + return await HttpsServer.DoReturn(bikeId, location, operatorUri); + } + + /// + /// Submits feedback to copri server. + /// + /// General purpose message or error description. + /// True if bike is broken. + public async Task DoSubmitFeedback(string message, bool isBikeBroken, Uri opertorUri) => + await HttpsServer.DoSubmitFeedback(message, isBikeBroken, opertorUri); + } +} \ No newline at end of file diff --git a/TINKLib/Services/CopriApi/CopriProviderMonkeyStore.cs b/TINKLib/Services/CopriApi/CopriProviderMonkeyStore.cs new file mode 100644 index 0000000..44ed90c --- /dev/null +++ b/TINKLib/Services/CopriApi/CopriProviderMonkeyStore.cs @@ -0,0 +1,87 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Repository; +using TINK.Model.Repository.Request; +using TINK.Model.Repository.Response; +using TINK.Repository.Response; + +namespace TINK.Model.Services.CopriApi +{ + public class CopriProviderMonkeyStore : ICopriServer + { + /// Object which manages stored copri answers. + private readonly CopriCallsMonkeyStore monkeyStore; + + /// True if connector has access to copri server, false if cached values are used. + public bool IsConnected => monkeyStore.IsConnected; + + /// Gets the session cookie if user is logged in, an empty string otherwise. + public string SessionCookie => monkeyStore.SessionCookie; + + /// Constructs object which Object which manages stored copri answers in a thread save way. + /// Id of the merchant TINK-App. + public CopriProviderMonkeyStore( + string merchantId, + string sessionCookie) + { + monkeyStore = new CopriCallsMonkeyStore(merchantId, sessionCookie); + } + + /// Gets the merchant id. + public string MerchantId => monkeyStore.MerchantId; + + public Task DoReserveAsync(int p_iBikeId, Uri operatorUri) + => throw new NotSupportedException($"{nameof(DoReserveAsync)} is not cachable."); + + public Task DoCancelReservationAsync(int p_iBikeId, Uri operatorUri) + => throw new NotSupportedException($"{nameof(DoCancelReservationAsync)} is not cachable."); + + public Task CalculateAuthKeysAsync(int bikeId, Uri operatorUri) + => throw new NotSupportedException($"{nameof(CalculateAuthKeysAsync)} is not cachable."); + + public async Task UpdateLockingStateAsync( + int bikeId, + LocationDto geolocation, + lock_state state, + double batteryLevel, + Uri operatorUri) + => await monkeyStore.UpdateLockingStateAsync(bikeId, geolocation, state, batteryLevel, operatorUri); + + public async Task DoBookAsync(int bikeId, Guid guid, double batteryPercentage, Uri operatorUri) + { + return await monkeyStore.DoBookAsync(bikeId, guid, batteryPercentage, operatorUri); + } + + public async Task DoReturn(int bikeId, LocationDto geolocation, Uri operatorUri) + { + return await monkeyStore.DoReturn(bikeId, geolocation, operatorUri); + } + + public Task DoSubmitFeedback(string messge, bool bIsBikeBroke, Uri operatorUri) => throw new NotImplementedException(); + + public async Task DoAuthorizationAsync(string p_strMailAddress, string p_strPassword, string p_strDeviceId) + { + return await monkeyStore.DoAuthorizationAsync(p_strMailAddress, p_strPassword, p_strDeviceId); + } + + public async Task DoAuthoutAsync() + { + return await monkeyStore.DoAuthoutAsync(); + } + + public async Task GetBikesAvailableAsync() + { + return await monkeyStore.GetBikesAvailableAsync(); + } + + public async Task GetBikesOccupiedAsync() + { + return await monkeyStore.GetBikesOccupiedAsync(); + } + + public async Task GetStationsAsync() + { + return await monkeyStore.GetStationsAsync(); + } + } +} diff --git a/TINKLib/Services/CopriApi/ICachedCopriServer.cs b/TINKLib/Services/CopriApi/ICachedCopriServer.cs new file mode 100644 index 0000000..f688dbf --- /dev/null +++ b/TINKLib/Services/CopriApi/ICachedCopriServer.cs @@ -0,0 +1,37 @@ + +using System; +using System.Threading.Tasks; +using TINK.Model.Repository; +using TINK.Model.Repository.Response; + +namespace TINK.Model.Services.CopriApi +{ + /// Manages cache which holds copri data. + public interface ICachedCopriServer : ICopriServerBase + { + /// Get list of stations. + /// List of all stations. + Task> GetStations(bool fromCache = false); + + /// Gets a list of bikes from Copri. + /// Response holding list of bikes. + Task> GetBikesAvailable(bool fromCache = false); + + /// Gets a list of bikes reserved/ booked by acctive user from Copri. + /// Response holding list of bikes. + Task> GetBikesOccupied(bool fromCache = false); + + /// Adds https--response to cache if response is ok. + /// Response to add to cache. + void AddToCache(Result result); + + /// Adds https--response to cache if response is ok. + /// Response to add to cache. + void AddToCache(Result result); + + /// Adds https--response to cache if response is ok. + /// Response to add to cache. + /// + void AddToCache(Result result); + } +} diff --git a/TINKLib/Services/CopriApi/ICopriCache.cs b/TINKLib/Services/CopriApi/ICopriCache.cs new file mode 100644 index 0000000..24bd2cc --- /dev/null +++ b/TINKLib/Services/CopriApi/ICopriCache.cs @@ -0,0 +1,29 @@ +using TINK.Model.Repository; +using TINK.Model.Repository.Response; + +namespace TINK.Model.Services.CopriApi +{ + public interface ICopriCache : ICopriServer + { + /// Gets a value indicating whether stations are expired or not. + bool IsStationsExpired { get; } + + /// Adds a stations all response to cache. + /// Stations to add. + void AddToCache(StationsAllResponse stations); + + /// Gets a value indicating whether stations are expired or not. + bool IsBikesAvailableExpired { get; } + + /// Adds a bikes response to cache. + /// Bikes to add. + void AddToCache(BikesAvailableResponse bikes); + + /// Gets a value indicating whether stations are expired or not. + bool IsBikesOccupiedExpired { get; } + + /// Adds a bikes response to cache. + /// Bikes to add. + void AddToCache(BikesReservedOccupiedResponse bikes); + } +} diff --git a/TINKLib/Services/CopriApi/Result.cs b/TINKLib/Services/CopriApi/Result.cs new file mode 100644 index 0000000..e756ea0 --- /dev/null +++ b/TINKLib/Services/CopriApi/Result.cs @@ -0,0 +1,23 @@ +using System; + +namespace TINK.Model.Services.CopriApi +{ + public class Result where T : class + { + public Result(Type source, T response, System.Exception exception = null) + { + Source = source ?? throw new ArgumentException(nameof(source)); + Response = response ?? throw new ArgumentException(nameof(response)); + Exception = exception; + } + + /// Holds the copri respsonse + public T Response { get; } + + /// Specifies the souce of the copri response. + public Type Source { get; } + + /// Holds the exception if a communication error occurred. + public System.Exception Exception { get; private set; } + } +} diff --git a/TINKLib/Services/CopriApi/ServerUris/CopriHelper.cs b/TINKLib/Services/CopriApi/ServerUris/CopriHelper.cs new file mode 100644 index 0000000..b4f35f1 --- /dev/null +++ b/TINKLib/Services/CopriApi/ServerUris/CopriHelper.cs @@ -0,0 +1,45 @@ +using System; +using TINK.Model.Services.CopriApi.ServerUris; + +namespace TINK.Services.CopriApi.ServerUris +{ + public static class CopriHelper + { + public const string SHAREE_SILTEFOLDERNAME = "site"; + + /// Gets a value indicating whether a host is copri or not. + /// Host name. + /// True if server is copri, fals if not. + public static bool GetIsCopri(this string hostName) + => new Uri(CopriServerUriList.TINK_DEVEL).Host == hostName + || new Uri(CopriServerUriList.TINK_LIVE).Host == hostName; + + /// Get folder name depending on host name. + /// Host name. + /// Folder name. + public static string GetAppFolderName(this string hostName) + => hostName.GetIsCopri() + ? "tinkapp" /* resource tree is more complex for TINK/ konrad*/ + : "app"; + + /// Get folder name for a static site depending on host name. + /// Host name. + /// Folder name. + public static string GetSiteFolderName(this string hostName) + => hostName.GetIsCopri() + ? "tinkapp" /* resource tree is more complex for TINK/ konrad*/ + : SHAREE_SILTEFOLDERNAME; + + /// Get the agb resource name name depending on host name. + /// Host name. + /// AGB resource.. + public static string GetAGBResource(this string hostName) + => $"{hostName.GetSiteFolderName()}/{(hostName.GetIsCopri()? "konrad-TINK-AGB" : "agb.html")}"; + + /// Get the agb resource name name depending on host name. + /// Host name. + /// AGB resource.. + public static string GetPrivacyResource(this string hostName) + => $"{hostName.GetSiteFolderName()}/{(hostName.GetIsCopri() ? "Datenschutz" : "privacy.html")}"; + } +} diff --git a/TINKLib/Services/CopriApi/ServerUris/CopriServerUriList.cs b/TINKLib/Services/CopriApi/ServerUris/CopriServerUriList.cs new file mode 100644 index 0000000..edde6fc --- /dev/null +++ b/TINKLib/Services/CopriApi/ServerUris/CopriServerUriList.cs @@ -0,0 +1,88 @@ +using Newtonsoft.Json; +using System; +using System.Collections.Generic; +using System.Linq; + +namespace TINK.Model.Services.CopriApi.ServerUris +{ + [JsonObject(MemberSerialization.OptIn)] + sealed public class CopriServerUriList + { + /// + /// Holds the rest resource root name. + /// + public const string REST_RESOURCE_ROOT = "APIjsonserver"; + + /// Holds the URL of the TINK/Konrad test server. + public const string TINK_DEVEL = @"https://tinkwwp.copri-bike.de/APIjsonserver"; + + /// Holds the URL the TINK/Konrad productive server. + public const string TINK_LIVE = @"https://app.tink-konstanz.de/APIjsonserver"; + + /// Holds the URL the Sharee server. + public const string SHAREE_DEVEL = @"https://shareeapp-primary.copri-bike.de/APIjsonserver"; + + /// Holds the URL the Sharee server. + public const string SHAREE_LIVE = @"https://shareeapp-primary.copri.eu/APIjsonserver"; + + /// Constructs default uri list. + public CopriServerUriList(Uri activeUri = null) : this ( + new List + { + new Uri(SHAREE_LIVE), + new Uri(TINK_LIVE), + new Uri(SHAREE_DEVEL), + new Uri(TINK_DEVEL) + }.ToArray(), + activeUri ?? new Uri(SHAREE_LIVE)) // Default URi which is used after install of app + { + } + + /// Constructs uris object. + /// Object to copy from. + public CopriServerUriList(CopriServerUriList p_oSource) : this( + p_oSource.Uris.ToArray(), + p_oSource.ActiveUri) + { + } + + /// Constructs a valid uris object. + /// Known uris. + /// Zero based index of active uri. + /// Uri of the development server. + public CopriServerUriList( + Uri[] uris, + Uri p_oActiveUri) + { + if (uris == null || uris.Length < 1) + { + throw new ArgumentException($"Can not construct {typeof(CopriServerUriList)}- object. Array of uris must not be null and contain at least one element."); + } + + if (!uris.Contains(p_oActiveUri)) + { + throw new ArgumentException($"Active uri {p_oActiveUri} not contained in ({string.Join("; ", uris.Select(x => x.AbsoluteUri))})."); + } + + Uris = new List(uris); + ActiveUri = p_oActiveUri; + } + + /// Gets the active uri. + [JsonProperty] + public Uri ActiveUri { get; } + + /// Gets the active uri. + public static Uri DevelopUri => new(TINK_DEVEL); + + /// Gets the known uris. + [JsonProperty] + public IList Uris { get ; } + + /// Gets the default uri which is active after first installation. + public static Uri DefaultActiveUri + { + get { return new CopriServerUriList().ActiveUri; } + } + } +} diff --git a/TINKLib/Services/CopriApi/StationsAndBikesContainer.cs b/TINKLib/Services/CopriApi/StationsAndBikesContainer.cs new file mode 100644 index 0000000..06c4869 --- /dev/null +++ b/TINKLib/Services/CopriApi/StationsAndBikesContainer.cs @@ -0,0 +1,18 @@ +using TINK.Model.Bike; +using TINK.Model.Station; + +namespace TINK.Model.Services.CopriApi +{ + public class StationsAndBikesContainer + { + public StationsAndBikesContainer(StationDictionary stations, BikeCollection bikes) + { + StationsAll = stations; + Bikes = bikes; + } + + public StationDictionary StationsAll { get; } + + public BikeCollection Bikes { get; } + } +} diff --git a/TINKLib/Services/Geolocation/GeolocationService.cs b/TINKLib/Services/Geolocation/GeolocationService.cs new file mode 100644 index 0000000..886541a --- /dev/null +++ b/TINKLib/Services/Geolocation/GeolocationService.cs @@ -0,0 +1,59 @@ +using Serilog; +using System; +using System.Threading.Tasks; +using TINK.Model.Device; +using Xamarin.Essentials; + +namespace TINK.Model.Services.Geolocation +{ + public class GeolocationService : IGeolocation + { + /// Timeout for geolocation request operations. + private const int GEOLOCATIONREQUEST_TIMEOUT_MS = 5000; + + private IGeolodationDependent Dependent { get; } + + public GeolocationService(IGeolodationDependent dependent) + { + Dependent = dependent; + } + + public bool IsSimulation => false; + + public bool IsGeolcationEnabled => Dependent.IsGeolcationEnabled; + + /// Time when geolocation is of interest. Is used to determine whether cached geoloation can be used or not. + public async Task GetAsync(DateTime? timeStamp = null) + { + try + { + var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromMilliseconds(GEOLOCATIONREQUEST_TIMEOUT_MS)); + return await Xamarin.Essentials.Geolocation.GetLocationAsync(request); + } + catch (FeatureNotSupportedException fnsEx) + { + // Handle not supported on device exception + Log.ForContext().Error("Retrieving Geolocation not supported on device. {Exception}", fnsEx); + throw new Exception("Abfrage Standort nicht möglich auf Gerät.", fnsEx); + } + catch (FeatureNotEnabledException fneEx) + { + // Handle not enabled on device exception + Log.ForContext().Error("Retrieving Geolocation not enabled on device. {Exception}", fneEx); + throw new Exception("Abfrage Standort nicht aktiviert auf Gerät.", fneEx); + } + catch (PermissionException pEx) + { + // Handle permission exception + Log.ForContext().Error("Retrieving Geolocation not permitted on device. {Exception}", pEx); + throw new Exception("Berechtiung für Abfrage Standort nicht erteilt für App.", pEx); + } + catch (Exception ex) + { + // Unable to get location + Log.ForContext().Error("Retrieving Geolocation failed. {Exception}", ex); + throw new Exception("Abfrage Standort fehlgeschlagen.", ex); + } + } + } +} diff --git a/TINKLib/Services/Geolocation/IGeolocation.cs b/TINKLib/Services/Geolocation/IGeolocation.cs new file mode 100644 index 0000000..d67db74 --- /dev/null +++ b/TINKLib/Services/Geolocation/IGeolocation.cs @@ -0,0 +1,19 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Device; +using Xamarin.Essentials; + +namespace TINK.Model.Services.Geolocation +{ + /// Query geolocation. + public interface IGeolocation : IGeolodationDependent + { + /// Gets the current location. + /// Time when geolocation is of interest. Is used to determine for some implementations whether cached geoloation can be used or not. + /// + Task GetAsync(DateTime? timeStamp = null); + + /// If true location data returned is simulated. + bool IsSimulation { get; } + } +} diff --git a/TINKLib/Services/Geolocation/LastKnownGeolocationService.cs b/TINKLib/Services/Geolocation/LastKnownGeolocationService.cs new file mode 100644 index 0000000..02c76e6 --- /dev/null +++ b/TINKLib/Services/Geolocation/LastKnownGeolocationService.cs @@ -0,0 +1,72 @@ +using Serilog; +using System; +using System.Threading.Tasks; +using TINK.Model.Device; +using Xamarin.Essentials; + +namespace TINK.Model.Services.Geolocation +{ + public class LastKnownGeolocationService : IGeolocation + { + /// Timeout for geolocation request operations. + private const int GEOLOCATIONREQUEST_TIMEOUT_MS = 5000; + + private IGeolodationDependent Dependent { get; } + + public LastKnownGeolocationService(IGeolodationDependent dependent) + { + Dependent = dependent; + } + + /// Time when geolocation is of interest. Is used to determine whether cached geoloation can be used or not. + public async Task GetAsync(DateTime? timeStamp = null) + { + Location location; + try + { + var request = new GeolocationRequest(GeolocationAccuracy.Medium, TimeSpan.FromMilliseconds(GEOLOCATIONREQUEST_TIMEOUT_MS)); + location = await Xamarin.Essentials.Geolocation.GetLocationAsync(request); + } + catch (FeatureNotSupportedException fnsEx) + { + // Handle not supported on device exception + Log.ForContext().Error("Retrieving Geolocation not supported on device. {Exception}", fnsEx); + throw new Exception("Abfrage Standort nicht möglich auf Gerät.", fnsEx); + } + catch (FeatureNotEnabledException fneEx) + { + // Handle not enabled on device exception + Log.ForContext().Error("Retrieving Geolocation not enabled on device. {Exception}", fneEx); + throw new Exception("Abfrage Standort nicht aktiviert auf Gerät.", fneEx); + } + catch (PermissionException pEx) + { + // Handle permission exception + Log.ForContext().Error("Retrieving Geolocation not permitted on device. {Exception}", pEx); + throw new Exception("Berechtiung für Abfrage Standort nicht erteilt für App.", pEx); + } + catch (Exception ex) + { + // Unable to get location + Log.ForContext().Error("Retrieving Geolocation failed. {Exception}", ex); + throw new Exception("Abfrage Standort fehlgeschlagen.", ex); + } + + if (location != null // Cached location is available. + && (timeStamp == null || timeStamp.Value.Subtract(location.Timestamp.DateTime) < MaxAge)) + { + // No time stamp available or location not too old. + return location; + } + + return await Xamarin.Essentials.Geolocation.GetLocationAsync(); + } + + /// If true location data returned is simulated. + public bool IsSimulation { get => false; } + + public TimeSpan MaxAge => new TimeSpan(0, 3, 0); + + public bool IsGeolcationEnabled => Dependent.IsGeolcationEnabled; + } +} diff --git a/TINKLib/Services/Geolocation/SimulatedGeolocationService.cs b/TINKLib/Services/Geolocation/SimulatedGeolocationService.cs new file mode 100644 index 0000000..641223b --- /dev/null +++ b/TINKLib/Services/Geolocation/SimulatedGeolocationService.cs @@ -0,0 +1,27 @@ +using System; +using System.Threading.Tasks; +using TINK.Model.Device; +using Xamarin.Essentials; + +namespace TINK.Model.Services.Geolocation +{ + public class SimulatedGeolocationService : IGeolocation + { + private IGeolodationDependent Dependent { get; } + + public SimulatedGeolocationService(IGeolodationDependent dependent) + { + Dependent = dependent; + } + + public async Task GetAsync(DateTime? timeStamp = null) + { + return await Task.FromResult(new Location(47.976634, 7.825490) { Accuracy = 0, Timestamp = timeStamp ?? DateTime.Now }); ; + } + + /// If true location data returned is simulated. + public bool IsSimulation { get => true; } + + public bool IsGeolcationEnabled => Dependent.IsGeolcationEnabled; + } +} diff --git a/TINKLib/Services/ServicesContainerMutable.cs b/TINKLib/Services/ServicesContainerMutable.cs new file mode 100644 index 0000000..a456faa --- /dev/null +++ b/TINKLib/Services/ServicesContainerMutable.cs @@ -0,0 +1,57 @@ +using System; +using System.Collections; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace TINK.Services +{ + /// Container of service objects (locks , geolocation, ...) where one service is active. + /// All service objects must be of different type. + public class ServicesContainerMutable: IEnumerable, INotifyPropertyChanged + { + private readonly Dictionary serviceDict; + + public event PropertyChangedEventHandler PropertyChanged; + + /// Constructs object on startup of app. + /// Fixed list of service- objects . + /// LocksService name which is activated on startup of app. + public ServicesContainerMutable( + IEnumerable services, + string activeName) + { + serviceDict = services.Distinct().ToDictionary(x => x.GetType().FullName); + + if (!serviceDict.ContainsKey(activeName)) + { + throw new ArgumentException($"Can not instantiate {typeof(T).Name}- object. Active lock service {activeName} must be contained in [{String.Join(",", serviceDict)}]."); + } + + Active = serviceDict[activeName]; + } + + /// Active locks service. + public T Active { get; private set; } + + /// Sets a lock service as active locks service by name. + /// Name of the new locks service. + public void SetActive(string active) + { + if (!serviceDict.ContainsKey(active)) + { + throw new ArgumentException($"Can not set active lock service {active}. Service must be contained in [{String.Join(",", serviceDict)}]."); + } + + Active = serviceDict[active]; + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Active))); + } + + public IEnumerator GetEnumerator() + => serviceDict.Values.GetEnumerator(); + + IEnumerator IEnumerable.GetEnumerator() + => serviceDict.Values.GetEnumerator(); + } +} diff --git a/TINKLib/TINKLib.csproj b/TINKLib/TINKLib.csproj new file mode 100644 index 0000000..754e503 --- /dev/null +++ b/TINKLib/TINKLib.csproj @@ -0,0 +1,80 @@ + + + + 9.0 + + + 4.0 + en-GB + true + true + + + netstandard2.0 + TINK + 3.0 + en-GB + + + TRACE;USERFEEDBACKDLG_OFF + + + TRACE;USERFEEDBACKDLG_OFF + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + + True + True + AppResources.resx + + + + + PublicResXFileCodeGenerator + AppResources.Designer.cs + + + \ No newline at end of file diff --git a/TINKLib/TINKLib.sln b/TINKLib/TINKLib.sln new file mode 100644 index 0000000..779f5c2 --- /dev/null +++ b/TINKLib/TINKLib.sln @@ -0,0 +1,37 @@ + +Microsoft Visual Studio Solution File, Format Version 12.00 +# Visual Studio Version 16 +VisualStudioVersion = 16.0.31229.75 +MinimumVisualStudioVersion = 10.0.40219.1 +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "TINKLib", "TINKLib.csproj", "{E58134F3-2A53-4825-8580-14554CA405FD}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItShared", "..\LockItShared\LockItShared.csproj", "{FA5648EF-4A5C-4513-8CBF-3BBB3D66CD9E}" +EndProject +Project("{9A19103F-16F7-4668-BE54-9A1E7A4F7556}") = "LockItBLE", "..\LockItBLE\LockItBLE.csproj", "{2BDECA07-F070-4796-B334-B012729748DA}" +EndProject +Global + GlobalSection(SolutionConfigurationPlatforms) = preSolution + Debug|Any CPU = Debug|Any CPU + Release|Any CPU = Release|Any CPU + EndGlobalSection + GlobalSection(ProjectConfigurationPlatforms) = postSolution + {E58134F3-2A53-4825-8580-14554CA405FD}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {E58134F3-2A53-4825-8580-14554CA405FD}.Debug|Any CPU.Build.0 = Debug|Any CPU + {E58134F3-2A53-4825-8580-14554CA405FD}.Release|Any CPU.ActiveCfg = Release|Any CPU + {E58134F3-2A53-4825-8580-14554CA405FD}.Release|Any CPU.Build.0 = Release|Any CPU + {FA5648EF-4A5C-4513-8CBF-3BBB3D66CD9E}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {FA5648EF-4A5C-4513-8CBF-3BBB3D66CD9E}.Debug|Any CPU.Build.0 = Debug|Any CPU + {FA5648EF-4A5C-4513-8CBF-3BBB3D66CD9E}.Release|Any CPU.ActiveCfg = Release|Any CPU + {FA5648EF-4A5C-4513-8CBF-3BBB3D66CD9E}.Release|Any CPU.Build.0 = Release|Any CPU + {2BDECA07-F070-4796-B334-B012729748DA}.Debug|Any CPU.ActiveCfg = Debug|Any CPU + {2BDECA07-F070-4796-B334-B012729748DA}.Debug|Any CPU.Build.0 = Debug|Any CPU + {2BDECA07-F070-4796-B334-B012729748DA}.Release|Any CPU.ActiveCfg = Release|Any CPU + {2BDECA07-F070-4796-B334-B012729748DA}.Release|Any CPU.Build.0 = Release|Any CPU + EndGlobalSection + GlobalSection(SolutionProperties) = preSolution + HideSolutionNode = FALSE + EndGlobalSection + GlobalSection(ExtensibilityGlobals) = postSolution + SolutionGuid = {616DBD26-7262-4782-9A99-8A7A0228DEC3} + EndGlobalSection +EndGlobal diff --git a/TINKLib/View/IViewService.cs b/TINKLib/View/IViewService.cs new file mode 100644 index 0000000..be1bb34 --- /dev/null +++ b/TINKLib/View/IViewService.cs @@ -0,0 +1,82 @@ +using System; +using System.Threading.Tasks; + +namespace TINK.View +{ + public interface IViewService + { + /// Displays alert message. + /// Title of message. + /// Message to display. + /// Text of button. + Task DisplayAlert( + string title, + string message, + string cancel); + + /// Displays alert message. + /// Title of message. + /// Message to display. + /// Detailed error description. + /// Text of button. + Task DisplayAdvancedAlert( + string title, + string message, + string details, + string cancel); + + /// Displays alert message. + /// Title of message. + /// Message to display. + /// Text of accept button. + /// Text of button. + /// True if user pressed accept. + Task DisplayAlert(string p_strTitle, string p_strMessage, string p_strAccept, string p_strCancel); + + /// Displays an action sheet. + /// Title of message. + /// Message to display. + /// Text of button. + /// + /// Buttons holding options to select. + /// T + Task DisplayActionSheet(string title, string cancel, string destruction, params string[] buttons); + + /// Show a page. + /// Type of page to show. + /// Title of page to show. + void ShowPage(ViewTypes type, string title = null); + + /// Pushes a page onto the stack. + /// Page to display. + Task PushAsync(ViewTypes typeOfPage); + + /// Pushes a page onto the modal stack. + /// Page to display. + Task PushModalAsync(ViewTypes typeOfPage); + + /// Pushes a page onto the modal stack. + Task PopModalAsync(); + + Task DisplayUserFeedbackPopup(); + + /// + /// Feedback given by user when returning bike. + /// + public interface IUserFeedback + { + /// + /// Holds whether bike is broken or not. + /// + bool IsBikeBroken { get; set; } + + /// + /// Holds either + /// - general feedback + /// - error description of broken bike + /// or both. + /// + string Message { get; set; } + } + } +} diff --git a/TINKLib/View/MasterDetail/EmptyNavigationMasterDetail.cs b/TINKLib/View/MasterDetail/EmptyNavigationMasterDetail.cs new file mode 100644 index 0000000..055f6ce --- /dev/null +++ b/TINKLib/View/MasterDetail/EmptyNavigationMasterDetail.cs @@ -0,0 +1,15 @@ +using Serilog; +using System; + +namespace TINK.View.MasterDetail +{ + public class EmptyNavigationMasterDetail : INavigationMasterDetail + { + public bool IsGestureEnabled { set => Log.ForContext().Error($"Unexpected call of {nameof(IsGestureEnabled)} detected."); } + + public void ShowPage(Type p_oTypeOfPage, string p_strTitle = null) + { + Log.ForContext().Error($"Unexpected call of {nameof(ShowPage)} detected."); + } + } +} diff --git a/TINKLib/View/MasterDetail/IDetailPage.cs b/TINKLib/View/MasterDetail/IDetailPage.cs new file mode 100644 index 0000000..a3ffec8 --- /dev/null +++ b/TINKLib/View/MasterDetail/IDetailPage.cs @@ -0,0 +1,15 @@ +using TINK.View.MasterDetail; + +namespace TINK.View +{ + /// + /// Interface to provide navigation functionality to detail page. + /// + public interface IDetailPage + { + /// + /// Delegate to perform navigation. + /// + INavigationMasterDetail NavigationMasterDetail { set; } + } +} diff --git a/TINKLib/View/MasterDetail/INavigationMasterDetail.cs b/TINKLib/View/MasterDetail/INavigationMasterDetail.cs new file mode 100644 index 0000000..84e53da --- /dev/null +++ b/TINKLib/View/MasterDetail/INavigationMasterDetail.cs @@ -0,0 +1,18 @@ +using System; + +namespace TINK.View.MasterDetail +{ + public interface INavigationMasterDetail + { + /// + /// Creates and a page an shows it. + /// + /// Type of page to show. + void ShowPage(Type p_oTypeOfPage, string p_strTitle = null); + + /// + /// Set a value indicating whether master deatail navigation menu is available or not. + /// + bool IsGestureEnabled { set; } + } +} diff --git a/TINKLib/View/Themes/ITheme.cs b/TINKLib/View/Themes/ITheme.cs new file mode 100644 index 0000000..c9937a1 --- /dev/null +++ b/TINKLib/View/Themes/ITheme.cs @@ -0,0 +1,7 @@ +namespace TINK.View.Themes +{ + public interface ITheme + { + string OperatorInfo { get; } + } +} diff --git a/TINKLib/View/Themes/Konrad.xaml b/TINKLib/View/Themes/Konrad.xaml new file mode 100644 index 0000000..f992bdb --- /dev/null +++ b/TINKLib/View/Themes/Konrad.xaml @@ -0,0 +1,11 @@ + + + + Red + + + \ No newline at end of file diff --git a/TINKLib/View/Themes/Konrad.xaml.cs b/TINKLib/View/Themes/Konrad.xaml.cs new file mode 100644 index 0000000..070d048 --- /dev/null +++ b/TINKLib/View/Themes/Konrad.xaml.cs @@ -0,0 +1,17 @@ +using TINK.View.Themes; +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace TINK.Themes +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class Konrad : ResourceDictionary, ITheme + { + public Konrad () + { + InitializeComponent (); + } + + public string OperatorInfo => "Konrad Konstanz"; + } +} \ No newline at end of file diff --git a/TINKLib/View/Themes/ShareeBike.xaml b/TINKLib/View/Themes/ShareeBike.xaml new file mode 100644 index 0000000..468b5fe --- /dev/null +++ b/TINKLib/View/Themes/ShareeBike.xaml @@ -0,0 +1,11 @@ + + + + #009899 + + + \ No newline at end of file diff --git a/TINKLib/View/Themes/ShareeBike.xaml.cs b/TINKLib/View/Themes/ShareeBike.xaml.cs new file mode 100644 index 0000000..daea7a2 --- /dev/null +++ b/TINKLib/View/Themes/ShareeBike.xaml.cs @@ -0,0 +1,17 @@ +using TINK.View.Themes; +using Xamarin.Forms; +using Xamarin.Forms.Xaml; + +namespace TINK.Themes +{ + [XamlCompilation(XamlCompilationOptions.Compile)] + public partial class ShareeBike : ResourceDictionary, ITheme + { + public ShareeBike () + { + InitializeComponent (); + } + + public string OperatorInfo => string.Empty; + } +} \ No newline at end of file diff --git a/TINKLib/ViewModel/Account/AccountPageViewModel.cs b/TINKLib/ViewModel/Account/AccountPageViewModel.cs new file mode 100644 index 0000000..2cb2a36 --- /dev/null +++ b/TINKLib/ViewModel/Account/AccountPageViewModel.cs @@ -0,0 +1,442 @@ + +using Plugin.Connectivity; +using Serilog; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using TINK.Model; +using TINK.Model.Connector; +using TINK.Model.Repository.Exception; +using TINK.View; +using TINK.ViewModel.Settings; +using System.Linq; +using TINK.MultilingualResources; + +namespace TINK.ViewModel.Account +{ + public class AccountPageViewModel : INotifyPropertyChanged + { + /// + /// Count of requested/ booked bikes of logged in user. + /// + private int? m_iMyBikesCount; + + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + private IViewService m_oViewService; + + /// + /// Holds the exception which occurred getting bikes occupied information. + /// + private Exception m_oException; + + /// + /// Fired if a property changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// Object to manage update of view model objects from Copri. + private IPollingUpdateTaskManager m_oViewUpdateManager; + + /// Holds a reference to the external trigger service. + private Action OpenUrlInExternalBrowser { get; } + + /// Reference on the tink app instance. + private ITinkApp TinkApp { get; } + + /// Constructs a settings page view model object. + /// Reference to tink app model. + /// + /// + /// Filter to apply on stations and bikes. + /// Available copri server host uris including uri to use for next start. + /// Holds whether to poll or not and the periode leght is polling is on. + /// Default polling periode lenght. + /// Controls logging level. + /// Interface to view + public AccountPageViewModel( + ITinkApp tinkApp, + Action openUrlInExternalBrowser, + IViewService p_oViewService) + { + TinkApp = tinkApp + ?? throw new ArgumentException("Can not instantiate settings page view model- object. No tink app object available."); + + OpenUrlInExternalBrowser = openUrlInExternalBrowser + ?? throw new ArgumentException("Can not instantiate settings page view model- object. No user external browse service available."); + + m_oViewService = p_oViewService + ?? throw new ArgumentException("Can not instantiate settings page view model- object. No user view service available."); + + m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); + + Polling = new PollingViewModel(TinkApp.Polling); + + TinkApp.ActiveUser.StateChanged += OnStateChanged; + } + + /// + /// Log in state of user changed. + /// + /// + /// + private void OnStateChanged(object p_oSender, EventArgs p_oEventArgs) + { + var l_oPropertyChanged = PropertyChanged; + if (l_oPropertyChanged != null) + { + l_oPropertyChanged(this, new PropertyChangedEventArgs(nameof(LoggedInInfo))); + l_oPropertyChanged(this, new PropertyChangedEventArgs(nameof(IsBookingStateInfoVisible))); + l_oPropertyChanged(this, new PropertyChangedEventArgs(nameof(BookingStateInfo))); + } + } + + /// Holds information whether app is connected to web or not. + private bool? isConnected = null; + + /// Exposes the is connected state. + private bool IsConnected + { + get => isConnected ?? false; + set + { + var bookingStateInfo = BookingStateInfo; + isConnected = value; + if (bookingStateInfo == BookingStateInfo) + { + // Nothing to do. + return; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingStateInfo))); + } + } + + /// + /// Gets the value of sessionn cookie (for debugging purposes). + /// + public string SessionCookie + { + get { return TinkApp.ActiveUser.SessionCookie; } + } + + /// + /// Gets value whether a user is logged in or not. + /// + public bool IsLogoutPossible + { + get + { + return m_oException == null + && TinkApp.ActiveUser.IsLoggedIn; + } + } + + /// + /// Is true if user is logged in. + /// + public bool IsLoggedIn + { + get + { + return TinkApp.ActiveUser.IsLoggedIn; + } + } + + /// + /// Shows info about logged in user if user is logged in. Text "Logged out" otherwise. + /// + public string LoggedInInfo + { + get + { + if (!TinkApp.ActiveUser.IsLoggedIn) + { + return AppResources.MarkingLoggedInStateInfoNotLoggedIn; + } + + return TinkApp.ActiveUser.Group.Intersect(new List { FilterHelper.FILTERTINKGENERAL, FilterHelper.FILTERKONRAD }).Any() + ? string.Format(AppResources.MarkingLoggedInStateInfoLoggedInGroup, TinkApp.ActiveUser.Mail, TinkApp.ActiveUser.GetUserGroupDisplayName()) + : string.Format(AppResources.MarkingLoggedInStateInfoLoggedIn, TinkApp.ActiveUser.Mail); + } + } + + /// + /// Gets a value indicating whether booking state info is avilable or not. + /// + public bool IsBookingStateInfoVisible + { + get + { + return BookingStateInfo.Length > 0; + } + } + + /// + /// Count of bikes reserved and/ or occupied. + /// + private int? BikesOccupiedCount + { + get + { + return m_iMyBikesCount; + } + + set + { + var isBookingStateInfoVisible = IsBookingStateInfoVisible; + var bookingStateInfo = BookingStateInfo; + m_iMyBikesCount = value; + + if (isBookingStateInfoVisible != IsBookingStateInfoVisible) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsBookingStateInfoVisible))); + } + + if (bookingStateInfo != BookingStateInfo) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingStateInfo))); + } + } + } + + /// + /// Shows info about reserved/ booked bikes if there are. + /// + public string BookingStateInfo + { + get + { + if (!TinkApp.ActiveUser.IsLoggedIn || !m_iMyBikesCount.HasValue || m_iMyBikesCount.Value <= 0) + { + // Either + // - user is not logged in or + // - user has no bikes requested/ booked + return string.Empty; + } + + var bookingStateInfo = string.Format("Aktuell {0} Fahrräder reserviert/ gebucht.", m_iMyBikesCount.Value); + + if (!IsConnected) + { + // Append offline info + return $"{bookingStateInfo} Verbindungsstatus: Offline."; + } + + if (Exception != null) + { + // Append offline info + return $"{bookingStateInfo} Verbindungstatus: Verbindung unterbrochen. "; + } + + return bookingStateInfo; + } + } + + /// + /// Commang object to bind login button to view model. + /// + public System.Windows.Input.ICommand OnLogoutRequest + { + get + { + return new Xamarin.Forms.Command(async () => await Logout(), () => IsLogoutPossible); + } + } + + /// Processes request to view personal data. + public System.Windows.Input.ICommand OnManageAccount => new Xamarin.Forms.Command(async () => + { + if (CrossConnectivity.Current.IsConnected) + { + await m_oViewService.PushAsync(ViewTypes.ManageAccountPage); + } + else + { + await m_oViewService.DisplayAlert( + "Hinweis", + "Bitte mit Internet verbinden zum Verwalten der persönlichen Daten.", + "OK"); + } + }); + + + /// + /// Request to log out. + /// + public async Task Logout() + { + try + { + // Backup logout message before logout. + var l_oMessage = string.Format("Benutzer {0} abgemeldet.", TinkApp.ActiveUser.Mail); + + // Stop polling before requesting bike. + await m_oViewUpdateManager.StopUpdatePeridically(); + + try + { + // Log out user. + IsConnected = TinkApp.GetIsConnected(); + await TinkApp.GetConnector(IsConnected).Command.DoLogout(); + } + catch (Exception l_oException) + { + // Copri server is not reachable. + if (l_oException is WebConnectFailureException) + { + await m_oViewService.DisplayAlert( + "Verbingungsfehler bei Abmeldung!", + string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), + "OK"); + } + else + { + await m_oViewService.DisplayAlert("Fehler bei Abmeldung!", l_oException.Message, "OK"); + } + + // Restart polling again. + await m_oViewUpdateManager.StartUpdateAyncPeridically(Polling.ToImmutable()); + return; + } + + TinkApp.ActiveUser.Logout(); + + // Display information that log out was perfomrmed. + await m_oViewService.DisplayAlert("Auf Wiedersehen!", l_oMessage, "OK"); + } + catch (Exception p_oException) + { + Log.Error("An unexpected error occurred displaying log out page. {@Exception}", p_oException); + return; + } + try + { + // Switch to map view after log out. + m_oViewService.ShowPage(ViewTypes.MapPage); + } + catch (Exception p_oException) + { + Log.Error("An unexpected error occurred switching back to map page after displaying log out page. {@Exception}", p_oException); + return; + } + } + + /// + /// Invoked when page is shown. + /// Starts update process. + /// + public async Task OnAppearing() + { + IsConnected = TinkApp.GetIsConnected(); + BikesOccupiedCount = TinkApp.ActiveUser.IsLoggedIn + ? (await TinkApp.GetConnector(IsConnected).Query.GetBikesOccupiedAsync()).Response.Count + : 0; + + + m_oViewUpdateManager = new PollingUpdateTaskManager( + () => GetType().Name, + () => + { + TinkApp.PostAction( + unused => IsConnected = TinkApp.GetIsConnected(), + null); + + int? l_iBikesCount = null; + + var bikesOccupied = TinkApp.GetConnector(IsConnected).Query.GetBikesOccupiedAsync().Result; + l_iBikesCount = TinkApp.ActiveUser.IsLoggedIn + ? bikesOccupied.Response.Count + : 0; + + TinkApp.PostAction( + unused => + { + BikesOccupiedCount = l_iBikesCount; + Exception = bikesOccupied.Exception; + }, + null); + } + ); + try + { + // Update bikes at station or my bikes depending on context. + await m_oViewUpdateManager.StartUpdateAyncPeridically(Polling.ToImmutable()); + } + catch (Exception l_oExcetion) + { + Log.Error("Getting count of bikes on settings pags failed. {@l_oExcetion}.", l_oExcetion); + } + } + /// + /// Invoked when page is shutdown. + /// Currently invoked by code behind, would be nice if called by XAML in future versions. + /// + public async Task OnDisappearing() + { + try + { + Log.ForContext().Information($"Entering {nameof(OnDisappearing)}..."); + + await m_oViewUpdateManager.StopUpdatePeridically(); + + } + catch (Exception l_oException) + { + await m_oViewService.DisplayAlert( + "Fehler", + $"Ein unerwarteter Fehler ist aufgetreten. \r\n{l_oException.Message}", + "OK"); + } + } + + + /// + /// Exception which occurred getting bike information. + /// + protected Exception Exception + { + get + { + return m_oException; + } + + set + { + var l_oException = m_oException; + var bookingStateInfo = BookingStateInfo; + m_oException = value; + if ((m_oException != null && l_oException == null) + || (m_oException == null && l_oException != null)) + { + // Error information is available and was not or the other way round. + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLogoutPossible))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLogoutPossible))); + } + if (bookingStateInfo != BookingStateInfo) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(BookingStateInfo))); + } + } + } + + /// Polling periode. + public PollingViewModel Polling { get; } + + /// Opens login page. + public void RegisterRequest(string url) + { + try + { + OpenUrlInExternalBrowser(url); + } + catch (Exception p_oException) + { + Log.Error("Ein unerwarteter Fehler ist auf der Einstellungs- Seite beim Öffnen eines Browsers, Seite {url}, aufgetreten. {@Exception}", url, p_oException); + return; + } + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/BikeViewModel.cs b/TINKLib/ViewModel/Bikes/Bike/BC/BikeViewModel.cs new file mode 100644 index 0000000..ea39f43 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/BikeViewModel.cs @@ -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 +{ + /// + /// View model for a BC bike. + /// Provides functionality for views + /// - MyBikes + /// - BikesAtStation + /// + public class BikeViewModel : BikeViewModelBase, INotifyPropertyChanged + { + /// + /// Notifies GUI about changes. + /// + public override event PropertyChangedEventHandler PropertyChanged; + + /// Holds object which manages requests. + private IRequestHandler RequestHandler { get; set; } + + /// Raises events in order to update GUI. + public override void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs) => PropertyChanged?.Invoke(sender, eventArgs); + + /// + /// Constructs a bike view model object. + /// + /// Bike to be displayed. + /// Object holding logged in user or an empty user object. + /// Provides in use state information. + /// View model to be used for progress report and unlocking/ locking view. + public BikeViewModel( + Func isConnectedDelegate, + Func connectorFactory, + Action bikeRemoveDelegate, + Func 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); + } + + /// + /// 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. + /// + /// + 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))); + } + } + + /// Gets visiblity of the copri command button. + public bool IsButtonVisible => RequestHandler.IsButtonVisible; + + /// Gets the text of the copri command button. + public string ButtonText => RequestHandler.ButtonText; + + /// Processes request to perform a copri action (reserve bike and cancel reservation). + 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))); + } + }); + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Base.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Base.cs new file mode 100644 index 0000000..2c0b85c --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Base.cs @@ -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 + { + /// + /// View model to be used by subclasses for progress report and unlocking/ locking view. + /// + public IBikesViewModel BikesViewModel { get; set; } + + /// Gets the bike state. + public abstract InUseStateEnum State { get; } + + /// + /// Gets a value indicating whether the button to reserve bike is visible or not. + /// + public bool IsButtonVisible { get; } + + /// + /// Gets the name of the button when bike is disposable. + /// + public string ButtonText { get; } + + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + protected IViewService ViewService { get; } + + /// Provides an connector object. + protected Func ConnectorFactory { get; } + + /// Delegate to retrieve connected state. + protected Func IsConnectedDelegate { get; } + + /// Object to manage update of view model objects from Copri. + protected Func ViewUpdateManager { get; } + + /// Reference on the user + protected IUser ActiveUser { get; } + + /// Bike to display. + protected T SelectedBike { get; } + + /// Holds the is connected state. + private bool isConnected; + + /// Gets the is connected state. + public bool IsConnected + { + get => isConnected; + set + { + if (value == isConnected) + return; + + isConnected = value; + } + } + + /// Gets if the bike has to be remvoed after action has been completed. + public bool IsRemoveBikeRequired { get; set; } + + + /// + /// Constructs the reqest handler base. + /// + /// Bike which is reserved or for which reservation is canceled. + /// View model to be used by subclasses for progress report and unlocking/ locking view. + public Base( + T selectedBike, + string buttonText, + bool isCopriButtonVisible, + Func isConnectedDelegate, + Func connectorFactory, + Func 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."); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Booked.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Booked.cs new file mode 100644 index 0000000..032e1d6 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Booked.cs @@ -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 + { + /// Gets the bike state. + public InUseStateEnum State => InUseStateEnum.Booked; + + /// + /// If a bike is booked unbooking can not be done by though app. + /// + public bool IsButtonVisible => false; + + /// + /// Gets the name of the button when bike is cancel reservation. + /// + public string ButtonText => AppResources.ActionReturn; // "Miete beenden" + + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + protected IViewService ViewService { get; } + + /// View model to be used for progress report and unlocking/ locking view. + public IBikesViewModel BikesViewModel { get; } + + /// Executes user request to cancel reservation. + public async Task HandleRequest() + { + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; + BikesViewModel.IsIdle = false; + + Log.ForContext().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; + } + + /// + /// Bike to display. + /// + protected IBikeInfoMutable SelectedBike { get; } + + /// Gets the is connected state. + public bool IsConnected { get; set; } + + /// Gets if the bike has to be remvoed after action has been completed. + public bool IsRemoveBikeRequired => false; + + /// View model to be used for progress report and unlocking/ locking view. + 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."); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Disposable.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Disposable.cs new file mode 100644 index 0000000..8711324 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Disposable.cs @@ -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 +{ + /// View model to be used for progress report and unlocking/ locking view. + public class Disposable : Base, IRequestHandler + { + public Disposable( + BikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + Func viewUpdateManager, + IViewService viewService, + IBikesViewModel bikesViewModel, + IUser activeUser) : base(selectedBike, selectedBike.State.Value.GetActionText(), true, isConnectedDelegate, connectorFactory, viewUpdateManager, viewService, bikesViewModel, activeUser) + { + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Disposable; + + /// Request bike. + public async Task 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().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().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().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().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().Information("User reserved bike {l_oId} successfully.", SelectedBike.Id); + + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/IRequestHandler.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/IRequestHandler.cs new file mode 100644 index 0000000..fd5b4e1 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/IRequestHandler.cs @@ -0,0 +1,14 @@ + +using System.Threading.Tasks; + +namespace TINK.ViewModel.Bikes.Bike.BC.RequestHandler +{ + public interface IRequestHandler : IRequestHandlerBase + { + /// + /// Performs the copri action to be executed when user presses the copri button managed by request handler. + /// + /// New handler object if action suceesed, same handler otherwise. + Task HandleRequest(); + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/NotLoggedIn.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/NotLoggedIn.cs new file mode 100644 index 0000000..d623761 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/NotLoggedIn.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + 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; } + + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + protected IViewService ViewService { get; } + + /// View model to be used for progress report and unlocking/ locking view. + public IBikesViewModel BikesViewModel { get; } + + public bool IsConnected => throw new NotImplementedException(); + + /// Gets if the bike has to be removed after action has been completed. + public bool IsRemoveBikeRequired => false; + + public async Task HandleRequest() + { + Log.ForContext().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().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; + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Reserved.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Reserved.cs new file mode 100644 index 0000000..2246373 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandler/Reserved.cs @@ -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, IRequestHandler + { + /// View model to be used for progress report and unlocking/ locking view. + public Reserved( + BikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + Func viewUpdateManager, + IViewService viewService, + IBikesViewModel bikesViewModel, + IUser activeUser) : base(selectedBike, AppResources.ActionCancelRequest, true, isConnectedDelegate, connectorFactory, viewUpdateManager, viewService, bikesViewModel, activeUser) + { + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Reserved; + + /// Executes user request to cancel reservation. + public async Task 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().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().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().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().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().Information("User canceled reservation of bike {l_oId} successfully.", SelectedBike.Id); + + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandlerFactory.cs b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandlerFactory.cs new file mode 100644 index 0000000..24873fd --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/RequestHandlerFactory.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public static IRequestHandler Create( + BikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + Func 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); + } + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BC/StateToText.cs b/TINKLib/ViewModel/Bikes/Bike/BC/StateToText.cs new file mode 100644 index 0000000..5aaad50 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BC/StateToText.cs @@ -0,0 +1,26 @@ +using TINK.Model.State; + +namespace TINK.ViewModel.Bikes.Bike.BC +{ + public static class StateToText + { + /// Get button text for given copri state. + 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}"; + } + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BikeViewModelBase.cs b/TINKLib/ViewModel/Bikes/Bike/BikeViewModelBase.cs new file mode 100644 index 0000000..da4840e --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BikeViewModelBase.cs @@ -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 +{ + /// + /// Defines the type of BikesViewModel child items, i.e. BikesViewModel derives from ObservableCollection<BikeViewModelBase>. + /// Holds references to + /// - connection state services + /// - copri service + /// - view service + /// + public abstract class BikeViewModelBase + { + /// + /// Time format for text "Gebucht seit". + /// + public const string TIMEFORMAT = "dd. MMMM HH:mm"; + + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + protected IViewService ViewService { get; } + + /// Provides an connector object. + protected Func ConnectorFactory { get; } + + /// Delegate to retrieve connected state. + protected Func IsConnectedDelegate { get; } + + /// Removes bike from bikes view model. + protected Action BikeRemoveDelegate { get; } + + /// Object to manage update of view model objects from Copri. + public Func ViewUpdateManager { get; } + + /// + /// Holds the bike to display. + /// + protected BikeInfoMutable bike; + + /// Reference on the user + protected IUser ActiveUser { get; } + + /// + /// Provides context related info. + /// + private IInUseStateInfoProvider StateInfoProvider { get; } + + /// View model to be used for progress report and unlocking/ locking view. + protected IBikesViewModel BikesViewModel { get; } + + /// + /// Notifies GUI about changes. + /// + public abstract event PropertyChangedEventHandler PropertyChanged; + + /// + /// Notfies childs about changed bike state. + /// + public abstract void OnSelectedBikeStateChanged(); + + /// Raises events in order to update GUI. + public abstract void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs); + + /// + /// Constructs a bike view model object. + /// + /// Bike to be displayed. + /// Object holding logged in user or an empty user object. + /// Provides in use state information. + /// View model to be used for progress report and unlocking/ locking view. + public BikeViewModelBase( + Func isConnectedDelegate, + Func connectorFactory, + Action bikeRemoveDelegate, + Func 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."); + } + + /// + /// Handles BikeInfoMutable events. + /// Helper member to raise events. Maps model event change notification to view model events. + /// + /// + 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; + } + } + + /// + /// Gets the display name of the bike containing of bike id and type of bike.. + /// + public string Name + { + get + { + return bike.GetDisplayName(); + } + } + + /// + /// Gets the unique Id of bike used by derived model to determine which bike to remove. + /// + public int Id + { + get { return bike.Id; } + } + + /// + /// Returns status of a bike as text. + /// + /// Log invalid states for diagnose purposes. + 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); + } + } + } + + /// Gets the value of property when PropertyChanged was fired. + private string LastStateText { get; set; } + + /// + /// Gets reserved into display text. + /// + /// Log unexpeced states. + /// + /// Display text + private string GetReservedInfo( + TimeSpan? p_oRemainingTime, + int? p_strStation = null, + string p_strCode = null) + { + return StateInfoProvider.GetReservedInfo(p_oRemainingTime, p_strStation, p_strCode); + } + + /// + /// Gets booked into display text. + /// + /// Log unexpeced states. + /// + /// Display text + private string GetBookedInfo( + DateTime? p_oFrom, + int? p_strStation = null, + string p_strCode = null) + { + return StateInfoProvider.GetBookedInfo(p_oFrom, p_strStation, p_strCode); + } + + /// + /// Exposes the bike state. + /// + public InUseStateEnum State => bike.State.Value; + + /// Gets the value of property when PropertyChanged was fired. + public InUseStateEnum LastState { get; set; } + + /// + /// Gets the color which visualizes the state of bike in relation to logged in user. + /// + 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; + } + } + } + + /// Holds description about the tarif. + public TariffDescriptionViewModel TariffDescription => new TariffDescriptionViewModel(bike.TariffDescription); + + /// Gets the value of property when PropertyChanged was fired. + public Color LastStateColor { get; set; } + + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BikeViewModelFactory.cs b/TINKLib/ViewModel/Bikes/Bike/BikeViewModelFactory.cs new file mode 100644 index 0000000..b93af70 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BikeViewModelFactory.cs @@ -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 + { + /// Provides in use state information. + /// View model to be used for progress report and unlocking/ locking view. + public static BikeViewModelBase Create( + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Action bikeRemoveDelegate, + Func 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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/BikeViewModel.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/BikeViewModel.cs new file mode 100644 index 0000000..2c115a9 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/BikeViewModel.cs @@ -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 +{ + /// + /// View model for a ILockIt bike. + /// Provides functionality for views + /// - MyBikes + /// - BikesAtStation + /// + public class BikeViewModel : BikeViewModelBase, INotifyPropertyChanged + { + /// Notifies GUI about changes. + public override event PropertyChangedEventHandler PropertyChanged; + + private IGeolocation Geolocation { get; } + + private ILocksService LockService { get; } + + /// Holds object which manages requests. + private IRequestHandler RequestHandler { get; set; } + + /// Raises events in order to update GUI. + public override void RaisePropertyChanged(object sender, PropertyChangedEventArgs eventArgs) => PropertyChanged?.Invoke(sender, eventArgs); + + /// Raises events if property values changed in order to update GUI. + 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))); + } + } + + /// + /// Constructs a bike view model object. + /// + /// Bike to be displayed. + /// Object holding logged in user or an empty user object. + /// Provides in use state information. + /// View model to be used for progress report and unlocking/ locking view. + public BikeViewModel( + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Action bikeRemoveDelegate, + Func 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."); + } + + /// + /// 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. + /// + public override void OnSelectedBikeStateChanged() + { + var lastHandler = RequestHandler; + RequestHandler = RequestHandlerFactory.Create( + bike, + IsConnectedDelegate, + ConnectorFactory, + Geolocation, + LockService, + ViewUpdateManager, + ViewService, + BikesViewModel, + ActiveUser); + + RaisePropertyChangedEvent(lastHandler); + } + + /// Gets visiblity of the copri command button. + public bool IsButtonVisible => RequestHandler.IsButtonVisible; + + /// Gets the text of the copri command button. + public string ButtonText => RequestHandler.ButtonText; + + /// Gets visiblity of the ILockIt command button. + public bool IsLockitButtonVisible => RequestHandler.IsLockitButtonVisible; + + /// Gets the text of the ILockIt command button. + public string LockitButtonText => RequestHandler.LockitButtonText; + + /// Processes request to perform a copri action (reserve bike and cancel reservation). + public System.Windows.Input.ICommand OnButtonClicked => new Xamarin.Forms.Command(async () => await ClickButton(RequestHandler.HandleRequestOption1())); + + /// Processes request to perform a ILockIt action (unlock bike and lock bike). + public System.Windows.Input.ICommand OnLockitButtonClicked => new Xamarin.Forms.Command(async () => await ClickButton(RequestHandler.HandleRequestOption2())); + + /// Processes request to perform a copri action (reserve bike and cancel reservation). + private async Task ClickButton(Task 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; + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/Base.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/Base.cs new file mode 100644 index 0000000..0cd60ee --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/Base.cs @@ -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 + { + /// + /// Constructs the reqest handler base. + /// + /// Bike which is reserved or for which reservation is canceled. + /// View model to be used for progress report and unlocking/ locking view. + public Base( + Model.Bikes.Bike.BluetoothLock.IBikeInfoMutable selectedBike, + string buttonText, + bool isCopriButtonVisible, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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; } + + /// Gets the bike state. + public abstract override InUseStateEnum State { get; } + + public string LockitButtonText { get; protected set; } + + public bool IsLockitButtonVisible { get; protected set; } + + public string ErrorText => string.Empty; + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedClosed.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedClosed.cs new file mode 100644 index 0000000..adb59a4 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedClosed.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public BookedClosed( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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. + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Booked; + + /// Return bike. + public async Task 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().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().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().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().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().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().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().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().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().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().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().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike); + } + else + { + Log.ForContext().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); + } + + /// Open bike and update COPRI lock state. + public async Task HandleRequestOption2() + { + // Unlock bike. + Log.ForContext().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().Debug("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockOutOfReadMessage, + "OK"); + } + else if (exception is CouldntOpenBoldBlockedException) + { + Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockMessage, + "OK"); + } + else if (exception is CouldntOpenInconsistentStateExecption inconsistentState + && inconsistentState.State == LockingState.Closed) + { + Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockStillClosedMessage, + "OK"); + } + else + { + Log.ForContext().Error("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + exception.Message, + "OK"); + } + + // 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().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; + + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); + } + catch (Exception exception) + { + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User 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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedDisconnected.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedDisconnected.cs new file mode 100644 index 0000000..1a915dc --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedDisconnected.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public BookedDisconnected( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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; + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Booked; + + public Task HandleRequestOption1() + { + throw new NotSupportedException(); + } + + /// Scan for lock. + /// + public async Task HandleRequestOption2() + { + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.IsIdle = false; + Log.ForContext().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().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().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().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().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().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().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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedOpen.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedOpen.cs new file mode 100644 index 0000000..10808f7 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedOpen.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public BookedOpen( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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. + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Disposable; + + /// Close lock and return bike. + /// + public async Task 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().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().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().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().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockBoldBlockedMessage, + "OK"); + } + else + { + Log.ForContext().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().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().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().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().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().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().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().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().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().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().Information("User selected booked bike {bike} but returing failed. COPRI returned an error.", SelectedBike); + } + else + { + Log.ForContext().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); + } + + /// Close lock in order to pause ride and update COPRI lock state. + public async Task HandleRequestOption2() + { + // Unlock bike. + BikesViewModel.IsIdle = false; + Log.ForContext().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().Debug("Lock can not be closed. {Exception}", exception); + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockOutOfReachMessage, + "OK"); + } + else if (exception is CounldntCloseMovingException) + { + Log.ForContext().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockBoldBlockedMessage, + "OK"); + } + else + { + Log.ForContext().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().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().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User 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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedUnknown.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedUnknown.cs new file mode 100644 index 0000000..f3f9410 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/BookedUnknown.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public BookedUnknown( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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. + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Booked; + + /// Open bike and update COPRI lock state. + public async Task HandleRequestOption1() + { + // Unlock bike. + Log.ForContext().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().Debug("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockOutOfReadMessage, + "OK"); + } + else if (exception is CouldntOpenBoldBlockedException) + { + Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockMessage, + "OK"); + } + else if (exception is CouldntOpenInconsistentStateExecption inconsistentState + && inconsistentState.State == LockingState.Closed) + { + Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockStillClosedMessage, + "OK"); + } + else + { + Log.ForContext().Error("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + exception.Message, + "OK"); + } + + // 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().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; + + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); + } + catch (Exception exception) + { + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User 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); + } + + /// Close lock in order to pause ride and update COPRI lock state. + public async Task HandleRequestOption2() + { + // Unlock bike. + BikesViewModel.IsIdle = false; + Log.ForContext().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().Debug("Lock can not be closed. {Exception}", exception); + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockOutOfReachMessage, + "OK"); + } + else if (exception is CounldntCloseMovingException) + { + Log.ForContext().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockBoldBlockedMessage, + "OK"); + } + else + { + Log.ForContext().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().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().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User 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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/DisposableDisconnected.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/DisposableDisconnected.cs new file mode 100644 index 0000000..6d63a61 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/DisposableDisconnected.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public DisposableDisconnected( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func viewUpdateManager, + IViewService viewService, + IBikesViewModel bikesViewModel, + IUser activeUser) : base( + selectedBike, + AppResources.ActionRequest, // Copri text: "Rad reservieren" + true, // Show copri button to enable reserving and opening + isConnectedDelegate, + connectorFactory, + geolocation, + lockService, + viewUpdateManager, + viewService, + bikesViewModel, + activeUser) + { + LockitButtonText = GetType().Name; + IsLockitButtonVisible = false; // If bike is not reserved/ booked app can not connect to lock + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Disposable; + + /// Reserve bike and connect to lock. + public async Task HandleRequestOption1() + { + BikesViewModel.IsIdle = false; + + // Ask whether to really book bike? + var alertResult = await ViewService.DisplayAlert( + string.Empty, + string.Format(AppResources.QuestionReserveBike, SelectedBike.GetDisplayName(), StateRequestedInfo.MaximumReserveTime.Minutes), + AppResources.MessageAnswerYes, + AppResources.MessageAnswerNo); + + if (alertResult == false) + { + // User aborted booking process + Log.ForContext().Information("User selected availalbe bike {bike} in order to reserve but action was canceled.", SelectedBike); + BikesViewModel.IsIdle = true; + return this; + } + + // Lock list to avoid multiple taps while copri action is pending. + Log.ForContext().Information("Request to book and open lock for bike {bike} detected.", SelectedBike); + BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; + + // Stop polling before requesting bike. + await ViewUpdateManager().StopUpdatePeridically(); + + BikesViewModel.ActionText = AppResources.ActivityTextReservingBike; + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.DoReserve(SelectedBike); + } + catch (Exception exception) + { + BikesViewModel.ActionText = string.Empty; + + if (exception is BookingDeclinedException) + { + // Too many bikes booked. + Log.ForContext().Information("Request declined because maximum count of bikes {l_oException.MaxBikesCount} already requested/ booked.", (exception as BookingDeclinedException).MaxBikesCount); + + await ViewService.DisplayAlert( + AppResources.MessageTitleHint, + string.Format(AppResources.MessageReservationBikeErrorTooManyReservationsRentals, SelectedBike.Id, (exception as BookingDeclinedException).MaxBikesCount), + AppResources.MessageAnswerOk); + } + else if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User selected availalbe bike {bike} but reserving failed (Copri server not reachable).", SelectedBike); + + await ViewService.DisplayAlert( + "Verbingungsfehler beim Reservieren des Rads!", + string.Format("{0}\r\n{1}", exception.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), + "OK"); + } + else + { + Log.ForContext().Error("User selected availalbe bike {bike} but reserving failed. {@l_oException}", SelectedBike, exception); + + await ViewService.DisplayAlert( + "Fehler beim Reservieren des Rads!", + exception.Message, + "OK"); + } + + // Restart polling again. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return this; + } + + // Search for lock. + LockInfoTdo result = null; + BikesViewModel.ActionText = AppResources.ActivityTextSearchingLock; + try + { + result = await LockService.ConnectAsync( + new LockInfoAuthTdo.Builder { Id = SelectedBike.LockInfo.Id, Guid = SelectedBike.LockInfo.Guid, K_seed = SelectedBike.LockInfo.Seed, K_u = SelectedBike.LockInfo.UserKey }.Build(), + LockService.TimeOut.GetSingleConnect(1)); + } + catch (Exception exception) + { + // Do not display any messages here, because search is implicit. + if (exception is OutOfReachException) + { + Log.ForContext().Debug("Lock state can not be retrieved, lock is out of reach. {Exception}", exception); + + BikesViewModel.ActionText = "Schloss außerhalb Reichweite"; + } + else + { + Log.ForContext().Error("Lock state can not be retrieved. {Exception}", exception); + BikesViewModel.ActionText = "Schloss nicht gefunden"; + } + + // Restart polling again. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + SelectedBike.LockInfo.State = result?.State?.GetLockingState() ?? LockingState.Disconnected; + if (SelectedBike.LockInfo.State == LockingState.Disconnected) + { + // Do not display any messages here, because search is implicit. + Log.ForContext().Information("Lock for bike {bike} not found.", SelectedBike); + + // Restart polling again. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + SelectedBike.LockInfo.Guid = result?.Guid ?? new Guid(); + + Log.ForContext().Information("Lock found {bike} successfully.", SelectedBike); + + BikesViewModel.ActionText = string.Empty; + + // Ask whether to really book bike? + alertResult = await ViewService.DisplayAlert( + string.Empty, + string.Format(AppResources.MessageOpenLockAndBookeBike, SelectedBike.GetDisplayName()), + AppResources.MessageAnswerYes, + AppResources.MessageAnswerNo); + + if (alertResult == false) + { + // User aborted booking process + Log.ForContext().Information("User selected recently requested bike {bike} in order to reserve but did deny to book bike.", SelectedBike); + + // Disconnect lock. + BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; + try + { + SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); + } + catch (Exception exception) + { + Log.ForContext().Error("Lock can not be disconnected. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; + } + + // Restart polling again. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + Log.ForContext().Information("User selected recently requested bike {bike} in order to book.", SelectedBike); + + // Book bike prior to opening lock. + BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; + IsConnected = IsConnectedDelegate(); + try + { + await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike); + } + catch (Exception l_oException) + { + BikesViewModel.ActionText = string.Empty; + + if (l_oException is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User selected recently requested bike {l_oId} but booking failed (Copri server not reachable).", SelectedBike.Id); + + await ViewService.DisplayAdvancedAlert( + AppResources.MessageRentingBikeErrorConnectionTitle, + WebConnectFailureException.GetHintToPossibleExceptionsReasons, + l_oException.Message, + AppResources.MessageAnswerOk); + } + else + { + Log.ForContext().Error("User selected recently requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException); + + await ViewService.DisplayAdvancedAlert( + AppResources.MessageRentingBikeErrorGeneralTitle, + string.Empty, + l_oException.Message, + AppResources.MessageAnswerOk); + } + + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + // Unlock bike. + BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock; + try + { + SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected; + } + catch (Exception exception) + { + BikesViewModel.ActionText = string.Empty; + if (exception is OutOfReachException) + { + Log.ForContext().Debug("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockOutOfReadMessage, + "OK"); + } + else if (exception is CouldntOpenBoldBlockedException) + { + Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockMessage, + "OK"); + } + else if (exception is CouldntOpenInconsistentStateExecption inconsistentState + && inconsistentState.State == LockingState.Closed) + { + Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockStillClosedMessage, + "OK"); + } + else + { + Log.ForContext().Error("Lock can not be opened. {Exception}", exception); + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + exception.Message, + "OK"); + } + + SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException + ? stateAwareException.State + : LockingState.Disconnected; + + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + if (SelectedBike.LockInfo.State != LockingState.Open) + { + // Opening lock failed. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + + BikesViewModel.ActionText = ""; + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; + try + { + SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync()); + } + catch (Exception exception) + { + if (exception is OutOfReachException) + { + Log.ForContext().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; + + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); + } + catch (Exception exception) + { + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User reserved bike {bike} successfully.", SelectedBike); + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + public Task HandleRequestOption2() + { + throw new NotSupportedException(); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/DisposableOpen.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/DisposableOpen.cs new file mode 100644 index 0000000..7673fd6 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/DisposableOpen.cs @@ -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 +{ + /// Bike is disposable, lock is open and connected to app. + /// + /// 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 + /// + public class DisposableOpen : Base, IRequestHandler + { + /// Bike is disposable, lock is open and can be reached via bluetooth. + /// + /// 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. + /// + /// View model to be used for progress report and unlocking/ locking view. + public DisposableOpen( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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; + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Disposable; + + /// Books bike by reserving bike, opening lock and booking bike. + /// Next request handler. + public async Task 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().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().Debug("Lock can not be closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockOutOfReachMessage, + "OK"); + } + else if (exception is CounldntCloseMovingException) + { + Log.ForContext().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockBoldBlockedMessage, + "OK"); + } + else + { + Log.ForContext().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().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().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().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // 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().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().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().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().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().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 HandleRequestOption2() + { + Log.ForContext().Error("Click of unsupported button detected."); + return await Task.FromResult(this); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/IRequestHandler.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/IRequestHandler.cs new file mode 100644 index 0000000..3e4c2bd --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/IRequestHandler.cs @@ -0,0 +1,26 @@ +using System.Threading.Tasks; + +namespace TINK.ViewModel.Bikes.Bike.BluetoothLock +{ + public interface IRequestHandler : IRequestHandlerBase + { + /// Gets a value indicating whether the ILockIt button which is managed by request hadnler is visible or not. + bool IsLockitButtonVisible { get; } + + /// Gets the text of the ILockIt button which is managed by request handler. + string LockitButtonText { get; } + + /// + /// Performs the copri action to be executed when user presses the copri button managed by request handler. + /// + /// New handler object if action suceeded, same handler otherwise. + Task HandleRequestOption1(); + + Task HandleRequestOption2(); + + /// + /// Holds error discription (invalid state). + /// + string ErrorText { get; } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/InvalidState.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/InvalidState.cs new file mode 100644 index 0000000..b752161 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/InvalidState.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + 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}."); + } + + /// View model to be used for progress report and unlocking/ locking view. + 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; + + /// Gets if the bike has to be remvoed after action has been completed. + public bool IsRemoveBikeRequired => false; + + public string ErrorText { get; } + + public async Task HandleRequestOption2() + { + Log.ForContext().Error($"Click of unsupported button {nameof(HandleRequestOption2)} detected."); + return await Task.FromResult(this); + } + + public async Task HandleRequestOption1() + { + Log.ForContext().Error($"Click of unsupported button {nameof(HandleRequestOption1)} detected."); + return await Task.FromResult(this); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/NotLoggedIn.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/NotLoggedIn.cs new file mode 100644 index 0000000..4833020 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/NotLoggedIn.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + 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."); + } + + /// View model to be used for progress report and unlocking/ locking view. + 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; + + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + private IViewService ViewService { get; } + + public bool IsConnected => throw new NotImplementedException(); + + /// Gets if the bike has to be remvoed after action has been completed. + public bool IsRemoveBikeRequired => false; + + public async Task HandleRequestOption1() + { + Log.ForContext().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().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 HandleRequestOption2() + { + Log.ForContext().Error("Click of unsupported button detected."); + return await Task.FromResult(this); + } + + public string ErrorText => string.Empty; + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedClosed.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedClosed.cs new file mode 100644 index 0000000..b29b471 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedClosed.cs @@ -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 +{ + /// Bike is reserved, lock is closed and and connected to app. + /// + /// Occures when + /// - biks was reserved out of reach and is in reach now + /// - bike is is reserved while in reach + /// + public class ReservedClosed : Base, IRequestHandler + { + /// View model to be used for progress report and unlocking/ locking view. + public ReservedClosed( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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 + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Reserved; + + /// Cancel reservation. + public async Task 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().Information("User selected reserved bike {l_oId} in order to cancel reservation but action was canceled.", SelectedBike.Id); + BikesViewModel.IsIdle = true; + return this; + } + + Log.ForContext().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id); + + // Stop polling before cancel request. + BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; + await ViewUpdateManager().StopUpdatePeridically(); + + BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation; + IsConnected = IsConnectedDelegate(); + try + { + await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike); + + // If canceling bike succedes remove bike because it is not ready to be booked again + IsRemoveBikeRequired = true; + } + catch (Exception l_oException) + { + BikesViewModel.ActionText = string.Empty; + + if (l_oException is InvalidAuthorizationResponseException) + { + // Copri response is invalid. + Log.ForContext().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id); + + await ViewService.DisplayAlert( + "Fehler beim Aufheben der Reservierung!", + l_oException.Message, + "OK"); + } + else if (l_oException is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id); + await ViewService.DisplayAlert( + "Verbingungsfehler beim Aufheben der Reservierung!", + string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), + "OK"); + } + else + { + Log.ForContext().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().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().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); + } + + /// Open lock and book bike. + public async Task 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().Information("User selected requested bike {bike} in order to book but action was canceled.", SelectedBike); + BikesViewModel.IsIdle = true; + return this; + } + + Log.ForContext().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().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().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().Debug("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockOutOfReadMessage, + "OK"); + } + else if (exception is CouldntOpenBoldBlockedException) + { + Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockMessage, + "OK"); + } + else if (exception is CouldntOpenInconsistentStateExecption inconsistentState + && inconsistentState.State == LockingState.Closed) + { + Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockStillClosedMessage, + "OK"); + } + else + { + Log.ForContext().Error("Lock can not be opened. {Exception}", exception); + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + exception.Message, + "OK"); + } + + SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException + ? stateAwareException.State + : LockingState.Disconnected; + + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + if (SelectedBike.LockInfo.State != LockingState.Open) + { + // Opening lock failed. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + + BikesViewModel.ActionText = ""; + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; + try + { + SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync()); + } + catch (Exception exception) + { + if (exception is OutOfReachException) + { + Log.ForContext().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; + + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); + } + catch (Exception exception) + { + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User reserved bike {bike} successfully.", SelectedBike); + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedDisconnected.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedDisconnected.cs new file mode 100644 index 0000000..188b585 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedDisconnected.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public ReservedDisconnected( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func viewUpdateManager, + 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 + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Reserved; + + /// Cancel reservation. + public async Task 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().Information("User selected reserved bike {l_oId} in order to cancel reservation but action was canceled.", SelectedBike.Id); + BikesViewModel.IsIdle = true; + return this; + } + + Log.ForContext().Information("User selected reserved bike {l_oId} in order to cancel reservation.", SelectedBike.Id); + + // Stop polling before cancel request. + BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; + await ViewUpdateManager().StopUpdatePeridically(); + + BikesViewModel.ActionText = AppResources.ActivityTextCancelingReservation; + IsConnected = IsConnectedDelegate(); + try + { + await ConnectorFactory(IsConnected).Command.DoCancelReservation(SelectedBike); + + // If canceling bike succedes remove bike because it is not ready to be booked again + IsRemoveBikeRequired = true; + } + catch (Exception l_oException) + { + BikesViewModel.ActionText = string.Empty; + if (l_oException is InvalidAuthorizationResponseException) + { + // Copri response is invalid. + Log.ForContext().Error("User selected reserved bike {l_oId} but canceling reservation failed (Invalid auth. response).", SelectedBike.Id); + await ViewService.DisplayAlert("Fehler beim Aufheben der Reservierung!", l_oException.Message, "OK"); + } + else if (l_oException is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User selected reserved bike {l_oId} but cancel reservation failed (Copri server not reachable).", SelectedBike.Id); + await ViewService.DisplayAlert( + "Verbingungsfehler beim Aufheben der Reservierung!", + string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), + "OK"); + } + else + { + Log.ForContext().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().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); + } + + /// Connect to reserved bike. + /// + public async Task HandleRequestOption2() + { + BikesViewModel.IsIdle = false; + Log.ForContext().Information("Request to search for {bike} detected.", SelectedBike); + + // Stop polling before getting new auth-values. + BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; + await ViewUpdateManager().StopUpdatePeridically(); + + BikesViewModel.ActionText = AppResources.ActivityTextQuerryServer; + IsConnected = IsConnectedDelegate(); + try + { + // Repeat reservation to get a new seed/ k_user value. + await ConnectorFactory(IsConnected).Command.CalculateAuthKeys(SelectedBike); + } + catch (Exception exception) + { + BikesViewModel.ActionText = string.Empty; + + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User selected requested bike {l_oId} to connect to lock. (Copri server not reachable).", SelectedBike.Id); + + await ViewService.DisplayAlert( + "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().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().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().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().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().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().Information("User selected recently requested bike {bike} in order to reserve but did deny to book bike.", SelectedBike); + + // Disconnect lock. + BikesViewModel.ActionText = AppResources.ActivityTextDisconnectingLock; + try + { + SelectedBike.LockInfo.State = await LockService.DisconnectAsync(SelectedBike.LockInfo.Id, SelectedBike.LockInfo.Guid); + } + catch (Exception exception) + { + Log.ForContext().Error("Lock can not be disconnected. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorDisconnect; + } + + // Restart polling again. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + Log.ForContext().Information("User selected recently requested bike {bike} in order to book.", SelectedBike); + + // Book bike prior to opening lock. + BikesViewModel.ActionText = AppResources.ActivityTextRentingBike; + IsConnected = IsConnectedDelegate(); + try + { + await ConnectorFactory(IsConnected).Command.DoBook(SelectedBike); + } + catch (Exception l_oException) + { + BikesViewModel.ActionText = string.Empty; + + if (l_oException is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User selected recently requested bike {l_oId} but booking failed (Copri server not reachable).", SelectedBike.Id); + + await ViewService.DisplayAdvancedAlert( + AppResources.MessageRentingBikeErrorConnectionTitle, + WebConnectFailureException.GetHintToPossibleExceptionsReasons, + l_oException.Message, + AppResources.MessageAnswerOk); + } + else + { + Log.ForContext().Error("User selected recently requested bike {l_oId} but reserving failed. {@l_oException}", SelectedBike.Id, l_oException); + + await ViewService.DisplayAdvancedAlert( + AppResources.MessageRentingBikeErrorGeneralTitle, + string.Empty, + l_oException.Message, + AppResources.MessageAnswerOk); + } + + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + // Unlock bike. + BikesViewModel.ActionText = AppResources.ActivityTextOpeningLock; + try + { + SelectedBike.LockInfo.State = (await LockService[SelectedBike.LockInfo.Id].OpenAsync())?.GetLockingState() ?? LockingState.Disconnected; + } + catch (Exception exception) + { + BikesViewModel.ActionText = string.Empty; + if (exception is OutOfReachException) + { + Log.ForContext().Debug("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockOutOfReadMessage, + "OK"); + } + else if (exception is CouldntOpenBoldBlockedException) + { + Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockMessage, + "OK"); + } + else if (exception is CouldntOpenInconsistentStateExecption inconsistentState + && inconsistentState.State == LockingState.Closed) + { + Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockStillClosedMessage, + "OK"); + } + else + { + Log.ForContext().Error("Lock can not be opened. {Exception}", exception); + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + exception.Message, + "OK"); + } + + SelectedBike.LockInfo.State = exception is StateAwareException stateAwareException + ? stateAwareException.State + : LockingState.Disconnected; + + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + + BikesViewModel.ActionText = string.Empty; + BikesViewModel.IsIdle = true; + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + if (SelectedBike.LockInfo.State != LockingState.Open) + { + // Opening lock failed. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdater; + await ViewUpdateManager().StartUpdateAyncPeridically(); // Restart polling again. + + BikesViewModel.ActionText = ""; + BikesViewModel.IsIdle = true; // Unlock GUI + return RequestHandlerFactory.Create(SelectedBike, IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, ViewUpdateManager, ViewService, BikesViewModel, ActiveUser); + } + + BikesViewModel.ActionText = AppResources.ActivityTextReadingChargingLevel; + try + { + SelectedBike.LockInfo.BatteryPercentage = (await LockService[SelectedBike.LockInfo.Id].GetBatteryPercentageAsync()); + } + catch (Exception exception) + { + if (exception is OutOfReachException) + { + Log.ForContext().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; + + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); + } + catch (Exception exception) + { + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User reserved bike {bike} successfully.", SelectedBike); + + // 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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedOpen.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedOpen.cs new file mode 100644 index 0000000..3f9f722 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedOpen.cs @@ -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 +{ + /// Bike is reserved, lock is open and connected to app. + /// + /// 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 + { + /// View model to be used for progress report and unlocking/ locking view. + public ReservedOpen( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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. + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Reserved; + + /// Cancel reservation. + public async Task 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().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().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // 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().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().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().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().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().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().Debug("Lock can not be closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockOutOfReachStateReservedMessage, + "OK"); + } + else if (exception is CounldntCloseMovingException) + { + Log.ForContext().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockBoldBlockedMessage, + "OK"); + } + else + { + Log.ForContext().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().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().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().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().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().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); + } + + /// Manage sound/ alarm settings. + /// + public async Task HandleRequestOption2() + { + // Stop polling before requesting bike. + BikesViewModel.ActionText = AppResources.ActivityTextOneMomentPlease; + await ViewUpdateManager().StopUpdatePeridically(); + + // Close lock + Log.ForContext().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().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().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().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().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().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().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().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().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().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().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; + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedUnknown.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedUnknown.cs new file mode 100644 index 0000000..3404ab6 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandler/ReservedUnknown.cs @@ -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 + { + /// View model to be used for progress report and unlocking/ locking view. + public ReservedUnknown( + IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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. + } + + /// Gets the bike state. + public override InUseStateEnum State => InUseStateEnum.Reserved; + + /// Open bike and update COPRI lock state. + public async Task HandleRequestOption1() + { + // Unlock bike. + Log.ForContext().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().Debug("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockOutOfReadMessage, + "OK"); + } + else if (exception is CouldntOpenBoldBlockedException) + { + Log.ForContext().Debug("Lock can not be opened. Bold is blocked. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockMessage, + "OK"); + } + else if (exception is CouldntOpenInconsistentStateExecption inconsistentState + && inconsistentState.State == LockingState.Closed) + { + Log.ForContext().Debug("Lock can not be opened. lock reports state closed. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + AppResources.ErrorOpenLockStillClosedMessage, + "OK"); + } + else + { + Log.ForContext().Error("Lock can not be opened. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorOpenLockTitle, + exception.Message, + "OK"); + } + + // 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().Debug("Akkustate can not be read, bike out of range. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelOutOfReach; + } + else + { + Log.ForContext().Error("Akkustate can not be read. {Exception}", exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorReadingChargingLevelGeneral; + } + } + + // Lock list to avoid multiple taps while copri action is pending. + BikesViewModel.ActionText = AppResources.ActivityTextStartingUpdatingLockingState; + + IsConnected = IsConnectedDelegate(); + + try + { + await ConnectorFactory(IsConnected).Command.UpdateLockingStateAsync(SelectedBike); + } + catch (Exception exception) + { + if (exception is WebConnectFailureException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. {response}.", SelectedBike, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed . {@l_oException}", SelectedBike.Id, exception); + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User 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); + } + + /// Close lock in order to pause ride and update COPRI lock state. + public async Task HandleRequestOption2() + { + // Unlock bike. + BikesViewModel.IsIdle = false; + Log.ForContext().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().Debug("Lock can not be closed. {Exception}", exception); + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockOutOfReachMessage, + "OK"); + } + else if (exception is CounldntCloseMovingException) + { + Log.ForContext().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().Debug("Lock can not be closed. Lock is out of reach. {Exception}", exception); + + await ViewService.DisplayAlert( + AppResources.ErrorCloseLockTitle, + AppResources.ErrorCloseLockBoldBlockedMessage, + "OK"); + } + else + { + Log.ForContext().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().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().Information("User locked bike {bike} in order to pause ride but updating failed (Copri server not reachable).", SelectedBike); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorNoWebUpdateingLockstate; + } + else if (exception is ResponseException copriException) + { + // Copri server is not reachable. + Log.ForContext().Information("User locked bike {bike} in order to pause ride but updating failed. Message: {Message} Details: {Details}", SelectedBike, copriException.Message, copriException.Response); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorStatusUpdateingLockstate; + } + else + { + Log.ForContext().Error("User locked bike {bike} in order to pause ride but updating failed. {@l_oException}", SelectedBike.Id, exception); + + BikesViewModel.ActionText = AppResources.ActivityTextErrorConnectionUpdateingLockstate; + } + } + + Log.ForContext().Information("User 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); + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandlerFactory.cs b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandlerFactory.cs new file mode 100644 index 0000000..18910e0 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/BluetoothLock/RequestHandlerFactory.cs @@ -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 + { + /// Creates a request handler. + /// + /// + /// + /// + /// + /// + /// View model to be used for progress report and unlocking/ locking view. + /// Request handler. + public static IRequestHandler Create( + Model.Bikes.Bike.BC.IBikeInfoMutable selectedBike, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + Func 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 + /// + 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)); + } + } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/IRequestHandlerBase.cs b/TINKLib/ViewModel/Bikes/Bike/IRequestHandlerBase.cs new file mode 100644 index 0000000..178ef0c --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/IRequestHandlerBase.cs @@ -0,0 +1,33 @@ +using TINK.Model.State; + +namespace TINK.ViewModel.Bikes.Bike +{ + /// + /// Base interface for Copri and ILockIt request handler. + /// Copri communication is used by both handlers. + /// + public interface IRequestHandlerBase + { + /// Gets the bike state. + InUseStateEnum State { get; } + + /// + /// Gets a value indicating whether the copri button which is managed by request handler is visible or not. + /// + bool IsButtonVisible { get; } + + /// View model to be used for progress report and unlocking/ locking view. + IBikesViewModel BikesViewModel { get; } + + /// + /// Gets the text of the copri button which is managed by request handler. + /// + string ButtonText { get; } + + /// Gets the is connected state. + bool IsConnected { get; } + + /// Gets if the bike has to be remvoed after action has been completed. + bool IsRemoveBikeRequired { get; } + } +} diff --git a/TINKLib/ViewModel/Bikes/Bike/TariffDescriptionViewModel.cs b/TINKLib/ViewModel/Bikes/Bike/TariffDescriptionViewModel.cs new file mode 100644 index 0000000..0f262dc --- /dev/null +++ b/TINKLib/ViewModel/Bikes/Bike/TariffDescriptionViewModel.cs @@ -0,0 +1,66 @@ +using System; +using TINK.Model.Bikes.Bike; +using TINK.MultilingualResources; + +namespace TINK.ViewModel.Bikes.Bike +{ + /// + /// View model for displaying tariff info. + /// + 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 : "-"); + } + } + + /// + /// Costs per hour in euro. + /// + public string FeeEuroPerHour + => !double.IsNaN(Tariff.FeeEuroPerHour) + ? string.Format("{0} {1}", Tariff.FeeEuroPerHour.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerHour) + : string.Empty; + + /// + /// Costs of the abo per month. + /// + public string AboEuroPerMonth + => !double.IsNaN(Tariff.AboEuroPerMonth) + ? string.Format("{0} {1}", Tariff.AboEuroPerMonth.ToString("0.00"), AppResources.MessageBikesManagementTariffDescriptionEuroPerHour) + : string.Empty; + + /// + /// Free use time. + /// + public string FreeTimePerSession + => Tariff.FreeTimePerSession != TimeSpan.Zero + ? string.Format("{0} {1}", Tariff.FreeTimePerSession.TotalHours, AppResources.MessageBikesManagementTariffDescriptionHour) + : string.Empty; + + /// + /// Max costs per day in euro. + /// + public string MaxFeeEuroPerDay + => !double.IsNaN(Tariff.FeeEuroPerHour) + ? string.Format("{0} {1}", Tariff.MaxFeeEuroPerDay.ToString("0.00"), AppResources.MessageBikesManagementMaxFeeEuroPerDay) + : string.Empty; + } +} diff --git a/TINKLib/ViewModel/Bikes/BikesViewModel.cs b/TINKLib/ViewModel/Bikes/BikesViewModel.cs new file mode 100644 index 0000000..a44991f --- /dev/null +++ b/TINKLib/ViewModel/Bikes/BikesViewModel.cs @@ -0,0 +1,443 @@ +using Serilog; +using System; +using System.Collections.ObjectModel; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Connector; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.Geolocation; +using TINK.Model.User; +using TINK.View; +using TINK.ViewModel.Bikes.Bike; +using TINK.ViewModel.Bikes.Bike.BC; +using Plugin.Permissions.Abstractions; +using Plugin.BLE.Abstractions.Contracts; + +namespace TINK.ViewModel.Bikes +{ + public abstract class BikesViewModel : ObservableCollection, IBikesViewModel + { + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + protected IViewService ViewService { get; } + + /// + /// Holds the exception which occurred getting bikes occupied information. + /// + private Exception m_oException; + + /// Provides an connector object. + protected Func ConnectorFactory { get; } + + protected IGeolocation Geolocation { get; } + + /// Provides an connector object. + protected ILocksService LockService { get; } + + /// Delegate to retrieve connected state. + protected Func IsConnectedDelegate { get; } + + /// Holds whether to poll or not and the periode leght is polling is on. + private TINK.Settings.PollingParameters m_oPolling; + + /// Object to manage update of view model objects from Copri. + protected IPollingUpdateTaskManager m_oViewUpdateManager; + + /// Action to post to GUI thread. + public Action PostAction { get; } + + /// Enables derived class to fire property changed event. + /// + protected override void OnPropertyChanged(PropertyChangedEventArgs p_oEventArgs) => base.OnPropertyChanged(p_oEventArgs); + + /// + /// Handles events from bike viewmodel which require GUI updates. + /// + /// + /// + private void OnBikeRequestHandlerPropertyChanged(object sender, PropertyChangedEventArgs e) + { + OnPropertyChanged(e); + } + + /// + /// Instantiates a new item. + /// + Func m_oItemFactory; + + /// + /// Constructs bike collection view model. + /// + /// + /// Mail address of active user. + /// Holds object to query location permisions. + /// Holds object to query bluetooth state. + /// Specifies on which platform code is run. + /// Returns if mobile is connected to web or not. + /// Connects system to copri for purposes of requesting a bike/ cancel request. + /// Service to control lock retrieve info. + /// Holds whether to poll or not and the periode leght is polling is on. + /// Executes actions on GUI thread. + /// Interface to actuate methodes on GUI. + public BikesViewModel( + User user, + IPermissions permissions, + IBluetoothLE bluetoothLE, + string runtimPlatform, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + TINK.Settings.PollingParameters polling, + Action postAction, + IViewService viewService, + Func itemFactory) + { + User = user + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No user available."); + + RuntimePlatform = runtimPlatform + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No runtime platform information available."); + + Permissions = permissions + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No permissions available."); + + BluetoothLE = bluetoothLE + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No bluetooth available."); + + ConnectorFactory = connectorFactory + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No connector available."); + + Geolocation = geolocation + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No geolocation object available."); + + LockService = lockService + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No lock service object available."); + + IsConnectedDelegate = isConnectedDelegate + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No is connected delegate available."); + + m_oItemFactory = itemFactory + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No factory member available."); + + PostAction = postAction + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No post action available."); + + ViewService = viewService + ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No view available."); + + m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); + + BikeCollection = new BikeCollectionMutable(); + + BikeCollection.CollectionChanged += OnDecoratedCollectionChanged; + + m_oPolling = polling; + + IsConnected = IsConnectedDelegate(); + + CollectionChanged += (sender, eventargs) => + { + // Notify about bikes occuring/ vanishing from list. + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBikesListVisible))); + }; + } + + /// + /// Is invoked if decorated bike collection changes. + /// Collection of view model objects has to be synced. + /// + /// Sender of the event. + /// Event arguments. + private void OnDecoratedCollectionChanged(object p_oSender, System.Collections.Specialized.NotifyCollectionChangedEventArgs p_oEventArgs) + { + switch (p_oEventArgs.Action) + { + case System.Collections.Specialized.NotifyCollectionChangedAction.Add: + + // New bike avaialbe (new arrived at station or bike wased booked on a different device) + foreach (var l_oBike in BikeCollection) + { + if (!Contains(l_oBike.Id)) + { + var bikeViewModel = BikeViewModelFactory.Create( + IsConnectedDelegate, + ConnectorFactory, + Geolocation, + LockService, + (id) => Remove(id), + () => m_oViewUpdateManager, + ViewService, + l_oBike, + User, + m_oItemFactory(), + this); + + bikeViewModel.PropertyChanged += OnBikeRequestHandlerPropertyChanged; + + Add(bikeViewModel); + } + } + + break; + + case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: + + // Bike was removed (either a different user requested/ booked a bike or request expired or bike was returned.) + foreach (BikeViewModelBase l_oBike in Items) + { + if (!BikeCollection.ContainsKey(l_oBike.Id)) + { + l_oBike.PropertyChanged -= OnBikeRequestHandlerPropertyChanged; + + Remove(l_oBike); + break; + } + } + + break; + } + } + + /// All bikes to be displayed. + protected BikeCollectionMutable BikeCollection { get; private set; } + + protected User User { get; private set; } + + /// Specified whether code is run under iOS or Android. + protected string RuntimePlatform { get; private set; } + + protected IPermissions Permissions { get; private set; } + + protected IBluetoothLE BluetoothLE { get; private set; } + + /// + /// User which is logged in. + /// + public User ActiveUser + { + get + { + return User; + } + } + + /// + /// Exception which occurred getting bike information. + /// + protected Exception Exception + { + get + { + return m_oException; + } + + set + { + var l_oException = m_oException; + var statusInfoText = StatusInfoText; + m_oException = value; + if ((m_oException != null && l_oException == null) + || (m_oException == null && l_oException != null)) + { + // Because an error occurred non error related info must be hidden. + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsIdle))); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBikesListVisible))); + } + + if (statusInfoText != StatusInfoText) + { + OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); + } + } + } + + /// + /// Bike selected in list of bikes, null if no bike is selected. + /// Binds to GUI. + /// + public BikeViewModelBase SelectedBike { get; set; } + + /// + /// True if bikes list has to be displayed. + /// + public bool IsBikesListVisible + { + get + { + // If an exception occurred there is no information about occupied bikes available. + return Count > 0; + } + } + + /// Used to block more than on copri requests at a given time. + private bool isIdle = false; + + /// + /// True if any action can be performed (request and cancel request) + /// + public virtual bool IsIdle + { + get => isIdle; + set + { + if (value == isIdle) + return; + + Log.ForContext().Debug($"Switch value of {nameof(IsIdle)} to {value}."); + isIdle = value; + base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsIdle))); + base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRunning))); + } + } + + public bool IsRunning => !isIdle; + + /// Holds info about current action. + private string actionText; + + /// Holds info about current action. + public string ActionText + { + get => actionText; + set + { + var statusInfoText = StatusInfoText; + actionText = value; + + if (statusInfoText == StatusInfoText) + { + // Nothing to do because value did not change. + Log.ForContext().Debug($"Property {nameof(ActionText)} set to value \"{actionText}\" but {nameof(StatusInfoText)} did not change."); + return; + } + + Log.ForContext().Debug($"Property {nameof(ActionText)} set to value \"{actionText}\" ."); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); + } + } + + /// Holds information whether app is connected to web or not. + protected bool? isConnected = null; + + /// Exposes the is connected state. + protected bool IsConnected + { + get => isConnected ?? false; + set + { + var statusInfoText = StatusInfoText; + isConnected = value; + if (statusInfoText == StatusInfoText) + { + // Nothing to do. + return; + } + + OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); + } + } + + /// Holds the status information text. + public string StatusInfoText + { + get + { + if (Exception != null) + { + // An error occurred getting data from copri. + return Exception.GetShortErrorInfoText(); + } + + if (!IsConnected) + { + return "Offline."; + } + + return ActionText ?? string.Empty; + } + } + + /// + /// Removes a bike view model by id. + /// + /// Id of bike to removed. + public void Remove(int p_iId) + { + foreach (var bike in BikeCollection) + { + if (bike.Id == p_iId) + { + BikeCollection.Remove(bike); + return; + } + } + } + + /// + /// Gets whether a bike is contained in collection of bikes. + /// + /// Id of bike to check existance. + /// True if bike exists. + private bool Contains(int p_iId) + { + foreach (var l_oBike in Items) + { + if (l_oBike.Id == p_iId) + { + return true; + } + } + + return false; + } + + /// + /// Transforms bikes view model object to string. + /// + /// + public new string ToString() + { + var l_oToString = string.Empty; + foreach (var item in Items) + { + l_oToString += item.ToString(); + } + + return l_oToString; + } + + /// + /// Invoked when page is shown. + /// Starts update process. + /// + /// Update fuction passed as argument by child class. + protected async Task OnAppearing(Action p_oUpdateAction) + { + m_oViewUpdateManager = new PollingUpdateTaskManager(() => GetType().Name, p_oUpdateAction); + + try + { + // Update bikes at station or my bikes depending on context. + await m_oViewUpdateManager.StartUpdateAyncPeridically(m_oPolling); + } + catch (Exception l_oExcetion) + { + Exception = l_oExcetion; + } + } + + /// + /// Invoked when page is shutdown. + /// Currently invoked by code behind, would be nice if called by XAML in future versions. + /// + public async Task OnDisappearing() + { + + await m_oViewUpdateManager.StopUpdatePeridically(); + } + } +} \ No newline at end of file diff --git a/TINKLib/ViewModel/Bikes/IBikesViewModel.cs b/TINKLib/ViewModel/Bikes/IBikesViewModel.cs new file mode 100644 index 0000000..d4f38a9 --- /dev/null +++ b/TINKLib/ViewModel/Bikes/IBikesViewModel.cs @@ -0,0 +1,13 @@ +namespace TINK.ViewModel.Bikes +{ + public interface IBikesViewModel + { + /// Holds info about current action. + string ActionText { get; set; } + + /// + /// True if any action can be performed (request and cancel request) + /// + bool IsIdle { get; set; } + } +} diff --git a/TINKLib/ViewModel/BikesAtStation/BikeAtStationInUseStateInfoProvider.cs b/TINKLib/ViewModel/BikesAtStation/BikeAtStationInUseStateInfoProvider.cs new file mode 100644 index 0000000..378206f --- /dev/null +++ b/TINKLib/ViewModel/BikesAtStation/BikeAtStationInUseStateInfoProvider.cs @@ -0,0 +1,59 @@ +using System; +using TINK.Model.State; +using TINK.MultilingualResources; +using TINK.ViewModel.Bikes.Bike; + +namespace TINK.ViewModel +{ + public class BikeAtStationInUseStateInfoProvider : IInUseStateInfoProvider + { + /// Gets reserved into display text. + /// Log unexpeced states. + /// Display text + public string GetReservedInfo( + TimeSpan? remainingTime, + int? station = null, + string code = null) + { + if (remainingTime == null) + { + // Remaining time not available. + if (string.IsNullOrEmpty(code)) + { + // Reservation code not available + return string.Format(AppResources.StatusTextReservationExpiredMaximumReservationTime, StateRequestedInfo.MaximumReserveTime.Minutes); + } + + return string.Format(AppResources.StatusTextReservationExpiredCodeMaxReservationTime, code, StateRequestedInfo.MaximumReserveTime.Minutes); + } + + if (!string.IsNullOrEmpty(code)) + { + return string.Format(AppResources.StatusTextReservationExpiredCodeRemaining, code, remainingTime.Value.Minutes); + } + + return string.Format(AppResources.StatusTextReservationExpiredRemaining, remainingTime.Value.Minutes); + } + + /// Gets booked into display text. + /// Log unexpeced states. + /// Display text + public string GetBookedInfo( + DateTime? from, + int? station = null, + string code = null) + { + if (from == null) + { + return AppResources.StatusTextBooked; + } + + if (!string.IsNullOrEmpty(code)) + { + return string.Format(AppResources.StatusTextBookedCodeSince, code, from.Value.ToString(BikeViewModelBase.TIMEFORMAT)); + } + + return string.Format(AppResources.StatusTextBookedSince, from.Value.ToString(BikeViewModelBase.TIMEFORMAT)); + } + } +} diff --git a/TINKLib/ViewModel/BikesAtStation/BikesAtStationPageViewModel.cs b/TINKLib/ViewModel/BikesAtStation/BikesAtStationPageViewModel.cs new file mode 100644 index 0000000..a040bf1 --- /dev/null +++ b/TINKLib/ViewModel/BikesAtStation/BikesAtStationPageViewModel.cs @@ -0,0 +1,380 @@ +using TINK.Model.Bike; +using System.Collections.Specialized; +using System.ComponentModel; +using TINK.Model.User; +using System.Threading.Tasks; +using TINK.Model.Connector; +using TINK.Settings; +using System; +using Serilog; +using System.Threading; +using TINK.Model; +using TINK.View; +using Xamarin.Forms; +using System.Linq; +using TINK.Model.Bike.BluetoothLock; +using System.Collections.Generic; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.Geolocation; +using TINK.ViewModel.Bikes; +using TINK.Services.BluetoothLock.Tdo; +using Plugin.Permissions.Abstractions; +using Plugin.BLE.Abstractions.Contracts; +using TINK.MultilingualResources; +using Plugin.Permissions; + +namespace TINK.ViewModel.BikesAtStation +{ + /// + /// Manages one or more bikes which are located at a single station. + /// + public class BikesAtStationPageViewModel : BikesViewModel, INotifyCollectionChanged, INotifyPropertyChanged + { + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + private IViewService m_oViewService; + + /// + /// Holds the Id of the selected station; + /// + private readonly int? m_oStation; + + /// Holds a reference to the external trigger service. + private Action OpenUrlInExternalBrowser { get; } + + /// + /// Constructs bike collection view model. + /// + /// Mail address of active user. + /// Holds object to query location permisions. + /// Holds object to query bluetooth state. + /// Specifies on which platform code is run. + /// Returns if mobile is connected to web or not. + /// Connects system to copri. + /// Service to control lock retrieve info. + /// Holds whether to poll or not and the periode leght is polling is on. + /// All bikes at given station. + /// Action to open an external browser. + /// Executes actions on GUI thread. + /// Interface to actuate methodes on GUI. + public BikesAtStationPageViewModel( + User user, + IPermissions permissions, + IBluetoothLE bluetoothLE, + string runtimPlatform, + int? selectedStation, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + PollingParameters polling, + Action openUrlInExternalBrowser, + Action postAction, + IViewService viewService) : base(user, permissions, bluetoothLE, runtimPlatform, isConnectedDelegate, connectorFactory, geolocation, lockService, polling, postAction, viewService, () => new BikeAtStationInUseStateInfoProvider()) + { + m_oViewService = viewService + ?? throw new ArgumentException("Can not instantiate bikes at station page view model- object. No view available."); + + OpenUrlInExternalBrowser = openUrlInExternalBrowser + ?? throw new ArgumentException("Can not instantiate login page view model- object. No user external browse service available."); + + m_oStation = selectedStation; + + Title = string.Format(m_oStation != null + ? string.Format(AppResources.MarkingBikesAtStationTitle, m_oStation.ToString()) + : string.Empty); + + CollectionChanged += (sender, eventargs) => + { + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesAtStationVisible))); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesAtStationText))); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsLoginRequiredHintVisible))); + }; + } + + /// + /// Name of the station which is displayed as title of the page. + /// + public string Title + { + get; private set; + } + + /// + /// Informs about need to log in before requesting an bike. + /// + public bool IsLoginRequiredHintVisible + { + get + { + return Count > 0 + && !ActiveUser.IsLoggedIn; + } + } + + /// + /// Informs about need to log in before requesting an bike. + /// + public FormattedString LoginRequiredHintText + { + get + { + if (ActiveUser.IsLoggedIn) + { + return string.Empty; + } + + var l_oHint = new FormattedString(); + l_oHint.Spans.Add(new Span { Text = "Bitte Anmelden um Fahrräder zu reservieren! " }); + l_oHint.Spans.Add(new Span { Text = "Hier", ForegroundColor = ViewModelHelper.LINK_COLOR }); + l_oHint.Spans.Add(new Span { Text = " tippen um auf Anmeldeseite zu wechseln."}); + + return l_oHint; + } + } + + /// Returns if info about the fact that user did not request or book any bikes is visible or not. + /// Gets message that logged in user has not booked any bikes. + /// + public bool IsNoBikesAtStationVisible + { + get + { + return Count <= 0 && IsIdle == true; + } + } + + /// Info about the fact that user did not request or book any bikes. + public string NoBikesAtStationText + { + get + { + return IsNoBikesAtStationVisible + ? $"Momentan sind keine Fahrräder an dieser Station verfügbar." + : string.Empty; + } + } + + /// Commang object to bind login button to view model. + public System.Windows.Input.ICommand LoginRequiredHintClickedCommand + { + get + { + return new Xamarin.Forms.Command(() => OpenLoginPage()); + } + } + + /// + /// Opens login page. + /// + public void OpenLoginPage() + { + try + { + // Switch to map page + m_oViewService.ShowPage(ViewTypes.LoginPage); + } + catch (Exception p_oException) + { + Log.Error("Ein unerwarteter Fehler ist auf der Seite Anmelden aufgetreten. Kontext: Klick auf Hinweistext auf Station N- seite ohne Anmeldung. {@Exception}", p_oException); + return; + } + } + + /// + /// Invoked when page is shown. + /// Starts update process. + /// + public async Task OnAppearing() + { + Log.ForContext().Information($"Bikes at station {m_oStation} is appearing, either due to tap on a station or to app being shown again."); + + ActionText = "Einen Moment bitte..."; + + // Stop polling before getting bikes info. + await m_oViewUpdateManager.StopUpdatePeridically(); + + ActionText = AppResources.ActivityTextBikesAtStationGetBikes; + + var bikesAll = await ConnectorFactory(IsConnected).Query.GetBikesAsync(); + + Exception = bikesAll.Exception; // Update communication error from query for bikes at station. + + var bikesAtStation = bikesAll.Response.GetAtStation(m_oStation); + var lockIdList = bikesAtStation + .GetLockIt() + .Cast() + .Select(x => x.LockInfo) + .ToList(); + + Title = string.Format(m_oStation != null + ? m_oStation.ToString() + : string.Empty); + + if (LockService is ILocksServiceFake serviceFake) + { + serviceFake.UpdateSimulation(bikesAtStation); + } + + ActionText = AppResources.ActivityTextSearchBikes; + + // Check location permissions. + if (bikesAtStation.GetLockIt().Count > 0 + && RuntimePlatform == Device.Android) + { + var status = await Permissions.CheckPermissionStatusAsync(); + if (status != PermissionStatus.Granted) + { + var permissionResult = await Permissions.RequestPermissionAsync(); + + if (permissionResult != PermissionStatus.Granted) + { + var dialogResult = await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementLocationPermissionOpenDialog, + AppResources.MessageAnswerYes, + AppResources.MessageAnswerNo); + + if (!dialogResult) + { + // User decided not to give access to locations permissions. + BikeCollection.Update(bikesAtStation); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + return; + } + + // Open permissions dialog. + Permissions.OpenAppSettings(); + } + } + + if (Geolocation.IsGeolcationEnabled == false) + { + await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementLocationActivation, + AppResources.MessageAnswerOk); + + BikeCollection.Update(bikesAtStation); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + return; + } + + // Check if bluetooth is activated. + if (await BluetoothLE.GetBluetoothState() != BluetoothState.On) + { + await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementBluetoothActivation, + AppResources.MessageAnswerOk); + + BikeCollection.Update(bikesAtStation); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + return; + } + } + + // Connect to bluetooth devices. + ActionText = AppResources.ActivityTextSearchBikes; + IEnumerable locksInfoTdo; + try + { + locksInfoTdo = await LockService.GetLocksStateAsync( + lockIdList.Select(x => x.ToLockInfoTdo()).ToList(), + LockService.TimeOut.MultiConnect); + } + catch (Exception exception) + { + Log.ForContext().Error("Getting bluetooth state failed. {Exception}", exception); + locksInfoTdo = new List(); + } + + var locksInfo = lockIdList.UpdateById(locksInfoTdo); + + BikeCollection.Update(bikesAtStation.UpdateLockInfo(locksInfo)); + + // Backup GUI synchronization context. + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + } + + /// Create task which updates my bike view model. + private void UpdateTask() + { + PostAction( + unused => + { + ActionText = "Aktualisiere..."; + IsConnected = IsConnectedDelegate(); + }, + null); + + var result = ConnectorFactory(IsConnected).Query.GetBikesAsync().Result; + + BikeCollection bikes = result.Response.GetAtStation(m_oStation); + + var exception = result.Exception; + if (exception != null) + { + Log.ForContext().Error("Getting all bikes bikes in polling context failed with exception {Exception}.", exception); + } + + PostAction( + unused => + { + BikeCollection.Update(bikes); + Exception = result.Exception; + ActionText = string.Empty; + }, + null); + } + + /// + /// True if any action can be performed (request and cancel request) + /// + public override bool IsIdle + { + get => base.IsIdle; + set + { + if (value == base.IsIdle) + return; + + Log.ForContext().Debug($"Switch value of {nameof(IsIdle)} to {value}."); + base.IsIdle = value; + base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesAtStationVisible))); + base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesAtStationText))); + } + } + + /// Opens login page. + /// Url to open. + private void RegisterRequest(string url) + { + try + { + OpenUrlInExternalBrowser(url); + } + catch (Exception p_oException) + { + Log.Error("Ein unerwarteter Fehler ist auf der Login Seite beim Öffnen eines Browsers, Seite {url}, aufgetreten. {@Exception}", url, p_oException); + return; + } + } + } +} \ No newline at end of file diff --git a/TINKLib/ViewModel/Contact/ContactPageViewModel.cs b/TINKLib/ViewModel/Contact/ContactPageViewModel.cs new file mode 100644 index 0000000..d14ec38 --- /dev/null +++ b/TINKLib/ViewModel/Contact/ContactPageViewModel.cs @@ -0,0 +1,282 @@ +using Plugin.Messaging; +using Serilog; +using System; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.Windows.Input; +using TINK.Model.Services.CopriApi.ServerUris; +using TINK.MultilingualResources; +using TINK.View; +using Xamarin.Essentials; +using Xamarin.Forms; + +namespace TINK.ViewModel.Info +{ + /// View model for contact page. + public class ContactPageViewModel + { + /// Reference on view service to show modal notifications and to perform navigation. + private IViewService ViewService { get; } + + Uri ActiveUri { get; } + + /// Holds a reference to the external trigger service. + private Action OpenUrlInExternalBrowser { get; } + + Func CreateAttachment { get; } + + /// Constructs a contact page view model. + /// Action to open an external browser. + /// View service to notify user. + public ContactPageViewModel( + Uri activeUri, + Func createAttachment, + Action openUrlInExternalBrowser, + IViewService viewService) + { + ActiveUri = activeUri + ?? throw new ArgumentException("Can not instantiate contact page view model- object. No active uri available."); + + CreateAttachment = createAttachment + ?? throw new ArgumentException("Can not instantiate contact page view model- object. No create attachment provider available."); + + ViewService = viewService + ?? throw new ArgumentException("Can not instantiate contact page view model- object. No user view service available."); + + OpenUrlInExternalBrowser = openUrlInExternalBrowser + ?? throw new ArgumentException("Can not instantiate contact page view model- object. No user external browse service available."); + } + + /// Command object to bind login button to view model. + public ICommand OnMailRequest + => new Command( + async () => await DoSendMail(), + () => IsSendMailAvailable); + + + /// True if sending mail is possible. + public bool IsSendMailAvailable => + CrossMessaging.Current.EmailMessenger.CanSendEmail; + + + /// cTrue if doing a phone call is possible. + public bool IsDoPhoncallAvailable + => CrossMessaging.Current.PhoneDialer.CanMakePhoneCall; + + /// Holds the mail address to mail to. + public string MailAddressText + { + get + { + switch (ActiveUri.AbsoluteUri) + { + case CopriServerUriList.TINK_DEVEL: + case CopriServerUriList.TINK_LIVE: + return "tink@fahrradspezialitaeten.com"; + + case CopriServerUriList.SHAREE_DEVEL: + case CopriServerUriList.SHAREE_LIVE: + return "hotline@sharee.bike"; + + default: + return "post@sharee.bike"; + } + } + } + + /// Holds the mail address to send mail to. + public string PhoneNumberText + { + get + { + switch (ActiveUri.AbsoluteUri) + { + case CopriServerUriList.TINK_DEVEL: + case CopriServerUriList.TINK_LIVE: + return "+49 7531 - 3694389"; + + case CopriServerUriList.SHAREE_DEVEL: + case CopriServerUriList.SHAREE_LIVE: + return "+49 761 - 45370097"; + + default: + return "+49 761 5158912"; + } + } + } + + /// Request to do a phone call. + public async Task DoSendMail() + { + try + { + if (!IsSendMailAvailable) + { + // Nothing to do because email can not be sent. + return; + } + + var tinkMail = await ViewService.DisplayAlert( + AppResources.QuestionTitle, + string.Format(AppResources.QuestionSupportmailSubject, GetAppName(ActiveUri)), + string.Format(AppResources.QuestionSupportmailAnswerOperator, GetAppName(ActiveUri)), + string.Format(AppResources.QuestionSupportmailAnswerApp, GetAppName(ActiveUri))); + + if (tinkMail) + { + // Send TINK- related support mail. + await Email.ComposeAsync(new EmailMessage { To = new List { MailAddressText }, Subject = $"{GetAppName(ActiveUri)} Anfrage" }); + return; + } + + // Send a tink app related mail + var appendFile = false; + appendFile = await ViewService.DisplayAlert( + AppResources.QuestionTitle, + AppResources.QuestionSupportmailAttachment, + AppResources.QuestionAnswerYes, + AppResources.QuestionAnswerNo); + + var message = new EmailMessage { To = new List { MailAddressText }, Subject = $"{GetAppName(ActiveUri)}-App Anfrage" }; + + if (appendFile == false) + { + // Send a tink app related mail + await Email.ComposeAsync(message); + return; + } + + var logFileName = string.Empty; + try + { + logFileName = CreateAttachment(); + } + catch (Exception l_oException) + { + await ViewService.DisplayAdvancedAlert( + AppResources.MessageWaring, + AppResources.ErrorSupportmailCreateAttachment, + l_oException.Message, + AppResources.MessageAnswerOk); + Log.Error("An error occurred creating attachment. {@l_oException)", l_oException); + } + + if (!string.IsNullOrEmpty(logFileName)) + { + message.Attachments.Add(new Xamarin.Essentials.EmailAttachment(logFileName)); + } + + // Send a tink app related mail + await Email.ComposeAsync(message); + } + catch (Exception p_oException) + { + Log.Error("An unexpected error occurred sending mail. {@Exception}", p_oException); + await ViewService.DisplayAdvancedAlert( + AppResources.MessageWaring, + AppResources.ErrorSupportmailMailingFailed, + p_oException.Message, + AppResources.MessageAnswerOk); + return; + } + } + + /// Command object to bind login button to view model. + public ICommand OnPhoneRequest + => new Command( + async () => await DoPhoneCall(), + () => IsDoPhoncallAvailable); + + /// Request to do a phone call. + public async Task DoPhoneCall() + { + try + { + // Make Phone Call + if (IsDoPhoncallAvailable) + { + CrossMessaging.Current.PhoneDialer.MakePhoneCall(PhoneNumberText); + } + } + catch (Exception p_oException) + { + Log.Error("An unexpected error occurred doing a phone call. {@Exception}", p_oException); + await ViewService.DisplayAdvancedAlert( + AppResources.MessageWaring, + AppResources.ErrorSupportmailPhoningFailed, + p_oException.Message, + AppResources.MessageAnswerOk); + return; + } + } + + /// Text providing mail address and possilbe reasons to contact. + public FormattedString MaliAddressAndMotivationsText + { + get + { + var hint = new FormattedString(); + hint.Spans.Add(new Span { Text = AppResources.MessageContactMail }); + return hint; + } + } + + /// Invitation to rate app. + public FormattedString LikeTinkApp + { + get + { + var l_oHint = new FormattedString(); + l_oHint.Spans.Add(new Span { Text = string.Format(AppResources.MessageRateMail, GetAppName(ActiveUri)) }); + return l_oHint; + } + } + + /// User clicks rate button. + public ICommand OnRateRequest + => new Command(() => RegisterRequest()); + + /// Opens login page. + public void RegisterRequest() + { + try + { + OpenUrlInExternalBrowser(); + } + catch (Exception p_oException) + { + Log.Error("Ein unerwarteter Fehler ist auf der Login Seite beim Öffnen eines Browsers, Seite {url}, aufgetreten. {@Exception}", p_oException); + return; + } + } + + /// Invitation to rate app. + public FormattedString PhoneContactText + { + get + { + var l_oHint = new FormattedString(); + l_oHint.Spans.Add(new Span { Text = string.Format(AppResources.MessagePhoneMail, GetAppName(ActiveUri)) }); + return l_oHint; + } + } + + /// Gets the application name. + public static string GetAppName(Uri activeUri) + { + switch (activeUri.AbsoluteUri) + { + case CopriServerUriList.TINK_DEVEL: + case CopriServerUriList.TINK_LIVE: + return "TINK"; + + case CopriServerUriList.SHAREE_DEVEL: + case CopriServerUriList.SHAREE_LIVE: + return "sharee.bike"; + + default: + return "Teilrad"; + } + } + } +} diff --git a/TINKLib/ViewModel/CopriWebView/ManageAccountViewModel.cs b/TINKLib/ViewModel/CopriWebView/ManageAccountViewModel.cs new file mode 100644 index 0000000..7cdcca3 --- /dev/null +++ b/TINKLib/ViewModel/CopriWebView/ManageAccountViewModel.cs @@ -0,0 +1,30 @@ + +namespace TINK.ViewModel.Login +{ + /// Manages the copri web view when user is logged in. + public class ManageAccountViewModel + { + /// Holds the auth cookie of the user logged in. + private string AuthCookie { get; } + + /// Holds the merchant id. + private string MerchantId { get; } + + /// Holds the name of the host. + private string HostName { get; } + + public ManageAccountViewModel( + string authCookie, + string merchantId, + string hostName) + { + AuthCookie = authCookie; + MerchantId = merchantId; + HostName = hostName; + } + + /// Get Uri of web view managing user account. + public string Uri => + $"https://{HostName}?sessionid={AuthCookie}{MerchantId}"; + } +} diff --git a/TINKLib/ViewModel/CopriWebView/PasswordForgottonViewModel.cs b/TINKLib/ViewModel/CopriWebView/PasswordForgottonViewModel.cs new file mode 100644 index 0000000..62b3c57 --- /dev/null +++ b/TINKLib/ViewModel/CopriWebView/PasswordForgottonViewModel.cs @@ -0,0 +1,26 @@ +using TINK.Services.CopriApi.ServerUris; + +namespace TINK.ViewModel.CopriWebView +{ + /// Manages the copri web view for password forgotton use case. + public class PasswordForgottonViewModel + { + /// Holds the merchant id. + private string MerchantId { get; } + + /// Holds the name of the host. + private string HostName { get; } + + public PasswordForgottonViewModel( + string merchantId, + string hostName) + { + MerchantId = merchantId; + HostName = hostName; + } + + /// Get Uri of web view providing password forgotton functionality. + public string Uri => + $"https://{HostName}/{HostName.GetAppFolderName()}/Account?sessionid={MerchantId}"; + } +} diff --git a/TINKLib/ViewModel/CopriWebView/RegisterPageViewModel.cs b/TINKLib/ViewModel/CopriWebView/RegisterPageViewModel.cs new file mode 100644 index 0000000..083de14 --- /dev/null +++ b/TINKLib/ViewModel/CopriWebView/RegisterPageViewModel.cs @@ -0,0 +1,19 @@ +using TINK.Services.CopriApi.ServerUris; + +namespace TINK.ViewModel.CopriWebView +{ + public class RegisterPageViewModel + { + /// Holds the name of the host. + private string HostName { get; } + + public RegisterPageViewModel( + string hostName) + { + HostName = hostName; + } /// Get Uri of web view for creating account. + public string Uri => + $"https://{HostName}/{HostName.GetAppFolderName()}/Account/1.%20Kundendaten"; + + } +} diff --git a/TINKLib/ViewModel/IInUseStateInfoProvider.cs b/TINKLib/ViewModel/IInUseStateInfoProvider.cs new file mode 100644 index 0000000..2bf213e --- /dev/null +++ b/TINKLib/ViewModel/IInUseStateInfoProvider.cs @@ -0,0 +1,25 @@ +using System; + +namespace TINK.ViewModel +{ + public interface IInUseStateInfoProvider + { + /// + /// Gets reserved info display text. + /// + /// Display text + string GetReservedInfo( + TimeSpan? p_oRemainingTime, + int? p_strStation = null, + string p_strCode = null); + + /// + /// Gets booked info display text. + /// + /// Display text + string GetBookedInfo( + DateTime? p_oFrom, + int? p_strStation = null, + string p_strCode = null); + } +} diff --git a/TINKLib/ViewModel/IPollingUpdateTaskManager.cs b/TINKLib/ViewModel/IPollingUpdateTaskManager.cs new file mode 100644 index 0000000..26b8d44 --- /dev/null +++ b/TINKLib/ViewModel/IPollingUpdateTaskManager.cs @@ -0,0 +1,20 @@ +using TINK.Settings; +using System.Threading.Tasks; + +namespace TINK.ViewModel +{ + public interface IPollingUpdateTaskManager + { + /// + /// Invoked when page is shown. + /// Actuates and awaits the first update process and starts a task wich actuate the subseqent update tasks. + /// + Task StartUpdateAyncPeridically(PollingParameters p_oPolling = null); + + /// + /// Invoked when pages is closed/ hidden. + /// Stops update process. + /// + Task StopUpdatePeridically(); + } +} diff --git a/TINKLib/ViewModel/IdlePollingUpdateTaskManager.cs b/TINKLib/ViewModel/IdlePollingUpdateTaskManager.cs new file mode 100644 index 0000000..ccac154 --- /dev/null +++ b/TINKLib/ViewModel/IdlePollingUpdateTaskManager.cs @@ -0,0 +1,20 @@ +using TINK.Settings; +using System.Threading.Tasks; + +namespace TINK.ViewModel +{ + /// Polling update task manager to handle calls if no update has to be performed. + /// Object MapPage calls OnDisappearing for some reasons after first start after installation before OnAppearing. This requires a IdlePollingUpdateManager to exist. + public class IdlePollingUpdateTaskManager : IPollingUpdateTaskManager + { + public async Task StartUpdateAyncPeridically(PollingParameters p_oPeriode) + { + await Task.CompletedTask; + } + + public async Task StopUpdatePeridically() + { + await Task.CompletedTask; + } + } +} diff --git a/TINKLib/ViewModel/Info/BikeInfo/BikeInfoCarouselViewModel.cs b/TINKLib/ViewModel/Info/BikeInfo/BikeInfoCarouselViewModel.cs new file mode 100644 index 0000000..4308a8f --- /dev/null +++ b/TINKLib/ViewModel/Info/BikeInfo/BikeInfoCarouselViewModel.cs @@ -0,0 +1,155 @@ +using System; +using System.Collections.Generic; +using TINK.View; +using Xamarin.Forms; + +namespace TINK.ViewModel.Info.BikeInfo +{ + public class BikeInfoViewModel + { + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + private readonly IViewService m_oViewService; + + + /// Interface to actuate methodes on GUI. + public BikeInfoViewModel(Func imageSourceFunc, IViewService p_oViewService) + { + m_oViewService = p_oViewService + ?? throw new ArgumentException("Can not instantiate bike info page view model- object. No view available."); + + CarouselItems = new List(); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Anleitung Benutzung Lastenräder", + $"Diese Anleitung wird einmalig nach der Anmeldung angezeigt.\r\nZum Blättern auf die nächste Seite bitte nach links wischen.\r\nNach Anzeige aller Seiten kann die Anleitung geschlossen werden.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction())); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Zweirädriges TINK Rad: Hochklappen des Fahrradständers (1/3)", + "So abgestellt hat das zweirädrige Transportrad einen sicheren Stand.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("trike_stand1_image.4HJ5PY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Zweirädriges TINK Rad: Hochklappen des Fahrradständers (2/3)", + "Zum Weiterfahren das Transportrad nach vorne bewegen, bis kein Gewicht mehr auf dem Fahrradständer liegt.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("trike_stand2_image.RIX2PY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Zweirädriges TINK Rad: Hochklappen des Fahrradständers (3/3).", + "Den Fahrradständer mit dem Fuß nach oben drücken, bis er hörbar am Magneten (Pfeil) einrastet. So fällt er unterwegs nicht herunter.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("trike_stand3_image.FDR7PY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Dreirädriges TINK Rad: Lösen und Aktivieren der Feststellbremse (1/3).", + "Die Feststellbremse ist der graue Stift, der aus der Bremse herausragt. Ist sie aktiv, kann das Dreirad nicht wegrollen.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("trike_brake1_image.HZ17PY_678_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Dreirädriges TINK Rad: Lösen und Aktivieren der Feststellbremse (2/3).", + "Lösen der Feststellbremse: Die Bremse vollständig anziehen, bis der Stift wieder auf seine ursprüngliche Position herausspringt.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("trike_brake2_image.1YBAQY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Dreirädriges TINK Rad: Lösen und Aktivieren der Feststellbremse (3/3).", + "Aktivieren der Feststellbremse: Die Bremse vollständig anziehen und den Stift hineindrücken.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("trike_brake3_image.FJM2PY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Höhenregulierung des Sattels (1/3).", + "Hier im Bild ist der Hebel zum Einstellen des Sattels zu sehen.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("seat1_image.ZQ65PY_680_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Höhenregulierung des Sattels (2/3).", + "Durch Drücken des Hebels ist die Sattelhöhe frei verstellbar. Vergleichbar mit einem Bürostuhl bewegt sich der Sattel automatisch nach oben.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("seat2_image.QQZCQY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Höhenregulierung des Sattels (3/3).", + "Durch kräftiges Herunterdrücken des Sattels (und gleichzeitigem Betätigen des Hebels) kann der Sattel nach unten verstellt werden. Tipp: Eventuell draufsetzen und dann den Hebel betätigen, um den Sattel nach unten zu drücken.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("seat3_image.NQ5FQY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Verbinden des Kindergurts (1/3).", + "Der Gurt besteht aus drei Einzelteilen. Zunächst die oberen beiden Einzelstücke nehmen.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("belt1_image.4XWCQY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Verbinden des Kindergurts (2/3).", + "Die beiden Einzelstücke zusammenfügen.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("belt2_image.X3F1PY_679_382.png"))); + + CarouselItems.Add( + new CourouselPageItemViewModel( + "Verbinden des Kindergurts (3/3).", + "Das obere und untere Teilstück verbinden (bis zum Einrasten). Lösen der Teilstücke durch Drücken auf den roten Knopf.", + CarouselItems.Count + 1, + () => CarouselItems.Count, + () => CloseAction(), + imageSourceFunc("belt3_image.DYOXPY_679_382.png"))); + } + + /// Gets the carousel page items + public IList CarouselItems { get; } + + /// + /// Commang object to bind login button to view model. + /// + private Action CloseAction + { + get + { + return () => m_oViewService.ShowPage(ViewTypes.MapPage); + } + } + } +} diff --git a/TINKLib/ViewModel/Info/BikeInfo/BikeInfoCarouseltemViewModel.cs b/TINKLib/ViewModel/Info/BikeInfo/BikeInfoCarouseltemViewModel.cs new file mode 100644 index 0000000..d1aad60 --- /dev/null +++ b/TINKLib/ViewModel/Info/BikeInfo/BikeInfoCarouseltemViewModel.cs @@ -0,0 +1,81 @@ +using System; +using System.Windows.Input; +using Xamarin.Forms; + +namespace TINK.ViewModel.Info.BikeInfo +{ + public class CourouselPageItemViewModel + { + public CourouselPageItemViewModel( + string p_strTitle, + string p_strLegend, + int p_iCurrentPageIndex, + Func p_iPagesCountProvider, + Action p_oCloseAction, + ImageSource image = null) + { + Title = p_strTitle; + IsImageVisble = image != null; + if (IsImageVisble) + { + Image = image; + } + + DescriptionText = p_strLegend; + CurrentPageIndex = p_iCurrentPageIndex; + PagesCountProvider = p_iPagesCountProvider; + CloseAction = p_oCloseAction; + } + + /// Gets the title of the navigation page. + public string Title { get; } + + public bool IsImageVisble { get; } + + /// Gets the image. + public ImageSource Image { get; } + + /// Gets the text which describes the image. + public string DescriptionText { get; } + + /// Get the progress of the carousselling progress. + public double ProgressValue + { + get + { + var l_oCount = PagesCountProvider(); + + return l_oCount > 0 ? (double)CurrentPageIndex / l_oCount : 0; + } + } + + /// Gets if user can leave carousel page. + public bool IsCloseVisible + { + get + { + return PagesCountProvider() == CurrentPageIndex; + } + } + + /// + /// Commang object to bind login button to view model. + /// + public ICommand OnCloseRequest + { + get + { + return new Command(() => CloseAction()); + } + } + + /// Returns one based index of the current page. + private int CurrentPageIndex { get; } + + /// Gets the count of carousel pages. + private Func PagesCountProvider { get; } + + /// Action to actuate when close is invoked. + private Action CloseAction { get; } + } +} diff --git a/TINKLib/ViewModel/Info/InfoViewModel.cs b/TINKLib/ViewModel/Info/InfoViewModel.cs new file mode 100644 index 0000000..3355fef --- /dev/null +++ b/TINKLib/ViewModel/Info/InfoViewModel.cs @@ -0,0 +1,131 @@ +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using TINK.Model.Device; +using TINK.Services.CopriApi.ServerUris; +using Xamarin.Essentials; +using Xamarin.Forms; + +namespace TINK.ViewModel.Info +{ + /// Manges the tabbed info page. + public class InfoViewModel : INotifyPropertyChanged + { + /// Fired whenever a property changed. + public event PropertyChangedEventHandler PropertyChanged; + + /// Holds the name of the host. + private string HostName { get; } + + /// Holds value wether site caching is on or off. + bool IsSiteCachingOn { get; } + + /// Constructs Info view model + /// Holds value wether site caching is on or off. + /// Delegate to get an an embedded html ressource. Used as fallback if download from web page does not work and cache is empty. + public InfoViewModel( + string hostName, + bool isSiteCachingOn, + Func resourceProvider) + { + HostName = hostName; + + IsSiteCachingOn = isSiteCachingOn; + + InfoAgb = new HtmlWebViewSource { Html = "Loading..." }; + + ResourceProvider = resourceProvider + ?? throw new ArgumentException($"Can not instantiate {typeof(InfoViewModel)}-object. No ressource provider availalbe."); + } + + /// Called when page is shown. + public async void OnAppearing() + { + InfoAgb = await GetAgb(HostName, IsSiteCachingOn, ResourceProvider); + + InfoPrivacy = new HtmlWebViewSource + { + Html = await ViewModelHelper.GetSource( + $"https://{HostName}/{HostName.GetPrivacyResource()}", + IsSiteCachingOn, + HostName.GetIsCopri() ? () => ResourceProvider("HtmlResouces.V02.InfoDatenschutz.html") : (Func) null) /* offline resources only available for TINK */, + }; + + InfoImpressum = new HtmlWebViewSource + { + Html = HostName.GetIsCopri() + ? ResourceProvider("HtmlResouces.V02.InfoImpressum.html") + : await ViewModelHelper.GetSource($"https://{HostName}/{CopriHelper.SHAREE_SILTEFOLDERNAME}/impress.html", IsSiteCachingOn) + }; + } + + /// Gets the AGBs + /// + /// AGBs + public static async Task GetAgb( + string hostName, + bool isSiteCachingOn, + Func resourceProvider) + { + return new HtmlWebViewSource + { + Html = await ViewModelHelper.GetSource( + $"https://{hostName}/{hostName.GetAGBResource()}", + isSiteCachingOn, + hostName.GetIsCopri() ? () => resourceProvider("HtmlResouces.V02.InfoAGB.html") : (Func) null) /* offline resources only available for TINK */, + }; + } + + /// Gets the platfrom specific prefix. + private Func ResourceProvider { get; set; } + + /// Gets the app related information (app version and licenses). + public HtmlWebViewSource InfoLicenses => new HtmlWebViewSource + { + Html = ResourceProvider("HtmlResouces.V02.InfoLicenses.html") + .Replace("CURRENT_VERSION_TINKAPP", DependencyService.Get().Version.ToString()) + .Replace("ACTIVE_APPNAME", AppInfo.Name) + }; + + /// Privacy text. + private HtmlWebViewSource infoImpress; + /// Gets the privacy related information. + public HtmlWebViewSource InfoImpressum + { + get => infoImpress; + set + { + infoImpress = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoImpressum))); + } + } + + /// Agb information text. + private HtmlWebViewSource infoAgb; + + /// Privacy text. + private HtmlWebViewSource infoPrivacy; + + /// Agb information text. + public HtmlWebViewSource InfoAgb + { + get => infoAgb; + set + { + infoAgb = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoAgb))); + } + } + + /// Agb information text. + public HtmlWebViewSource InfoPrivacy + { + get => infoPrivacy; + set + { + infoPrivacy = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoPrivacy))); + } + } + } +} diff --git a/TINKLib/ViewModel/Login/LoginPageViewModel.cs b/TINKLib/ViewModel/Login/LoginPageViewModel.cs new file mode 100644 index 0000000..735664e --- /dev/null +++ b/TINKLib/ViewModel/Login/LoginPageViewModel.cs @@ -0,0 +1,359 @@ +using System.Windows.Input; +using Xamarin.Forms; +using TINK.View; +using TINK.Model.User; +using TINK.Model.User.Account; +using System.ComponentModel; +using System; +using System.Threading.Tasks; +using TINK.Model.Repository.Exception; +using Serilog; +using TINK.ViewModel.Map; +using Plugin.Connectivity; +using TINK.Model; +using System.Linq; +using System.Collections.Generic; +using TINK.MultilingualResources; + +namespace TINK.ViewModel +{ + public class LoginPageViewModel : INotifyPropertyChanged + { + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + private IViewService m_oViewService; + +#if BACKSTYLE + /// Reference to naviagion object to navigate back to map page when login succeeded. + private INavigation m_oNavigation; +#endif + /// Reference on the tink app instance. + private ITinkApp TinkApp { get; } + + /// + /// Holds the mail address candidate being entered by user. + /// + private string m_strMailAddress; + + /// + /// Holds the password candidate being entered by user. + /// + private string m_strPassword; + + private bool m_bMailAndPasswordCandidatesOk = false; + + /// Holds a reference to the external trigger service. + private Action OpenUrlInExternalBrowser { get; } + + /// + /// Event which notifies clients about changed properties. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// + /// + /// + /// Delegate to set new mail address and password to model. + /// Mail address of user if login succeeded. + /// Action to open an external browser. + /// Interface to actuate methodes on GUI. + /// Interface to navigate. + public LoginPageViewModel( + ITinkApp tinkApp, + Action openUrlInExternalBrowser, +#if !BACKSTYLE + IViewService p_oViewService) +#else + IViewService p_oViewService, + INavigation p_oNavigation) +#endif + { + TinkApp = tinkApp + ?? throw new ArgumentException("Can not instantiate map page view model- object. No tink app object available."); + + OpenUrlInExternalBrowser = openUrlInExternalBrowser + ?? throw new ArgumentException("Can not instantiate login page view model- object. No user external browse service available."); + + m_oViewService = p_oViewService + ?? throw new ArgumentException("Can not instantiate login page view model- object. No view available."); +#if BACKSTYLE + m_oNavigation = p_oNavigation + ?? throw new ArgumentException("Can not instantiate login page view model- object. No navigation service available."); +#endif + + m_strMailAddress = tinkApp.ActiveUser.Mail; + m_strPassword = tinkApp.ActiveUser.Password; + + IsRegisterTargetsInfoVisible = new List { Model.Services.CopriApi.ServerUris.CopriServerUriList.TINK_LIVE, Model.Services.CopriApi.ServerUris.CopriServerUriList.TINK_DEVEL }.Contains(tinkApp.Uris.ActiveUri.AbsoluteUri); + + tinkApp.ActiveUser.StateChanged += OnStateChanged; + } + + /// + /// Login state changed. + /// + /// + /// + private void OnStateChanged(object p_oSender, EventArgs p_oEventArgs) + { + var l_oPropertyChanged = PropertyChanged; + if (l_oPropertyChanged != null) + { + l_oPropertyChanged(this, new PropertyChangedEventArgs(nameof(IsLoggedOut))); + l_oPropertyChanged(this, new PropertyChangedEventArgs(nameof(IsLoginRequestAllowed))); + } + } + + /// Gets a value indicating whether user is logged out or not. + public bool IsLoggedOut { get { return !TinkApp.ActiveUser.IsLoggedIn; } } + + /// Gets a value indicating whether user can try to login. + public bool IsLoginRequestAllowed + { + get + { + return !TinkApp.ActiveUser.IsLoggedIn + && m_bMailAndPasswordCandidatesOk; + } + } + + /// Gets mail address of user. + public string MailAddress + { + get + { + return m_strMailAddress; + } + + set + { + m_strMailAddress = value; + UpdateAndFireIfRequiredOnEnteringMailOrPwd(); + } + } + + /// Gets password user. + public string Password + { + get + { + return m_strPassword; + } + + set + { + m_strPassword = value; + UpdateAndFireIfRequiredOnEnteringMailOrPwd(); + } + } + + /// Update on password or mailaddress set. + private void UpdateAndFireIfRequiredOnEnteringMailOrPwd() + { + var l_bLastMailAndPasswordCandidatesOk = m_bMailAndPasswordCandidatesOk; + + m_bMailAndPasswordCandidatesOk = Validator.ValidateMailAndPasswordDelegate(MailAddress, Password).ValidElement == Elements.Account; + + if (m_bMailAndPasswordCandidatesOk != l_bLastMailAndPasswordCandidatesOk) + { + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsLoginRequestAllowed))); + } + } + + /// + /// Command object to bind login button to view model. + /// + public ICommand OnLoginRequest + { + get + { + return new Command(async () => await TryLogin()); + } + } + + /// Processes request to register a new user. + public ICommand OnRegisterRequest => new Command(async () => + { + if (CrossConnectivity.Current.IsConnected) + { + await m_oViewService.PushAsync(ViewTypes.RegisterPage); + } + else + { + await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageLoginRegisterNoNet, + AppResources.MessageAnswerOk); + } + }); + + /// Processes request to recover password. + public ICommand OnPasswordForgottonRequest => new Command(async () => + { + if (CrossConnectivity.Current.IsConnected) + { + await m_oViewService.PushAsync(ViewTypes.PasswordForgottenPage); + } + else + { + await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageLoginRecoverPassword, + AppResources.MessageAnswerOk); + } + }); + + /// + /// User request to log in. + /// +#if BACKSTYLE + public async void TryLogin() +#else + public async Task TryLogin() +#endif + { + try + { + Log.ForContext().Information("User taped login button."); + + try + { + TinkApp.ActiveUser.CheckIsPasswordValid(MailAddress, Password); + + // Do login. + var l_oAccount = await TinkApp.GetConnector(CrossConnectivity.Current.IsConnected).Command.DoLogin(MailAddress, Password, TinkApp.ActiveUser.DeviceId); + + TinkApp.ActiveUser.Login(l_oAccount); + + // Update map page filter because user might be of both groups TINK an Konrad. + TinkApp.GroupFilterMapPage = + GroupFilterMapPageHelper.CreateUpdated( + TinkApp.GroupFilterMapPage, + TinkApp.ActiveUser.DoFilter(TinkApp.FilterGroupSetting.DoFilter())); + + // Update settings page filter because user might be of both groups TINK an Konrad. + TinkApp.FilterGroupSetting.DoFilter(TinkApp.ActiveUser.Group); + + // Persist new settings. + TinkApp.Save(); + + TinkApp.UpdateConnector(); + } + catch (InvalidAuthorizationResponseException l_oException) + { + // Copri response is invalid. + Log.ForContext().Error("Login failed (invalid. auth. response). {@l_oException}.", l_oException); + + await m_oViewService.DisplayAlert( + AppResources.MessageLoginErrorTitle, + l_oException.Message, + AppResources.MessageAnswerOk); + + return; + } + catch (Exception l_oException) + { + // Copri server is not reachable. + if (l_oException is WebConnectFailureException) + { + Log.ForContext().Information("Login failed (web communication exception). {@l_oException}.", l_oException); + + await m_oViewService.DisplayAlert( + AppResources.MessageLoginConnectionErrorTitle, + string.Format("{0}\r\n{1}", l_oException.Message, WebConnectFailureException.GetHintToPossibleExceptionsReasons), + AppResources.MessageAnswerOk); + } else if (l_oException is UsernamePasswordInvalidException) + { + // Cookie is empty. + Log.ForContext().Error("Login failed (empty cookie). {@l_oException}.", l_oException); + await m_oViewService.DisplayAlert( + AppResources.MessageLoginErrorTitle, + string.Format(AppResources.MessageLoginConnectionErrorMessage, l_oException.Message), + "OK"); + } + else + { + Log.ForContext().Error("Login failed. {@l_oException}.", l_oException); + await m_oViewService.DisplayAlert( + AppResources.MessageLoginErrorTitle, + l_oException.Message, + AppResources.MessageAnswerOk); + } + + return; + } + + // Display information that login succeeded. + Log.ForContext().Information("Login succeeded. {@tinkApp.ActiveUser}.", TinkApp.ActiveUser); + + var title = TinkApp.ActiveUser.Group.Intersect(new List { Model.Connector.FilterHelper.FILTERTINKGENERAL, Model.Connector.FilterHelper.FILTERKONRAD }).Any() + ? string.Format(AppResources.MessageLoginWelcomeTitleGroup, TinkApp.ActiveUser.GetUserGroupDisplayName()) + : string.Format(AppResources.MessageLoginWelcomeTitle); + + await m_oViewService.DisplayAlert( + title, + string.Format(AppResources.MessageLoginWelcome, TinkApp.ActiveUser.Mail), + AppResources.MessageAnswerOk); + } + catch (Exception p_oException) + { + Log.ForContext().Error("An unexpected error occurred displaying log out page. {@Exception}", p_oException); + return; + } + + try + { + if (!TinkApp.ActiveUser.Group.Contains(Model.Connector.FilterHelper.FILTERTINKGENERAL)) + { + // No need to show "Anleitung TINK Räder" because user can not use tink. + m_oViewService.ShowPage(ViewTypes.MapPage); + return; + } + + // Swich to map page + m_oViewService.ShowPage(ViewTypes.BikeInfoCarouselPage, AppResources.MarkingLoginInstructions); + } + catch (Exception p_oException) + { + Log.ForContext().Error("Ein unerwarteter Fehler ist auf der Seite Anleitung TINK Räder (nach Anmeldeseite) aufgetreten. {@Exception}", p_oException); + return; + } + +#if BACKSTYLE + // Navigate back to map page. + await m_oNavigation.PopToRootAsync(); +#endif + } + + /// Holds whether TINK/ Copri info is shown. + public bool IsRegisterTargetsInfoVisible { get; private set; } + + /// Text providing info about TINK/ konrad registration. + public FormattedString RegisterTargetsInfo + { + get + { + var l_oHint = new FormattedString(); + l_oHint.Spans.Add(new Span { Text = AppResources.MarkingLoginInstructionsTinkKonradTitle }); + l_oHint.Spans.Add(new Span { Text = AppResources.MarkingLoginInstructionsTinkKonradMessage }); + + return l_oHint; + } + } + + /// Opens login page. + public void RegisterRequest(string url) + { + try + { + OpenUrlInExternalBrowser(url); + } + catch (Exception p_oException) + { + Log.Error("Ein unerwarteter Fehler ist auf der Login Seite beim Öffnen eines Browsers, Seite {url}, aufgetreten. {@Exception}", url, p_oException); + return; + } + } + } +} diff --git a/TINKLib/ViewModel/Map/EmptyToggleViewModel.cs b/TINKLib/ViewModel/Map/EmptyToggleViewModel.cs new file mode 100644 index 0000000..73b78b0 --- /dev/null +++ b/TINKLib/ViewModel/Map/EmptyToggleViewModel.cs @@ -0,0 +1,29 @@ +using System.Collections.Generic; +using TINK.Model; +using Xamarin.Forms; + +namespace TINK.ViewModel.Map +{ + /// Holds an empty filter object. + /// Old name: EmptyMapPageFilter + public class EmptyToggleViewModel : ITinkKonradToggleViewModel + { + /// Holds the map page filter. + public IGroupFilterMapPage FilterDictionary => new GroupFilterMapPage(); + + /// Active filter + public string CurrentFitler => string.Empty; + + public bool IsTinkEnabled => false; + + public Color TinkColor => Color.Default; + + public bool IsKonradEnabled => false; + + public Color KonradColor => Color.Default; + + public bool IsToggleVisible => false; + + public string CurrentFilter => string.Empty; + } +} diff --git a/TINKLib/ViewModel/Map/GroupFilterMapPage.cs b/TINKLib/ViewModel/Map/GroupFilterMapPage.cs new file mode 100644 index 0000000..5acd44e --- /dev/null +++ b/TINKLib/ViewModel/Map/GroupFilterMapPage.cs @@ -0,0 +1,81 @@ +using System.Collections; +using System.Collections.Generic; +using System.Linq; +using TINK.Model; +using TINK.Model.Connector.Filter; + +namespace TINK.ViewModel.Map +{ + public class GroupFilterMapPage : IGroupFilterMapPage + { + public GroupFilterMapPage(IDictionary filterDictionary = null) + { + FilterDictionary = filterDictionary ?? new Dictionary(); + Filter = filterDictionary != null + ? (IGroupFilter)new IntersectGroupFilter(FilterDictionary.Where(x => x.Value == FilterState.On).Select(x => x.Key)) + : new NullGroupFilter(); + } + + private IGroupFilter Filter { get; } + + /// Gets the active filters. + /// Filter collection to get group from. + /// List of active filters. + /// Rename to ToFilterList + public IList GetGroup() + { + return this.Where(x => x.Value == FilterState.On).Select(x => x.Key).ToList(); + } + /// Performs filtering on response-group. + public IEnumerable DoFilter(IEnumerable filter = null) => Filter.DoFilter(filter); + + private IDictionary FilterDictionary { get; } + + public FilterState this[string key] { get => FilterDictionary[key]; set => FilterDictionary[key] = value ; } + + public ICollection Keys => FilterDictionary.Keys; + + public ICollection Values => FilterDictionary.Values; + + public int Count => FilterDictionary.Count; + + public bool IsReadOnly => true; + + public void Add(string key, FilterState value) + { + throw new System.NotImplementedException(); + } + + public void Add(KeyValuePair item) + { + throw new System.NotImplementedException(); + } + + public void Clear() + { + throw new System.NotImplementedException(); + } + + public bool Contains(KeyValuePair item) => FilterDictionary.Contains(item); + + public bool ContainsKey(string key) => FilterDictionary.ContainsKey(key); + + public void CopyTo(KeyValuePair[] array, int arrayIndex) => FilterDictionary.CopyTo(array, arrayIndex); + + public IEnumerator> GetEnumerator() => FilterDictionary.GetEnumerator(); + + public bool Remove(string key) + { + throw new System.NotImplementedException(); + } + + public bool Remove(KeyValuePair item) + { + throw new System.NotImplementedException(); + } + + public bool TryGetValue(string key, out FilterState value) => FilterDictionary.TryGetValue(key, out value); + + IEnumerator IEnumerable.GetEnumerator() => FilterDictionary.GetEnumerator(); + } +} diff --git a/TINKLib/ViewModel/Map/GroupFilterMapPageHelper.cs b/TINKLib/ViewModel/Map/GroupFilterMapPageHelper.cs new file mode 100644 index 0000000..9fceb21 --- /dev/null +++ b/TINKLib/ViewModel/Map/GroupFilterMapPageHelper.cs @@ -0,0 +1,74 @@ +using System.Collections.Generic; +using System.Linq; +using TINK.Model; + +namespace TINK.ViewModel.Map +{ + /// Helper functionality. + /// Former name: MapPageFilterFactory + public static class GroupFilterMapPageHelper + { + /// Verifies that filters available are always consistent with filter configuration from settings page/ user group. + /// Last filter from map page. Might have to be updated. + /// Filter from settings page/ user group. + public static IGroupFilterMapPage CreateUpdated( + this IGroupFilterMapPage mapPageFilterDictionary, + IEnumerable settingsAndUserFilter) + { + if (settingsAndUserFilter == null) + { + // All filters are null filters no update has to be performed. + return mapPageFilterDictionary; + } + + if (mapPageFilterDictionary == null || mapPageFilterDictionary.Count <= 0) + { + return new GroupFilterMapPage(); + } + + // Filter dictionary by enumeration. + var updatedMapPageFilterDictionary = new Dictionary(mapPageFilterDictionary).Where(x => settingsAndUserFilter.Contains(x.Key)).ToDictionary(x => x.Key, x => x.Value); + + // Get key of activated filter if there is still one. + var activatedFilter = updatedMapPageFilterDictionary.FirstOrDefault(x => x.Value == FilterState.On).Key + ?? string.Empty; + + // Add entries which have become available. + var filterState = FilterState.On; // Set first filter added to on. + var filtersToAdd = settingsAndUserFilter.Except(updatedMapPageFilterDictionary.Select(x => x.Key)); + foreach (var l_oEntry in filtersToAdd) + { + updatedMapPageFilterDictionary.Add(l_oEntry, filterState); + filterState = FilterState.Off; + } + + if (updatedMapPageFilterDictionary.Count <= 0) + { + return new GroupFilterMapPage(updatedMapPageFilterDictionary); + } + + // Ensure that there is at least one element on. + if (updatedMapPageFilterDictionary.Where(x => x.Value == FilterState.On).Count() == 0) + { + // No element is active. Set one element active. + updatedMapPageFilterDictionary[updatedMapPageFilterDictionary.ToArray()[0].Key] = FilterState.On; + return new GroupFilterMapPage(updatedMapPageFilterDictionary); + } + + // Ensure that there is only one selected element. + if (updatedMapPageFilterDictionary.Where(x => x.Value == FilterState.On).Count() > 1) + { + // More than one element is active. Set element inactive. + if (updatedMapPageFilterDictionary.ContainsKey(activatedFilter)) + { + // Turn filter off. + updatedMapPageFilterDictionary[activatedFilter] = FilterState.Off; + } + + return new GroupFilterMapPage(updatedMapPageFilterDictionary); + } + + return new GroupFilterMapPage(updatedMapPageFilterDictionary); + } + } +} diff --git a/TINKLib/ViewModel/Map/IGroupFilterMapPage.cs b/TINKLib/ViewModel/Map/IGroupFilterMapPage.cs new file mode 100644 index 0000000..9b14606 --- /dev/null +++ b/TINKLib/ViewModel/Map/IGroupFilterMapPage.cs @@ -0,0 +1,13 @@ +using System.Collections.Generic; +using TINK.Model; + +namespace TINK.ViewModel.Map +{ + public interface IGroupFilterMapPage : IDictionary + { + /// Performs filtering on response-group. + IEnumerable DoFilter(IEnumerable filter = null); + + IList GetGroup(); + } +} diff --git a/TINKLib/ViewModel/Map/ITinkKonradToggleViewModel.cs b/TINKLib/ViewModel/Map/ITinkKonradToggleViewModel.cs new file mode 100644 index 0000000..85c64a8 --- /dev/null +++ b/TINKLib/ViewModel/Map/ITinkKonradToggleViewModel.cs @@ -0,0 +1,21 @@ +using Xamarin.Forms; + +namespace TINK.ViewModel.Map +{ + public interface ITinkKonradToggleViewModel + { + IGroupFilterMapPage FilterDictionary { get; } + + string CurrentFilter { get; } + + bool IsTinkEnabled { get; } + + Color TinkColor { get; } + + bool IsKonradEnabled { get; } + + Color KonradColor { get; } + + bool IsToggleVisible { get; } + } +} diff --git a/TINKLib/ViewModel/Map/MapPageViewModel.cs b/TINKLib/ViewModel/Map/MapPageViewModel.cs new file mode 100644 index 0000000..d721d25 --- /dev/null +++ b/TINKLib/ViewModel/Map/MapPageViewModel.cs @@ -0,0 +1,914 @@ +using Xamarin.Forms; +using TINK.View; +using TINK.Model.Station; +using System; +using System.Linq; +using TINK.Model.Bike; +using TINK.Model.Repository.Exception; +using TINK.Model; +using Serilog; +using System.Collections.Generic; +using System.Threading.Tasks; +using System.ComponentModel; +using Xamarin.Forms.GoogleMaps; +using System.Collections.ObjectModel; +using TINK.View.MasterDetail; +using TINK.Settings; +using TINK.Model.Connector; +using TINK.Model.Services.CopriApi; +using Plugin.Permissions; +using Xamarin.Essentials; +using Plugin.BLE; +using System.Threading; +using TINK.MultilingualResources; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.CopriApi.ServerUris; +using TINK.ViewModel.Info; + +#if !TRYNOTBACKSTYLE +#endif + +namespace TINK.ViewModel.Map +{ + public class MapPageViewModel : INotifyPropertyChanged + { + /// Holds the count of custom idcons availalbe. + private const int CUSTOM_ICONS_COUNT = 30; + + /// Reference on view servcie to show modal notifications and to perform navigation. + private IViewService m_oViewService; + + /// + /// Holds the exception which occurred getting bikes occupied information. + /// + private Exception m_oException; + + /// Notifies view about changes. + public event PropertyChangedEventHandler PropertyChanged; + + /// Object to manage update of view model objects from Copri. + private IPollingUpdateTaskManager m_oViewUpdateManager; + + /// Holds whether to poll or not and the periode leght is polling is on. + private PollingParameters Polling { get; set; } + + /// Reference on the tink app instance. + private ITinkApp TinkApp { get; } + + /// Delegate to perform navigation. + private INavigation m_oNavigation; + + /// Delegate to perform navigation. + private INavigationMasterDetail m_oNavigationMasterDetail; + + private ObservableCollection pins; + + public ObservableCollection Pins + { + get + { + if (pins == null) + pins = new ObservableCollection(); // If view model is not binding context pins collection must be set programmatically. + + return pins; + } + + set => pins = value; + } + + /// Delegate to move map to region. + private Action m_oMoveToRegionDelegate; + + /// False if user tabed on station marker to show bikes at a given station. + private bool isMapPageEnabled = false; + + /// False if user tabed on station marker to show bikes at a given station. + public bool IsMapPageEnabled { + get => isMapPageEnabled; + private set + { + if (isMapPageEnabled == value) + return; + + isMapPageEnabled = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsMapPageEnabled))); + } + } + + /// Prevents an invalid instane to be created. + /// Reference to tink app model. + /// Delegate to center map and set zoom level. + /// View service to notify user. + /// Interface to navigate. + public MapPageViewModel( + ITinkApp tinkApp, + Action p_oMoveToRegionDelegate, + IViewService p_oViewService, + INavigation p_oNavigation) + { + TinkApp = tinkApp + ?? throw new ArgumentException("Can not instantiate map page view model- object. No tink app object available."); + + m_oMoveToRegionDelegate = p_oMoveToRegionDelegate + ?? throw new ArgumentException("Can not instantiate map page view model- object. No move delegate available."); + + m_oViewService = p_oViewService + ?? throw new ArgumentException("Can not instantiate map page view model- object. No view available."); + + m_oNavigation = p_oNavigation + ?? throw new ArgumentException("Can not instantiate map page view model- object. No navigation service available."); + + m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); + + m_oNavigationMasterDetail = new EmptyNavigationMasterDetail(); + + Polling = PollingParameters.NoPolling; + + tinkKonradToggleViewModel = new EmptyToggleViewModel(); + + IsConnected = TinkApp.GetIsConnected(); + } + + /// Sets the stations filter to to apply (Konrad or TINK). + public IGroupFilterMapPage ActiveFilterMap + { + get => tinkKonradToggleViewModel.FilterDictionary ?? new GroupFilterMapPage(); + set + { + tinkKonradToggleViewModel = new TinkKonradToggleViewModel(value); + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(TinkColor))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(KonradColor))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsToggleVisible))); + } + } + + /// Delegate to perform navigation. + public INavigationMasterDetail NavigationMasterDetail + { + set { m_oNavigationMasterDetail = value; } + } + + public Command PinClickedCommand => new Command( + args => + { + OnStationClicked(int.Parse(args.Pin.Tag.ToString())); + args.Handled = true; // Prevents map to be centered to selected pin. + }); + + /// + /// One time setup: Sets pins into map and connects to events. + /// + private void InitializePins(StationDictionary p_oStations) + { + // Add pins to stations. + Log.ForContext().Debug($"Request to draw {p_oStations.Count} pins."); + foreach (var l_oStation in p_oStations) + { + if (l_oStation.Position == null) + { + // There should be no reason for a position object to be null but this alreay occurred in past. + Log.ForContext().Error("Postion object of station {@l_oStation} is null.", l_oStation); + continue; + } + + var l_oPin = new Pin + { + Position = new Xamarin.Forms.GoogleMaps.Position(l_oStation.Position.Latitude, l_oStation.Position.Longitude), + Label = l_oStation.Id > CUSTOM_ICONS_COUNT + ? l_oStation.GetStationName() + : string.Empty, // Stations with custom icons have already a id marker. No need for a label. + Tag = l_oStation.Id, + IsVisible = false, // Set to false to prevent showing default icons (flickering). + }; + + Pins.Add(l_oPin); + } + } + + /// Update all stations from TINK. + /// List of colors to apply. + private void UpdatePinsColor(IList p_oStationsColorList) + { + Log.ForContext().Debug($"Starting update of stations pins color for {p_oStationsColorList.Count} stations..."); + + // Update colors of pins. + for (int l_iPinIndex = 0; l_iPinIndex < p_oStationsColorList.Count; l_iPinIndex++) + { + var l_iStationId = int.Parse(Pins[l_iPinIndex].Tag.ToString()); + + var indexPartPrefix = l_iStationId <= CUSTOM_ICONS_COUNT + ? $"{l_iStationId}" // there is a station marker with index letter for given station id + : "Open"; // there is no station marker. Use open marker. + + var colorPartPrefix = GetRessourceNameColorPart(p_oStationsColorList[l_iPinIndex]); + + var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? ".png" : string.Empty)}"; + try + { + Pins[l_iPinIndex].Icon = BitmapDescriptorFactory.FromBundle(l_iName); + } + catch (Exception l_oException) + { + Log.ForContext().Error("Station icon {l_strName} can not be loaded. {@l_oException}.", l_oException); + Pins[l_iPinIndex].Label = l_iStationId.ToString(); + Pins[l_iPinIndex].Icon = BitmapDescriptorFactory.DefaultMarker(p_oStationsColorList[l_iPinIndex]); + } + + Pins[l_iPinIndex].IsVisible = true; + } + + var pinsCount = Pins.Count; + for (int pinIndex = p_oStationsColorList.Count; pinIndex < pinsCount; pinIndex++) + { + Log.ForContext().Error($"Unexpected count of pins detected. Expected {p_oStationsColorList.Count} but is {pinsCount}."); + Pins[pinIndex].IsVisible = false; + } + + Log.ForContext().Debug("Update of stations pins color done."); + } + + /// Gets the color related part of the ressrouce name. + /// Color to get name for. + /// Resource name. + private static string GetRessourceNameColorPart(Color p_oColor) + { + if (p_oColor == Color.Blue) + { + return "Blue"; + } + + if (p_oColor == Color.Green) + { + return "Green"; + } + + if (p_oColor == Color.LightBlue) + { + return "LightBlue"; + } + + if (p_oColor == Color.Red) + { + return "Red"; + } + + return p_oColor.ToString(); + } + + /// + /// Invoked when page is shown. + /// Starts update process. + /// + /// Holds map page filter settings. + /// Holds polling management object. + /// If true whats new page will be shown. + public async Task OnAppearing() + { + try + { + IsRunning = true; + // Process map page. + Polling = TinkApp.Polling; + + Log.ForContext().Information( + $"{(Polling != null && Polling.IsActivated ? $"Map page is appearing. Update periode is {Polling.Periode.TotalSeconds} sec." : "Map page is appearing. Polling is off.")}" + + $"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}."); + + // Update map page filter + ActiveFilterMap = TinkApp.GroupFilterMapPage; + + if (Pins.Count <= 0) + { + ActionText = AppResources.ActivityTextMyBikesLoadingBikes; + + // Check location permission + var _permissions = TinkApp.Permissions; + var status = await _permissions.CheckPermissionStatusAsync(); + if (TinkApp.CenterMapToCurrentLocation + && !TinkApp.GeolocationServices.Active.IsSimulation + && status != Plugin.Permissions.Abstractions.PermissionStatus.Granted) + { + var permissionResult = await _permissions.RequestPermissionAsync(); + + if (permissionResult != Plugin.Permissions.Abstractions.PermissionStatus.Granted) + { + var dialogResult = await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageCenterMapLocationPermissionOpenDialog, + AppResources.MessageAnswerYes, + AppResources.MessageAnswerNo); + + if (dialogResult) + { + // User decided to give access to locations permissions. + _permissions.OpenAppSettings(); + ActionText = ""; + IsRunning = false; + IsMapPageEnabled = true; + return; + } + } + } + + // Move and scale before getting stations and bikes which takes some time. + ActionText = AppResources.ActivityTextCenterMap; + Location currentLocation = null; + try + { + currentLocation = TinkApp.CenterMapToCurrentLocation + ? await TinkApp.GeolocationServices.Active.GetAsync() + : null; + } + catch (Exception ex) + { + Log.ForContext().Error("Getting location failed. {Exception}", ex); + } + + MoveAndScale(m_oMoveToRegionDelegate, TinkApp.Uris.ActiveUri, ActiveFilterMap, currentLocation); + } + + ActionText = AppResources.ActivityTextMapLoadingStationsAndBikes; + IsConnected = TinkApp.GetIsConnected(); + var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); + + // Check if there are alreay any pins to the map + // i.e detecte first call of member OnAppearing after construction + if (Pins.Count <= 0) + { + Log.ForContext().Debug($"{(ActiveFilterMap.GetGroup().Any() ? $"Active map filter is {string.Join(",", ActiveFilterMap.GetGroup())}." : "Map filter is off.")}"); + + // Map was not yet initialized. + // Get stations from Copri + Log.ForContext().Verbose("No pins detected on page."); + if (resultStationsAndBikes.Response.StationsAll.CopriVersion >= new Version(4, 1)) + { + await m_oViewService.DisplayAlert( + "Warnung", + string.Format(AppResources.MessageAppVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)), + "OK"); + + Log.ForContext().Error($"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}."); + } + + // Set pins to their positions on map. + InitializePins(resultStationsAndBikes.Response.StationsAll); + + Log.ForContext().Verbose("Update of pins done."); + } + + + if (resultStationsAndBikes.Exception?.GetType() == typeof(AuthcookieNotDefinedException)) + { + Log.ForContext().Error("Map page is shown (probable for the first time after startup of app) and COPRI copri an auth cookie not defined error.{@l_oException}", resultStationsAndBikes.Exception); + + // COPRI reports an auth cookie error. + await m_oViewService.DisplayAlert( + AppResources.MessageWaring, + AppResources.MessageMapPageErrorAuthcookieUndefined, + AppResources.MessageAnswerOk); + + await TinkApp.GetConnector(IsConnected).Command.DoLogout(); + TinkApp.ActiveUser.Logout(); + } + + // Update pin colors. + Log.ForContext().Verbose("Starting update pins color..."); + + var l_oColors = GetStationColors( + Pins.Select(x => x.Tag.ToString()).ToList(), + resultStationsAndBikes.Response.Bikes); + + // Update pins color form count of bikes located at station. + UpdatePinsColor(l_oColors); + + m_oViewUpdateManager = CreateUpdateTask(); + + Log.ForContext().Verbose("Update pins color done."); + + try + { + // Update bikes at station or my bikes depending on context. + await m_oViewUpdateManager.StartUpdateAyncPeridically(Polling); + } + catch (Exception) + { + // Excpetions are handled insde update task; + } + + Exception = resultStationsAndBikes.Exception; + ActionText = ""; + IsRunning = false; + IsMapPageEnabled = true; + } + catch (Exception l_oException) + { + Log.ForContext().Error($"An error occurred switching view TINK/ Konrad.\r\n{l_oException.Message}"); + + IsRunning = false; + + await m_oViewService.DisplayAlert( + "Fehler", + $"Beim Anzeigen der Fahrradstandorte- Seite ist ein Fehler aufgetreten.\r\n{l_oException.Message}", + "OK"); + + IsMapPageEnabled = true; + } + } + + /// Moves map and scales visible region depending on active filter. + public static void MoveAndScale( + Action moveToRegionDelegate, + Uri activeUri, + IGroupFilterMapPage groupFilterMapPage, + Location currentLocation = null) + { + if (currentLocation != null) + { + // Move to current location. + moveToRegionDelegate(MapSpan.FromCenterAndRadius( + new Xamarin.Forms.GoogleMaps.Position(currentLocation.Latitude, currentLocation.Longitude), + Distance.FromKilometers(1.0))); + return; + } + + if (activeUri.AbsoluteUri == CopriServerUriList.SHAREE_LIVE || + activeUri.AbsoluteUri == CopriServerUriList.SHAREE_DEVEL) + { + // Center map to Freiburg + moveToRegionDelegate(MapSpan.FromCenterAndRadius( + new Xamarin.Forms.GoogleMaps.Position(47.995865, 7.815086), + Distance.FromKilometers(2.9))); + return; + } + + // Depending on whether TINK or Conrad is active set center of map and scale. + if (groupFilterMapPage.GetGroup().Contains(FilterHelper.FILTERKONRAD)) + { + // Konrad is activated, + moveToRegionDelegate(MapSpan.FromCenterAndRadius( + new Xamarin.Forms.GoogleMaps.Position(47.680, 9.180), + Distance.FromKilometers(2.9))); + } + else + { + // TINK + moveToRegionDelegate(MapSpan.FromCenterAndRadius( + new Xamarin.Forms.GoogleMaps.Position(47.667, 9.172), + Distance.FromKilometers(0.9))); + } + } + + /// Creates a update task object. + /// Object to use for synchronization. + private PollingUpdateTaskManager CreateUpdateTask() + { + // Start task which periodically updates pins. + return new PollingUpdateTaskManager( + () => GetType().Name, + () => + { + try + { + Log.ForContext().Verbose("Entering update cycle."); + Result resultStationsAndBikes; + + TinkApp.PostAction( + unused => + { + ActionText = "Aktualisiere..."; + IsConnected = TinkApp.GetIsConnected(); + }, + null); + + resultStationsAndBikes = TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync().Result; + + var exception = resultStationsAndBikes.Exception; + if (exception != null) + { + Log.ForContext().Error("Getting bikes and stations in polling context failed with exception {Exception}.", exception); + } + + // Check if there are alreay any pins to the map. + // If no initialze pins. + if (Pins.Count <= 0) + { + // Set pins to their positions on map. + TinkApp.PostAction( + unused => { InitializePins(resultStationsAndBikes.Response.StationsAll); }, + null); + } + + // Set/ update pins colors. + var l_oColors = GetStationColors( + Pins.Select(x => x.Tag.ToString()).ToList(), + resultStationsAndBikes.Response.Bikes); + + // Update pins color form count of bikes located at station. + TinkApp.PostAction( + unused => + { + UpdatePinsColor(l_oColors); + ActionText = string.Empty; + Exception = resultStationsAndBikes.Exception; + }, + null); + + Log.ForContext().Verbose("Leaving update cycle."); + } + catch (Exception exception) + { + Log.ForContext().Error("Getting stations and bikes from update task failed. {Exception}", exception); + TinkApp.PostAction( + unused => + { + Exception = exception; + ActionText = string.Empty; + }, + null); + + Log.ForContext().Verbose("Leaving update cycle."); + return; + } + }); + } + + /// + /// Invoked when pages is closed/ hidden. + /// Stops update process. + /// + public async Task OnDisappearing() + { + Log.Information("Map page is disappearing..."); + + await m_oViewUpdateManager.StopUpdatePeridically(); + } + + /// User clicked on a bike. + /// Id of station user clicked on. + public async void OnStationClicked(int selectedStationId) + { + try + { + Log.ForContext().Information($"User taped station {selectedStationId}."); + + // Lock action to prevent multiple instances of "BikeAtStation" being opened. + IsMapPageEnabled = false; + + TinkApp.SelectedStation = selectedStationId; + +#if TRYNOTBACKSTYLE + m_oNavigation.ShowPage( + typeof(BikesAtStationPage), + p_strStationName); +#else + // Show page. + await m_oViewService.PushAsync(ViewTypes.BikesAtStation); + + IsMapPageEnabled = true; + ActionText = ""; + } + catch (Exception exception) + { + IsMapPageEnabled = true; + ActionText = ""; + + Log.ForContext().Error("Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {Exception}", exception); + await m_oViewService.DisplayAlert( + "Fehler", + $"Fehler beim Öffnen der Ansicht \"Fahrräder an Station\" aufgetreten. {exception.Message}", + "OK"); + } +#endif + } + + /// + /// Gets the list of station color for all stations. + /// + /// Station id list to get color for. + /// + private static IList GetStationColors( + IEnumerable p_oStationsId, + BikeCollection bikesAll) + { + if (p_oStationsId == null) + { + Log.ForContext().Debug("No stations available to update color for."); + return new List(); + } + + if (bikesAll == null) + { + // If object is null an error occurred querrying bikes availalbe or bikes occpied which results in an unknown state. + Log.ForContext().Error("No bikes available to determine pins color."); + return new List(p_oStationsId.Select(x => Color.Blue)); + } + + // Get state for each station. + var l_oColors = new List(); + foreach (var l_strStationId in p_oStationsId) + { + if (int.TryParse(l_strStationId, out int l_iStationId) == false) + { + // Station id is not valid. + Log.ForContext().Error($"A station id {l_strStationId} is invalid (not integer)."); + l_oColors.Add(Color.Blue); + continue; + } + + // Get color of given station. + var l_oBikesAtStation = bikesAll.Where(x => x.CurrentStation == l_iStationId); + if (l_oBikesAtStation.FirstOrDefault(x => x.State.Value != Model.State.InUseStateEnum.Disposable) != null) + { + // There is at least one requested or booked bike + l_oColors.Add(Color.LightBlue); + continue; + } + + if (l_oBikesAtStation.ToList().Count > 0) + { + // There is at least one bike available + l_oColors.Add(Color.Green); + continue; + } + + l_oColors.Add(Color.Red); + } + + return l_oColors; + } + + /// + /// Exception which occurred getting bike information. + /// + public Exception Exception + { + get + { + return m_oException; + } + + private set + { + var statusInfoText = StatusInfoText; + m_oException = value; + if (statusInfoText == StatusInfoText) + { + // Nothing to do because value did not change. + return; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); + } + } + + /// Holds info about current action. + private string actionText; + + /// Holds info about current action. + private string ActionText + { + get => actionText; + set + { + var statusInfoText = StatusInfoText; + actionText = value; + if (statusInfoText == StatusInfoText) + { + // Nothing to do because value did not change. + return; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); + } + } + + /// Used to block more than on copri requests at a given time. + private bool isRunning = false; + + /// + /// True if any action can be performed (request and cancel request) + /// + public bool IsRunning + { + get => isRunning; + set + { + if (value == isRunning) + return; + + Log.ForContext().Debug($"Switch value of {nameof(isRunning)} to {value}."); + isRunning = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsRunning))); + } + } + + + /// Holds information whether app is connected to web or not. + private bool? isConnected = null; + + /// Exposes the is connected state. + private bool IsConnected + { + get => isConnected ?? false; + set + { + var statusInfoText = StatusInfoText; + isConnected = value; + if (statusInfoText == StatusInfoText) + { + // Nothing to do. + return; + } + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(StatusInfoText))); + } + } + + /// Holds the status information text. + public string StatusInfoText + { + get + { + if (Exception != null) + { + // An error occurred getting data from copri. + return Exception.GetShortErrorInfoText(); + } + + if (!IsConnected) + { + return "Offline."; + } + + return ActionText ?? string.Empty; + } + } + + /// Command object to bind login button to view model. + public System.Windows.Input.ICommand OnToggleTinkToKonrad => new Xamarin.Forms.Command(async () => await ToggleTinkToKonrad()); + + /// Command object to bind login button to view model. + public System.Windows.Input.ICommand OnToggleKonradToTink => new Xamarin.Forms.Command(async () => await ToggleKonradToTink()); + + /// Manages toggle functionality. + private ITinkKonradToggleViewModel tinkKonradToggleViewModel; + + /// User request to toggle from TINK to Konrad. + public async Task ToggleTinkToKonrad() + { + if (tinkKonradToggleViewModel.CurrentFilter == FilterHelper.FILTERKONRAD) + { + // Konrad is already activated, nothing to do. + return; + } + + Log.ForContext().Information("User toggles to Konrad."); + await ActivateFilter(FilterHelper.FILTERTINKGENERAL); + } + + /// User request to toggle from TINK to Konrad. + public async Task ToggleKonradToTink() + { + if (tinkKonradToggleViewModel.CurrentFilter == FilterHelper.FILTERTINKGENERAL) + { + // Konrad is already activated, nothing to do. + return; + } + + Log.ForContext().Information("User toggles to TINK."); + + await ActivateFilter(FilterHelper.FILTERKONRAD); + } + + /// User request to toggle from TINK to Konrad. + private async Task ActivateFilter(string p_strSelectedFilter) + { + try + { + Log.ForContext().Information($"Request to toggle to \"{p_strSelectedFilter}\"."); + + // Stop polling. + await m_oViewUpdateManager.StopUpdatePeridically(); + + // Clear error info. + Exception = null; + + // Toggle view + tinkKonradToggleViewModel = new TinkKonradToggleViewModel(ActiveFilterMap).DoToggle(); + + ActiveFilterMap = tinkKonradToggleViewModel.FilterDictionary; + TinkApp.GroupFilterMapPage = ActiveFilterMap; + TinkApp.Save(); + + TinkApp.UpdateConnector(); + + Pins.Clear(); + + // Check location permission + var _permissions = TinkApp.Permissions; + var status = await _permissions.CheckPermissionStatusAsync(); + if (TinkApp.CenterMapToCurrentLocation + && !TinkApp.GeolocationServices.Active.IsSimulation + && status != Plugin.Permissions.Abstractions.PermissionStatus.Granted) + { + var permissionResult = await _permissions.RequestPermissionAsync(); + + if (permissionResult != Plugin.Permissions.Abstractions.PermissionStatus.Granted) + { + var dialogResult = await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementLocationPermission, + "Ja", + "Nein"); + + if (dialogResult) + { + // User decided to give access to locations permissions. + _permissions.OpenAppSettings(); + IsMapPageEnabled = true; + ActionText = ""; + return; + } + } + + // Do not use property .State to get bluetooth state due + // to issue https://hausource.visualstudio.com/TINK/_workitems/edit/116 / + // see https://github.com/xabre/xamarin-bluetooth-le/issues/112#issuecomment-380994887 + if (await CrossBluetoothLE.Current.GetBluetoothState() != Plugin.BLE.Abstractions.Contracts.BluetoothState.On) + { + await m_oViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementBluetoothActivation, + AppResources.MessageAnswerOk); + IsMapPageEnabled = true; + ActionText = ""; + return; + } + } + + // Move and scale before getting stations and bikes which takes some time. + Location currentLocation = null; + try + { + currentLocation = TinkApp.CenterMapToCurrentLocation + ? await TinkApp.GeolocationServices.Active.GetAsync() + : null; + } + catch (Exception ex) + { + Log.ForContext().Error("Getting location failed. {Exception}", ex); + } + + // Update stations + // Depending on whether TINK or Conrad is active set center of map and scale. + MoveAndScale(m_oMoveToRegionDelegate, TinkApp.Uris.ActiveUri, ActiveFilterMap, currentLocation); + + IsConnected = TinkApp.GetIsConnected(); + var resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync(); + + // Set pins to their positions on map. + InitializePins(resultStationsAndBikes.Response.StationsAll); + Log.ForContext().Verbose("Update of pins on toggle done..."); + + // Update pin colors. + Log.ForContext().Verbose("Starting update pins color on toggle..."); + var l_oColors = GetStationColors( + Pins.Select(x => x.Tag.ToString()).ToList(), + resultStationsAndBikes.Response.Bikes); + + // Update pins color form count of bikes located at station. + UpdatePinsColor(l_oColors); + + Log.ForContext().Verbose("Update pins color done."); + + try + { + // Update bikes at station or my bikes depending on context. + await m_oViewUpdateManager.StartUpdateAyncPeridically(Polling); + } + catch (Exception) + { + // Excpetions are handled insde update task; + } + + Log.ForContext().Information($"Toggle to \"{p_strSelectedFilter}\" done."); + } + catch (Exception l_oException) + { + Log.ForContext().Error("An error occurred switching view TINK/ Konrad.{}"); + + await m_oViewService.DisplayAlert( + "Fehler", + $"Beim Umschalten TINK/ Konrad ist ein Fehler aufgetreten.\r\n{l_oException.Message}", + "OK"); + } + } + + public Color TinkColor => tinkKonradToggleViewModel.TinkColor; + + public Color KonradColor => tinkKonradToggleViewModel.KonradColor; + + public bool IsToggleVisible => tinkKonradToggleViewModel.IsToggleVisible; + } +} \ No newline at end of file diff --git a/TINKLib/ViewModel/Map/TinkKonradToggleViewModel.cs b/TINKLib/ViewModel/Map/TinkKonradToggleViewModel.cs new file mode 100644 index 0000000..f5cfe58 --- /dev/null +++ b/TINKLib/ViewModel/Map/TinkKonradToggleViewModel.cs @@ -0,0 +1,81 @@ +using System.Collections.Generic; +using System.Linq; +using TINK.Model; +using TINK.Model.Connector; +using Xamarin.Forms; + +namespace TINK.ViewModel.Map +{ + /// Old name: MapPageFilter. + public class TinkKonradToggleViewModel : ITinkKonradToggleViewModel + { + /// Constructs a map page filter view model object from values. + /// Filters and options dictionary. + public TinkKonradToggleViewModel(IGroupFilterMapPage currentFilter) + { + FilterDictionary = currentFilter ?? new GroupFilterMapPage(); + } + + /// Gets the filter values. + public IGroupFilterMapPage FilterDictionary { get; } + + /// Gets the activated filter. + public string CurrentFilter { + get + { + return FilterDictionary.FirstOrDefault(x => x.Value == FilterState.On).Key ?? string.Empty; + } + } + + /// Gets value whether TINK is enabled or not. + public bool IsTinkEnabled => !string.IsNullOrEmpty(CurrentFilter) && CurrentFilter != FilterHelper.FILTERTINKGENERAL; + + /// Gets color of the TINK button. + public Color TinkColor => CurrentFilter == FilterHelper.FILTERTINKGENERAL ? Color.Blue : Color.Gray; + + /// Gets value whether Konrad is enabled or not. + public bool IsKonradEnabled => !string.IsNullOrEmpty(CurrentFilter) && CurrentFilter != FilterHelper.FILTERKONRAD; + + /// Gets color of the Konrad button. + public Color KonradColor => CurrentFilter == FilterHelper.FILTERKONRAD ? Color.Red : Color.Gray; + + /// Gets whether toggle functionality is visible or not. + public bool IsToggleVisible => + FilterDictionary.ContainsKey(FilterHelper.FILTERKONRAD) + && FilterDictionary.ContainsKey(FilterHelper.FILTERTINKGENERAL) + && (IsTinkEnabled || IsKonradEnabled); + + /// + /// Toggles from bike group TINK to Konrad or vice versa. + /// Toggling means one of + /// - TINK => Konrad or + /// - TINK => Konrad. + /// + /// Filter set to toggle. + /// + public TinkKonradToggleViewModel DoToggle() + { + var l_oCurrentFilterSet = FilterDictionary.ToArray(); + + if (l_oCurrentFilterSet.Length < 2) + { + // There is nothing to toggle because filter set contains only one element. + return new TinkKonradToggleViewModel(FilterDictionary); + } + + var l_oCurrentState = l_oCurrentFilterSet[l_oCurrentFilterSet.Length - 1].Value == FilterState.On + ? FilterState.On + : FilterState.Off; + + var l_oToggledFilterSet = new Dictionary(); + + foreach (var l_oFilterElement in l_oCurrentFilterSet) + { + l_oToggledFilterSet.Add(l_oFilterElement.Key, l_oCurrentState); + l_oCurrentState = l_oFilterElement.Value; + } + + return new TinkKonradToggleViewModel(new GroupFilterMapPage(l_oToggledFilterSet)); + } + } +} diff --git a/TINKLib/ViewModel/MyBikes/MyBikeViewModel.cs b/TINKLib/ViewModel/MyBikes/MyBikeViewModel.cs new file mode 100644 index 0000000..d9adff0 --- /dev/null +++ b/TINKLib/ViewModel/MyBikes/MyBikeViewModel.cs @@ -0,0 +1,95 @@ +using System; +using TINK.Model.State; +using TINK.MultilingualResources; +using TINK.ViewModel.Bikes.Bike; + +namespace TINK.ViewModel +{ + public class MyBikeInUseStateInfoProvider : IInUseStateInfoProvider + { + /// Gets reserved into display text. + /// Log unexpeced states. + /// Display text + public string GetReservedInfo( + TimeSpan? remainingTime, + int? stationId = null, + string code = null) + { + if (remainingTime == null) + { + // Reamining time is not available. + if (stationId == null) + { + + if (string.IsNullOrEmpty(code)) + { + // Code is not avilable + return string.Format(AppResources.StatusTextReservationExpiredMaximumReservationTime, StateRequestedInfo.MaximumReserveTime.Minutes); + } + + return string.Format(AppResources.StatusTextReservationExpiredCodeMaxReservationTime, code, StateRequestedInfo.MaximumReserveTime.Minutes); + } + + if (string.IsNullOrEmpty(code)) + { + return string.Format(AppResources.StatusTextReservationExpiredLocationMaxReservationTime, stationId, StateRequestedInfo.MaximumReserveTime.Minutes); + } + + return string.Format(AppResources.StatusTextReservationExpiredCodeLocationMaxReservationTime, code, stationId, StateRequestedInfo.MaximumReserveTime.Minutes); + } + + if (stationId.HasValue) + { + if (!string.IsNullOrEmpty(code)) + { + return string.Format( + AppResources.StatusTextReservationExpiredCodeLocationReservationTime, + code, + ViewModelHelper.GetStationName(stationId.Value), + remainingTime.Value.Minutes); + } + + return string.Format( + AppResources.StatusTextReservationExpiredLocationReservationTime, + ViewModelHelper.GetStationName(stationId.Value), + remainingTime.Value.Minutes); + } + + return string.Format( + AppResources.StatusTextReservationExpiredRemaining, + remainingTime.Value.Minutes); + } + + /// + /// Gets booked into display text. + /// + /// Log unexpeced states. + /// Display text + public string GetBookedInfo( + DateTime? from, + int? stationId = null, + string code = null) + { + if (from == null) + { + return AppResources.StatusTextBooked; + } + + if (!string.IsNullOrEmpty(code)) + { + if(stationId.HasValue) + { + return string.Format( + AppResources.StatusTextBookedCodeLocationSince, + code, + ViewModelHelper.GetStationName(stationId.Value), + from.Value.ToString(BikeViewModelBase.TIMEFORMAT)); + } + + return string.Format(AppResources.StatusTextBookedCodeSince, code, from.Value.ToString(BikeViewModelBase.TIMEFORMAT)); + } + + return string.Format(AppResources.StatusTextBookedSince, from.Value.ToString(BikeViewModelBase.TIMEFORMAT)); + } + } +} diff --git a/TINKLib/ViewModel/MyBikes/MyBikesPageViewModel.cs b/TINKLib/ViewModel/MyBikes/MyBikesPageViewModel.cs new file mode 100644 index 0000000..37657b9 --- /dev/null +++ b/TINKLib/ViewModel/MyBikes/MyBikesPageViewModel.cs @@ -0,0 +1,258 @@ +using Serilog; +using System; +using System.Collections.Specialized; +using System.ComponentModel; +using System.Threading; +using System.Threading.Tasks; +using TINK.Model.Bike; +using TINK.Model.Connector; +using TINK.Model.User; +using TINK.View; +using TINK.Settings; +using TINK.Model.Bike.BluetoothLock; +using System.Collections.Generic; +using TINK.Services.BluetoothLock; +using TINK.Model.Services.Geolocation; +using System.Linq; +using TINK.Model; +using Xamarin.Forms; +using TINK.ViewModel.Bikes; +using TINK.Services.BluetoothLock.Tdo; +using Plugin.Permissions; +using Plugin.Permissions.Abstractions; +using Plugin.BLE.Abstractions.Contracts; +using TINK.MultilingualResources; + +namespace TINK.ViewModel.MyBikes +{ + public class MyBikesPageViewModel : BikesViewModel, INotifyCollectionChanged, INotifyPropertyChanged + { + /// + /// Constructs bike collection view model in case information about occupied bikes is available. + /// + /// Mail address of active user. + /// Holds object to query location permisions. + /// Holds object to query bluetooth state. + /// Specifies on which platform code is run. + /// Returns if mobile is connected to web or not. + /// Connects system to copri. + /// Service to control lock retrieve info. + /// Holds whether to poll or not and the periode leght is polling is on. + /// Executes actions on GUI thread. + /// Interface to actuate methodes on GUI. + public MyBikesPageViewModel( + User p_oUser, + IPermissions permissions, + IBluetoothLE bluetoothLE, + string runtimPlatform, + Func isConnectedDelegate, + Func connectorFactory, + IGeolocation geolocation, + ILocksService lockService, + PollingParameters p_oPolling, + Action postAction, + IViewService viewService) : base(p_oUser, permissions, bluetoothLE, runtimPlatform, isConnectedDelegate, connectorFactory, geolocation, lockService, p_oPolling, postAction, viewService, () => new MyBikeInUseStateInfoProvider()) + { + CollectionChanged += (sender, eventargs) => + { + OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesOccupiedVisible))); + OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesOccupiedText))); + }; + } + + /// Returns if info about the fact that user did not request or book any bikes is visible or not. + /// Gets message that logged in user has not booked any bikes. + /// + public bool IsNoBikesOccupiedVisible + { + get + { + return Count <= 0 && IsIdle == true; + } + } + + /// Info about the fact that user did not request or book any bikes. + public string NoBikesOccupiedText + { + get + { + return IsNoBikesOccupiedVisible + ? $"Momentan sind keine Fahrräder auf Benutzer {ActiveUser?.Mail} reserviert/ gebucht." + : string.Empty; + } + } + + /// + /// Invoked when page is shown. + /// Starts update process. + /// + public async Task OnAppearing() + { + // Get my bikes from COPRI + Log.ForContext().Information("User request to show page MyBikes/ page re-appearing"); + + ActionText = AppResources.ActivityTextMyBikesLoadingBikes; + + var bikesOccupied = await ConnectorFactory(IsConnected).Query.GetBikesOccupiedAsync(); + + Exception = bikesOccupied.Exception; // Update communication error from query for bikes occupied. + + var lockIdList = bikesOccupied.Response + .GetLockIt() + .Cast() + .Select(x => x.LockInfo) + .ToList(); + + if (LockService is ILocksServiceFake serviceFake) + { + serviceFake.UpdateSimulation(bikesOccupied.Response); + } + + // Check bluetooth and location permission and states + ActionText = AppResources.ActivityTextMyBikesCheckBluetoothState; + + if (bikesOccupied.Response.FirstOrDefault(x => x is BikeInfo btBike) != null + && RuntimePlatform == Device.Android) + { + // Check location permission + var status = await Permissions.CheckPermissionStatusAsync(); + if (status != PermissionStatus.Granted) + { + var permissionResult = await Permissions.RequestPermissionAsync(); + + if (permissionResult != PermissionStatus.Granted) + { + var dialogResult = await ViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementLocationPermissionOpenDialog, + AppResources.MessageAnswerYes, + AppResources.MessageAnswerNo); + + if (!dialogResult) + { + // User decided not to give access to locations permissions. + BikeCollection.Update(bikesOccupied.Response); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + return; + } + + // Open permissions dialog. + Permissions.OpenAppSettings(); + } + } + + // Location state + if (Geolocation.IsGeolcationEnabled == false) + { + await ViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementLocationActivation, + AppResources.MessageAnswerOk); + + BikeCollection.Update(bikesOccupied.Response); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + return; + } + + // Bluetooth state + if (await BluetoothLE.GetBluetoothState() != BluetoothState.On) + { + await ViewService.DisplayAlert( + AppResources.MessageTitleHint, + AppResources.MessageBikesManagementBluetoothActivation, + AppResources.MessageAnswerOk); + + BikeCollection.Update(bikesOccupied.Response); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + return; + } + } + + // Connect to bluetooth devices. + ActionText = AppResources.ActivityTextSearchBikes; + IEnumerable locksInfoTdo; + try + { + locksInfoTdo = await LockService.GetLocksStateAsync( + lockIdList.Select(x => x.ToLockInfoTdo()).ToList(), + LockService.TimeOut.MultiConnect); + } + catch (Exception exception) + { + Log.ForContext().Error("Getting bluetooth state failed. {Exception}", exception); + locksInfoTdo = new List(); + } + + var locksInfo = lockIdList.UpdateById(locksInfoTdo); + + BikeCollection.Update(bikesOccupied.Response.UpdateLockInfo(locksInfo)); + + await OnAppearing(() => UpdateTask()); + + ActionText = ""; + IsIdle = true; + } + + /// + /// True if any action can be performed (request and cancel request) + /// + public override bool IsIdle + { + get => base.IsIdle; + set + { + if (value == base.IsIdle) + return; + + Log.ForContext().Debug($"Switch value of {nameof(IsIdle)} to {value}."); + base.IsIdle = value; + base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesOccupiedVisible))); + base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesOccupiedText))); + } + } + + /// Create task which updates my bike view model. + private void UpdateTask() + { + // Start task which periodically updates pins. + PostAction( + unused => + { + ActionText = "Aktualisiere..."; + IsConnected = IsConnectedDelegate(); + }, + null); + + var result = ConnectorFactory(IsConnected).Query.GetBikesOccupiedAsync().Result; + + var bikes = result.Response; + + var exception = result.Exception; + if (exception != null) + { + Log.ForContext().Error("Getting bikes occupied in polling context failed with exception {Exception}.", exception); + } + + PostAction( + unused => + { + BikeCollection.Update(bikes); // Updating collection leads to update of GUI. + Exception = result.Exception; + ActionText = string.Empty; + }, + null); + } + } +} diff --git a/TINKLib/ViewModel/PollingUpdateTask.cs b/TINKLib/ViewModel/PollingUpdateTask.cs new file mode 100644 index 0000000..10aa360 --- /dev/null +++ b/TINKLib/ViewModel/PollingUpdateTask.cs @@ -0,0 +1,127 @@ +using Serilog; +using System; +using System.Linq; +using System.Threading; +using System.Threading.Tasks; +using TINK.Model.Repository.Exception; +using TINK.Model.Repository.Request; + +namespace TINK.ViewModel +{ + public class PollingUpdateTask + { + /// Name of child class provider. + private readonly Func m_oGetChildName; + + /// Object to control canelling. + private readonly CancellationTokenSource m_oCanellationTokenSource; + + /// Task to perform update. + private readonly Task m_oUpdateTask; + + /// Action which performs an update. + private readonly Action m_oUpdateAction; + + /// Obects which does a periodic update. + /// Name of the child class. For logging purposes. + /// Update action to perform. + /// Holds whether to poll or not and the periode leght is polling is on. + public PollingUpdateTask( + Func p_oGetChildName, + Action p_oUpdateAction, + TimeSpan? p_oPolling) + { + m_oGetChildName = p_oGetChildName + ?? throw new ArgumentException($"Can not construct {GetType().Name}- obect. Argument {nameof(p_oGetChildName)} must not be null."); + + m_oUpdateAction = p_oUpdateAction + ?? throw new ArgumentException($"Can not construct {GetType().Name}- obect. Argument {nameof(p_oUpdateAction)} must not be null."); + + if (!p_oPolling.HasValue) + { + // Automatic update is switched off. + Log.ForContext().Debug($"Automatic update is off, context {GetType().Name} at {DateTime.Now}."); + return; + } + + var l_iUpdatePeriodeSet = p_oPolling.Value; + + m_oCanellationTokenSource = new CancellationTokenSource(); + + int l_iCycleIndex = 2; + m_oUpdateTask = Task.Run( + async () => + { + while (!m_oCanellationTokenSource.IsCancellationRequested) + { + await Task.Delay(l_iUpdatePeriodeSet, m_oCanellationTokenSource.Token); + { + // N. update cycle + Log.ForContext().Information($"Actuating {l_iCycleIndex} update cycle, context {GetType().Name} at {DateTime.Now}."); + + m_oUpdateAction(); + + l_iCycleIndex = l_iCycleIndex < int.MaxValue ? ++l_iCycleIndex : 0; + } + } + }, + m_oCanellationTokenSource.Token); + } + + /// + /// Invoked when pages is closed/ hidden. + /// Stops update process. + /// + public async Task Terminate() + { + // Cancel update task; + if (m_oCanellationTokenSource == null) + { + throw new Exception($"Can not terminate periodical update task, context {GetType().Name} at {DateTime.Now}. No task running."); + } + + Log.Information($"Request to terminate update cycle, context {GetType().Name} at {DateTime.Now}."); + m_oCanellationTokenSource.Cancel(); + + try + { + await m_oUpdateTask; + } + catch (TaskCanceledException) + { + // Polling update was canceled. + // Nothing to notice/ worry about. + } + catch (Exception l_oException) + { + // An error occurred updating pin. + var l_oAggregateException = l_oException as AggregateException; + if (l_oAggregateException == null) + { + // Unexpected exception detected. Exception should alyways be of type AggregateException + Log.Error("An/ several errors occurred on update task. {@Exceptions}.", l_oException); + } + else + { + if (l_oAggregateException.InnerExceptions.Count == 1 + && l_oAggregateException.InnerExceptions[0].GetType() == typeof(TaskCanceledException)) + { + // Polling update was canceled. + // Nothing to notice/ worry about. + } + else if (l_oAggregateException.InnerExceptions.Count() > 0 && + l_oAggregateException.InnerExceptions.First(x => !x.GetIsConnectFailureException()) == null) // There is no exception which is not of type connect failure. + { + // All exceptions were caused by communication error + Log.Information("An/ several web related connect failure exceptions occurred on update task. {@Exceptions}.", l_oException); + } + else + { + // All exceptions were caused by communication errors. + Log.Error("An/ several exceptions occurred on update task. {@Exception}.", l_oException); + } + } + } + } + } +} diff --git a/TINKLib/ViewModel/PollingUpdateTaskManager.cs b/TINKLib/ViewModel/PollingUpdateTaskManager.cs new file mode 100644 index 0000000..95e3c56 --- /dev/null +++ b/TINKLib/ViewModel/PollingUpdateTaskManager.cs @@ -0,0 +1,106 @@ +using Serilog; +using System; +using TINK.Settings; +using System.Threading.Tasks; + +namespace TINK.ViewModel +{ + /// + /// Performs a periodic update. + /// + public class PollingUpdateTaskManager : IPollingUpdateTaskManager + { + /// Name of child class provider. + private readonly Func m_oGetChildName; + + /// Action which performs an update. + private Action m_oUpdateAction; + + /// Reference on the current polling task + private PollingUpdateTask m_oPollingUpdateTask; + + /// + /// Gets or sets polling parameters. + /// + private PollingParameters PollingParameters { get; set; } + + /// Prevents an invalid instance to be created. + private PollingUpdateTaskManager() + { } + + /// Constructs a update manager object. + /// Name of the child class. For logging purposes. + /// + public PollingUpdateTaskManager( + Func p_oGetChildName, + Action p_oUpdateAction) + { + m_oGetChildName = p_oGetChildName ?? throw new ArgumentException( + $"Can not construct {GetType().Name}- obect. Argument {nameof(p_oGetChildName)} must not be null."); + + m_oUpdateAction = p_oUpdateAction ?? throw new ArgumentException( + $"Can not construct {GetType().Name}- obect. Argument {nameof(p_oUpdateAction)} must not be null."); + } + + /// + /// Invoked when page is shown. + /// Actuates and awaits the first update process and starts a task wich actuate the subseqent update tasks. + /// + /// Parametes holding polling periode, if null last parameters are used if available. + public async Task StartUpdateAyncPeridically(PollingParameters pollingParameters = null) + { + if (m_oPollingUpdateTask != null) + { + Log.Error($"Call to stop periodic update task missed in context {GetType().Name} at {DateTime.Now}."); + + // Call of StopUpdatePeriodically missed. + // Terminate running polling update task before starting a new one + await m_oPollingUpdateTask.Terminate(); + } + + if (pollingParameters != null) + { + // Backup polling parameter for purposee of restart. + PollingParameters = pollingParameters; + } + + if (PollingParameters == null) + { + // No polling parameters avaialable. + Log.Error($"Can not start polling. No parameters available."); + return; + } + + + if (!PollingParameters.IsActivated) + { + // Automatic supdate is switched off. + Log.ForContext().Debug($"Automatic update is off, context {GetType().Name} at {DateTime.Now}."); + return; + } + + m_oPollingUpdateTask = new PollingUpdateTask( + m_oGetChildName, + m_oUpdateAction, + PollingParameters.Periode); + } + + /// + /// Invoked when pages is closed/ hidden. + /// Stops update process. + /// + public async Task StopUpdatePeridically() + { + if (m_oPollingUpdateTask == null) + { + // Nothing to do if there is no update task pending. + return; + } + + await m_oPollingUpdateTask.Terminate(); + + m_oPollingUpdateTask = null; + } + } +} + diff --git a/TINKLib/ViewModel/Settings/CopriServerUriListViewModel.cs b/TINKLib/ViewModel/Settings/CopriServerUriListViewModel.cs new file mode 100644 index 0000000..5965005 --- /dev/null +++ b/TINKLib/ViewModel/Settings/CopriServerUriListViewModel.cs @@ -0,0 +1,101 @@ +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; +using TINK.Model.Services.CopriApi.ServerUris; + +namespace TINK.Model.Connector +{ + /// View model managing active uri and sets of uris. + public class CopriServerUriListViewModel : INotifyPropertyChanged + { + /// + /// Object holding active uris. + /// + private CopriServerUriList m_oUris; + + /// + /// Fired whenever a property changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// Maps uris to user fiendly descriptions. + private Dictionary uriToServerText; + + /// Maps user fiendly descriptions to uris. + private Dictionary serverTextToUri; + + public CopriServerUriListViewModel(CopriServerUriList p_oSource) + { + uriToServerText = new Dictionary { + { CopriServerUriList.TINK_DEVEL, "TINK-Konrad-devellopment" }, + { CopriServerUriList.TINK_LIVE, "TINK-Konrad-live" }, + { CopriServerUriList.SHAREE_DEVEL, "Sharee-fr01-devellopment" }, + { CopriServerUriList.SHAREE_LIVE, "Sharee-fr01-live" } + }; + + serverTextToUri = uriToServerText.ToDictionary(x => x.Value, x => x.Key); + + m_oUris = new CopriServerUriList(p_oSource); + NextActiveUri = m_oUris.ActiveUri; + + } + + /// + /// Sets the active uri. + /// + private Uri ActiveUri + { + set + { + if (m_oUris.ActiveUri.AbsoluteUri == value.AbsoluteUri) + { + /// Nothing to do. + return; + } + + m_oUris = new CopriServerUriList(m_oUris.Uris.ToArray(), value); + } + } + + /// Gets the known uris. + public IList ServerTextList + { + get + { + return m_oUris.Uris.Select(x => (uriToServerText.ContainsKey(x.AbsoluteUri) ? uriToServerText[x.AbsoluteUri] : x.AbsoluteUri)).OrderBy(x => x).ToList(); + } + } + + /// Holds the uri which will be applied after restart of app. + public Uri NextActiveUri { get; private set; } + + /// Holds the uri which will be applied after restart. + public string NextActiveServerText + { + get + { + return uriToServerText.ContainsKey(NextActiveUri.AbsoluteUri) ? uriToServerText[NextActiveUri.AbsoluteUri] : NextActiveUri.AbsoluteUri; + } + + set + { + NextActiveUri = new Uri(serverTextToUri.ContainsKey(value) ? serverTextToUri[value] : value); + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(CorpiServerUriDescription))); + } + } + + /// Holds the description of the picker. + public string CorpiServerUriDescription + { + get + { + return m_oUris.ActiveUri.AbsoluteUri == NextActiveUri.AbsoluteUri + ? "Aktiver Copri- Server" + : "Copri- Server.\r\nNeustart erforderlich für Wechsel!"; + } + } + + } +} diff --git a/TINKLib/ViewModel/Settings/FilterItemMutable.cs b/TINKLib/ViewModel/Settings/FilterItemMutable.cs new file mode 100644 index 0000000..112426b --- /dev/null +++ b/TINKLib/ViewModel/Settings/FilterItemMutable.cs @@ -0,0 +1,62 @@ +using TINK.Model; + +namespace TINK.ViewModel.Settings +{ + /// Holds filter item incluting full state (avaialble, activated, name, ...). + public class FilterItemMutable + { + /// Switch value + private bool m_bIsActivatedSwitch; + + /// Constructs a filter object. + /// Key of the filter state. + /// State of filter, on or off. + /// If filter does not apply because user does not belong to group (TINK, Konrad, ...) filter is deactivated. + /// Text of the switch describing the filter. + public FilterItemMutable( + string p_strKey, + FilterState p_oFilterState, + bool p_bIsEnabled, + string p_strLabelText) + { + Text = p_strLabelText; + IsEnabled = p_bIsEnabled; + State = p_oFilterState; + Key = p_strKey; + m_bIsActivatedSwitch = p_bIsEnabled && p_oFilterState == FilterState.On; + } + + /// Text describing the filter. + public string Text { get; } + + /// True if switch can be toggeled. + public bool IsEnabled { get; } + + /// True if switch is on. + public bool IsActivated + { + get + { + return m_bIsActivatedSwitch; + } + + set + { + m_bIsActivatedSwitch = value; + if (!IsEnabled) + { + // Nothing to do if filter does not apply to user account. + return; + } + + State = m_bIsActivatedSwitch ? FilterState.On : FilterState.Off; + } + } + + /// Key of the filter. + public string Key { get; } + + /// State of the filter. + public FilterState State { get; private set; } + } +} diff --git a/TINKLib/ViewModel/Settings/LocksServicesViewModel.cs b/TINKLib/ViewModel/Settings/LocksServicesViewModel.cs new file mode 100644 index 0000000..4675da6 --- /dev/null +++ b/TINKLib/ViewModel/Settings/LocksServicesViewModel.cs @@ -0,0 +1,36 @@ +using System; +using System.ComponentModel; + +namespace TINK.ViewModel.Settings +{ + /// Manages locks services and related parameters. + public class LocksServicesViewModel : INotifyPropertyChanged + { + private TimeSpan ConnectTimeout { get; set; } + + public LocksServicesViewModel( + TimeSpan connectTimeout, + ServicesViewModel servicesViewModel) + { + ConnectTimeout = connectTimeout; + Services = servicesViewModel; + } + + public ServicesViewModel Services { get; } + + public string ConnectTimeoutSecText { get => ConnectTimeout.TotalSeconds.ToString(); } + + public double ConnectTimeoutSec + { + get => ConnectTimeout.TotalSeconds; + + set + { + ConnectTimeout = TimeSpan.FromSeconds(value); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ConnectTimeoutSecText))); + } + } + + public event PropertyChangedEventHandler PropertyChanged; + } +} \ No newline at end of file diff --git a/TINKLib/ViewModel/Settings/PollingViewModel.cs b/TINKLib/ViewModel/Settings/PollingViewModel.cs new file mode 100644 index 0000000..9657452 --- /dev/null +++ b/TINKLib/ViewModel/Settings/PollingViewModel.cs @@ -0,0 +1,110 @@ +using System; +using System.ComponentModel; +using TINK.Settings; + +namespace TINK.ViewModel.Settings +{ + /// Polling relagted parameters + public class PollingViewModel : INotifyPropertyChanged + { + /// Holds the views polling parameters. + private PollingParameters m_oPolling = PollingParameters.Default; + + /// Current polling periode. Used to check whether values were modified or not. + private readonly PollingParameters m_oPollingActive; + + /// Constructs polling object. + /// Object to construct from + public PollingViewModel(PollingParameters p_oSource) + { + m_oPollingActive = p_oSource + ?? throw new ArgumentException("Can not instantiate polling parameters view model- object. Polling parameter object is null."); + + m_oPolling = p_oSource; + } + + /// Gets the immutable version of polling parameters. + /// Polling parameters object. + public PollingParameters ToImmutable() { return m_oPolling; } + + /// Holds value whether polling is activated or not. + public bool IsActivated + { + get + { + return m_oPolling.IsActivated; + } + + set + { + if (value == m_oPolling.IsActivated) + { + // Nothing to do. + return; + } + + m_oPolling = new PollingParameters(m_oPolling.Periode, value); + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(IsActivated))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PeriodeTotalSeconds))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PeriodeTotalSecondsText))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollingText))); + } + } + + /// Gests or sets the polling periode [sec]. + public int PeriodeTotalSeconds + { + get + { + return (int)m_oPolling.Periode.TotalSeconds; + } + + set + { + if (value == (int)m_oPolling.Periode.TotalSeconds) + { + // Nothing to do. + return; + } + + m_oPolling = new PollingParameters(new TimeSpan(0, 0, value), IsActivated); + + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PeriodeTotalSeconds))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PeriodeTotalSecondsText))); + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(PollingText))); + } + } + + /// Gets info about polling periode. + public string PeriodeTotalSecondsText + { + get + { + if (IsActivated) + { + return $"Polling Periode: {PeriodeTotalSeconds} [Sek.]"; + } + + return $"Polling abgeschalten."; + } + } + + /// Gets the text of the polling related controls. + public string PollingText + { + get + { + if (m_oPolling == m_oPollingActive) + { + return "Polling"; + } + + return "Polling\r\nAnsicht verlassen um Änderungen anzuwenden."; + } + } + + /// Notifies GUI about modified values. + public event PropertyChangedEventHandler PropertyChanged; + } +} diff --git a/TINKLib/ViewModel/Settings/ServicesViewModel.cs b/TINKLib/ViewModel/Settings/ServicesViewModel.cs new file mode 100644 index 0000000..7a8d9a4 --- /dev/null +++ b/TINKLib/ViewModel/Settings/ServicesViewModel.cs @@ -0,0 +1,80 @@ +using Serilog; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Linq; + +namespace TINK.ViewModel.Settings +{ + /// ViewModel for an active service plus a list of services which are not active. + /// + /// Example for services are lock services, geolocation services, ..., + /// + public class ServicesViewModel : INotifyPropertyChanged + { + /// Active service. + private string active; + + /// Holds the dictionary which maps services to service display texts. + private IDictionary ServiceToText { get; } + + /// Holds the dictionary which maps service display texts to services. + private IDictionary TextToService { get; } + + /// Constructs view model ensuring consistency. + /// List of available services. + /// Dictionary holding display text for services as values. + /// Active service. + public ServicesViewModel( + IEnumerable services, + IDictionary serviceToText, + string active) + { + if (!services.Contains(active)) + throw new ArgumentException($"Can not instantiate {typeof(ServicesViewModel).Name}- object. Active lock service {active} must be contained in [{String.Join(",", services)}]."); + + ServiceToText = services.Distinct().ToDictionary( + x => x, + x => serviceToText.ContainsKey(x) ? serviceToText[x] : x); + + TextToService = ServiceToText.ToDictionary(x => x.Value, x => x.Key); + Active = active; + } + + /// Fired whenever active service changes. + public event PropertyChangedEventHandler PropertyChanged; + + /// Holds active service. + public string Active + { + get => active; + set + { + if (active == value) + return; + + active = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(Active))); + } + } + + /// List of display texts of services. + public IList ServicesTextList => ServiceToText.Select(x => x.Value).OrderBy(x => x).ToList(); + + /// Active locks service. + public string ActiveText + { + get => ServiceToText[Active]; + set + { + if (!TextToService.ContainsKey(value)) + { + Log.ForContext().Error($"Can not set service {value} to services view model. List of services {{{string.Join(";", TextToService)}}} does not hold machting element."); + throw new ArgumentException($"Can not set service {value} to services view model. List of services {{{string.Join(";", TextToService)}}} does not hold machting element."); + } + + Active = TextToService[value]; + } + } + } +} diff --git a/TINKLib/ViewModel/Settings/SettingsBikeFilterViewModel.cs b/TINKLib/ViewModel/Settings/SettingsBikeFilterViewModel.cs new file mode 100644 index 0000000..1b92a27 --- /dev/null +++ b/TINKLib/ViewModel/Settings/SettingsBikeFilterViewModel.cs @@ -0,0 +1,64 @@ +using System.Collections.Generic; +using System.Collections.ObjectModel; +using System.Linq; +using TINK.Model; +using TINK.Model.Connector; + +namespace TINK.ViewModel.Settings +{ + /// Holds the filters to display.. + /// Former name: FilterCollectionMutable. + public class SettingsBikeFilterViewModel : ObservableCollection + { + /// Constructs a filter collection object. + /// All available filters. + /// Filters which apply to logged in user. + public SettingsBikeFilterViewModel( + IGroupFilterSettings p_oFilterSettings, + IEnumerable p_oFilterGroupUser) + { + foreach (var l_oFilter in p_oFilterSettings) + { + if (l_oFilter.Key == FilterHelper.FILTERTINKGENERAL) + { + Add(new FilterItemMutable( + l_oFilter.Key, + l_oFilter.Value, + p_oFilterGroupUser != null ? p_oFilterGroupUser.Contains(l_oFilter.Key) : true, + "TINK Lastenräder")); + continue; + } + if (l_oFilter.Key == FilterHelper.FILTERKONRAD) + { + Add(new FilterItemMutable( + l_oFilter.Key, + l_oFilter.Value, + p_oFilterGroupUser != null ? p_oFilterGroupUser.Contains(l_oFilter.Key) : true, + "Konrad Stadträder")); + continue; + } + + Add(new FilterItemMutable( + l_oFilter.Key, + l_oFilter.Value, + p_oFilterGroupUser != null ? p_oFilterGroupUser.Contains(l_oFilter.Key) : true, + l_oFilter.Key)); + } + } + + /// Get filter collection which might have been modified for serialization purposes. + public Dictionary FilterCollection + { + get + { + var l_Dictionary = new Dictionary(); + foreach (var l_oEntry in this) + { + l_Dictionary.Add(l_oEntry.Key, l_oEntry.State); + } + + return l_Dictionary; + } + } + } +} diff --git a/TINKLib/ViewModel/Settings/SettingsPageViewModel.cs b/TINKLib/ViewModel/Settings/SettingsPageViewModel.cs new file mode 100644 index 0000000..16b39c9 --- /dev/null +++ b/TINKLib/ViewModel/Settings/SettingsPageViewModel.cs @@ -0,0 +1,384 @@ +using Serilog; +using Serilog.Events; +using System; +using System.Collections.Generic; +using System.ComponentModel; +using System.Threading.Tasks; +using TINK.Model; +using TINK.Model.Connector; +using TINK.Model.Repository.Exception; +using TINK.Model.Services.Geolocation; +using TINK.Settings; +using TINK.View; +using TINK.ViewModel.Map; +using TINK.ViewModel.Settings; +using System.Linq; +using TINK.Model.User.Account; +using TINK.Services.BluetoothLock; +using Xamarin.Forms; + +namespace TINK.ViewModel +{ + /// + /// View model for settings. + /// + public class SettingsPageViewModel : INotifyPropertyChanged + { + /// + /// Reference on view servcie to show modal notifications and to perform navigation. + /// + private IViewService m_oViewService; + + /// + /// Fired if a property changes. + /// + public event PropertyChangedEventHandler PropertyChanged; + + /// Object to manage update of view model objects from Copri. + private IPollingUpdateTaskManager m_oViewUpdateManager; + + /// List of copri server uris. + public CopriServerUriListViewModel CopriServerUriList { get; } + + /// Manages selection of locks services. + public LocksServicesViewModel LocksServices { get; } + + /// Manages selection of geolocation services. + public ServicesViewModel GeolocationServices { get; } + + /// + /// Object to switch logging level. + /// + private LogEventLevel m_oMinimumLogEventLevel; + + /// List of copri server uris. + public ServicesViewModel Themes { get; } + + /// Reference on the tink app instance. + private ITinkApp TinkApp { get; } + + /// Constructs a settings page view model object. + /// Reference to tink app model. + /// + /// + /// Filter to apply on stations and bikes. + /// Available copri server host uris including uri to use for next start. + /// Holds whether to poll or not and the periode leght is polling is on. + /// Default polling periode lenght. + /// Controls logging level. + /// Interface to view + public SettingsPageViewModel( + ITinkApp tinkApp, + IViewService p_oViewService) + { + TinkApp = tinkApp + ?? throw new ArgumentException("Can not instantiate settings page view model- object. No tink app object available."); + + m_oViewService = p_oViewService + ?? throw new ArgumentException("Can not instantiate settings page view model- object. No user view service available."); + + m_oMinimumLogEventLevel = TinkApp.MinimumLogEventLevel; + + CenterMapToCurrentLocation = TinkApp.CenterMapToCurrentLocation; + + ExternalFolder = TinkApp.ExternalFolder; + + IsLogToExternalFolderVisible = !string.IsNullOrEmpty(ExternalFolder); + + LogToExternalFolderDisplayValue = IsLogToExternalFolderVisible ? TinkApp.LogToExternalFolder : false; + + IsSiteCachingOnDisplayValue = TinkApp.IsSiteCachingOn; + + if (TinkApp.Uris == null + || TinkApp.Uris.Uris.Count <= 0) + { + throw new ArgumentException("Can not instantiate settings page view model- object. No uri- list available."); + } + + if (string.IsNullOrEmpty(TinkApp.NextActiveUri.AbsoluteUri)) + { + throw new ArgumentException("Can not instantiate settings page view model- object. Next active uri must not be null or empty."); + } + + GroupFilter = new SettingsBikeFilterViewModel( + TinkApp.FilterGroupSetting, + TinkApp.ActiveUser.IsLoggedIn ? TinkApp.ActiveUser.Group : null); + + m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); + + Polling = new PollingViewModel(TinkApp.Polling); + + ExpiresAfterTotalSeconds = Convert.ToInt32(TinkApp.ExpiresAfter.TotalSeconds); + + CopriServerUriList = new CopriServerUriListViewModel(TinkApp.Uris); + + Themes = new ServicesViewModel( + TinkApp.Themes.Select(x => x.GetType().FullName), + new Dictionary { + { typeof(Themes.Konrad).FullName, "Konrad" }, + { typeof(Themes.ShareeBike).FullName, "sharee.bike" } + }, + TinkApp.Themes.Active.GetType().FullName); + + Themes.PropertyChanged += OnThemesChanged; + + + LocksServices = new LocksServicesViewModel( + TinkApp.LocksServices.Active.TimeOut.MultiConnect, + new ServicesViewModel( + TinkApp.LocksServices, + new Dictionary { + { typeof(LocksServiceInReach).FullName, "Simulation - AllLocksInReach" }, + { typeof(LocksServiceOutOfReach).FullName, "Simulation - AllLocksOutOfReach" }, + { typeof(Services.BluetoothLock.BLE.LockItByScanServiceEventBased).FullName, "Live - Scan" }, + { typeof(Services.BluetoothLock.BLE.LockItByScanServicePolling).FullName, "Live - Scan (Polling)" }, + { typeof(Services.BluetoothLock.BLE.LockItByGuidService).FullName, "Live - Guid" }, + /* { typeof(Services.BluetoothLock.Arendi.LockItByGuidService).FullName, "Live - Guid (Arendi)" }, + { typeof(Services.BluetoothLock.Arendi.LockItByScanService).FullName, "Live - Scan (Arendi)" }, + { typeof(Services.BluetoothLock.Bluetoothle.LockItByGuidService).FullName, "Live - Guid (Ritchie)" }, */ + }, + TinkApp.LocksServices.Active.GetType().FullName)); + + GeolocationServices = new ServicesViewModel( + TinkApp.GeolocationServices.Select(x => x.GetType().FullName), + new Dictionary { + { typeof(LastKnownGeolocationService).FullName, "Smartdevice-LastKnowGeolocation" }, + { typeof(GeolocationService).FullName, "Smartdevice-MediumAccuracy" }, + { typeof(SimulatedGeolocationService).FullName, "Simulation-AlwaysSamePosition" } }, + TinkApp.GeolocationServices.Active.GetType().FullName); + } + + /// + /// User switches scheme. + /// + private void OnThemesChanged(object sender, PropertyChangedEventArgs e) + { + // Set active theme (leads to switch of title) + TinkApp.Themes.SetActive(Themes.Active); + + // Switch theme. + ICollection mergedDictionaries = Application.Current.Resources.MergedDictionaries; + if (mergedDictionaries == null) + { + Log.ForContext().Error("No merged dictionary available."); + return; + } + + mergedDictionaries.Clear(); + + if (Themes.Active == typeof(Themes.Konrad).FullName) + { + mergedDictionaries.Add(new Themes.Konrad()); + } + else if (Themes.Active == typeof(Themes.ShareeBike).FullName) + { + mergedDictionaries.Add(new Themes.ShareeBike()); + } + else + { + Log.ForContext().Debug($"No theme {Themes.Active} found."); + } + } + + /// Holds information whether app is connected to web or not. + private bool? isConnected = null; + + /// Exposes the is connected state. + private bool IsConnected + { + get => isConnected ?? false; + set + { + isConnected = value; + } + } + + /// Holds a value indicating whether group filters GUI are visible or not + public bool IsGroupFilterVisible => GroupFilter.Count > 0; + + /// Holds the bike types to fade out or show + public SettingsBikeFilterViewModel GroupFilter { get; } + + /// Gets the value to path were copri mock files are located (for debugging purposes). + public string ExternalFolder { get; } + + /// + /// Gets the value to path were copri mock files are located (for debugging purposes). + /// + public string InternalPath => TinkApp.SettingsFileFolder; + + /// + /// Gets the value of device identifier (for debugging purposes). + /// + public string DeviceIdentifier + { + get { return TinkApp.Device.GetIdentifier(); } + } + + /// + /// Invoked when page is shutdown. + /// Currently invoked by code behind, would be nice if called by XAML in future versions. + /// + public async Task OnDisappearing() + { + try + { + Log.ForContext().Information($"Entering {nameof(OnDisappearing)}..."); + + // Update model values. + TinkApp.NextActiveUri = CopriServerUriList.NextActiveUri; + + TinkApp.Polling = new PollingParameters( + new TimeSpan(0, 0, Polling.PeriodeTotalSeconds), + Polling.IsActivated); + + TinkApp.ExpiresAfter = TimeSpan.FromSeconds(ExpiresAfterTotalSeconds); + + var filterGroup = GroupFilter.ToDictionary(x => x.Key, x => x.State); + TinkApp.FilterGroupSetting = new GroupFilterSettings(filterGroup.Count > 0 ? filterGroup : null); + + // Update map page filter. + // Reasons for which map page filter has to be updated: + // - user activated/ deactivated a group (TINKCorpi/ TINKSms/ Konrad) + // - user logged off + TinkApp.GroupFilterMapPage = + GroupFilterMapPageHelper.CreateUpdated( + TinkApp.GroupFilterMapPage, + TinkApp.ActiveUser.DoFilter(TinkApp.FilterGroupSetting.DoFilter())); + + TinkApp.CenterMapToCurrentLocation = CenterMapToCurrentLocation; + + if (IsLogToExternalFolderVisible) + { + // If no external folder is available do not update model value. + TinkApp.LogToExternalFolder = LogToExternalFolderDisplayValue; + } + + TinkApp.IsSiteCachingOn = IsSiteCachingOnDisplayValue; + + TinkApp.MinimumLogEventLevel = m_oMinimumLogEventLevel; // Update value to be serialized. + TinkApp.UpdateLoggingLevel(m_oMinimumLogEventLevel); // Update logging server. + + TinkApp.LocksServices.SetActive(LocksServices.Services.Active); + + TinkApp.GeolocationServices.SetActive(GeolocationServices.Active); + + TinkApp.LocksServices.SetTimeOut(TimeSpan.FromSeconds(LocksServices.ConnectTimeoutSec)); + + // Persist settings in case app is closed directly. + TinkApp.Save(); + + TinkApp.UpdateConnector(); + + await m_oViewUpdateManager.StopUpdatePeridically(); + + Log.ForContext().Information($"{nameof(OnDisappearing)} done."); + } + catch (Exception l_oException) + { + await m_oViewService.DisplayAlert( + "Fehler", + $"Ein unerwarteter Fehler ist aufgetreten. \r\n{l_oException.Message}", + "OK"); + } + } + + /// True if there is an error message to display. + + + /// + /// Exception which occurred getting bike information. + /// + protected Exception Exception { get; set; } + + /// + /// If true debug controls are visible, false if not. + /// + public Permissions DebugLevel + { + get + { + return (Exception == null || Exception is WebConnectFailureException) + ? TinkApp.ActiveUser.DebugLevel + : Permissions.None; + } + } + + /// Polling periode. + public PollingViewModel Polling { get; } + + /// Active logging level + public string SelectedLoggingLevel + { + get + { + return m_oMinimumLogEventLevel.ToString(); + } + set + { + if (!Enum.TryParse(value, out LogEventLevel l_oNewLevel)) + { + return; + } + + m_oMinimumLogEventLevel = l_oNewLevel; + } + } + + public bool CenterMapToCurrentLocation { get; set; } + + /// Holds either + /// - a value indicating whether to use external folder (e.g. SD card)/ or internal folder for storing log-files or + /// - is false if external folder is not available + /// + public bool LogToExternalFolderDisplayValue { get; set; } + + public bool IsSiteCachingOnDisplayValue { get; set; } + + /// Holds a value indicating whether user can use external folder (e.g. SD card) for storing log-files. + public bool IsLogToExternalFolderVisible { get; } + + /// + /// Holds the logging level serilog provides. + /// + public List LoggingLevels + { + get + { + return new List + { + LogEventLevel.Verbose.ToString(), + LogEventLevel.Debug.ToString(), + LogEventLevel.Information.ToString(), + LogEventLevel.Warning.ToString(), + LogEventLevel.Error.ToString(), + LogEventLevel.Fatal.ToString(), + }; + } + } + + double expiresAfterTotalSeconds; + + public double ExpiresAfterTotalSeconds + { + get => expiresAfterTotalSeconds; + set + { + if (value == expiresAfterTotalSeconds) + { + return; + } + + expiresAfterTotalSeconds = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExpiresAfterTotalSecondsText))); + } + } + + public string ExpiresAfterTotalSecondsText + { + get => expiresAfterTotalSeconds.ToString("0"); + } + + + } +} diff --git a/TINKLib/ViewModel/ViewModelHelper.cs b/TINKLib/ViewModel/ViewModelHelper.cs new file mode 100644 index 0000000..252fda6 --- /dev/null +++ b/TINKLib/ViewModel/ViewModelHelper.cs @@ -0,0 +1,245 @@ +using MonkeyCache.FileStore; +using Serilog; +using System; +using System.Threading.Tasks; +using TINK.Model.Repository.Exception; +using TINK.Model.Device; +using TINK.Model.State; +using TINK.Model.Station; +using TINK.Model.User; +using Xamarin.Forms; + +using TINK.Model.Bikes.Bike.BC; +using TINK.Model.Repository; +using TINK.Repository.Exception; +using System.Net; +using TINK.MultilingualResources; + +namespace TINK.ViewModel +{ + public static class ViewModelHelper + { + /// First part of text making up a station name. + private const string USER_FIENDLY_STATIONNUMBER_PREFIX = "Station"; + + /// Holds the color which marks link to TINK- app pages, web sites, ... + public static Color LINK_COLOR = Color.Blue; + + /// + /// Gets station name from station object. + /// + /// Station to get id from + /// + public static string GetStationName(this IStation p_oStation) + { + if (p_oStation == null) + { + return string.Empty; + } + + if (!string.IsNullOrEmpty(p_oStation.StationName)) + { + return $"{p_oStation.StationName}, Nr. {p_oStation.Id}."; + } + + return GetStationName(p_oStation.Id); + } + + /// + /// Gets station name from station object. + /// + /// Station to get id from + /// + public static string GetStationName(int p_iStationId) + { + return string.Format("{0} {1}", USER_FIENDLY_STATIONNUMBER_PREFIX, p_iStationId); + } + + /// + /// Gets station id from station name. + /// + /// Station to get id from + /// + public static int GetStationId(string p_strStationName) + { + return int.Parse(p_strStationName.Replace(USER_FIENDLY_STATIONNUMBER_PREFIX, "").Trim()); + } + + /// Get the display name of a bike. + /// bike to get name for. + /// Display name of bike. + public static string GetDisplayName(this IBikeInfoMutable bike) + { + var l_oId = bike.Id; + var l_oIsDemo = bike.IsDemo; + + // Not known how many whells cargo bike has. + return $"{(!string.IsNullOrEmpty(bike.Description) ? $"{bike.Description}, " : string.Empty)}Nr. {l_oId}"; + } + + /// + /// Maps state to color. + /// + /// + /// + public static Color GetColor(this InUseStateEnum p_eState) + { + switch (p_eState) + { + case InUseStateEnum.Disposable: + return Color.Default; + + case InUseStateEnum.Reserved: + return Color.Orange; + + case InUseStateEnum.Booked: + return Color.Green; + default: + return Color.Default; + } + } + + /// Gets message that logged in user has not booked any bikes. + public static string GetShortErrorInfoText(this Exception exception) + { + if (exception == null) + { + return string.Empty; + } + + // An error occurred getting bikes information. + switch (exception) + { + case WebConnectFailureException: + return AppResources.ActivityTextErrorWebConnectFailureException; + case InvalidResponseException: + return AppResources.ActivityTextErrorInvalidResponseException; + case WebForbiddenException: + return AppResources.ActivityTextErrorWebForbiddenException; + case DeserializationException: + return AppResources.ActivityTextErrorDeserializationException; + case WebException webException: + return string.Format(AppResources.ActivityTextErrorWebException, webException.Status); + default: + return AppResources.ActivityTextErrorException; + } + + } + + /// Gets message that logged in user has not booked any bikes. + public static FormattedString GetErrorInfoText(this Exception p_oException) + { + if (p_oException == null) + { + return string.Empty; + } + + FormattedString l_oError; + // An error occurred getting bikes information. + if (p_oException is WebConnectFailureException) + { + + l_oError = new FormattedString(); + l_oError.Spans.Add(new Span { Text = "Information!\r\n", FontAttributes = FontAttributes.Bold }); + l_oError.Spans.Add(new Span { Text = $"{p_oException.Message}\r\n{WebConnectFailureException.GetHintToPossibleExceptionsReasons}" }); + return l_oError; + + } + else if (p_oException is InvalidResponseException) + { + l_oError = new FormattedString(); + l_oError.Spans.Add(new Span { Text = "Fehler, ungültige Serverantwort!\r\n", FontAttributes = FontAttributes.Bold }); + l_oError.Spans.Add(new Span { Text = $"{p_oException.Message}" }); + return l_oError; + } + else if (p_oException is WebForbiddenException) + { + l_oError = new FormattedString(); + l_oError.Spans.Add(new Span { Text = "Beschäftigt... Einen Moment bitte!" }); + return l_oError; + } + + l_oError = new FormattedString(); + l_oError.Spans.Add(new Span { Text = "Allgemeiner Fehler!\r\n", FontAttributes = FontAttributes.Bold }); + l_oError.Spans.Add(new Span { Text = $"{p_oException}" }); + return l_oError; + } + + + /// User tabbed a URI. + /// Sender of the event. + /// Event arguments + public static void OnNavigating(object p_oSender, WebNavigatingEventArgs p_eEventArgs) + { + + if (!p_eEventArgs.Url.ToUpper().StartsWith("HTTP")) + { + // An internal link was detected. + // Stay inside WebView + p_eEventArgs.Cancel = false; + return; + } + + // Do not navigate outside the document. + p_eEventArgs.Cancel = true; + + DependencyService.Get().OpenUrl(p_eEventArgs.Url); + } + + /// Gets the user group if a user friendly name. + /// + /// + public static string GetUserGroupDisplayName(this User p_oUser) + { + return string.Join(" & ", p_oUser.Group); + } + + + /// Called when page is shown. + /// Url to load data from. + /// Holds value wether site caching is on or off. + /// Provides resource from embedded ressources. + public static async Task GetSource( + string resourceUrl, + bool isSiteCachingOn, + Func resourceProvider = null) + { + if (!Barrel.Current.IsExpired(resourceUrl) && isSiteCachingOn) + { + // Cached html is still valid and caching is on. + return Barrel.Current.Get(resourceUrl); + } + + string htmlContent = string.Empty; + + try + { + // Get info from web server. + htmlContent = await CopriCallsHttps.Get(resourceUrl); + } + catch (Exception l_oException) + { + // Getting html failed. + Log.Error($"Requesting {resourceUrl} failed. {l_oException.Message}"); + } + + switch (string.IsNullOrEmpty(htmlContent)) + { + case true: + // An error occurred getting resource from web + htmlContent = Barrel.Current.Exists(resourceUrl) + ? Barrel.Current.Get(key: resourceUrl) // Get from MonkeyCache + : resourceProvider != null ? resourceProvider() : $"Error loading {resourceUrl}."; // Get build in ressource. + break; + + default: + // Add resource to cache. + Barrel.Current.Add(key: resourceUrl, data: htmlContent, expireIn: isSiteCachingOn ? TimeSpan.FromDays(1) : TimeSpan.FromMilliseconds(1) ); + break; + } + + return htmlContent ?? string.Format("An error occurred loading html- ressource."); + } + + } +} diff --git a/TINKLib/ViewModel/WhatsNew/Agb/AgbViewModel.cs b/TINKLib/ViewModel/WhatsNew/Agb/AgbViewModel.cs new file mode 100644 index 0000000..106614b --- /dev/null +++ b/TINKLib/ViewModel/WhatsNew/Agb/AgbViewModel.cs @@ -0,0 +1,79 @@ +using TINK.View; +using System.Windows.Input; +using Xamarin.Forms; +using System; +using System.ComponentModel; +using System.Threading.Tasks; +using TINK.ViewModel.Info; +using TINK.Model.User.Account; + +namespace TINK.ViewModel.WhatsNew.Agb +{ + public class AgbViewModel : INotifyPropertyChanged + { + /// Fired whenever a property changed. + public event PropertyChangedEventHandler PropertyChanged; + + /// Holds the name of the host. + private string HostName { get; } + + /// Holds value wether site caching is on or off. + bool IsSiteCachingOn { get; } + + /// Constructs AGB view model + /// Holds value wether site caching is on or off. + /// Delegate to get an an embedded html ressource. Used as fallback if download from web page does not work and cache is empty. + /// View service to close page. + public AgbViewModel( + string hostName, + bool isSiteCachingOn, + Func resourceProvider, + IViewService p_oViewService) + { + HostName = hostName; + + IsSiteCachingOn = isSiteCachingOn; + + ViewService = p_oViewService + ?? throw new ArgumentException($"Can not instantiate {typeof(WhatsNewViewModel)}-object. No view available."); + + ResourceProvider = resourceProvider + ?? throw new ArgumentException($"Can not instantiate {typeof(WhatsNewViewModel)}-object. No ressource provider availalbe."); + } + + /// Gets the platfrom specific prefix. + private Func ResourceProvider { get; set; } + + /// Agb information text. + private HtmlWebViewSource infoAgb; + + /// Agb information text. + public HtmlWebViewSource InfoAgb + { + get => infoAgb; + set + { + infoAgb = value; + PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(InfoAgb))); + } + } + + /// Called when page is shown. + public async Task OnAppearing() + { + InfoAgb = await InfoViewModel.GetAgb(HostName, IsSiteCachingOn, ResourceProvider); + } + + /// User clicks OK button. + public ICommand OnOk + { + get + { + return new Command(async () => await ViewService.PopModalAsync()); + } + } + + /// Reference to view service object. + private IViewService ViewService; + } +} diff --git a/TINKLib/ViewModel/WhatsNew/WhatsNewViewModel.cs b/TINKLib/ViewModel/WhatsNew/WhatsNewViewModel.cs new file mode 100644 index 0000000..cc9e4d7 --- /dev/null +++ b/TINKLib/ViewModel/WhatsNew/WhatsNewViewModel.cs @@ -0,0 +1,106 @@ +using TINK.View; +using System.Windows.Input; +using Xamarin.Forms; +using System; +using System.ComponentModel; + +namespace TINK.ViewModel.WhatsNew +{ + public class WhatsNewViewModel : INotifyPropertyChanged + { + /// Constructs view model. + /// + /// + /// + /// Delegate to invoke master detail. + /// View service to show agb- page. + public WhatsNewViewModel( + Version currentVersion, + string whatsNewText, + bool isShowAgbRequired, + Action showMasterDetail, + IViewService p_oViewService) + { + ViewService = p_oViewService + ?? throw new ArgumentException($"Can not instantiate {typeof(WhatsNewViewModel)}-object. No view available."); + + ShowMasterDetail = showMasterDetail + ?? throw new ArgumentException($"Can not instantiate {typeof(WhatsNewViewModel)}-object. No delegate to activated maste detail page avilable."); + + CurrentVersion = currentVersion; + WhatsNewText = new FormattedString(); + WhatsNewText.Spans.Add(new Span { Text = whatsNewText }); + IsAgbChangedVisible = isShowAgbRequired; + } + + public Version CurrentVersion { get; } + + public FormattedString WhatsNewText {get;} + + /// + /// Title of the WhatsNewPage. + /// + public string WhatsNewTitle + { + get + { + // Get version info. + return $"Neu in Version {CurrentVersion}"; + } + } + + /// Text saying that AGBs were modified. + public FormattedString AgbChangedText + { + get + { + var l_oHint = new FormattedString(); + l_oHint.Spans.Add(new Span { Text = "AGBs ", ForegroundColor = ViewModelHelper.LINK_COLOR }); + l_oHint.Spans.Add(new Span { Text = "überarbeitet.\r\n" }); + + return l_oHint; + } + } + + public bool IsAgbChangedVisible { get; } + + /// Command object to bind agb link to view model. + public ICommand OnShowAgbTapped + { + get + { + return new Command(async () => await ViewService.PushModalAsync(ViewTypes.AgbPage)); + } + } + + /// Called when dialog is disappearing. + /// + public void OnDisappearing(Action setWhatsNewWasShown) + { + setWhatsNewWasShown(); + } + +#if !SHOWFEEDBACK + public bool IsFeedbackVisible => false; +#else + public bool IsFeedbackVisible => true; +#endif + /// User clicks rate button. + public ICommand OnOk + { + get + { + return new Command(() => ShowMasterDetail()); + } + } + + /// Reference to view service object. + private IViewService ViewService; + + /// Reference to view service object. + private Action ShowMasterDetail { get; } + + /// Fired whenever a property changes. + public event PropertyChangedEventHandler PropertyChanged; + } +} diff --git a/TINKLib/ViewTypes.cs b/TINKLib/ViewTypes.cs new file mode 100644 index 0000000..8efb893 --- /dev/null +++ b/TINKLib/ViewTypes.cs @@ -0,0 +1,20 @@ +namespace TINK +{ + /// Holds a entry for each page of the TINK app. + public enum ViewTypes + { + LoginPage, + MapPage, + RegisterPage, + PasswordForgottenPage, + BikeInfoCarouselPage, + MyBikesPage, + SettingsPage, + TabbedPageInfo, + TabbedPageHelpContact, + ManageAccountPage, + AgbPage, + WhatsNewPage, + BikesAtStation + } +}