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.Station; 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.Forms; namespace TINK.ViewModel.BikesAtStation { /// /// Manages one or more bikes which are located at a single station. /// public class BikesAtStationPageViewModel : BikesViewModel, INotifyCollectionChanged, INotifyPropertyChanged { /// /// Holds the selected station; /// private IStation Station { get; } /// /// Constructs bike collection view model. /// /// Mail address of active user. /// 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. /// Holds whether to poll or not and the periode leght is polling is on. /// Action to open an external browser. /// Executes actions on GUI thread. /// Provides info about the smart device (phone, tablet, ...). /// Interface to actuate methodes on GUI. public BikesAtStationPageViewModel( User user, ILocationPermission permissions, IBluetoothLE bluetoothLE, string runtimPlatform, IStation selectedStation, Func isConnectedDelegate, Func connectorFactory, IGeolocation geolocation, ILocksService lockService, PollingParameters polling, Action openUrlInExternalBrowser, Action postAction, ISmartDevice smartDevice, IViewService viewService) : base(user, permissions, bluetoothLE, runtimPlatform, isConnectedDelegate, connectorFactory, geolocation, lockService, polling, postAction, smartDevice, viewService, openUrlInExternalBrowser, () => new BikeAtStationInUseStateInfoProvider()) { Station = selectedStation ?? new NullStation(); Title = Station.StationName; StationDetailText = Station.Id != null ? string.Format(AppResources.MarkingBikesAtStationStationId, Station.Id) : string.Empty; CollectionChanged += (sender, eventargs) => { OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesAtStationVisible))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesAtStationText))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsLoginRequiredHintVisible))); }; } /// /// Name of the station which is displayed as title of the page. /// public string Title { get; private set; } /// /// Informs about need to log in before requesting an bike. /// public bool IsLoginRequiredHintVisible { get { return Count > 0 && !ActiveUser.IsLoggedIn; } } /// /// Informs about need to log in before requesting an bike. /// public string LoginRequiredHintText => ActiveUser.IsLoggedIn ? string.Empty : AppResources.MarkingLoginRequiredToRerserve; public string ContactSupportHintText => string.Format(IsIdle ? AppResources.MarkingContactSupport : AppResources.MarkingContactSupportBusy, Station.OperatorData?.Name ?? "Operator"); /// /// Returns if info about the fact that user did not request or book any bikes is visible or not. /// public bool IsNoBikesAtStationVisible { get { return Count <= 0 && IsIdle == true; } } /// Info about the fact that user did not request or book any bikes. public string NoBikesAtStationText { get { return IsNoBikesAtStationVisible ? $"Momentan sind keine Fahrräder an dieser Station verfügbar." : string.Empty; } } /// Command object to bind login page redirect link to view model. public System.Windows.Input.ICommand ContactSupportClickedCommand #if USEFLYOUT => new Xamarin.Forms.Command(() => OpenSupportPageAsync()); #else => new Xamarin.Forms.Command(async () => await OpenSupportPageAsync()); #endif /// Command object to bind login page redirect link to view model. public System.Windows.Input.ICommand LoginRequiredHintClickedCommand #if USEFLYOUT => new Xamarin.Forms.Command(() => OpenLoginPageAsync()); #else => new Xamarin.Forms.Command(async () => await OpenLoginPageAsync()); #endif /// Opens login page. #if USEFLYOUT public void OpenLoginPageAsync() #else public async Task OpenLoginPageAsync() #endif { try { // Switch to map page #if USEFLYOUT ViewService.ShowPage(ViewTypes.LoginPage); #else await ViewService.ShowPage("//LoginPage"); #endif } catch (Exception p_oException) { Log.Error("Ein unerwarteter Fehler ist in der Klasse BikesAtStationPageViewModel aufgetreten. Kontext: Klick auf Hinweistext auf Station N- seite ohne Anmeldung. {@Exception}", p_oException); return; } } /// Opens support. #if USEFLYOUT public void OpenSupportPageAsync() #else public async Task OpenSupportPageAsync() #endif { try { if (!IsIdle) { // Prevent navigation when app is not idle because this might lead to aborting return bike workflow. return; } // Switch to map page #if USEFLYOUT ViewService.ShowPage(ViewTypes.ContactPage, AppResources.MarkingFeedbackAndContact); #else await ViewService.ShowPage("//ContactPage"); #endif } catch (Exception p_oException) { Log.Error("Ein unerwarteter Fehler ist auf der Seite Kontakt aufgetreten. Kontext: Klick auf Hinweistext auf Station N- seite ohne Anmeldung. {@Exception}", p_oException); return; } } /// Returns detailed info about the station (station id). public string StationDetailText { get; private set; } /// /// Invoked when page is shown. /// Starts update process. /// public async Task OnAppearing() { Log.ForContext().Information($"Bikes at station {Station.StationName} is appearing, either due to tap on a station or to app being shown again."); ActionText = AppResources.ActivityTextOneMomentPlease; // Stop polling before getting bikes info. await m_oViewUpdateManager.StopUpdatePeridically(); ActionText = AppResources.ActivityTextBikesAtStationGetBikes; var bikesAll = await ConnectorFactory(IsConnected).Query.GetBikesAsync(); Exception = bikesAll.Exception; // Update communication error from query for bikes at station. var bikesAtStation = bikesAll.Response.GetAtStation(Station.Id); var lockIdList = bikesAtStation .GetLockIt() .Cast() .Select(x => x.LockInfo) .ToList(); if (LockService is ILocksServiceFake serviceFake) { serviceFake.UpdateSimulation(bikesAtStation); } ActionText = AppResources.ActivityTextCheckBluetoothState; // Check location permissions. if (bikesAtStation.GetLockIt().Count > 0 && RuntimePlatform == Device.Android ) { var status = await PermissionsService.CheckStatusAsync(); if (status != 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(bikesAtStation, new List { Station }); await OnAppearing(() => UpdateTask()); ActionText = ""; IsIdle = true; return; } else if (dialogResult) { // Open permissions dialog. PermissionsService.OpenAppSettings(); } } if (Geolocation.IsGeolcationEnabled == false) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementLocationActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikesAtStation, new List { Station }); await OnAppearing(() => UpdateTask()); ActionText = ""; IsIdle = true; } // Check if bluetooth is activated. if (await BluetoothService.GetBluetoothState() != BluetoothState.On) { await ViewService.DisplayAlert( AppResources.MessageTitleHint, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikesAtStation, new List { Station }); await OnAppearing(() => 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(bikesAtStation.UpdateLockInfo(locksInfo), new List { Station }); // Backup GUI synchronization context. await OnAppearing(() => UpdateTask()); ActionText = ""; IsIdle = true; } /// Create task which updates my bike view model. private void UpdateTask() { PostAction( unused => { Log.ForContext().Debug("Updating action text..."); ActionText = AppResources.ActivityTextUpdating; IsConnected = IsConnectedDelegate(); }, null); var result = ConnectorFactory(IsConnected).Query.GetBikesAsync().Result; BikeCollection bikes = result.Response.GetAtStation(Station.Id); var exception = result.Exception; if (exception != null) { Log.ForContext().Error("Getting all bikes bikes in polling context failed with exception {Exception}.", exception); } PostAction( unused => { Log.ForContext().Debug("Updating bikes at station..."); BikeCollection.Update(bikes, new List { Station }); Exception = result.Exception; ActionText = string.Empty; }, null); } /// /// 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(IsNoBikesAtStationVisible))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesAtStationText))); base.OnPropertyChanged(new PropertyChangedEventArgs(nameof(ContactSupportHintText))); } } } }