Initial version.

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

14
TINKLib/CSharp9.cs Normal file
View file

@ -0,0 +1,14 @@
using System.ComponentModel;
// ReSharper disable once CheckNamespace
namespace System.Runtime.CompilerServices
{
/// <summary>
/// Reserved to be used by the compiler for tracking metadata.
/// This class should not be used by developers in source code.
/// </summary>
[EditorBrowsable(EditorBrowsableState.Never)]
internal static class IsExternalInit
{
}
}

View file

@ -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
{
/// <summary> Default value of demo property. </summary>
public const bool DEFAULTVALUEISDEMO = false;
/// <summary> Holds the info about the bike state. </summary>
private readonly IStateInfo m_oStateInfo;
/// <summary>
/// Holds the bike object.
/// </summary>
private Bike Bike { get; }
/// <summary> Constructs a bike object.</summary>
protected BikeInfo(
IStateInfo stateInfo,
int id,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> 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<string>();
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) { }
/// <summary>
/// Constructs a bike info object for a available bike.
/// </summary>
/// <param name="id">Unique id of bike.</param>
/// <param name="currentStationId">Id of station where bike is located.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="wheelType"></param>
public BikeInfo(
int id,
int? currentStationId,
Uri operatorUri = null,
TariffDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> group = null,
WheelType? wheelType = null,
TypeOfBike? typeOfBike = null,
string description = null) : this(
new StateInfo(),
id,
isDemo,
group,
wheelType,
typeOfBike,
description,
currentStationId,
operatorUri,
tariffDescription)
{
}
/// <summary>
/// Constructs a bike info object for a requested bike.
/// </summary>
/// <param name="dateTimeProvider">Provider for current date time to calculate remainig time on demand for state of type reserved.</param>
/// <param name="wheelType"></param>
/// <param name="id">Unique id of bike.</param>
/// <param name="stationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="requestedAt">Date time when bike was requested</param>
/// <param name="mailAddress">Mail address of user which requested bike.</param>
/// <param name="code">Booking code.</param>
/// <param name="p_oDateTimeNowProvider">Date time provider to calculate reaining time.</param>
public BikeInfo(
int id,
bool? isDemo,
IEnumerable<string> group,
WheelType? wheelType,
TypeOfBike? typeOfBike,
string description,
int? stationId,
Uri operatorUri,
TariffDescription tariffDescription,
DateTime requestedAt,
string mailAddress,
string code,
Func<DateTime> dateTimeProvider = null) : this(
new StateInfo(
dateTimeProvider,
requestedAt,
mailAddress,
code),
id,
isDemo,
group,
wheelType,
typeOfBike,
description,
stationId,
operatorUri,
tariffDescription)
{
}
/// <summary>
/// Constructs a bike info object for a booked bike.
/// </summary>
/// <param name="dateTimeProvider">Provider for current date time to calculate remainig time on demand for state of type reserved.</param>
/// <param name="wheelType"></param>
/// <param name="id">Unique id of bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="bookedAt">Date time when bike was booked</param>
/// <param name="mailAddress">Mail address of user which booked bike.</param>
/// <param name="code">Booking code.</param>
public BikeInfo(
int id,
bool? isDemo,
IEnumerable<string> 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)
{
}
/// <summary> True if device is demo device, false otherwise. </summary>
public bool IsDemo { get; }
/// <summary> Returns the group (TINK, Konrad, ...). </summary>
public IEnumerable<string> Group { get; }
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
public int? CurrentStation { get; }
/// <summary> Holds description about the tarif. </summary>
public TariffDescription TariffDescription { get; }
/// Holds the rent state of the bike.
/// </summary>
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;
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
public Uri OperatorUri { get; }
/// <summary>
/// Converts the instance to text.
/// </summary>
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}.";
}
}
}

View file

@ -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
{
/// <summary> Holds the bike. </summary>
private readonly Bike m_oBike;
/// <summary> Holds the state info of the bike. </summary>
private readonly StateInfoMutable m_oStateInfo;
/// <summary>
/// Constructs a bike.
/// </summary>
/// <param name="id">Unique id of bike.</param>
/// <param name="isDemo">True if device is demo device, false otherwise.</param>
/// <param name="dateTimeProvider">Provider for current date time to calculate remainig time on demand for state of type reserved.</param>
/// <param name="wheelType"></param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="stateInfo">Bike state info.</param>
protected BikeInfoMutable(
int id,
bool isDemo = BikeInfo.DEFAULTVALUEISDEMO,
IEnumerable<string> group = null,
WheelType? wheelType = null,
TypeOfBike? typeOfBike = null,
string description = null,
int? currentStationId = null,
Uri operatorUri = null,
TariffDescription tariffDescription = null,
Func<DateTime> 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;
}
/// <summary> Constructs a bike object from source. </summary>
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)
{
}
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
[DataMember]
public int? CurrentStation { get; }
/// <summary> Holds description about the tarif. </summary>
[DataMember]
public TariffDescription TariffDescription { get; private set; }
/// <summary>
/// Holds the rent state of the bike.
/// </summary>
[DataMember]
public StateInfoMutable State
{
get { return m_oStateInfo; }
}
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
public Uri OperatorUri { get; }
/// <summary> Unused member. </summary>
IStateInfoMutable IBikeInfoMutable.State => m_oStateInfo;
public int Id => m_oBike.Id;
public bool IsDemo { get; }
/// <summary> Returns the group (TINK, Konrad, ...). </summary>
public IEnumerable<string> Group { get; }
public WheelType? WheelType => m_oBike.WheelType;
public TypeOfBike? TypeOfBike => m_oBike.TypeOfBike;
public string Description => m_oBike.Description;
/// <summary>
/// Fired whenever property of bike changes.
/// </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Converts the instance to text.
/// </summary>
/// <returns></returns>
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")}.";
}
}
}

View file

@ -0,0 +1,55 @@
using System;
using System.Collections.Generic;
using TINK.Model.Bikes.Bike;
using TINK.Model.State;
namespace TINK.Model.Bike.BC
{
/// <summary>
/// Allows to access bike info.
/// </summary>
public interface IBikeInfo
{
/// <summary>
/// Holds the unique id of the bike;
/// </summary>
int Id { get; }
/// <summary> True if bike is a demo bike. </summary>
bool IsDemo { get; }
/// <summary> Returns the group (TINK, Konrad, ...). </summary>
IEnumerable<string> Group { get; }
/// <summary>
/// Holds the count of wheels.
/// </summary>
WheelType? WheelType { get; }
/// <summary>
/// Holds the type of bike.
/// </summary>
TypeOfBike? TypeOfBike { get; }
/// <summary> Holds the description of the bike. </summary>
string Description { get; }
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
int? CurrentStation { get; }
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
Uri OperatorUri { get; }
/// <summary> Holds description about the tarif. </summary>
TariffDescription TariffDescription { get; }
/// <summary>
/// Holds the rent state of the bike.
/// </summary>
IStateInfo State { get; }
}
}

View file

@ -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
{
/// <summary>
/// Holds the unique id of the bike;
/// </summary>
int Id { get; }
/// <summary> True if bike is a demo bike. </summary>
bool IsDemo { get; }
/// <summary> Returns the group (TINK, Konrad, ...). </summary>
IEnumerable<string> Group { get; }
/// <summary>
/// Holds the count of wheels.
/// </summary>
WheelType? WheelType { get; }
/// <summary>
/// Holds the type of bike.
/// </summary>
TypeOfBike? TypeOfBike { get; }
/// <summary> Holds the description of the bike. </summary>
string Description { get; }
/// <summary>
/// Station a which bike is located, null otherwise.
/// </summary>
int? CurrentStation { get; }
/// <summary>
/// Holds the rent state of the bike.
/// </summary>
IStateInfoMutable State { get; }
/// <summary>
/// Uri of the operator or null, in case of single operator setup.
/// </summary>
Uri OperatorUri { get; }
event PropertyChangedEventHandler PropertyChanged;
}
public enum NotifyPropertyChangedLevel
{
/// <summary> Notify about all property changes.</summary>
All,
/// <summary> Notify about no property changes.</summary>
None
}
}

View file

@ -0,0 +1,126 @@
using System;
using System.Collections.Generic;
namespace TINK.Model.Bike
{
/// <summary> Count of wheels. </summary>
public enum WheelType
{
Mono = 0,
Two = 1,
Trike = 2,
}
/// <summary> Type of bike. </summary>
public enum TypeOfBike
{
Allround = 0,
Cargo = 1,
Citybike = 2,
}
public class Bike : IEquatable<Bike>
{
/// <summary>
/// Constructs a bike.
/// </summary>
/// <param name="dateTimeProvider">Provider for current date time to calculate remainig time on demand for state of type reserved.</param>
/// <param name="wheelType"></param>
/// <param name="p_iId">Unique id of bike.</param>
/// <param name="p_strCurrentStationName">Name of station where bike is located, null if bike is on the road.</param>
public Bike(
int p_iId,
WheelType? wheelType = null,
TypeOfBike? typeOfBike = null,
string description = null)
{
WheelType = wheelType;
TypeOfBike = typeOfBike;
Id = p_iId;
Description = description;
}
/// <summary>
/// Holds the unique id of the bike;
/// </summary>
public int Id { get; }
/// <summary>
/// Holds the count of wheels.
/// </summary>
public WheelType? WheelType { get; }
/// <summary>
/// Holds the type of bike.
/// </summary>
public TypeOfBike? TypeOfBike { get; }
/// <summary> Holds the description of the bike. </summary>
public string Description { get; }
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public override bool Equals(object obj)
{
var l_oBike = obj as Bike;
if (l_oBike == null)
{
return false;
}
return Equals(l_oBike);
}
/// <summary> Converts the instance to text.</summary>
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}" : "")}.";
}
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public bool Equals(Bike other)
{
return other != null &&
Id == other.Id &&
WheelType == other.WheelType &&
TypeOfBike == other.TypeOfBike
&& Description == other.Description;
}
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public static bool operator ==(Bike bike1, Bike bike2)
{
return EqualityComparer<Bike>.Default.Equals(bike1, bike2);
}
/// <summary> Compares two bike object.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if bikes are equal.</returns>
public static bool operator !=(Bike bike1, Bike bike2)
{
return !(bike1 == bike2);
}
/// <summary>
/// Generates hash code for bike object.
/// </summary>
/// <returns></returns>
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;
}
}
}

View file

@ -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
{
/// <summary>
/// Constructs a bike info object for a available bike.
/// </summary>
/// <param name="bikeId">Unique id of bike.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="currentStationId">Id of station where bike is located.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="wheelType">Trike, two wheels, mono, ....</param>
public BikeInfo(
int bikeId,
int lockId,
Guid lockGuid,
int? currentStationId,
Uri operatorUri = null,
TariffDescription tariffDescription = null,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> 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();
}
/// <summary>
/// Constructs a bike info object for a requested bike.
/// </summary>
/// <param name="dateTimeProvider">Provider for current date time to calculate remainig time on demand for state of type reserved.</param>
/// <param name="id">Unique id of bike.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="requestedAt">Date time when bike was requested</param>
/// <param name="mailAddress">Mail address of user which requested bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="p_oDateTimeNowProvider">Date time provider to calculate reaining time.</param>
/// <param name="wheelType"></param>
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<DateTime> dateTimeProvider,
bool? isDemo = DEFAULTVALUEISDEMO,
IEnumerable<string> 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();
}
/// <summary>
/// Constructs a bike info object for a booked bike.
/// </summary>
/// <param name="id">Unique id of bike.</param>
/// <param name="lockId">Id of the lock.</param>
/// <param name="lockGuid">GUID specifying the lock.</param>
/// <param name="bookedAt">Date time when bike was booked</param>
/// <param name="mailAddress">Mail address of user which booked bike.</param>
/// <param name="currentStationId">Name of station where bike is located, null if bike is on the road.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <param name="tariffDescription">Hold tariff description of bike.</param>
/// <param name="wheelType"></param>
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<string> 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; }
}
}

View file

@ -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
{
/// <summary> Constructs a bike object from source. </summary>
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;
}
}

View file

@ -0,0 +1,6 @@
namespace TINK.Model.Bike.BluetoothLock
{
public interface IBikeInfo : BC.IBikeInfo
{
}
}

View file

@ -0,0 +1,7 @@
namespace TINK.Model.Bikes.Bike.BluetoothLock
{
public interface IBikeInfoMutable : BC.IBikeInfoMutable
{
ILockInfoMutable LockInfo { get; }
}
}

View file

@ -0,0 +1,24 @@
using System;
using TINK.Model.Bike.BluetoothLock;
namespace TINK.Model.Bikes.Bike.BluetoothLock
{
public interface ILockInfoMutable
{
/// <summary> Identification number of bluetooth lock.</summary>
int Id { get; }
/// <summary> Gets the user key.</summary>
byte[] UserKey { get; }
LockingState State { get; set; }
/// <summary> Holds the percentage of lock battery.</summary>
double BatteryPercentage { get; set; }
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
Guid Guid { get; set; }
byte[] Seed { get; }
}
}

View file

@ -0,0 +1,54 @@
using System;
using TINK.Model.Bikes.Bike.BluetoothLock;
namespace TINK.Model.Bike.BluetoothLock
{
public class LockInfoMutable : ILockInfoMutable
{
/// <summary> Lock info object. </summary>
private LockInfo LockInfo { get; set; }
/// <summary> Constructs a bluetooth lock info object. </summary>
/// <param name="id">Id of lock must always been known when constructing an lock info object.</param>
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;
/// <summary> Changes during runtime: Can be unknown when set from copri and chang to a valid value when set from lock.</summary>
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();
}
/// <summary> Holds the percentage of lock battery.</summary>
public double BatteryPercentage { get; set; } = double.NaN;
/// <summary> Loads lock info object from values. </summary>
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();
}
}
}

View file

@ -0,0 +1,40 @@
using System;
namespace TINK.Model.Bikes.Bike
{
/// <summary>
/// Holds tariff info for a single bike.
/// </summary>
public record TariffDescription
{
/// <summary>
/// Name of the tariff.
/// </summary>
public string Name { get; init; }
/// <summary>
/// Number of the tariff.
/// </summary>
public int? Number { get; init; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
public double FeeEuroPerHour { get; init; }
/// <summary>
/// Costs of the abo per month.
/// </summary>
public double AboEuroPerMonth { get; init; }
/// <summary>
/// Costs per hour in euro.
/// </summary>
public TimeSpan FreeTimePerSession { get; init; }
/// <summary>
/// Max. costs per day in euro.
/// </summary>
public double MaxFeeEuroPerDay { get; init; }
}
}

View file

@ -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<BikeInfo>
{
/// <summary> Holds the bike dictionary object.</summary>
private Dictionary<int, BikeInfo> BikeDictionary { get; }
/// <summary>Constructs an empty bike info dictionary object.</summary>
public BikeCollection()
{
BikeDictionary = new Dictionary<int, BikeInfo>();
}
/// <summary> Constructs a bike collection object.</summary>
/// <param name="bikeDictionary"></param>
public BikeCollection(Dictionary<int, BikeInfo> bikeDictionary)
{
BikeDictionary = bikeDictionary ??
throw new ArgumentNullException(nameof(bikeDictionary), "Can not construct BikeCollection object.");
}
/// <summary> Gets a bike by its id.</summary>
/// <param name="p_iId">Id of the bike to get.</param>
/// <returns></returns>
public BikeInfo GetById(int p_iId)
{
return BikeDictionary.FirstOrDefault(x => x.Key == p_iId).Value;
}
/// <summary> Gets the count of bikes. </summary>
public int Count => BikeDictionary.Count;
/// <summary> Gets if a bike with given id exists.</summary>
/// <param name="p_iId">Id of bike.</param>
/// <returns>True if bike is contained, false otherwise.</returns>
public bool ContainsKey(int p_iId) => BikeDictionary.Keys.Contains(p_iId);
/// <summary> Gets the enumerator. </summary>
/// <returns>Enumerator object.</returns>
public IEnumerator<BikeInfo> GetEnumerator() => BikeDictionary.Values.GetEnumerator();
/// <summary> Gets the enumerator. </summary>
/// <returns>Enumerator object.</returns>
IEnumerator IEnumerable.GetEnumerator() => GetEnumerator();
}
}

View file

@ -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
{
/// <summary> Filters bikes by station. </summary>
/// <param name="bikesAtAnyStation">Bikes available, requested and/ or occupied bikes to filter.</param>
/// <param name="selectedStation">Id of station, might be null</param>
/// <returns>BikeCollection holding bikes at given station or empty BikeCollection, if there are no bikes.</returns>
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<int, BikeInfo>());
}
/// <summary> Filters bikes by bike type. </summary>
/// <param name="bcAndLockItBikes">Bikes available, requested and/ or occupied bikes to filter.</param>
/// <returns>BikeCollection holding LockIt-bikes empty BikeCollection, if there are no LockIt-bikes.</returns>
public static BikeCollection GetLockIt(this BikeCollection bcAndLockItBikes)
{
return new BikeCollection(bcAndLockItBikes?
.Where(bike => bike is Bike.BluetoothLock.BikeInfo)
.ToDictionary(x => x.Id) ?? new Dictionary<int, BikeInfo>());
}
}
}

View file

@ -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
{
/// <summary> Holds entity of bikes. </summary>
public class BikeCollectionMutable : ObservableCollection<BikeInfoMutable>, IBikeDictionaryMutable<BikeInfoMutable>
{
/// <summary> Constructs a mutable bike collection object. </summary>
public BikeCollectionMutable()
{
SelectedBike = null;
}
/// <summary>
/// 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
/// </summary>
/// <param name="bikesAll"> Object holding bikes info from copri to update from.</param>
/// <param name="p_oDateTimeProvider">Provices date time information.</param>
public void Update(
IEnumerable<BikeInfo> 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<BikeInfo>()))
{
/// 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<int>(bikeInfo.Id))
{
// Remove list from obsolete list.
bikesToBeRemoved.Remove(bikeInfo.Id);
}
}
// Remove obsolete bikes.
foreach (var l_oId in bikesToBeRemoved)
{
RemoveById(l_oId);
}
}
/// <summary>
/// Adds a new bike to collecion of bike.
/// </summary>
/// <param name="p_oNewBike">New bike to add.</param>
/// <exception cref="Exception">Thrown if bike is not unique.</exception>
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);
}
/// <summary>
/// Bike selected by user for regerving or cancel reservation.
/// </summary>
public BikeInfoMutable SelectedBike
{
get;
private set;
}
public void SetSelectedBike(int p_intId)
{
SelectedBike = GetById(p_intId);
}
/// <summary>
/// Gets a bike by its id.
/// </summary>
/// <param name="p_iId"></param>
/// <returns></returns>
public BikeInfoMutable GetById(int p_iId)
{
{
return this.FirstOrDefault(bike => bike.Id == p_iId);
}
}
/// <summary>
/// Deteermines whether a bike by given key exists.
/// </summary>
/// <param name="p_strKey">Key to check.</param>
/// <returns>True if bike exists.</returns>
public bool ContainsKey(int p_iId)
{
return GetById(p_iId) != null;
}
/// <summary>
/// Removes a bike by its id.
/// </summary>
/// <param name="p_iId">Id of bike to be removed.</param>
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);
}
/// <summary>
/// Create mutable objects from immutable objects.
/// </summary>
private static class BikeInfoMutableFactory
{
public static BikeInfoMutable Create(BikeInfo bikeInfo)
{
return (bikeInfo is BluetoothLock.BikeInfo bluetoothLockBikeInfo)
? new BluetoothLock.BikeInfoMutable(bluetoothLockBikeInfo)
: new BikeInfoMutable(bikeInfo);
}
}
}
}

View file

@ -0,0 +1,38 @@
using System.Collections.Generic;
using System.Linq;
namespace TINK.Model.Bike
{
public static class BikeCollectionUpdater
{
/// <summary> Updates bikes lock info with the latest lock info from bluetooth service.</summary>
/// <param name="bikes">bikes to be updated.</param>
/// <param name="locksInfo">locks info to be used for updating bikes.</param>
/// <returns></returns>
public static BikeCollection UpdateLockInfo(
this BikeCollection bikes,
IEnumerable<BluetoothLock.LockInfo> locksInfo)
{
var updatedBikesCollection = new Dictionary<int, BC.BikeInfo>();
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);
}
}
}

View file

@ -0,0 +1,35 @@
using System.Collections.Generic;
namespace TINK.Model.Bike
{
public interface IBikeDictionary<T> : IReadOnlyCollection<T>
{
/// <summary>
/// Gets a bike by its id.
/// </summary>
/// <param name="p_iId"></param>
/// <returns></returns>
T GetById(int p_iId);
/// <summary>
/// Deteermines whether a bike by given key exists.
/// </summary>
/// <param name="p_strKey">Key to check.</param>
/// <returns>True if bike exists.</returns>
bool ContainsKey(int p_iId);
}
public interface IBikeDictionaryMutable<T> : IBikeDictionary<T>
{
/// <summary>
/// Removes a bike by its id.
/// </summary>
/// <param name="p_iId">Id of bike to be removed.</param>
void RemoveById(int p_iId);
/// <summary>
/// Adds a new element to dictinary.
/// </summary>
/// <param name="p_oNewElement">New element to add.</param>
void Add(T p_oNewElement);
}
}

View file

@ -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
{
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
public bool IsConnected => CopriServer.IsConnected;
/// <summary> No user is logged in.</summary>
public string SessionCookie => null;
/// <summary> Is raised whenever login state has changed.</summary>
public event LoginStateChangedEventHandler LoginStateChanged;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
public Command(
ICopriServerBase p_oCopriServer) : base(p_oCopriServer)
{
}
/// <summary>
/// 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.
/// </summary>
public async Task<IAccount> 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;
}
/// <summary> Logs user out. </summary>
public async Task DoLogout()
{
Log.ForContext<Command>().Error("Unexpected log out request detected. No user logged in.");
await Task.CompletedTask;
}
/// <summary>
/// Request to reserve a bike.
/// </summary>
/// <param name="p_oBike">Bike to book.</param>
public async Task DoReserve(
Bikes.Bike.BC.IBikeInfoMutable p_oBike)
{
Log.ForContext<Command>().Error("Unexpected booking request detected. No user logged in.");
await Task.CompletedTask;
}
/// <summary> Request to cancel a reservation.</summary>
/// <param name="p_oBike">Bike to book.</param>
public async Task DoCancelReservation(Bikes.Bike.BC.IBikeInfoMutable bike)
{
Log.ForContext<Command>().Error("Unexpected cancel reservation request detected. No user logged in.");
await Task.CompletedTask;
}
/// <summary> Get authentication keys.</summary>
/// <param name="bike">Bike to book.</param>
public async Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike)
{
Log.ForContext<Command>().Error("Unexpected request to get authenticatin keys detected. No user logged in.");
await Task.CompletedTask;
}
/// <summary> Updates COPRI lock state for a booked bike. </summary>
/// <param name="bike">Bike to update locking state for.</param>
/// <param name="location">Location where lock was opened/ changed.</param>
/// <returns>Response on updating locking state.</returns>
public async Task UpdateLockingStateAsync(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location)
{
Log.ForContext<Command>().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<Command>().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<Command>().Error("Unexpected returning request detected. No user logged in.");
await Task.CompletedTask;
}
/// <summary>
/// Submits feedback to copri server.
/// </summary>
/// <param name="userFeedback">Feedback to submit.</param>
public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri)
{
Log.ForContext<Command>().Error("Unexpected submit feedback request detected. No user logged in.");
await Task.CompletedTask;
}
}
}

View file

@ -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
{
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
public bool IsConnected => CopriServer.IsConnected;
/// <summary> Is raised whenever login state has changed.</summary>
public event LoginStateChangedEventHandler LoginStateChanged;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
public CommandLoggedIn(ICopriServerBase p_oCopriServer,
string p_strSessionCookie,
string p_strMail,
Func<DateTime> p_oDateTimeProvider) : base(p_oCopriServer, p_strSessionCookie, p_strMail, p_oDateTimeProvider)
{
}
/// <summary>
/// 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.
/// </summary>
/// <param name="p_oAccount">Account to use for login.</param>
public Task<IAccount> 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.");
}
/// <summary> Logs user out. </summary>
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());
}
/// <summary>
/// Request to reserve a bike.
/// </summary>
/// <param name="bike">Bike to book.</param>
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);
}
/// <summary> Request to cancel a reservation.</summary>
/// <param name="bike">Bike to cancel reservation.</param>
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);
}
/// <summary> Get authentication keys.</summary>
/// <param name="bike">Bike to get new keys for.</param>
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);
}
/// <summary> Updates COPRI lock state for a booked bike. </summary>
/// <param name="bike">Bike to update locking state for.</param>
/// <returns>Response on updating locking state.</returns>
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;
}
}
/// <summary> Request to book a bike. </summary>
/// <param name="bike">Bike to book.</param>
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);
}
/// <summary> Request to return a bike.</summary>
/// <param name="latitude">Latitude of the bike.</param>
/// <param name="longitude">Longitude of the bike.</param>
/// <param name="bike">Bike to return.</param>
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);
}
/// <summary>
/// Submits feedback to copri server.
/// </summary>
/// <param name="userFeedback">Feedback to submit.</param>
public async Task DoSubmitFeedback(ICommand.IUserFeedback userFeedback, Uri opertorUri)
=> await CopriServer.DoSubmitFeedback(userFeedback.Message, userFeedback.IsBikeBroken, opertorUri);
}
}

View file

@ -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
{
/// <summary> Is raised whenever login state has changed.</summary>
event LoginStateChangedEventHandler LoginStateChanged;
/// <summary>
/// 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.
/// </summary>
Task<IAccount> DoLogin(string p_strMail, string p_strPassword, string p_strDeviceId);
/// <summary> Logs user out. </summary>
Task DoLogout();
/// <summary> Request to reserve a bike.</summary>
/// <param name="p_oBike">Bike to book.</param>
Task DoReserve(Bikes.Bike.BC.IBikeInfoMutable p_oBike);
/// <summary> Request to cancel a reservation.</summary>
/// <param name="p_oBike">Bike to book.</param>
Task DoCancelReservation(Bikes.Bike.BC.IBikeInfoMutable p_oBike);
/// <summary> Get authentication keys to connect to lock.</summary>
/// <param name="bike">Bike to book.</param>
Task CalculateAuthKeys(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike);
/// <summary> Updates COPRI lock state for a booked bike. </summary>
/// <param name="bikeId">Id of the bike to update locking state for.</param>
/// <param name="location">Geolocation of lock when returning bike.</param>
/// <returns>Response on updating locking state.</returns>
Task UpdateLockingStateAsync(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto location = null);
/// <summary> Request to book a bike.</summary>
/// <param name="bike">Bike to book.</param>
Task DoBook(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike);
/// <summary> Request to return a bike.</summary>
/// <param name="location">Geolocation of lock when returning bike.</param>
/// <param name="bike">Bike to return.</param>
Task DoReturn(Bikes.Bike.BluetoothLock.IBikeInfoMutable bike, LocationDto geolocation = null);
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
bool IsConnected { get; }
/// <summary> True if user is logged in false if not. </summary>
string SessionCookie { get; }
Task DoSubmitFeedback(IUserFeedback userFeedback, Uri opertorUri);
/// <summary>
/// Feedback given by user when returning bike.
/// </summary>
public interface IUserFeedback
{
/// <summary>
/// Holds whether bike is broken or not.
/// </summary>
bool IsBikeBroken { get; }
/// <summary>
/// Holds either
/// - general feedback
/// - error description of broken bike
/// or both.
/// </summary>
string Message { get; }
}
}
/// <summary>Defines delegate to be raised whenever login state changes.</summary>
/// <param name="p_oEventArgs">Holds session cookie and mail address if user logged in successfully.</param>
public delegate void LoginStateChangedEventHandler(object p_oSender, LoginStateChangedEventArgs p_oEventArgs);
/// <summary> Event arguments to notify about changes of logged in state.</summary>
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; }
}
}

View file

@ -0,0 +1,9 @@

namespace TINK.Model.Connector
{
public record UserFeedbackDto : ICommand.IUserFeedback
{
public bool IsBikeBroken { get; init; }
public string Message { get; init; }
}
}

View file

