From 5ebc19f1157f6122ab8ff80deec0e29a444a9e8c Mon Sep 17 00:00:00 2001 From: Zane Schepke Date: Fri, 26 Apr 2024 13:27:17 -0400 Subject: [PATCH] fix: country caching Add hilt performance improvements Fixes issue where countries were not caching properly on app startup. --- .../main/java/net/nymtech/nymvpn/NymVpn.kt | 11 ++ .../nymtech/nymvpn/data/GatewayRepository.kt | 2 +- .../datastore/DataStoreGatewayRepository.kt | 2 +- .../datastore/DataStoreSettingsRepository.kt | 9 +- .../net/nymtech/nymvpn/data/model/Gateways.kt | 2 +- .../nymtech/nymvpn/receiver/BootReceiver.kt | 9 +- .../nymvpn/service/AlwaysOnVpnService.kt | 12 +- .../nymvpn/service/tile/VpnQuickTile.kt | 18 +- .../net/nymtech/nymvpn/ui/AppViewModel.kt | 12 +- .../net/nymtech/nymvpn/ui/SplashActivity.kt | 44 +++-- .../nymvpn/ui/screens/hop/HopViewModel.kt | 4 +- .../nymvpn/ui/screens/main/MainScreen.kt | 37 ++-- .../nymvpn/ui/screens/main/MainViewModel.kt | 26 ++- .../screens/settings/login/LoginViewModel.kt | 8 +- buildSrc/src/main/kotlin/Constants.kt | 4 +- .../metadata/android/en-US/changelogs/17.txt | 3 + .../java/net/nymtech/vpn/model/Country.kt | 4 +- .../nymtech/vpn/nym_vpn_lib/nym_vpn_lib.kt | 163 +++++++++--------- 18 files changed, 203 insertions(+), 167 deletions(-) create mode 100644 fastlane/metadata/android/en-US/changelogs/17.txt diff --git a/app/src/main/java/net/nymtech/nymvpn/NymVpn.kt b/app/src/main/java/net/nymtech/nymvpn/NymVpn.kt index 2adc340..4145a0e 100644 --- a/app/src/main/java/net/nymtech/nymvpn/NymVpn.kt +++ b/app/src/main/java/net/nymtech/nymvpn/NymVpn.kt @@ -7,6 +7,8 @@ import android.service.quicksettings.TileService import androidx.compose.ui.unit.Dp import androidx.compose.ui.unit.TextUnit import dagger.hilt.android.HiltAndroidApp +import kotlinx.coroutines.MainScope +import kotlinx.coroutines.cancel import net.nymtech.nymvpn.service.tile.VpnQuickTile import net.nymtech.nymvpn.util.actionBarSize import net.nymtech.nymvpn.util.log.DebugTree @@ -16,6 +18,7 @@ import timber.log.Timber @HiltAndroidApp class NymVpn : Application() { + override fun onCreate() { super.onCreate() instance = this @@ -27,7 +30,15 @@ class NymVpn : Application() { } } + override fun onLowMemory() { + super.onLowMemory() + applicationScope.cancel("onLowMemory() called by system") + applicationScope = MainScope() + } + companion object { + + var applicationScope = MainScope() lateinit var instance: NymVpn private set diff --git a/app/src/main/java/net/nymtech/nymvpn/data/GatewayRepository.kt b/app/src/main/java/net/nymtech/nymvpn/data/GatewayRepository.kt index 2196998..d6c3470 100644 --- a/app/src/main/java/net/nymtech/nymvpn/data/GatewayRepository.kt +++ b/app/src/main/java/net/nymtech/nymvpn/data/GatewayRepository.kt @@ -6,7 +6,7 @@ import net.nymtech.vpn.model.Country interface GatewayRepository { - suspend fun getLowLatencyCountry(): Country + suspend fun getLowLatencyCountry(): Country? suspend fun setLowLatencyCountry(country: Country) diff --git a/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreGatewayRepository.kt b/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreGatewayRepository.kt index de710f2..b34f5d5 100644 --- a/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreGatewayRepository.kt +++ b/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreGatewayRepository.kt @@ -16,7 +16,7 @@ class DataStoreGatewayRepository(private val dataStoreManager: DataStoreManager) val EXIT_COUNTRIES = stringPreferencesKey("EXIT_COUNTRIES") } - override suspend fun getLowLatencyCountry(): Country { + override suspend fun getLowLatencyCountry(): Country? { val country = dataStoreManager.getFromStore(LOW_LATENCY_COUNTRY) return Country.from(country) } diff --git a/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt b/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt index 7df2648..bc477df 100644 --- a/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt +++ b/app/src/main/java/net/nymtech/nymvpn/data/datastore/DataStoreSettingsRepository.kt @@ -14,6 +14,7 @@ import timber.log.Timber class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager) : SettingsRepository { + private val default = Country(isDefault = true) companion object { val FIRST_HOP_COUNTRY = stringPreferencesKey("FIRST_HOP_COUNTRY") val LAST_HOP_COUNTRY = stringPreferencesKey("LAST_HOP_COUNTRY") @@ -60,7 +61,7 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager override suspend fun getFirstHopCountry(): Country { val country = dataStoreManager.getFromStore(FIRST_HOP_COUNTRY) - return Country.from(country) + return Country.from(country) ?: default } override suspend fun setFirstHopCountry(country: Country) { @@ -73,7 +74,7 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager override suspend fun getLastHopCountry(): Country { val country = dataStoreManager.getFromStore(LAST_HOP_COUNTRY) - return Country.from(country) + return Country.from(country) ?: default } override suspend fun setLastHopCountry(country: Country) { @@ -146,8 +147,8 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager pref[FIRST_HOP_SELECTION] ?: Settings.FIRST_HOP_SELECTION_DEFAULT, isAnalyticsShown = pref[ANALYTICS_SHOWN] ?: Settings.ANALYTICS_SHOWN_DEFAULT, - firstHopCountry = Country.from(pref[FIRST_HOP_COUNTRY]), - lastHopCountry = Country.from(pref[LAST_HOP_COUNTRY]), + firstHopCountry = Country.from(pref[FIRST_HOP_COUNTRY]) ?: default, + lastHopCountry = Country.from(pref[LAST_HOP_COUNTRY]) ?: default, ) } catch (e: IllegalArgumentException) { Timber.e(e) diff --git a/app/src/main/java/net/nymtech/nymvpn/data/model/Gateways.kt b/app/src/main/java/net/nymtech/nymvpn/data/model/Gateways.kt index fb801ea..13e3807 100644 --- a/app/src/main/java/net/nymtech/nymvpn/data/model/Gateways.kt +++ b/app/src/main/java/net/nymtech/nymvpn/data/model/Gateways.kt @@ -3,7 +3,7 @@ package net.nymtech.nymvpn.data.model import net.nymtech.vpn.model.Country data class Gateways( - val lowLatencyCountry: Country = Country(), + val lowLatencyCountry: Country? = null, val entryCountries: Set = emptySet(), val exitCountries: Set = emptySet(), ) diff --git a/app/src/main/java/net/nymtech/nymvpn/receiver/BootReceiver.kt b/app/src/main/java/net/nymtech/nymvpn/receiver/BootReceiver.kt index 3a93ee7..996214e 100644 --- a/app/src/main/java/net/nymtech/nymvpn/receiver/BootReceiver.kt +++ b/app/src/main/java/net/nymtech/nymvpn/receiver/BootReceiver.kt @@ -12,6 +12,7 @@ import net.nymtech.vpn.VpnClient import net.nymtech.vpn.util.InvalidCredentialException import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider @AndroidEntryPoint class BootReceiver : BroadcastReceiver() { @@ -20,24 +21,24 @@ class BootReceiver : BroadcastReceiver() { lateinit var settingsRepository: SettingsRepository @Inject - lateinit var secretsRepository: SecretsRepository + lateinit var secretsRepository: Provider @Inject - lateinit var vpnClient: VpnClient + lateinit var vpnClient: Provider override fun onReceive(context: Context?, intent: Intent?) = goAsync { if (Intent.ACTION_BOOT_COMPLETED != intent?.action) return@goAsync if (settingsRepository.isAutoStartEnabled()) { val entryCountry = settingsRepository.getFirstHopCountry() val exitCountry = settingsRepository.getLastHopCountry() - val credential = secretsRepository.getCredential() + val credential = secretsRepository.get().getCredential() val mode = settingsRepository.getVpnMode() if (credential != null) { context?.let { context -> val entry = entryCountry.toEntryPoint() val exit = exitCountry.toExitPoint() try { - vpnClient.apply { + vpnClient.get().apply { this.mode = mode this.exitPoint = exit this.entryPoint = entry diff --git a/app/src/main/java/net/nymtech/nymvpn/service/AlwaysOnVpnService.kt b/app/src/main/java/net/nymtech/nymvpn/service/AlwaysOnVpnService.kt index fc7fbc9..a4d9a89 100644 --- a/app/src/main/java/net/nymtech/nymvpn/service/AlwaysOnVpnService.kt +++ b/app/src/main/java/net/nymtech/nymvpn/service/AlwaysOnVpnService.kt @@ -5,6 +5,7 @@ import android.os.IBinder import androidx.lifecycle.LifecycleService import androidx.lifecycle.lifecycleScope import dagger.hilt.android.AndroidEntryPoint +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.nymtech.nymvpn.NymVpn import net.nymtech.nymvpn.data.SecretsRepository @@ -13,6 +14,7 @@ import net.nymtech.vpn.VpnClient import net.nymtech.vpn.util.InvalidCredentialException import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider @AndroidEntryPoint class AlwaysOnVpnService : LifecycleService() { @@ -21,10 +23,10 @@ class AlwaysOnVpnService : LifecycleService() { lateinit var settingsRepository: SettingsRepository @Inject - lateinit var secretsRepository: SecretsRepository + lateinit var secretsRepository: Provider @Inject - lateinit var vpnClient: VpnClient + lateinit var vpnClient: Provider override fun onBind(intent: Intent): IBinder? { super.onBind(intent) @@ -35,8 +37,8 @@ class AlwaysOnVpnService : LifecycleService() { override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { if (intent == null || intent.component == null || intent.component!!.packageName != packageName) { Timber.i("Always-on VPN requested start") - lifecycleScope.launch { - val credential = secretsRepository.getCredential() + lifecycleScope.launch(Dispatchers.IO) { + val credential = secretsRepository.get().getCredential() if (credential != null) { val entryCountry = settingsRepository.getFirstHopCountry() val exitCountry = settingsRepository.getLastHopCountry() @@ -44,7 +46,7 @@ class AlwaysOnVpnService : LifecycleService() { val entry = entryCountry.toEntryPoint() val exit = exitCountry.toExitPoint() try { - vpnClient.apply { + vpnClient.get().apply { this.mode = mode this.entryPoint = entry this.exitPoint = exit diff --git a/app/src/main/java/net/nymtech/nymvpn/service/tile/VpnQuickTile.kt b/app/src/main/java/net/nymtech/nymvpn/service/tile/VpnQuickTile.kt index bb23bac..e01cd5a 100644 --- a/app/src/main/java/net/nymtech/nymvpn/service/tile/VpnQuickTile.kt +++ b/app/src/main/java/net/nymtech/nymvpn/service/tile/VpnQuickTile.kt @@ -8,6 +8,7 @@ import kotlinx.coroutines.CoroutineScope import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.cancel import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext import net.nymtech.nymvpn.R import net.nymtech.nymvpn.data.SecretsRepository import net.nymtech.nymvpn.data.SettingsRepository @@ -17,17 +18,18 @@ import net.nymtech.vpn.model.VpnState import net.nymtech.vpn.util.InvalidCredentialException import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider @AndroidEntryPoint class VpnQuickTile : TileService() { @Inject - lateinit var secretsRepository: SecretsRepository + lateinit var secretsRepository: Provider @Inject lateinit var settingsRepository: SettingsRepository @Inject - lateinit var vpnClient: VpnClient + lateinit var vpnClient: Provider private val scope = CoroutineScope(Dispatchers.Main) @@ -36,7 +38,7 @@ class VpnQuickTile : TileService() { Timber.d("Quick tile listening called") setTileText() scope.launch { - vpnClient.stateFlow.collect { + vpnClient.get().stateFlow.collect { when (it.vpnState) { VpnState.Up -> { setActive() @@ -79,19 +81,21 @@ class VpnQuickTile : TileService() { super.onClick() setTileText() unlockAndRun { - when (vpnClient.getState().vpnState) { + when (vpnClient.get().getState().vpnState) { VpnState.Up -> { scope.launch { setTileDescription(this@VpnQuickTile.getString(R.string.disconnecting)) - vpnClient.stop(this@VpnQuickTile, true) + vpnClient.get().stop(this@VpnQuickTile, true) } } VpnState.Down -> { scope.launch { - val credential = secretsRepository.getCredential() + val credential = withContext(Dispatchers.IO) { + secretsRepository.get().getCredential() + } if (credential != null) { try { - vpnClient.apply { + vpnClient.get().apply { this.mode = settingsRepository.getVpnMode() this.exitPoint = settingsRepository.getLastHopCountry().toExitPoint() this.entryPoint = settingsRepository.getFirstHopCountry().toEntryPoint() diff --git a/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt b/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt index 516a803..8401b4a 100644 --- a/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt +++ b/app/src/main/java/net/nymtech/nymvpn/ui/AppViewModel.kt @@ -27,14 +27,17 @@ import net.nymtech.logcathelper.model.LogMessage import net.nymtech.nymvpn.R import net.nymtech.nymvpn.data.GatewayRepository import net.nymtech.nymvpn.data.SettingsRepository +import net.nymtech.nymvpn.service.country.CountryCacheService import net.nymtech.nymvpn.util.Constants import net.nymtech.nymvpn.util.FileUtils import net.nymtech.nymvpn.util.log.NymLibException import net.nymtech.vpn.NymApi import net.nymtech.vpn.VpnClient +import net.nymtech.vpn.model.Country import timber.log.Timber import java.time.Instant import javax.inject.Inject +import javax.inject.Provider @HiltViewModel class AppViewModel @@ -42,7 +45,8 @@ class AppViewModel constructor( private val settingsRepository: SettingsRepository, private val gatewayRepository: GatewayRepository, - private val vpnClient: VpnClient, + private val countryCacheService: CountryCacheService, + private val vpnClient: Provider, private val nymApi: NymApi, ) : ViewModel() { @@ -52,7 +56,7 @@ constructor( private val logsBuffer = mutableListOf() val uiState = - combine(_uiState, settingsRepository.settingsFlow, vpnClient.stateFlow) { state, settings, vpnState -> + combine(_uiState, settingsRepository.settingsFlow, vpnClient.get().stateFlow) { state, settings, vpnState -> AppUiState( false, state.snackbarMessage, @@ -112,7 +116,7 @@ constructor( settingsRepository.setAnalyticsShown(true) } - fun onEntryLocationSelected(selected: Boolean) = viewModelScope.launch { + fun onEntryLocationSelected(selected: Boolean) = viewModelScope.launch(Dispatchers.IO) { settingsRepository.setFirstHopSelection(selected) setFirstHopToLowLatency() } @@ -131,7 +135,7 @@ constructor( }.onFailure { Timber.e(it) }.onSuccess { - settingsRepository.setFirstHopCountry(it) + settingsRepository.setFirstHopCountry(it ?: Country(isDefault = true)) } } diff --git a/app/src/main/java/net/nymtech/nymvpn/ui/SplashActivity.kt b/app/src/main/java/net/nymtech/nymvpn/ui/SplashActivity.kt index bce48bd..3adc39f 100644 --- a/app/src/main/java/net/nymtech/nymvpn/ui/SplashActivity.kt +++ b/app/src/main/java/net/nymtech/nymvpn/ui/SplashActivity.kt @@ -12,54 +12,62 @@ import androidx.lifecycle.repeatOnLifecycle import dagger.hilt.android.AndroidEntryPoint import io.sentry.android.core.SentryAndroid import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll import kotlinx.coroutines.launch import net.nymtech.nymvpn.BuildConfig -import net.nymtech.nymvpn.data.GatewayRepository +import net.nymtech.nymvpn.NymVpn import net.nymtech.nymvpn.data.SettingsRepository import net.nymtech.nymvpn.service.country.CountryCacheService import net.nymtech.nymvpn.util.Constants -import net.nymtech.vpn.NymApi +import timber.log.Timber import javax.inject.Inject @SuppressLint("CustomSplashScreen") @AndroidEntryPoint class SplashActivity : ComponentActivity() { - @Inject - lateinit var gatewayRepository: GatewayRepository - @Inject lateinit var countryCacheService: CountryCacheService - @Inject - lateinit var nymApi: NymApi - @Inject lateinit var settingsRepository: SettingsRepository + override fun onCreate(savedInstanceState: Bundle?) { if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.S) { val splashScreen = installSplashScreen() splashScreen.setKeepOnScreenCondition { true } } super.onCreate(savedInstanceState) - lifecycleScope.launch { + lifecycleScope.launch(Dispatchers.IO) { repeatOnLifecycle(Lifecycle.State.CREATED) { // init data settingsRepository.init() + NymVpn.applicationScope.launch(Dispatchers.IO) { + listOf( + async { + Timber.d("Updating exit country cache") + countryCacheService.updateExitCountriesCache() + Timber.d("Exit countries updated") + }, + async { + Timber.d("Updating entry country cache") + countryCacheService.updateEntryCountriesCache() + Timber.d("Entry countries updated") + }, + async { + Timber.d("Updating low latency country cache") + countryCacheService.updateLowLatencyEntryCountryCache() + Timber.d("Low latency country updated") + }, + ).awaitAll() + } + configureSentry() val isAnalyticsShown = settingsRepository.isAnalyticsShown() - launch(Dispatchers.IO) { - countryCacheService.updateEntryCountriesCache() - countryCacheService.updateExitCountriesCache() - } - - launch(Dispatchers.IO) { - countryCacheService.updateLowLatencyEntryCountryCache() - } - val intent = Intent(this@SplashActivity, MainActivity::class.java).apply { putExtra(IS_ANALYTICS_SHOWN_INTENT_KEY, isAnalyticsShown) } diff --git a/app/src/main/java/net/nymtech/nymvpn/ui/screens/hop/HopViewModel.kt b/app/src/main/java/net/nymtech/nymvpn/ui/screens/hop/HopViewModel.kt index 3e059a2..069d48a 100644 --- a/app/src/main/java/net/nymtech/nymvpn/ui/screens/hop/HopViewModel.kt +++ b/app/src/main/java/net/nymtech/nymvpn/ui/screens/hop/HopViewModel.kt @@ -78,7 +78,7 @@ constructor( } } - private fun setSelectedCountry() = viewModelScope.launch { + private fun setSelectedCountry() = viewModelScope.launch(Dispatchers.IO) { val selectedCountry = when (_uiState.value.hopType) { HopType.FIRST -> settingsRepository.getFirstHopCountry() @@ -90,7 +90,7 @@ constructor( ) } - fun onSelected(country: Country) = viewModelScope.launch { + fun onSelected(country: Country) = viewModelScope.launch(Dispatchers.IO) { when (_uiState.value.hopType) { HopType.FIRST -> settingsRepository.setFirstHopCountry(country) HopType.LAST -> settingsRepository.setLastHopCountry(country) diff --git a/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt b/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt index a9ffc7b..835a150 100644 --- a/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt +++ b/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainScreen.kt @@ -24,6 +24,7 @@ import androidx.compose.runtime.Composable import androidx.compose.runtime.getValue import androidx.compose.runtime.mutableStateOf import androidx.compose.runtime.remember +import androidx.compose.runtime.rememberCoroutineScope import androidx.compose.runtime.saveable.rememberSaveable import androidx.compose.runtime.setValue import androidx.compose.ui.Alignment @@ -39,6 +40,7 @@ import androidx.navigation.NavController import com.google.accompanist.permissions.ExperimentalPermissionsApi import com.google.accompanist.permissions.isGranted import com.google.accompanist.permissions.rememberPermissionState +import kotlinx.coroutines.launch import net.nymtech.nymvpn.R import net.nymtech.nymvpn.ui.AppViewModel import net.nymtech.nymvpn.ui.NavItem @@ -64,6 +66,7 @@ import net.nymtech.vpn.model.VpnMode fun MainScreen(navController: NavController, appViewModel: AppViewModel, viewModel: MainViewModel = hiltViewModel()) { val uiState by viewModel.uiState.collectAsStateWithLifecycle() val context = LocalContext.current + val scope = rememberCoroutineScope() val notificationPermissionState = if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.TIRAMISU) { @@ -227,23 +230,25 @@ fun MainScreen(navController: NavController, appViewModel: AppViewModel, viewMod MainStyledButton( testTag = Constants.CONNECT_TEST_TAG, onClick = { - if (viewModel.isCredentialImported()) { - if (notificationPermissionState != null && - !notificationPermissionState.status.isGranted - ) { - return@MainStyledButton notificationPermissionState.launchPermissionRequest() + scope.launch { + if (viewModel.isCredentialImported()) { + if (notificationPermissionState != null && + !notificationPermissionState.status.isGranted + ) { + return@launch notificationPermissionState.launchPermissionRequest() + } + if (vpnIntent != null) { + return@launch vpnActivityResultState.launch( + vpnIntent, + ) + } + if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !appViewModel.isAlarmPermissionGranted(context)) { + return@launch appViewModel.requestAlarmPermission(context) + } + viewModel.onConnect() + } else { + navController.navigate(NavItem.Settings.Login.route) } - if (vpnIntent != null) { - return@MainStyledButton vpnActivityResultState.launch( - vpnIntent, - ) - } - if (Build.VERSION.SDK_INT >= Build.VERSION_CODES.UPSIDE_DOWN_CAKE && !appViewModel.isAlarmPermissionGranted(context)) { - return@MainStyledButton appViewModel.requestAlarmPermission(context) - } - viewModel.onConnect() - } else { - navController.navigate(NavItem.Settings.Login.route) } }, content = { diff --git a/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainViewModel.kt b/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainViewModel.kt index 2dfb2a3..4c6c10a 100644 --- a/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainViewModel.kt +++ b/app/src/main/java/net/nymtech/nymvpn/ui/screens/main/MainViewModel.kt @@ -8,10 +8,9 @@ import kotlinx.coroutines.flow.SharingStarted import kotlinx.coroutines.flow.combine import kotlinx.coroutines.flow.stateIn import kotlinx.coroutines.launch -import kotlinx.coroutines.runBlocking +import kotlinx.coroutines.withContext import net.nymtech.nymvpn.NymVpn import net.nymtech.nymvpn.R -import net.nymtech.nymvpn.data.GatewayRepository import net.nymtech.nymvpn.data.SecretsRepository import net.nymtech.nymvpn.data.SettingsRepository import net.nymtech.nymvpn.ui.model.ConnectionState @@ -25,22 +24,21 @@ import net.nymtech.vpn.model.VpnMode import net.nymtech.vpn.util.InvalidCredentialException import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider @HiltViewModel class MainViewModel @Inject constructor( - private val gatewayRepository: GatewayRepository, private val settingsRepository: SettingsRepository, - private val secretsRepository: SecretsRepository, - private val vpnClient: VpnClient, + private val secretsRepository: Provider, + private val vpnClient: Provider, ) : ViewModel() { val uiState = combine( - gatewayRepository.gatewayFlow, settingsRepository.settingsFlow, - vpnClient.stateFlow, - ) { gateways, settings, clientState -> + vpnClient.get().stateFlow, + ) { settings, clientState -> val connectionTime = clientState.statistics.connectionSeconds?.let { NumberUtils.convertSecondsToTimeString( @@ -87,14 +85,14 @@ constructor( NymVpn.requestTileServiceStateUpdate() } - fun isCredentialImported(): Boolean { - return runBlocking { - secretsRepository.getCredential() != null + suspend fun isCredentialImported(): Boolean { + return withContext(Dispatchers.IO) { + secretsRepository.get().getCredential() != null } } fun onConnect() = viewModelScope.launch(Dispatchers.IO) { - val credential = secretsRepository.getCredential() + val credential = secretsRepository.get().getCredential() if (credential != null) { val entryCountry = settingsRepository.getFirstHopCountry() val exitCountry = settingsRepository.getLastHopCountry() @@ -102,7 +100,7 @@ constructor( val entry = entryCountry.toEntryPoint() val exit = exitCountry.toExitPoint() try { - vpnClient.apply { + vpnClient.get().apply { this.exitPoint = exit this.entryPoint = entry this.mode = mode @@ -115,7 +113,7 @@ constructor( } fun onDisconnect() = viewModelScope.launch { - vpnClient.stop(NymVpn.instance) + vpnClient.get().stop(NymVpn.instance) NymVpn.requestTileServiceStateUpdate() } } diff --git a/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/login/LoginViewModel.kt b/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/login/LoginViewModel.kt index 8d16282..042dc60 100644 --- a/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/login/LoginViewModel.kt +++ b/app/src/main/java/net/nymtech/nymvpn/ui/screens/settings/login/LoginViewModel.kt @@ -3,6 +3,7 @@ package net.nymtech.nymvpn.ui.screens.settings.login import androidx.lifecycle.ViewModel import androidx.lifecycle.viewModelScope import dagger.hilt.android.lifecycle.HiltViewModel +import kotlinx.coroutines.Dispatchers import kotlinx.coroutines.launch import net.nymtech.nymvpn.data.SecretsRepository import net.nymtech.nymvpn.util.Event @@ -10,12 +11,13 @@ import net.nymtech.nymvpn.util.Result import net.nymtech.vpn.NymVpnClient import timber.log.Timber import javax.inject.Inject +import javax.inject.Provider @HiltViewModel class LoginViewModel @Inject constructor( - private val secretsRepository: SecretsRepository, + private val secretsRepository: Provider, ) : ViewModel() { fun onImportCredential(credential: String): Result { return if (NymVpnClient.validateCredential(credential).isSuccess) { @@ -27,7 +29,7 @@ constructor( } } - private fun saveCredential(credential: String) = viewModelScope.launch { - secretsRepository.saveCredential(credential) + private fun saveCredential(credential: String) = viewModelScope.launch(Dispatchers.IO) { + secretsRepository.get().saveCredential(credential) } } diff --git a/buildSrc/src/main/kotlin/Constants.kt b/buildSrc/src/main/kotlin/Constants.kt index 1d4757d..441b715 100644 --- a/buildSrc/src/main/kotlin/Constants.kt +++ b/buildSrc/src/main/kotlin/Constants.kt @@ -1,8 +1,8 @@ import org.gradle.api.JavaVersion object Constants { - const val VERSION_NAME = "v0.1.4" - const val VERSION_CODE = 16 + const val VERSION_NAME = "v0.1.5" + const val VERSION_CODE = 17 const val TARGET_SDK = 34 const val COMPILE_SDK = 34 const val MIN_SDK = 24 diff --git a/fastlane/metadata/android/en-US/changelogs/17.txt b/fastlane/metadata/android/en-US/changelogs/17.txt new file mode 100644 index 0000000..f985d1a --- /dev/null +++ b/fastlane/metadata/android/en-US/changelogs/17.txt @@ -0,0 +1,3 @@ +What's new: +- Fix country caching issue +- Hilt performance improvements diff --git a/nym_vpn_client/src/main/java/net/nymtech/vpn/model/Country.kt b/nym_vpn_client/src/main/java/net/nymtech/vpn/model/Country.kt index 06a1ef2..68f09b8 100644 --- a/nym_vpn_client/src/main/java/net/nymtech/vpn/model/Country.kt +++ b/nym_vpn_client/src/main/java/net/nymtech/vpn/model/Country.kt @@ -33,8 +33,8 @@ data class Country( } companion object { - fun from(string: String?): Country { - return string?.let { Json.decodeFromString(string) } ?: Country() + fun from(string: String?): Country? { + return string?.let { Json.decodeFromString(string) } } fun fromCollectionString(string: String?): Set { diff --git a/nym_vpn_client/src/main/java/net/nymtech/vpn/nym_vpn_lib/nym_vpn_lib.kt b/nym_vpn_client/src/main/java/net/nymtech/vpn/nym_vpn_lib/nym_vpn_lib.kt index 2b9fa55..37385cd 100644 --- a/nym_vpn_client/src/main/java/net/nymtech/vpn/nym_vpn_lib/nym_vpn_lib.kt +++ b/nym_vpn_client/src/main/java/net/nymtech/vpn/nym_vpn_lib/nym_vpn_lib.kt @@ -18,20 +18,17 @@ package nym_vpn_lib; // helpers directly inline like we're doing here. import com.sun.jna.Library -import com.sun.jna.IntegerType import com.sun.jna.Native import com.sun.jna.Pointer import com.sun.jna.Structure -import com.sun.jna.Callback import com.sun.jna.ptr.* +import java.net.URI +import java.net.URL import java.nio.ByteBuffer import java.nio.ByteOrder import java.nio.CharBuffer import java.nio.charset.CodingErrorAction -import java.util.concurrent.atomic.AtomicLong import java.util.concurrent.ConcurrentHashMap -import java.net.URI -import java.net.URL // This is a helper for safely working with byte buffers returned from the Rust code. // A rust-owned buffer is represented by its capacity, its current length, and a @@ -716,28 +713,28 @@ internal interface UniffiLib : Library { uniffiCheckApiChecksums(lib) } } - + } - fun uniffi_nym_vpn_lib_fn_func_checkcredential(`credential`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun uniffi_nym_vpn_lib_fn_func_checkcredential(`credential`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit - fun uniffi_nym_vpn_lib_fn_func_getgatewaycountries(`apiUrl`: RustBuffer.ByValue,`explorerUrl`: RustBuffer.ByValue,`harbourMasterUrl`: RustBuffer.ByValue,`exitOnly`: Byte,uniffi_out_err: UniffiRustCallStatus, + fun uniffi_nym_vpn_lib_fn_func_getgatewaycountries(`apiUrl`: RustBuffer.ByValue,`explorerUrl`: RustBuffer.ByValue,`harbourMasterUrl`: RustBuffer.ByValue,`exitOnly`: Byte,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue - fun uniffi_nym_vpn_lib_fn_func_getlowlatencyentrycountry(`apiUrl`: RustBuffer.ByValue,`explorerUrl`: RustBuffer.ByValue,`harbourMasterUrl`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun uniffi_nym_vpn_lib_fn_func_getlowlatencyentrycountry(`apiUrl`: RustBuffer.ByValue,`explorerUrl`: RustBuffer.ByValue,`harbourMasterUrl`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue - fun uniffi_nym_vpn_lib_fn_func_importcredential(`credential`: RustBuffer.ByValue,`path`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun uniffi_nym_vpn_lib_fn_func_importcredential(`credential`: RustBuffer.ByValue,`path`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit - fun uniffi_nym_vpn_lib_fn_func_runvpn(`config`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun uniffi_nym_vpn_lib_fn_func_runvpn(`config`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit - fun uniffi_nym_vpn_lib_fn_func_stopvpn(uniffi_out_err: UniffiRustCallStatus, + fun uniffi_nym_vpn_lib_fn_func_stopvpn(uniffi_out_err: UniffiRustCallStatus, ): Unit - fun ffi_nym_vpn_lib_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rustbuffer_alloc(`size`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue - fun ffi_nym_vpn_lib_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rustbuffer_from_bytes(`bytes`: ForeignBytes.ByValue,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue - fun ffi_nym_vpn_lib_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rustbuffer_free(`buf`: RustBuffer.ByValue,uniffi_out_err: UniffiRustCallStatus, ): Unit - fun ffi_nym_vpn_lib_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rustbuffer_reserve(`buf`: RustBuffer.ByValue,`additional`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue fun ffi_nym_vpn_lib_rust_future_poll_u8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -745,7 +742,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_u8(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_u8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Byte fun ffi_nym_vpn_lib_rust_future_poll_i8(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -753,7 +750,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_i8(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_i8(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Byte fun ffi_nym_vpn_lib_rust_future_poll_u16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -761,7 +758,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_u16(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_u16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Short fun ffi_nym_vpn_lib_rust_future_poll_i16(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -769,7 +766,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_i16(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_i16(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Short fun ffi_nym_vpn_lib_rust_future_poll_u32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -777,7 +774,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_u32(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_u32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Int fun ffi_nym_vpn_lib_rust_future_poll_i32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -785,7 +782,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_i32(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_i32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Int fun ffi_nym_vpn_lib_rust_future_poll_u64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -793,7 +790,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_u64(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_u64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Long fun ffi_nym_vpn_lib_rust_future_poll_i64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -801,7 +798,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_i64(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_i64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Long fun ffi_nym_vpn_lib_rust_future_poll_f32(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -809,7 +806,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_f32(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_f32(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Float fun ffi_nym_vpn_lib_rust_future_poll_f64(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -817,7 +814,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_f64(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_f64(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Double fun ffi_nym_vpn_lib_rust_future_poll_pointer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -825,7 +822,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_pointer(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_pointer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Pointer fun ffi_nym_vpn_lib_rust_future_poll_rust_buffer(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -833,7 +830,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_rust_buffer(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_rust_buffer(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): RustBuffer.ByValue fun ffi_nym_vpn_lib_rust_future_poll_void(`handle`: Long,`callback`: UniffiRustFutureContinuationCallback,`callbackData`: Long, ): Unit @@ -841,7 +838,7 @@ internal interface UniffiLib : Library { ): Unit fun ffi_nym_vpn_lib_rust_future_free_void(`handle`: Long, ): Unit - fun ffi_nym_vpn_lib_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, + fun ffi_nym_vpn_lib_rust_future_complete_void(`handle`: Long,uniffi_out_err: UniffiRustCallStatus, ): Unit fun uniffi_nym_vpn_lib_checksum_func_checkcredential( ): Short @@ -857,7 +854,7 @@ internal interface UniffiLib : Library { ): Short fun ffi_nym_vpn_lib_uniffi_contract_version( ): Int - + } private fun uniffiCheckContractApiVersion(lib: UniffiLib) { @@ -1027,13 +1024,13 @@ public object FfiConverterString: FfiConverter { data class Location ( - var `twoLetterIsoCountryCode`: kotlin.String, - var `threeLetterIsoCountryCode`: kotlin.String, - var `countryName`: kotlin.String, - var `latitude`: kotlin.Double?, + var `twoLetterIsoCountryCode`: kotlin.String, + var `threeLetterIsoCountryCode`: kotlin.String, + var `countryName`: kotlin.String, + var `latitude`: kotlin.Double?, var `longitude`: kotlin.Double? ) { - + companion object } @@ -1068,13 +1065,13 @@ public object FfiConverterTypeLocation: FfiConverterRustBuffer { data class VpnConfig ( - var `apiUrl`: Url, - var `explorerUrl`: Url, - var `entryGateway`: EntryPoint, - var `exitRouter`: ExitPoint, + var `apiUrl`: Url, + var `explorerUrl`: Url, + var `entryGateway`: EntryPoint, + var `exitRouter`: ExitPoint, var `enableTwoHop`: kotlin.Boolean ) { - + companion object } @@ -1109,25 +1106,25 @@ public object FfiConverterTypeVPNConfig: FfiConverterRustBuffer { sealed class EntryPoint { - + data class Gateway( val `identity`: NodeIdentity) : EntryPoint() { companion object } - + data class Location( val `location`: kotlin.String) : EntryPoint() { companion object } - + object RandomLowLatency : EntryPoint() - - + + object Random : EntryPoint() - - - + + + companion object } @@ -1204,24 +1201,24 @@ public object FfiConverterTypeEntryPoint : FfiConverterRustBuffer{ sealed class ExitPoint { - + data class Address( val `address`: Recipient) : ExitPoint() { companion object } - + data class Gateway( val `identity`: NodeIdentity) : ExitPoint() { companion object } - + data class Location( val `location`: kotlin.String) : ExitPoint() { companion object } - - + + companion object } @@ -1293,88 +1290,88 @@ public object FfiConverterTypeExitPoint : FfiConverterRustBuffer{ sealed class FfiException: Exception() { - + class InvalidValueUniffi( ) : FfiException() { override val message get() = "" } - + class InvalidCredential( ) : FfiException() { override val message get() = "" } - + class InvalidPath( ) : FfiException() { override val message get() = "" } - + class FdNotFound( ) : FfiException() { override val message get() = "" } - + class VpnNotStopped( ) : FfiException() { override val message get() = "" } - + class VpnNotStarted( ) : FfiException() { override val message get() = "" } - + class VpnAlreadyRunning( ) : FfiException() { override val message get() = "" } - + class VpnNotRunning( ) : FfiException() { override val message get() = "" } - + class NoContext( ) : FfiException() { override val message get() = "" } - + class LibException( - + val `inner`: kotlin.String ) : FfiException() { override val message get() = "inner=${ `inner` }" } - + class GatewayDirectoryException( - + val `inner`: kotlin.String ) : FfiException() { override val message get() = "inner=${ `inner` }" } - + companion object ErrorHandler : UniffiRustCallStatusErrorHandler { override fun lift(error_buf: RustBuffer.ByValue): FfiException = FfiConverterTypeFFIError.lift(error_buf) } - + } public object FfiConverterTypeFFIError : FfiConverterRustBuffer { override fun read(buf: ByteBuffer): FfiException { - + return when(buf.getInt()) { 1 -> FfiException.InvalidValueUniffi() @@ -1643,13 +1640,13 @@ public object FfiConverterTypeUrl: FfiConverter { } } @Throws(FfiException::class) fun `checkCredential`(`credential`: kotlin.String) - = + = uniffiRustCallWithError(FfiException) { _status -> UniffiLib.INSTANCE.uniffi_nym_vpn_lib_fn_func_checkcredential( FfiConverterString.lower(`credential`),_status) } - - + + @Throws(FfiException::class) fun `getGatewayCountries`(`apiUrl`: Url, `explorerUrl`: Url, `harbourMasterUrl`: Url?, `exitOnly`: kotlin.Boolean): List { return FfiConverterSequenceTypeLocation.lift( @@ -1659,7 +1656,7 @@ public object FfiConverterTypeUrl: FfiConverter { } ) } - + @Throws(FfiException::class) fun `getLowLatencyEntryCountry`(`apiUrl`: Url, `explorerUrl`: Url, `harbourMasterUrl`: Url?): Location { return FfiConverterTypeLocation.lift( @@ -1669,33 +1666,33 @@ public object FfiConverterTypeUrl: FfiConverter { } ) } - + @Throws(FfiException::class) fun `importCredential`(`credential`: kotlin.String, `path`: kotlin.String) - = + = uniffiRustCallWithError(FfiException) { _status -> UniffiLib.INSTANCE.uniffi_nym_vpn_lib_fn_func_importcredential( FfiConverterString.lower(`credential`),FfiConverterString.lower(`path`),_status) } - - + + @Throws(FfiException::class) fun `runVpn`(`config`: VpnConfig) - = + = uniffiRustCallWithError(FfiException) { _status -> UniffiLib.INSTANCE.uniffi_nym_vpn_lib_fn_func_runvpn( FfiConverterTypeVPNConfig.lower(`config`),_status) } - - + + @Throws(FfiException::class) fun `stopVpn`() - = + = uniffiRustCallWithError(FfiException) { _status -> UniffiLib.INSTANCE.uniffi_nym_vpn_lib_fn_func_stopvpn( _status) } - - + +