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.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.Forms;
using Command = Xamarin.Forms.Command;
namespace TINK.ViewModel.SelectBike
{
public class SelectBikePageViewModel : 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 > 1 && BikeIdUserInput.Any(x => char.IsLetter(x)) && BikeIdUserInput.Any(x => char.IsDigit(x));
/// 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; }
/// Reference on the tink app instance.
private ITinkApp TinkApp { 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.
/// Reference to tink app model.
/// 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 SelectBikePageViewModel(
User user,
ITinkApp tinkApp,
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.SelectBike), 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));
TinkApp = tinkApp
?? throw new ArgumentException("Can not instantiate settings page view model- object. No tink app object available.");
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 SelectBike- 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;
}
// Get Active Filtered BikeType
ActiveFilteredBikeType = GetActiveFilteredBikeType(GroupFilterMapPage);
ActionText = string.Empty;
IsIdle = true;
}
/// Command object to bind select bike button to view model.
public System.Windows.Input.ICommand OnSelectBikeRequest => new Command(async () => await SelectBike());
/// Select a bike by ID
public async Task SelectBike()
{
if (!IsSelectBikeEnabled)
{
await ViewService.DisplayAlert(
String.Empty,
AppResources.ErrorSelectBikeInputNotSufficent,
AppResources.MessageAnswerOk);
return;
}
else
{
// Get List of bike to be able to connect to.
ActionText = AppResources.ActivityTextSelectBikeLoadingBikes;
IsIdle = false;
IsConnected = IsConnectedDelegate();
Result bikes = null;
try
{
bikes = await ConnectorFactory(IsConnected).Query.GetBikesAsync(bikeId: BikeIdUserInput.Trim());
}
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.Response.FirstOrDefault();
if (selectedBike == null)
{
await ViewService.DisplayAlert(
AppResources.ErrorSelectBikeTitle,
TinkApp.Flavor == AppFlavor.MeinKonrad
? $"{string.Format(AppResources.ErrorSelectBikeNoBikeFound, BikeIdUserInput)}\r\n\r\n{string.Format(AppResources.ErrorSelectBikeNoBikeFoundBikeTypeHint, ActiveFilteredBikeType)}"
: 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);
}
///
/// Selected Bike Type in MapFilter shown as label in View. Default empty.
///
private string activeFilteredBikeType = string.Empty;
///
/// Selected Bike Type in MapFilter shown as label in View. If empty, label is not shown = no wrong info.
///
public string ActiveFilteredBikeType
{
get { return activeFilteredBikeType; }
set
{
if (value == activeFilteredBikeType)
{
return;
}
activeFilteredBikeType = value;
OnPropertyChanged(new PropertyChangedEventArgs(nameof(ActiveFilteredBikeType)));
}
}
/// Gets the group filter from map page.
private IGroupFilterMapPage GroupFilterMapPage => TinkApp.GroupFilterMapPage;
///
/// Get Selected Bike Type in MapFilter
///
public static string GetActiveFilteredBikeType(IGroupFilterMapPage filter)
{
Log.ForContext().Debug($"Bike type of active filter is extracted.");
List currentFilteredBikeType = filter.Where(x => x.Value == FilterState.On).Select(x => x.Key).ToList();
string filteredBikeType = currentFilteredBikeType.Count == 1 ? currentFilteredBikeType[0] : string.Empty;
if (filteredBikeType == FilterHelper.CARGOBIKE)
{
return AppResources.MarkingCargoBike;
}else if (filteredBikeType == FilterHelper.CITYBIKE)
{
return AppResources.MarkingCityBike;
}
else
{
return string.Empty;
}
}
}
}