@ -0,0 +1,57 @@
using System;
using TINK.Model.Services.CopriApi;
using TINK.Model.Repository;
namespace TINK.Model.Connector
{
/// <summary>
/// Connects tink app to copri by getting data from copri and updating tink app model (i.e. bikes, user, ...)
/// </summary>
public class Connector : IConnector
{
/// <summary>Constructs a copri connector object.</summary>
/// <param name="activeUri"> Uri to connect to.</param>
/// <param name="userAgent">Holds the name and version of the TINKApp.</param>
/// /// <param name="sessionCookie"> Holds the session cookie.</param>
/// <param name="p_strMail">Mail of user.</param>
/// <param name="expiresAfter">Timespan which holds value after which cache expires.</param>
/// <param name="server"> Provides cached addess to copri.</param>
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);
}
/// <summary> Object for queriying stations and bikes.</summary>
public ICommand Command { get; private set; }
/// <summary> Object for queriying stations and bikes.</summary>
public IQuery Query { get; private set; }
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
public bool IsConnected => Command.IsConnected;
/// <summary> Gets a command object to perform copri commands. </summary>
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;
/// <summary> Gets a command object to perform copri queries. </summary>
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);
}
}

View file

@ -0,0 +1,47 @@
using System;
using TINK.Model.Services.CopriApi;
using TINK.Model.Repository;
namespace TINK.Model.Connector
{
/// <summary>
/// Connects tink app to copri by getting data from copri and updating tink app model (i.e. bikes, user, ...)
/// </summary>
public class ConnectorCache : IConnector
{
/// <summary>Constructs a copri connector object.</summary>
/// <param name="p_strSessionCookie"> Holds the session cookie.</param>
/// <param name="p_strMail">Mail of user.</param>
/// <param name="server"> Provides addess to copri.</param>
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);
}
/// <summary> Object for queriying stations and bikes.</summary>
public ICommand Command { get; private set; }
/// <summary> Object for queriying stations and bikes.</summary>
public IQuery Query { get; private set; }
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
public bool IsConnected => Command.IsConnected;
/// <summary> Gets a command object to perform copri queries. </summary>
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);
}
}

View file

@ -0,0 +1,19 @@
using System;
namespace TINK.Model.Connector
{
public class ConnectorFactory
{
/// <summary>
/// Gets a connector object depending on whether beein onlin or offline.
/// </summary>
/// <param name="isConnected">True if online, false if offline</param>
/// <returns></returns>
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);
}
}
}

View file

@ -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<string> group)
{
return group != null ? (IGroupFilter) new IntersectGroupFilter(group) : new NullGroupFilter();
}
}
}

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace TINK.Model.Connector.Filter
{
public interface IGroupFilter
{
IEnumerable<string> DoFilter(IEnumerable<string> filter);
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
using System.Linq;
namespace TINK.Model.Connector.Filter
{
/// <summary> Filters to enumerations of string by intersecting.</summary>
public class IntersectGroupFilter : IGroupFilter
{
private IEnumerable<string> Group { get; set; }
public IntersectGroupFilter(IEnumerable<string> group) => Group = group ?? new List<string>();
/// <summary> Applies filtering. </summary>
/// <param name="filter">Enumeration of filter values to filter with or null if no filtering has to be applied.</param>
/// <returns></returns>
public IEnumerable<string> DoFilter(IEnumerable<string> filter) => filter != null
? Group.Intersect(filter)
: Group;
}
}

View file

@ -0,0 +1,9 @@
using System.Collections.Generic;
namespace TINK.Model.Connector.Filter
{
public class NullGroupFilter : IGroupFilter
{
public IEnumerable<string> DoFilter(IEnumerable<string> filter) => filter;
}
}

View file

@ -0,0 +1,11 @@
namespace TINK.Model.Connector
{
public static class FilterHelper
{
/// <summary> Holds the Konrad group (city bikes).</summary>
public const string FILTERKONRAD = "Konrad";
/// <summary> Holds the tink group (Lastenräder).</summary>
public const string FILTERTINKGENERAL = "TINK";
}
}

View file

@ -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
{
/// <summary> Filters connector respones.</summary>
/// <remarks>Former name: Filter</remarks>
public class FilteredConnector : IFilteredConnector
{
/// <summary> Constructs a filter object. </summary>
/// <param name="group">Filter group.</param>
/// <param name="connector">Connector object.</param>
public FilteredConnector(
IEnumerable<string> 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));
}
/// <summary> Inner connector object.</summary>
public IConnector Connector { get; }
/// <summary> Command object. </summary>
public ICommand Command => Connector.Command;
/// <summary> Object to query information. </summary>
public IQuery Query { get; }
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
public bool IsConnected => Connector.IsConnected;
/// <summary> Object to perform filtered queries.</summary>
private class QueryProvider : IQuery
{
/// <summary> Holds the filter. </summary>
private IGroupFilter Filter { get; }
/// <summary> Holds the reference to object which performs copry queries.</summary>
private IQuery m_oInnerQuery;
/// <summary> Constructs a query object.</summary>
/// <param name="innerQuerry"></param>
/// <param name="filter"></param>
public QueryProvider(IQuery innerQuerry, IGroupFilter filter)
{
m_oInnerQuery = innerQuerry;
Filter = filter;
}
/// <summary> Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. </summary>
public async Task<Result<BikeCollection>> GetBikesAsync()
{
var result = await m_oInnerQuery.GetBikesAsync();
return new Result<BikeCollection>(
result.Source,
new BikeCollection(DoFilter(result.Response, Filter)),
result.Exception);
}
/// <summary> Gets bikes occupied if a user is logged in. </summary>
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
{
var result = await m_oInnerQuery.GetBikesOccupiedAsync();
return new Result<BikeCollection>(
result.Source,
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
result.Exception);
}
/// <summary> Gets all station applying filter rules. </summary>
/// <returns></returns>
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
{
var result = await m_oInnerQuery.GetBikesAndStationsAsync();
return new Result<StationsAndBikesContainer>(
result.Source,
new StationsAndBikesContainer(
new StationDictionary(result.Response.StationsAll.CopriVersion, DoFilter(result.Response.StationsAll, Filter)),
new BikeCollection(DoFilter(result.Response.Bikes, Filter))),
result.Exception);
}
/// <summary> Filter bikes by group. </summary>
/// <param name="p_oBikes">Bikes to filter.</param>
/// <returns>Filtered bikes.</returns>
private static Dictionary<int, BikeInfo> DoFilter(BikeCollection p_oBikes, IGroupFilter filter)
{
return p_oBikes.Where(x => filter.DoFilter(x.Group).Count() > 0).ToDictionary(x => x.Id);
}
/// <summary> Filter stations by broup. </summary>
/// <returns></returns>
private static Dictionary<int, Station.Station> DoFilter(StationDictionary p_oStations, IGroupFilter filter)
{
return p_oStations.Where(x => filter.DoFilter(x.Group).Count() > 0).ToDictionary((x => x.Id));
}
}
}
}

View file

@ -0,0 +1,16 @@
using System.Collections.Generic;
namespace TINK.Model.Connector
{
public static class FilteredConnectorFactory
{
/// <summary> Creates a filter object. </summary>
/// <param name="group"></param>
public static IFilteredConnector Create(IEnumerable<string> group, IConnector connector)
{
return group != null
? (IFilteredConnector) new FilteredConnector(group, connector)
: new NullFilterConnector(connector);
}
}
}

View file

@ -0,0 +1,14 @@
namespace TINK.Model.Connector
{
public interface IConnector
{
/// <summary> Object for queriying stations and bikes.</summary>
ICommand Command { get; }
/// <summary> Object for queriying stations and bikes.</summary>
IQuery Query { get; }
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
bool IsConnected { get; }
}
}

View file

@ -0,0 +1,7 @@
namespace TINK.Model.Connector
{
public interface IFilteredConnector : IConnector
{
IConnector Connector { get; }
}
}

View file

@ -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
{
/// <summary> Filters connector respones.</summary>
public class NullFilterConnector : IFilteredConnector
{
/// <summary> Constructs a filter object. </summary>
/// <param name="p_oGroup">Filter group.</param>
/// <param name="connector">Connector object.</param>
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);
}
/// <summary> Inner connector object.</summary>
public IConnector Connector { get; }
/// <summary> Command object. </summary>
public ICommand Command => Connector.Command;
/// <summary> Object to query information. </summary>
public IQuery Query { get; }
/// <summary> True if connector has access to copri server, false if cached values are used. </summary>
public bool IsConnected => Connector.IsConnected;
/// <summary> Object to perform filtered queries.</summary>
private class QueryProvider : IQuery
{
/// <summary> Holds the reference to object which performs copry queries.</summary>
private IQuery m_oInnerQuery;
/// <summary> Constructs a query object.</summary>
/// <param name="p_oInnerQuery"></param>
/// <param name="p_oFilter"></param>
public QueryProvider(IQuery p_oInnerQuery)
{
m_oInnerQuery = p_oInnerQuery;
}
/// <summary> Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. </summary>
public async Task<Result<BikeCollection>> GetBikesAsync()
{
var result = await m_oInnerQuery.GetBikesAsync();
return new Result<BikeCollection>(
result.Source,
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
result.Exception);
}
/// <summary> Gets bikes occupied if a user is logged in. </summary>
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
{
var result = await m_oInnerQuery.GetBikesOccupiedAsync();
return new Result<BikeCollection>(
result.Source,
new BikeCollection(result.Response.ToDictionary(x => x.Id)),
result.Exception);
}
/// <summary> Gets all station applying filter rules. </summary>
/// <returns></returns>
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
{
var result = await m_oInnerQuery.GetBikesAndStationsAsync();
return new Result<StationsAndBikesContainer>(
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);
}
/// <summary> Filter bikes by group. </summary>
/// <param name="p_oBikes">Bikes to filter.</param>
/// <returns>Filtered bikes.</returns>
public static Dictionary<int, BikeInfo> DoFilter(BikeCollection p_oBikes, IEnumerable<string> p_oFilter)
{
return p_oBikes.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary(x => x.Id);
}
/// <summary> Filter stations by broup. </summary>
/// <returns></returns>
public static Dictionary<int, Station.Station> DoFilter(StationDictionary p_oStations, IEnumerable<string> p_oFilter)
{
return p_oStations.Where(x => x.Group.Intersect(p_oFilter).Count() > 0).ToDictionary((x => x.Id));
}
}
}
}

View file

@ -0,0 +1,27 @@
using System;
using TINK.Model.Repository;
namespace TINK.Model.Connector
{
/// <summary>
/// Provides information required for copri commands/ query operations.
/// </summary>
public class Base
{
/// <summary> Reference to object which provides access to copri server. </summary>
protected ICopriServerBase CopriServer { get; }
/// <summary> Gets the merchant id.</summary>
protected string MerchantId => CopriServer.MerchantId;
/// <summary> Constructs a query base object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
/// <param name="p_oErrorStack">Object which hold communication objects.</param>
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.");
}
}
}

View file

@ -0,0 +1,39 @@
using System;
using TINK.Model.Repository;
namespace TINK.Model.Connector
{
/// <summary>Holds user infromation required for copri related commands/ query operations. </summary>
public class BaseLoggedIn : Base
{
/// <summary>Session cookie used to sign in to copri.</summary>
public string SessionCookie { get; }
/// <summary> Mail address of the user. </summary>
protected string Mail { get; }
/// <summary> Object which provides date time info. </summary>
protected readonly Func<DateTime> DateTimeProvider;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
public BaseLoggedIn(ICopriServerBase p_oCopriServer,
string p_strSessionCookie,
string p_strMail,
Func<DateTime> 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;
}
}
}

View file

@ -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
{
/// <summary> Cached copri server. </summary>
private readonly ICachedCopriServer server;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
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()}.");
}
}
/// <summary> Gets all stations including postions and bikes.</summary>
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
{
var resultStations = await server.GetStations();
if (resultStations.Source == typeof(CopriCallsMonkeyStore))
{
// Communication with copri in order to get stations failed.
return new Result<StationsAndBikesContainer>(
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<StationsAndBikesContainer>(
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<StationsAndBikesContainer>(
resultStations.Source,
new StationsAndBikesContainer(resultStations.Response.GetStationsAllMutable(), resultBikes.Response.GetBikesAvailable()));
}
/// <summary> Gets bikes occupied. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
{
Log.ForContext<CachedQuery>().Error("Unexpected call to get be bikes occpied detected. No user is logged in.");
return new Result<BikeCollection>(
typeof(CopriCallsMonkeyStore),
await Task.Run(() => new BikeCollection(new Dictionary<int, BikeInfo>())),
new System.Exception("Abfrage der reservierten/ gebuchten Räder nicht möglich. Kein Benutzer angemeldet."));
}
/// <summary> Gets bikes available. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> GetBikesAsync()
{
var result = await server.GetBikesAvailable();
server.AddToCache(result);
return new Result<BikeCollection>(result.Source, result.Response.GetBikesAvailable(), result.Exception);
}
}
}

View file

@ -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
{
/// <summary> Provides query functionality for a logged in user. </summary>
public class CachedQueryLoggedIn : BaseLoggedIn, IQuery
{
/// <summary> Cached copri server. </summary>
private readonly ICachedCopriServer server;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
public CachedQueryLoggedIn(ICopriServerBase p_oCopriServer,
string p_strSessionCookie,
string p_strMail,
Func<DateTime> 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()}.");
}
}
/// <summary> Gets all stations including postions.</summary>
public async Task<Result<StationsAndBikesContainer>> 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<StationsAndBikesContainer>(
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<StationsAndBikesContainer>(
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<StationsAndBikesContainer>(
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<StationsAndBikesContainer>(
resultStations.Source,
new StationsAndBikesContainer(
resultStations.Response.GetStationsAllMutable(),
UpdaterJSON.GetBikesAll(
l_oBikesAvailableResponse.Response,
l_oBikesOccupiedResponse.Response,
Mail,
DateTimeProvider)),
exceptions.Length > 0 ? new AggregateException(exceptions) : null);
}
/// <summary> Gets bikes occupied. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
{
var result = await server.GetBikesOccupied();
server.AddToCache(result);
return new Result<BikeCollection>(result.Source, result.Response.GetBikesOccupied(Mail, DateTimeProvider), result.Exception);
}
/// <summary> Gets bikes available and bikes occupied. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> 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<BikeCollection>(
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<BikeCollection>(
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<BikeCollection>(
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);
}
}
}

View file

@ -0,0 +1,20 @@
using System.Threading.Tasks;
using TINK.Model.Bike;
using TINK.Model.Services.CopriApi;
namespace TINK.Model.Connector
{
public interface IQuery
{
/// <summary> Gets all stations including postions.</summary>
Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync();
/// <summary> Gets bikes occupied is a user is logged in. </summary>
/// <returns>Collection of bikes.</returns>
Task<Result<BikeCollection>> GetBikesOccupiedAsync();
/// <summary> Gets bikes either bikes available if no user is logged in or bikes available and bikes occupied if a user is logged in. </summary>
/// <returns>Collection of bikes.</returns>
Task<Result<BikeCollection>> GetBikesAsync();
}
}

View file

@ -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
{
/// <summary> Provides query functionality without login. </summary>
public class Query : Base, IQuery
{
/// <summary> Cached copri server. </summary>
private readonly ICopriServer server;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
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()}.");
}
}
/// <summary> Gets all stations including postions.</summary>
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
{
var stationsAllResponse = await server.GetStationsAsync();
var bikesAvailableResponse = await server.GetBikesAvailableAsync();
return new Result<StationsAndBikesContainer>(
typeof(CopriCallsMonkeyStore),
new StationsAndBikesContainer( stationsAllResponse.GetStationsAllMutable(), bikesAvailableResponse.GetBikesAvailable()));
}
/// <summary> Gets bikes occupied. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
{
Log.ForContext<Query>().Error("Unexpected call to get be bikes occpied detected. No user is logged in.");
return new Result<BikeCollection>(
typeof(CopriCallsMonkeyStore),
await Task.Run(() => new BikeCollection(new Dictionary<int, BikeInfo>())),
new System.Exception("Abfrage der reservierten/ gebuchten Räder fehlgeschlagen. Kein Benutzer angemeldet."));
}
/// <summary> Gets bikes occupied. </summary>
/// <returns> Collection of bikes. </returns>
public async Task<Result<BikeCollection>> GetBikesAsync()
{
return new Result<BikeCollection>(
typeof(CopriCallsMonkeyStore),
(await server.GetBikesAvailableAsync()).GetBikesAvailable());
}
}
}

View file

@ -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
{
/// <summary> Provides query functionality for a logged in user. </summary>
public class QueryLoggedIn : BaseLoggedIn, IQuery
{
/// <summary> Cached copri server. </summary>
private readonly ICopriServer server;
/// <summary>Constructs a copri query object.</summary>
/// <param name="p_oCopriServer">Server which implements communication.</param>
public QueryLoggedIn(ICopriServerBase p_oCopriServer,
string p_strSessionCookie,
string p_strMail,
Func<DateTime> 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;
}
/// <summary> Gets all stations including postions.</summary>
public async Task<Result<StationsAndBikesContainer>> GetBikesAndStationsAsync()
{
var stationResponse = await server.GetStationsAsync();
var bikesAvailableResponse = await server.GetBikesAvailableAsync();
var bikesOccupiedResponse = await server.GetBikesOccupiedAsync();
return new Result<StationsAndBikesContainer>(
typeof(CopriCallsMonkeyStore),
new StationsAndBikesContainer(
stationResponse.GetStationsAllMutable(),
UpdaterJSON.GetBikesAll(bikesAvailableResponse, bikesOccupiedResponse, Mail, DateTimeProvider)));
}
/// <summary> Gets bikes occupied. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> GetBikesOccupiedAsync()
{
return new Result<BikeCollection>(
typeof(CopriCallsMonkeyStore),
(await server.GetBikesOccupiedAsync()).GetBikesOccupied(Mail, DateTimeProvider));
}
/// <summary> Gets bikes available and bikes occupied. </summary>
/// <returns>Collection of bikes.</returns>
public async Task<Result<BikeCollection>> GetBikesAsync()
{
var l_oBikesAvailableResponse = await server.GetBikesAvailableAsync();
var l_oBikesOccupiedResponse = await server.GetBikesOccupiedAsync();
return new Result<BikeCollection>(
typeof(CopriCallsMonkeyStore),
UpdaterJSON.GetBikesAll(l_oBikesAvailableResponse, l_oBikesOccupiedResponse, Mail, DateTimeProvider));
}
}
}

View file

@ -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
{
/// <summary>
/// Conversion helper functionality.
/// </summary>
public static class TextToTypeHelper
{
/// <summary> Holds the text for demo bikes. </summary>
private const string DEMOBIKEMARKER = "DEMO";
/// <summary> Part text denoting two wheel cargo bike.. </summary>
private const string TWOWHEELCARGOMARKERFRAGMENT = "LONG";
/// <summary>
/// Gets the position from StationInfo object.
/// </summary>
/// <param name="p_oStationInfo">Object to get information from.</param>
/// <returns>Position information.</returns>
public static Station.Position GetPosition(this StationsAllResponse.StationInfo p_oStationInfo)
{
return GetPosition(p_oStationInfo.gps);
}
/// <summary> Gets the position from StationInfo object. </summary>
/// <param name="p_oAuthorizationResponse">Object to get information from.</param>
/// <returns>Position information.</returns>
public static IEnumerable<string> 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);
}
}
/// <summary> Gets the position from StationInfo object. </summary>
/// <param name="p_oAuthorizationResponse">Object to get information from.</param>
/// <returns>Position information.</returns>
public static IEnumerable<string> 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<string>(p_oGroup.Split(',')).ToList();
}
/// <summary> Gets the position from StationInfo object. </summary>
/// <param name="p_oAuthorizationResponse">Object to get information from.</param>
/// <returns>Position information.</returns>
public static string GetGroup(this IEnumerable<string> p_oGroup)
{
return string.Join(",", p_oGroup);
}
/// <summary> Gets the position from StationInfo object. </summary>
/// <param name="p_oStationInfo">Object to get information from.</param>
/// <returns>Position information.</returns>
public static IEnumerable<string> 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);
}
}
/// <summary>
/// Gets the position from StationInfo object.
/// </summary>
/// <param name="p_oBikeInfo">Object to get information from.</param>
/// <returns>Position information.</returns>
public static Station.Position GetPosition(this BikeInfoAvailable p_oBikeInfo)
{
return GetPosition(p_oBikeInfo.gps);
}
/// <summary>
/// Gets the position from StationInfo object.
/// </summary>
/// <param name="p_oBikeInfo">Object to get information from.</param>
/// <returns>Position information.</returns>
public static InUseStateEnum GetState(this BikeInfoBase p_oBikeInfo)
{
var l_oState = p_oBikeInfo.state;
if (string.IsNullOrEmpty(l_oState))
{
throw new InvalidResponseException<BikeInfoBase>(
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));
}
/// <summary>
/// Gets the from date information from JSON.
/// </summary>
/// <param name="p_oBikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
public static DateTime GetFrom(this BikeInfoReservedOrBooked p_oBikeInfo)
{
return DateTime.Parse(p_oBikeInfo.start_time);
}
/// <summary>
/// Gets whether the bike is a trike or not.
/// </summary>
/// <param name="p_oBikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
public static bool? GetIsDemo(this BikeInfoBase p_oBikeInfo)
{
return p_oBikeInfo?.description != null
? p_oBikeInfo.description.ToUpper().Contains(DEMOBIKEMARKER)
: (bool?) null;
}
/// <summary>
/// Gets whether the bike is a trike or not.
/// </summary>
/// <param name="p_oBikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
public static IEnumerable<string> GetGroup(this BikeInfoBase p_oBikeInfo)
{
try
{
return p_oBikeInfo?.bike_group?.GetGroup()?.ToList() ?? new List<string>();
}
catch (System.Exception l_oException)
{
throw new System.Exception($"Can not get group of user from text \"{p_oBikeInfo.bike_group}\".", l_oException);
}
}
/// <summary> Gets whether the bike has a bord computer or not. </summary>
/// <param name="p_oBikeInfo">JSON to get information from.</param>
/// <returns>From information.</returns>
public static bool GetIsManualLockBike(this BikeInfoBase p_oBikeInfo)
{
return !string.IsNullOrEmpty(p_oBikeInfo.system)
&& p_oBikeInfo.system.ToUpper().StartsWith("LOCK");
}
/// <summary> Gets whether the bike has a bord computer or not. </summary>
/// <param name="p_oBikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
public static bool GetIsBluetoothLockBike(this BikeInfoBase p_oBikeInfo)
{
return !string.IsNullOrEmpty(p_oBikeInfo.system)
&& p_oBikeInfo.system.ToUpper().StartsWith("ILOCKIT");
}
/// <summary> Gets whether the bike has a bord computer or not. </summary>
/// <param name="bikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
public static int GetBluetoothLockId(this BikeInfoAvailable bikeInfo)
{
return TextToLockItTypeHelper.GetBluetoothLockId(bikeInfo?.Ilockit_ID);
}
/// <summary> Gets whether the bike has a bord computer or not. </summary>
/// <param name="bikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
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);
}
/// <summary>
/// Get array of keys from string of format "[12, -9, 5]"
/// </summary>
/// <param name="keyArrayText"></param>
/// <returns></returns>
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];
}
}
/// <summary>
/// Gets whether the bike is a trike or not.
/// </summary>
/// <param name="bikeInfo">JSON to get information from..</param>
/// <returns>From information.</returns>
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;
}
/// <summary>
/// Gets the type of bike.
/// </summary>
/// <param name="bikeInfo">Object to get bike type from.</param>
/// <returns>Type of bike.</returns>
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;
}
/// <summary>
/// Get position from a ,- separated string.
/// </summary>
/// <param name="p_strGps">Text to extract positon from.</param>
/// <returns>Position object.</returns>
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);
}
/// <summary> Gets text of bike from. </summary>
/// <param name="p_eType">Type to get text for.</param>
/// <returns></returns>
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;
}
}
}

View file

@ -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
{
/// <summary>
/// Connects TINK app to copri using JSON as input data format.
/// </summary>
/// <todo>Rename to UpdateFromCopri.</todo>
public static class UpdaterJSON
{
/// <summary> Loads a bike object from copri server cancel reservation/ booking update request.</summary>
/// <param name="bike">Bike object to load response into.</param>
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
public static void Load(
this IBikeInfoMutable bike,
Bikes.Bike.BC.NotifyPropertyChangedLevel notifyLevel)
{
bike.State.Load(InUseStateEnum.Disposable, notifyLevel: notifyLevel);
}
/// <summary>
/// Gets all statsion for station provider and add them into station list.
/// </summary>
/// <param name="p_oStationList">List of stations to update.</param>
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<StationsAllResponse>(
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;
}
/// <summary> Gets account object from login response.</summary>
/// <param name="merchantId">Needed to extract cookie from autorization response.</param>
/// <param name="loginResponse">Response to get session cookie and debug level from.</param>
/// <param name="mail">Mail address needed to construct a complete account object (is not part of response).</param>
/// <param name="password">Password needed to construct a complete account object (is not part of response).</param>
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) ;
}
/// <summary> Load bike object from booking response. </summary>
/// <param name="bike">Bike object to load from response.</param>
/// <param name="bikeInfo">Booking response.</param>
/// <param name="mailAddress">Mail address of user which books bike.</param>
/// <param name="p_strSessionCookie">Session cookie of user which books bike.</param>
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
public static void Load(
this IBikeInfoMutable bike,
BikeInfoReservedOrBooked bikeInfo,
string mailAddress,
Func<DateTime> 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));
}
}
/// <summary> Gets bikes available from copri server response.</summary>
/// <param name="p_oBikesAvailableResponse">Response to create collection from.</param>
/// <returns>New collection of available bikes.</returns>
public static BikeCollection GetBikesAvailable(
this BikesAvailableResponse p_oBikesAvailableResponse)
{
return GetBikesAll(
p_oBikesAvailableResponse,
new BikesReservedOccupiedResponse(), // There are no occupied bikes.
string.Empty,
() => DateTime.Now);
}
/// <summary> Gets bikes occupied from copri server response. </summary>
/// <param name="p_oBikesAvailable">Response to create bikes from.</param>
/// <returns>New collection of occupied bikes.</returns>
public static BikeCollection GetBikesOccupied(
this BikesReservedOccupiedResponse p_oBikesOccupiedResponse,
string p_strMail,
Func<DateTime> p_oDateTimeProvider)
{
return GetBikesAll(
new BikesAvailableResponse(),
p_oBikesOccupiedResponse,
p_strMail,
p_oDateTimeProvider);
}
/// <summary> Gets bikes occupied from copri server response. </summary>
/// <param name="p_oBikesAvailable">Response to create bikes from.</param>
/// <returns>New collection of occupied bikes.</returns>
public static BikeCollection GetBikesAll(
BikesAvailableResponse p_oBikesAvailableResponse,
BikesReservedOccupiedResponse p_oBikesOccupiedResponse,
string p_strMail,
Func<DateTime> p_oDateTimeProvider)
{
var l_oBikesDictionary = new Dictionary<int, BikeInfo>();
var l_oDuplicates = new Dictionary<int, BikeInfo>();
// 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);
}
}
/// <summary>
/// Constructs bike info instances/ bike info derived instances.
/// </summary>
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;
}
}
/// <summary> Creates a bike info object from copri response. </summary>
/// <param name="bikeInfo">Copri response. </param>
/// <param name="mailAddress">Mail address of user.</param>
/// <param name="dateTimeProvider">Date and time provider function.</param>
/// <returns></returns>
public static BikeInfo Create(
BikeInfoReservedOrBooked bikeInfo,
string mailAddress,
Func<DateTime> 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,
};
}
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace TINK.Model.Device
{
/// <summary> Interface to get version info. </summary>
public interface IAppInfo
{
/// <summary> Gets the app version to display to user.</summary>
Version Version { get; }
/// <summary> Gets the URL to the app store. </summary>
/// <value>The store URL.</value>
string StoreUrl { get; }
}
}

View file

@ -0,0 +1,9 @@
namespace TINK.Model.Device
{
public interface IDevice
{
/// <summary> Gets unitque device identifier. </summary>
/// <returns>Gets the identifies specifying device.</returns>
string GetIdentifier();
}
}

View file

@ -0,0 +1,9 @@
namespace TINK.Model.Device
{
public interface IExternalBrowserService
{
/// <summary> Opens an external browser. </summary>
/// <param name="url">Url to open.</param>
void OpenUrl(string url);
}
}

View file

@ -0,0 +1,7 @@
namespace TINK.Model.Device
{
public interface IGeolodationDependent
{
bool IsGeolcationEnabled { get; }
}
}

View file

@ -0,0 +1,15 @@
namespace TINK.Model.Device
{
public interface ISpecialFolder
{
/// <summary>
/// Get the folder name of external folder to write to.
/// </summary>
/// <returns>External directory.</returns>
string GetExternalFilesDir();
/// <summary> Gets the folder name of the personal data folder dir on internal storage. </summary>
/// <returns>Directory name.</returns>
string GetInternalPersonalDir();
}
}

