using System; using System.Collections.Generic; using System.ComponentModel; using System.Linq; using System.Threading.Tasks; using Serilog; using Serilog.Events; using ShareeBike.Model; using ShareeBike.Model.Connector; using ShareeBike.Model.User.Account; using ShareeBike.MultilingualResources; using ShareeBike.Repository.Exception; using ShareeBike.Services; using ShareeBike.Services.BluetoothLock; using ShareeBike.Services.Geolocation; using ShareeBike.Settings; using ShareeBike.View; using ShareeBike.ViewModel.Map; using ShareeBike.ViewModel.Settings; using Xamarin.Forms; namespace ShareeBike.ViewModel { /// /// View model for settings. /// public class SettingsPageViewModel : INotifyPropertyChanged { /// /// Reference on view service to show modal notifications and to perform navigation. /// private IViewService m_oViewService; /// /// Fired if a property changes. /// public event PropertyChangedEventHandler PropertyChanged; /// Object to manage update of view model objects from Copri. private IPollingUpdateTaskManager m_oViewUpdateManager; /// List of copri server uris. public CopriServerUriListViewModel CopriServerUriList { get; } /// Manages selection of locks services. public LocksServicesViewModel LocksServices { get; } /// Settings determining the startup behavior of the app. public ServicesViewModel StartupSettings { get; } /// Manages selection of geolocation services. public ServicesViewModel GeolocationServices { get; } /// /// Object to switch logging level. /// private LogEventLevel m_oMinimumLogEventLevel; /// Gets a value indicating whether reporting level is verbose or not. public bool IsReportLevelVerbose { get; set; } /// List of copri server uris. public ServicesViewModel Themes { get; } /// Reference on the shareeBike app instance. private IShareeBikeApp ShareeBikeApp { get; } IServicesContainer GeolocationServicesContainer { get; } /// Constructs a settings page view model object. /// Reference to shareeBike app model. /// /// Interface to view public SettingsPageViewModel( IShareeBikeApp shareeBikeApp, IServicesContainer geolocationServicesContainer, IViewService viewService) { ShareeBikeApp = shareeBikeApp ?? throw new ArgumentException("Can not instantiate settings page view model- object. No shareeBike app object available."); GeolocationServicesContainer = geolocationServicesContainer ?? throw new ArgumentException($"Can not instantiate {nameof(SettingsPageViewModel)}- object. Geolocation services container object must not be null."); m_oViewService = viewService ?? throw new ArgumentException("Can not instantiate settings page view model- object. No user view service available."); m_oMinimumLogEventLevel = ShareeBikeApp.MinimumLogEventLevel; IsReportLevelVerbose = ShareeBikeApp.IsReportLevelVerbose; CenterMapToCurrentLocation = ShareeBikeApp.CenterMapToCurrentLocation; ExternalFolder = ShareeBikeApp.ExternalFolder; IsLogToExternalFolderVisible = !string.IsNullOrEmpty(ExternalFolder); LogToExternalFolderDisplayValue = IsLogToExternalFolderVisible ? ShareeBikeApp.LogToExternalFolder : false; IsSiteCachingOnDisplayValue = ShareeBikeApp.IsSiteCachingOn; if (ShareeBikeApp.Uris == null || ShareeBikeApp.Uris.Uris.Count <= 0) { throw new ArgumentException("Can not instantiate settings page view model- object. No uri- list available."); } if (string.IsNullOrEmpty(ShareeBikeApp.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( ShareeBikeApp.FilterGroupSetting, ShareeBikeApp.ActiveUser.IsLoggedIn ? ShareeBikeApp.ActiveUser.Group : null); GroupFilter.PropertyChanged += (s, e) => { // Serialize on value changed. On iOS OnDisappearing might not be invoked (when app is directly closed before leaving settings page). try { var filterGroup = GroupFilter.ToDictionary(x => x.Key, x => x.State); ShareeBikeApp.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 (cargo/ city bikes) ShareeBikeApp.GroupFilterMapPage = GroupFilterMapPageHelper.CreateUpdated( ShareeBikeApp.GroupFilterMapPage, ShareeBikeApp.ActiveUser.DoFilter(ShareeBikeApp.FilterGroupSetting.DoFilter())); ShareeBikeApp.Save(); } catch (Exception ex) { Log.ForContext().Error(ex, "Serializing startup page failed."); } }; m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); Polling = new PollingViewModel(ShareeBikeApp.Polling); ExpiresAfterTotalSeconds = Convert.ToInt32(ShareeBikeApp.ExpiresAfter.TotalSeconds); CopriServerUriList = new CopriServerUriListViewModel(ShareeBikeApp.Uris); Themes = new ServicesViewModel( ShareeBikeApp.Themes.Select(x => x.GetType().FullName), new Dictionary { { typeof(Themes.ShareeBike).FullName, "sharee.bike" /* display name in picker */}, { typeof(Themes.LastenradBayern).FullName, "LastenradBayern" /* display name in picker */} }, ShareeBikeApp.Themes.Active.GetType().FullName); Themes.PropertyChanged += OnThemesChanged; LocksServices = new LocksServicesViewModel( ShareeBikeApp.LocksServices.Active.TimeOut.MultiConnect, new ServicesViewModel( ShareeBikeApp.LocksServices, new Dictionary { { 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" }, }, ShareeBikeApp.LocksServices.Active.GetType().FullName)); GeolocationServices = new ServicesViewModel( GeolocationServicesContainer.Select(x => x.GetType().FullName), new Dictionary { { typeof(LastKnownGeolocationService).FullName, "LastKnowGeolocation" }, { typeof(GeolocationAccuracyMediumService).FullName, "Medium Accuracy" }, { typeof(GeolocationAccuracyHighService).FullName, "High Accuracy" }, { typeof(GeolocationAccuracyBestService).FullName, "Best Accuracy" }, { typeof(SimulatedGeolocationService).FullName, "Simulation-AlwaysSamePosition" } }, GeolocationServicesContainer.Active.GetType().FullName); StartupSettings = new PickerViewModel( new Dictionary { { ViewTypes.MapPage.ToString(), AppResources.MarkingBikeLocations }, { ViewTypes.SelectBikePage.ToString(), AppResources.MarkingSelectBike }, }, shareeBikeApp.StartupSettings.StartupPage.ToString()); StartupSettings.PropertyChanged += (s, e) => { // Serialize on value changed. On iOS OnDisappearing might not be invoked (when app is directly closed before leaving settings page). try { ShareeBikeApp.StartupSettings.StartupPage = Enum.TryParse(StartupSettings.Active, out ViewTypes startupPage) ? startupPage : Model.Settings.StartupSettings.DefaultStartupPage; ShareeBikeApp.Save(); } catch (Exception ex) { Log.ForContext().Error(ex, "Serializing startup page failed."); } }; } /// /// User switches scheme. /// private void OnThemesChanged(object sender, PropertyChangedEventArgs e) { // Set active theme (leads to switch of title) ShareeBikeApp.Themes.SetActive(Themes.Active); // Switch theme. ICollection mergedDictionaries = Application.Current.Resources.MergedDictionaries; if (mergedDictionaries == null) { Log.ForContext().Error("No merged dictionary available."); return; } mergedDictionaries.Clear(); if (Themes.Active == typeof(Themes.ShareeBike).FullName) { mergedDictionaries.Add(new Themes.ShareeBike()); } else if (Themes.Active == typeof(Themes.LastenradBayern).FullName) { mergedDictionaries.Add(new Themes.LastenradBayern()); } else { Log.ForContext().Debug($"No theme {Themes.Active} found."); } } /// Holds information whether app is connected to web or not. private bool? isConnected = null; /// Exposes the is connected state. private bool IsConnected { get => isConnected ?? false; set { isConnected = value; } } /// Holds a value indicating whether group filters GUI are visible or not public bool IsGroupFilterVisible => GroupFilter.Count > 0; /// Holds the bike types to fade out or show public SettingsBikeFilterViewModel GroupFilter { get; } /// Gets the value to path were copri mock files are located (for debugging purposes). public string ExternalFolder { get; } /// /// Gets the value to path were copri mock files are located (for debugging purposes). /// public string InternalPath => ShareeBikeApp.SettingsFileFolder; /// /// Gets the value of device identifier (for debugging purposes). /// public string DeviceIdentifier => ShareeBikeApp.SmartDevice.Identifier; /// /// Invoked when page is shutdown. /// Currently invoked by code behind, would be nice if called by XAML in future versions. /// public async Task OnDisappearing() { try { Log.ForContext().Information($"Entering {nameof(OnDisappearing)}..."); // Update model values. ShareeBikeApp.NextActiveUri = CopriServerUriList.NextActiveUri; ShareeBikeApp.Polling = new PollingParameters( new TimeSpan(0, 0, Polling.PeriodeTotalSeconds), Polling.IsActivated); ShareeBikeApp.ExpiresAfter = TimeSpan.FromSeconds(ExpiresAfterTotalSeconds); if (IsLogToExternalFolderVisible) { // If no external folder is available do not update model value. ShareeBikeApp.LogToExternalFolder = LogToExternalFolderDisplayValue; } ShareeBikeApp.IsSiteCachingOn = IsSiteCachingOnDisplayValue; ShareeBikeApp.MinimumLogEventLevel = m_oMinimumLogEventLevel; // Update value to be serialized. ShareeBikeApp.UpdateLoggingLevel(m_oMinimumLogEventLevel); // Update logging server. ShareeBikeApp.IsReportLevelVerbose = IsReportLevelVerbose; ShareeBikeApp.LocksServices.SetActive(LocksServices.Services.Active); GeolocationServicesContainer.SetActive(GeolocationServices.Active); ShareeBikeApp.LocksServices.SetTimeOut(TimeSpan.FromSeconds(LocksServices.ConnectTimeoutSec)); // Persist settings in case app is closed directly. ShareeBikeApp.Save(); ShareeBikeApp.UpdateConnector(); await m_oViewUpdateManager.StopAsync(); Log.ForContext().Information($"{nameof(OnDisappearing)} done."); } catch (Exception l_oException) { await m_oViewService.DisplayAlert( AppResources.ErrorPageNotLoadedTitle, $"{AppResources.ErrorPageNotLoaded}\r\n{l_oException.Message}", AppResources.MessageAnswerOk); } } /// True if there is an error message to display. /// /// Exception which occurred getting bike information. /// protected Exception Exception { get; set; } /// /// If true debug controls are visible, false if not. /// public Permissions DebugLevel { get { return (Exception == null || Exception is WebConnectFailureException) ? ShareeBikeApp.ActiveUser.DebugLevel : Permissions.None; } } /// Empty if no user is logged in session cookie otherwise. public string SessionCookie => ShareeBikeApp.ActiveUser.IsLoggedIn ? ShareeBikeApp.ActiveUser.SessionCookie : ""; /// Polling period. public PollingViewModel Polling { get; } /// Active logging level public string SelectedLoggingLevel { get { return m_oMinimumLogEventLevel.ToString(); } set { if (!Enum.TryParse(value, out LogEventLevel l_oNewLevel)) { return; } m_oMinimumLogEventLevel = l_oNewLevel; } } bool _CenterMapToCurrentLocation = true; public bool CenterMapToCurrentLocation { get => _CenterMapToCurrentLocation; set { if (value == _CenterMapToCurrentLocation) { // Nothing to do. return; } _CenterMapToCurrentLocation = value; // Serialize on value changed. On iOS OnDisappearing might not be invoked (when app is directly closed before leaving settings page). ShareeBikeApp.CenterMapToCurrentLocation = CenterMapToCurrentLocation; ShareeBikeApp.Save(); } } /// 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 /// public bool LogToExternalFolderDisplayValue { get; set; } public bool IsSiteCachingOnDisplayValue { get; set; } /// Holds a value indicating whether user can use external folder (e.g. SD card) for storing log-files. public bool IsLogToExternalFolderVisible { get; } /// /// Holds the logging level serilog provides. /// public List LoggingLevels { get { return new List { 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"); } } }