Skip to content

Commit

Permalink
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
Add support location translations
Browse files Browse the repository at this point in the history
Rawa committed Aug 9, 2024
1 parent cc9878d commit 1ea0fa2
Showing 11 changed files with 172 additions and 9 deletions.
1 change: 1 addition & 0 deletions android/app/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -339,6 +339,7 @@ dependencies {
implementation(Dependencies.AndroidX.lifecycleViewmodelKtx)
implementation(Dependencies.AndroidX.lifecycleRuntimeCompose)
implementation(Dependencies.Arrow.core)
implementation(Dependencies.Arrow.optics)
implementation(Dependencies.Arrow.resilience)
implementation(Dependencies.Compose.constrainLayout)
implementation(Dependencies.Compose.foundation)
6 changes: 6 additions & 0 deletions android/app/src/main/AndroidManifest.xml
Original file line number Diff line number Diff line change
@@ -106,5 +106,11 @@
<meta-data android:name="android.support.FILE_PROVIDER_PATHS"
android:resource="@xml/provider_paths" />
</provider>
<receiver android:name=".broadcastreceiver.LocaleChangedBroadcastReceiver"
android:exported="false">
<intent-filter>
<action android:name="android.intent.action.LOCALE_CHANGED" />
</intent-filter>
</receiver>
</application>
</manifest>
Original file line number Diff line number Diff line change
@@ -0,0 +1,18 @@
package net.mullvad.mullvadvpn.broadcastreceiver

import android.content.BroadcastReceiver
import android.content.Context
import android.content.Intent
import net.mullvad.mullvadvpn.lib.shared.LocaleRepository
import org.koin.core.component.KoinComponent
import org.koin.core.component.inject

class LocaleChangedBroadcastReceiver : BroadcastReceiver(), KoinComponent {
private val localeRepository by inject<LocaleRepository>()

override fun onReceive(context: Context?, intent: Intent?) {
if (intent?.action == Intent.ACTION_LOCALE_CHANGED) {
localeRepository.refreshLocale()
}
}
}
Original file line number Diff line number Diff line change
@@ -11,6 +11,8 @@ import net.mullvad.mullvadvpn.lib.model.BuildVersion
import net.mullvad.mullvadvpn.lib.shared.AccountRepository
import net.mullvad.mullvadvpn.lib.shared.ConnectionProxy
import net.mullvad.mullvadvpn.lib.shared.DeviceRepository
import net.mullvad.mullvadvpn.lib.shared.LocaleRepository
import net.mullvad.mullvadvpn.lib.shared.RelayLocationTranslationRepository
import net.mullvad.mullvadvpn.lib.shared.VpnPermissionRepository
import org.koin.android.ext.koin.androidContext
import org.koin.core.qualifier.named
@@ -32,5 +34,7 @@ val appModule = module {
single { AccountRepository(get(), get(), MainScope()) }
single { DeviceRepository(get()) }
single { VpnPermissionRepository(androidContext()) }
single { ConnectionProxy(get(), get()) }
single { ConnectionProxy(get(), get(), get()) }
single { LocaleRepository(get()) }
single { RelayLocationTranslationRepository(get(), get(), MainScope()) }
}
Original file line number Diff line number Diff line change
@@ -117,7 +117,7 @@ val uiModule = module {
single { MullvadProblemReport(get()) }
single { RelayOverridesRepository(get()) }
single { CustomListsRepository(get()) }
single { RelayListRepository(get()) }
single { RelayListRepository(get(), get()) }
single { RelayListFilterRepository(get()) }
single { VoucherRepository(get(), get()) }
single { SplitTunnelingRepository(get()) }
Original file line number Diff line number Diff line change
@@ -1,11 +1,15 @@
package net.mullvad.mullvadvpn.repository

import arrow.optics.Every
import arrow.optics.copy
import arrow.optics.dsl.every
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.combine
import kotlinx.coroutines.flow.distinctUntilChanged
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
@@ -17,18 +21,41 @@ import net.mullvad.mullvadvpn.lib.model.RelayItem
import net.mullvad.mullvadvpn.lib.model.RelayItemId
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData
import net.mullvad.mullvadvpn.lib.model.cities
import net.mullvad.mullvadvpn.lib.model.name
import net.mullvad.mullvadvpn.lib.shared.RelayLocationTranslationRepository
import net.mullvad.mullvadvpn.relaylist.findByGeoLocationId

class RelayListRepository(
private val managementService: ManagementService,
private val translationRepository: RelayLocationTranslationRepository,
dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val relayList: StateFlow<List<RelayItem.Location.Country>> =
managementService.relayCountries.stateIn(
CoroutineScope(dispatcher),
SharingStarted.WhileSubscribed(),
emptyList()
)
combine(managementService.relayCountries, translationRepository.translations) {
countries,
translations ->
countries.translateRelay(translations).sortedBy { it.name.lowercase() }
}
.stateIn(CoroutineScope(dispatcher), SharingStarted.WhileSubscribed(), emptyList())

private fun List<RelayItem.Location.Country>.translateRelay(
translations: Map<String, String>
): List<RelayItem.Location.Country> {
if (translations.isEmpty()) {
return this
}

return Every.list<RelayItem.Location.Country>().modify(this) {
it.copy<RelayItem.Location.Country> {
RelayItem.Location.Country.name set translations.getOrDefault(it.name, it.name)
RelayItem.Location.Country.cities.every(Every.list()).name transform
{ cityName ->
translations.getOrDefault(cityName, cityName)
}
}
}
}

val wireguardEndpointData: StateFlow<WireguardEndpointData> =
managementService.wireguardEndpointData.stateIn(
Original file line number Diff line number Diff line change
@@ -1,7 +1,9 @@
package net.mullvad.mullvadvpn.lib.model

import arrow.optics.optics
import java.net.InetAddress

@optics
data class GeoIpLocation(
val ipv4: InetAddress?,
val ipv6: InetAddress?,
@@ -10,4 +12,6 @@ data class GeoIpLocation(
val latitude: Double,
val longitude: Double,
val hostname: String?,
)
) {
companion object
}
1 change: 1 addition & 0 deletions android/lib/shared/build.gradle.kts
Original file line number Diff line number Diff line change
@@ -27,6 +27,7 @@ android {
}

dependencies {
implementation(project(Dependencies.Mullvad.resourceLib))
implementation(project(Dependencies.Mullvad.commonLib))
implementation(project(Dependencies.Mullvad.daemonGrpc))
implementation(project(Dependencies.Mullvad.modelLib))
Original file line number Diff line number Diff line change
@@ -3,14 +3,38 @@ package net.mullvad.mullvadvpn.lib.shared
import arrow.core.Either
import arrow.core.raise.either
import arrow.core.raise.ensure
import kotlinx.coroutines.flow.combine
import mullvad_daemon.management_interface.location
import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService
import net.mullvad.mullvadvpn.lib.model.ConnectError
import net.mullvad.mullvadvpn.lib.model.GeoIpLocation
import net.mullvad.mullvadvpn.lib.model.TunnelState
import net.mullvad.mullvadvpn.lib.model.location

class ConnectionProxy(
private val managementService: ManagementService,
private val translationRepository: RelayLocationTranslationRepository,
private val vpnPermissionRepository: VpnPermissionRepository
) {
val tunnelState = managementService.tunnelState
val tunnelState =
combine(managementService.tunnelState, translationRepository.translations) {
tunnelState,
translations ->
tunnelState.translateLocations(translations)
}

private fun TunnelState.translateLocations(translations: Map<String, String>): TunnelState {
return when (this) {
is TunnelState.Connecting -> copy(location = location?.translate(translations))
is TunnelState.Disconnected -> copy(location = location?.translate(translations))
is TunnelState.Disconnecting -> this
is TunnelState.Error -> this
is TunnelState.Connected -> copy(location = location?.translate(translations))
}
}

private fun GeoIpLocation.translate(translations: Map<String, String>): GeoIpLocation =
copy(city = translations[city] ?: city, country = translations[country] ?: country)

suspend fun connect(): Either<ConnectError, Boolean> = either {
ensure(vpnPermissionRepository.hasVpnPermission()) { ConnectError.NoVpnPermission }
Original file line number Diff line number Diff line change
@@ -0,0 +1,19 @@
package net.mullvad.mullvadvpn.lib.shared

import android.content.res.Resources
import co.touchlab.kermit.Logger
import java.util.Locale
import kotlin.also
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.StateFlow

class LocaleRepository(val resources: Resources) {
private val _currentLocale = MutableStateFlow(getLocale())
val currentLocale: StateFlow<Locale?> = _currentLocale

private fun getLocale(): Locale? = resources.configuration.locales.get(0)

fun refreshLocale() {
_currentLocale.value = getLocale().also { Logger.d("New locale: $it") }
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,59 @@
package net.mullvad.mullvadvpn.lib.shared

import android.content.Context
import android.content.res.XmlResourceParser
import co.touchlab.kermit.Logger
import java.util.Locale
import kotlin.collections.set
import kotlin.collections.toMap
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.withContext

typealias Translations = Map<String, String>

class RelayLocationTranslationRepository(
val context: Context,
val localeRepository: LocaleRepository,
externalScope: CoroutineScope = MainScope(),
val dispatcher: CoroutineDispatcher = Dispatchers.IO
) {
val translations: StateFlow<Translations> =
localeRepository.currentLocale
.map { loadTranslations(it) }
.stateIn(externalScope, SharingStarted.Eagerly, emptyMap())

private suspend fun loadTranslations(locale: Locale?): Translations =
withContext(dispatcher) {
Logger.d("Updating translations based $locale")
if (locale == null || locale.language == DEFAULT_LANGUAGE) emptyMap()
else {
// Load current translations
val xml = context.resources.getXml(R.xml.relay_locations)
loadRelayTranslation(xml)
}
}

private fun loadRelayTranslation(xml: XmlResourceParser): Map<String, String> {
val translation = mutableMapOf<String, String>()
while (xml.eventType != XmlResourceParser.END_DOCUMENT) {
if (xml.eventType == XmlResourceParser.START_TAG && xml.name == "string") {
val key = xml.getAttributeValue(null, "name")
val value = xml.nextText()
translation[key] = value
}
xml.next()
}
return translation.toMap()
}

companion object {
private const val DEFAULT_LANGUAGE = "en"
}
}

0 comments on commit 1ea0fa2

Please sign in to comment.