View file

@ -0,0 +1,8 @@
namespace TINK.Model.Device
{
public interface IWebView
{
/// <summary> Clears the cookie cache for all web views. </summary>
void ClearCookies();
}
}

View file

@ -0,0 +1,11 @@
using System;
namespace TINK.Model
{
/// <summary> Operation fired when a file operation fails.</summary>
public class FileOperationException : Exception
{
public FileOperationException(string message, Exception innerException) : base(message, innerException)
{ }
}
}

View file

@ -0,0 +1,14 @@
using Newtonsoft.Json;
using System.Collections.Generic;
using System.Linq;
namespace TINK.Model
{
public static class FilterCollectionStore
{
/// <summary> Writes filter collection state to string. </summary>
/// <returns> Filter collection to write to string.</returns>
/// <param name="p_oDictionary">P o dictionary.</param>
public static string ToString(this IDictionary<string, FilterState> p_oDictionary) => "{" + string.Join(", ", p_oDictionary.Select(x => "(" + x.Key + "= " + x.Value + ")")) + "}";
}
}

View file

@ -0,0 +1,45 @@
using System.Collections.Generic;
using TINK.Model.Connector;
using TINK.ViewModel.Map;
using TINK.ViewModel.Settings;
namespace TINK.Model
{
/// <summary> Holds collecion of filters to filter options (TINK, Konrad, ....). </summary>
/// <remarks> Former name: FilterCollection.</remarks>
public static class GroupFilterHelper
{
/// <summary> Gets default filter set.</summary>
public static IGroupFilterSettings GetSettingsFilterDefaults
{
get
{
return new GroupFilterSettings(new Dictionary<string, FilterState> {
{ FilterHelper.FILTERTINKGENERAL, FilterState.On },
{FilterHelper.FILTERKONRAD, FilterState.On }
});
}
}
/// <summary> Gets default filter set.</summary>
public static IGroupFilterMapPage GetMapPageFilterDefaults
{
get
{
return new ViewModel.Map.GroupFilterMapPage(new Dictionary<string, FilterState> {
{ FilterHelper.FILTERTINKGENERAL, FilterState.On },
{FilterHelper.FILTERKONRAD, FilterState.Off }
});
}
}
}
/// <summary> Holds value whether filter (on TINK, Konrad, ....) is on or off. </summary>
public enum FilterState
{
/// <summary> Option (TINK, Konrad, ....) is available.</summary>
On,
/// <summary> Option is off</summary>
Off,
}
}

98
TINKLib/Model/ITinkApp.cs Normal file
View file

@ -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
{
/// <summary> Update connector from depending on whether user is logged in or not.</summary>
void UpdateConnector();
/// <summary> Saves object to file. </summary>
void Save();
/// <summary> Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. </summary>
IGroupFilterMapPage GroupFilterMapPage { get; set; }
/// <summary> Holds the user of the app. </summary>
User.User ActiveUser { get; }
/// <summary>Sets flag whats new page was already shown to true. </summary>
void SetWhatsNewWasShown();
/// <summary> Holds the system to copri.</summary>
IFilteredConnector GetConnector(bool isConnected);
/// <summary> Name of the station which is selected. </summary>
int? SelectedStation { get; set; }
/// <summary>Polling periode.</summary>
PollingParameters Polling { get; set; }
TimeSpan ExpiresAfter { get; set; }
/// <summary> Holds status about whants new page. </summary>
WhatsNew WhatsNew { get; }
/// <summary> Gets whether device is connected to internet or not. </summary>
bool GetIsConnected();
/// <summary> Action to post to GUI thread.</summary>
Action<SendOrPostCallback, object> PostAction { get; }
/// <summary> Holds the uri which is applied after restart. </summary>
Uri NextActiveUri { get; set; }
/// <summary> Holds the filters loaded from settings. </summary>
IGroupFilterSettings FilterGroupSetting { get; set; }
/// <summary> Value indicating whether map is centerted to current position or not. </summary>
bool CenterMapToCurrentLocation { get; set; }
bool LogToExternalFolder { get; set; }
bool IsSiteCachingOn { get; set; }
/// <summary> Gets the minimum logging level. </summary>
LogEventLevel MinimumLogEventLevel { get; set; }
/// <summary> Updates logging level. </summary>
/// <param name="p_oNewLevel">New level to set.</param>
void UpdateLoggingLevel(LogEventLevel p_oNewLevel);
/// <summary>Holds uris of copri servers. </summary>
CopriServerUriList Uris { get; }
/// <summary> Holds the different lock service implementations.</summary>
LocksServicesContainerMutable LocksServices { get; }
/// <summary> Holds the different geo location service implementations.</summary>
ServicesContainerMutable<IGeolocation> GeolocationServices { get; }
/// <summary> Holds available app themes.</summary>
ServicesContainerMutable<object> Themes { get; }
/// <summary> Reference of object which provides device information. </summary>
IDevice Device { get; }
/// <summary> Os permission.</summary>
IPermissions Permissions { get; }
/// <summary> Holds the folder where settings files are stored. </summary>
string SettingsFileFolder { get; }
/// <summary> Holds the external path. </summary>
string ExternalFolder { get; }
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace TINK.Model.Logging
{
public class EmptyDirectoryLoggingManger : ILoggingDirectoryManager
{
public void DeleteObsoleteLogs() { }
/// <summary> Gets the log file name. </summary>
public string LogFileName { get { return string.Empty; } }
/// <summary> Gets all log files in logging directory. </summary>
/// <returns>List of log files.</returns>
public IList<string> GetLogFiles() { return new List<string>(); }
/// <summary> Gets path where log files are located. </summary>
/// <returns>Path to log files.</returns>
public string LogFilePath { get { return string.Empty; } }
}
}

View file

@ -0,0 +1,21 @@
using System.Collections.Generic;
namespace TINK.Model.Logging
{
public interface ILoggingDirectoryManager
{
/// <summary> Deletes files which are out of retainment scope. </summary>
void DeleteObsoleteLogs();
/// <summary> Gets the log file name. </summary>
string LogFileName { get; }
/// <summary> Gets all log files in logging directory. </summary>
/// <returns>List of log files.</returns>
IList<string> GetLogFiles();
/// <summary> Gets path where log files are located. </summary>
/// <returns>Path to log files.</returns>
string LogFilePath { get; }
}
}

View file

@ -0,0 +1,35 @@
using Serilog;
using System;
using TINK.Model.Repository.Exception;
namespace TINK.Model.Logging
{
public static class LogEntryClassifyHelper
{
/// <summary> Classifies exception and logs information or error depending on result of classification. </summary>
/// <typeparam name="T0">Type of first message parameter.</typeparam>
/// <typeparam name="T1">Type of second message parameter.</typeparam>
/// <param name="p_oLogger">Object to use for logging.</param>
/// <param name="messageTemplate">Templated used to output message.</param>
/// <param name="propertyValue0">First message parameter.</param>
/// <param name="propertyValue1">Second message parameter.</param>
/// <param name="p_oException">Exception to classify.</param>
public static void InformationOrError<T0, T1>(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);
}
}
}

View file

@ -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
{
/// <summary> Holds new logging levels. </summary>
public enum RollingInterval
{
/// <summary> Create a new log file for each session (start of app).</summary>
Session,
}
/// <summary> Provides logging file name helper functionality.</summary>
public static class LoggerConfigurationHelper
{
/// <summary> Holds the log file name. </summary>
private static ILoggingDirectoryManager m_oDirectoryManager = new EmptyDirectoryLoggingManger();
/// <summary> Sets up logging to file.</summary>
/// <param name="p_oLoggerConfiguration">Object to set up logging with.</param>
/// <param name="p_oDevice">Object to get file informaton from.</param>
/// <param name="p_oRollingInterval">Specifies rolling type.</param>
/// <param name="p_iRetainedFilesCountLimit">Count of file being retained.</param>
/// <returns>Logger object.</returns>
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);
}
/// <summary> Gets all log files in logging directory. </summary>
/// <param name="p_oLogger"></param>
/// <returns>List of log files.</returns>
public static IList<string> 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<string>();
}
}
/// <summary> Gets path where log files are located. </summary>
/// <param name="p_oLogger"></param>
/// <returns>List of log files.</returns>
public static string GetLogFilePath(
this ILogger p_oLogger)
{
return m_oDirectoryManager.LogFilePath;
}
}
}

View file

@ -0,0 +1,147 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace TINK.Model.Logging
{
public class LoggingDirectoryManager : ILoggingDirectoryManager
{
/// <summary> Name of logging subdirectory.</summary>
private const string LOGDIRECTORYTITLE = "Log";
/// <summary> Prevents an invalid instance to be created. </summary>
private LoggingDirectoryManager() { }
public LoggingDirectoryManager(
Func<string, IList<string>> p_oFileListProvider,
Func<string, bool> p_oDirectoryExistsChecker,
Action<string> p_oDirectoryCreator,
Action<string> 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);
}
}
}
/// <summary> Deletes files which are out of retainment scope. </summary>
public void DeleteObsoleteLogs()
{
var l_oExceptions = new List<Exception>();
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());
}
/// <summary> Gets all log files in logging directory. </summary>
/// <param name="p_oLogger"></param>
/// <returns>List of log files.</returns>
public IList<string> 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);
}
}
/// <summary>Holds delegate to provide file names.</summary>
private readonly Func<string, IList<string>> m_oFileListProvider;
/// <summary>Holds delegate to delete files.</summary>
private readonly Action<string> m_oFileEraser;
/// <summary>Holds delegate to provide file names.</summary>
private int m_iRetainedFilesCountLimit;
/// <summary> Holds the log file name. </summary>
private string LogFileTitle { get; }
/// <summary> Holds the log file name. </summary>
public string LogFilePath { get; }
/// <summary> Holds the directory separator character. </summary>
private string DirectorySeparatorChar { get; }
/// <summary> Holds the log file name. </summary>
public string LogFileName { get { return $"{LogFilePath}{DirectorySeparatorChar}{LogFileTitle}"; } }
}
}

View file

@ -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<string, FilterState> filterDictionary = null)
{
FilterDictionary = filterDictionary ?? new Dictionary<string, FilterState>();
Filter = filterDictionary != null
? (IGroupFilter) new IntersectGroupFilter(FilterDictionary.Where(x => x.Value == FilterState.On).Select(x => x.Key))
: new NullGroupFilter();
}
private IDictionary<string, FilterState> FilterDictionary { get; set; }
private IGroupFilter Filter { get; }
/// <summary> Performs filtering on response-group. </summary>
public IEnumerable<string> DoFilter(IEnumerable<string> filter = null) => Filter.DoFilter(filter);
public FilterState this[string key] { get => FilterDictionary[key]; set => FilterDictionary[key] = value; }
public ICollection<string> Keys => FilterDictionary.Keys;
public ICollection<FilterState> 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<string, FilterState> item)
{
throw new System.NotImplementedException();
}
public void Clear()
{
throw new System.NotImplementedException();
}
public bool Contains(KeyValuePair<string, FilterState> item) => FilterDictionary.Contains(item);
public bool ContainsKey(string key) => FilterDictionary.ContainsKey(key);
public void CopyTo(KeyValuePair<string, FilterState>[] array, int arrayIndex) => FilterDictionary.CopyTo(array, arrayIndex);
public IEnumerator<KeyValuePair<string, FilterState>> GetEnumerator() => FilterDictionary.GetEnumerator();
public bool Remove(string key)
{
throw new System.NotImplementedException();
}
public bool Remove(KeyValuePair<string, FilterState> item)
{
throw new System.NotImplementedException();
}
public bool TryGetValue(string key, out FilterState value) => FilterDictionary.TryGetValue(key, out value);
IEnumerator IEnumerable.GetEnumerator() => FilterDictionary.GetEnumerator();
}
}

View file

@ -0,0 +1,11 @@
using System.Collections.Generic;
using TINK.Model;
namespace TINK.ViewModel.Settings
{
public interface IGroupFilterSettings : IDictionary<string, FilterState>
{
/// <summary> Performs filtering on response-group. </summary>
IEnumerable<string> DoFilter(IEnumerable<string> filter = null);
}
}

View file

@ -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
{
/// <summary>Title of the settings file.</summary>
public const string SETTINGSFILETITLE = "Setting.Json";
/// <summary> Key of the app version entry. </summary>
public const string APPVERIONKEY = "AppVersion";
/// <summary> Key of the app version entry. </summary>
public const string SHOWWHATSNEWKEY = "ShowWhatsNew";
/// <summary> Key of the app version entry. </summary>
public const string EXPIRESAFTER = "ExpiresAfter";
/// <summary> Key of the connect timeout. </summary>
public const string CONNECTTIMEOUT = "ConnectTimeout";
/// <summary> Key of the logging level entry. </summary>
public const string MINLOGGINGLEVELKEY = "MinimumLoggingLevel";
/// <summary> Key of the center to ... entry. </summary>
public const string CENTERMAPTOCURRENTLOCATION = "CenterMapToCurrentLocation";
public const string LOGTOEXTERNALFOLDER = "LogToExternalFolder";
public const string THEMEKEY = "Theme";
public const string ISSITECACHINGON = "IsSiteCachingOn";
/// <summary> Gets a nullable value.</summary>
/// <param name="settingsJSON">Dictionary to get value from.</param>
public static T? GetNullableEntry<T>(
string keyName,
Dictionary<string, string> settingsJSON) where T : struct
{
if (!settingsJSON.TryGetValue(keyName, out string boolText)
|| string.IsNullOrEmpty(boolText))
{
// File holds no entry.
return null;
}
return JsonConvert.DeserializeObject<T>(boolText);
}
/// <summary> Gets a value to type class.</summary>
/// <param name="settingsJSON">Dictionary to get value from.</param>
public static T GetEntry<T>(
string keyName,
Dictionary<string, string> settingsJSON) where T : class
{
if (!settingsJSON.TryGetValue(keyName, out string boolText)
|| string.IsNullOrEmpty(boolText))
{
// File holds no entry.
return null;
}
return JsonConvert.DeserializeObject<T>(boolText);
}
/// <summary> Sets a nullable.</summary>
/// <param name="entry">Entry to add to dictionary.</param>
/// <param name="keyName">Key to use for value. </param>
/// <param name="targetDictionary">Dictionary to set value to.</param>
public static Dictionary<string, string> SetEntry<T>(T entry, string keyName, IDictionary<string, string> 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<string, string>
{
{ keyName, JsonConvert.SerializeObject(entry) }
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Sets the timeout to apply when connecting to bluetooth lock.</summary>
/// <param name="targetDictionary">Dictionary to write information to.</param>
/// <param name="connectTimeout">Connect timeout value.</param>
public static Dictionary<string, string> SetConnectTimeout(
this IDictionary<string, string> targetDictionary,
TimeSpan connectTimeout)
{
if (targetDictionary == null)
throw new Exception("Writing conntect timeout info failed. Dictionary must not be null.");
return targetDictionary.Union(new Dictionary<string, string>
{
{ CONNECTTIMEOUT, JsonConvert.SerializeObject(connectTimeout, new JavaScriptDateTimeConverter()) }
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Sets the uri of the active copri host. </summary>
/// <param name="p_oSettingsJSON">Dictionary holding parameters from JSON.</param>
public static Dictionary<string, string> SetCopriHostUri(this IDictionary<string, string> 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<string, string>
{
{ typeof(CopriServerUriList).ToString(), JsonConvert.SerializeObject(p_strNextActiveUriText) },
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Gets the timeout to apply when connecting to bluetooth lock.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get information from.</param>
/// <returns>Connect timeout value.</returns>
public static TimeSpan? GetConnectTimeout(Dictionary<string, string> p_oSettingsJSON)
{
if (!p_oSettingsJSON.TryGetValue(CONNECTTIMEOUT, out string connectTimeout)
|| string.IsNullOrEmpty(connectTimeout))
{
// File holds no entry.
return null;
}
return JsonConvert.DeserializeObject<TimeSpan>(connectTimeout, new JavaScriptDateTimeConverter());
}
/// <summary> Gets the logging level.</summary>
/// <param name="settingsJSON">Dictionary to get logging level from.</param>
/// <returns>Logging level</returns>
public static Uri GetCopriHostUri(this IDictionary<string, string> 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<Uri>(uriText);
}
/// <summary> Sets the version of the app. </summary>
/// <param name="p_oSettingsJSON">Dictionary holding parameters from JSON.</param>
public static Dictionary<string, string> SetAppVersion(this IDictionary<string, string> 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<string, string>
{
{APPVERIONKEY , JsonConvert.SerializeObject(p_strAppVersion, new VersionConverter()) },
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Gets the app versions.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get logging level from.</param>
/// <returns>Logging level</returns>
public static Version GetAppVersion(this IDictionary<string, string> 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<Version>(l_oAppVersion, new VersionConverter())
: Version.Parse(l_oAppVersion); // Format used up to version 3.0.0.115
}
/// <summary> Sets whether polling is on or off and the periode if polling is on. </summary>
/// <param name="p_oSettingsJSON">Dictionary to write entries to.</param>
public static Dictionary<string, string> SetPollingParameters(this IDictionary<string, string> 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<string, string>
{
{ $"{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);
}
/// <summary> Get whether polling is on or off and the periode if polling is on. </summary>
/// <param name="p_oSettingsJSON">Dictionary holding parameters from JSON.</param>
/// <returns>Polling parameters.</returns>
public static PollingParameters GetPollingParameters(this IDictionary<string, string> 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<TimeSpan>(l_strPeriode),
JsonConvert.DeserializeObject<bool>(l_strIsActive));
}
return null;
}
/// <summary> Saves object to file. </summary>
/// <param name="p_SettingsList">Settings to save.</param>
public static void Serialize(string p_strSettingsFileFolder, IDictionary<string, string> 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);
}
/// <summary> Gets TINK app settings form xml- file. </summary>
/// <param name="p_strSettingsDirectory">Directory to read settings from.</param>
/// <returns>Dictionary of settings.</returns>
public static Dictionary<string, string> 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<string, string>(); ;
}
var l_oJSONFile = System.IO.File.ReadAllText(l_oFileName);
if (string.IsNullOrEmpty(l_oJSONFile))
{
// File is empty. Nothing to read.
return new Dictionary<string, string>();
}
// Load setting file.
return JsonConvert.DeserializeObject<Dictionary<string, string>>(l_oJSONFile);
}
/// <summary> Gets the logging level.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get logging level from.</param>
/// <returns>Logging level.</returns>
public static LogEventLevel? GetMinimumLoggingLevel(
Dictionary<string, string> 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<string>(l_strLevel));
}
/// <summary> Sets the logging level.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get logging level from.</param>
public static Dictionary<string, string> SetMinimumLoggingLevel(this IDictionary<string, string> 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<string, string>
{
{ MINLOGGINGLEVELKEY, JsonConvert.SerializeObject((int)p_oLevel) }
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Gets the version of app when whats new was shown.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get logging level from.</param>
/// <returns>Version of the app.</returns>
public static Version GetWhatsNew(Dictionary<string, string> 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<Version>(l_strWhatsNewVersion, new VersionConverter());
}
/// <summary> Sets the version of app when whats new was shown.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get information from.</param>
public static Dictionary<string, string> SetWhatsNew(this IDictionary<string, string> 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<string, string>
{
{ SHOWWHATSNEWKEY, JsonConvert.SerializeObject(p_oAppVersion, new VersionConverter()) }
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Gets the expires after value.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get expries after value from.</param>
/// <returns>Expires after value.</returns>
public static TimeSpan? GetExpiresAfter(Dictionary<string, string> p_oSettingsJSON)
{
if (!p_oSettingsJSON.TryGetValue(EXPIRESAFTER, out string expiresAfter)
|| string.IsNullOrEmpty(expiresAfter))
{
// File holds no entry.
return null;
}
return JsonConvert.DeserializeObject<TimeSpan>(expiresAfter, new JavaScriptDateTimeConverter());
}
/// <summary> Sets the the expiration time.</summary>
/// <param name="p_oSettingsJSON">Dictionary to write information to.</param>
public static Dictionary<string, string> SetExpiresAfter(this IDictionary<string, string> 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<string, string>
{
{ EXPIRESAFTER, JsonConvert.SerializeObject(expiresAfter, new JavaScriptDateTimeConverter()) }
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Sets the active lock service name. </summary>
/// <param name="targetDictionary">Dictionary holding parameters from JSON.</param>
public static Dictionary<string, string> SetActiveLockService(this IDictionary<string, string> 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<string, string>
{
{ typeof(ILocksService).Name, activeLockService },
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Gets the active lock service name.</summary>
/// <param name="settingsJSON">Dictionary to get logging level from.</param>
/// <returns>Active lock service name.</returns>
public static string GetActiveLockService(this IDictionary<string, string> 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;
}
/// <summary> Sets the active Geolocation service name. </summary>
/// <param name="targetDictionary">Dictionary holding parameters from JSON.</param>
public static Dictionary<string, string> SetActiveGeolocationService(
this IDictionary<string, string> 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<string, string>
{
{ typeof(IGeolocation).Name, activeGeolocationService },
}).ToDictionary(key => key.Key, value => value.Value);
}
/// <summary> Gets the active Geolocation service name.</summary>
/// <param name="settingsJSON">Dictionary to get name of geolocation service from.</param>
/// <returns>Active lock service name.</returns>
public static string GetActiveGeolocationService(this IDictionary<string, string> 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;
}
/// <summary> Gets a value indicating whether to center the map to location or not.</summary>
/// <param name="settingsJSON">Dictionary to get value from.</param>
public static bool? GetCenterMapToCurrentLocation(Dictionary<string, string> settingsJSON) => GetNullableEntry<bool>(CENTERMAPTOCURRENTLOCATION, settingsJSON);
/// <summary> Sets a value indicating whether to center the map to location or not.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get value from.</param>
public static Dictionary<string, string> SetCenterMapToCurrentLocation(this IDictionary<string, string> targetDictionary, bool centerMapToCurrentLocation)
=> SetEntry(centerMapToCurrentLocation, CENTERMAPTOCURRENTLOCATION, targetDictionary);
/// <summary> Gets whether to store logging data on SD card or not.</summary>
/// <param name="settingsJSON">Dictionary to get value from.</param>
public static bool? GetLogToExternalFolder(Dictionary<string, string> settingsJSON) => GetNullableEntry<bool>(LOGTOEXTERNALFOLDER, settingsJSON);
/// <summary> Gets full class name of active theme.</summary>
/// <param name="settingsJSON">Dictionary to get value from.</param>
public static string GetActiveTheme(Dictionary<string, string> settingsJSON) => GetEntry<string>(THEMEKEY, settingsJSON);
/// <summary> Gets a value indicating whether site caching is on or off.</summary>
/// <param name="settingsJSON">Dictionary to get value from.</param>
public static bool? GetIsSiteCachingOn(Dictionary<string, string> settingsJSON) => GetNullableEntry<bool>(ISSITECACHINGON, settingsJSON);
/// <summary> Sets whether to store logging data on SD card or not.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get value from.</param>
public static Dictionary<string, string> SetLogToExternalFolder(this IDictionary<string, string> targetDictionary, bool useSdCard) => SetEntry(useSdCard, LOGTOEXTERNALFOLDER, targetDictionary);
/// <summary> Sets active theme.</summary>
/// <param name="targetDictionary">Dictionary to set value to.</param>
public static Dictionary<string, string> SetActiveTheme(this IDictionary<string, string> targetDictionary, string theme) => SetEntry(theme, THEMEKEY, targetDictionary);
/// <summary> Sets whether site caching is on or off.</summary>
/// <param name="p_oSettingsJSON">Dictionary to get value from.</param>
public static Dictionary<string, string> SetIsSiteCachingOn(this IDictionary<string, string> targetDictionary, bool useSdCard) => SetEntry(useSdCard, ISSITECACHINGON, targetDictionary);
/// <summary> Gets the map page filter. </summary>
/// <param name="settings">Settings objet to load from.</param>
public static IGroupFilterMapPage GetGroupFilterMapPage(this IDictionary<string, string> settings)
{
var l_oKeyName = "FilterCollection_MapPageFilter";
if (settings == null || !settings.ContainsKey(l_oKeyName))
{
return null;
}
return new GroupFilterMapPage(JsonConvert.DeserializeObject<IDictionary<string, FilterState>>(settings[l_oKeyName]));
}
public static IDictionary<string, string> SetGroupFilterMapPage(
this IDictionary<string, string> settings,
IDictionary<string, FilterState> p_oFilterCollection)
{
if (settings == null
|| p_oFilterCollection == null
|| p_oFilterCollection.Count < 1)
{
return settings;
}
settings["FilterCollection_MapPageFilter"] = JsonConvert.SerializeObject(p_oFilterCollection);
return settings;
}
/// <summary> Gets the settings filter. </summary>
/// <param name="settings">Settings objet to load from.</param>
public static IGroupFilterSettings GetGoupFilterSettings(this IDictionary<string, string> settings)
{
var l_oKeyName = "FilterCollection";
if (settings == null || !settings.ContainsKey(l_oKeyName))
{
return null;
}
var legacyFilterCollection = new GroupFilterSettings(JsonConvert.DeserializeObject<IDictionary<string, FilterState>>(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<string, string> SetGroupFilterSettings(
this IDictionary<string, string> settings,
IDictionary<string, FilterState> p_oFilterCollection)
{
if (settings == null
|| p_oFilterCollection == null
|| p_oFilterCollection.Count < 1)
{
return settings;
}
settings["FilterCollection"] = JsonConvert.SerializeObject(p_oFilterCollection);
return settings;
}
}
}

View file

@ -0,0 +1,100 @@
using System;
namespace TINK.Settings
{
/// <summary> Holds polling parameters.</summary>
public sealed class PollingParameters : IEquatable<PollingParameters>
{
/// <summary> Holds default polling parameters. </summary>
public static PollingParameters Default { get; } = new PollingParameters(
new TimeSpan(0, 0, 0, 10 /*secs*/, 0),// Default polling interval.
true);
/// <summary> Holds polling parameters which represent polling off (empty polling object). </summary>
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);
/// <summary> Constructs a polling parameter object. </summary>
/// <param name="p_oPeriode">Polling periode.</param>
/// <param name="p_bIsActivated">True if polling is activated.</param>
public PollingParameters(TimeSpan p_oPeriode, bool p_bIsActivated)
{
Periode = p_oPeriode; // Can not be null because is a struct.
IsActivated = p_bIsActivated;
}
/// <summary>Holds the polling periode.</summary>
public TimeSpan Periode { get; }
/// <summary> Holds value whether polling is activated or not.</summary>
public bool IsActivated { get; }
/// <summary> Checks equallity.</summary>
/// <param name="other">Object to compare with.</param>
/// <returns>True if objects are equal.</returns>
public bool Equals(PollingParameters other)
{
return this == other;
}
/// <summary> Checks equallity.</summary>
/// <param name="obj">Object to compare with.</param>
/// <returns>True if objects are equal.</returns>
public override bool Equals(object obj)
{
var l_oParameters = obj as PollingParameters;
if (l_oParameters == null)
{
return false;
}
return this == l_oParameters;
}
/// <summary> Gets the has code of object.</summary>
/// <returns></returns>
public override int GetHashCode()
{
return Periode.GetHashCode() ^ IsActivated.GetHashCode();
}
/// <summary> Gets the string representation of the object.</summary>
/// <returns></returns>
public override string ToString()
{
return $"Polling is on={IsActivated}, polling interval={Periode.TotalSeconds}[sec].";
}
/// <summary>Defines equality of thwo polling parameter objects.</summary>
/// <param name="p_oSource">First object to compare.</param>
/// <param name="p_oTarget">Second object to compare.</param>
/// <returns>True if objects are equal</returns>
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;
}
/// <summary>Defines equality of thwo polling parameter objects.</summary>
/// <param name="p_oSource">First object to compare.</param>
/// <param name="p_oTarget">Second object to compare.</param>
/// <returns>True if objects are equal</returns>
public static bool operator !=(PollingParameters p_oSource, PollingParameters p_oTarget)
{
return (p_oSource == p_oTarget) == false;
}
}
}

View file

@ -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
{
/// <summary> Holds settings which are persisted.</summary>
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);
/// <summary> Constructs settings object. </summary>
/// <param name="groupFilterMapPage">filter which is applied on the map view. Either TINK or Konrad stations are displayed.</param>
/// <param name="groupFilterSettings"></param>
/// <param name="activeUri"></param>
/// <param name="pollingParameters"></param>
/// <param name="minimumLogEventLevel">Minimum logging level to be applied.</param>
/// <param name="expiresAfter">Holds the expires after value.</param>
/// <param name="activeLockService">Gets the name of the lock service to use.</param>
/// <param name="connectTimeout">Timeout to apply when connecting to bluetooth lock</param>
/// <param name="activeTheme">Full class name of active app theme.</param>
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;
}
/// <summary> Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. </summary>
public IGroupFilterMapPage GroupFilterMapPage { get; }
/// <summary> Holds the filters loaded from settings. </summary>
public IGroupFilterSettings GroupFilterSettings { get; }
/// <summary> Holds the uri to connect to. </summary>
public Uri ActiveUri { get; }
/// <summary> Holds the polling parameters. </summary>
public PollingParameters PollingParameters { get; }
/// <summary> Gets the minimum logging level. </summary>
public LogEventLevel MinimumLogEventLevel { get; }
/// <summary> Gets the expires after value.</summary>
public TimeSpan ExpiresAfter { get; }
/// <summary> Gets the lock service to use.</summary>
public string ActiveLockService { get; private set; }
/// <summary> Timeout to apply when connecting to bluetooth lock.</summary>
public TimeSpan ConnectTimeout { get; }
/// <summary> Gets the geolocation service to use.</summary>
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();
}
}
}

View file

@ -0,0 +1,20 @@
using System.Runtime.Serialization;
namespace TINK.Model.State
{
/// <summary>
/// Base type for serialization purposes.
/// </summary>
[DataContract]
[KnownType(typeof(StateAvailableInfo))]
[KnownType(typeof(StateRequestedInfo))]
[KnownType(typeof(StateOccupiedInfo))]
public abstract class BaseState
{
/// <summary> Constructor for Json serialization. </summary>
/// <param name="p_eValue">State value.</param>
protected BaseState(InUseStateEnum p_eValue) {}
public abstract InUseStateEnum Value { get; }
}
}

View file

@ -0,0 +1,11 @@

namespace TINK.Model.State
{
/// <summary>
/// Base state information.
/// </summary>
public interface IBaseState
{
InUseStateEnum Value { get; }
}
}

View file

@ -0,0 +1,14 @@
using System;
namespace TINK.Model.State
{
/// <summary>
/// State of bikes which are either reserved or booked.
/// </summary>
public interface INotAvailableState : IBaseState
{
DateTime From { get; }
string MailAddress { get; }
string Code { get; }
}
}

View file

@ -0,0 +1,16 @@
using System;
namespace TINK.Model.State
{
/// <summary>
/// Interface to access informations about bike information.
/// </summary>
public interface IStateInfo : IBaseState
{
string MailAddress { get; }
DateTime? From { get; }
string Code { get; }
}
}

View file

@ -0,0 +1,23 @@
using System;
namespace TINK.Model.State
{
public interface IStateInfoMutable
{
InUseStateEnum Value { get; }
/// <summary> Updates state from webserver. </summary>
/// <param name="p_oState">State of the bike.</param>
/// <param name="p_oFrom">Date time when bike was reserved/ booked.</param>
/// <param name="p_oDuration">Lenght of time span for which bike remains booked.</param>
/// <param name="p_strMailAddress">Mailaddress of the one which reserved/ booked.</param>
/// <param name="p_strCode">Booking code if bike is booked or reserved.</param>
/// <param name="notifyLevel">Controls whether notify property changed events are fired or not.</param>
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);
}
}

View file

@ -0,0 +1,39 @@
using Newtonsoft.Json;
using System;
using System.Runtime.Serialization;
namespace TINK.Model.State
{
/// <summary>
/// Represents the state available.
/// </summary>
[DataContract]
public sealed class StateAvailableInfo : BaseState, IBaseState
{
/// <summary>
/// Constructs state info object representing state available.
/// </summary>
public StateAvailableInfo() : base(InUseStateEnum.Disposable)
{
}
/// <summary> Constructor for Json serialization. </summary>
/// <param name="p_eValue">Unused value.</param>
[JsonConstructor]
private StateAvailableInfo (InUseStateEnum p_eValue) : base(InUseStateEnum.Disposable)
{
}
/// <summary>
/// Gets the info that state is disposable.
/// Setter exists only for serialization purposes.
/// </summary>
public override InUseStateEnum Value
{
get
{
return InUseStateEnum.Disposable;
}
}
}
}

View file

@ -0,0 +1,161 @@
using System;
namespace TINK.Model.State
{
/// <summary>
/// Types of rent states
/// </summary>
public enum InUseStateEnum
{
/// <summary>
/// Bike is not in use. Corresponding COPRI state is "available".
/// </summary>
Disposable,
/// <summary>
/// Bike is reserved. Corresponding COPRI state is "requested".
/// </summary>
Reserved,
/// <summary>
/// Bike is booked. Corresponding COPRI statie is "occupied".
/// </summary>
Booked
}
/// <summary>
/// Manages the state of a bike.
/// </summary>
public class StateInfo : IStateInfo
{
// Holds the current disposable state value
private readonly BaseState m_oInUseState;
/// <summary>
/// Constructs a state info object when state is available.
/// </summary>
/// <param name="p_oDateTimeNowProvider">Provider for current date time to calculate remainig time on demand for state of type reserved.</param>
public StateInfo()
{
m_oInUseState = new StateAvailableInfo();
}
/// <summary>
/// Constructs a state info object when state is requested.
/// </summary>
/// <param name="p_oRequestedAt">Date time when bike was requested</param>
/// <param name="p_strMailAddress">Mail address of user which requested bike.</param>
/// <param name="p_strCode">Booking code.</param>
/// <param name="p_oDateTimeNowProvider">Date time provider to calculate reaining time.</param>
public StateInfo(
Func<DateTime> 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);
}
/// <summary>
/// Constructs a state info object when state is booked.
/// </summary>
/// <param name="p_oBookedAt">Date time when bike was booked</param>
/// <param name="p_strMailAddress">Mail address of user which booked bike.</param>
/// <param name="p_strCode">Booking code.</param>
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);
}
/// <summary>
/// Gets the state value of object.
/// </summary>
public InUseStateEnum Value
{
get { return m_oInUseState.Value; }
}
/// <summary>
/// Member for serialization purposes.
/// </summary>
internal BaseState StateInfoObject
{
get { return m_oInUseState; }
}
/// Transforms object to string.
/// </summary>
/// <returns></returns>
public new string ToString()
{
return m_oInUseState.Value.ToString("g");
}
/// <summary>
/// Date of request/ bookeing action.
/// </summary>
public DateTime? From
{
get
{
var l_oNotDisposableInfo = m_oInUseState as INotAvailableState;
return l_oNotDisposableInfo != null ? l_oNotDisposableInfo.From : (DateTime?)null;
}
}
/// <summary>
/// Mail address.
/// </summary>
public string MailAddress
{
get
{
var l_oNotDisposableInfo = m_oInUseState as INotAvailableState;
return l_oNotDisposableInfo?.MailAddress;
}
}
/// <summary>
/// Reservation code.
/// </summary>
public string Code
{
get
{
var l_oNotDisposableInfo = m_oInUseState as INotAvailableState;
return l_oNotDisposableInfo?.Code;
}
}
/// <summary>
/// Tries update
/// </summary>
/// <returns>True if reservation span has not exeeded and state remains reserved, false otherwise.</returns>
/// <todo>Implement logging of time stamps.</todo>
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);
}
}
}

View file

@ -0,0 +1,299 @@
using System;
namespace TINK.Model.State
{
using System.ComponentModel;
using System.Runtime.Serialization;
/// <summary>
/// Manges the state of a bike.
/// </summary>
[DataContract]
public class StateInfoMutable : INotifyPropertyChanged, IStateInfoMutable
{
/// <summary>
/// Provider for current date time to calculate remainig time on demand for state of type reserved.
/// </summary>
private readonly Func<DateTime> m_oDateTimeNowProvider;
// Holds the current disposable state value
private StateInfo m_oStateInfo;
/// <summary>
/// Backs up remaining time of child object.
/// </summary>
private TimeSpan? m_oRemainingTime = null;
/// <summary> Notifies clients about state changes. </summary>
public event PropertyChangedEventHandler PropertyChanged;
/// <summary>
/// Constructs a state object from source.
/// </summary>
/// <param name="p_oState">State info to load from.</param>
public StateInfoMutable(
Func<DateTime> 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);
}
/// <summary>
/// Loads state from immutable source.
/// </summary>
/// <param name="p_oState">State to load from.</param>
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)));
}
/// <summary>
/// Creates a state info object.
/// </summary>
/// <param name="p_oState">State to load from.</param>
private static StateInfo Create(
IStateInfo p_oState,
Func<DateTime> 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));
}
}
/// <summary>
/// Gets the state value of object.
/// </summary>
public InUseStateEnum Value
{
get { return m_oStateInfo.Value; }
}
/// <summary>
/// Member for serialization purposes.
/// </summary>
[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.
/// </summary>
/// <returns></returns>
public new string ToString()
{
return m_oStateInfo.ToString();
}
/// <summary>
/// Checks and updates state if required.
/// </summary>
/// <returns>Value indicating wheter state has changed</returns>
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)));
}
/// <summary> Updates state from webserver. </summary>
/// <param name="p_oState">State of the bike.</param>
/// <param name="p_oFrom">Date time when bike was reserved/ booked.</param>
/// <param name="p_oDuration">Lenght of time span for which bike remains booked.</param>
/// <param name="p_strMailAddress">Mailaddress of the one which reserved/ booked.</param>
/// <param name="p_strCode">Booking code if bike is booked or reserved.</param>
/// <param name="supressNotifyPropertyChanged">Controls whether notify property changed events are fired or not.</param>
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)));
}
}
/// <summary>
/// If bike is reserved time raimaining while bike stays reserved, null otherwise.
/// </summary>
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;
}
}
}
}

