using Serilog; using Serilog.Events; using System; using System.Collections.Generic; using System.ComponentModel; using System.Threading.Tasks; using TINK.Model; using TINK.Model.Connector; using TINK.Repository.Exception; using TINK.Model.Services.Geolocation; using TINK.Settings; using TINK.View; using TINK.ViewModel.Map; using TINK.ViewModel.Settings; using System.Linq; using TINK.Model.User.Account; using TINK.Services.BluetoothLock; using Xamarin.Forms; using TINK.Services; namespace TINK.ViewModel { /// <summary> /// View model for settings. /// </summary> public class SettingsPageViewModel : INotifyPropertyChanged { /// <summary> /// Reference on view servcie to show modal notifications and to perform navigation. /// </summary> private IViewService m_oViewService; /// <summary> /// Fired if a property changes. /// </summary> public event PropertyChangedEventHandler PropertyChanged; /// <summary> Object to manage update of view model objects from Copri.</summary> private IPollingUpdateTaskManager m_oViewUpdateManager; /// <summary> List of copri server uris.</summary> public CopriServerUriListViewModel CopriServerUriList { get; } /// <summary> Manages selection of locks services.</summary> public LocksServicesViewModel LocksServices { get; } /// <summary> Manages selection of geolocation services.</summary> public ServicesViewModel GeolocationServices { get; } /// <summary> /// Object to switch logging level. /// </summary> private LogEventLevel m_oMinimumLogEventLevel; /// <summary> Gets a value indicating whether reporting level is verbose or not.</summary> public bool IsReportLevelVerbose { get; set; } /// <summary> List of copri server uris.</summary> public ServicesViewModel Themes { get; } /// <summary> Reference on the tink app instance. </summary> private ITinkApp TinkApp { get; } IServicesContainer<IGeolocation> GeoloctionServicesContainer { get; } /// <summary> Constructs a settings page view model object.</summary> /// <param name="tinkApp"> Reference to tink app model.</param> /// <param name="p_oUser"></param> /// <param name="p_oDevice"></param> /// <param name="p_oFilterGroup">Filter to apply on stations and bikes.</param> /// <param name="p_oUris">Available copri server host uris including uri to use for next start.</param> /// <param name="p_oPolling"> Holds whether to poll or not and the periode leght is polling is on. </param> /// <param name="p_oDefaultPollingPeriode">Default polling periode lenght.</param> /// <param name="p_oMinimumLogEventLevel">Controls logging level.</param> /// <param name="p_oViewService">Interface to view</param> public SettingsPageViewModel( ITinkApp tinkApp, IServicesContainer<IGeolocation> geoloctionServicesContainer, IViewService p_oViewService) { TinkApp = tinkApp ?? throw new ArgumentException("Can not instantiate settings page view model- object. No tink app object available."); GeoloctionServicesContainer = geoloctionServicesContainer ?? throw new ArgumentException($"Can not instantiate {nameof(SettingsPageViewModel)}- object. Geolocation services container object must not be null."); m_oViewService = p_oViewService ?? throw new ArgumentException("Can not instantiate settings page view model- object. No user view service available."); m_oMinimumLogEventLevel = TinkApp.MinimumLogEventLevel; IsReportLevelVerbose = TinkApp.IsReportLevelVerbose; CenterMapToCurrentLocation = TinkApp.CenterMapToCurrentLocation; ExternalFolder = TinkApp.ExternalFolder; IsLogToExternalFolderVisible = !string.IsNullOrEmpty(ExternalFolder); LogToExternalFolderDisplayValue = IsLogToExternalFolderVisible ? TinkApp.LogToExternalFolder : false; IsSiteCachingOnDisplayValue = TinkApp.IsSiteCachingOn; if (TinkApp.Uris == null || TinkApp.Uris.Uris.Count <= 0) { throw new ArgumentException("Can not instantiate settings page view model- object. No uri- list available."); } if (string.IsNullOrEmpty(TinkApp.NextActiveUri.AbsoluteUri)) { throw new ArgumentException("Can not instantiate settings page view model- object. Next active uri must not be null or empty."); } GroupFilter = new SettingsBikeFilterViewModel( TinkApp.FilterGroupSetting, TinkApp.ActiveUser.IsLoggedIn ? TinkApp.ActiveUser.Group : null); m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); Polling = new PollingViewModel(TinkApp.Polling); ExpiresAfterTotalSeconds = Convert.ToInt32(TinkApp.ExpiresAfter.TotalSeconds); CopriServerUriList = new CopriServerUriListViewModel(TinkApp.Uris); Themes = new ServicesViewModel( TinkApp.Themes.Select(x => x.GetType().FullName), new Dictionary<string, string> { { typeof(Themes.Konrad).FullName, "Konrad" }, { typeof(Themes.ShareeBike).FullName, "sharee.bike" } }, TinkApp.Themes.Active.GetType().FullName); Themes.PropertyChanged += OnThemesChanged; LocksServices = new LocksServicesViewModel( TinkApp.LocksServices.Active.TimeOut.MultiConnect, new ServicesViewModel( TinkApp.LocksServices, new Dictionary<string, string> { { typeof(LocksServiceInReach).FullName, "Simulation - AllLocksInReach" }, { typeof(LocksServiceOutOfReach).FullName, "Simulation - AllLocksOutOfReach" }, { typeof(Services.BluetoothLock.BLE.LockItByScanServiceEventBased).FullName, "Live - Scan" }, { typeof(Services.BluetoothLock.BLE.LockItByScanServicePolling).FullName, "Live - Scan (Polling)" }, { typeof(Services.BluetoothLock.BLE.LockItByGuidService).FullName, "Live - Guid" }, /* { typeof(Services.BluetoothLock.Arendi.LockItByGuidService).FullName, "Live - Guid (Arendi)" }, { typeof(Services.BluetoothLock.Arendi.LockItByScanService).FullName, "Live - Scan (Arendi)" }, { typeof(Services.BluetoothLock.Bluetoothle.LockItByGuidService).FullName, "Live - Guid (Ritchie)" }, */ }, TinkApp.LocksServices.Active.GetType().FullName)); GeolocationServices = new ServicesViewModel( GeoloctionServicesContainer.Select(x => x.GetType().FullName), new Dictionary<string, string> { { typeof(LastKnownGeolocationService).FullName, "Smartdevice-LastKnowGeolocation" }, { typeof(GeolocationService).FullName, "Smartdevice-MediumAccuracy" }, { typeof(SimulatedGeolocationService).FullName, "Simulation-AlwaysSamePosition" } }, GeoloctionServicesContainer.Active.GetType().FullName); } /// <summary> /// User switches scheme. /// </summary> private void OnThemesChanged(object sender, PropertyChangedEventArgs e) { // Set active theme (leads to switch of title) TinkApp.Themes.SetActive(Themes.Active); // Switch theme. ICollection<ResourceDictionary> mergedDictionaries = Application.Current.Resources.MergedDictionaries; if (mergedDictionaries == null) { Log.ForContext<SettingsPageViewModel>().Error("No merged dictionary available."); return; } mergedDictionaries.Clear(); if (Themes.Active == typeof(Themes.Konrad).FullName) { mergedDictionaries.Add(new Themes.Konrad()); } else if (Themes.Active == typeof(Themes.ShareeBike).FullName) { mergedDictionaries.Add(new Themes.ShareeBike()); } else { Log.ForContext<SettingsPageViewModel>().Debug($"No theme {Themes.Active} found."); } } /// <summary> Holds information whether app is connected to web or not. </summary> private bool? isConnected = null; /// <summary>Exposes the is connected state. </summary> private bool IsConnected { get => isConnected ?? false; set { isConnected = value; } } /// <summary> Holds a value indicating whether group filters GUI are visible or not</summary> public bool IsGroupFilterVisible => GroupFilter.Count > 0; /// <summary> Holds the bike types to fade out or show</summary> public SettingsBikeFilterViewModel GroupFilter { get; } /// <summary> Gets the value to path were copri mock files are located (for debugging purposes).</summary> public string ExternalFolder { get; } /// <summary> /// Gets the value to path were copri mock files are located (for debugging purposes). /// </summary> public string InternalPath => TinkApp.SettingsFileFolder; /// <summary> /// Gets the value of device identifier (for debugging purposes). /// </summary> public string DeviceIdentifier { get { return TinkApp.SmartDevice.Identifier; } } /// <summary> /// Invoked when page is shutdown. /// Currently invoked by code behind, would be nice if called by XAML in future versions. /// </summary> public async Task OnDisappearing() { try { Log.ForContext<SettingsPageViewModel>().Information($"Entering {nameof(OnDisappearing)}..."); // Update model values. TinkApp.NextActiveUri = CopriServerUriList.NextActiveUri; TinkApp.Polling = new PollingParameters( new TimeSpan(0, 0, Polling.PeriodeTotalSeconds), Polling.IsActivated); TinkApp.ExpiresAfter = TimeSpan.FromSeconds(ExpiresAfterTotalSeconds); var filterGroup = GroupFilter.ToDictionary(x => x.Key, x => x.State); TinkApp.FilterGroupSetting = new GroupFilterSettings(filterGroup.Count > 0 ? filterGroup : null); // Update map page filter. // Reasons for which map page filter has to be updated: // - user activated/ deactivated a group (TINKCorpi/ TINKSms/ Konrad) // - user logged off TinkApp.GroupFilterMapPage = GroupFilterMapPageHelper.CreateUpdated( TinkApp.GroupFilterMapPage, TinkApp.ActiveUser.DoFilter(TinkApp.FilterGroupSetting.DoFilter())); TinkApp.CenterMapToCurrentLocation = CenterMapToCurrentLocation; if (IsLogToExternalFolderVisible) { // If no external folder is available do not update model value. TinkApp.LogToExternalFolder = LogToExternalFolderDisplayValue; } TinkApp.IsSiteCachingOn = IsSiteCachingOnDisplayValue; TinkApp.MinimumLogEventLevel = m_oMinimumLogEventLevel; // Update value to be serialized. TinkApp.UpdateLoggingLevel(m_oMinimumLogEventLevel); // Update logging server. TinkApp.IsReportLevelVerbose = IsReportLevelVerbose; TinkApp.LocksServices.SetActive(LocksServices.Services.Active); GeoloctionServicesContainer.SetActive(GeolocationServices.Active); TinkApp.LocksServices.SetTimeOut(TimeSpan.FromSeconds(LocksServices.ConnectTimeoutSec)); // Persist settings in case app is closed directly. TinkApp.Save(); TinkApp.UpdateConnector(); await m_oViewUpdateManager.StopUpdatePeridically(); Log.ForContext<SettingsPageViewModel>().Information($"{nameof(OnDisappearing)} done."); } catch (Exception l_oException) { await m_oViewService.DisplayAlert( "Fehler", $"Ein unerwarteter Fehler ist aufgetreten. \r\n{l_oException.Message}", "OK"); } } /// <summary> True if there is an error message to display.</summary> /// <summary> /// Exception which occurred getting bike information. /// </summary> protected Exception Exception { get; set; } /// <summary> /// If true debug controls are visible, false if not. /// </summary> public Permissions DebugLevel { get { return (Exception == null || Exception is WebConnectFailureException) ? TinkApp.ActiveUser.DebugLevel : Permissions.None; } } /// <summary>Polling periode.</summary> public PollingViewModel Polling { get; } /// <summary> Active logging level</summary> public string SelectedLoggingLevel { get { return m_oMinimumLogEventLevel.ToString(); } set { if (!Enum.TryParse(value, out LogEventLevel l_oNewLevel)) { return; } m_oMinimumLogEventLevel = l_oNewLevel; } } public bool CenterMapToCurrentLocation { get; set; } /// <summary> Holds either /// - a value indicating whether to use external folder (e.g. SD card)/ or internal folder for storing log-files or /// - is false if external folder is not available /// </summary> public bool LogToExternalFolderDisplayValue { get; set; } public bool IsSiteCachingOnDisplayValue { get; set; } /// <summary> Holds a value indicating whether user can use external folder (e.g. SD card) for storing log-files.</summary> public bool IsLogToExternalFolderVisible { get; } /// <summary> /// Holds the logging level serilog provides. /// </summary> public List<string> LoggingLevels { get { return new List<string> { LogEventLevel.Verbose.ToString(), LogEventLevel.Debug.ToString(), LogEventLevel.Information.ToString(), LogEventLevel.Warning.ToString(), LogEventLevel.Error.ToString(), LogEventLevel.Fatal.ToString(), }; } } double expiresAfterTotalSeconds; public double ExpiresAfterTotalSeconds { get => expiresAfterTotalSeconds; set { if (value == expiresAfterTotalSeconds) { return; } expiresAfterTotalSeconds = value; PropertyChanged?.Invoke(this, new PropertyChangedEventArgs(nameof(ExpiresAfterTotalSecondsText))); } } public string ExpiresAfterTotalSecondsText { get => expiresAfterTotalSeconds.ToString("0"); } } }