Manually merged.

This commit is contained in:
Oliver Hauff 2021-12-08 17:58:06 +01:00
parent c7c9f252af
commit e5c09b9b8d
33 changed files with 39827 additions and 529 deletions

View file

@ -551,39 +551,5 @@ namespace TINK.Model.Connector
return bookingFinished;
}
/// <summary> Creates a survey object from response.</summary>
/// <param name="response">Response to create survey object from.</param>
public static MiniSurveyModel Create(this ReservationCancelReturnResponse response)
{
if (response?.user_miniquery == null)
{
return new MiniSurveyModel();
}
var miniquery = response.user_miniquery;
var survey = new MiniSurveyModel
{
Title = miniquery.title,
Subtitle = miniquery.subtitle,
Footer = miniquery.footer
};
foreach (var question in miniquery?.questions?.OrderBy(x => x.Key) ?? new Dictionary<string, MiniSurveyResponse.Question>().OrderBy(x => x.Key))
{
if (string.IsNullOrEmpty(question.Key.Trim())
|| question.Value.query == null)
{
// Skip invalid entries.
continue;
}
survey.Questions.Add(
question.Key,
new MiniSurveyModel.QuestionModel());
}
return survey;
}
}
}

View file

@ -0,0 +1,589 @@
using System;
using TINK.Model.Bike;
using TINK.Model.Station;
using TINK.Repository.Response;
using TINK.Model.User.Account;
using System.Collections.Generic;
using TINK.Model.State;
using TINK.Repository.Exception;
using Serilog;
using BikeInfo = TINK.Model.Bike.BC.BikeInfo;
using IBikeInfoMutable = TINK.Model.Bikes.Bike.BC.IBikeInfoMutable;
using System.Globalization;
using TINK.Model.Station.Operator;
using Xamarin.Forms;
using System.Linq;
using TINK.Model.MiniSurvey;
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 StationsAvailableResponse stationsAllResponse)
{
// Get stations from Copri/ file/ memory, ....
if (stationsAllResponse == null
|| stationsAllResponse.stations == null)
{
// Latest list of stations could not be retrieved from provider.
return new StationDictionary();
}
Version.TryParse(stationsAllResponse.copri_version, out Version copriVersion);
var stations = new StationDictionary(p_oVersion: copriVersion);
foreach (var station in stationsAllResponse.stations)
{
if (stations.GetById(station.Value.station) != null)
{
// Can not add station to list of station. Id is not unique.
throw new InvalidResponseException<StationsAvailableResponse>(
string.Format("Station id {0} is not unique.", station.Value.station), stationsAllResponse);
}
stations.Add(new Station.Station(
station.Value.station,
station.Value.GetGroup(),
station.Value.GetPosition(),
station.Value.description,
new Data(station.Value.operator_data?.operator_name,
station.Value.operator_data?.operator_phone,
station.Value.operator_data?.operator_hours,
station.Value.operator_data?.operator_email,
!string.IsNullOrEmpty(station.Value.operator_data?.operator_color)
? Color.FromHex(station.Value.operator_data?.operator_color)
: (Color?)null)));
}
return stations;
}
/// <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(nameof(loginResponse));
}
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="bikesAvailableResponse">Response to create collection from.</param>
/// <returns>New collection of available bikes.</returns>
public static BikeCollection GetBikesAvailable(
this BikesAvailableResponse bikesAvailableResponse)
{
return GetBikesAll(
bikesAvailableResponse,
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 bikesOccupiedResponse,
string mail,
Func<DateTime> dateTimeProvider)
{
return GetBikesAll(
new BikesAvailableResponse(),
bikesOccupiedResponse,
mail,
dateTimeProvider);
}
/// <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 bikesAvailableResponse,
BikesReservedOccupiedResponse bikesOccupiedResponse,
string mail,
Func<DateTime> dateTimeProvider)
{
var bikesDictionary = new Dictionary<string, BikeInfo>();
var duplicates = new Dictionary<string, BikeInfo>();
// Get bikes from Copri/ file/ memory, ....
if (bikesAvailableResponse != null
&& bikesAvailableResponse.bikes != null)
{
foreach (var bikeInfoResponse in bikesAvailableResponse.bikes.Values)
{
var bikeInfo = BikeInfoFactory.Create(bikeInfoResponse);
if (bikeInfo == null)
{
// Response is not valid.
continue;
}
if (bikesDictionary.ContainsKey(bikeInfo.Id))
{
// Duplicates are not allowed.
Log.Error($"Duplicate bike with id {bikeInfo.Id} detected evaluating bikes available. Bike status is {bikeInfo.State.Value}.");
if (!duplicates.ContainsKey(bikeInfo.Id))
{
duplicates.Add(bikeInfo.Id, bikeInfo);
}
continue;
}
bikesDictionary.Add(bikeInfo.Id, bikeInfo);
}
}
// Get bikes from Copri/ file/ memory, ....
if (bikesOccupiedResponse != null
&& bikesOccupiedResponse.bikes_occupied != null)
{
foreach (var bikeInfoResponse in bikesOccupiedResponse.bikes_occupied.Values)
{
BikeInfo bikeInfo = BikeInfoFactory.Create(
bikeInfoResponse,
mail,
dateTimeProvider);
if (bikeInfo == null)
{
continue;
}
if (bikesDictionary.ContainsKey(bikeInfo.Id))
{
// Duplicates are not allowed.
Log.Error($"Duplicate bike with id {bikeInfo.Id} detected evaluating bikes occupied. Bike status is {bikeInfo.State.Value}.");
if (!duplicates.ContainsKey(bikeInfo.Id))
{
duplicates.Add(bikeInfo.Id, bikeInfo);
}
continue;
}
bikesDictionary.Add(bikeInfo.Id, bikeInfo);
}
}
// Remove entries which are not unique.
foreach (var l_oDuplicate in duplicates)
{
bikesDictionary.Remove(l_oDuplicate.Key);
}
return new BikeCollection(bikesDictionary);
}
}
/// <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 (string.IsNullOrEmpty(bikeInfo.station))
{
// 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(),
bikeInfo.description);
}
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(),
bikeInfo.description);
}
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 TariffDescription tariffDesciption)
{
return new Bikes.Bike.TariffDescription
{
Name = tariffDesciption?.name,
#if USCSHARP9
Number = int.TryParse(tariffDesciption?.number, out int number) ? number : null,
#else
Number = int.TryParse(tariffDesciption?.number, out int number) ? number : (int?) null,
#endif
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,
OperatorAgb = tariffDesciption?.operator_agb,
TrackingInfo = tariffDesciption?.track_info
};
}
/// <summary> Creates a booking finished object from response.</summary>
/// <param name="response">Response to create survey object from.</param>
public static BookingFinishedModel Create(this DoReturnResponse response)
{
var bookingFinished = new BookingFinishedModel
{
Co2Saving = response?.co2saving
};
if (response?.user_miniquery == null)
{
return bookingFinished;
}
var miniquery = response.user_miniquery;
bookingFinished.MiniSurvey = new MiniSurveyModel
{
Title = miniquery.title,
Subtitle = miniquery.subtitle,
Footer = miniquery.footer
};
foreach (var question in miniquery?.questions?.OrderBy(x => x.Key) ?? new Dictionary<string, MiniSurveyResponse.Question>().OrderBy(x => x.Key))
{
if (string.IsNullOrEmpty(question.Key.Trim())
|| question.Value.query == null)
{
// Skip invalid entries.
continue;
}
bookingFinished.MiniSurvey.Questions.Add(
question.Key,
new MiniSurveyModel.QuestionModel());
}
return bookingFinished;
}
/// <summary> Creates a survey object from response.</summary>
/// <param name="response">Response to create survey object from.</param>
public static MiniSurveyModel Create(this ReservationCancelReturnResponse response)
{
if (response?.user_miniquery == null)
{
return new MiniSurveyModel();
}
var miniquery = response.user_miniquery;
var survey = new MiniSurveyModel
{
Title = miniquery.title,
Subtitle = miniquery.subtitle,
Footer = miniquery.footer
};
foreach (var question in miniquery?.questions?.OrderBy(x => x.Key) ?? new Dictionary<string, MiniSurveyResponse.Question>().OrderBy(x => x.Key))
{
if (string.IsNullOrEmpty(question.Key.Trim())
|| question.Value.query == null)
{
// Skip invalid entries.
continue;
}
survey.Questions.Add(
question.Key,
new MiniSurveyModel.QuestionModel());
}
return survey;
}
}
}