View file

@ -0,0 +1,80 @@
using Newtonsoft.Json;
using System;
using System.Runtime.Serialization;
namespace TINK.Model.State
{
/// <summary>
/// Manages state booked.
/// </summary>
[DataContract]
public sealed class StateOccupiedInfo : BaseState, IBaseState, INotAvailableState
{
/// <summary>
/// Prevents an invalid instance to be created.
/// </summary>
private StateOccupiedInfo() : base(InUseStateEnum.Booked)
{
}
/// <summary>
/// Constructs an object holding booked state info.
/// </summary>
/// <param name="p_oFrom">Date time when bike was booked</param>
/// <param name="p_strMailAddress"></param>
/// <param name="p_strCode"></param>
public StateOccupiedInfo(
DateTime p_oFrom,
string p_strMailAddress,
string p_strCode) : base(InUseStateEnum.Booked)
{
From = p_oFrom;
MailAddress = p_strMailAddress;
Code = p_strCode;
}
/// <summary> Constructor for Json serialization. </summary>
/// <param name="Value">Unused value.</param>
/// <param name="From">Date time when bike was booked</param>
/// <param name="MailAddress"></param>
/// <param name="Code"></param>
[JsonConstructor]
private StateOccupiedInfo(
InUseStateEnum Value,
DateTime From,
string MailAddress,
string Code) : this(From, MailAddress, Code)
{
}
/// <summary>
/// Gets the info that state is reserved.
/// Setter exists only for serialization purposes.
/// </summary>
public override InUseStateEnum Value
{
get
{
return InUseStateEnum.Booked;
}
}
/// <summary>
/// Prevents an invalid instance to be created.
/// </summary>
[DataMember]
public DateTime From { get; }
/// <summary>
/// Mail address of user who bookec the bike.
/// </summary>
[DataMember]
public string MailAddress { get; }
/// <summary>
/// Booking code.
/// </summary>
[DataMember]
public string Code { get; }
}
}

View file

@ -0,0 +1,114 @@
using Newtonsoft.Json;
using System;
using System.Runtime.Serialization;
namespace TINK.Model.State
{
/// <summary>
/// Manages state reserved.
/// </summary>
[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<DateTime> m_oDateTimeNowProvider;
/// <summary>
/// Prevents an invalid instance to be created.
/// Used by serializer only.
/// </summary>
private StateRequestedInfo() : base(InUseStateEnum.Reserved)
{
// Is called in context of JSON deserialization.
m_oDateTimeNowProvider = () => DateTime.Now;
}
/// <summary>
/// Reservation performed with other device/ before start of app.
/// Date time info when bike was reserved has been received from webserver.
/// </summary>
/// <param name="p_oRemainingTime">Time span which holds duration how long bike still will be reserved.</param>
[JsonConstructor]
private StateRequestedInfo(
InUseStateEnum Value,
DateTime From,
string MailAddress,
string Code) : this(() => DateTime.Now, From, MailAddress, Code)
{
}
/// <summary>
/// Reservation performed with other device/ before start of app.
/// Date time info when bike was reserved has been received from webserver.
/// </summary>
/// <param name="p_oDateTimeNowProvider">
/// Used to to provide current date time information for potential calls of <seealso cref="GetIsStillReserved"/>.
/// Not used to calculate remaining time because this duration whould always be shorter as the one received from webserver.
/// </param>
/// <param name="p_oRemainingTime">Time span which holds duration how long bike still will be reserved.</param>
public StateRequestedInfo(
Func<DateTime> 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;
}
/// <summary>
/// Tries update
/// </summary>
/// <returns>True if reservation span has not exeeded and state remains reserved, false otherwise.</returns>
/// <todo>Implement logging of time stamps.</todo>
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;
}
/// <summary>
/// State reserved.
/// Setter exists only for serialization purposes.
/// </summary>
public override InUseStateEnum Value
{
get
{
return InUseStateEnum.Reserved;
}
}
/// <summary>
/// Date time when bike was reserved.
/// </summary>
[DataMember]
public DateTime From { get; }
/// <summary>
/// Mail address of user who reserved the bike.
/// </summary>
[DataMember]
public string MailAddress { get; }
/// <summary>
/// Booking code.
/// </summary>
[DataMember]
public string Code { get; }
}
}

View file

@ -0,0 +1,19 @@
using System.Collections.Generic;
namespace TINK.Model.Station
{
public interface IStation
{
/// <summary> Holds the unique id of the station.c</summary>
int Id { get; }
/// <summary> Holds the group to which the station belongs.</summary>
IEnumerable<string> Group { get; }
/// <summary> Gets the name of the station.</summary>
string StationName { get; }
/// <summary> Holds the gps- position of the station.</summary>
Position Position { get; }
}
}

View file

@ -0,0 +1,20 @@
using System.Collections.Generic;
namespace TINK.Model.Station
{
/// <summary> Holds object representing null station.</summary>
public class NullStation : IStation
{
/// <summary> Holds the unique id of the station.c</summary>
public int Id => -1;
/// <summary> Holds the group to which the station belongs.</summary>
public IEnumerable<string> Group => new List<string>();
/// <summary> Gets the name of the station.</summary>
public string StationName => string.Empty;
/// <summary> Holds the gps- position of the station.</summary>
public Position Position => new Position(double.NaN, double.NaN);
}
}

View file

@ -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; }
/// <summary>
/// Compares position with a target position.
/// </summary>
/// <param name="p_oTarget">Target position to compare with.</param>
/// <returns>True if positions are equal.</returns>
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;
}
}
}

View file

@ -0,0 +1,38 @@
using System;
using System.Collections.Generic;
namespace TINK.Model.Station
{
/// <summary> Holds station info. </summary>
public class Station : IStation
{
/// <summary> Constructs a station object.</summary>
/// <param name="p_iId">Id of the station.</param>
/// <param name="p_oGroup">Group (TINK, Konrad) to which station is related.</param>
/// <param name="p_oPosition">GPS- position of the station.</param>
/// <param name="p_strStationName">Name of the station.</param>
public Station(
int p_iId,
IEnumerable<string> 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;
}
/// <summary> Holds the unique id of the station.c</summary>
public int Id { get; }
/// <summary> Holds the group to which the station belongs.</summary>
public IEnumerable<string> Group { get; }
/// <summary> Gets the name of the station.</summary>
public string StationName { get; }
/// <summary> Holds the gps- position of the station.</summary>
public Position Position { get; }
}
}

View file

@ -0,0 +1,97 @@
using System;
using System.Collections;
using System.Collections.Generic;
namespace TINK.Model.Station
{
public class StationDictionary : IEnumerable<Station>
{
/// <summary> Holds the list of stations. </summary>
private readonly IDictionary<int, Station> m_oStationDictionary;
/// <summary> Count of stations. </summary>
public int Count { get { return m_oStationDictionary.Count; } }
public Version CopriVersion { get; }
/// <summary> Constructs a station dictionary object. </summary>
/// <param name="p_oVersion">Version of copri- service.</param>
public StationDictionary(Version p_oVersion = null, IDictionary<int, Station> p_oStations = null)
{
m_oStationDictionary = p_oStations ?? new Dictionary<int, Station>();
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<Station> GetEnumerator()
{
return m_oStationDictionary.Values.GetEnumerator();
}
/// <summary>
/// Deteermines whether a station by given key exists.
/// </summary>
/// <param name="p_strKey">Key to check.</param>
/// <returns>True if station exists.</returns>
public bool ContainsKey(int p_strKey)
{
return m_oStationDictionary.ContainsKey(p_strKey);
}
/// <summary>
/// Remove a station by station id.
/// </summary>
/// <param name="p_iId"></param>
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);
}
/// <summary>
/// Remove a station by station name.
/// </summary>
/// <param name="p_iId"></param>
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];
}
/// <summary>
/// Adds a station to dictionary of stations.
/// </summary>
/// <param name="p_oStation"></param>
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();
}
}
}

423
TINKLib/Model/TinkApp.cs Normal file
View file

@ -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
{
/// <summary> Delegate used by login view to commit user name and password. </summary>
/// <param name="p_strMailAddress">Mail address used as id login.</param>
/// <param name="p_strPassword">Password for login.</param>
/// <returns>True if setting credentials succeeded.</returns>
public delegate bool SetCredentialsDelegate(string p_strMailAddress, string p_strPassword);
/// <summary>Returns the id of the app to be identified by copri.</summary>
public static string MerchantId => "oiF2kahH";
/// <summary>
/// Holds status about whants new page.
/// </summary>
public WhatsNew WhatsNew { get; private set; }
/// <summary>Sets flag whats new page was already shown to true. </summary>
public void SetWhatsNewWasShown() => WhatsNew = WhatsNew.SetWasShown();
/// <summary>Holds uris of copri servers. </summary>
public CopriServerUriList Uris { get; }
/// <summary> Holds the filters loaded from settings. </summary>
public IGroupFilterSettings FilterGroupSetting { get; set; }
/// <summary> Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. </summary>
private IGroupFilterMapPage m_oFilterDictionaryMapPage;
/// <summary> Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed. </summary>
public IGroupFilterMapPage GroupFilterMapPage
{
get => m_oFilterDictionaryMapPage;
set => m_oFilterDictionaryMapPage = value ?? new GroupFilterMapPage();
}
/// <summary> Value indicating whether map is centerted to current position or not. </summary>
public bool CenterMapToCurrentLocation { get; set; }
/// <summary> Gets the minimum logging level. </summary>
public LogEventLevel MinimumLogEventLevel { get; set; }
/// <summary> Holds the uri which is applied after restart. </summary>
public Uri NextActiveUri { get; set; }
/// <summary> Saves object to file. </summary>
public void Save()
=> JsonSettingsDictionary.Serialize(
SettingsFileFolder,
new Dictionary<string, string>()
.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));
/// <summary>
/// Update connector from filters when
/// - login state changes
/// - view is toggled (TINK to Kornrad and vice versa)
/// </summary>
public void UpdateConnector()
{
// Create filtered connector.
m_oConnector = FilteredConnectorFactory.Create(
FilterGroupSetting.DoFilter(ActiveUser.DoFilter(GroupFilterMapPage.DoFilter())),
m_oConnector.Connector);
}
/// <summary>Polling periode.</summary>
public PollingParameters Polling { get; set; }
public TimeSpan ExpiresAfter { get; set; }
/// <summary> Holds the version of the app.</summary>
public Version AppVersion { get; }
/// <summary>
/// Holds the default polling value.
/// </summary>
public TimeSpan DefaultPolling => new TimeSpan(0, 0, 10);
/// <summary> Constructs TinkApp object. </summary>
/// <param name="settings"></param>
/// <param name="accountStore"></param>
/// <param name="passwordValidator"></param>
/// <param name="p_oConnectorFactory"></param>
/// <param name="geolocationService">Null in productive context. Service to querry geoloation for testing purposes. Parameter can be made optional.</param>
/// <param name="locksService">Null in productive context. Service to control locks/ get locks information for testing proposes. Parameter can be made optional.</param>
/// <param name="device">Object allowing platform specific operations.</param>
/// <param name="specialFolder"></param>
/// <param name="p_oDateTimeProvider"></param>
/// <param name="isConnectedFunc">True if connector has access to copri server, false if cached values are used.</param>
/// <param name="currentVersion">Version of the app. If null version is set to a fixed dummy value (3.0.122) for testing purposes.</param>
/// <param name="lastVersion">Version of app which was used before this session.</param>
/// <param name="whatsNewShownInVersion"> 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.
/// /// </param>
public TinkApp(
Settings.Settings settings,
IStore accountStore,
Func<bool, Uri, string, string, TimeSpan, IConnector> connectorFactory,
IGeolocation geolocationService,
IGeolodationDependent geolodationServiceDependent,
ILocksService locksService,
IDevice device,
ISpecialFolder specialFolder,
ICipher cipher,
IPermissions permissions = null,
object arendiCentral = null,
Func<bool> isConnectedFunc = null,
Action<SendOrPostCallback, object> 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<ILocksService> { locksService }
: new HashSet<ILocksService> {
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<object>(
new HashSet<object> { new Themes.Konrad() , new Themes.ShareeBike() },
settings.ActiveTheme);
GeolocationServices = new ServicesContainerMutable<IGeolocation>(
geolocationService == null
? new HashSet<IGeolocation> { new LastKnownGeolocationService(geolodationServiceDependent), new SimulatedGeolocationService(geolodationServiceDependent), new GeolocationService(geolodationServiceDependent) }
: new HashSet<IGeolocation> { 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<string, FilterState> { { "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<ResourceDictionary> mergedDictionaries = Application.Current.Resources.MergedDictionaries;
if (mergedDictionaries == null)
{
Log.ForContext<TinkApp>().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<TinkApp>().Debug($"No theme {Themes.Active} found.");
}
}
/// <summary> Holds the user of the app. </summary>
[DataMember]
public User.User ActiveUser { get; }
/// <summary> Reference of object which provides device information. </summary>
public IDevice Device { get; }
/// <summary> Os permission.</summary>
public IPermissions Permissions { get; }
/// <summary> Holds delegate to determine whether device is connected or not.</summary>
private Func<bool> isConnectedFunc;
/// <summary> Gets whether device is connected to internet or not. </summary>
public bool GetIsConnected() => isConnectedFunc();
/// <summary> Holds the folder where settings files are stored. </summary>
public string SettingsFileFolder { get; }
/// <summary> Holds folder parent of the folder where log files are stored. </summary>
public string LogFileParentFolder => LogToExternalFolder && !string.IsNullOrEmpty(ExternalFolder) ? ExternalFolder : SettingsFileFolder;
/// <summary> Holds a value indicating whether to log to external or internal folder. </summary>
public bool LogToExternalFolder { get; set; }
/// <summary> Holds a value indicating whether Site caching is on or off. </summary>
public bool IsSiteCachingOn { get; set; }
/// <summary> External folder. </summary>
public string ExternalFolder { get; }
public ICipher Cipher { get; }
/// <summary> Name of the station which is selected. </summary>
public int? SelectedStation { get; set; }
/// <summary> Action to post to GUI thread.</summary>
public Action<SendOrPostCallback, object> PostAction { get; }
/// <summary> Function which creates a connector depending on connected status.</summary>
private Func<bool, Uri, string, string, TimeSpan, IConnector> ConnectorFactory { get; }
/// <summary> Holds the object which provides offline data.</summary>
private IFilteredConnector m_oConnector;
/// <summary> Holds the system to copri.</summary>
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;
}
/// <summary> Query geolocation. </summary>
public IGeolocation Geolocation => GeolocationServices.Active;
/// <summary> Manages the different types of LocksService objects.</summary>
public LocksServicesContainerMutable LocksServices { get; set; }
/// <summary> Holds available app themes.</summary>
public ServicesContainerMutable<IGeolocation> GeolocationServices { get; }
/// <summary> Manages the different types of LocksService objects.</summary>
public ServicesContainerMutable<object> Themes { get; }
/// <summary> Object to switch logging level. </summary>
private LoggingLevelSwitch m_oLoggingLevelSwitch;
/// <summary>
/// Object to allow swithing logging level
/// </summary>
public LoggingLevelSwitch Level
{
get
{
if (m_oLoggingLevelSwitch == null)
{
m_oLoggingLevelSwitch = new LoggingLevelSwitch
{
// Set warning level to error.
MinimumLevel = Settings.Settings.DEFAULTLOGGINLEVEL
};
}
return m_oLoggingLevelSwitch;
}
}
/// <summary> Updates logging level. </summary>
/// <param name="p_oNewLevel">New level to set.</param>
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();
}
}
}

View file

@ -0,0 +1,91 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace TINK.Model.User.Account
{
/// <summary> Specifies extra user permissions. </summary>
[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,
}
/// <summary>
/// Specifies parts of account data.
/// </summary>
/// <remarks>
/// Usage: Account can be valid (user and password set) partly valid or completely invalid.
/// </remarks>
[Flags]
public enum Elements
{
None = 0,
Mail = 1,
Password = 2,
Account = Mail + Password
}
/// <summary>
/// Holds account data.
/// </summary>
public class Account : IAccount
{
/// <summary> Constructs an account object.</summary>
/// <param name="p_oMail">Mail addresss.</param>
/// <param name="p_Pwd">Password.</param>
/// <param name="p_oSessionCookie">Session cookie from copri.</param>
/// <param name="p_strGroup">Group holdig info about Group (TINK, Konrad, ...)</param>
/// <param name="p_iDebugLevel">Flag which controls display of debug settings.</param>
public Account(
string p_oMail,
string p_Pwd,
string p_oSessionCookie,
IEnumerable<string> p_strGroup,
Permissions debugLevel = Permissions.None)
{
Mail = p_oMail;
Pwd = p_Pwd;
SessionCookie = p_oSessionCookie;
DebugLevel = debugLevel;
Group = p_strGroup != null
? new HashSet<string>(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)
{
}
/// <summary>Mail address.</summary>
public string Mail { get; }
/// <summary>Password of the account.</summary>
public string Pwd { get; }
/// <summary>Session cookie used to sign in to copri.</summary>
public string SessionCookie { get; }
/// <summary>Debug level used to determine which features are available.</summary>
public Permissions DebugLevel { get; }
/// <summary> Holds the group of the bike (TINK, Konrad, ...).</summary>
public IEnumerable<string> Group { get; }
}
}

View file

@ -0,0 +1,34 @@
using System.Collections.Generic;
using TINK.Model.Connector.Filter;
namespace TINK.Model.User.Account
{
public static class AccountExtensions
{
/// <summary> Gets information whether user is logged in or not from account object. </summary>
/// <param name="p_oAccount">Object to get information from.</param>
/// <returns>True if user is logged in, false if not.</returns>
public static bool GetIsLoggedIn(this IAccount p_oAccount)
{
return !string.IsNullOrEmpty(p_oAccount.Mail)
&& !string.IsNullOrEmpty(p_oAccount.SessionCookie);
}
/// <summary>
/// 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.
/// </summary>
/// <param name="account">Account to filter with.</param>
/// <param name="filter">Groups to filter.</param>
/// <returns>Filtered bike groups.</returns>
public static IEnumerable<string> DoFilter(
this IAccount account,
IEnumerable<string> 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.
}
}
}

View file

@ -0,0 +1,74 @@
using System.Collections.Generic;
namespace TINK.Model.User.Account
{
/// <summary>
/// Holds email address and password.
/// </summary>
public class AccountMutable : IAccount
{
/// <summary>
/// Holds the account data.
/// </summary>
private Account m_oAccount;
/// <summary> Prevents an invalid instance to be created. </summary>
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);
}
/// <summary>
/// Mail address.
/// </summary>
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); }
}
/// <summary>
/// Password of the account.
/// </summary>
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); }
}
/// <summary>
/// Session cookie used to sign in to copri.
/// </summary>
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); }
}
/// <summary>
/// Holds the group of the bike (TINK, Konrad, ...).
/// </summary>
public IEnumerable<string> Group
{
get { return m_oAccount.Group; }
}
/// <summary>
/// Debug level used to determine which features are available.
/// </summary>
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); }
}
}
}

View file

