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.BikeNS; using TINK.Model.Bikes.BikeInfoNS.BluetoothLock; using TINK.Model.Connector; using TINK.Model.Connector.Filter; using TINK.Model.Device; using TINK.Model.Services.CopriApi; using TINK.Model.State; using TINK.Model.Stations.StationNS; using TINK.Model.User; using TINK.MultilingualResources; using TINK.Repository.Exception; 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 TINK.ViewModel.Map; using Xamarin.Essentials; using Xamarin.Forms; using Command = Xamarin.Forms.Command; namespace TINK.ViewModel.FindBike { public class FindBikePageViewModel : BikesViewModel, INotifyCollectionChanged, INotifyPropertyChanged { private string bikeIdUserInput = string.Empty; /// Text entered by user to specify a bike. public string BikeIdUserInput { get => bikeIdUserInput; set { if (value == bikeIdUserInput) { return; } bikeIdUserInput = value; base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(BikeIdUserInput))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsSelectBikeEnabled))); } } /// /// True if any action can be performed (request and cancel request) /// public override bool IsIdle { get => base.IsIdle; set { if (value == IsIdle) return; Log.ForContext().Debug($"Switch value of {nameof(IsIdle)} to {value}."); base.IsIdle = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsSelectBikeEnabled))); // Enable select bike button. } } /// Holds all bikes available. public BikeCollection Bikes { get; set; } /// Do not allow to select bike if id is not set. public bool IsSelectBikeEnabled => IsIdle && BikeIdUserInput != null && BikeIdUserInput.Length > 0; /// Hide id input fields as soon as bike is found. public bool IsSelectBikeVisible => BikeCollection != null && BikeCollection.Count == 0; /// 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; } public Command ShowFilterBikeTypeInfoCommand { get; private set; } /// /// 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 FindBikePageViewModel( 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.FindBike), permissions, bluetoothLE, runtimPlatform, isConnectedDelegate, connectorFactory, geolocation, lockService, polling, postAction, smartDevice, viewService, openUrlInBrowser, () => new MyBikeInUseStateInfoProvider()) { CollectionChanged += (sender, eventargs) => { OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsSelectBikeVisible))); }; Stations = stations ?? throw new ArgumentException(nameof(stations)); RefreshCommand = new Command(async () => { IsRefreshing = false; await SelectBike(); }); ShowFilterBikeTypeInfoCommand = new Xamarin.Forms.Command(async () => { await ViewService.DisplayAlert( AppResources.MessageBikeTypeInfoTitle, AppResources.MessageBikeTypeInfoText, AppResources.MessageAnswerOk); }); } /// /// Invoked when page is shown. /// Starts update process. /// public async Task OnAppearingOrRefresh() { IsIdle = false; Log.ForContext().Information("User request to show page FindBike- page re-appearing"); IsConnected = IsConnectedDelegate(); // Stop polling before getting bikes info. await m_oViewUpdateManager.StopAsync(); if (string.IsNullOrEmpty(BikeIdUserInput) /* Find bike page flyout was taped */ && BikeCollection.Count > 0 /* Bike was successfully selected */) { // Find bike page flyout was taped and page was already opened before and bike has been selected. // Clear bike collection to allow user to enter bike id and search for bike. BikeCollection.Clear(); } if (BikeCollection.Count > 0) { // Page is appearing not because page flyout was taped. // Bike has already been selected. // Restart update. await StartUpdateTask(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } ActionText = string.Empty; IsIdle = true; var result = await ConnectorFactory(IsConnected).Query.GetBikesAsync(); var bikes = result.Response; var exception = result.Exception; if (exception != null) { Log.ForContext().Error("Getting bikes in polling context failed with exception {Exception}.", exception); } // Get Active Filtered BikeType GetActiveFilteredBikeType(bikes); } /// Command object to bind select bike button to view model. public System.Windows.Input.ICommand OnSelectBikeRequest => new Xamarin.Forms.Command(async () => await SelectBike(), () => IsSelectBikeEnabled); /// Select a bike by ID public async Task SelectBike() { // Get List of bike to be able to connect to. ActionText = AppResources.ActivityTextFindBikeLoadingBikes; IsIdle = false; IsConnected = IsConnectedDelegate(); Result bikes = null; try { bikes = await ConnectorFactory(IsConnected).Query.GetBikesAsync(); } catch (Exception exception) { if (exception is WebConnectFailureException) { // Copri server is not reachable. Log.ForContext().Information("Getting bikes failed (Copri server not reachable)."); await ViewService.DisplayAlert( AppResources.ErrorSelectBikeTitle, AppResources.ErrorNoWeb, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("Getting bikes failed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.ErrorSelectBikeTitle, exception.Message, AppResources.MessageAnswerOk); } ActionText = string.Empty; IsIdle = true; return; } finally { Exception = bikes?.Exception ?? null; // Update communication error from query for bikes occupied. Bikes = bikes.Response; } try { var selectedBike = Bikes.FirstOrDefault(x => x.Id.Equals(BikeIdUserInput.Trim(), StringComparison.OrdinalIgnoreCase)); if (selectedBike == null) { await ViewService.DisplayAlert( AppResources.MessageHintTitle, string.Format(AppResources.ErrorSelectBikeNoBikeFound, BikeIdUserInput), AppResources.MessageAnswerOk); ActionText = string.Empty; IsIdle = true; return; } var bikeCollection = new BikeCollection(new Dictionary { { selectedBike.Id, selectedBike } }); var lockIdList = bikeCollection .GetLockIt() .Cast() .Select(x => x.LockInfo) .ToList(); if (LockService is ILocksServiceFake serviceFake) { serviceFake.UpdateSimulation(bikeCollection); } // Check bluetooth and location permission and states ActionText = AppResources.ActivityTextCheckBluetoothState; if (bikeCollection.FirstOrDefault(x => x is BikeInfo btBike) != null //&& RuntimePlatform == Device.Android ) { // Check location permission var status = await PermissionsService.CheckStatusAsync(); if (status != Status.Granted) { if (RuntimePlatform == Device.Android) { var permissionResult = await PermissionsService.RequestAsync(); if (permissionResult != Status.Granted) { var dialogResult = await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.MessageBikesManagementLocationPermissionOpenDialog, AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (!dialogResult) { // User decided not to give access to locations permissions. BikeCollection.Update(bikeCollection, Stations); await StartUpdateTask(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } // Open permissions dialog. PermissionsService.OpenAppSettings(); } } else { var dialogResult = await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.MessageBikesManagementLocationPermissionOpenDialog, AppResources.MessageAnswerYes, AppResources.MessageAnswerNo); if (!dialogResult) { // User decided not to give access to locations permissions. BikeCollection.Update(bikeCollection, Stations); await StartUpdateTask(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } // Open permissions dialog. PermissionsService.OpenAppSettings(); } } // Location state if (GeolocationService.IsGeolcationEnabled == false) { await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.MessageBikesManagementLocationActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikeCollection, Stations); await StartUpdateTask(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; return; } // Bluetooth state if (await BluetoothService.GetBluetoothState() != BluetoothState.On) { await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikeCollection, Stations); await StartUpdateTask(() => 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(bikeCollection.UpdateLockInfo(locksInfo), Stations); await StartUpdateTask(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; } catch (Exception exception) { await ViewService.DisplayAlert( AppResources.ErrorSelectBikeTitle, exception.Message, AppResources.MessageAnswerOk); Log.ForContext().Error("Running command to select bike failed. {Exception}", exception); ActionText = string.Empty; IsIdle = true; return; } } /// 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.GetBikesAsync().Result; var bikes = result.Response; var exception = result.Exception; if (exception != null) { Log.ForContext().Error("Getting bikes in polling context failed with exception {Exception}.", exception); } var selectedBike = bikes.FirstOrDefault(x => x.Id.Equals(BikeIdUserInput.Trim(), StringComparison.OrdinalIgnoreCase)); bikes = selectedBike != null ? new BikeCollection(new Dictionary { { selectedBike.Id, selectedBike } }) : new BikeCollection(); PostAction( unused => { BikeCollection.Update(bikes, Stations); // Updating collection leads to update of GUI. Exception = result.Exception; ActionText = string.Empty; }, null); } private string activeFilteredBikeType = string.Empty; /// /// Selected Bike Type in MapFilter /// public string ActiveFilteredBikeType { get { return activeFilteredBikeType; } set { if (value == activeFilteredBikeType) { return; } activeFilteredBikeType = value; OnPropertyChanged(new PropertyChangedEventArgs(nameof(ActiveFilteredBikeType))); } } /// /// Get Selected Bike Type in MapFilter /// public void GetActiveFilteredBikeType(BikeCollection bikesAll) { Log.ForContext().Debug($"Bike type of active filter is extracted."); if (bikesAll != null) { var firstOrDefaultBikeType = bikesAll.FirstOrDefault().TypeOfBike; if(firstOrDefaultBikeType == TypeOfBike.Cargo) { ActiveFilteredBikeType = AppResources.MarkingCargoBike; } else if(firstOrDefaultBikeType == TypeOfBike.City) { ActiveFilteredBikeType = AppResources.MarkingCityBike; } } } } }