View file

@ -15,6 +15,7 @@ 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;
@ -155,6 +156,7 @@ namespace TINK.Model
ISmartDevice device,
ISpecialFolder specialFolder,
ICipher cipher,
IPermissions permissions = null,
object arendiCentral = null,
Func<bool> isConnectedFunc = null,
Action<SendOrPostCallback, object> postAction = null,

View file

@ -0,0 +1,419 @@
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 TINK.Services.BluetoothLock.Crypto;
using TINK.ViewModel.Map;
using TINK.ViewModel.Settings;
using TINK.Services;
using TINK.Services.BluetoothLock.BLE;
using Xamarin.Forms;
using TINK.Model.Station;
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 (sharee.bike) 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> Holds the map area to display. </summary>
public Xamarin.Forms.GoogleMaps.MapSpan MapSpan { get; set; }
/// <summary> Gets the minimum logging level. </summary>
public LogEventLevel MinimumLogEventLevel { get; set; }
/// <summary> Gets a value indicating whether reporting level is verbose or not.</summary>
public bool IsReportLevelVerbose { 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)
.SetIsReportLevelVerbose(IsReportLevelVerbose)
.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>
#if USCSHARP9
public TimeSpan DefaultPolling => new (0, 0, 10);
#else
public TimeSpan DefaultPolling => new TimeSpan(0, 0, 10);
#endif
/// <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 /* session cookie*/, string /* mail address*/, TimeSpan, IConnector> connectorFactory,
IServicesContainer<IGeolocation> geolocationServicesContainer,
ILocksService locksService,
ISmartDevice device,
ISpecialFolder specialFolder,
ICipher cipher,
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(),
new Themes.LastenradBayern()
},
settings.ActiveTheme);
GeolocationServices = geolocationServicesContainer
?? throw new ArgumentException($"Can not instantiate {nameof(TinkApp)}- object. No geolocation services container object available.");
FilterGroupSetting = settings.GroupFilterSettings;
GroupFilterMapPage = settings.GroupFilterMapPage;
CenterMapToCurrentLocation = settings.CenterMapToCurrentLocation;
MapSpan = settings.MapSpan;
SmartDevice = 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();
ActiveUser = new User.User(
accountStore.GetType().Name == "StoreLegacy" ? new Store() : accountStore,
accountStore.Load().Result,
device.Identifier);
this.isConnectedFunc = isConnectedFunc ?? (() => CrossConnectivity.Current.IsConnected);
ExpiresAfter = settings.ExpiresAfter;
// Create filtered connector for offline mode.
m_oConnector = FilteredConnectorFactory.Create(
FilterGroupSetting.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;
IsReportLevelVerbose = settings.IsReportLevelVerbose;
WhatsNew = new WhatsNew(AppVersion, lastVersion, whatsNewShownInVersion);
if (Themes.Active.GetType().FullName == typeof(Themes.ShareeBike).FullName)
{
// Nothing to do.
// Theme to activate is default theme.
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 if (Themes.Active.GetType().FullName == typeof(Themes.LastenradBayern).FullName)
{
mergedDictionaries.Add(new Themes.LastenradBayern());
}
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 ISmartDevice SmartDevice { get; }
/// <summary> Holds delegate to determine whether device is connected or not.</summary>
private readonly 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 IStation SelectedStation { get; set; } = new NullStation();
/// <summary> Holds the stations availalbe. </summary>
public IEnumerable<IStation> Stations { get; set; } = new List<Station.Station>();
/// <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 /*userAgent*/, string /*sessionCookie*/, 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> Manages the different types of LocksService objects.</summary>
public LocksServicesContainerMutable LocksServices { get; set; }
/// <summary> Holds available app themes.</summary>
public IServicesContainer<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

@ -7,8 +7,5 @@ namespace TINK.Repository.Response
/// </summary>
public class ReservationCancelReturnResponse : BikesReservedOccupiedResponse
{
/// <summary> Mini survey.</summary>
[DataMember]
public MiniSurveyResponse user_miniquery { get; private set; }
}
}

View file

@ -0,0 +1,14 @@

namespace TINK.Repository.Response
{
/// <summary>
/// Holds the information about a cancel booking request and is used for deserialization of copri answer.
/// </summary>
public class ReservationCancelReturnResponse : BikesReservedOccupiedResponse
{
/// <summary> Mini survey.</summary>
[DataMember]
public MiniSurveyResponse user_miniquery { get; private set; }
}
}

View file

@ -28,6 +28,7 @@
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Plugin.BLE" Version="2.1.2" />
<PackageReference Include="Plugin.BluetoothLE" Version="6.3.0.19" />
<PackageReference Include="Plugin.Permissions" Version="6.0.1" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />

View file

@ -0,0 +1,74 @@
<?xml version="1.0" encoding="utf-8"?>
<Project Sdk="Microsoft.NET.Sdk">
<PropertyGroup Label="MultilingualAppToolkit">
<MultilingualAppToolkitVersion>4.0</MultilingualAppToolkitVersion>
<MultilingualFallbackLanguage>en-GB</MultilingualFallbackLanguage>
<TranslationReport Condition="'$(Configuration)' == 'Release'">true</TranslationReport>
<SuppressPseudoWarning Condition="'$(Configuration)' == 'Debug'">true</SuppressPseudoWarning>
</PropertyGroup>
<PropertyGroup>
<TargetFramework>netstandard2.0</TargetFramework>
<RootNamespace>TINK</RootNamespace>
<ReleaseVersion>3.0</ReleaseVersion>
<NeutralLanguage>en-GB</NeutralLanguage>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Debug|AnyCPU'">
<DefineConstants>TRACE;USEFLYOUT</DefineConstants>
</PropertyGroup>
<PropertyGroup Condition="'$(Configuration)|$(Platform)'=='Release|AnyCPU'">
<DefineConstants>TRACE;USEFLYOUT</DefineConstants>
</PropertyGroup>
<Import Project="$(MSBuildExtensionsPath)\Microsoft\Multilingual App Toolkit\Microsoft.Multilingual.ResxResources.targets" Label="MultilingualAppToolkit" Condition="Exists('$(MSBuildExtensionsPath)\Microsoft\Multilingual App Toolkit\v$(MultilingualAppToolkitVersion)\Microsoft.Multilingual.ResxResources.targets')" />
<Target Name="MATPrerequisite" BeforeTargets="PrepareForBuild" Condition="!Exists('$(MSBuildExtensionsPath)\Microsoft\Multilingual App Toolkit\Microsoft.Multilingual.ResxResources.targets')" Label="MultilingualAppToolkit">
<Warning Text="$(MSBuildProjectFile) is Multilingual build enabled, but the Multilingual App Toolkit is unavailable during the build. If building with Visual Studio, please check to ensure that toolkit is properly installed." />
</Target>
<ItemGroup>
<PackageReference Include="MonkeyCache" Version="1.5.2" />
<PackageReference Include="MonkeyCache.FileStore" Version="1.5.2" />
<PackageReference Include="Newtonsoft.Json" Version="13.0.1" />
<PackageReference Include="Plugin.BLE" Version="2.1.2" />
<PackageReference Include="Plugin.BluetoothLE" Version="6.3.0.19" />
<PackageReference Include="Serilog" Version="2.10.0" />
<PackageReference Include="Serilog.Sinks.Debug" Version="2.0.0" />
<PackageReference Include="Serilog.Sinks.File" Version="5.0.0" />
<PackageReference Include="System.Collections" Version="4.3.0" />
<PackageReference Include="System.Net.Http" Version="4.3.4" />
<PackageReference Include="System.Net.Primitives" Version="4.3.1" />
<PackageReference Include="System.Net.Sockets" Version="4.3.0" />
<PackageReference Include="System.Private.DataContractSerialization" Version="4.3.0" />
<PackageReference Include="System.Runtime.Serialization.Formatters" Version="4.3.0" />
<PackageReference Include="System.Runtime.Serialization.Primitives" Version="4.3.0" />
<PackageReference Include="System.Xml.XDocument" Version="4.3.0" />
<PackageReference Include="Xam.Plugin.Connectivity" Version="3.2.0" />
<PackageReference Include="Xam.Plugins.Messaging" Version="5.2.0" />
<PackageReference Include="Xamarin.Essentials" Version="1.7.0" />
<PackageReference Include="Xamarin.Forms" Version="5.0.0.2196" />
<PackageReference Include="Xamarin.Forms.GoogleMaps" Version="3.3.0" />
</ItemGroup>
<ItemGroup>
<PackageReference Update="NETStandard.Library" Version="2.0.3" />
</ItemGroup>
<ItemGroup>
<Folder Include="Services\Permissions\Essentials\" />
<Folder Include="Services\Permissions\Plugin\" />
<Folder Include="ViewModel\Info\BikeInfo\" />
<Folder Include="ViewModel\FeesAndBikes\" />
</ItemGroup>
<ItemGroup>
<ProjectReference Include="..\LockItBLE\LockItBLE.csproj" />
<ProjectReference Include="..\LockItShared\LockItShared.csproj" />
</ItemGroup>
<ItemGroup>
<Compile Update="MultilingualResources\AppResources.Designer.cs">
<DesignTime>True</DesignTime>
<AutoGen>True</AutoGen>
<DependentUpon>AppResources.resx</DependentUpon>
</Compile>
</ItemGroup>
<ItemGroup>
<EmbeddedResource Update="MultilingualResources\AppResources.resx">
<Generator>PublicResXFileCodeGenerator</Generator>
<LastGenOutput>AppResources.Designer.cs</LastGenOutput>
</EmbeddedResource>
</ItemGroup>
</Project>

View file

@ -207,7 +207,7 @@ namespace TINK.ViewModel.Map
var l_oPin = new Pin
{
Position = new Xamarin.Forms.GoogleMaps.Position(station.Position.Latitude, station.Position.Longitude),
Label = long.TryParse(station.Id, out long stationId) && stationId > CUSTOM_ICONS_COUNT
? station.GetStationName()
@ -231,13 +231,13 @@ namespace TINK.ViewModel.Map
for (int pinIndex = 0; pinIndex < stationsColorList.Count; pinIndex++)
{
var indexPartPrefix = int.TryParse(Pins[pinIndex].Tag.ToString(), out int stationId)
var indexPartPrefix = int.TryParse(Pins[pinIndex].Tag.ToString(), out int stationId)
&& stationId <= CUSTOM_ICONS_COUNT
? $"{stationId}" // there is a station marker with index letter for given station id
: "Open"; // there is no station marker. Use open marker.
var colorPartPrefix = GetRessourceNameColorPart(stationsColorList[pinIndex]);
var l_iName = $"{indexPartPrefix.ToString().PadLeft(2, '0')}_{colorPartPrefix}{(DeviceInfo.Platform == DevicePlatform.Android ? ".png" : string.Empty)}";
try
{
@ -292,7 +292,7 @@ namespace TINK.ViewModel.Map
}
/// <summary>
/// Invoked when page is shown.
/// Invoked when page is shown.
/// Starts update process.
/// </summary>
/// <param name="p_oFilterDictionaryMapPage">Holds map page filter settings.</param>
@ -307,10 +307,10 @@ namespace TINK.ViewModel.Map
Polling = TinkApp.Polling;
Log.ForContext<MapPageViewModel>().Information(
$"{(Polling != null && Polling.IsActivated ? $"Map page is appearing. Update periode is {Polling.Periode.TotalSeconds} sec." : "Map page is appearing. Polling is off.")}" +
$"{(Polling != null && Polling.IsActivated ? $"Map page is appearing. Update periode is {Polling.Periode.TotalSeconds} sec." : "Map page is appearing. Polling is off.")}" +
$"Current UI language is {Thread.CurrentThread.CurrentUICulture.Name}.");
// Update map page filter
// Update map page filter
ActiveFilterMap = TinkApp.GroupFilterMapPage;
ActionText = AppResources.ActivityTextRequestingLocationPermissions;
@ -383,9 +383,19 @@ namespace TINK.ViewModel.Map
string.Format(AppResources.MessageAppVersionIsOutdated, ContactPageViewModel.GetAppName(TinkApp.Uris.ActiveUri)),
AppResources.MessageAnswerOk);
Result<StationsAndBikesContainer> resultStationsAndBikes = await TinkApp.GetConnector(IsConnected).Query.GetBikesAndStationsAsync();
Log.ForContext<MapPageViewModel>().Error($"Outdated version of app detected. Version expected is {resultStationsAndBikes.Response.StationsAll.CopriVersion}.");
}
TinkApp.Stations = resultStationsAndBikes.Response.StationsAll;
// Set pins to their positions on map.
InitializePins(resultStationsAndBikes.Response.StationsAll);
Log.ForContext<MapPageViewModel>().Verbose("Update of pins done.");
}
if (resultStationsAndBikes.Exception?.GetType() == typeof(AuthcookieNotDefinedException))
{
Log.ForContext<MapPageViewModel>().Error("Map page is shown (probable for the first time after startup of app) and COPRI auth cookie is not defined. {@l_oException}", resultStationsAndBikes.Exception);
// COPRI reports an auth cookie error.
await ViewService.DisplayAlert(
@ -393,6 +403,9 @@ namespace TINK.ViewModel.Map
AppResources.MessageMapPageErrorAuthcookieUndefined,
AppResources.MessageAnswerOk);
await TinkApp.GetConnector(IsConnected).Command.DoLogout();
TinkApp.ActiveUser.Logout();
}
// Update pin colors.
Log.ForContext<MapPageViewModel>().Verbose("Starting update pins color...");
@ -431,12 +444,6 @@ namespace TINK.ViewModel.Map
Log.ForContext<MapPageViewModel>().Verbose("Update pins color done.");
// Move and scale before getting stations and bikes which takes some time.
ActionText = AppResources.ActivityTextCenterMap;
await MoveMapToCurrentPositionOfUser(status);
m_oViewUpdateManager = CreateUpdateTask();
try
{
// Update bikes at station or my bikes depending on context.
@ -510,7 +517,7 @@ namespace TINK.ViewModel.Map
Log.ForContext<MapPageViewModel>().Error("Getting bikes and stations in polling context failed with exception {Exception}.", exception);
}
// Check if there are alreay any pins to the map.
// Check if there are alreay any pins to the map.
// If no initialze pins.
if (Pins.Count <= 0)
{
@ -555,7 +562,7 @@ namespace TINK.ViewModel.Map
}
/// <summary>
/// Invoked when pages is closed/ hidden.
/// Invoked when pages is closed/ hidden.
/// Stops update process.
/// </summary>
public async Task OnDisappearing()
@ -576,7 +583,7 @@ namespace TINK.ViewModel.Map
// Lock action to prevent multiple instances of "BikeAtStation" being opened.
IsMapPageEnabled = false;
TinkApp.SelectedStation = TinkApp.Stations.FirstOrDefault(x => x.Id == selectedStationId)
TinkApp.SelectedStation = TinkApp.Stations.FirstOrDefault(x => x.Id == selectedStationId)
?? new Station(selectedStationId, new List<string>(), null); // Station might not be in list StationDictinaly because this list is not updatd in background task.
#if TRYNOTBACKSTYLE
@ -847,14 +854,14 @@ namespace TINK.ViewModel.Map
}
}
// Do not use property .State to get bluetooth state due
// to issue https://hausource.visualstudio.com/TINK/_workitems/edit/116 /
// Do not use property .State to get bluetooth state due
// to issue https://hausource.visualstudio.com/TINK/_workitems/edit/116 /
// see https://github.com/xabre/xamarin-bluetooth-le/issues/112#issuecomment-380994887
if (await BluetoothService.GetBluetoothState() != Plugin.BLE.Abstractions.Contracts.BluetoothState.On)
{
await ViewService.DisplayAlert(
AppResources.MessageTitleHint,
AppResources.MessageBikesManagementBluetoothActivation,
AppResources.MessageBikesManagementBluetoothActivation,
AppResources.MessageAnswerOk);
IsMapPageEnabled = true;
ActionText = "";
@ -932,4 +939,4 @@ namespace TINK.ViewModel.Map
public bool IsToggleVisible => tinkKonradToggleViewModel.IsToggleVisible;
}
}
}