@ -0,0 +1,18 @@
using System.Collections.Generic;
namespace TINK.Model.User.Account
{
/// <summary> Represents an empty account.</summary>
public class EmptyAccount : IAccount
{
public string Mail => null;
public string Pwd => null;
public string SessionCookie => null;
public Permissions DebugLevel => Permissions.None;
public IEnumerable<string> Group => new List<string>();
}
}

View file

@ -0,0 +1,25 @@
using System.Collections.Generic;
namespace TINK.Model.User.Account
{
/// <summary>
/// Holds account data.
/// </summary>
public interface IAccount
{
/// <summary>Mail address.</summary>
string Mail { get; }
/// <summary>Password of the account.</summary>
string Pwd { get; }
/// <summary>Session cookie used to sign in to copri.</summary>
string SessionCookie { get; }
/// <summary>Debug level used to determine which features are available.</summary>
Permissions DebugLevel { get; }
/// <summary> Holds the group of the bike (TINK, Konrad, ...).</summary>
IEnumerable<string> Group { get; }
}
}

View file

@ -0,0 +1,26 @@
namespace TINK.Model.User.Account
{
/// <summary>
/// Interface to manage an account store.
/// </summary>
public interface IStore
{
/// <summary>
/// Reads mail address and password from account store.
/// </summary>
/// <returns></returns>
IAccount Load();
/// <summary>
/// Writes mail address and password to account store.
/// </summary>
/// <param name="p_oMailAndPwd"></param>
void Save(IAccount p_oMailAndPwd);
/// <summary>
/// Deletes mail address and password from account store.
/// </summary>
/// <returns> Empty account instance if deleting succeeded.</returns>
IAccount Delete(IAccount p_oMailAndPwd);
}
}

View file

@ -0,0 +1,167 @@
using System;
using System.Collections.Generic;
using System.Linq;
namespace TINK.Model.User.Account
{
/// <summary>
/// Holds the state of mail and password and information about some invalid parts if there are.
/// </summary>
public class State
{
/// <summary>
/// Consider state to be invalid after construction.
/// </summary>
private Elements m_eElements = Elements.None;
private Dictionary<Elements, string> m_oDescription = new Dictionary<Elements, string>();
/// <summary>
/// Constructs object to state all entries are valid.
/// </summary>
public State()
{
m_eElements = Elements.Account;
m_oDescription = new Dictionary<Elements, string>
{
{ Elements.None, string.Empty },
{ Elements.Account, string.Empty }
};
}
/// <summary>
/// Constructs object to state some/ all elements are invalid.
/// </summary>
/// <param name="p_oValidParts">Specifies the parts which are invalid.</param>
/// <param name="p_oDescription">Description of invalid parts.</param>
public State(Elements p_oValidParts, Dictionary<Elements, string> p_oDescription)
{
m_eElements = p_oValidParts;
m_oDescription = p_oDescription ?? new Dictionary<Elements, string>();
// 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);
}
}
}
/// <summary>
/// True if account is valid.
/// </summary>
public bool IsValid { get { return ValidElement == Elements.Account; } }
/// <summary>
/// Specifies if both mail and password are valid, one of them or none.
/// </summary>
public Elements ValidElement
{
get
{
return m_eElements;
}
}
/// <summary>
/// Holds the message about invalid elements.
/// </summary>
public Dictionary<Elements, string> Description
{
get
{
var l_oUserFriendlyDescription = new Dictionary<Elements, string>();
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 ;
}
}
}
/// <summary>
/// Verifies if a password is valid or not.
/// </summary>
/// <param name="p_strMail"></param>
/// <param name="p_strPassword"></param>
/// <returns></returns>
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<Elements, string>();
// 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);
}
}
}

View file

@ -0,0 +1,16 @@
using TINK.Model.User.Account;
namespace TINK.Model.User
{
public interface IUser
{
/// <summary> Holds a value indicating whether user is logged in or not.</summary>
bool IsLoggedIn { get; }
/// <summary> Holds the mail address. </summary>
string Mail { get; }
/// <summary>Holds the debug level.</summary>
Permissions DebugLevel { get; }
}
}

154
TINKLib/Model/User/User.cs Normal file
View file

@ -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);
/// <summary>
/// Manages user of the app.
/// </summary>
public class User : IUser
{
/// <summary>
/// Holds account data.
/// </summary>
private readonly AccountMutable m_oAccount;
/// <summary>
/// Provides storing functionality.
/// </summary>
private IStore m_oStore;
/// <summary> Holds the id of the device. </summary>
public string DeviceId { get; }
/// Loads user name and passwort from account store.
/// </summary>
/// <param name="p_oAccountStore"> Object to use for loading and saving user data.</param>
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);
}
/// <summary> Is fired wheneverlogin state changes. </summary>
public event LoginStateChangedDelegate StateChanged;
/// <summary>
/// Holds a value indicating whether user is logged in or not.
/// </summary>
public bool IsLoggedIn {
get
{
return m_oAccount.GetIsLoggedIn();
}
}
/// <summary>
/// Holds the mail address.
/// </summary>
public string Mail
{
get { return m_oAccount.Mail; }
}
/// <summary>
/// Gets the sessiong cookie.
/// </summary>
public string SessionCookie
{
get { return m_oAccount.SessionCookie; }
}
/// <summary>
/// Holds the password.
/// </summary>
public string Password
{
get { return m_oAccount.Pwd; }
}
/// <summary>Holds the debug level.</summary>
public Permissions DebugLevel
{
get { return m_oAccount.DebugLevel; }
}
/// <summary> Holds the group of the bike (TINK, Konrad, ...).</summary>
public IEnumerable<string> Group { get { return m_oAccount.Group; } }
/// <summary> Logs in user. </summary>
/// <param name="p_oAccount">Account to use for login.</param>
/// <param name="p_str_DeviceId">Holds the Id to identify the device.</param>
/// <param name="isConnected">True if connector has access to copri server, false if cached values are used.</param>
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]);
}
}
/// <summary> Logs in user. </summary>
/// <param name="p_oAccount">Account to use for login.</param>
/// <param name="p_str_DeviceId">Holds the Id to identify the device.</param>
/// <param name="isConnected">True if connector has access to copri server, false if cached values are used.</param>
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());
}
/// <summary> Logs in user</summary>
/// <returns></returns>
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());
}
/// <summary>
/// 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.
/// </summary>
/// <param name="p_oAccount">Account to filter with.</param>
/// <param name="p_oSource">Groups to filter..</param>
/// <returns>Filtered bike groups.</returns>
public IEnumerable<string> DoFilter(IEnumerable<string> p_oSource = null)
{
return m_oAccount.DoFilter(p_oSource);
}
}
}

View file

@ -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.")
{
}
}
}

454
TINKLib/Model/WhatsNew.cs Normal file
View file

@ -0,0 +1,454 @@
using System;
using System.Collections.Generic;
using TINK.MultilingualResources;
namespace TINK.Model
{
/// <summary> Holds information about TINKApp development. </summary>
public class WhatsNew
{
private static readonly Version AGBMODIFIEDBUILD = new Version(3, 0, 131);
/// <summary>Change of of think App.</summary>
private Dictionary<Version /* when change was introduced*/, string> WhatsNewMessages = new Dictionary<Version, string>
{
{
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
}
};
/// <summary> Manges the whats new information.</summary>
/// <param name="currentVersion">Current version of the app.</param>
/// <param name="shownInVersion">Null or version in which whats new dialog was shown last.</param>
public WhatsNew(
Version currentVersion,
Version lastVersion,
Version shownInVersion)
{
WasShownVersion = shownInVersion;
LastVersion = lastVersion;
CurrentVersion = currentVersion;
}
/// <summary> Retruns a new WhatsNew object with property was shown set to true. </summary>
/// <returns></returns>
public WhatsNew SetWasShown()
{
return new WhatsNew(CurrentVersion, LastVersion, CurrentVersion);
}
/// <summary> Holds the information in which version of the app the whats new dialog has been shown.</summary>
private Version WasShownVersion { get; }
/// <summary> Holds the information in which version of the app the whats new dialog has been shown.</summary>
private Version CurrentVersion { get; }
private Version LastVersion;
/// <summary> Holds information whether whats new page was already shown or not.</summary>
public bool IsShowRequired
{
get
{
if (CurrentVersion == null)
{
// Unexpected state detected.
return false;
}
if (LastVersion == null)
{
// Initial install detected.
return false;
}
return (WasShownVersion ?? LastVersion) < CurrentVersion;
}
}
/// <summary> True if info about modified agb has to be displayed. </summary>
public bool IsShowAgbRequired => (WasShownVersion ?? AGBMODIFIEDBUILD) < AGBMODIFIEDBUILD;
/// <summary> Get the whats new text depening of version gap.</summary>
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 += $"<p><b>{l_oInfo.Key}</b><br/>{l_oInfo.Value}</p>";
}
return whatsNew;
}
}
}
}

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,546 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ActionBookOrClose" xml:space="preserve">
<value>Rad mieten oder Schloss schließen</value>
</data>
<data name="ActionCancelRequest" xml:space="preserve">
<value>Reservierung aufheben</value>
</data>
<data name="ActionClose" xml:space="preserve">
<value>Schloss schließen</value>
</data>
<data name="ActionCloseAndReturn" xml:space="preserve">
<value>Schloss schließen &amp; Miete beenden</value>
</data>
<data name="ActionOpen" xml:space="preserve">
<value>Schloss öffnen</value>
</data>
<data name="ActionOpenAndBook" xml:space="preserve">
<value>Schloss öffnen &amp; Rad mieten</value>
</data>
<data name="ActionOpenAndPause" xml:space="preserve">
<value>Schloss öffnen &amp; Miete fortsetzen</value>
</data>
<data name="ActionRequest" xml:space="preserve">
<value>Rad reservieren</value>
</data>
<data name="ActionReturn" xml:space="preserve">
<value>Miete beenden</value>
</data>
<data name="MarkingMapPage" xml:space="preserve">
<value>Fahrradstandorte</value>
</data>
<data name="MessageLoginWelcome" xml:space="preserve">
<value>Benutzer {0} erfolgreich angemeldet.</value>
</data>
<data name="MessageLoginWelcomeTitle" xml:space="preserve">
<value>Willkommen!</value>
</data>
<data name="MessageLoginWelcomeTitleGroup" xml:space="preserve">
<value>Wilkommen bei {0}!</value>
</data>
<data name="MarkingLoggedInStateInfoLoggedIn" xml:space="preserve">
<value>Angemeldet als {0}.</value>
</data>
<data name="MarkingLoggedInStateInfoLoggedInGroup" xml:space="preserve">
<value>Angemeldet als {0} bei {1}.</value>
</data>
<data name="MarkingLoggedInStateInfoNotLoggedIn" xml:space="preserve">
<value>Kein Benutzer angemeldet.</value>
</data>
<data name="MessageAppVersionIsOutdated" xml:space="preserve">
<value>Diese version der {0} App ist veraltet. Bitte auf aktuelle Version aktualisieren.</value>
</data>
<data name="QuestionSupportmailSubject" xml:space="preserve">
<value>Betrifft die Anfrage/ Anmerkung die {0}-App oder ein allgemeines Thema?</value>
</data>
<data name="QuestionSupportmailAnswerApp" xml:space="preserve">
<value>{0}-App Anfrage</value>
</data>
<data name="QuestionSupportmailAnswerOperator" xml:space="preserve">
<value>{0} Anfrage</value>
</data>
<data name="MarkingAbout" xml:space="preserve">
<value>Über {0}</value>
</data>
<data name="MarkingAccount" xml:space="preserve">
<value>Konto</value>
</data>
<data name="MarkingFeedbackAndContact" xml:space="preserve">
<value>Kontakt</value>
</data>
<data name="MarkingLogin" xml:space="preserve">
<value>Anmelden</value>
</data>
<data name="MarkingMyBikes" xml:space="preserve">
<value>Meine Räder</value>
</data>
<data name="MarkingFeesAndBikes" xml:space="preserve">
<value>Bedienung</value>
</data>
<data name="MarkingSettings" xml:space="preserve">
<value>Einstellungen</value>
</data>
<data name="MarkingTabBikes" xml:space="preserve">
<value>Bedienung</value>
</data>
<data name="MarkingTabFees" xml:space="preserve">
<value>Tarife</value>
</data>
<data name="ErrorOpenLockMessage" xml:space="preserve">
<value>Schloss ist blockiert. Bitte Ursache von Blockierung beheben und Vorgang wiederholen.</value>
</data>
<data name="ErrorCloseLockBoldBlockedMessage" xml:space="preserve">
<value>Schloss ist blockiert. Bitte sicherstellen, dass keine Speiche oder ein anderer Gegenstand das Schloss blockiert und Vorgang wiederholen.</value>
</data>
<data name="ErrorCloseLockMovingMessage" xml:space="preserve">
<value>Schloss kann erst geschlossen werden, wenn Rad nicht mehr bewegt wird. Bitte Rad abstellen und Vorgang wiederholen.</value>
</data>
<data name="ErrorBookedSearchMessage" xml:space="preserve">
<value>Schloss des gemieteten Rads kann nicht gefunden werden.</value>
</data>
<data name="ErrorReservedSearchMessage" xml:space="preserve">
<value>Schloss des reservierten Rads kann nicht gefunden werden.</value>
</data>
<data name="ActivityTextBikesAtStationGetBikes" xml:space="preserve">
<value>Lade Räder an Station...</value>
</data>
<data name="ActivityTextMyBikesLoadingBikes" xml:space="preserve">
<value>Lade meine Räder...</value>
</data>
<data name="ActivityTextSearchBikes" xml:space="preserve">
<value>Suche Schlösser...</value>
</data>
<data name="ActivityTextMyBikesCheckBluetoothState" xml:space="preserve">
<value>Prüfe Berechtigungen...</value>
</data>
<data name="ActivityTextCenterMap" xml:space="preserve">
<value>Zentriere Karte...</value>
</data>
<data name="ActivityTextMapLoadingStationsAndBikes" xml:space="preserve">
<value>Lade Stationen und Räder...</value>
</data>
<data name="ErrorReturnBikeNotAtStationMessage" xml:space="preserve">
<value>Rückgabe ausserhalb von Station nicht möglich. Entfernung zur Station {0} ist {1} m.</value>
</data>
<data name="ErrorReturnBikeNotAtStationTitle" xml:space="preserve">
<value>Fehler bei Radrückgabe!</value>
</data>
<data name="ErrorReturnBikeLockClosedNoGPSMessage" xml:space="preserve">
<value>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.</value>
</data>
<data name="ErrorReturnBikeLockOpenNoGPSMessage" xml:space="preserve">
<value>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.</value>
</data>
<data name="ErrorSupportmailCreateAttachment" xml:space="preserve">
<value>Mailanhang konnte nich erzeugt werden.</value>
</data>
<data name="ErrorSupportmailMailingFailed" xml:space="preserve">
<value>Mailapp konnte nicht geöffnet werden.</value>
</data>
<data name="ErrorSupportmailPhoningFailed" xml:space="preserve">
<value>Telefonapp konnte nicht geöffnet werden.</value>
</data>
<data name="MessageAnswerOk" xml:space="preserve">
<value>OK</value>
</data>
<data name="MessageContactMail" xml:space="preserve">
<value>Fragen? Hinweise? Kritik?</value>
</data>
<data name="MessagePhoneMail" xml:space="preserve">
<value>Eilige Frage rund um {0}? (Montag-Freitag: 10:00 18:00)</value>
</data>
<data name="MessageRateMail" xml:space="preserve">
<value>Gefällt die {0}-App?</value>
</data>
<data name="MessageWaring" xml:space="preserve">
<value>Warnung</value>
</data>
<data name="QuestionAnswerNo" xml:space="preserve">
<value>Nein</value>
</data>
<data name="QuestionAnswerYes" xml:space="preserve">
<value>Ja</value>
</data>
<data name="QuestionSupportmailAttachment" xml:space="preserve">
<value>Soll der Mail eine Datei mit Diagnoseinformationen angehängt werden?</value>
</data>
<data name="QuestionTitle" xml:space="preserve">
<value>Frage</value>
</data>
<data name="MessageMapPageErrorAuthcookieUndefined" xml:space="preserve">
<value>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.</value>
</data>
<data name="MarkingBikesAtStationTitle" xml:space="preserve">
<value>Fahrradstandort {0}</value>
</data>
<data name="StatusTextReservationExpiredCodeMaxReservationTime" xml:space="preserve">
<value>Code ist {0}, max. Reservierungszeit von {1} Min. abgelaufen.
</value>
</data>
<data name="StatusTextReservationExpiredCodeRemaining" xml:space="preserve">
<value>Code ist {0}, noch {1} Minuten reserviert.
</value>
</data>
<data name="StatusTextBooked" xml:space="preserve">
<value>Rad ist gemietet.</value>
</data>
<data name="StatusTextBookedSince" xml:space="preserve">
<value>Gemietet seit {0}.</value>
</data>
<data name="StatusTextReservationExpiredRemaining" xml:space="preserve">
<value>Noch {0} Minuten reserviert.</value>
</data>
<data name="StatusTextReservationExpiredMaximumReservationTime" xml:space="preserve">
<value>Max. Reservierungszeit von {0} Min. abgelaufen.</value>
</data>
<data name="StatusTextBookedCodeSince" xml:space="preserve">
<value>Code ist {0}, gemietet seit {1}.</value>
</data>
<data name="StatusTextReservationExpiredLocationMaxReservationTime" xml:space="preserve">
<value>Standort {0}, max. Reservierungszeit von {1} Min. abgelaufen.
</value>
</data>
<data name="StatusTextAvailable" xml:space="preserve">
<value>Frei.</value>
</data>
<data name="StatusTextBookedCodeLocationSince" xml:space="preserve">
<value>Code {0}, Standort {1}, gemietet seit {2}.
</value>
</data>
<data name="StatusTextReservationExpiredCodeLocationMaxReservationTime" xml:space="preserve">
<value>Code ist {0}, Standort {1}, max. Reservierungszeit von {2} Min. abgelaufen.
</value>
</data>
<data name="StatusTextReservationExpiredCodeLocationReservationTime" xml:space="preserve">
<value>Code ist {0}, Standort {1}, noch {2} Minuten reserviert.
</value>
</data>
<data name="StatusTextReservationExpiredLocationReservationTime" xml:space="preserve">
<value>Standort {0}, noch {1} Minuten reserviert.
</value>
</data>
<data name="ActionLoginLogin" xml:space="preserve">
<value>Anmelden</value>
</data>
<data name="ActionLoginPasswordForgotten" xml:space="preserve">
<value>Passwort vergessen?</value>
</data>
<data name="ActionLoginRegister" xml:space="preserve">
<value>Registrieren</value>
</data>
<data name="MarkingLoginEmailAddressLabel" xml:space="preserve">
<value>E-Mail Adresse</value>
</data>
<data name="MarkingLoginEmailAddressPlaceholder" xml:space="preserve">
<value>E-Mail Adresse</value>
</data>
<data name="MarkingLoginPasswordLabel" xml:space="preserve">
<value>Passwort, Mindestlänge 8 Zeichen</value>
</data>
<data name="MarkingLoginPasswordPlaceholder" xml:space="preserve">
<value>Passwort</value>
</data>
<data name="ActionContactRate" xml:space="preserve">
<value>Bewertung abgeben</value>
</data>
<data name="MarkingLoginInstructions" xml:space="preserve">
<value>Anleitung TINK Räder</value>
</data>
<data name="MarkingLoginInstructionsTinkKonradMessage" xml:space="preserve">
<value>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.</value>
</data>
<data name="MarkingLoginInstructionsTinkKonradTitle" xml:space="preserve">
<value>Zur Information!</value>
</data>
<data name="MessageLoginConnectionErrorMessage" xml:space="preserve">
<value>Anmeldungskeks darf nicht leer sein. {0}</value>
</data>
<data name="MessageLoginConnectionErrorTitle" xml:space="preserve">
<value>Verbindungsfehler beim Registrieren!</value>
</data>
<data name="MessageLoginErrorTitle" xml:space="preserve">
<value>Fehler bei der Anmeldung!</value>
</data>
<data name="MessageLoginRecoverPassword" xml:space="preserve">
<value>Bitte mit dem Internet verbinden zum Wiederherstellen des Passworts.</value>
</data>
<data name="MessageLoginRegisterNoNet" xml:space="preserve">
<value>Bitte mit dem Internet verbinden zum Registrieren.</value>
</data>
<data name="MessageTitleHint" xml:space="preserve">
<value>Hinweis</value>
</data>
<data name="ErrorOpenLockStillClosedMessage" xml:space="preserve">
<value>Nach Versuch Schloss zu öffnen wird Status geschlossen zurückgemeldet.</value>
</data>
<data name="ErrorOpenLockOutOfReadMessage" xml:space="preserve">
<value>Schloss kann erst geöffnet werden, wenn Rad in der Nähe ist.</value>
</data>
<data name="ErrorOpenLockTitle" xml:space="preserve">
<value>Schloss kann nicht geöffnet werden!</value>
</data>
<data name="ErrorCloseLockOutOfReachMessage" xml:space="preserve">
<value>Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist. </value>
</data>
<data name="ErrorCloseLockOutOfReachStateReservedMessage" xml:space="preserve">
<value>Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist.
Bitte Schließen nochmals versuchen oder Rad dem Support melden!</value>
</data>
<data name="ErrorCloseLockTitle" xml:space="preserve">
<value>Schloss kann nicht geschlossen werden!</value>
</data>
<data name="ErrorCloseLockStillOpenMessage" xml:space="preserve">
<value>Nach Versuch Schloss zu schließen wird Status geöffnet zurückgemeldet.</value>
</data>
<data name="ErrorCloseLockUnexpectedStateMessage" xml:space="preserve">
<value>Schloss meldet Status "{0}".</value>
</data>
<data name="ErrorCloseLockUnkErrorMessage" xml:space="preserve">
<value>Bitte schließen nochmals versuchen oder Rad dem Support melden!
{0}</value>
</data>
<data name="MessageBikesManagementLocationPermission" xml:space="preserve">
<value>Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können.</value>
</data>
<data name="MessageBikesManagementLocationPermissionOpenDialog" xml:space="preserve">
<value>Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können.
Freigabedialog öffen?</value>
</data>
<data name="MessageCenterMapLocationPermissionOpenDialog" xml:space="preserve">
<value>Bitte Standortfreigabe erlauben, damit Karte zentriert werden werden kann.
Freigabedialog öffen?</value>
</data>
<data name="MessageBikesManagementLocationActivation" xml:space="preserve">
<value>Bitte Standort aktivieren, damit Fahrradschloss gefunden werden kann!</value>
</data>
<data name="MessageAnswerNo" xml:space="preserve">
<value>Nein</value>
</data>
<data name="MessageAnswerYes" xml:space="preserve">
<value>Ja</value>
</data>
<data name="MessageBikesManagementBluetoothActivation" xml:space="preserve">
<value>Bitte Bluetooth aktivieren, damit Fahrradschloss/ Schlösser verwaltet werden können.</value>
</data>
<data name="ActionSearchLock" xml:space="preserve">
<value>Schloss suchen</value>
</data>
<data name="ActivityTextOneMomentPlease" xml:space="preserve">
<value>Einen Moment bitte...</value>
</data>
<data name="ActivityTextOpeningLock" xml:space="preserve">
<value>Öffne Schloss...</value>
</data>
<data name="ActivityTextStartingUpdater" xml:space="preserve">
<value>Starte Aktualisierung...</value>
</data>
<data name="ActivityTextStartingUpdatingLockingState" xml:space="preserve">
<value>Aktualisiere Schlossstatus...</value>
</data>
<data name="ActivityTextReadingChargingLevel" xml:space="preserve">
<value>Lese Akkustatus...</value>
</data>
<data name="ActivityTextErrorStatusUpdateingLockstate" xml:space="preserve">
<value>Statusfehler beim Aktualisieren des Schlossstatusses.</value>
</data>
<data name="ActivityTextErrorConnectionUpdateingLockstate" xml:space="preserve">
<value>Verbingungsfehler beim Aktualisieren des Schlossstatusses.</value>
</data>
<data name="ActivityTextErrorNoWebUpdateingLockstate" xml:space="preserve">
<value>Kein Netz beim Aktualisieren des Schlossstatusses.</value>
</data>
<data name="ActivityTextClosingLock" xml:space="preserve">
<value>Schließe Schloss...</value>
</data>
<data name="ChangeLog3_0_203" xml:space="preserve">
<value>Aktualisierrt auf aktuelle Schloss-Firmware.</value>
</data>
<data name="ChangeLog3_0_204" xml:space="preserve">
<value>Bluetooth Kommunikation verbessert.</value>
</data>
<data name="ChangeLog3_0_205" xml:space="preserve">
<value>Stationssymbole verbessert.</value>
</data>
<data name="ChangeLog3_0_206" xml:space="preserve">
<value>Bluetooth- und Geolocation-Funktionalität verbessert.</value>
</data>
<data name="ChangeLog3_0_207" xml:space="preserve">
<value>Kleinere Fehlerbehebungen.
Software Pakete aktualisiert.
Zielplatform Android 11.</value>
</data>
<data name="MessageOpenLockAndBookeBike" xml:space="preserve">
<value>Fahrrad {0} mieten und Schloss öffnen?</value>
</data>
<data name="ActivityTextReservingBike" xml:space="preserve">
<value>Reserviere Rad...</value>
</data>
<data name="ActivityTextErrorReadingChargingLevelGeneral" xml:space="preserve">
<value>Akkustatus kann nicht gelesen werden.</value>
</data>
<data name="ActivityTextErrorReadingChargingLevelOutOfReach" xml:space="preserve">
<value>Akkustatus kann erst gelesen werden, wenn Rad in der Nähe ist.</value>
</data>
<data name="ActivityTextRentingBike" xml:space="preserve">
<value>Miete Rad...</value>
</data>
<data name="MessageRentingBikeErrorConnectionTitle" xml:space="preserve">
<value>Verbingungsfehler beim Mieten des Rads!</value>
</data>
<data name="MessageRentingBikeErrorGeneralTitle" xml:space="preserve">
<value>Fehler beim Mieten des Rads!</value>
</data>
<data name="MessageRentingBikeErrorTooManyReservationsRentals" xml:space="preserve">
<value>Eine Miete des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden.</value>
</data>
<data name="MessageReservationBikeErrorTooManyReservationsRentals" xml:space="preserve">
<value>Eine Reservierung des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden.</value>
</data>
<data name="MessageErrorLockIsClosedThreeLines" xml:space="preserve">
<value>Achtung: Schloss wird geschlossen!
{0}
{1}</value>
</data>
<data name="MessageErrorLockIsClosedTwoLines" xml:space="preserve">
<value>Achtung: Schloss wird geschlossen!
{0}</value>
</data>
<data name="ExceptionTextRentingBikeFailedGeneral" xml:space="preserve">
<value>Die Miete des Fahrads Nr. {0} ist fehlgeschlagen.</value>
</data>
<data name="ExceptionTextRentingBikeFailedUnavailalbe" xml:space="preserve">
<value>Die Miete des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar.</value>
</data>
<data name="ExceptionTextReservationBikeFailedGeneral" xml:space="preserve">
<value>Die Reservierung des Fahrads Nr. {0} ist fehlgeschlagen.</value>
</data>
<data name="ExceptionTextReservationBikeFailedUnavailalbe" xml:space="preserve">
<value>Die Reservierung des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar.</value>
</data>
<data name="ChangeLog3_0_208" xml:space="preserve">
<value>Kleinere Fehlerbehebungen.</value>
</data>
<data name="ActivityTextDisconnectingLock" xml:space="preserve">
<value>Trenne Schloss...</value>
</data>
<data name="ActivityTextErrorDisconnect" xml:space="preserve">
<value>Fehler beim Trennen...</value>
</data>
<data name="MarkingBikeInfoErrorStateDisposableClosedDetected" xml:space="preserve">
<value>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.</value>
</data>
<data name="MarkingBikeInfoErrorStateUnknownDetected" xml:space="preserve">
<value>Ungültiger Mietstatus von Rad {0} erkannt.
</value>
</data>
<data name="ActivityTextCancelingReservation" xml:space="preserve">
<value>Reservierung aufheben...</value>
</data>
<data name="QuestionCancelReservation" xml:space="preserve">
<value>Reservierung für Fahrrad {0} aufheben?</value>
</data>
<data name="ChangeLog3_0_209" xml:space="preserve">
<value>Kleinere Fehlerbehebung: Verbindung wird getrennt, sobald Rad verfügbar ist.</value>
</data>
<data name="QuestionReserveBike" xml:space="preserve">
<value>Fahrrad {0} kostenlos für {1} Min. reservieren?</value>
</data>
<data name="ActivityTextErrorDeserializationException" xml:space="preserve">
<value>Verbindungsfehler: Deserialisierung fehlgeschlagen.</value>
</data>
<data name="ActivityTextErrorException" xml:space="preserve">
<value>Verbindung unterbrochen.</value>
</data>
<data name="ActivityTextErrorInvalidResponseException" xml:space="preserve">
<value>Verbindungsfehler, ungülige Serverantwort.</value>
</data>
<data name="ActivityTextErrorWebConnectFailureException" xml:space="preserve">
<value>Verbindung unterbrochen, Server nicht erreichbar.</value>
</data>
<data name="ActivityTextErrorWebException" xml:space="preserve">
<value>Verbindungsfehler. Code: {0}.</value>
</data>
<data name="ActivityTextErrorWebForbiddenException" xml:space="preserve">
<value>Verbindung unterbrochen, Server beschäftigt.</value>
</data>
<data name="MessageBikesManagementTariffDescriptionAboEuroPerMonth" xml:space="preserve">
<value>Abo-Preis</value>
</data>
<data name="MessageBikesManagementTariffDescriptionFeeEuroPerHour" xml:space="preserve">
<value>Mietgebühren</value>
</data>
<data name="MessageBikesManagementTariffDescriptionFreeTimePerSession" xml:space="preserve">
<value>Gratis Nutzung</value>
</data>
<data name="MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay" xml:space="preserve">
<value>Max. Gebühr</value>
</data>
<data name="MessageBikesManagementTariffDescriptionEuroPerHour" xml:space="preserve">
<value>€/Std.</value>
</data>
<data name="MessageBikesManagementTariffDescriptionHour" xml:space="preserve">
<value>Std./Tag</value>
</data>
<data name="MessageBikesManagementTariffDescriptionTariffHeader" xml:space="preserve">
<value>Tarif {0}, Nr. {1}</value>
</data>
<data name="ActivityTextQuerryServer" xml:space="preserve">
<value>Anfrage Server...</value>
</data>
<data name="ActivityTextSearchingLock" xml:space="preserve">
<value>Suche Schloss...</value>
</data>
<data name="ChangeLog3_0_214" xml:space="preserve">
<value>Mehrbetreiber Unterstützung.</value>
</data>
<data name="ChangeLog3_0_215" xml:space="preserve">
<value>Layout des "Whats New"-Dialos verbessert :-)</value>
</data>
<data name="MessageBikesManagementMaxFeeEuroPerDay" xml:space="preserve">
<value>€/Tag</value>
</data>
<data name="ChangeLog3_0_216" xml:space="preserve">
<value>Oberflächenlayout verbessert.</value>
</data>
<data name="ChangeLog3_0_217" xml:space="preserve">
<value>Pakete aktualisiert.</value>
</data>
<data name="ChangeLog3_0_218" xml:space="preserve">
<value>Kleine Verbesserungen.</value>
</data>
<data name="ChangeLog3_0_219" xml:space="preserve">
<value>Icons zum Flyout-Menü hinzugefügt.</value>
</data>
<data name="ChangeLog3_0_220" xml:space="preserve">
<value>Oberflächenlayout verbessert.</value>
</data>
<data name="ChangeLog3_0_221" xml:space="preserve">
<value>Oberflächenlayout verbessert.</value>
</data>
<data name="ChangeLog3_0_222" xml:space="preserve">
<value>Kleine Fehlerbehebungen und Verbesserungen.</value>
</data>
</root>

