using System; using System.Collections.ObjectModel; using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using Plugin.BLE.Abstractions.Contracts; using Serilog; using TINK.Model.Bikes; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Model.User; using TINK.Services.BluetoothLock; using TINK.Services.Geolocation; using TINK.Services.Permissions; using TINK.View; using TINK.ViewModel.Bikes.Bike; 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 IGeolocationService GeolocationService { 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 period length 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 view model 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. /// Holds the view context in which bikes view model is used. /// True if report level is verbose, false if not. /// Holds object to query location permissions. /// 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 period length is polling is on. /// Executes actions on GUI thread. /// Provides info about the smart device (phone, tablet, ...) /// Interface to actuate methods on GUI. /// Delegate to open browser. public BikesViewModel( User user, ViewContext viewContext, ILocationPermission permissions, IBluetoothLE bluetoothLE, string runtimPlatform, Func isConnectedDelegate, Func connectorFactory, IGeolocationService 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."); GeolocationService = 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."); ViewContext = viewContext; m_oViewUpdateManager = new IdlePollingUpdateTaskManager(); BikeCollection = new BikeCollectionMutable( geolocation, lockService, isConnectedDelegate, connectorFactory, () => m_oViewUpdateManager); BikeCollection.CollectionChanged += OnDecoratedCollectionChanged; m_oPolling = polling; OpenUrlInBrowser = openUrlInBrowser; CollectionChanged += (sender, eventargs) => { // Notify about bikes occurring/ 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 available (new arrived at station or bike was booked on a different device) foreach (var bike in BikeCollection) { if (Contains(bike.Id)) continue; var bikeViewModel = BikeViewModelFactory.Create( IsConnectedDelegate, ConnectorFactory, GeolocationService, LockService, (id) => Remove(id), () => m_oViewUpdateManager, SmartDevice, ViewService, bike, User, ViewContext, 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; case System.Collections.Specialized.NotifyCollectionChangedAction.Reset: // Empty collection. // Occurs in context of find bike page when searching for second, third, ... bike (reopen of page via flyout). ClearItems(); break; } } /// All bikes to be displayed. protected BikeCollectionMutable BikeCollection { get; private set; } protected User User { get; private set; } /// Holds the view context in which bikes view model is used. private ViewContext ViewContext { get; } #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(IsProcessWithRunningProcessView))); } } /// Used to display active rental process. private IRentalProcessViewModel _rentalProcess = new RentalProcessViewModel(); /// Holds the active rental process. public IRentalProcessViewModel RentalProcess => _rentalProcess; /// /// Starts the rental process. /// /// Rental process values to start with. public void StartRentalProcess(IRentalProcessViewModel processViewModel) { if (processViewModel == _rentalProcess) return; _rentalProcess.LoadFrom(processViewModel); BikeInRentalProcess = this.FirstOrDefault(bike => bike.Id == _rentalProcess.BikeId) as Bike.BluetoothLock.BikeViewModel; base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(RentalProcess))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(BikeInRentalProcess))); } public Bike.BluetoothLock.BikeViewModel BikeInRentalProcess { get; private set; } /// Used to display current step in rental process. private int? currentStep = null; /// Holds the number of current step in rental process. public int? CurrentStep { get => currentStep; set { if (value == CurrentStep) return; currentStep = value; base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(CurrentStep))); } } /// Used to display status of current step in rental process. private CurrentStepStatus currentStepStatus = CurrentStepStatus.None; /// Holds the status of current step in rental process.e public virtual CurrentStepStatus CurrentStepStatus { get => currentStepStatus; set { if (value == CurrentStepStatus) return; currentStepStatus = value; base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(CurrentStepStatus))); } } public bool IsProcessWithRunningProcessView => !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); //} 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 existence. /// 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 override string ToString() { var l_oToString = string.Empty; foreach (var item in Items) { l_oToString += item.ToString(); } return l_oToString; } /// /// Invoked when page is shown and starts update process. /// /// Update function passed as argument by child class. protected async Task OnAppearing(Action updateAction) => await StartUpdateTask(updateAction); /// /// Starts update process. /// /// Update function passed as argument by child class. public async Task StartUpdateTask(Action updateAction) { m_oViewUpdateManager = new PollingUpdateTaskManager(updateAction); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartAsync(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 virtual async Task OnDisappearing() => await m_oViewUpdateManager.StopAsync(); } }