using System; using System.Collections.Generic; using System.Collections.Specialized; using System.ComponentModel; using System.Linq; using System.Threading; using System.Threading.Tasks; using Plugin.BLE.Abstractions.Contracts; using Serilog; using TINK.Model; using TINK.Model.Bikes; using TINK.Model.Bikes.BikeInfoNS.BluetoothLock; using TINK.Model.Connector; using TINK.Model.Device; using TINK.Model.Stations.StationNS; using TINK.Model.User; using TINK.MultilingualResources; using TINK.Services.BluetoothLock; using TINK.Services.BluetoothLock.Tdo; using TINK.Services.Geolocation; using TINK.Services.Permissions; using TINK.Settings; using TINK.View; using TINK.ViewModel.Bikes; using Xamarin.Essentials; using Xamarin.Forms; using Command = Xamarin.Forms.Command; namespace TINK.ViewModel.MyBikes { public class MyBikesPageViewModel : BikesViewModel, INotifyCollectionChanged, INotifyPropertyChanged { /// Holds the stations to get station names form station ids. private IEnumerable Stations { get; } /// /// True if ListView of Bikes is refreshing after user pulled; /// private bool _isRefreshing = false; public bool IsRefreshing { get { return _isRefreshing; } set { _isRefreshing = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRefreshing))); } } /// /// Holds what should be executed on pull to refresh /// public Command RefreshCommand { get; } /// /// Constructs bike collection view model in case information about occupied bikes is available. /// /// Mail address of active user. /// 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. /// Service to control lock retrieve info. /// Stations to get station name from station id. /// 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 MyBikesPageViewModel( User user, ILocationPermission permissions, IBluetoothLE bluetoothLE, string runtimPlatform, Func isConnectedDelegate, Func connectorFactory, IGeolocationService geolocation, ILocksService lockService, IEnumerable stations, PollingParameters polling, Action postAction, ISmartDevice smartDevice, IViewService viewService, Action openUrlInBrowser) : base(user, new ViewContext(PageContext.MyBikes), permissions, bluetoothLE, runtimPlatform, isConnectedDelegate, connectorFactory, geolocation, lockService, polling, postAction, smartDevice, viewService, openUrlInBrowser, () => new MyBikeInUseStateInfoProvider()) { CollectionChanged += (sender, eventargs) => { OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesOccupiedVisible))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesOccupiedText))); }; Stations = stations ?? throw new ArgumentException(nameof(stations)); RefreshCommand = new Command(async () => { IsRefreshing = false; await OnAppearingOrRefresh(); }); } /// Returns if info about the fact that user did not request or book any bikes is visible or not. /// Gets message that logged in user has not booked any bikes. /// public bool IsNoBikesOccupiedVisible { get { return Count <= 0 && IsIdle == true; } } /// Info about the fact that user did not request or book any bikes. public string NoBikesOccupiedText { get { return IsNoBikesOccupiedVisible ? string.Format(AppResources.MarkingMyBikesNoBikesReservedRented, ActiveUser?.Mail) : string.Empty; } } /// /// Invoked when page is shown. /// Starts update process. /// public async Task OnAppearingOrRefresh() { IsIdle = false; IsConnected = IsConnectedDelegate(); // Get my bikes from COPRI Log.ForContext().Information("User request to show page MyBikes/ page re-appearing"); ActionText = AppResources.ActivityTextMyBikesLoadingBikes; // Stop polling before getting bikes info. await m_oViewUpdateManager.StopUpdatePeridically(); var bikesOccupied = await ConnectorFactory(IsConnected).Query.GetBikesOccupiedAsync(); Exception = bikesOccupied.Exception; // Update communication error from query for bikes occupied. var lockIdList = bikesOccupied.Response .GetLockIt() .Cast() .Select(x => x.LockInfo) .ToList(); if (LockService is ILocksServiceFake serviceFake) { serviceFake.UpdateSimulation(bikesOccupied.Response); } // Check bluetooth and location permission and states ActionText = AppResources.ActivityTextCheckBluetoothState; if (bikesOccupied.Response.FirstOrDefault(x => x is BikeInfo btBike) != null && RuntimePlatform == Device.Android) { // Check location permission var status = await PermissionsService.CheckStatusAsync(); if (status != Status.Granted) { var permissionResult = await PermissionsService.RequestAsync(); if (permissionResult != Status.Granted) { var dialogResult = await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementLocationPermissionOpenDialog, AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (!dialogResult) { // User decided not to give access to locations permissions. BikeCollection.Update(bikesOccupied.Response, Stations); await OnAppearing(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } // Open permissions dialog. PermissionsService.OpenAppSettings(); } } // Location state if (GeolocationService.IsGeolcationEnabled == false) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementLocationActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikesOccupied.Response, Stations); await OnAppearing(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } // Bluetooth state if (await BluetoothService.GetBluetoothState() != BluetoothState.On) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikesOccupied.Response, Stations); await OnAppearing(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } } // Connect to bluetooth devices. ActionText = AppResources.ActivityTextSearchBikes; IEnumerable locksInfoTdo; try { locksInfoTdo = await LockService.GetLocksStateAsync( lockIdList.Select(x => x.ToLockInfoTdo()).ToList(), LockService.TimeOut.MultiConnect); } catch (Exception exception) { Log.ForContext().Error("Getting bluetooth state failed. {Exception}", exception); locksInfoTdo = new List(); } var locksInfo = lockIdList.UpdateById(locksInfoTdo); BikeCollection.Update(bikesOccupied.Response.UpdateLockInfo(locksInfo), Stations); await OnAppearing(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; } /// /// True if any action can be performed (request and cancel request) /// public override bool IsIdle { get => base.IsIdle; set { if (value == base.IsIdle) return; Log.ForContext().Debug($"Switch value of {nameof(IsIdle)} to {value}."); base.IsIdle = value; base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesOccupiedVisible))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesOccupiedText))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(FlyoutBehavior))); // Hide flyout menu if app is busy. Prevents especially navigation on returning bike. } } /// /// Determines if flyout menu is available or not. /// public FlyoutBehavior FlyoutBehavior => base.IsIdle ? FlyoutBehavior.Flyout : FlyoutBehavior.Disabled; /// Create task which updates my bike view model. private void UpdateTask() { // Start task which periodically updates pins. PostAction( unused => { ActionText = AppResources.ActivityTextUpdating; IsConnected = IsConnectedDelegate(); }, null); var result = ConnectorFactory(IsConnected).Query.GetBikesOccupiedAsync().Result; var bikes = result.Response; var exception = result.Exception; if (exception != null) { Log.ForContext().Error("Getting bikes occupied in polling context failed with exception {Exception}.", exception); } PostAction( unused => { BikeCollection.Update(bikes, Stations); // Updating collection leads to update of GUI. Exception = result.Exception; ActionText = string.Empty; }, null); } } }