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.Services.CopriApi; using TINK.Model.Station; 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 Xamarin.Forms; 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; } /// /// 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 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. /// Service to control lock retrieve info. /// Stations to get station name from station id. /// 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 FindBikePageViewModel( User p_oUser, ILocationPermission permissions, IBluetoothLE bluetoothLE, string runtimPlatform, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, IEnumerable stations, PollingParameters polling, Action postAction, ISmartDevice smartDevice, IViewService viewService, Action openUrlInBrowser) : base(p_oUser, 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)); } /// /// Invoked when page is shown. /// Starts update process. /// public async Task OnAppearing() { IsIdle = false; Log.ForContext().Information("User request to show page FindBike- page re-appearing"); 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 = ""; IsIdle = true; return; } ActionText = ""; IsIdle = true; } /// 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; 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 failed (Copri server not reachable)."); await ViewService.DisplayAdvancedAlert( AppResources.ErrorReturnBikeNoWebTitle, string.Format("{0}\r\n{1}", AppResources.ErrorReturnBikeNoWebMessage, WebConnectFailureException.GetHintToPossibleExceptionsReasons), exception.Message, AppResources.MessageAnswerOk); } else { Log.ForContext().Error("Getting bikes failed. {Exception}", exception); await ViewService.DisplayAlert( AppResources.MessageTitleHint, exception.Message, AppResources.MessageAnswerOk); } ActionText = ""; 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.MessageTitleHint, string.Format(AppResources.MessageErrorSelectBikeNoBikeFound, BikeIdUserInput), AppResources.MessageAnswerOk); ActionText = ""; 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.MessageTitleHint, 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 = ""; IsIdle = true; return; } // Open permissions dialog. PermissionsService.OpenAppSettings(); } } else { 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(bikeCollection, Stations); await StartUpdateTask(() => UpdateTask()); ActionText = ""; IsIdle = true; return; } // Open permissions dialog. PermissionsService.OpenAppSettings(); } } // Location state if (Geolocation.IsGeolcationEnabled == false) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementLocationActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikeCollection, Stations); await StartUpdateTask(() => UpdateTask()); ActionText = ""; IsIdle = true; return; } // Bluetooth state if (await BluetoothService.GetBluetoothState() != BluetoothState.On) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikeCollection, Stations); await StartUpdateTask(() => UpdateTask()); ActionText = ""; 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 = ""; IsIdle = true; } catch (Exception exception) { await ViewService.DisplayAlert( AppResources.MessageErrorSelectBikeTitle, exception.Message, AppResources.MessageAnswerOk); Log.ForContext().Error("Running command to select bike failed. {Exception}", exception); ActionText = ""; 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 occupied 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); } } }