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.Model.Services.Geolocation; using TINK.Model.User; using TINK.View; using TINK.ViewModel.Bikes.Bike; using TINK.ViewModel.Bikes.Bike.BC; using Plugin.Permissions.Abstractions; using Plugin.BLE.Abstractions.Contracts; using TINK.MultilingualResources; using TINK.Model.Device; namespace TINK.ViewModel.Bikes { public abstract class BikesViewModel : ObservableCollection<BikeViewModelBase>, IBikesViewModel { /// <summary> Provides info about the smart device (phone, tablet, ...).</summary> protected ISmartDevice SmartDevice; /// <summary> /// Reference on view servcie to show modal notifications and to perform navigation. /// </summary> protected IViewService ViewService { get; } /// <summary> /// Holds the exception which occurred getting bikes occupied information. /// </summary> private Exception m_oException; /// <summary> Provides an connector object.</summary> protected Func<bool, IConnector> ConnectorFactory { get; } protected IGeolocation Geolocation { get; } /// <summary> Provides an connector object.</summary> protected ILocksService LockService { get; } /// <summary> Delegate to retrieve connected state. </summary> protected Func<bool> IsConnectedDelegate { get; } /// <summary>Holds whether to poll or not and the periode leght is polling is on.</summary> private TINK.Settings.PollingParameters m_oPolling; /// <summary> Object to manage update of view model objects from Copri.</summary> protected IPollingUpdateTaskManager m_oViewUpdateManager; /// <summary> Action to post to GUI thread.</summary> public Action<SendOrPostCallback, object> PostAction { get; } /// <summary>Enables derived class to fire property changed event. </summary> /// <param name="p_oEventArgs"></param> protected override void OnPropertyChanged(PropertyChangedEventArgs p_oEventArgs) => base.OnPropertyChanged(p_oEventArgs); /// <summary> /// Handles events from bike viewmodel which require GUI updates. /// </summary> /// <param name="sender"></param> /// <param name="e"></param> private void OnBikeRequestHandlerPropertyChanged(object sender, PropertyChangedEventArgs e) { OnPropertyChanged(e); } /// <summary> /// Instantiates a new item. /// </summary> Func<IInUseStateInfoProvider> m_oItemFactory; /// <summary> /// Constructs bike collection view model. /// </summary> /// </param> /// <param name="p_oUser">Mail address of active user.</param> /// <param name="isReportLevelVerbose">True if report level is verbose, false if not.</param> /// <param name="permissions">Holds object to query location permisions.</param> /// <param name="bluetoothLE">Holds object to query bluetooth state.</param> /// <param name="runtimPlatform">Specifies on which platform code is run.</param> /// <param name="isConnectedDelegate">Returns if mobile is connected to web or not.</param> /// <param name="connectorFactory">Connects system to copri for purposes of requesting a bike/ cancel request.</param> /// <param name="lockService">Service to control lock retrieve info.</param> /// <param name="p_oPolling"> Holds whether to poll or not and the periode leght is polling is on. </param> /// <param name="postAction">Executes actions on GUI thread.</param> /// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...)</param> /// <param name="p_oViewService">Interface to actuate methodes on GUI.</param> public BikesViewModel( User user, IPermissions permissions, IBluetoothLE bluetoothLE, string runtimPlatform, Func<bool> isConnectedDelegate, Func<bool, IConnector> connectorFactory, IGeolocation geolocation, ILocksService lockService, TINK.Settings.PollingParameters polling, Action<SendOrPostCallback, object> postAction, ISmartDevice smartDevice, IViewService viewService, Func<IInUseStateInfoProvider> 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(); CollectionChanged += (sender, eventargs) => { // Notify about bikes occuring/ vanishing from list. OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsBikesListVisible))); }; } /// <summary> /// Is invoked if decorated bike collection changes. /// Collection of view model objects has to be synced. /// </summary> /// <param name="p_oSender">Sender of the event.</param> /// <param name="p_oEventArgs">Event arguments.</param> 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 l_oBike in BikeCollection) { if (!Contains(l_oBike.Id)) { var bikeViewModel = BikeViewModelFactory.Create( IsConnectedDelegate, ConnectorFactory, Geolocation, LockService, (id) => Remove(id), () => m_oViewUpdateManager, SmartDevice, ViewService, l_oBike, User, m_oItemFactory(), this); 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; } } /// <summary> All bikes to be displayed. </summary> 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 /// <summary> Specified whether code is run under iOS or Android.</summary> protected string RuntimePlatform { get; private set; } /// <summary> /// Service to manage permissions (location) of the app. /// </summary> protected IPermissions PermissionsService { get; private set; } /// <summary> /// Service to manage bluetooth stack. /// </summary> protected IBluetoothLE BluetoothService { get; private set; } /// <summary> /// User which is logged in. /// </summary> public User ActiveUser { get { return User; } } /// <summary> /// Exception which occurred getting bike information. /// </summary> 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))); } } } /// <summary> /// Bike selected in list of bikes, null if no bike is selected. /// Binds to GUI. /// </summary> public BikeViewModelBase SelectedBike { get; set; } /// <summary> /// True if bikes list has to be displayed. /// </summary> public bool IsBikesListVisible { get { // If an exception occurred there is no information about occupied bikes available. return Count > 0; } } /// <summary> Used to block more than on copri requests at a given time.</summary> private bool isIdle = false; /// <summary> /// True if any action can be performed (request and cancel request) /// </summary> public virtual bool IsIdle { get => isIdle; set { if (value == isIdle) return; Log.ForContext<BikesViewModel>().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; /// <summary> Holds info about current action. </summary> private string actionText; /// <summary> Holds info about current action. </summary> public string ActionText { get => actionText; set { var statusInfoText = StatusInfoText; actionText = value; if (statusInfoText == StatusInfoText) { // Nothing to do because value did not change. Log.ForContext<BikeViewModel>().Debug($"Property {nameof(ActionText)} set to value \"{actionText}\" but {nameof(StatusInfoText)} did not change."); return; } Log.ForContext<BikeViewModel>().Debug($"Property {nameof(ActionText)} set to value \"{actionText}\" ."); OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// <summary> Holds information whether app is connected to web or not. </summary> protected bool? isConnected = null; /// <summary>Exposes the is connected state. </summary> protected bool IsConnected { get => isConnected ?? false; set { var statusInfoText = StatusInfoText; isConnected = value; if (statusInfoText == StatusInfoText) { // Nothing to do. return; } OnPropertyChanged(new PropertyChangedEventArgs(nameof(StatusInfoText))); } } /// <summary> Holds the status information text. </summary> public string StatusInfoText { get { if (Exception != null) { // An error occurred getting data from copri. return IsReportLevelVerbose ? Exception.GetShortErrorInfoText() : AppResources.ActivityTextException; } if (!IsConnected) { return AppResources.ActivityTextConnectionStateOffline; } return ActionText ?? string.Empty; } } /// <summary> /// Removes a bike view model by id. /// </summary> /// <param name="id">Id of bike to removed.</param> public void Remove(string id) { foreach (var bike in BikeCollection) { if (bike.Id == id) { BikeCollection.Remove(bike); return; } } } /// <summary> /// Gets whether a bike is contained in collection of bikes. /// </summary> /// <param name="id">Id of bike to check existance.</param> /// <returns>True if bike exists.</returns> private bool Contains(string id) { foreach (var l_oBike in Items) { if (l_oBike.Id == id) { return true; } } return false; } /// <summary> /// Transforms bikes view model object to string. /// </summary> /// <returns></returns> public new string ToString() { var l_oToString = string.Empty; foreach (var item in Items) { l_oToString += item.ToString(); } return l_oToString; } /// <summary> /// Invoked when page is shown. /// Starts update process. /// </summary> /// <param name="p_oUpdateAction"> Update fuction passed as argument by child class.</param> protected async Task OnAppearing(Action p_oUpdateAction) { m_oViewUpdateManager = new PollingUpdateTaskManager(() => GetType().Name, p_oUpdateAction); try { // Update bikes at station or my bikes depending on context. await m_oViewUpdateManager.StartUpdateAyncPeridically(m_oPolling); } catch (Exception l_oExcetion) { Exception = l_oExcetion; } } /// <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() { await m_oViewUpdateManager.StopUpdatePeridically(); } } }