View file

@ -0,0 +1,643 @@
<?xml version="1.0" encoding="utf-8"?>
<root>
<!--
Microsoft ResX Schema
Version 2.0
The primary goals of this format is to allow a simple XML format
that is mostly human readable. The generation and parsing of the
various data types are done through the TypeConverter classes
associated with the data types.
Example:
... ado.net/XML headers & schema ...
<resheader name="resmimetype">text/microsoft-resx</resheader>
<resheader name="version">2.0</resheader>
<resheader name="reader">System.Resources.ResXResourceReader, System.Windows.Forms, ...</resheader>
<resheader name="writer">System.Resources.ResXResourceWriter, System.Windows.Forms, ...</resheader>
<data name="Name1"><value>this is my long string</value><comment>this is a comment</comment></data>
<data name="Color1" type="System.Drawing.Color, System.Drawing">Blue</data>
<data name="Bitmap1" mimetype="application/x-microsoft.net.object.binary.base64">
<value>[base64 mime encoded serialized .NET Framework object]</value>
</data>
<data name="Icon1" type="System.Drawing.Icon, System.Drawing" mimetype="application/x-microsoft.net.object.bytearray.base64">
<value>[base64 mime encoded string representing a byte array form of the .NET Framework object]</value>
<comment>This is a comment</comment>
</data>
There are any number of "resheader" rows that contain simple
name/value pairs.
Each data row contains a name, and value. The row also contains a
type or mimetype. Type corresponds to a .NET class that support
text/value conversion through the TypeConverter architecture.
Classes that don't support this are serialized and stored with the
mimetype set.
The mimetype is used for serialized objects, and tells the
ResXResourceReader how to depersist the object. This is currently not
extensible. For a given mimetype the value must be set accordingly:
Note - application/x-microsoft.net.object.binary.base64 is the format
that the ResXResourceWriter will generate, however the reader can
read any of the formats listed below.
mimetype: application/x-microsoft.net.object.binary.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Binary.BinaryFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.soap.base64
value : The object must be serialized with
: System.Runtime.Serialization.Formatters.Soap.SoapFormatter
: and then encoded with base64 encoding.
mimetype: application/x-microsoft.net.object.bytearray.base64
value : The object must be serialized into a byte array
: using a System.ComponentModel.TypeConverter
: and then encoded with base64 encoding.
-->
<xsd:schema id="root" xmlns="" xmlns:xsd="http://www.w3.org/2001/XMLSchema" xmlns:msdata="urn:schemas-microsoft-com:xml-msdata">
<xsd:import namespace="http://www.w3.org/XML/1998/namespace" />
<xsd:element name="root" msdata:IsDataSet="true">
<xsd:complexType>
<xsd:choice maxOccurs="unbounded">
<xsd:element name="metadata">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" />
</xsd:sequence>
<xsd:attribute name="name" use="required" type="xsd:string" />
<xsd:attribute name="type" type="xsd:string" />
<xsd:attribute name="mimetype" type="xsd:string" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="assembly">
<xsd:complexType>
<xsd:attribute name="alias" type="xsd:string" />
<xsd:attribute name="name" type="xsd:string" />
</xsd:complexType>
</xsd:element>
<xsd:element name="data">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
<xsd:element name="comment" type="xsd:string" minOccurs="0" msdata:Ordinal="2" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" msdata:Ordinal="1" />
<xsd:attribute name="type" type="xsd:string" msdata:Ordinal="3" />
<xsd:attribute name="mimetype" type="xsd:string" msdata:Ordinal="4" />
<xsd:attribute ref="xml:space" />
</xsd:complexType>
</xsd:element>
<xsd:element name="resheader">
<xsd:complexType>
<xsd:sequence>
<xsd:element name="value" type="xsd:string" minOccurs="0" msdata:Ordinal="1" />
</xsd:sequence>
<xsd:attribute name="name" type="xsd:string" use="required" />
</xsd:complexType>
</xsd:element>
</xsd:choice>
</xsd:complexType>
</xsd:element>
</xsd:schema>
<resheader name="resmimetype">
<value>text/microsoft-resx</value>
</resheader>
<resheader name="version">
<value>2.0</value>
</resheader>
<resheader name="reader">
<value>System.Resources.ResXResourceReader, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<resheader name="writer">
<value>System.Resources.ResXResourceWriter, System.Windows.Forms, Version=4.0.0.0, Culture=neutral, PublicKeyToken=b77a5c561934e089</value>
</resheader>
<data name="ActionBookOrClose" xml:space="preserve">
<value>Rent bike or close lock</value>
</data>
<data name="ActionCancelRequest" xml:space="preserve">
<value>Cancel bike reservation</value>
</data>
<data name="ActionClose" xml:space="preserve">
<value>Close lock</value>
</data>
<data name="ActionCloseAndReturn" xml:space="preserve">
<value>Close lock &amp; return bike</value>
</data>
<data name="ActionOpen" xml:space="preserve">
<value>Open lock</value>
</data>
<data name="ActionOpenAndBook" xml:space="preserve">
<value>Open lock &amp; rent bike</value>
</data>
<data name="ActionOpenAndPause" xml:space="preserve">
<value>Open lock &amp; continue renting</value>
</data>
<data name="ActionRequest" xml:space="preserve">
<value>Reserve bike</value>
</data>
<data name="ActionReturn" xml:space="preserve">
<value>Return bike</value>
</data>
<data name="ActivityTextMapLoadingStationsAndBikes" xml:space="preserve">
<value>Loading Stations and Bikes...</value>
</data>
<data name="ErrorBookedSearchMessage" xml:space="preserve">
<value>Lock of rented bike can not be found.</value>
</data>
<data name="ErrorCloseLockBoldBlockedMessage" xml:space="preserve">
<value>Lock is blocked. Please ensure that no spoke or any other obstacle prevents the lock from closing and try again.</value>
</data>
<data name="ErrorCloseLockMovingMessage" xml:space="preserve">
<value>Lock can only be closed if bike is not moving. Please park bike and try again.</value>
</data>
<data name="ErrorOpenLockMessage" xml:space="preserve">
<value>Lock is blocked. Please ensure that no obstacle prevents lock from opening and try again.</value>
</data>
<data name="ErrorReservedSearchMessage" xml:space="preserve">
<value>Lock of reserved bike can not be found.</value>
</data>
<data name="ErrorReturnBikeLockClosedNoGPSMessage" xml:space="preserve">
<value>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"</value>
</data>
<data name="ErrorReturnBikeLockOpenNoGPSMessage" xml:space="preserve">
<value>Returning bike at an unknown location is not possible.
Bike can only be returned if bike is in reach and location information is available.</value>
</data>
<data name="ErrorReturnBikeNotAtStationMessage" xml:space="preserve">
<value>Returning bike outside of station is not possible. Distance to station {0} is {1} m.</value>
</data>
<data name="ErrorReturnBikeNotAtStationTitle" xml:space="preserve">
<value>Error returning bike!</value>
</data>
<data name="ErrorSupportmailCreateAttachment" xml:space="preserve">
<value>Attachment could not be created.</value>
</data>
<data name="ErrorSupportmailMailingFailed" xml:space="preserve">
<value>Opening mail app failed.</value>
</data>
<data name="ErrorSupportmailPhoningFailed" xml:space="preserve">
<value>Opening phone app failed.</value>
</data>
<data name="MarkingAbout" xml:space="preserve">
<value>About {0}</value>
</data>
<data name="MarkingAccount" xml:space="preserve">
<value>Account</value>
</data>
<data name="MarkingBikesAtStationTitle" xml:space="preserve">
<value>Bike Location {0}</value>
</data>
<data name="MarkingFeedbackAndContact" xml:space="preserve">
<value>Contact</value>
</data>
<data name="MarkingFeesAndBikes" xml:space="preserve">
<value>Instructions</value>
</data>
<data name="MarkingLoggedInStateInfoLoggedIn" xml:space="preserve">
<value>Logged in as {0}.</value>
</data>
<data name="MarkingLoggedInStateInfoLoggedInGroup" xml:space="preserve">
<value>Logged in as {0} at {1}.</value>
</data>
<data name="MarkingLoggedInStateInfoNotLoggedIn" xml:space="preserve">
<value>No user logged in.</value>
</data>
<data name="MarkingLogin" xml:space="preserve">
<value>Login</value>
</data>
<data name="MarkingMapPage" xml:space="preserve">
<value>Bike Locations</value>
</data>
<data name="MarkingMyBikes" xml:space="preserve">
<value>My Bikes</value>
</data>
<data name="MarkingSettings" xml:space="preserve">
<value>Settings</value>
</data>
<data name="MarkingTabBikes" xml:space="preserve">
<value>Instructions</value>
</data>
<data name="MarkingTabFees" xml:space="preserve">
<value>Pricing</value>
</data>
<data name="MessageAnswerOk" xml:space="preserve">
<value>OK</value>
</data>
<data name="MessageAppVersionIsOutdated" xml:space="preserve">
<value>This version of the {0} App is outdated. Please update to the latest version.</value>
</data>
<data name="MessageContactMail" xml:space="preserve">
<value>Questions? Remarks? Criticism?</value>
</data>
<data name="MessageLoginWelcome" xml:space="preserve">
<value>User {0} successfully logged in.</value>
</data>
<data name="MessageLoginWelcomeTitle" xml:space="preserve">
<value>Welcome!</value>
</data>
<data name="MessageLoginWelcomeTitleGroup" xml:space="preserve">
<value>Welcome to {0}!</value>
</data>
<data name="MessageMapPageErrorAuthcookieUndefined" xml:space="preserve">
<value>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.</value>
</data>
<data name="MessagePhoneMail" xml:space="preserve">
<value>Urgent question related to {0}? (Monday-Friday: 10:00 18:00)</value>
</data>
<data name="MessageRateMail" xml:space="preserve">
<value>Are you enjoying the {0}-App?</value>
</data>
<data name="MessageWaring" xml:space="preserve">
<value>Warning</value>
</data>
<data name="QuestionAnswerNo" xml:space="preserve">
<value>No</value>
</data>
<data name="QuestionAnswerYes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="QuestionSupportmailAnswerApp" xml:space="preserve">
<value>{0} app request</value>
</data>
<data name="QuestionSupportmailAnswerOperator" xml:space="preserve">
<value>{0} request</value>
</data>
<data name="QuestionSupportmailAttachment" xml:space="preserve">
<value>Attach file containing diagnosis information to mail?</value>
</data>
<data name="QuestionSupportmailSubject" xml:space="preserve">
<value>Does your request/ comment relate to the {0}-app or to a more general subject?</value>
</data>
<data name="QuestionTitle" xml:space="preserve">
<value>Question</value>
</data>
<data name="ActivityTextBikesAtStationGetBikes" xml:space="preserve">
<value>Loading bikes located at station...</value>
</data>
<data name="ActivityTextCenterMap" xml:space="preserve">
<value>Centering map...</value>
</data>
<data name="ActivityTextMyBikesCheckBluetoothState" xml:space="preserve">
<value>Check Bluetooth state and location permissions...</value>
</data>
<data name="ActivityTextMyBikesLoadingBikes" xml:space="preserve">
<value>Loading reserved/ booked bikes...</value>
</data>
<data name="ActivityTextSearchBikes" xml:space="preserve">
<value>Searching locks...</value>
</data>
<data name="StatusTextBookedCodeSince" xml:space="preserve">
<value>Code {0}, rented since {1}.</value>
</data>
<data name="StatusTextBooked" xml:space="preserve">
<value>Bike is rented.</value>
</data>
<data name="StatusTextBookedSince" xml:space="preserve">
<value>Rented since {0}.</value>
</data>
<data name="StatusTextReservationExpiredMaximumReservationTime" xml:space="preserve">
<value>Max. reservation time of {0} minutes expired.</value>
</data>
<data name="StatusTextReservationExpiredCodeRemaining" xml:space="preserve">
<value>Code {0}, still {1} minutes reserved.</value>
</data>
<data name="StatusTextReservationExpiredRemaining" xml:space="preserve">
<value>Still {0} minutes reserved.</value>
</data>
<data name="StatusTextReservationExpiredCodeMaxReservationTime" xml:space="preserve">
<value>Code {0}, max. reservation time of {1} minutes expired.</value>
</data>
<data name="StatusTextAvailable" xml:space="preserve">
<value>Available.</value>
</data>
<data name="StatusTextBookedCodeLocationSince" xml:space="preserve">
<value>Code {0}, location {1}, rented since {2}.</value>
</data>
<data name="StatusTextReservationExpiredCodeLocationMaxReservationTime" xml:space="preserve">
<value>Code {0}, location {1}, max. reservation time of {2} minutes expired.</value>
</data>
<data name="StatusTextReservationExpiredCodeLocationReservationTime" xml:space="preserve">
<value>Code {0}, location {1}, still {2} minutes reserved.</value>
</data>
<data name="StatusTextReservationExpiredLocationMaxReservationTime" xml:space="preserve">
<value>Location {0}, max. reservation time of {1} minutes expired.</value>
</data>
<data name="StatusTextReservationExpiredLocationReservationTime" xml:space="preserve">
<value>Location {0}, still {1} minutes reserved.</value>
</data>
<data name="ActionContactRate" xml:space="preserve">
<value>Submit rating</value>
</data>
<data name="ActionLoginLogin" xml:space="preserve">
<value>Login</value>
</data>
<data name="ActionLoginPasswordForgotten" xml:space="preserve">
<value>Password forgotten?</value>
</data>
<data name="ActionLoginRegister" xml:space="preserve">
<value>Register</value>
</data>
<data name="MarkingLoginEmailAddressLabel" xml:space="preserve">
<value>E-mail address</value>
</data>
<data name="MarkingLoginEmailAddressPlaceholder" xml:space="preserve">
<value>E-mail address</value>
</data>
<data name="MarkingLoginInstructions" xml:space="preserve">
<value>Instructions TINK bikes</value>
</data>
<data name="MarkingLoginInstructionsTinkKonradMessage" xml:space="preserve">
<value>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.</value>
</data>
<data name="MarkingLoginInstructionsTinkKonradTitle" xml:space="preserve">
<value>For your information!
</value>
</data>
<data name="MarkingLoginPasswordLabel" xml:space="preserve">
<value>Password, minimum length 8 characters</value>
</data>
<data name="MarkingLoginPasswordPlaceholder" xml:space="preserve">
<value>Password</value>
</data>
<data name="MessageLoginConnectionErrorMessage" xml:space="preserve">
<value>Login cookie must not be empty. {0}</value>
</data>
<data name="MessageLoginConnectionErrorTitle" xml:space="preserve">
<value>Connection error during registration!</value>
</data>
<data name="MessageLoginErrorTitle" xml:space="preserve">
<value>Error during login!</value>
</data>
<data name="MessageLoginRecoverPassword" xml:space="preserve">
<value>Please connect to Internet to recover the password.</value>
</data>
<data name="MessageLoginRegisterNoNet" xml:space="preserve">
<value>Please connect to Internet to register.</value>
</data>
<data name="MessageTitleHint" xml:space="preserve">
<value>Hint</value>
</data>
<data name="ErrorOpenLockStillClosedMessage" xml:space="preserve">
<value>After try to open lock state closed is reported.</value>
</data>
<data name="ErrorOpenLockOutOfReadMessage" xml:space="preserve">
<value>Lock cannot be opened until bike is near.</value>
</data>
<data name="ErrorOpenLockTitle" xml:space="preserve">
<value>Lock can not be opened!</value>
</data>
<data name="ErrorCloseLockOutOfReachMessage" xml:space="preserve">
<value>Lock cannot be closed until bike is near.</value>
</data>
<data name="ErrorCloseLockOutOfReachStateReservedMessage" xml:space="preserve">
<value>Lock cannot be closed until bike is near.
Please try again to close bike or report bike to support!</value>
</data>
<data name="ErrorCloseLockTitle" xml:space="preserve">
<value>Lock can not be closed!</value>
</data>
<data name="ErrorCloseLockStillOpenMessage" xml:space="preserve">
<value>After try to close lock state open is reported.</value>
</data>
<data name="ErrorCloseLockUnexpectedStateMessage" xml:space="preserve">
<value>Lock reports state "{0}".</value>
</data>
<data name="ErrorCloseLockUnkErrorMessage" xml:space="preserve">
<value>Please try to lock again or report bike to support!
{0}</value>
</data>
<data name="MessageBikesManagementLocationPermission" xml:space="preserve">
<value>Please allow location sharing so that bike lock/locks can be managed.</value>
</data>
<data name="MessageBikesManagementLocationPermissionOpenDialog" xml:space="preserve">
<value>Please allow location sharing so that bike lock/locks can be managed.
Open sharing dialog?</value>
</data>
<data name="MessageCenterMapLocationPermissionOpenDialog" xml:space="preserve">
<value>Please allow location sharing so that map can be centered.
Open sharing dialog?</value>
</data>
<data name="MessageAnswerNo" xml:space="preserve">
<value>No</value>
</data>
<data name="MessageAnswerYes" xml:space="preserve">
<value>Yes</value>
</data>
<data name="MessageBikesManagementBluetoothActivation" xml:space="preserve">
<value>Please enable Bluetooth to manage bike lock/locks.</value>
</data>
<data name="MessageBikesManagementLocationActivation" xml:space="preserve">
<value>Please activate location so that bike lock can be found!</value>
</data>
<data name="ActionSearchLock" xml:space="preserve">
<value>Search lock</value>
</data>
<data name="ActivityTextOneMomentPlease" xml:space="preserve">
<value>One moment please...</value>
</data>
<data name="ActivityTextOpeningLock" xml:space="preserve">
<value>Opening lock...</value>
</data>
<data name="ActivityTextStartingUpdater" xml:space="preserve">
<value>Updating...</value>
</data>
<data name="ActivityTextStartingUpdatingLockingState" xml:space="preserve">
<value>Updating lock state...</value>
</data>
<data name="ActivityTextReadingChargingLevel" xml:space="preserve">
<value>Reading charging level...</value>
</data>
<data name="ActivityTextErrorStatusUpdateingLockstate" xml:space="preserve">
<value>Status error on updating lock state.</value>
</data>
<data name="ActivityTextErrorConnectionUpdateingLockstate" xml:space="preserve">
<value>Connection error on updating locking status.</value>
</data>
<data name="ActivityTextErrorNoWebUpdateingLockstate" xml:space="preserve">
<value>No web error on updating locking status.</value>
</data>
<data name="ActivityTextClosingLock" xml:space="preserve">
<value>Closing lock...</value>
</data>
<data name="ChangeLog3_0_203" xml:space="preserve">
<value>Updated to latest lock firmware.</value>
</data>
<data name="ChangeLog3_0_204" xml:space="preserve">
<value>Bluetooth communication inproved.</value>
</data>
<data name="ChangeLog3_0_205" xml:space="preserve">
<value>Nicer station markers for iOS.</value>
</data>
<data name="ChangeLog3_0_206" xml:space="preserve">
<value>Bluetooth and geolocation functionality improved.</value>
</data>
<data name="ChangeLog3_0_207" xml:space="preserve">
<value>Minor fixes related to renting functionality.
Software packages updated.
Targets Android 11.</value>
</data>
<data name="ActivityTextReservingBike" xml:space="preserve">
<value>Reserving bike...</value>
</data>
<data name="MessageOpenLockAndBookeBike" xml:space="preserve">
<value>Rent bike {0} and open lock?</value>
</data>
<data name="ActivityTextErrorReadingChargingLevelGeneral" xml:space="preserve">
<value>Battery status cannot be read.</value>
</data>
<data name="ActivityTextErrorReadingChargingLevelOutOfReach" xml:space="preserve">
<value>Battery status can only be read when bike is nearby.</value>
</data>
<data name="ActivityTextRentingBike" xml:space="preserve">
<value>Renting bike...</value>
</data>
<data name="MessageRentingBikeErrorConnectionTitle" xml:space="preserve">
<value>Connection error when renting the bike!</value>
</data>
<data name="MessageRentingBikeErrorGeneralTitle" xml:space="preserve">
<value>Error when renting the bike!</value>
</data>
<data name="MessageRentingBikeErrorTooManyReservationsRentals" xml:space="preserve">
<value>A rental of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made.</value>
</data>
<data name="MessageReservationBikeErrorTooManyReservationsRentals" xml:space="preserve">
<value>A reservation of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made.</value>
</data>
<data name="MessageErrorLockIsClosedThreeLines" xml:space="preserve">
<value>Attention: Lock is closed!
{0}
{1}</value>
</data>
<data name="MessageErrorLockIsClosedTwoLines" xml:space="preserve">
<value>Attention: Lock is closed!
{0}</value>
</data>
<data name="ExceptionTextRentingBikeFailedGeneral" xml:space="preserve">
<value>The rental of bike No. {0} has failed.</value>
</data>
<data name="ExceptionTextRentingBikeFailedUnavailalbe" xml:space="preserve">
<value>The rental of bike No. {0} has failed, the bike is currently unavailable.{1}</value>
</data>
<data name="ExceptionTextReservationBikeFailedGeneral" xml:space="preserve">
<value>The reservation of bike no. {0} has failed.</value>
</data>
<data name="ExceptionTextReservationBikeFailedUnavailalbe" xml:space="preserve">
<value>The reservation of bike No. {0} has failed, the bike is currently unavailable.{1}</value>
</data>
<data name="ChangeLog3_0_208" xml:space="preserve">
<value>Minor fixes.</value>
</data>
<data name="ActivityTextDisconnectingLock" xml:space="preserve">
<value>Disconnecting lock...</value>
</data>
<data name="ActivityTextErrorDisconnect" xml:space="preserve">
<value>Error occurred disconnecting</value>
</data>
<data name="MarkingBikeInfoErrorStateDisposableClosedDetected" xml:space="preserve">
<value>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.</value>
</data>
<data name="MarkingBikeInfoErrorStateUnknownDetected" xml:space="preserve">
<value>Unknown status for bike {0} detected.</value>
</data>
<data name="ActivityTextCancelingReservation" xml:space="preserve">
<value>Canceling reservation...</value>
</data>
<data name="QuestionCancelReservation" xml:space="preserve">
<value>Cancel reservation for bike {0}?</value>
</data>
<data name="ChangeLog3_0_209" xml:space="preserve">
<value>Minor fix: Bikes are disconnected as soon as becoming disposable.</value>
</data>
<data name="QuestionReserveBike" xml:space="preserve">
<value>Reserve bike {0} free of charge for {1} min?</value>
</data>
<data name="ActivityTextErrorDeserializationException" xml:space="preserve">
<value>Connection error: Deserialization failed.</value>
</data>
<data name="ActivityTextErrorException" xml:space="preserve">
<value>Connection interrupted.</value>
</data>
<data name="ActivityTextErrorInvalidResponseException" xml:space="preserve">
<value>Connection error, invalid server response.</value>
</data>
<data name="ActivityTextErrorWebConnectFailureException" xml:space="preserve">
<value>Connection interrupted, server unreachable.</value>
</data>
<data name="ActivityTextErrorWebException" xml:space="preserve">
<value>Connection error. Code: {0}.</value>
</data>
<data name="ActivityTextErrorWebForbiddenException" xml:space="preserve">
<value>Connection interrupted, server busy.</value>
</data>
<data name="MessageBikesManagementTariffDescriptionAboEuroPerMonth" xml:space="preserve">
<value>Subscription price</value>
</data>
<data name="MessageBikesManagementTariffDescriptionEuroPerHour" xml:space="preserve">
<value>€/hour</value>
</data>
<data name="MessageBikesManagementTariffDescriptionFeeEuroPerHour" xml:space="preserve">
<value>Rental fees</value>
</data>
<data name="MessageBikesManagementTariffDescriptionFreeTimePerSession" xml:space="preserve">
<value>Free use</value>
</data>
<data name="MessageBikesManagementTariffDescriptionHour" xml:space="preserve">
<value>hour(s)/day</value>
</data>
<data name="MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay" xml:space="preserve">
<value>Max. fee</value>
</data>
<data name="MessageBikesManagementTariffDescriptionTariffHeader" xml:space="preserve">
<value>Tariff {0}, nr. {1}</value>
</data>
<data name="ActivityTextQuerryServer" xml:space="preserve">
<value>Request server...</value>
</data>
<data name="ActivityTextSearchingLock" xml:space="preserve">
<value>Searching lock...</value>
</data>
<data name="ChangeLog3_0_214" xml:space="preserve">
<value>Multiple operators support.</value>
</data>
<data name="ChangeLog3_0_215" xml:space="preserve">
<value>Layout of "Whats New"-dialog improved :-)</value>
</data>
<data name="MessageBikesManagementMaxFeeEuroPerDay" xml:space="preserve">
<value>€/day</value>
</data>
<data name="ChangeLog3_0_216" xml:space="preserve">
<value>GUI layout improved.</value>
</data>
<data name="ChangeLog3_0_217" xml:space="preserve">
<value>Packages updated.</value>
</data>
<data name="ChangeLog3_0_218" xml:space="preserve">
<value>Minor fixes.</value>
</data>
<data name="ChangeLog3_0_219" xml:space="preserve">
<value>Icons added to flyout menu.</value>
</data>
<data name="ChangeLog3_0_220" xml:space="preserve">
<value>GUI layout improved.</value>
</data>
<data name="ChangeLog3_0_221" xml:space="preserve">
<value>GUI layout improved.</value>
</data>
<data name="ChangeLog3_0_222" xml:space="preserve">
<value>Minor bugfix and improvements.</value>
</data>
</root>

