2022-09-22 20:58:30 +02:00
using System ;
2022-08-30 15:42:25 +02:00
using System.Collections.Generic ;
2021-05-13 20:03:07 +02:00
using System.Collections.Specialized ;
using System.ComponentModel ;
2022-08-30 15:42:25 +02:00
using System.Linq ;
using System.Threading ;
2021-05-13 20:03:07 +02:00
using System.Threading.Tasks ;
2022-08-30 15:42:25 +02:00
using Plugin.BLE.Abstractions.Contracts ;
2021-05-13 20:03:07 +02:00
using Serilog ;
using TINK.Model ;
2022-08-30 15:42:25 +02:00
using TINK.Model.Bikes ;
using TINK.Model.Bikes.BikeInfoNS.BluetoothLock ;
using TINK.Model.Connector ;
using TINK.Model.Device ;
2023-04-19 12:14:14 +02:00
using TINK.Model.Stations ;
using TINK.Model.Stations.StationNS ;
2022-08-30 15:42:25 +02:00
using TINK.Model.User ;
using TINK.MultilingualResources ;
2021-05-13 20:03:07 +02:00
using TINK.Services.BluetoothLock ;
using TINK.Services.BluetoothLock.Tdo ;
2022-08-30 15:42:25 +02:00
using TINK.Services.Geolocation ;
2021-11-07 19:42:59 +01:00
using TINK.Services.Permissions ;
2022-08-30 15:42:25 +02:00
using TINK.Settings ;
using TINK.View ;
using TINK.ViewModel.Bikes ;
using Xamarin.Forms ;
2023-01-18 14:22:51 +01:00
using Command = Xamarin . Forms . Command ;
2021-05-13 20:03:07 +02:00
namespace TINK.ViewModel.BikesAtStation
{
2022-09-06 16:08:19 +02:00
/// <summary>
/// Manages one or more bikes which are located at a single station.
/// </summary>
public class BikesAtStationPageViewModel : BikesViewModel , INotifyCollectionChanged , INotifyPropertyChanged
{
/// <summary>
/// Holds the selected station;
/// </summary>
private IStation Station { get ; }
2023-01-18 14:22:51 +01:00
/// <summary>
/// True if ListView of Bikes is refreshing after user pulled;
/// </summary>
private bool _isRefreshing = false ;
public bool IsRefreshing
{
get { return _isRefreshing ; }
set
{
_isRefreshing = value ;
OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( IsRefreshing ) ) ) ;
}
}
2023-02-22 14:03:35 +01:00
/// <summary>
/// Holds what should be executed on pull to refresh
/// </summary>
2023-01-18 14:22:51 +01:00
public Command RefreshCommand { get ; }
2022-09-06 16:08:19 +02:00
/// <summary>
/// Constructs bike collection view model.
/// </summary>
/// <param name="user">Mail address of active user.</param>
2023-04-19 12:14:14 +02:00
/// <param name="permissions">Holds object to query location permissions.</param>
2022-09-06 16:08:19 +02:00
/// <param name="bluetoothLE">Holds object to query bluetooth state.</param>
/// <param name="runtimPlatform">Specifies on which platform code is run.</param>
/// <param name="isConnectedDelegate">Returns if mobile is connected to web or not.</param>
/// <param name="connectorFactory">Connects system to copri.</param>
/// <param name="lockService">Service to control lock retrieve info.</param>
2023-04-19 12:14:14 +02:00
/// <param name="polling"> Holds whether to poll or not and the period length is polling is on. </param>
2022-09-06 16:08:19 +02:00
/// <param name="openUrlInExternalBrowser">Action to open an external browser.</param>
/// <param name="postAction">Executes actions on GUI thread.</param>
/// <param name="smartDevice">Provides info about the smart device (phone, tablet, ...).</param>
2023-04-19 12:14:14 +02:00
/// <param name="viewService">Interface to actuate methods on GUI.</param>
2022-09-06 16:08:19 +02:00
public BikesAtStationPageViewModel (
User user ,
ILocationPermission permissions ,
IBluetoothLE bluetoothLE ,
string runtimPlatform ,
IStation selectedStation ,
Func < bool > isConnectedDelegate ,
Func < bool , IConnector > connectorFactory ,
2023-04-05 15:02:10 +02:00
IGeolocationService geolocation ,
2022-09-06 16:08:19 +02:00
ILocksService lockService ,
PollingParameters polling ,
Action < string > openUrlInExternalBrowser ,
Action < SendOrPostCallback , object > 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 ;
2023-01-18 14:22:51 +01:00
RefreshCommand = new Command ( async ( ) = > {
IsRefreshing = false ;
2023-02-22 14:03:35 +01:00
await OnAppearingOrRefresh ( ) ;
2023-01-18 14:22:51 +01:00
} ) ;
2022-09-06 16:08:19 +02:00
CollectionChanged + = ( sender , eventargs ) = >
{
OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( IsNoBikesAtStationVisible ) ) ) ;
OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( NoBikesAtStationText ) ) ) ;
OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( IsLoginRequiredHintVisible ) ) ) ;
2023-01-18 14:22:51 +01:00
OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( IsRefreshing ) ) ) ;
2022-09-06 16:08:19 +02:00
} ;
}
/// <summary>
/// Name of the station which is displayed as title of the page.
/// </summary>
public string Title
{
get ; private set ;
}
/// <summary>
/// Informs about need to log in before requesting an bike.
/// </summary>
public bool IsLoginRequiredHintVisible
{
get
{
return Count > 0
& & ! ActiveUser . IsLoggedIn ;
}
}
/// <summary>
/// Informs about need to log in before requesting an bike.
/// </summary>
public string LoginRequiredHintText
= > ActiveUser . IsLoggedIn
? string . Empty
: AppResources . MarkingLoginRequiredToRerserve ;
public string ContactSupportHintText
2022-09-22 20:58:30 +02:00
= > string . Format ( IsIdle ? AppResources . MarkingContactSupport : AppResources . MarkingContactSupportBusy , Station . OperatorData ? . Name ? ? "Operator" ) ;
2022-09-06 16:08:19 +02:00
/// <summary>
/// Returns if info about the fact that user did not request or book any bikes is visible or not.
/// </summary>
public bool IsNoBikesAtStationVisible
{
get
{
return Count < = 0 & & IsIdle = = true ;
}
}
/// <summary> Info about the fact that user did not request or book any bikes. </summary>
public string NoBikesAtStationText
{
get
{
return IsNoBikesAtStationVisible
2023-01-18 14:22:51 +01:00
? AppResources . MarkingBikesAtStationNoBikesAvailable
2022-09-06 16:08:19 +02:00
: string . Empty ;
}
}
/// <summary> Command object to bind login page redirect link to view model.</summary>
public System . Windows . Input . ICommand ContactSupportClickedCommand
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-20 23:06:09 +02:00
= > new Xamarin . Forms . Command ( ( ) = > OpenSupportPageAsync ( ) ) ;
#else
2022-09-06 16:08:19 +02:00
= > new Xamarin . Forms . Command ( async ( ) = > await OpenSupportPageAsync ( ) ) ;
2021-07-20 23:06:09 +02:00
#endif
2022-09-06 16:08:19 +02:00
/// <summary> Command object to bind login page redirect link to view model.</summary>
public System . Windows . Input . ICommand LoginRequiredHintClickedCommand
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-20 23:06:09 +02:00
= > new Xamarin . Forms . Command ( ( ) = > OpenLoginPageAsync ( ) ) ;
#else
2022-09-06 16:08:19 +02:00
= > new Xamarin . Forms . Command ( async ( ) = > await OpenLoginPageAsync ( ) ) ;
2021-07-20 23:06:09 +02:00
#endif
2022-09-06 16:08:19 +02:00
/// <summary> Opens login page. </summary>
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-20 23:06:09 +02:00
public void OpenLoginPageAsync ( )
#else
2022-09-06 16:08:19 +02:00
public async Task OpenLoginPageAsync ( )
2021-07-20 23:06:09 +02:00
#endif
2022-09-06 16:08:19 +02:00
{
try
{
// Switch to map page
2021-07-20 23:06:09 +02:00
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-20 23:06:09 +02:00
ViewService . ShowPage ( ViewTypes . LoginPage ) ;
2021-06-26 20:57:55 +02:00
#else
2022-09-06 16:08:19 +02:00
await ViewService . ShowPage ( "//LoginPage" ) ;
2021-06-26 20:57:55 +02:00
#endif
2022-09-06 16:08:19 +02:00
}
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 ;
}
}
/// <summary> Opens support. </summary>
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-07-20 23:06:09 +02:00
public void OpenSupportPageAsync ( )
2021-06-26 20:57:55 +02:00
#else
2022-09-06 16:08:19 +02:00
public async Task OpenSupportPageAsync ( )
2021-06-26 20:57:55 +02:00
#endif
2022-09-06 16:08:19 +02:00
{
try
{
2022-09-22 20:58:30 +02:00
if ( ! IsIdle )
{
// Prevent navigation when app is not idle because this might lead to aborting return bike workflow.
return ;
}
2022-09-06 16:08:19 +02:00
// Switch to map page
2021-06-26 20:57:55 +02:00
2021-08-28 10:04:10 +02:00
#if USEFLYOUT
2021-08-01 17:24:15 +02:00
ViewService . ShowPage ( ViewTypes . ContactPage , AppResources . MarkingFeedbackAndContact ) ;
2021-06-26 20:57:55 +02:00
#else
2022-09-06 16:08:19 +02:00
await ViewService . ShowPage ( "//ContactPage" ) ;
2021-06-26 20:57:55 +02:00
#endif
2022-09-06 16:08:19 +02:00
}
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 ;
}
}
/// <summary> Returns detailed info about the station (station id).<summary>
public string StationDetailText { get ; private set ; }
/// <summary>
/// Invoked when page is shown.
/// Starts update process.
/// </summary>
2023-02-22 14:03:35 +01:00
public async Task OnAppearingOrRefresh ( )
2022-09-06 16:08:19 +02:00
{
2023-01-18 14:22:51 +01:00
IsIdle = false ;
2023-02-22 14:03:35 +01:00
IsConnected = IsConnectedDelegate ( ) ;
2022-09-06 16:08:19 +02:00
Log . ForContext < BikesAtStationPageViewModel > ( ) . Information ( $"Bikes at station {Station.StationName} is appearing, either due to tap on a station or to app being shown again." ) ;
2022-09-22 20:58:30 +02:00
ActionText = AppResources . ActivityTextOneMomentPlease ;
2022-09-06 16:08:19 +02:00
// 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 ( )
2022-09-22 20:58:30 +02:00
. Cast < BikeInfo > ( )
2022-09-06 16:08:19 +02:00
. Select ( x = > x . LockInfo )
. ToList ( ) ;
if ( LockService is ILocksServiceFake serviceFake )
{
serviceFake . UpdateSimulation ( bikesAtStation ) ;
}
2022-09-22 20:58:30 +02:00
ActionText = AppResources . ActivityTextCheckBluetoothState ;
2022-09-06 16:08:19 +02:00
// Check location permissions.
if ( bikesAtStation . GetLockIt ( ) . Count > 0
2022-12-07 16:54:52 +01:00
& & RuntimePlatform = = Device . Android
)
2022-09-06 16:08:19 +02:00
{
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 < IStation > { Station } ) ;
await OnAppearing ( ( ) = > UpdateTask ( ) ) ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2022-09-06 16:08:19 +02:00
IsIdle = true ;
return ;
}
2022-12-07 16:54:52 +01:00
else if ( dialogResult )
{
// Open permissions dialog.
PermissionsService . OpenAppSettings ( ) ;
}
2022-09-06 16:08:19 +02:00
}
2023-04-05 15:02:10 +02:00
if ( GeolocationService . IsGeolcationEnabled = = false )
2022-09-06 16:08:19 +02:00
{
await ViewService . DisplayAlert (
AppResources . MessageTitleHint ,
AppResources . MessageBikesManagementLocationActivation ,
AppResources . MessageAnswerOk ) ;
BikeCollection . Update ( bikesAtStation , new List < IStation > { Station } ) ;
await OnAppearing ( ( ) = > UpdateTask ( ) ) ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2022-09-06 16:08:19 +02:00
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 < IStation > { Station } ) ;
await OnAppearing ( ( ) = > UpdateTask ( ) ) ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2022-09-06 16:08:19 +02:00
IsIdle = true ;
return ;
}
}
// Connect to bluetooth devices.
ActionText = AppResources . ActivityTextSearchBikes ;
IEnumerable < LockInfoTdo > locksInfoTdo ;
try
{
locksInfoTdo = await LockService . GetLocksStateAsync (
lockIdList . Select ( x = > x . ToLockInfoTdo ( ) ) . ToList ( ) ,
LockService . TimeOut . MultiConnect ) ;
}
catch ( Exception exception )
{
Log . ForContext < BikesAtStationPageViewModel > ( ) . Error ( "Getting bluetooth state failed. {Exception}" , exception ) ;
locksInfoTdo = new List < LockInfoTdo > ( ) ;
}
var locksInfo = lockIdList . UpdateById ( locksInfoTdo ) ;
BikeCollection . Update ( bikesAtStation . UpdateLockInfo ( locksInfo ) , new List < IStation > { Station } ) ;
// Backup GUI synchronization context.
await OnAppearing ( ( ) = > UpdateTask ( ) ) ;
2023-02-22 14:03:35 +01:00
ActionText = string . Empty ;
2022-09-06 16:08:19 +02:00
IsIdle = true ;
}
/// <summary> Create task which updates my bike view model.</summary>
2023-01-18 14:22:51 +01:00
public void UpdateTask ( )
2022-09-06 16:08:19 +02:00
{
PostAction (
unused = >
{
Log . ForContext < BikesAtStationPageViewModel > ( ) . 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 )
{
2023-04-19 12:14:14 +02:00
Log . ForContext < BikesAtStationPageViewModel > ( ) . Error ( "Getting all bikes in polling context failed with exception {Exception}." , exception ) ;
2022-09-06 16:08:19 +02:00
}
PostAction (
unused = >
{
Log . ForContext < BikesAtStationPageViewModel > ( ) . Debug ( "Updating bikes at station..." ) ;
BikeCollection . Update ( bikes , new List < IStation > { Station } ) ;
Exception = result . Exception ;
ActionText = string . Empty ;
} ,
null ) ;
}
/// <summary>
/// True if any action can be performed (request and cancel request)
/// </summary>
public override bool IsIdle
{
get = > base . IsIdle ;
set
{
if ( value = = base . IsIdle )
return ;
Log . ForContext < BikesViewModel > ( ) . Debug ( $"Switch value of {nameof(IsIdle)} to {value}." ) ;
base . IsIdle = value ;
base . OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( IsNoBikesAtStationVisible ) ) ) ;
base . OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( NoBikesAtStationText ) ) ) ;
2022-09-22 20:58:30 +02:00
base . OnPropertyChanged ( new PropertyChangedEventArgs ( nameof ( ContactSupportHintText ) ) ) ;
2022-09-06 16:08:19 +02:00
}
}
}
2022-09-22 20:58:30 +02:00
}