/*
* Copyright (c) 2024 Proton AG
*
* This file is part of ProtonVPN.
*
* ProtonVPN is free software: you can redistribute it and/or modify
* it under the terms of the GNU General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* ProtonVPN is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU General Public License for more details.
*
* You should have received a copy of the GNU General Public License
* along with ProtonVPN. If not, see .
*/
using CommunityToolkit.Mvvm.ComponentModel;
using ProtonVPN.Client.Common.Collections;
using ProtonVPN.Client.Core.Bases;
using ProtonVPN.Client.Core.Bases.ViewModels;
using ProtonVPN.Client.Core.Enums;
using ProtonVPN.Client.EventMessaging.Contracts;
using ProtonVPN.Client.Factories;
using ProtonVPN.Client.Logic.Connection.Contracts.Enums;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Features;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.Cities;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.Countries;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.Gateways;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.GatewayServers;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.Servers;
using ProtonVPN.Client.Logic.Connection.Contracts.Models.Intents.Locations.States;
using ProtonVPN.Client.Logic.Servers.Contracts;
using ProtonVPN.Client.Logic.Servers.Contracts.Enums;
using ProtonVPN.Client.Logic.Servers.Contracts.Messages;
using ProtonVPN.Client.Models;
using ProtonVPN.Client.Models.Connections;
using ProtonVPN.Client.Models.Connections.Countries;
using ProtonVPN.Client.Models.Connections.Gateways;
using ProtonVPN.Client.UI.Main.Profiles.Contracts;
using ProtonVPN.Common.Core.Extensions;
namespace ProtonVPN.Client.UI.Main.Profiles.Components;
public partial class ConnectionIntentSelectorViewModel : ViewModelBase,
IConnectionIntentSelector,
IEventMessageReceiver
{
private readonly IServersLoader _serversLoader;
private readonly ILocationItemFactory _locationItemFactory;
private readonly ICommonItemFactory _commonItemFactory;
private IConnectionIntent _originalConnectionIntent = ConnectionIntent.Default;
private ILocationIntent? _currentLocationIntent;
private IFeatureIntent? _currentFeatureIntent;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(FirstLevelHeader))]
private FeatureItem? _selectedFeature;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasSecondLevel))]
[NotifyPropertyChangedFor(nameof(HasThirdLevel))]
private ConnectionItemBase? _selectedFirstLevelLocationItem;
[ObservableProperty]
[NotifyPropertyChangedFor(nameof(HasThirdLevel))]
private ConnectionItemBase? _selectedSecondLevelLocationItem;
[ObservableProperty]
private ConnectionItemBase? _selectedThirdLevelLocationItem;
private IReadOnlyList _gateways = [];
private IReadOnlyList _countries = [];
private IReadOnlyList _p2pCountries = [];
private IReadOnlyList _secureCoreCountries = [];
private bool _isUpdatingFeatureSelection;
private bool _isUpdatingLocationSelection;
public SmartObservableCollection FirstLevelLocationItems { get; } = [];
public SmartObservableCollection SecondLevelLocationItems { get; } = [];
public SmartObservableCollection ThirdLevelLocationItems { get; } = [];
public SmartObservableCollection Features { get; } = [];
public bool AreOnlyGatewaysAvailable => _serversLoader.HasAnyGateways() && !_serversLoader.HasAnyCountries();
public string FirstLevelHeader => SelectedFeature?.Feature switch
{
Feature.B2B => Localizer.Get("Connections_Gateway"),
_ => Localizer.Get("Connections_Country"),
};
public string SecondLevelHeader => SecondLevelLocationItems.FirstOrDefault(i => i is not GenericFastestLocationItem) switch
{
SecureCoreCountryPairLocationItem => Localizer.Get("Connections_Country_Middle"),
GatewayServerLocationItem => Localizer.Get("Connections_Gateways_Server"),
StateLocationItemBase => Localizer.Get("Connections_State"),
_ => Localizer.Get("Connections_City"),
};
public string ThirdLevelHeader => Localizer.Get("Server");
public bool HasSecondLevel => SelectedFirstLevelLocationItem is IHostLocationItem;
public bool HasThirdLevel => HasSecondLevel && SelectedSecondLevelLocationItem is IHostLocationItem;
public ConnectionIntentSelectorViewModel(
IServersLoader serversLoader,
ILocationItemFactory locationItemFactory,
ICommonItemFactory commonItemFactory,
IViewModelHelper viewModelHelper)
: base(viewModelHelper)
{
_serversLoader = serversLoader;
_locationItemFactory = locationItemFactory;
_commonItemFactory = commonItemFactory;
}
public IConnectionIntent GetConnectionIntent()
{
InvalidateCurrentFeatureIntent();
InvalidateCurrentLocationIntent();
return _currentLocationIntent != null
? new ConnectionIntent(_currentLocationIntent, _currentFeatureIntent)
: ConnectionIntent.Default;
}
public void SetConnectionIntent(IConnectionIntent connectionIntent)
{
_originalConnectionIntent = connectionIntent ?? ConnectionIntent.Default;
_currentFeatureIntent = _originalConnectionIntent.Feature?.Copy();
InvalidateFeatureSelection();
_currentLocationIntent = _originalConnectionIntent.Location.Copy();
InvalidateLocationSelection();
}
public void Receive(ServerListChangedMessage message)
{
ExecuteOnUIThread(() =>
{
OnPropertyChanged(nameof(AreOnlyGatewaysAvailable));
InvalidateServers();
});
}
public bool HasChanged()
{
bool isSameIntent =
_originalConnectionIntent.Location.IsSameAs(_currentLocationIntent) &&
((_originalConnectionIntent.Feature == null && _currentFeatureIntent == null) || _originalConnectionIntent.Feature?.IsSameAs(_currentFeatureIntent) == true);
return !isSameIntent;
}
public bool IsReconnectionRequired()
{
return HasChanged();
}
private void InvalidateServers()
{
_gateways = GetGateways();
_countries = GetCountries();
_p2pCountries = GetP2PCountries();
_secureCoreCountries = GetSecureCoreCountries();
InvalidateFeatures();
InvalidateFeatureSelection();
InvalidateLocationSelection();
}
private IReadOnlyList GetGateways()
{
IEnumerable gateways =
_serversLoader.GetGateways()
.Select(_locationItemFactory.GetGateway);
return gateways.ToList();
}
private IReadOnlyList GetCountries()
{
IEnumerable genericCountries =
[
_locationItemFactory.GetGenericCountry(CountriesConnectionType.All, SelectionStrategy.Fastest, false),
_locationItemFactory.GetGenericCountry(CountriesConnectionType.All, SelectionStrategy.Fastest, true),
_locationItemFactory.GetGenericCountry(CountriesConnectionType.All, SelectionStrategy.Random, false),
];
IEnumerable countries =
_serversLoader.GetCountries()
.Select(c => _locationItemFactory.GetCountry(c));
return genericCountries
.Concat(countries)
.ToList();
}
private IReadOnlyList GetP2PCountries()
{
IEnumerable genericCountries =
[
_locationItemFactory.GetGenericCountry(CountriesConnectionType.P2P, SelectionStrategy.Fastest, false),
_locationItemFactory.GetGenericCountry(CountriesConnectionType.P2P, SelectionStrategy.Fastest, true),
_locationItemFactory.GetGenericCountry(CountriesConnectionType.P2P, SelectionStrategy.Random, false),
];
IEnumerable countries =
_serversLoader.GetCountriesByFeatures(ServerFeatures.P2P)
.Select(c => _locationItemFactory.GetP2PCountry(c));
return genericCountries
.Concat(countries)
.ToList();
}
private IReadOnlyList GetSecureCoreCountries()
{
IEnumerable genericCountries =
[
_locationItemFactory.GetGenericCountry(CountriesConnectionType.SecureCore, SelectionStrategy.Fastest, false),
_locationItemFactory.GetGenericCountry(CountriesConnectionType.SecureCore, SelectionStrategy.Fastest, true),
_locationItemFactory.GetGenericCountry(CountriesConnectionType.SecureCore, SelectionStrategy.Random, false),
];
IEnumerable countries =
_serversLoader.GetCountriesByFeatures(ServerFeatures.SecureCore)
.Select(c => _locationItemFactory.GetSecureCoreCountry(c));
return genericCountries
.Concat(countries)
.ToList();
}
private void InvalidateFeatures()
{
if (AreOnlyGatewaysAvailable)
{
Features.Reset([_commonItemFactory.GetFeature(Feature.B2B)]);
return;
}
List features = [Feature.None];
if (_gateways.Any())
{
features.Add(Feature.B2B);
}
if (_secureCoreCountries.Any())
{
features.Add(Feature.SecureCore);
}
if (_p2pCountries.Any())
{
features.Add(Feature.P2P);
}
Features.Reset(
features.Select(_commonItemFactory.GetFeature));
}
private void InvalidateFeatureSelection()
{
try
{
_isUpdatingFeatureSelection = true;
SelectedFeature = _currentFeatureIntent switch
{
B2BFeatureIntent when Features.Any(f => f.Feature == Feature.B2B) => Features.FirstOrDefault(f => f.Feature == Feature.B2B),
P2PFeatureIntent when Features.Any(f => f.Feature == Feature.P2P) => Features.FirstOrDefault(f => f.Feature == Feature.P2P),
SecureCoreFeatureIntent when Features.Any(f => f.Feature == Feature.SecureCore) => Features.FirstOrDefault(f => f.Feature == Feature.SecureCore),
_ when AreOnlyGatewaysAvailable => Features.FirstOrDefault(f => f.Feature == Feature.B2B),
_ => Features.FirstOrDefault(f => f.Feature == Feature.None)
};
}
finally
{
_isUpdatingFeatureSelection = false;
}
}
private void InvalidateLocationSelection()
{
try
{
_isUpdatingLocationSelection = true;
InvalidateFirstLevelLocations();
SelectedFirstLevelLocationItem = _currentLocationIntent switch
{
MultiCountryLocationIntent intent when intent.IsToExcludeMyCountry => intent.Strategy switch
{
SelectionStrategy.Fastest => FirstLevelLocationItems.OfType().FirstOrDefault(c => c.Strategy == SelectionStrategy.Fastest && c.ExcludeMyCountry),
SelectionStrategy.Random => FirstLevelLocationItems.OfType().FirstOrDefault(c => c.Strategy == SelectionStrategy.Random && c.ExcludeMyCountry),
_ => null,
},
MultiCountryLocationIntent intent => intent.Strategy switch
{
SelectionStrategy.Fastest => FirstLevelLocationItems.OfType().FirstOrDefault(c => c.Strategy == SelectionStrategy.Fastest && !c.ExcludeMyCountry),
SelectionStrategy.Random => FirstLevelLocationItems.OfType().FirstOrDefault(c => c.Strategy == SelectionStrategy.Random && !c.ExcludeMyCountry),
_ => null,
},
SingleCountryLocationIntent intent => FirstLevelLocationItems.OfType().FirstOrDefault(c => intent.CountryCode.EqualsIgnoringCase(c.ExitCountryCode)),
StateLocationIntentBase intent => FirstLevelLocationItems.OfType().FirstOrDefault(c => intent.Country.CountryCode.EqualsIgnoringCase(c.ExitCountryCode)),
CityLocationIntentBase intent => FirstLevelLocationItems.OfType().FirstOrDefault(c => intent.Country.CountryCode.EqualsIgnoringCase(c.ExitCountryCode)),
ServerLocationIntentBase intent => FirstLevelLocationItems.OfType().FirstOrDefault(c => intent.Country.CountryCode.EqualsIgnoringCase(c.ExitCountryCode)),
MultiGatewayLocationIntent intent => intent.Strategy switch
{
SelectionStrategy.Fastest => FirstLevelLocationItems.OfType().FirstOrDefault(g => g.Strategy == SelectionStrategy.Fastest),
SelectionStrategy.Random => FirstLevelLocationItems.OfType().FirstOrDefault(g => g.Strategy == SelectionStrategy.Random),
_ => null,
},
GatewayServerLocationIntentBase intent => FirstLevelLocationItems.OfType().FirstOrDefault(g => intent.Gateway.GatewayName.EqualsIgnoringCase(g.Gateway.Name)),
_ => null
};
SelectedFirstLevelLocationItem ??= FirstLevelLocationItems.FirstOrDefault();
SelectedSecondLevelLocationItem = _currentLocationIntent switch
{
_ when _currentFeatureIntent is SecureCoreFeatureIntent intent => SecondLevelLocationItems.OfType().FirstOrDefault(cp => cp.CountryPair.EntryCountry == intent.EntryCountryCode),
MultiStateLocationIntent intent => intent.Strategy switch
{
// TODO: Pick the right generic intent based on the strategy (fastest or random)
SelectionStrategy.Fastest => SecondLevelLocationItems.OfType().FirstOrDefault(),
SelectionStrategy.Random => SecondLevelLocationItems.OfType().FirstOrDefault(),
_ => null
},
SingleStateLocationIntent intent => SecondLevelLocationItems.OfType().FirstOrDefault(s => intent.StateName.EqualsIgnoringCase(s.State.Name)),
MultiCityLocationIntent intent => intent.Strategy switch
{
// TODO: Pick the right generic intent based on the strategy (fastest or random)
SelectionStrategy.Fastest => SecondLevelLocationItems.OfType().FirstOrDefault(),
SelectionStrategy.Random => SecondLevelLocationItems.OfType().FirstOrDefault(),
_ => null
},
SingleCityLocationIntent intent => SecondLevelLocationItems.OfType().FirstOrDefault(c => intent.CityName.EqualsIgnoringCase(c.City.Name)),
ServerLocationIntentBase intent when intent.State != null => SecondLevelLocationItems.OfType().FirstOrDefault(s => intent.State.StateName.EqualsIgnoringCase(s.State.Name)),
ServerLocationIntentBase intent when intent.City != null => SecondLevelLocationItems.OfType().FirstOrDefault(c => intent.City.CityName.EqualsIgnoringCase(c.City.Name)),
SingleServerLocationIntent intent => SecondLevelLocationItems.OfType().FirstOrDefault(s => intent.Server.Id == s.Server.Id),
SingleGatewayServerLocationIntent intent => SecondLevelLocationItems.OfType().FirstOrDefault(s => intent.Server.Id == s.Server.Id),
_ => null
};
SelectedSecondLevelLocationItem ??= SecondLevelLocationItems.FirstOrDefault();
SelectedThirdLevelLocationItem = _currentLocationIntent switch
{
SingleServerLocationIntent intent => ThirdLevelLocationItems.OfType().FirstOrDefault(s => intent.Server.Id == s.Server.Id),
_ => null
};
SelectedThirdLevelLocationItem ??= ThirdLevelLocationItems.FirstOrDefault();
}
finally
{
_isUpdatingLocationSelection = false;
}
}
private void InvalidateFirstLevelLocations()
{
IEnumerable items = SelectedFeature?.Feature switch
{
Feature.B2B => _gateways,
Feature.P2P => _p2pCountries,
Feature.SecureCore => _secureCoreCountries,
_ => _countries,
};
FirstLevelLocationItems.Reset(
items.OrderBy(g => g.FirstSortProperty)
.ThenBy(g => g.SecondSortProperty));
}
private void InvalidateSecondLevelLocations()
{
List locations = [];
if (SelectedFirstLevelLocationItem is IHostLocationItem hostLocationItem)
{
hostLocationItem.FetchSubItems();
ConnectionItemBase fastestLocation =
_locationItemFactory.GetGenericFastestLocation(hostLocationItem.GroupType, hostLocationItem.LocationIntent);
locations.Add(fastestLocation);
locations.AddRange(
hostLocationItem.SubItems
.OrderBy(l => l.Header)
.ToList());
}
SecondLevelLocationItems.Reset(locations);
OnPropertyChanged(nameof(SecondLevelHeader));
}
private void InvalidateThirdLevelLocations()
{
List locations = [];
if (SelectedSecondLevelLocationItem is IHostLocationItem hostLocationItem)
{
hostLocationItem.FetchSubItems();
ConnectionItemBase fastestLocation =
_locationItemFactory.GetGenericFastestLocation(hostLocationItem.GroupType, hostLocationItem.LocationIntent);
locations.Add(fastestLocation);
locations.AddRange(
hostLocationItem.SubItems
.OfType()
.OrderBy(s => s.ServerTag)
.ThenBy(s => s.ServerNumber)
.ToList());
}
ThirdLevelLocationItems.Reset(locations);
}
private void InvalidateCurrentFeatureIntent()
{
_currentFeatureIntent = SelectedFeature?.Feature switch
{
Feature.SecureCore when SelectedSecondLevelLocationItem is SecureCoreCountryPairLocationItem countryPair => new SecureCoreFeatureIntent(countryPair.CountryPair.EntryCountry),
Feature.SecureCore => new SecureCoreFeatureIntent(),
Feature.Tor => new TorFeatureIntent(),
Feature.P2P => new P2PFeatureIntent(),
Feature.B2B => new B2BFeatureIntent(),
_ => null,
};
}
private void InvalidateCurrentLocationIntent()
{
_currentLocationIntent =
HasThirdLevel && SelectedThirdLevelLocationItem is LocationItemBase thirdLevelLocationItem
? thirdLevelLocationItem.LocationIntent
: HasSecondLevel && SelectedSecondLevelLocationItem is LocationItemBase secondLevelLocationItem
? secondLevelLocationItem.LocationIntent
: SelectedFirstLevelLocationItem is LocationItemBase firstLevelLocationItem
? firstLevelLocationItem.LocationIntent
: MultiCountryLocationIntent.Default;
}
partial void OnSelectedFeatureChanged(FeatureItem? value)
{
if (!_isUpdatingFeatureSelection)
{
InvalidateCurrentFeatureIntent();
InvalidateCurrentLocationIntent();
InvalidateLocationSelection();
}
}
partial void OnSelectedFirstLevelLocationItemChanged(ConnectionItemBase? value)
{
InvalidateSecondLevelLocations();
if (!_isUpdatingLocationSelection)
{
SelectedSecondLevelLocationItem = SecondLevelLocationItems.FirstOrDefault();
InvalidateCurrentLocationIntent();
}
}
partial void OnSelectedSecondLevelLocationItemChanged(ConnectionItemBase? value)
{
InvalidateThirdLevelLocations();
if (!_isUpdatingLocationSelection)
{
SelectedThirdLevelLocationItem = ThirdLevelLocationItems.FirstOrDefault();
InvalidateCurrentLocationIntent();
if (_currentFeatureIntent is SecureCoreFeatureIntent)
{
InvalidateCurrentFeatureIntent();
}
}
}
partial void OnSelectedThirdLevelLocationItemChanged(ConnectionItemBase? value)
{
if (!_isUpdatingLocationSelection)
{
InvalidateCurrentLocationIntent();
}
}
}