View file

@ -0,0 +1,730 @@
<?xml version="1.0" encoding="utf-8"?>
<xliff version="1.2" xmlns="urn:oasis:names:tc:xliff:document:1.2" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="urn:oasis:names:tc:xliff:document:1.2 xliff-core-1.2-transitional.xsd">
<file datatype="xml" source-language="en-GB" target-language="de" original="TINKLIB/MULTILINGUALRESOURCES/APPRESOURCES.RESX" tool-id="MultilingualAppToolkit" product-name="n/a" product-version="n/a" build-num="n/a">
<header>
<tool tool-id="MultilingualAppToolkit" tool-name="Multilingual App Toolkit" tool-version="4.0.6916.0" tool-company="Microsoft" />
</header>
<body>
<group id="TINKLIB/MULTILINGUALRESOURCES/APPRESOURCES.RESX" datatype="resx">
<trans-unit id="ActionBookOrClose" translate="yes" xml:space="preserve">
<source>Rent bike or close lock</source>
<target state="translated">Rad mieten oder Schloss schließen</target>
</trans-unit>
<trans-unit id="ActionCancelRequest" translate="yes" xml:space="preserve">
<source>Cancel bike reservation</source>
<target state="final">Reservierung aufheben</target>
</trans-unit>
<trans-unit id="ActionClose" translate="yes" xml:space="preserve">
<source>Close lock</source>
<target state="final">Schloss schließen</target>
</trans-unit>
<trans-unit id="ActionCloseAndReturn" translate="yes" xml:space="preserve">
<source>Close lock &amp; return bike</source>
<target state="final">Schloss schließen &amp; Miete beenden</target>
</trans-unit>
<trans-unit id="ActionOpen" translate="yes" xml:space="preserve">
<source>Open lock</source>
<target state="final">Schloss öffnen</target>
</trans-unit>
<trans-unit id="ActionOpenAndBook" translate="yes" xml:space="preserve">
<source>Open lock &amp; rent bike</source>
<target state="final">Schloss öffnen &amp; Rad mieten</target>
</trans-unit>
<trans-unit id="ActionOpenAndPause" translate="yes" xml:space="preserve">
<source>Open lock &amp; continue renting</source>
<target state="final">Schloss öffnen &amp; Miete fortsetzen</target>
</trans-unit>
<trans-unit id="ActionRequest" translate="yes" xml:space="preserve">
<source>Reserve bike</source>
<target state="final">Rad reservieren</target>
</trans-unit>
<trans-unit id="ActionReturn" translate="yes" xml:space="preserve">
<source>Return bike</source>
<target state="final">Miete beenden</target>
</trans-unit>
<trans-unit id="MarkingMapPage" translate="yes" xml:space="preserve">
<source>Bike Locations</source>
<target state="translated">Fahrradstandorte</target>
</trans-unit>
<trans-unit id="MessageLoginWelcome" translate="yes" xml:space="preserve">
<source>User {0} successfully logged in.</source>
<target state="final">Benutzer {0} erfolgreich angemeldet.</target>
</trans-unit>
<trans-unit id="MessageLoginWelcomeTitle" translate="yes" xml:space="preserve">
<source>Welcome!</source>
<target state="final">Willkommen!</target>
</trans-unit>
<trans-unit id="MessageLoginWelcomeTitleGroup" translate="yes" xml:space="preserve">
<source>Welcome to {0}!</source>
<target state="translated">Wilkommen bei {0}!</target>
</trans-unit>
<trans-unit id="MarkingLoggedInStateInfoLoggedIn" translate="yes" xml:space="preserve">
<source>Logged in as {0}.</source>
<target state="final">Angemeldet als {0}.</target>
</trans-unit>
<trans-unit id="MarkingLoggedInStateInfoLoggedInGroup" translate="yes" xml:space="preserve">
<source>Logged in as {0} at {1}.</source>
<target state="final">Angemeldet als {0} bei {1}.</target>
</trans-unit>
<trans-unit id="MarkingLoggedInStateInfoNotLoggedIn" translate="yes" xml:space="preserve">
<source>No user logged in.</source>
<target state="final">Kein Benutzer angemeldet.</target>
</trans-unit>
<trans-unit id="MessageAppVersionIsOutdated" translate="yes" xml:space="preserve">
<source>This version of the {0} App is outdated. Please update to the latest version.</source>
<target state="translated">Diese version der {0} App ist veraltet. Bitte auf aktuelle Version aktualisieren.</target>
</trans-unit>
<trans-unit id="QuestionSupportmailSubject" translate="yes" xml:space="preserve">
<source>Does your request/ comment relate to the {0}-app or to a more general subject?</source>
<target state="translated">Betrifft die Anfrage/ Anmerkung die {0}-App oder ein allgemeines Thema?</target>
</trans-unit>
<trans-unit id="QuestionSupportmailAnswerApp" translate="yes" xml:space="preserve">
<source>{0} app request</source>
<target state="final">{0}-App Anfrage</target>
</trans-unit>
<trans-unit id="QuestionSupportmailAnswerOperator" translate="yes" xml:space="preserve">
<source>{0} request</source>
<target state="final">{0} Anfrage</target>
</trans-unit>
<trans-unit id="MarkingAbout" translate="yes" xml:space="preserve">
<source>About {0}</source>
<target state="final">Über {0}</target>
</trans-unit>
<trans-unit id="MarkingAccount" translate="yes" xml:space="preserve">
<source>Account</source>
<target state="final">Konto</target>
</trans-unit>
<trans-unit id="MarkingFeedbackAndContact" translate="yes" xml:space="preserve">
<source>Contact</source>
<target state="translated">Kontakt</target>
</trans-unit>
<trans-unit id="MarkingLogin" translate="yes" xml:space="preserve">
<source>Login</source>
<target state="final">Anmelden</target>
</trans-unit>
<trans-unit id="MarkingMyBikes" translate="yes" xml:space="preserve">
<source>My Bikes</source>
<target state="final">Meine Räder</target>
</trans-unit>
<trans-unit id="MarkingFeesAndBikes" translate="yes" xml:space="preserve">
<source>Instructions</source>
<target state="translated">Bedienung</target>
</trans-unit>
<trans-unit id="MarkingSettings" translate="yes" xml:space="preserve">
<source>Settings</source>
<target state="final">Einstellungen</target>
</trans-unit>
<trans-unit id="MarkingTabBikes" translate="yes" xml:space="preserve">
<source>Instructions</source>
<target state="final">Bedienung</target>
</trans-unit>
<trans-unit id="MarkingTabFees" translate="yes" xml:space="preserve">
<source>Pricing</source>
<target state="translated">Tarife</target>
</trans-unit>
<trans-unit id="ErrorOpenLockMessage" translate="yes" xml:space="preserve">
<source>Lock is blocked. Please ensure that no obstacle prevents lock from opening and try again.</source>
<target state="translated">Schloss ist blockiert. Bitte Ursache von Blockierung beheben und Vorgang wiederholen.</target>
</trans-unit>
<trans-unit id="ErrorCloseLockBoldBlockedMessage" translate="yes" xml:space="preserve">
<source>Lock is blocked. Please ensure that no spoke or any other obstacle prevents the lock from closing and try again.</source>
<target state="translated">Schloss ist blockiert. Bitte sicherstellen, dass keine Speiche oder ein anderer Gegenstand das Schloss blockiert und Vorgang wiederholen.</target>
</trans-unit>
<trans-unit id="ErrorCloseLockMovingMessage" translate="yes" xml:space="preserve">
<source>Lock can only be closed if bike is not moving. Please park bike and try again.</source>
<target state="final">Schloss kann erst geschlossen werden, wenn Rad nicht mehr bewegt wird. Bitte Rad abstellen und Vorgang wiederholen.</target>
</trans-unit>
<trans-unit id="ErrorBookedSearchMessage" translate="yes" xml:space="preserve">
<source>Lock of rented bike can not be found.</source>
<target state="final">Schloss des gemieteten Rads kann nicht gefunden werden.</target>
</trans-unit>
<trans-unit id="ErrorReservedSearchMessage" translate="yes" xml:space="preserve">
<source>Lock of reserved bike can not be found.</source>
<target state="translated">Schloss des reservierten Rads kann nicht gefunden werden.</target>
</trans-unit>
<trans-unit id="ActivityTextBikesAtStationGetBikes" translate="yes" xml:space="preserve">
<source>Loading bikes located at station...</source>
<target state="translated">Lade Räder an Station...</target>
</trans-unit>
<trans-unit id="ActivityTextMyBikesLoadingBikes" translate="yes" xml:space="preserve">
<source>Loading reserved/ booked bikes...</source>
<target state="translated">Lade meine Räder...</target>
</trans-unit>
<trans-unit id="ActivityTextSearchBikes" translate="yes" xml:space="preserve">
<source>Searching locks...</source>
<target state="translated">Suche Schlösser...</target>
</trans-unit>
<trans-unit id="ActivityTextMyBikesCheckBluetoothState" translate="yes" xml:space="preserve">
<source>Check Bluetooth state and location permissions...</source>
<target state="translated">Prüfe Berechtigungen...</target>
</trans-unit>
<trans-unit id="ActivityTextCenterMap" translate="yes" xml:space="preserve">
<source>Centering map...</source>
<target state="translated">Zentriere Karte...</target>
</trans-unit>
<trans-unit id="ActivityTextMapLoadingStationsAndBikes" translate="yes" xml:space="preserve">
<source>Loading Stations and Bikes...</source>
<target state="translated">Lade Stationen und Räder...</target>
</trans-unit>
<trans-unit id="ErrorReturnBikeNotAtStationMessage" translate="yes" xml:space="preserve">
<source>Returning bike outside of station is not possible. Distance to station {0} is {1} m.</source>
<target state="translated">Rückgabe ausserhalb von Station nicht möglich. Entfernung zur Station {0} ist {1} m.</target>
</trans-unit>
<trans-unit id="ErrorReturnBikeNotAtStationTitle" translate="yes" xml:space="preserve">
<source>Error returning bike!</source>
<target state="translated">Fehler bei Radrückgabe!</target>
</trans-unit>
<trans-unit id="ErrorReturnBikeLockClosedNoGPSMessage" translate="yes" xml:space="preserve">
<source>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"</source>
<target state="translated">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.</target>
</trans-unit>
<trans-unit id="ErrorReturnBikeLockOpenNoGPSMessage" translate="yes" xml:space="preserve">
<source>Returning bike at an unknown location is not possible.
Bike can only be returned if bike is in reach and location information is available.</source>
<target state="translated">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.</target>
</trans-unit>
<trans-unit id="ErrorSupportmailCreateAttachment" translate="yes" xml:space="preserve">
<source>Attachment could not be created.</source>
<target state="translated">Mailanhang konnte nich erzeugt werden.</target>
</trans-unit>
<trans-unit id="ErrorSupportmailMailingFailed" translate="yes" xml:space="preserve">
<source>Opening mail app failed.</source>
<target state="translated">Mailapp konnte nicht geöffnet werden.</target>
</trans-unit>
<trans-unit id="ErrorSupportmailPhoningFailed" translate="yes" xml:space="preserve">
<source>Opening phone app failed.</source>
<target state="translated">Telefonapp konnte nicht geöffnet werden.</target>
</trans-unit>
<trans-unit id="MessageAnswerOk" translate="yes" xml:space="preserve">
<source>OK</source>
<target state="translated">OK</target>
</trans-unit>
<trans-unit id="MessageContactMail" translate="yes" xml:space="preserve">
<source>Questions? Remarks? Criticism?</source>
<target state="translated">Fragen? Hinweise? Kritik?</target>
</trans-unit>
<trans-unit id="MessagePhoneMail" translate="yes" xml:space="preserve">
<source>Urgent question related to {0}? (Monday-Friday: 10:00 18:00)</source>
<target state="translated">Eilige Frage rund um {0}? (Montag-Freitag: 10:00 18:00)</target>
</trans-unit>
<trans-unit id="MessageRateMail" translate="yes" xml:space="preserve">
<source>Are you enjoying the {0}-App?</source>
<target state="translated">Gefällt die {0}-App?</target>
</trans-unit>
<trans-unit id="MessageWaring" translate="yes" xml:space="preserve">
<source>Warning</source>
<target state="translated">Warnung</target>
</trans-unit>
<trans-unit id="QuestionAnswerNo" translate="yes" xml:space="preserve">
<source>No</source>
<target state="translated">Nein</target>
</trans-unit>
<trans-unit id="QuestionAnswerYes" translate="yes" xml:space="preserve">
<source>Yes</source>
<target state="translated">Ja</target>
</trans-unit>
<trans-unit id="QuestionSupportmailAttachment" translate="yes" xml:space="preserve">
<source>Attach file containing diagnosis information to mail?</source>
<target state="translated">Soll der Mail eine Datei mit Diagnoseinformationen angehängt werden?</target>
</trans-unit>
<trans-unit id="QuestionTitle" translate="yes" xml:space="preserve">
<source>Question</source>
<target state="translated">Frage</target>
</trans-unit>
<trans-unit id="MessageMapPageErrorAuthcookieUndefined" translate="yes" xml:space="preserve">
<source>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.</source>
<target state="translated">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.</target>
</trans-unit>
<trans-unit id="MarkingBikesAtStationTitle" translate="yes" xml:space="preserve">
<source>Bike Location {0}</source>
<target state="translated">Fahrradstandort {0}</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredCodeMaxReservationTime" translate="yes" xml:space="preserve">
<source>Code {0}, max. reservation time of {1} minutes expired.</source>
<target state="translated">Code ist {0}, max. Reservierungszeit von {1} Min. abgelaufen.
</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredCodeRemaining" translate="yes" xml:space="preserve">
<source>Code {0}, still {1} minutes reserved.</source>
<target state="translated">Code ist {0}, noch {1} Minuten reserviert.
</target>
</trans-unit>
<trans-unit id="StatusTextBooked" translate="yes" xml:space="preserve">
<source>Bike is rented.</source>
<target state="translated">Rad ist gemietet.</target>
</trans-unit>
<trans-unit id="StatusTextBookedSince" translate="yes" xml:space="preserve">
<source>Rented since {0}.</source>
<target state="translated">Gemietet seit {0}.</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredRemaining" translate="yes" xml:space="preserve">
<source>Still {0} minutes reserved.</source>
<target state="translated">Noch {0} Minuten reserviert.</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredMaximumReservationTime" translate="yes" xml:space="preserve">
<source>Max. reservation time of {0} minutes expired.</source>
<target state="translated">Max. Reservierungszeit von {0} Min. abgelaufen.</target>
</trans-unit>
<trans-unit id="StatusTextBookedCodeSince" translate="yes" xml:space="preserve">
<source>Code {0}, rented since {1}.</source>
<target state="translated">Code ist {0}, gemietet seit {1}.</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredLocationMaxReservationTime" translate="yes" xml:space="preserve">
<source>Location {0}, max. reservation time of {1} minutes expired.</source>
<target state="translated">Standort {0}, max. Reservierungszeit von {1} Min. abgelaufen.
</target>
</trans-unit>
<trans-unit id="StatusTextAvailable" translate="yes" xml:space="preserve">
<source>Available.</source>
<target state="translated">Frei.</target>
</trans-unit>
<trans-unit id="StatusTextBookedCodeLocationSince" translate="yes" xml:space="preserve">
<source>Code {0}, location {1}, rented since {2}.</source>
<target state="translated">Code {0}, Standort {1}, gemietet seit {2}.
</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredCodeLocationMaxReservationTime" translate="yes" xml:space="preserve">
<source>Code {0}, location {1}, max. reservation time of {2} minutes expired.</source>
<target state="translated">Code ist {0}, Standort {1}, max. Reservierungszeit von {2} Min. abgelaufen.
</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredCodeLocationReservationTime" translate="yes" xml:space="preserve">
<source>Code {0}, location {1}, still {2} minutes reserved.</source>
<target state="translated">Code ist {0}, Standort {1}, noch {2} Minuten reserviert.
</target>
</trans-unit>
<trans-unit id="StatusTextReservationExpiredLocationReservationTime" translate="yes" xml:space="preserve">
<source>Location {0}, still {1} minutes reserved.</source>
<target state="translated">Standort {0}, noch {1} Minuten reserviert.
</target>
</trans-unit>
<trans-unit id="ActionLoginLogin" translate="yes" xml:space="preserve">
<source>Login</source>
<target state="translated">Anmelden</target>
</trans-unit>
<trans-unit id="ActionLoginPasswordForgotten" translate="yes" xml:space="preserve">
<source>Password forgotten?</source>
<target state="translated">Passwort vergessen?</target>
</trans-unit>
<trans-unit id="ActionLoginRegister" translate="yes" xml:space="preserve">
<source>Register</source>
<target state="translated">Registrieren</target>
</trans-unit>
<trans-unit id="MarkingLoginEmailAddressLabel" translate="yes" xml:space="preserve">
<source>E-mail address</source>
<target state="translated">E-Mail Adresse</target>
</trans-unit>
<trans-unit id="MarkingLoginEmailAddressPlaceholder" translate="yes" xml:space="preserve">
<source>E-mail address</source>
<target state="translated">E-Mail Adresse</target>
</trans-unit>
<trans-unit id="MarkingLoginPasswordLabel" translate="yes" xml:space="preserve">
<source>Password, minimum length 8 characters</source>
<target state="translated">Passwort, Mindestlänge 8 Zeichen</target>
</trans-unit>
<trans-unit id="MarkingLoginPasswordPlaceholder" translate="yes" xml:space="preserve">
<source>Password</source>
<target state="translated">Passwort</target>
</trans-unit>
<trans-unit id="ActionContactRate" translate="yes" xml:space="preserve">
<source>Submit rating</source>
<target state="translated">Bewertung abgeben</target>
</trans-unit>
<trans-unit id="MarkingLoginInstructions" translate="yes" xml:space="preserve">
<source>Instructions TINK bikes</source>
<target state="translated">Anleitung TINK Räder</target>
</trans-unit>
<trans-unit id="MarkingLoginInstructionsTinkKonradMessage" translate="yes" xml:space="preserve">
<source>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.</source>
<target state="translated">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.</target>
</trans-unit>
<trans-unit id="MarkingLoginInstructionsTinkKonradTitle" translate="yes" xml:space="preserve">
<source>For your information!
</source>
<target state="translated">Zur Information!</target>
</trans-unit>
<trans-unit id="MessageLoginConnectionErrorMessage" translate="yes" xml:space="preserve">
<source>Login cookie must not be empty. {0}</source>
<target state="translated">Anmeldungskeks darf nicht leer sein. {0}</target>
</trans-unit>
<trans-unit id="MessageLoginConnectionErrorTitle" translate="yes" xml:space="preserve">
<source>Connection error during registration!</source>
<target state="translated">Verbindungsfehler beim Registrieren!</target>
</trans-unit>
<trans-unit id="MessageLoginErrorTitle" translate="yes" xml:space="preserve">
<source>Error during login!</source>
<target state="translated">Fehler bei der Anmeldung!</target>
</trans-unit>
<trans-unit id="MessageLoginRecoverPassword" translate="yes" xml:space="preserve">
<source>Please connect to Internet to recover the password.</source>
<target state="translated">Bitte mit dem Internet verbinden zum Wiederherstellen des Passworts.</target>
</trans-unit>
<trans-unit id="MessageLoginRegisterNoNet" translate="yes" xml:space="preserve">
<source>Please connect to Internet to register.</source>
<target state="translated">Bitte mit dem Internet verbinden zum Registrieren.</target>
</trans-unit>
<trans-unit id="MessageTitleHint" translate="yes" xml:space="preserve">
<source>Hint</source>
<target state="translated">Hinweis</target>
</trans-unit>
<trans-unit id="ErrorOpenLockStillClosedMessage" translate="yes" xml:space="preserve">
<source>After try to open lock state closed is reported.</source>
<target state="translated">Nach Versuch Schloss zu öffnen wird Status geschlossen zurückgemeldet.</target>
</trans-unit>
<trans-unit id="ErrorOpenLockOutOfReadMessage" translate="yes" xml:space="preserve">
<source>Lock cannot be opened until bike is near.</source>
<target state="translated">Schloss kann erst geöffnet werden, wenn Rad in der Nähe ist.</target>
</trans-unit>
<trans-unit id="ErrorOpenLockTitle" translate="yes" xml:space="preserve">
<source>Lock can not be opened!</source>
<target state="translated">Schloss kann nicht geöffnet werden!</target>
</trans-unit>
<trans-unit id="ErrorCloseLockOutOfReachMessage" translate="yes" xml:space="preserve">
<source>Lock cannot be closed until bike is near.</source>
<target state="translated">Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist. </target>
</trans-unit>
<trans-unit id="ErrorCloseLockOutOfReachStateReservedMessage" translate="yes" xml:space="preserve">
<source>Lock cannot be closed until bike is near.
Please try again to close bike or report bike to support!</source>
<target state="translated">Schloss kann erst geschlossen werden, wenn das Rad in der Nähe ist.
Bitte Schließen nochmals versuchen oder Rad dem Support melden!</target>
</trans-unit>
<trans-unit id="ErrorCloseLockTitle" translate="yes" xml:space="preserve">
<source>Lock can not be closed!</source>
<target state="translated">Schloss kann nicht geschlossen werden!</target>
</trans-unit>
<trans-unit id="ErrorCloseLockStillOpenMessage" translate="yes" xml:space="preserve">
<source>After try to close lock state open is reported.</source>
<target state="translated">Nach Versuch Schloss zu schließen wird Status geöffnet zurückgemeldet.</target>
</trans-unit>
<trans-unit id="ErrorCloseLockUnexpectedStateMessage" translate="yes" xml:space="preserve">
<source>Lock reports state "{0}".</source>
<target state="translated">Schloss meldet Status "{0}".</target>
</trans-unit>
<trans-unit id="ErrorCloseLockUnkErrorMessage" translate="yes" xml:space="preserve">
<source>Please try to lock again or report bike to support!
{0}</source>
<target state="translated">Bitte schließen nochmals versuchen oder Rad dem Support melden!
{0}</target>
</trans-unit>
<trans-unit id="MessageBikesManagementLocationPermission" translate="yes" xml:space="preserve">
<source>Please allow location sharing so that bike lock/locks can be managed.</source>
<target state="translated">Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können.</target>
</trans-unit>
<trans-unit id="MessageBikesManagementLocationPermissionOpenDialog" translate="yes" xml:space="preserve">
<source>Please allow location sharing so that bike lock/locks can be managed.
Open sharing dialog?</source>
<target state="translated">Bitte Standortfreigabe erlauben, damit Fahrradschloss/ Schlösser verwaltet werden können.
Freigabedialog öffen?</target>
</trans-unit>
<trans-unit id="MessageCenterMapLocationPermissionOpenDialog" translate="yes" xml:space="preserve">
<source>Please allow location sharing so that map can be centered.
Open sharing dialog?</source>
<target state="translated">Bitte Standortfreigabe erlauben, damit Karte zentriert werden werden kann.
Freigabedialog öffen?</target>
</trans-unit>
<trans-unit id="MessageBikesManagementLocationActivation" translate="yes" xml:space="preserve">
<source>Please activate location so that bike lock can be found!</source>
<target state="translated">Bitte Standort aktivieren, damit Fahrradschloss gefunden werden kann!</target>
</trans-unit>
<trans-unit id="MessageAnswerNo" translate="yes" xml:space="preserve">
<source>No</source>
<target state="translated">Nein</target>
</trans-unit>
<trans-unit id="MessageAnswerYes" translate="yes" xml:space="preserve">
<source>Yes</source>
<target state="translated">Ja</target>
</trans-unit>
<trans-unit id="MessageBikesManagementBluetoothActivation" translate="yes" xml:space="preserve">
<source>Please enable Bluetooth to manage bike lock/locks.</source>
<target state="translated">Bitte Bluetooth aktivieren, damit Fahrradschloss/ Schlösser verwaltet werden können.</target>
</trans-unit>
<trans-unit id="ActionSearchLock" translate="yes" xml:space="preserve">
<source>Search lock</source>
<target state="translated">Schloss suchen</target>
</trans-unit>
<trans-unit id="ActivityTextOneMomentPlease" translate="yes" xml:space="preserve">
<source>One moment please...</source>
<target state="translated">Einen Moment bitte...</target>
</trans-unit>
<trans-unit id="ActivityTextOpeningLock" translate="yes" xml:space="preserve">
<source>Opening lock...</source>
<target state="translated">Öffne Schloss...</target>
</trans-unit>
<trans-unit id="ActivityTextStartingUpdater" translate="yes" xml:space="preserve">
<source>Updating...</source>
<target state="translated">Starte Aktualisierung...</target>
</trans-unit>
<trans-unit id="ActivityTextStartingUpdatingLockingState" translate="yes" xml:space="preserve">
<source>Updating lock state...</source>
<target state="translated">Aktualisiere Schlossstatus...</target>
</trans-unit>
<trans-unit id="ActivityTextReadingChargingLevel" translate="yes" xml:space="preserve">
<source>Reading charging level...</source>
<target state="translated">Lese Akkustatus...</target>
</trans-unit>
<trans-unit id="ActivityTextErrorStatusUpdateingLockstate" translate="yes" xml:space="preserve">
<source>Status error on updating lock state.</source>
<target state="translated">Statusfehler beim Aktualisieren des Schlossstatusses.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorConnectionUpdateingLockstate" translate="yes" xml:space="preserve">
<source>Connection error on updating locking status.</source>
<target state="translated">Verbingungsfehler beim Aktualisieren des Schlossstatusses.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorNoWebUpdateingLockstate" translate="yes" xml:space="preserve">
<source>No web error on updating locking status.</source>
<target state="translated">Kein Netz beim Aktualisieren des Schlossstatusses.</target>
</trans-unit>
<trans-unit id="ActivityTextClosingLock" translate="yes" xml:space="preserve">
<source>Closing lock...</source>
<target state="translated">Schließe Schloss...</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_203" translate="yes" xml:space="preserve">
<source>Updated to latest lock firmware.</source>
<target state="translated">Aktualisierrt auf aktuelle Schloss-Firmware.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_204" translate="yes" xml:space="preserve">
<source>Bluetooth communication inproved.</source>
<target state="translated">Bluetooth Kommunikation verbessert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_205" translate="yes" xml:space="preserve">
<source>Nicer station markers for iOS.</source>
<target state="translated">Stationssymbole verbessert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_206" translate="yes" xml:space="preserve">
<source>Bluetooth and geolocation functionality improved.</source>
<target state="translated">Bluetooth- und Geolocation-Funktionalität verbessert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_207" translate="yes" xml:space="preserve">
<source>Minor fixes related to renting functionality.
Software packages updated.
Targets Android 11.</source>
<target state="translated">Kleinere Fehlerbehebungen.
Software Pakete aktualisiert.
Zielplatform Android 11.</target>
</trans-unit>
<trans-unit id="MessageOpenLockAndBookeBike" translate="yes" xml:space="preserve">
<source>Rent bike {0} and open lock?</source>
<target state="translated">Fahrrad {0} mieten und Schloss öffnen?</target>
</trans-unit>
<trans-unit id="ActivityTextReservingBike" translate="yes" xml:space="preserve">
<source>Reserving bike...</source>
<target state="translated">Reserviere Rad...</target>
</trans-unit>
<trans-unit id="ActivityTextErrorReadingChargingLevelGeneral" translate="yes" xml:space="preserve">
<source>Battery status cannot be read.</source>
<target state="translated">Akkustatus kann nicht gelesen werden.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorReadingChargingLevelOutOfReach" translate="yes" xml:space="preserve">
<source>Battery status can only be read when bike is nearby.</source>
<target state="translated">Akkustatus kann erst gelesen werden, wenn Rad in der Nähe ist.</target>
</trans-unit>
<trans-unit id="ActivityTextRentingBike" translate="yes" xml:space="preserve">
<source>Renting bike...</source>
<target state="translated">Miete Rad...</target>
</trans-unit>
<trans-unit id="MessageRentingBikeErrorConnectionTitle" translate="yes" xml:space="preserve">
<source>Connection error when renting the bike!</source>
<target state="translated">Verbingungsfehler beim Mieten des Rads!</target>
</trans-unit>
<trans-unit id="MessageRentingBikeErrorGeneralTitle" translate="yes" xml:space="preserve">
<source>Error when renting the bike!</source>
<target state="translated">Fehler beim Mieten des Rads!</target>
</trans-unit>
<trans-unit id="MessageRentingBikeErrorTooManyReservationsRentals" translate="yes" xml:space="preserve">
<source>A rental of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made.</source>
<target state="translated">Eine Miete des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden.</target>
</trans-unit>
<trans-unit id="MessageReservationBikeErrorTooManyReservationsRentals" translate="yes" xml:space="preserve">
<source>A reservation of bike {0} was rejected because the maximum allowed number of {1} reservations/ rentals had already been made.</source>
<target state="translated">Eine Reservierung des Rads {0} wurde abgelehnt, weil die maximal erlaubte Anzahl von {1} Reservierungen/ Buchungen bereits getätigt wurden.</target>
</trans-unit>
<trans-unit id="MessageErrorLockIsClosedThreeLines" translate="yes" xml:space="preserve">
<source>Attention: Lock is closed!
{0}
{1}</source>
<target state="translated">Achtung: Schloss wird geschlossen!
{0}
{1}</target>
</trans-unit>
<trans-unit id="MessageErrorLockIsClosedTwoLines" translate="yes" xml:space="preserve">
<source>Attention: Lock is closed!
{0}</source>
<target state="translated">Achtung: Schloss wird geschlossen!
{0}</target>
</trans-unit>
<trans-unit id="ExceptionTextRentingBikeFailedGeneral" translate="yes" xml:space="preserve">
<source>The rental of bike No. {0} has failed.</source>
<target state="translated">Die Miete des Fahrads Nr. {0} ist fehlgeschlagen.</target>
</trans-unit>
<trans-unit id="ExceptionTextRentingBikeFailedUnavailalbe" translate="yes" xml:space="preserve">
<source>The rental of bike No. {0} has failed, the bike is currently unavailable.{1}</source>
<target state="translated">Die Miete des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar.</target>
</trans-unit>
<trans-unit id="ExceptionTextReservationBikeFailedGeneral" translate="yes" xml:space="preserve">
<source>The reservation of bike no. {0} has failed.</source>
<target state="translated">Die Reservierung des Fahrads Nr. {0} ist fehlgeschlagen.</target>
</trans-unit>
<trans-unit id="ExceptionTextReservationBikeFailedUnavailalbe" translate="yes" xml:space="preserve">
<source>The reservation of bike No. {0} has failed, the bike is currently unavailable.{1}</source>
<target state="translated">Die Reservierung des Rads Nr. {0} ist fehlgeschlagen, das Rad ist momentan nicht erreichbar.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_208" translate="yes" xml:space="preserve">
<source>Minor fixes.</source>
<target state="translated">Kleinere Fehlerbehebungen.</target>
</trans-unit>
<trans-unit id="ActivityTextDisconnectingLock" translate="yes" xml:space="preserve">
<source>Disconnecting lock...</source>
<target state="translated">Trenne Schloss...</target>
</trans-unit>
<trans-unit id="ActivityTextErrorDisconnect" translate="yes" xml:space="preserve">
<source>Error occurred disconnecting</source>
<target state="translated">Fehler beim Trennen...</target>
</trans-unit>
<trans-unit id="MarkingBikeInfoErrorStateDisposableClosedDetected" translate="yes" xml:space="preserve">
<source>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.</source>
<target state="translated">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.</target>
</trans-unit>
<trans-unit id="MarkingBikeInfoErrorStateUnknownDetected" translate="yes" xml:space="preserve">
<source>Unknown status for bike {0} detected.</source>
<target state="translated">Ungültiger Mietstatus von Rad {0} erkannt.
</target>
</trans-unit>
<trans-unit id="ActivityTextCancelingReservation" translate="yes" xml:space="preserve">
<source>Canceling reservation...</source>
<target state="translated">Reservierung aufheben...</target>
</trans-unit>
<trans-unit id="QuestionCancelReservation" translate="yes" xml:space="preserve">
<source>Cancel reservation for bike {0}?</source>
<target state="translated">Reservierung für Fahrrad {0} aufheben?</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_209" translate="yes" xml:space="preserve">
<source>Minor fix: Bikes are disconnected as soon as becoming disposable.</source>
<target state="translated">Kleinere Fehlerbehebung: Verbindung wird getrennt, sobald Rad verfügbar ist.</target>
</trans-unit>
<trans-unit id="QuestionReserveBike" translate="yes" xml:space="preserve">
<source>Reserve bike {0} free of charge for {1} min?</source>
<target state="translated">Fahrrad {0} kostenlos für {1} Min. reservieren?</target>
</trans-unit>
<trans-unit id="ActivityTextErrorDeserializationException" translate="yes" xml:space="preserve">
<source>Connection error: Deserialization failed.</source>
<target state="translated">Verbindungsfehler: Deserialisierung fehlgeschlagen.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorException" translate="yes" xml:space="preserve">
<source>Connection interrupted.</source>
<target state="translated">Verbindung unterbrochen.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorInvalidResponseException" translate="yes" xml:space="preserve">
<source>Connection error, invalid server response.</source>
<target state="translated">Verbindungsfehler, ungülige Serverantwort.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorWebConnectFailureException" translate="yes" xml:space="preserve">
<source>Connection interrupted, server unreachable.</source>
<target state="translated">Verbindung unterbrochen, Server nicht erreichbar.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorWebException" translate="yes" xml:space="preserve">
<source>Connection error. Code: {0}.</source>
<target state="translated">Verbindungsfehler. Code: {0}.</target>
</trans-unit>
<trans-unit id="ActivityTextErrorWebForbiddenException" translate="yes" xml:space="preserve">
<source>Connection interrupted, server busy.</source>
<target state="translated">Verbindung unterbrochen, Server beschäftigt.</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionAboEuroPerMonth" translate="yes" xml:space="preserve">
<source>Subscription price</source>
<target state="translated">Abo-Preis</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionFeeEuroPerHour" translate="yes" xml:space="preserve">
<source>Rental fees</source>
<target state="translated">Mietgebühren</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionFreeTimePerSession" translate="yes" xml:space="preserve">
<source>Free use</source>
<target state="translated">Gratis Nutzung</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionMaxFeeEuroPerDay" translate="yes" xml:space="preserve">
<source>Max. fee</source>
<target state="translated">Max. Gebühr</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionEuroPerHour" translate="yes" xml:space="preserve">
<source>€/hour</source>
<target state="translated">€/Std.</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionHour" translate="yes" xml:space="preserve">
<source>hour(s)/day</source>
<target state="translated">Std./Tag</target>
</trans-unit>
<trans-unit id="MessageBikesManagementTariffDescriptionTariffHeader" translate="yes" xml:space="preserve">
<source>Tariff {0}, nr. {1}</source>
<target state="translated">Tarif {0}, Nr. {1}</target>
</trans-unit>
<trans-unit id="ActivityTextQuerryServer" translate="yes" xml:space="preserve">
<source>Request server...</source>
<target state="translated">Anfrage Server...</target>
</trans-unit>
<trans-unit id="ActivityTextSearchingLock" translate="yes" xml:space="preserve">
<source>Searching lock...</source>
<target state="translated">Suche Schloss...</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_214" translate="yes" xml:space="preserve">
<source>Multiple operators support.</source>
<target state="translated">Mehrbetreiber Unterstützung.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_215" translate="yes" xml:space="preserve">
<source>Layout of "Whats New"-dialog improved :-)</source>
<target state="translated">Layout des "Whats New"-Dialos verbessert :-)</target>
</trans-unit>
<trans-unit id="MessageBikesManagementMaxFeeEuroPerDay" translate="yes" xml:space="preserve">
<source>€/day</source>
<target state="translated">€/Tag</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_216" translate="yes" xml:space="preserve">
<source>GUI layout improved.</source>
<target state="translated">Oberflächenlayout verbessert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_217" translate="yes" xml:space="preserve">
<source>Packages updated.</source>
<target state="translated">Pakete aktualisiert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_218" translate="yes" xml:space="preserve">
<source>Minor fixes.</source>
<target state="translated">Kleine Verbesserungen.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_219" translate="yes" xml:space="preserve">
<source>Icons added to flyout menu.</source>
<target state="translated">Icons zum Flyout-Menü hinzugefügt.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_220" translate="yes" xml:space="preserve">
<source>GUI layout improved.</source>
<target state="translated">Oberflächenlayout verbessert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_221" translate="yes" xml:space="preserve">
<source>GUI layout improved.</source>
<target state="translated">Oberflächenlayout verbessert.</target>
</trans-unit>
<trans-unit id="ChangeLog3_0_222" translate="yes" xml:space="preserve">
<source>Minor bugfix and improvements.</source>
<target state="translated">Kleine Fehlerbehebungen und Verbesserungen.</target>
</trans-unit>
</group>
</body>
</file>
</xliff>

