mirror of
https://dev.azure.com/TeilRad/sharee.bike%20App/_git/Code
synced 2024-11-13 22:06:29 +01:00
511 lines
19 KiB
C#
511 lines
19 KiB
C#
using System;
|
|
using System.Collections.Generic;
|
|
using System.Runtime.Serialization;
|
|
using System.Threading;
|
|
using Plugin.BLE.Abstractions.Contracts;
|
|
using Plugin.Connectivity;
|
|
using Serilog;
|
|
using Serilog.Core;
|
|
using Serilog.Events;
|
|
using Serilog.Filters;
|
|
using TINK.Model.Connector;
|
|
using TINK.Model.Device;
|
|
using TINK.Model.Logging;
|
|
using TINK.Model.Services.CopriApi.ServerUris;
|
|
using TINK.Model.Settings;
|
|
using TINK.Model.Stations.StationNS;
|
|
using TINK.Model.User.Account;
|
|
using TINK.Services;
|
|
using TINK.Services.BluetoothLock;
|
|
using TINK.Services.BluetoothLock.BLE;
|
|
using TINK.Services.BluetoothLock.Crypto;
|
|
using TINK.Services.Geolocation;
|
|
using TINK.Services.Logging;
|
|
using TINK.Services.Permissions;
|
|
using TINK.Services.ThemeNS;
|
|
using TINK.Settings;
|
|
using TINK.ViewModel.Map;
|
|
using TINK.ViewModel.Settings;
|
|
|
|
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 { get; private set; }
|
|
|
|
/// <summary>
|
|
/// Holds status about whats new page.
|
|
/// </summary>
|
|
public WhatsNew WhatsNew { get; private set; }
|
|
|
|
/// <summary>Holds uris of copri servers. </summary>
|
|
public CopriServerUriList Uris { get; private set; }
|
|
|
|
/// <summary> Holds the filters loaded from settings. </summary>
|
|
public IGroupFilterSettings FilterGroupSetting { get; set; }
|
|
|
|
/// <summary> Settings determining the startup behavior of the app.</summary>
|
|
public IStartupSettings StartupSettings { get; private 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 centered to current position or not. </summary>
|
|
public bool CenterMapToCurrentLocation { get; set; }
|
|
|
|
/// <summary> Holds the map area to display when starting app for first time/ when center map to is off. </summary>
|
|
private Xamarin.Forms.GoogleMaps.MapSpan HomeMapSpan { get; }
|
|
|
|
/// <summary> Holds the map area where user is or was located or null if this position is unknown. </summary>
|
|
public Xamarin.Forms.GoogleMaps.MapSpan UserMapSpan { get; set; } = null;
|
|
|
|
/// <summary> Holds the map span to display either default span or span centered to current position depending on option <see cref="CenterMapToCurrentLocation"/>.</summary>
|
|
public Xamarin.Forms.GoogleMaps.MapSpan ActiveMapSpan
|
|
{
|
|
get
|
|
{
|
|
if (CenterMapToCurrentLocation == false)
|
|
{
|
|
return HomeMapSpan;
|
|
}
|
|
|
|
return UserMapSpan ?? HomeMapSpan;
|
|
}
|
|
}
|
|
|
|
/// <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)
|
|
.SetStartupSettings(StartupSettings)
|
|
.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));
|
|
|
|
/// <summary>
|
|
/// Update connector from filters when
|
|
/// - login state changes
|
|
/// - view is toggled (TINK to Konrad 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 period.</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 query geolocation 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, null if app is installed for the first time.</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> isConnectedFunc,
|
|
Func<bool, Uri, string /* session cookie*/, string /* mail address*/, TimeSpan, IConnector> connectorFactory,
|
|
string merchantId,
|
|
IBluetoothLE bluetoothService,
|
|
ILocationPermission locationPermissionsService,
|
|
IServicesContainer<IGeolocationService> locationServicesContainer,
|
|
ILocksService locksService,
|
|
ISmartDevice device,
|
|
ISpecialFolder specialFolder,
|
|
ICipher cipher,
|
|
ITheme theme,
|
|
Action<SendOrPostCallback, object> postAction = null,
|
|
Version currentVersion = null,
|
|
Version lastVersion = null,
|
|
Version whatsNewShownInVersion = null,
|
|
AppFlavor flavor = AppFlavor.ShareeBike)
|
|
{
|
|
PostAction = postAction
|
|
?? ((d, obj) => d(obj));
|
|
|
|
ConnectorFactory = connectorFactory
|
|
?? throw new ArgumentException($"Can not instantiate {nameof(TinkApp)}- object. No connector factory object available.");
|
|
|
|
MerchantId = merchantId
|
|
?? throw new ArgumentException($"Can not instantiate {nameof(TinkApp)}- object. No merchant id available.");
|
|
|
|
if (settings == null)
|
|
throw new ArgumentException($"Can not instantiate {nameof(TinkApp)}- object. Settings must not be null.");
|
|
|
|
Cipher = cipher ?? new Cipher();
|
|
|
|
Flavor = flavor;
|
|
|
|
// Log application and environment information.
|
|
new AppAndEnvironmentInfo().LogHeader(device, flavor, currentVersion);
|
|
|
|
var locksServices = locksService != null
|
|
? new HashSet<ILocksService> { locksService }
|
|
: new HashSet<ILocksService> {
|
|
new LockItByScanServiceEventBased(
|
|
Cipher,
|
|
bluetoothService,
|
|
async () => device.Platform == Xamarin.Essentials.DevicePlatform.Android && await locationPermissionsService.CheckStatusAsync() != Status.Granted,
|
|
() => device.Platform == Xamarin.Essentials.DevicePlatform.Android && !locationServicesContainer.Active.IsGeolcationEnabled),
|
|
new LockItByScanServicePolling(
|
|
Cipher,
|
|
bluetoothService,
|
|
async () => device.Platform == Xamarin.Essentials.DevicePlatform.Android && await locationPermissionsService.CheckStatusAsync() != Status.Granted,
|
|
() => device.Platform == Xamarin.Essentials.DevicePlatform.Android && !locationServicesContainer.Active.IsGeolcationEnabled),
|
|
new LockItByGuidService(Cipher),
|
|
#if BLUETOOTHLE // Requires LockItBluetoothle library.
|
|
new Bluetoothle.LockItByGuidService(Cipher),
|
|
#endif
|
|
new LocksServiceInReach(),
|
|
new LocksServiceOutOfReach(),
|
|
};
|
|
|
|
LocksServices = new LocksServicesContainerMutable(
|
|
lastVersion >= new Version(3, 0, 173) ? settings.ActiveLockService : LocksServicesContainerMutable.DefaultLocksservice,
|
|
locksServices);
|
|
|
|
LocksServices.SetTimeOut(settings.ConnectTimeout);
|
|
|
|
Themes = new ServicesContainerMutable(
|
|
new HashSet<string> {
|
|
ThemeSet.Konrad.ToString(),
|
|
ThemeSet.ShareeBike.ToString(),
|
|
ThemeSet.LastenradBayern.ToString()
|
|
},
|
|
Enum.TryParse(settings.ActiveTheme, true, out ThemeSet active) ? active.ToString() : ThemeSet.ShareeBike.ToString());
|
|
|
|
GeolocationServices = locationServicesContainer
|
|
?? throw new ArgumentException($"Can not instantiate {nameof(TinkApp)}- object. No geolocation services container object available.");
|
|
|
|
FilterGroupSetting = settings.GroupFilterSettings;
|
|
GroupFilterMapPage = settings.GroupFilterMapPage;
|
|
|
|
StartupSettings = settings.StartupSettings;
|
|
|
|
CenterMapToCurrentLocation = settings.CenterMapToCurrentLocation;
|
|
|
|
HomeMapSpan = 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;
|
|
|
|
if (settings.PollingParameters == null)
|
|
throw new ArgumentException($"Can not instantiate {nameof(TinkApp)}- object. Polling parameters must never be null.");
|
|
|
|
Polling = (lastVersion != null && lastVersion < new Version(3, 0, 358))
|
|
? PollingParameters.Default // Default polling period was 10s up to 3.0.357. Is 60s for later versions.
|
|
: settings.PollingParameters;
|
|
|
|
AppVersion = currentVersion ?? new Version(3, 0, 122);
|
|
|
|
MinimumLogEventLevel = settings.MinimumLogEventLevel;
|
|
|
|
IsReportLevelVerbose = settings.IsReportLevelVerbose;
|
|
|
|
WhatsNew = new WhatsNew(AppVersion, lastVersion, whatsNewShownInVersion, Flavor, SmartDevice.Platform);
|
|
|
|
if (Themes.Active.GetType().FullName == typeof(Themes.ShareeBike).FullName)
|
|
{
|
|
// Nothing to do.
|
|
// Theme to activate is default theme.
|
|
return;
|
|
}
|
|
|
|
// Set active app theme from settings.
|
|
// Value might differ from default scheme value defined in ResourceDictionary.MergedDictionaries (App.xaml)
|
|
if (theme == null)
|
|
{
|
|
Log.ForContext<TinkApp>().Error("No merged dictionary available.");
|
|
return;
|
|
}
|
|
|
|
theme.SetActiveTheme(Themes.Active);
|
|
}
|
|
|
|
/// <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 centered. </summary>
|
|
public IEnumerable<IStation> Stations { get; set; } = new List<Station>();
|
|
|
|
public IResourceUrls ResourceUrls { get; set; } = new ResourceUrls();
|
|
|
|
/// <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<IGeolocationService> GeolocationServices { get; }
|
|
|
|
/// <summary> Holds the flavor of the app, i.e. specifies if app is sharee.bike, Mein konrad or LastenRad Bayern.</summary>
|
|
public AppFlavor Flavor { get; private set; }
|
|
|
|
/// <summary> Manages the different types of LocksService objects.</summary>
|
|
public ServicesContainerMutable Themes { get; private set; }
|
|
|
|
/// <summary> Object to switch logging level. </summary>
|
|
private LoggingLevelSwitch m_oLoggingLevelSwitch;
|
|
|
|
/// <summary>
|
|
/// Object to allow switching 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="minimumLevel">New level to set.</param>
|
|
public void UpdateLoggingLevel(LogEventLevel minimumLevel)
|
|
{
|
|
if (Level.MinimumLevel == minimumLevel)
|
|
{
|
|
// Nothing to do.
|
|
return;
|
|
}
|
|
|
|
Log.CloseAndFlush(); // Close before modifying logger configuration. Otherwise a sharing violation occurs.
|
|
|
|
Level.MinimumLevel = minimumLevel;
|
|
|
|
SetupLogging(Level, LogFileParentFolder);
|
|
}
|
|
|
|
/// <summary>
|
|
/// Sets up logging.
|
|
/// </summary>
|
|
/// <param name="levelSwitch">Logging level to use.</param>
|
|
/// <param name="logFilePath">Path to logging file.</param>
|
|
public static void SetupLogging(
|
|
LoggingLevelSwitch levelSwitch,
|
|
string logFilePath)
|
|
{
|
|
bool LogToFileFilter(LogEvent e)
|
|
{
|
|
if (e.Level >= levelSwitch.MinimumLevel)
|
|
{
|
|
// If level is above global logging level do log.
|
|
return true;
|
|
}
|
|
|
|
if (!e.Properties.ContainsKey(Constants.SourceContextPropertyName))
|
|
{
|
|
// Do not log if source context is not available.
|
|
return false;
|
|
}
|
|
var sourceContex = e.Properties[Constants.SourceContextPropertyName].ToString();
|
|
|
|
if ((e.Level == LogEventLevel.Information) &&
|
|
(sourceContex.Contains(typeof(AppAndEnvironmentInfo).Namespace) /* Log App and environment info. */
|
|
|| sourceContex.Contains(typeof(ViewModel.Bikes.Bike.BluetoothLock.RequestHandler.Base).Namespace /* Log info-level messages to provide context for bluetooth log. */ )))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
if (e.Level >= LogEventLevel.Debug
|
|
&& sourceContex.Contains(typeof(LockItBase).Namespace /*Scanning, connect and management functionality */))
|
|
{
|
|
return true;
|
|
}
|
|
|
|
return false;
|
|
}
|
|
|
|
Log.Logger = new LoggerConfiguration()
|
|
.MinimumLevel.Verbose()
|
|
.WriteTo.Logger(consoleLoggerConfig => consoleLoggerConfig
|
|
.MinimumLevel.Information()
|
|
.WriteTo.Debug()
|
|
)
|
|
.WriteTo.Logger(fileLoggerConfig => fileLoggerConfig
|
|
.Filter.ByIncludingOnly(e => LogToFileFilter(e))
|
|
.WriteTo.File(logFilePath, Logging.RollingInterval.Session)
|
|
)
|
|
.WriteTo.Logger(copriLoggerConfig => copriLoggerConfig
|
|
.MinimumLevel.Debug()
|
|
.Filter.ByIncludingOnly(Matching.FromSource(typeof(LockItBase/*Scanning, connect and management functionality */).Namespace))
|
|
.WriteTo.MemoryQueueSink()
|
|
)
|
|
.CreateLogger();
|
|
}
|
|
}
|
|
}
|