Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 12 additions & 0 deletions core/src/main/java/net/ivpn/core/common/Mapper.kt
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,7 @@ import com.google.gson.JsonSyntaxException
import com.google.gson.reflect.TypeToken
import net.ivpn.core.rest.data.ServersListResponse
import net.ivpn.core.rest.data.model.AntiTracker
import net.ivpn.core.rest.data.model.Host
import net.ivpn.core.rest.data.model.Port
import net.ivpn.core.rest.data.model.Server
import net.ivpn.core.rest.data.session.SessionErrorResponse
Expand All @@ -35,6 +36,17 @@ import net.ivpn.core.vpn.model.V2RaySettings
import java.util.*

object Mapper {
fun hostFrom(json: String?): Host? {
return if (json == null || json.isEmpty()) null else try {
Gson().fromJson(json, Host::class.java)
} catch (_: JsonSyntaxException) {
null
}
}

fun stringFromHost(host: Host?): String {
return Gson().toJson(host)
}
fun from(json: String?): Server? {
return if (json == null) null else Gson().fromJson(json, Server::class.java)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -37,6 +37,8 @@
import net.ivpn.core.v2.splittunneling.items.ApplicationItem;
import net.ivpn.core.v2.network.NetworkRecyclerViewAdapter;
import net.ivpn.core.v2.serverlist.ServerBasedRecyclerViewAdapter;
import net.ivpn.core.v2.serverlist.favourite.FavouriteServerListRecyclerViewAdapter;
import net.ivpn.core.v2.serverlist.items.ConnectionOption;
import net.ivpn.core.vpn.model.WifiItem;

import java.util.ArrayList;
Expand All @@ -57,6 +59,13 @@ public static void setItems(RecyclerView recyclerView, List<Server> items) {
}
}

@BindingAdapter("favouriteItems")
public static void setFavouriteItems(RecyclerView recyclerView, List<ConnectionOption> items) {
if (recyclerView.getAdapter() instanceof FavouriteServerListRecyclerViewAdapter) {
((FavouriteServerListRecyclerViewAdapter) recyclerView.getAdapter()).replaceDataConnectionOptions(items != null ? items : new ArrayList<>());
}
}

@BindingAdapter("pings")
public static void setPings(RecyclerView recyclerView, Map<Server, PingResultFormatter> pings) {
ServerBasedRecyclerViewAdapter adapter = (ServerBasedRecyclerViewAdapter) recyclerView.getAdapter();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -59,6 +59,7 @@ class EncryptedSettingsPreference @Inject constructor(val preference: Preference
private const val SETTINGS_BYPASS_LOCAL = "SETTINGS_BYPASS_LOCAL"
private const val SETTINGS_IPV6 = "SETTINGS_IPV6"
private const val IPV6_SHOW_ALL_SERVERS = "IPV6_SHOW_ALL_SERVERS"
private const val SETTINGS_SELECT_HOST = "SETTINGS_SELECT_HOST"

private const val OV_PORT = "OV_PORT"
private const val WG_PORT = "WG_PORT"
Expand Down Expand Up @@ -177,6 +178,10 @@ class EncryptedSettingsPreference @Inject constructor(val preference: Preference
return sharedPreferences.getBoolean(SETTINGS_MULTI_HOP, false)
}

fun getSettingSelectHost(): Boolean {
return sharedPreferences.getBoolean(SETTINGS_SELECT_HOST, false)
}

fun getSettingStartOnBoot(): Boolean {
return sharedPreferences.getBoolean(SETTINGS_START_ON_BOOT, false)
}
Expand Down Expand Up @@ -241,6 +246,12 @@ class EncryptedSettingsPreference @Inject constructor(val preference: Preference
}
}

fun putSettingSelectHost(value: Boolean) {
sharedPreferences.edit {
putBoolean(SETTINGS_SELECT_HOST, value)
}
}

fun putSettingCustomDNS(value: Boolean) {
sharedPreferences.edit {
putBoolean(SETTINGS_CUSTOM_DNS, value)
Expand Down
96 changes: 96 additions & 0 deletions core/src/main/java/net/ivpn/core/common/prefs/ServersPreference.kt
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ package net.ivpn.core.common.prefs
import android.content.SharedPreferences
import net.ivpn.core.common.Mapper
import net.ivpn.core.common.dagger.ApplicationScope
import net.ivpn.core.rest.data.model.Host
import net.ivpn.core.rest.data.model.Server
import net.ivpn.core.rest.data.model.ServerLocation
import net.ivpn.core.rest.data.model.ServerLocation.Companion.from
Expand Down Expand Up @@ -45,6 +46,8 @@ class ServersPreference @Inject constructor(
companion object {
private const val CURRENT_ENTER_SERVER = "CURRENT_ENTER_SERVER"
private const val CURRENT_EXIT_SERVER = "CURRENT_EXIT_SERVER"
private const val CURRENT_ENTER_HOST = "CURRENT_ENTER_HOST"
private const val CURRENT_EXIT_HOST = "CURRENT_EXIT_HOST"
private const val SERVERS_LIST = "SERVERS_LIST"
private const val LOCATION_LIST = "LOCATION_LIST"
private const val FAVOURITES_SERVERS_LIST = "FAVOURITES_SERVERS_LIST"
Expand All @@ -53,6 +56,7 @@ class ServersPreference @Inject constructor(
private const val SETTINGS_RANDOM_ENTER_SERVER = "SETTINGS_RANDOM_ENTER_SERVER"
private const val SETTINGS_RANDOM_EXIT_SERVER = "SETTINGS_RANDOM_EXIT_SERVER"
private const val V2RAY_SETTINGS = "V2RAY_SETTINGS"
private const val FAVOURITES_HOSTS_LIST = "FAVOURITES_HOSTS_LIST"
}

var listeners = ArrayList<OnValueChangeListener>()
Expand Down Expand Up @@ -200,6 +204,34 @@ class ServersPreference @Inject constructor(
return Mapper.from(sharedPreferences.getString(serverKey, null))
}

fun setCurrentHost(serverType: ServerType?, host: Host?) {
if (serverType == null) return
val hostKey =
if (serverType == ServerType.ENTRY) CURRENT_ENTER_HOST else CURRENT_EXIT_HOST
preference.serversSharedPreferences.edit {
putString(hostKey, Mapper.stringFromHost(host))
}
preference.wireguardServersSharedPreferences.edit {
putString(hostKey, Mapper.stringFromHost(host))
}
}

fun getCurrentHost(serverType: ServerType?): Host? {
if (serverType == null) return null
val sharedPreferences = properSharedPreference
val hostKey =
if (serverType == ServerType.ENTRY) CURRENT_ENTER_HOST else CURRENT_EXIT_HOST
return Mapper.hostFrom(sharedPreferences.getString(hostKey, null))
}

fun clearCurrentHost(serverType: ServerType?) {
if (serverType == null) return
val hostKey =
if (serverType == ServerType.ENTRY) CURRENT_ENTER_HOST else CURRENT_EXIT_HOST
preference.serversSharedPreferences.edit { remove(hostKey) }
preference.wireguardServersSharedPreferences.edit { remove(hostKey) }
}

fun addFavouriteServer(server: Server?) {
val openvpnServer = openvpnServersList?.first { it == server }
val wireguardServer = wireguardServersList?.first { it == server }
Expand Down Expand Up @@ -236,6 +268,70 @@ class ServersPreference @Inject constructor(
.putString(FAVOURITES_SERVERS_LIST, Mapper.stringFrom(wireguardServers)).apply()
}

private fun getFavouriteHostsKey(server: Server): String {
return "${server.city}_${server.countryCode}"
}

private fun getFavouriteHostsSet(server: Server): MutableSet<String> {
val key = getFavouriteHostsKey(server)
val currentProtocol = protocolController.currentProtocol
val prefs = if (currentProtocol == Protocol.WIREGUARD) {
preference.wireguardServersSharedPreferences
} else {
preference.serversSharedPreferences
}
return prefs.getStringSet("${FAVOURITES_HOSTS_LIST}_$key", mutableSetOf())?.toMutableSet() ?: mutableSetOf()
}

private fun saveFavouriteHostsSet(server: Server, hosts: Set<String>) {
val key = getFavouriteHostsKey(server)
val currentProtocol = protocolController.currentProtocol
val prefs = if (currentProtocol == Protocol.WIREGUARD) {
preference.wireguardServersSharedPreferences
} else {
preference.serversSharedPreferences
}
prefs.edit().putStringSet("${FAVOURITES_HOSTS_LIST}_$key", hosts).apply()
}

fun addFavouriteHost(host: Host, parentServer: Server) {
val hostname = host.hostname ?: return
val favouriteHosts = getFavouriteHostsSet(parentServer)
if (!favouriteHosts.contains(hostname)) {
favouriteHosts.add(hostname)
saveFavouriteHostsSet(parentServer, favouriteHosts)
}
}

fun removeFavouriteHost(host: Host, parentServer: Server) {
val hostname = host.hostname ?: return
val favouriteHosts = getFavouriteHostsSet(parentServer)
if (favouriteHosts.contains(hostname)) {
favouriteHosts.remove(hostname)
saveFavouriteHostsSet(parentServer, favouriteHosts)
}
}

fun isHostFavourite(host: Host, parentServer: Server): Boolean {
val hostname = host.hostname ?: return false
val favouriteHosts = getFavouriteHostsSet(parentServer)
return favouriteHosts.contains(hostname)
}


fun getFavouriteHosts(servers: List<Server>): List<Pair<Host, Server>> {
val result = mutableListOf<Pair<Host, Server>>()
for (server in servers) {
val hostnames = getFavouriteHostsSet(server)
server.hosts?.forEach { host ->
if (host.hostname != null && hostnames.contains(host.hostname)) {
result.add(Pair(host, server))
}
}
}
return result
}

fun addToExcludedServersList(server: Server?) {
val openvpnServer = openvpnServersList?.first { it == server }
val wireguardServer = wireguardServersList?.first { it == server }
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -24,16 +24,19 @@ along with the IVPN Android app. If not, see <https://www.gnu.org/licenses/>.

import net.ivpn.core.common.Mapper
import net.ivpn.core.common.dagger.ApplicationScope
import net.ivpn.core.common.prefs.OnServerChangedListener
import net.ivpn.core.rest.HttpClientFactory
import net.ivpn.core.rest.IVPNApi
import net.ivpn.core.rest.RequestListener
import net.ivpn.core.rest.data.ServersListResponse
import net.ivpn.core.rest.data.model.AntiTracker
import net.ivpn.core.rest.data.model.Config
import net.ivpn.core.rest.data.model.Host
import net.ivpn.core.rest.data.model.Server
import net.ivpn.core.rest.data.model.ServerLocation
import net.ivpn.core.rest.data.model.ServerType
import net.ivpn.core.rest.requests.common.Request
import net.ivpn.core.v2.serverlist.items.HostItem
import net.ivpn.core.rest.requests.common.RequestWrapper
import net.ivpn.core.vpn.Protocol
import net.ivpn.core.vpn.ProtocolController
Expand Down Expand Up @@ -61,7 +64,7 @@ class ServersRepository @Inject constructor(
private val currentServers = EnumMap<Protocol, EnumMap<ServerType, Server?>>(Protocol::class.java)
private var onFavouritesChangedListeners: MutableList<OnFavouriteServersChangedListener> = ArrayList()
private var onServerListUpdatedListeners: MutableList<OnServerListUpdatedListener> = ArrayList()
private var onServerChangedListeners: List<OnServerChangedListener> = ArrayList()
private var onServerChangedListeners: MutableList<OnServerChangedListener> = ArrayList()
private var request: Request<ServersListResponse>? = null

init {
Expand Down Expand Up @@ -179,6 +182,14 @@ class ServersRepository @Inject constructor(
return serversPreference.favouritesServersList
}


fun getFavouriteHosts(): List<HostItem> {
val servers = getServers(false) ?: return emptyList()
return serversPreference.getFavouriteHosts(servers).map { (host, server) ->
HostItem(host, server, isFavourite = true)
}
}

fun addFavouritesServer(server: Server) {
LOGGER.info("addFavouritesServer server = $server")
serversPreference.addFavouriteServer(server)
Expand Down Expand Up @@ -276,11 +287,31 @@ class ServersRepository @Inject constructor(
serversPreference.putSettingFastestServer(false)
serversPreference.putSettingRandomServer(false, type)
setCurrentServer(type, server)
// Clear host selection when a different server is selected
serversPreference.clearCurrentHost(type)
for (listener in onServerChangedListeners) {
listener.onServerChanged()
}
}

fun hostSelected(server: Server?, host: Host?, type: ServerType) {
serversPreference.putSettingFastestServer(false)
serversPreference.putSettingRandomServer(false, type)
setCurrentServer(type, server)
serversPreference.setCurrentHost(type, host)
for (listener in onServerChangedListeners) {
listener.onServerChanged()
}
}

fun getCurrentHost(serverType: ServerType): Host? {
return serversPreference.getCurrentHost(serverType)
}

fun clearCurrentHost(serverType: ServerType) {
serversPreference.clearCurrentHost(serverType)
}

private fun tryUpdateServerListOffline() {
LOGGER.info("Trying update server list offline from cache...")
if (getCachedServers() != null) {
Expand Down Expand Up @@ -431,6 +462,14 @@ class ServersRepository @Inject constructor(
onServerListUpdatedListeners.remove(listener)
}

fun addOnServerChangedListener(listener: OnServerChangedListener) {
onServerChangedListeners.add(listener)
}

fun removeOnServerChangedListener(listener: OnServerChangedListener) {
onServerChangedListeners.remove(listener)
}

private val currentProtocolType: Protocol
get() = protocolController.currentProtocol

Expand All @@ -454,4 +493,18 @@ class ServersRepository @Inject constructor(
private fun notifyFavouriteServerRemoved(server: Server) {
onFavouritesChangedListeners.forEach { it.notifyFavouriteServerRemoved(server) }
}

fun addFavouriteHost(host: Host, parentServer: Server) {
LOGGER.info("addFavouriteHost host = ${host.hostname}, server = $parentServer")
serversPreference.addFavouriteHost(host, parentServer)
}

fun removeFavouriteHost(host: Host, parentServer: Server) {
LOGGER.info("removeFavouriteHost host = ${host.hostname}, server = $parentServer")
serversPreference.removeFavouriteHost(host, parentServer)
}

fun isHostFavourite(host: Host, parentServer: Server): Boolean {
return serversPreference.isHostFavourite(host, parentServer)
}
}
6 changes: 6 additions & 0 deletions core/src/main/java/net/ivpn/core/common/prefs/Settings.kt
Original file line number Diff line number Diff line change
Expand Up @@ -126,6 +126,12 @@ class Settings @Inject constructor(
settingsPreference.putSettingMultiHop(value)
}

var isSelectHostEnabled: Boolean
get() = settingsPreference.getSettingSelectHost()
set(value) {
settingsPreference.putSettingSelectHost(value)
}

var isMultiHopSameProviderAllowed: Boolean
get() = settingsPreference.isMultiHopSameProviderAllowed
set(value) {
Expand Down
48 changes: 48 additions & 0 deletions core/src/main/java/net/ivpn/core/rest/data/model/Server.java
Original file line number Diff line number Diff line change
Expand Up @@ -134,6 +134,54 @@ public String getDescription(Filters filter) {
return getDescription();
}

/**
* Returns description with host prefix if a host is provided.
* Format: "City (hostPrefix), CountryCode" e.g., "Vienna (at1), AT"
*/
public String getDescriptionWithHostPrefix(Host host) {
if (host == null || host.getHostname() == null) {
return getDescription();
}

String hostPrefix = extractHostPrefix(host.getHostname());
if (hostPrefix.isEmpty()) {
return getDescription();
}

return city + " (" + hostPrefix + "), " + countryCode;
}

/**
* Extracts short host prefix from hostname for display (e.g. "at1", "at").
* Examples:
* "at1.wg.ivpn.net" -> "at1"
* "at-vie-wg-001.relays.ivpn.net" -> "at"
* "at1-vie-wg-001.relays.ivpn.net" -> "at1"
*/
private String extractHostPrefix(String hostname) {
if (hostname == null || hostname.isEmpty()) {
return "";
}

String shortName = hostname;
int relaysIndex = hostname.indexOf(".relays");
if (relaysIndex > 0) {
shortName = hostname.substring(0, relaysIndex);
}

int dashIndex = shortName.indexOf('-');
if (dashIndex > 0) {
return shortName.substring(0, dashIndex);
}

int dotIndex = shortName.indexOf('.');
if (dotIndex > 0) {
return shortName.substring(0, dotIndex);
}

return shortName;
}

public List<Host> getHosts() {
return hosts;
}
Expand Down
Loading