View file

@ -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
{
/// <summary> Object which manages calls to copri. </summary>
public class CopriCallsHttps : ICopriServer
{
/// <summary> Builds requests.</summary>
private IRequestBuilder requestBuilder;
/// <summary> Initializes a instance of the copri calls https object. </summary>
/// <param name="p_oCopriHost">Host to connect to. </param>
/// <param name="p_strMerchantId">Id of the merchant.</param>
/// <param name="userAgent">Holds the name and version of the TINKApp.</param>
/// <param name="sessionCookie">Session cookie if user is logged in, null otherwise.</param>
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);
}
/// <summary> Holds the URL for rest calls.</summary>
private Uri m_oCopriHost;
/// <summary> Spacifies name and version of app. </summary>
private string UserAgent { get; }
/// <summary> Returns true because value requested form copri server are returned. </summary>
public bool IsConnected => true;
/// <summary> Gets the merchant id.</summary>
public string MerchantId => requestBuilder.MerchantId;
/// <summary> Gets the session cookie if user is logged in, an empty string otherwise. </summary>
public string SessionCookie => requestBuilder.SessionCookie;
/// <summary> Logs user in. </summary>
/// <param name="mailAddress">Mailaddress of user to log in.</param>
/// <param name="password">Password to log in.</param>
/// <param name="deviceId">Id specifying user and hardware.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public async Task<AuthorizationResponse> DoAuthorizationAsync(
string mailAddress,
string password,
string deviceId)
{
return await DoAuthorizationAsync(
m_oCopriHost.AbsoluteUri,
requestBuilder.DoAuthorization(mailAddress, password, deviceId),
() => requestBuilder.DoAuthorization(mailAddress, "********", deviceId),
UserAgent);
}
/// <summary> Logs user out. </summary>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public async Task<AuthorizationoutResponse> DoAuthoutAsync()
{
return await DoAuthoutAsync(m_oCopriHost.AbsoluteUri, requestBuilder.DoAuthout(), UserAgent);
}
/// <summary>Gets bikes available.</summary>
/// <returns>Response holding list of bikes.</returns>
public async Task<BikesAvailableResponse> GetBikesAvailableAsync()
{
return await GetBikesAvailableAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesAvailable(), UserAgent);
}
/// <summary> Gets a list of bikes reserved/ booked by acctive user. </summary>
/// <returns>Response holding list of bikes.</returns>
public async Task<BikesReservedOccupiedResponse> GetBikesOccupiedAsync()
{
try
{
return await GetBikesOccupiedAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetBikesOccupied(), UserAgent);
}
catch (NotSupportedException)
{
// No user logged in.
await Task.CompletedTask;
return ResponseHelper.GetBikesOccupiedNone();
}
}
/// <summary> Get list of stations. </summary>
/// <returns>List of files.</returns>
public async Task<StationsAllResponse> GetStationsAsync()
{
return await GetStationsAsync(m_oCopriHost.AbsoluteUri, requestBuilder.GetStations(), UserAgent);
}
/// <summary> Get authentication keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <returns>Response holding authentication keys.</returns>
public async Task<ReservationBookingResponse> GetAuthKeys(int bikeId)
=> await GetAuthKeysAsync(m_oCopriHost.AbsoluteUri, requestBuilder.CalculateAuthKeys(bikeId), UserAgent);
/// <summary> Gets booking request response.</summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Booking response.</returns>
public async Task<ReservationBookingResponse> DoReserveAsync(
int bikeId,
Uri operatorUri)
{
return await DoReserveAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoReserve(bikeId),
UserAgent);
}
/// <summary> Gets canel booking request response.</summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on cancel booking request.</returns>
public async Task<ReservationCancelReturnResponse> DoCancelReservationAsync(
int bikeId,
Uri operatorUri)
{
return await DoCancelReservationAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoCancelReservation(bikeId),
UserAgent);
}
/// <summary> Get authentication keys. </summary>
/// <param name="bikeId">Id of the bike to get keys for.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response holding authentication keys.</returns>
public async Task<ReservationBookingResponse> CalculateAuthKeysAsync(
int bikeId,
Uri operatorUri)
=> await GetAuthKeysAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.CalculateAuthKeys(bikeId),
UserAgent);
/// <summary> Updates lock state for a booked bike. </summary>
/// <param name="bikeId">Id of the bike to update locking state for.</param>
/// <param name="location">Geolocation of lock.</param>
/// <param name="state">New locking state.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on updating locking state.</returns>
public async Task<ReservationBookingResponse> 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);
}
/// <summary> Gets booking request request. </summary>
/// <param name="bikeId">Id of the bike to book.</param>
/// <param name="guid">Used to publish GUID from app to copri. Used for initial setup of bike in copri.</param>
/// <param name="batteryPercentage">Holds the filling level percentage of the battery.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Requst on booking request.</returns>
public async Task<ReservationBookingResponse> DoBookAsync(
int bikeId,
Guid guid,
double batteryPercentage,
Uri operatorUri)
{
return await DoBookAsync(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoBook(bikeId, guid, batteryPercentage),
UserAgent);
}
/// <summary> Returns a bike. </summary>
/// <param name="bikeId">Id of the bike to return.</param>
/// <param name="location">Geolocation of lock.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on returning request.</returns>
public async Task<ReservationCancelReturnResponse> DoReturn(
int bikeId,
LocationDto location,
Uri operatorUri)
{
return await DoReturn(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoReturn(bikeId, location),
UserAgent);
}
/// <summary> Submits feedback to copri server. </summary>
/// <param name="isBikeBroken">True if bike is broken.</param>
/// <param name="message">General purpose message or error description.</param>
/// <param name="operatorUri">Holds the uri of the operator or null, in case of single operator setup.</param>
/// <returns>Response on submitting feedback request.</returns>
public async Task<SubmitFeedbackResponse> DoSubmitFeedback(
string message,
bool isBikeBroken,
Uri operatorUri) =>
await DoSubmitFeedback(
operatorUri?.AbsoluteUri ?? m_oCopriHost.AbsoluteUri,
requestBuilder.DoSubmitFeedback(message, isBikeBroken),
UserAgent);
/// <summary> Logs user in. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <remarks>Response which holds auth cookie <see cref="ResponseBase.authcookie"/></remarks>
public static async Task<AuthorizationResponse> DoAuthorizationAsync(
string copriHost,
string command,
Func<string> 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<ResponseContainer<AuthorizationResponse>>(l_oResponseText)?.tinkjson;
#else
return null;
#endif
}
/// <summary> Logs user out. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user out.</param>
public static async Task<AuthorizationoutResponse> 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<ResponseContainer<AuthorizationoutResponse>>(l_oLogoutResponse)?.tinkjson;
#else
return null;
#endif
}
/// <summary>
/// Get list of stations from file.
/// </summary>
/// <param name="p_strCopriHost">URL of the copri host to connect to.</param>
/// <param name="p_oCommand">Command to get stations.</param>
/// <returns>List of files.</returns>
public static async Task<StationsAllResponse> 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<ResponseContainer<StationsAllResponse>>(l_oStationsAllResponse)?.tinkjson;
#else
return null;
#endif
}
/// <summary> Gets a list of bikes from Copri. </summary>
/// <param name="p_strCopriHost">URL of the copri host to connect to.</param>
/// <param name="p_oCommand">Command to get bikes.</param>
/// <returns>Response holding list of bikes.</returns>
public static async Task<BikesAvailableResponse> 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
}
/// <summary> Gets a list of bikes reserved/ booked by acctive user from Copri.</summary>
/// <param name="p_strCopriHost">URL of the copri host to connect to.</param>
/// <param name="p_oCommand">Command to post.</param>
/// <returns>Response holding list of bikes.</returns>
public static async Task<BikesReservedOccupiedResponse> 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
}
/// <summary> Get auth keys from COPRI. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <returns>Response on booking request.</returns>
public static async Task<ReservationBookingResponse> 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<ResponseContainer<ReservationBookingResponse>>(l_oBikesAvaialbeResponse)?.tinkjson;
#else
return null;
#endif
}
/// <summary> Gets booking request response. </summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <returns>Response on booking request.</returns>
public static async Task<ReservationBookingResponse> 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<ResponseContainer<ReservationBookingResponse>>(l_oBikesAvaialbeResponse)?.tinkjson;
#else
return null;
#endif
}
/// <summary> Gets canel booking request response.</summary>
/// <param name="copriHost">Host to connect to. </param>
/// <param name="command">Command to log user in.</param>
/// <returns>Response on cancel booking request.</returns>
public static async Task<ReservationCancelReturnResponse> 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<ResponseContainer<ReservationCancelReturnResponse>>(l_oBikesAvaialbeResponse)?.tinkjson;
#else
return null;
#endif
}
public static async Task<ReservationBookingResponse> 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<ResponseContainer<ReservationBookingResponse>>(l_oBikesAvaialbeResponse)?.tinkjson;
#else
return null;
#endif
}
public static async Task<ReservationBookingResponse> 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<ResponseContainer<ReservationBookingResponse>>(l_oBikesAvaialbeResponse)?.tinkjson;
#else
return null;
#endif
}
public static async Task<ReservationCancelReturnResponse> 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<ResponseContainer<ReservationCancelReturnResponse>>(l_oBikesAvaialbeResponse)?.tinkjson;
#else
return null;
#endif
}
public async Task<SubmitFeedbackResponse> 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<ResponseContainer<SubmitFeedbackResponse>>(userFeedbackResponse)?.tinkjson;
#else
return null;
#endif
}
/// <summary> http get- request.</summary>
/// <param name="Url">Ulr to get info from.</param>
/// <returns>response from server</returns>
public static async Task<string> 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;
}
/// <summary> http- post request.</summary>
/// <param name="p_strCommand">Command to send.</param>
/// <param name="p_oDisplayCommand">Command to display/ log used for error handling.</param>
/// <param name="uRL">Address of server to communicate with.</param>
/// <returns>Response as text.</returns>
/// <changelog> An unused member PostAsyncHttpClient using HttpClient for posting was removed 2020-04-02.</changelog>
private static async Task<string> PostAsync(
string uRL,
string p_strCommand,
string userAgent = null,
Func<string> p_oDisplayCommand = null)
{
if (string.IsNullOrEmpty(p_strCommand))
{
Log.ForContext<CopriCallsHttps>().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<CopriCallsHttps>().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<string> 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<CopriCallsHttps>().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<CopriCallsHttps>().InformationOrError("Posting command {DisplayCommand} to host {URL} failed. {Exception}.", displayCommandFunc(), uRL, l_oException);
throw;
}
}
}
}

View file

@ -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
{
/// <summary> Prevents concurrent communictation. </summary>
private object monkeyLock = new object();
/// <summary> Builds requests.</summary>
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""
}";
/// <summary>
/// Holds the seconds after which station and bikes info is considered to be invalid.
/// Default value 1s.
/// </summary>
private TimeSpan ExpiresAfter { get; }
/// <summary> Returns false because cached values are returned. </summary>
public bool IsConnected => false;
/// <summary> Gets the merchant id.</summary>
public string MerchantId => requestBuilder.MerchantId;
/// <summary> Gets the merchant id.</summary>
public string SessionCookie => requestBuilder.SessionCookie;
/// <summary> Initializes a instance of the copri monkey store object. </summary>
/// <param name="p_strMerchantId">Id of the merchant.</param>
/// <param name="sessionCookie">Session cookie if user is logged in, null otherwise.</param>
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<BikesAvailableResponse>(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<BikesReservedOccupiedResponse>(BIKESOCCUPIED), new TimeSpan(0));
}
if (!Barrel.Current.Exists(requestBuilder.GetStations()))
{
AddToCache(JsonConvert.DeserializeObject<StationsAllResponse>(STATIONS), new TimeSpan(0));
}
}
public Task<ReservationBookingResponse> DoReserveAsync(int bikeId, Uri operatorUri)
{
throw new System.Exception("Reservierung im Offlinemodus nicht möglich!");
}
public Task<ReservationCancelReturnResponse> DoCancelReservationAsync(int p_iBikeId, Uri operatorUri)
{
throw new System.Exception("Abbrechen einer Reservierung im Offlinemodus nicht möglich!");
}
public Task<ReservationBookingResponse> CalculateAuthKeysAsync(int bikeId, Uri operatorUri)
=> throw new System.Exception("Schlosssuche im Offlinemodus nicht möglich!");
public Task<ReservationBookingResponse> 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<ReservationBookingResponse> DoBookAsync(int bikeId, Guid guid, double batteryPercentage, Uri operatorUri)
{
throw new System.Exception("Buchung im Offlinemodus nicht möglich!");
}
public Task<ReservationCancelReturnResponse> DoReturn(int bikeId, LocationDto geolocation, Uri operatorUri)
{
throw new System.Exception("Rückgabe im Offlinemodus nicht möglich!");
}
public Task<SubmitFeedbackResponse> DoSubmitFeedback(string message, bool isBikeBroken, Uri operatorUri) =>
throw new System.Exception("Übermittlung von Feedback im Offlinemodus nicht möglich!");
public Task<AuthorizationResponse> DoAuthorizationAsync(string p_strMailAddress, string p_strPassword, string p_strDeviceId)
{
throw new System.Exception("Anmelden im Offlinemodus nicht möglich!");
}
public Task<AuthorizationoutResponse> DoAuthoutAsync()
{
throw new System.Exception("Abmelden im Offlinemodus nicht möglich!");
}
public async Task<BikesAvailableResponse> GetBikesAvailableAsync()
{
var l_oBikesAvailableTask = new TaskCompletionSource<BikesAvailableResponse>();
lock (monkeyLock)
{
l_oBikesAvailableTask.SetResult(Barrel.Current.Get<BikesAvailableResponse>(requestBuilder.GetBikesAvailable()));
}
return await l_oBikesAvailableTask.Task;
}
public async Task<BikesReservedOccupiedResponse> GetBikesOccupiedAsync()
{
try
{
var l_oBikesOccupiedTask = new TaskCompletionSource<BikesReservedOccupiedResponse>();
lock (monkeyLock)
{
l_oBikesOccupiedTask.SetResult(Barrel.Current.Get<BikesReservedOccupiedResponse>(requestBuilder.GetBikesOccupied()));
}
return await l_oBikesOccupiedTask.Task;
}
catch (NotSupportedException)
{
// No user logged in.
await Task.CompletedTask;
return ResponseHelper.GetBikesOccupiedNone();
}
}
public async Task<StationsAllResponse> GetStationsAsync()
{
var l_oStationsAllTask = new TaskCompletionSource<StationsAllResponse>();
lock (monkeyLock)
{
l_oStationsAllTask.SetResult(Barrel.Current.Get<StationsAllResponse>(requestBuilder.GetStations()));
}
return await l_oStationsAllTask.Task;
}
/// <summary> Gets a value indicating whether stations are expired or not.</summary>
public bool IsStationsExpired
{
get
{
lock (monkeyLock)
{
return Barrel.Current.IsExpired(requestBuilder.GetStations());
}
}
}
/// <summary> Adds a stations all response to cache.</summary>
/// <param name="stations">Stations to add.</param>
public void AddToCache(StationsAllResponse stations)
{
AddToCache(stations, ExpiresAfter);
}
/// <summary> Adds a stations all response to cache.</summary>
/// <param name="stations">Stations to add.</param>
/// <param name="expiresAfter">Time after which anser is considered to be expired.</param>
private void AddToCache(StationsAllResponse stations, TimeSpan expiresAfter)
{
lock (monkeyLock)
{
Barrel.Current.Add(
requestBuilder.GetStations(),
JsonConvert.SerializeObject(stations),
expiresAfter);
}
}
/// <summary> Gets a value indicating whether stations are expired or not.</summary>
public bool IsBikesAvailableExpired
{
get
{
lock (monkeyLock)
{
return Barrel.Current.IsExpired(requestBuilder.GetBikesAvailable());
}
}
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
public void AddToCache(BikesAvailableResponse bikes)
{
AddToCache(bikes, ExpiresAfter);
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
/// <param name="expiresAfter">Time after which anser is considered to be expired.</param>
private void AddToCache(BikesAvailableResponse bikes, TimeSpan expiresAfter)
{
lock (monkeyLock)
{
Barrel.Current.Add(
requestBuilder.GetBikesAvailable(),
JsonConvert.SerializeObject(bikes),
expiresAfter);
}
}
/// <summary> Gets a value indicating whether stations are expired or not.</summary>
public bool IsBikesOccupiedExpired
{
get
{
lock (monkeyLock)
{
return Barrel.Current.IsExpired(requestBuilder.GetBikesOccupied());
}
}
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
public void AddToCache(BikesReservedOccupiedResponse bikes)
{
AddToCache(bikes, ExpiresAfter);
}
/// <summary> Adds a bikes response to cache.</summary>
/// <param name="bikes">Bikes to add.</param>
/// <param name="expiresAfter">Time after which anser is considered to be expired.</param>
private void AddToCache(BikesReservedOccupiedResponse bikes, TimeSpan expiresAfter)
{
lock (monkeyLock)
{
Barrel.Current.Add(
requestBuilder.GetBikesOccupied(),
JsonConvert.SerializeObject(bikes),
expiresAfter);
}
}
}
}

View file

@ -0,0 +1,28 @@
using TINK.Model.Repository.Response;
using TINK.Repository.Response;
namespace TINK.Model.Repository
{
public static class CopriCallsStatic
{
/// <summary>
/// Deserializes JSON from response string.
/// </summary>
/// <param name="p_strResponse">Response to deserialize.</param>
/// <returns></returns>
public static BikesAvailableResponse DeserializeBikesAvailableResponse(string p_strResponse)
{
return JsonConvert.DeserializeObject<ResponseContainer<BikesAvailableResponse>>(p_strResponse)?.tinkjson;
}
/// <summary>
/// Deserializes JSON from response string.
/// </summary>
/// <param name="p_strResponse">Response to deserialize.</param>
/// <returns></returns>
public static BikesReservedOccupiedResponse DeserializeBikesOccupiedResponse(string p_strResponse)
{
return JsonConvert.DeserializeObject<ResponseContainer<BikesReservedOccupiedResponse>>(p_strResponse)?.tinkjson;
}
}
}

View file

@ -0,0 +1,48 @@
namespace TINK.Model.Repository.Exception
{
/// <summary>
/// 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?
/// </summary>
public class AuthcookieNotDefinedException : InvalidResponseException<Response.ResponseBase>
{
/// <summary>Constructs a authorization exceptions. </summary>
/// <param name="p_strTextOfAction">Text describing request which is shown if validation fails.</param>
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;
}
/// <summary> Holds error description if session expired. From COPRI 4.0.0.0 1001 is the only authcookie not defined error. </summary>
private const string AUTH_FAILURE_QUERY_AUTHCOOKIENOTDEFIED = "Failure 1001: authcookie not defined";
/// <summary> Holds error description if session expired (Applies to COPRI < 4.0.0.0) </summary>
private const string AUTH_FAILURE_BOOK_AUTICOOKIENOTDEFIED = "Failure 1002: authcookie not defined";
/// <summary> Holds error description if session expired (Applies to COPRI < 4.0.0.0) </summary>
private const string AUTH_FAILURE_BIKESOCCUPIED_AUTICOOKIENOTDEFIED = "Failure 1003: authcookie not defined";
/// <summary> Holds error description if session expired. (Applies to COPRI < 4.0.0.0)</summary>
private const string AUTH_FAILURE_LOGOUT_AUTHCOOKIENOTDEFIED = "Failure 1004: authcookie not defined";
}
}

View file

@ -0,0 +1,15 @@
namespace TINK.Model.Repository.Exception
{
public class InvalidAuthorizationResponseException : InvalidResponseException<Response.ResponseBase>
{
/// <summary>Constructs a authorization exceptions. </summary>
/// <param name="p_strMail">Mail address to create a detailed error message.</param>
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)
{
}
/// <summary> Holds error description if user/ password combination is not valid. </summary>
public const string AUTH_FAILURE_STATUS_MESSAGE_UPPERCASE = "FAILURE: CANNOT GENERATE AUTHCOOKIE";
}
}

Some files were not shown because too many files have changed in this diff Show more