@@ -3,6 +3,7 @@ package net.ivpn.core.common.prefs
33import android.content.SharedPreferences
44import net.ivpn.core.common.Mapper
55import net.ivpn.core.common.dagger.ApplicationScope
6+ import net.ivpn.core.rest.data.model.FavoriteIdentifier
67import net.ivpn.core.rest.data.model.Server
78import net.ivpn.core.rest.data.model.ServerLocation
89import net.ivpn.core.rest.data.model.ServerLocation.Companion.from
@@ -48,6 +49,7 @@ class ServersPreference @Inject constructor(
4849 private const val SERVERS_LIST = " SERVERS_LIST"
4950 private const val LOCATION_LIST = " LOCATION_LIST"
5051 private const val FAVOURITES_SERVERS_LIST = " FAVOURITES_SERVERS_LIST"
52+ private const val UNIFIED_FAVOURITES_LIST = " UNIFIED_FAVOURITES_LIST"
5153 private const val EXCLUDED_FASTEST_SERVERS = " EXCLUDED_FASTEST_SERVERS"
5254 private const val SETTINGS_FASTEST_SERVER = " SETTINGS_FASTEST_SERVER"
5355 private const val SETTINGS_RANDOM_ENTER_SERVER = " SETTINGS_RANDOM_ENTER_SERVER"
@@ -91,28 +93,75 @@ class ServersPreference @Inject constructor(
9193 return Mapper .serverListFrom(sharedPreferences.getString(SERVERS_LIST , null ))
9294 }
9395
96+ /* *
97+ * Returns the list of favorite servers for the current protocol.
98+ * This uses the unified favorites system where favorites are stored as
99+ * protocol-agnostic identifiers (gateway prefix for locations, dns_name for hosts).
100+ * When retrieving, it matches these identifiers against the current protocol's servers.
101+ */
94102 val favouritesServersList: MutableList <Server >
95103 get() {
96- val sharedPreferences = properSharedPreference
97- val servers =
98- Mapper .serverListFrom(sharedPreferences.getString(FAVOURITES_SERVERS_LIST , null ))
99- return servers ? : ArrayList ()
104+ val identifiers = unifiedFavouritesList
105+ val currentServers = serversList ? : return ArrayList ()
106+
107+ val favourites = ArrayList <Server >()
108+ for (server in currentServers) {
109+ for (identifier in identifiers) {
110+ if (identifier.matches(server)) {
111+ favourites.add(server)
112+ break
113+ }
114+ }
115+ }
116+ return favourites
117+ }
118+
119+ /* *
120+ * Returns the unified list of favorite identifiers.
121+ * These identifiers are protocol-agnostic and work across OpenVPN and WireGuard.
122+ */
123+ val unifiedFavouritesList: MutableList <FavoriteIdentifier >
124+ get() {
125+ // First try to get from unified storage
126+ val sharedPreferences = preference.stickySharedPreferences
127+ val identifiers = Mapper .favoriteIdentifierListFrom(
128+ sharedPreferences.getString(UNIFIED_FAVOURITES_LIST , null )
129+ )
130+ return identifiers ? : ArrayList ()
100131 }
101132
102133 val openvpnFavouritesServersList: MutableList <Server >
103134 get() {
104- val sharedPreferences = preference.serversSharedPreferences
105- val servers =
106- Mapper .serverListFrom(sharedPreferences.getString(FAVOURITES_SERVERS_LIST , null ))
107- return servers ? : ArrayList ()
135+ val identifiers = unifiedFavouritesList
136+ val currentServers = openvpnServersList ? : return ArrayList ()
137+
138+ val favourites = ArrayList <Server >()
139+ for (server in currentServers) {
140+ for (identifier in identifiers) {
141+ if (identifier.matches(server)) {
142+ favourites.add(server)
143+ break
144+ }
145+ }
146+ }
147+ return favourites
108148 }
109149
110150 val wireguardFavouritesServersList: MutableList <Server >
111151 get() {
112- val sharedPreferences = preference.wireguardServersSharedPreferences
113- val servers =
114- Mapper .serverListFrom(sharedPreferences.getString(FAVOURITES_SERVERS_LIST , null ))
115- return servers ? : ArrayList ()
152+ val identifiers = unifiedFavouritesList
153+ val currentServers = wireguardServersList ? : return ArrayList ()
154+
155+ val favourites = ArrayList <Server >()
156+ for (server in currentServers) {
157+ for (identifier in identifiers) {
158+ if (identifier.matches(server)) {
159+ favourites.add(server)
160+ break
161+ }
162+ }
163+ }
164+ return favourites
116165 }
117166
118167 val excludedServersList: MutableList <Server >
@@ -200,40 +249,111 @@ class ServersPreference @Inject constructor(
200249 return Mapper .from(sharedPreferences.getString(serverKey, null ))
201250 }
202251
252+ /* *
253+ * Adds a server to the unified favorites list.
254+ * The server is stored as a protocol-agnostic identifier:
255+ * - For locations: normalized gateway (with .wg. replaced by .gw.)
256+ * - For hosts: dns_name
257+ * This matches the iOS implementation.
258+ */
203259 fun addFavouriteServer (server : Server ? ) {
204- val openvpnServer = openvpnServersList?.first { it == server }
205- val wireguardServer = wireguardServersList?.first { it == server }
206- if (server == null || openvpnServer == null || wireguardServer == null ) {
260+ if (server == null ) return
261+
262+ val identifier = FavoriteIdentifier .fromServer(server)
263+ val identifiers = unifiedFavouritesList
264+
265+ // Check if already in favorites
266+ if (identifiers.any { it == identifier }) {
207267 return
208268 }
209- val openvpnServers = openvpnFavouritesServersList
210- val wireguardServers = wireguardFavouritesServersList
211- if (! openvpnServers.contains(openvpnServer)) {
212- openvpnServers.add(openvpnServer)
213- preference.serversSharedPreferences.edit()
214- .putString(FAVOURITES_SERVERS_LIST , Mapper .stringFrom(openvpnServers)).apply ()
269+
270+ identifiers.add(identifier)
271+ saveUnifiedFavourites(identifiers)
272+ }
273+
274+ /* *
275+ * Removes a server from the unified favorites list.
276+ */
277+ fun removeFavouriteServer (server : Server ) {
278+ val identifier = FavoriteIdentifier .fromServer(server)
279+ val identifiers = unifiedFavouritesList
280+
281+ identifiers.removeAll { it == identifier }
282+ saveUnifiedFavourites(identifiers)
283+ }
284+
285+ /* *
286+ * Adds a specific host to favorites by dns_name.
287+ */
288+ fun addFavouriteHost (dnsName : String ) {
289+ val identifier = FavoriteIdentifier .forHost(dnsName)
290+ val identifiers = unifiedFavouritesList
291+
292+ if (identifiers.any { it == identifier }) {
293+ return
215294 }
216- if (! wireguardServers.contains(wireguardServer)) {
217- wireguardServers.add(wireguardServer)
218- preference.wireguardServersSharedPreferences.edit()
219- .putString(FAVOURITES_SERVERS_LIST , Mapper .stringFrom(wireguardServers)).apply ()
295+
296+ identifiers.add(identifier)
297+ saveUnifiedFavourites(identifiers)
298+ }
299+
300+ /* *
301+ * Removes a specific host from favorites by dns_name.
302+ */
303+ fun removeFavouriteHost (dnsName : String ) {
304+ val identifier = FavoriteIdentifier .forHost(dnsName)
305+ val identifiers = unifiedFavouritesList
306+
307+ identifiers.removeAll { it == identifier }
308+ saveUnifiedFavourites(identifiers)
309+ }
310+
311+ /* *
312+ * Checks if a server is in the favorites list.
313+ */
314+ fun isFavourite (server : Server ): Boolean {
315+ val identifier = FavoriteIdentifier .fromServer(server)
316+ return unifiedFavouritesList.any { it == identifier }
317+ }
318+
319+ /* *
320+ * Saves the unified favorites list.
321+ */
322+ private fun saveUnifiedFavourites (identifiers : List <FavoriteIdentifier >) {
323+ preference.stickySharedPreferences.edit {
324+ putString(UNIFIED_FAVOURITES_LIST , Mapper .stringFromFavoriteIdentifiers(identifiers))
220325 }
221326 }
222327
223- fun removeFavouriteServer (server : Server ) {
224- val openvpnServer = openvpnServersList?.first { it == server }
225- val wireguardServer = wireguardServersList?.first { it == server }
226- if (openvpnServer == null || wireguardServer == null ) {
328+ /* *
329+ * Migrates old per-protocol favorites to the new unified format.
330+ * This should be called once during app upgrade.
331+ */
332+ fun migrateOldFavouritesToUnified () {
333+ // Check if already migrated
334+ if (preference.stickySharedPreferences.contains(UNIFIED_FAVOURITES_LIST )) {
227335 return
228336 }
229- val openvpnServers = openvpnFavouritesServersList
230- val wireguardServers = wireguardFavouritesServersList
231- openvpnServers.remove(openvpnServer)
232- wireguardServers.remove(wireguardServer)
233- preference.serversSharedPreferences.edit()
234- .putString(FAVOURITES_SERVERS_LIST , Mapper .stringFrom(openvpnServers)).apply ()
235- preference.wireguardServersSharedPreferences.edit()
236- .putString(FAVOURITES_SERVERS_LIST , Mapper .stringFrom(wireguardServers)).apply ()
337+
338+ val identifiers = mutableSetOf<FavoriteIdentifier >()
339+
340+ val oldOpenvpnFavourites = Mapper .serverListFrom(
341+ preference.serversSharedPreferences.getString(FAVOURITES_SERVERS_LIST , null )
342+ )
343+ oldOpenvpnFavourites?.forEach { server ->
344+ identifiers.add(FavoriteIdentifier .fromServer(server))
345+ }
346+
347+ val oldWireguardFavourites = Mapper .serverListFrom(
348+ preference.wireguardServersSharedPreferences.getString(FAVOURITES_SERVERS_LIST , null )
349+ )
350+ oldWireguardFavourites?.forEach { server ->
351+ identifiers.add(FavoriteIdentifier .fromServer(server))
352+ }
353+
354+ if (identifiers.isNotEmpty()) {
355+ saveUnifiedFavourites(identifiers.toList())
356+ }
237357 }
238358
239359 fun addToExcludedServersList (server : Server ? ) {
0 commit comments