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; 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.Forms; using Command = Xamarin.Forms.Command; 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; } /// /// 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. /// /// Mail address of active user. /// 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. /// Holds whether to poll or not and the period length 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 methods on GUI. public BikesAtStationPageViewModel( User user, ILocationPermission permissions, IBluetoothLE bluetoothLE, string runtimPlatform, IStation selectedStation, Func isConnectedDelegate, Func connectorFactory, IGeolocationService geolocation, ILocksService lockService, PollingParameters polling, Action openUrlInExternalBrowser, Action postAction, ISmartDevice smartDevice, IViewService viewService) : base(user, new ViewContext(PageContext.BikesAtStation, selectedStation.Id), 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; RefreshCommand = new Command(async () => { IsRefreshing = false; await OnAppearingOrRefresh(); }); CollectionChanged += (sender, eventargs) => { OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsNoBikesAtStationVisible))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(NoBikesAtStationText))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsLoginRequiredHintVisible))); OnPropertyChanged(new PropertyChangedEventArgs(nameof(IsRefreshing))); }; } /// /// 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 ? AppResources.MarkingBikesAtStationNoBikesAvailable : 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 OnAppearingOrRefresh() { IsIdle = false; IsConnected = IsConnectedDelegate(); 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.StopAsync(); 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.MessageHintTitle, 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 = string.Empty; IsIdle = true; return; } else if (dialogResult) { // Open permissions dialog. PermissionsService.OpenAppSettings(); } } if (GeolocationService.IsGeolcationEnabled == false) { await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.MessageBikesManagementLocationActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikesAtStation, new List { Station }); await OnAppearing(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; } // Check if bluetooth is activated. if (await BluetoothService.GetBluetoothState() != BluetoothState.On) { await ViewService.DisplayAlert( AppResources.MessageHintTitle, AppResources.MessageBikesManagementBluetoothActivation, AppResources.MessageAnswerOk); BikeCollection.Update(bikesAtStation, new List { Station }); 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(bikesAtStation.UpdateLockInfo(locksInfo), new List { Station }); // Backup GUI synchronization context. await OnAppearing(() => UpdateTask()); ActionText = string.Empty; IsIdle = true; } /// Create task which updates my bike view model. public 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 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))); } } } }