using Serilog; using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Threading; using System.Threading.Tasks; using TINK.Model.Bike; using TINK.Model.Connector; using TINK.Services.BluetoothLock; using TINK.Services.Geolocation; using TINK.Model.User; using TINK.View; using TINK.ViewModel.Bikes.Bike; using TINK.ViewModel.Bikes.Bike.BC; using TINK.Services.Permissions; using Plugin.BLE.Abstractions.Contracts; using TINK.MultilingualResources; using TINK.Model.Device; namespace TINK.ViewModel.Bikes { public abstract class BikesViewModel : ObservableCollection, IBikesViewModel { /// Provides info about the smart device (phone, tablet, ...). protected ISmartDevice SmartDevice; /// /// Reference on view service to show modal notifications and to perform navigation. /// protected IViewService ViewService { get; } /// /// Holds the exception which occurred getting bikes occupied information. /// private Exception m_oException; /// Provides a connector object. protected Func ConnectorFactory { get; } protected IGeolocation Geolocation { get; } /// Provides a connector object. protected ILocksService LockService { get; } /// Delegate to retrieve connected state. protected Func IsConnectedDelegate { get; } /// Holds whether to poll or not and the periode leght is polling is on. private TINK.Settings.PollingParameters m_oPolling; /// Object to manage update of view model objects from Copri. protected IPollingUpdateTaskManager m_oViewUpdateManager; /// Action to post to GUI thread. public Action PostAction { get; } /// Delegate to open browser. private Action OpenUrlInBrowser { get; } /// Enables derived class to fire property changed event. /// protected override void OnPropertyChanged(PropertyChangedEventArgs p_oEventArgs) => base.OnPropertyChanged(p_oEventArgs); /// /// Handles events from bike viewmodel which require GUI updates. /// /// /// private void OnBikeRequestHandlerPropertyChanged(object sender, PropertyChangedEventArgs e) { OnPropertyChanged(e); } /// /// Instantiates a new item. /// Func m_oItemFactory; /// /// Constructs bike collection view model. /// /// /// Mail address of active user. /// True if report level is verbose, false if not. /// Holds object to query location permisions. /// Holds object to query bluetooth state. /// Specifies on which platform code is run. /// Returns if mobile is connected to web or not. /// Connects system to copri for purposes of requesting a bike/ cancel request. /// Service to control lock retrieve info. /// Holds whether to poll or not and the periode leght is polling is on. /// Executes actions on GUI thread. /// Provides info about the smart device (phone, tablet, ...) /// Interface to actuate methodes on GUI. /// Delegate to open browser. public BikesViewModel( User user, ILocationPermission permissions, IBluetoothLE bluetoothLE, string runtimPlatform, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, TINK.Settings.PollingParameters polling, Action postAction, ISmartDevice smartDevice, IViewService viewService, Action openUrlInBrowser, Func itemFactory) { User = user ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No user available."); RuntimePlatform = runtimPlatform ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No runtime platform information available."); PermissionsService = permissions ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No permissions available."); BluetoothService = bluetoothLE ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No bluetooth available."); ConnectorFactory = connectorFactory ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No connector available."); Geolocation = geolocation ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No geolocation object available."); LockService = lockService ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No lock service object available."); IsConnectedDelegate = isConnectedDelegate ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No is connected delegate available."); m_oItemFactory = itemFactory ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No factory member available."); PostAction = postAction ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No post action available."); SmartDevice = smartDevice ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No smart device object available."); ViewService = viewService ?? throw new ArgumentException("Can not instantiate bikes page view model- object. No view available."); m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); BikeCollection = new BikeCollectionMutable(); BikeCollection.CollectionChanged += OnDecoratedCollectionChanged; m_oPolling = polling; isConnected = IsConnectedDelegate(); OpenUrlInBrowser = openUrlInBrowser; CollectionChanged += (sender, eventargs) => { // Notify about bikes occuring/ vanishing from list. OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBikesListVisible))); }; } /// /// Is invoked if decorated bike collection changes. /// Collection of view model objects has to be synced. /// /// Sender of the event. /// Event arguments. private void OnDecoratedCollectionChanged(object p_oSender, System.Collections.Specialized.NotifyCollectionChangedEventArgs p_oEventArgs) { switch (p_oEventArgs.Action) { case System.Collections.Specialized.NotifyCollectionChangedAction.Add: // New bike avaialbe (new arrived at station or bike wased booked on a different device) foreach (var bike in BikeCollection) { if (Contains(bike.Id)) continue; var bikeViewModel = BikeViewModelFactory.Create( IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, (id) => Remove(id), () => m_oViewUpdateManager, SmartDevice, ViewService, bike, User, m_oItemFactory(), this, OpenUrlInBrowser); bikeViewModel.PropertyChanged += OnBikeRequestHandlerPropertyChanged; Add(bikeViewModel); } break; case System.Collections.Specialized.NotifyCollectionChangedAction.Remove: // Bike was removed (either a different user requested/ booked a bike or request expired or bike was returned.) foreach (BikeViewModelBase l_oBike in Items) { if (!BikeCollection.ContainsKey(l_oBike.Id)) { l_oBike.PropertyChanged -= OnBikeRequestHandlerPropertyChanged; Remove(l_oBike); break; } } break; } } /// All bikes to be displayed. protected BikeCollectionMutable BikeCollection { get; private set; } protected User User { get; private set; } #if USCSHARP9 public bool IsReportLevelVerbose { get; init; } #else public bool IsReportLevelVerbose { get; set; } #endif /// Specified whether code is run under iOS or Android. protected string RuntimePlatform { get; private set; } /// /// Service to manage permissions (location) of the app. /// protected ILocationPermission PermissionsService { get; private set; } /// /// Service to manage bluetooth stack. /// protected IBluetoothLE BluetoothService { get; private set; } /// /// User which is logged in. /// public User ActiveUser { get { return User; } } /// /// Exception which occurred getting bike information. /// protected Exception Exception { get { return m_oException; } set { var l_oException = m_oException; var statusInfoText = StatusInfoText; m_oException = value; if ((m_oException != null && l_oException == null) || (m_oException == null && l_oException != null)) { // Because an error occurred non error related info must be hidden. OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsIdle))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBikesListVisible))); } if (statusInfoText != StatusInfoText) { OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); } } } /// /// Bike selected in list of bikes, null if no bike is selected. /// Binds to GUI. /// public BikeViewModelBase SelectedBike { get; set; } /// /// True if bikes list has to be displayed. /// public bool IsBikesListVisible { get { // If an exception occurred there is no information about occupied bikes available. return Count > 0; } } /// Used to block more than on copri requests at a given time. private bool isIdle = false; /// /// True if any action can be performed (request and cancel request) /// public virtual bool IsIdle { get => isIdle; set { if (value == isIdle) return; Log.ForContext().Debug($"Switch value of {nameof(IsIdle)} to {value}."); isIdle = value; base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsIdle))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRunning))); } } public bool IsRunning => !isIdle; /// Holds info about current action. private string actionText; /// Holds info about current action. public string ActionText { get => actionText; set { var statusInfoText = StatusInfoText; actionText = value; if (statusInfoText == StatusInfoText) { // Nothing to do because value did not change. Log.ForContext().Debug($"Property {nameof(ActionText)} set to value \"{actionText}\" but {nameof(StatusInfoText)} did not change."); return; } Log.ForContext().Debug($"Property {nameof(ActionText)} set to value \"{actionText}\" ."); OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Holds information whether app is connected to web or not. protected bool? isConnected = null; /// Exposes the is connected state. protected bool IsConnected { get => isConnected ?? false; set { var statusInfoText = StatusInfoText; isConnected = value; if (statusInfoText == StatusInfoText) { // Nothing to do. return; } OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// Holds the status information text. public string StatusInfoText { get { if (Exception != null) { // An error occurred getting data from copri. return Exception.GetShortErrorInfoText(IsReportLevelVerbose); } if (!IsConnected) { return AppResources.ActivityTextConnectionStateOffline; } return ActionText ?? string.Empty; } } /// /// Removes a bike view model by id. /// /// Id of bike to removed. public void Remove(string id) { foreach (var bike in BikeCollection) { if (bike.Id == id) { BikeCollection.Remove(bike); return; } } } /// /// Gets whether a bike is contained in collection of bikes. /// /// Id of bike to check existance. /// True if bike exists. private bool Contains(string id) { foreach (var l_oBike in Items) { if (l_oBike.Id == id) { return true; } } return false; } /// /// Transforms bikes view model object to string. /// /// public new string ToString() { var l_oToString = string.Empty; foreach (var item in Items) { l_oToString += item.ToString(); } return l_oToString; } /// /// Invoked when page is shown. /// Starts update process. /// /// Update fuction passed as argument by child class. protected async Task OnAppearing(Action updateAction) { m_oViewUpdateManager = new PollingUpdateTaskManager(updateAction); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartUpdateAyncPeridically(m_oPolling); } catch (Exception l_oExcetion) { Exception = l_oExcetion; } } /// /// Invoked when page is shutdown. /// Currently invoked by code behind, would be nice if called by XAML in future versions. /// public async Task OnDisappearing() { await m_oViewUpdateManager.StopUpdatePeridically(); } } }