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(); } } }