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
{
/// Delegate used by login view to commit user name and password.
/// Mail address used as id login.
/// Password for login.
/// True if setting credentials succeeded.
public delegate bool SetCredentialsDelegate(string p_strMailAddress, string p_strPassword);
/// Returns the id of the app (sharee.bike) to be identified by copri.
public static string MerchantId { get; private set; }
///
/// Holds status about whats new page.
///
public WhatsNew WhatsNew { get; private set; }
/// Holds uris of copri servers.
public CopriServerUriList Uris { get; private set; }
/// Holds the filters loaded from settings.
public IGroupFilterSettings FilterGroupSetting { get; set; }
/// Settings determining the startup behavior of the app.
public IStartupSettings StartupSettings { get; private set; }
/// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed.
private IGroupFilterMapPage m_oFilterDictionaryMapPage;
/// Holds the filter which is applied on the map view. Either TINK or Konrad stations are displayed.
public IGroupFilterMapPage GroupFilterMapPage
{
get => m_oFilterDictionaryMapPage;
set => m_oFilterDictionaryMapPage = value ?? new GroupFilterMapPage();
}
/// Value indicating whether map is centered to current position or not.
public bool CenterMapToCurrentLocation { get; set; }
/// Holds the map area to display when starting app for first time/ when center map to is off.
private Xamarin.Forms.GoogleMaps.MapSpan HomeMapSpan { get; }
/// Holds the map area where user is or was located or null if this position is unknown.
public Xamarin.Forms.GoogleMaps.MapSpan UserMapSpan { get; set; } = null;
/// Holds the map span to display either default span or span centered to current position depending on option .
public Xamarin.Forms.GoogleMaps.MapSpan ActiveMapSpan
{
get
{
if (CenterMapToCurrentLocation == false)
{
return HomeMapSpan;
}
return UserMapSpan ?? HomeMapSpan;
}
}
/// Gets the minimum logging level.
public LogEventLevel MinimumLogEventLevel { get; set; }
/// Gets a value indicating whether reporting level is verbose or not.
public bool IsReportLevelVerbose { get; set; }
/// Holds the uri which is applied after restart.
public Uri NextActiveUri { get; set; }
/// Saves object to file.
public void Save()
=> JsonSettingsDictionary.Serialize(
SettingsFileFolder,
new Dictionary()
.SetGroupFilterMapPage(GroupFilterMapPage)
.SetCopriHostUri(NextActiveUri.AbsoluteUri)
.SetPollingParameters(Polling)
.SetGroupFilterSettings(FilterGroupSetting)
.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));
///
/// Update connector from filters when
/// - login state changes
/// - view is toggled (TINK to Konrad and vice versa)
///
public void UpdateConnector()
{
// Create filtered connector.
m_oConnector = FilteredConnectorFactory.Create(
FilterGroupSetting.DoFilter(ActiveUser.DoFilter(GroupFilterMapPage.DoFilter())),
m_oConnector.Connector);
}
/// Polling period.
public PollingParameters Polling { get; set; }
public TimeSpan ExpiresAfter { get; set; }
/// Holds the version of the app.
public Version AppVersion { get; }
///
/// Holds the default polling value.
///
#if USCSHARP9
public TimeSpan DefaultPolling => new (0, 0, 10);
#else
public TimeSpan DefaultPolling => new TimeSpan(0, 0, 10);
#endif
/// Constructs TinkApp object.
///
///
///
///
/// Null in productive context. Service to query geolocation for testing purposes. Parameter can be made optional.
/// Null in productive context. Service to control locks/ get locks information for testing proposes. Parameter can be made optional.
/// Object allowing platform specific operations.
///
///
/// True if connector has access to copri server, false if cached values are used.
/// Version of the app. If null version is set to a fixed dummy value (3.0.122) for testing purposes.
/// Version of app which was used before this session, null if app is installed for the first time.
/// Holds
/// - the version when whats new info was shown last or
/// - version of application used last if whats new functionality was not implemented in this version or
/// - null if app is installed for the first time.
/// ///
public TinkApp(
Settings.Settings settings,
IStore accountStore,
Func isConnectedFunc,
Func connectorFactory,
string merchantId,
IBluetoothLE bluetoothService,
ILocationPermission locationPermissionsService,
IServicesContainer locationServicesContainer,
ILocksService locksService,
ISmartDevice device,
ISpecialFolder specialFolder,
ICipher cipher,
ITheme theme,
object arendiCentral = null,
Action 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 { locksService }
: new HashSet {
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
#if ARENDI // Requires LockItArendi library.
new Arendi.LockItByGuidService(Cipher, arendiCentral),
new Arendi.LockItByScanService(Cipher, arendiCentral),
#endif
new LocksServiceInReach(),
new LocksServiceOutOfReach(),
};
LocksServices = new LocksServicesContainerMutable(
lastVersion >= new Version(3, 0, 173) ? settings.ActiveLockService : LocksServicesContainerMutable.DefaultLocksservice,
locksServices);
LocksServices.SetTimeOut(settings.ConnectTimeout);
Themes = new ServicesContainerMutable(
new HashSet {
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().Error("No merged dictionary available.");
return;
}
theme.SetActiveTheme(Themes.Active);
}
/// Holds the user of the app.
[DataMember]
public User.User ActiveUser { get; }
/// Reference of object which provides device information.
public ISmartDevice SmartDevice { get; }
/// Holds delegate to determine whether device is connected or not.
private readonly Func isConnectedFunc;
/// Gets whether device is connected to Internet or not.
public bool GetIsConnected() => isConnectedFunc();
/// Holds the folder where settings files are stored.
public string SettingsFileFolder { get; }
/// Holds folder parent of the folder where log files are stored.
public string LogFileParentFolder => LogToExternalFolder && !string.IsNullOrEmpty(ExternalFolder) ? ExternalFolder : SettingsFileFolder;
/// Holds a value indicating whether to log to external or internal folder.
public bool LogToExternalFolder { get; set; }
/// Holds a value indicating whether Site caching is on or off.
public bool IsSiteCachingOn { get; set; }
/// External folder.
public string ExternalFolder { get; }
public ICipher Cipher { get; }
/// Name of the station which is selected.
public IStation SelectedStation { get; set; } = new NullStation();
/// Holds the stations centered.
public IEnumerable Stations { get; set; } = new List();
public IResourceUrls ResourceUrls { get; set; } = new ResourceUrls();
/// Action to post to GUI thread.
public Action PostAction { get; }
/// Function which creates a connector depending on connected status.
private Func ConnectorFactory { get; }
/// Holds the object which provides offline data.
private IFilteredConnector m_oConnector;
/// Holds the system to copri.
public IFilteredConnector GetConnector(bool isConnected)
{
if (m_oConnector.IsConnected == isConnected
&& m_oConnector.Command.SessionCookie == ActiveUser.SessionCookie)
{
// Neither connection nor logged in stated changed.
return m_oConnector;
}
// Connected state changed. New connection object has to be created.
m_oConnector = FilteredConnectorFactory.Create(
FilterGroupSetting.DoFilter(ActiveUser.DoFilter(GroupFilterMapPage.DoFilter())),
ConnectorFactory(
isConnected,
Uris.ActiveUri,
ActiveUser.SessionCookie,
ActiveUser.Mail,
ExpiresAfter));
return m_oConnector;
}
/// Manages the different types of LocksService objects.
public LocksServicesContainerMutable LocksServices { get; set; }
/// Holds available app themes.
public IServicesContainer GeolocationServices { get; }
/// Holds the flavor of the app, i.e. specifies if app is sharee.bike, Mein konrad or LastenRad Bayern.
public AppFlavor Flavor { get; private set; }
/// Manages the different types of LocksService objects.
public ServicesContainerMutable Themes { get; private set; }
/// Object to switch logging level.
private LoggingLevelSwitch m_oLoggingLevelSwitch;
///
/// Object to allow switching logging level
///
public LoggingLevelSwitch Level
{
get
{
if (m_oLoggingLevelSwitch == null)
{
m_oLoggingLevelSwitch = new LoggingLevelSwitch
{
// Set warning level to error.
MinimumLevel = Settings.Settings.DEFAULTLOGGINLEVEL
};
}
return m_oLoggingLevelSwitch;
}
}
/// Updates logging level.
/// New level to set.
public void UpdateLoggingLevel(LogEventLevel 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);
}
///
/// Sets up logging.
///
/// Logging level to use.
/// Path to logging file.
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();
}
}
}