Skip to content

Commit

Permalink
android: add account integration (#1314)
Browse files Browse the repository at this point in the history
* android: add account integration

* bump deps

* remove query packages
  • Loading branch information
zaneschepke authored Oct 16, 2024
1 parent cbb002e commit f74befe
Show file tree
Hide file tree
Showing 28 changed files with 261 additions and 249 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ import net.nymtech.nymvpn.data.domain.Settings
import net.nymtech.nymvpn.ui.theme.Theme
import net.nymtech.vpn.backend.Tunnel
import net.nymtech.vpn.model.Country
import java.time.Instant

interface SettingsRepository {

Expand Down Expand Up @@ -50,10 +49,6 @@ interface SettingsRepository {

suspend fun setApplicationShortcuts(enabled: Boolean)

suspend fun getCredentialExpiry(): Instant?

suspend fun saveCredentialExpiry(instant: Instant)

suspend fun getEnvironment(): Tunnel.Environment

suspend fun setEnvironment(environment: Tunnel.Environment)
Expand Down
Original file line number Diff line number Diff line change
@@ -1,7 +1,6 @@
package net.nymtech.nymvpn.data.datastore

import androidx.datastore.preferences.core.booleanPreferencesKey
import androidx.datastore.preferences.core.longPreferencesKey
import androidx.datastore.preferences.core.stringPreferencesKey
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.map
Expand All @@ -11,7 +10,6 @@ import net.nymtech.nymvpn.ui.theme.Theme
import net.nymtech.vpn.backend.Tunnel
import net.nymtech.vpn.model.Country
import timber.log.Timber
import java.time.Instant

class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager) :
SettingsRepository {
Expand All @@ -27,7 +25,6 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager
private val autoStart = booleanPreferencesKey("AUTO_START")
private val analyticsShown = booleanPreferencesKey("ANALYTICS_SHOWN")
private val applicationShortcuts = booleanPreferencesKey("APPLICATION_SHORTCUTS")
private val credentialExpiry = longPreferencesKey("CREDENTIAL_EXPIRY")
private val environment = stringPreferencesKey("ENVIRONMENT")

override suspend fun init() {
Expand Down Expand Up @@ -135,16 +132,6 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager
dataStoreManager.saveToDataStore(applicationShortcuts, enabled)
}

override suspend fun getCredentialExpiry(): Instant? {
return dataStoreManager.getFromStore(credentialExpiry)?.let {
Instant.ofEpochSecond(it)
}
}

override suspend fun saveCredentialExpiry(instant: Instant) {
dataStoreManager.saveToDataStore(credentialExpiry, instant.epochSecond)
}

override suspend fun getEnvironment(): Tunnel.Environment {
return dataStoreManager.getFromStore(environment)?.let {
Tunnel.Environment.valueOf(it)
Expand Down Expand Up @@ -181,7 +168,6 @@ class DataStoreSettingsRepository(private val dataStoreManager: DataStoreManager
firstHopCountry = Country.from(pref[firstHopCountry]) ?: default,
lastHopCountry = Country.from(pref[lastHopCountry]) ?: default,
isShortcutsEnabled = pref[applicationShortcuts] ?: Settings.SHORTCUTS_DEFAULT,
credentialExpiry = pref[credentialExpiry]?.let { Instant.ofEpochSecond(it) },
environment = pref[environment]?.let { Tunnel.Environment.valueOf(it) } ?: Settings.DEFAULT_ENVIRONMENT,
)
} catch (e: IllegalArgumentException) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,6 @@ package net.nymtech.nymvpn.data.domain
import net.nymtech.nymvpn.ui.theme.Theme
import net.nymtech.vpn.backend.Tunnel
import net.nymtech.vpn.model.Country
import java.time.Instant

data class Settings(
val theme: Theme? = null,
Expand All @@ -16,7 +15,6 @@ data class Settings(
val firstHopCountry: Country = Country(),
val lastHopCountry: Country = Country(),
val isShortcutsEnabled: Boolean = SHORTCUTS_DEFAULT,
val credentialExpiry: Instant? = null,
val environment: Tunnel.Environment = DEFAULT_ENVIRONMENT,
) {
companion object {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -12,7 +12,6 @@ import kotlinx.coroutines.launch
import net.nymtech.nymvpn.R
import net.nymtech.nymvpn.data.SettingsRepository
import net.nymtech.nymvpn.service.tunnel.TunnelManager
import net.nymtech.nymvpn.util.extensions.isInvalid
import net.nymtech.nymvpn.util.extensions.startTunnelFromBackground
import net.nymtech.nymvpn.util.extensions.stopTunnelFromBackground
import net.nymtech.vpn.backend.Tunnel
Expand Down Expand Up @@ -40,8 +39,7 @@ class VpnQuickTile : TileService(), LifecycleOwner {
lifecycleRegistry.handleLifecycleEvent(Lifecycle.Event.ON_START)

lifecycleScope.launch {
val credExpiry = settingsRepository.getCredentialExpiry()
if (credExpiry.isInvalid()) return@launch setUnavailable()
if (!tunnelManager.isMnemonicStored()) return@launch setUnavailable()
val state = tunnelManager.getState()
kotlin.runCatching {
when (state) {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,15 +2,19 @@ package net.nymtech.nymvpn.service.tunnel

import android.content.Context
import android.net.VpnService
import kotlinx.coroutines.CoroutineScope
import kotlinx.coroutines.flow.Flow
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.asStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.onStart
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.flow.update
import net.nymtech.nymvpn.NymVpn
import net.nymtech.nymvpn.R
import net.nymtech.nymvpn.data.SettingsRepository
import net.nymtech.nymvpn.module.qualifiers.ApplicationScope
import net.nymtech.nymvpn.service.notification.NotificationService
import net.nymtech.nymvpn.util.extensions.isInvalid
import net.nymtech.nymvpn.util.Constants
import net.nymtech.nymvpn.util.extensions.requestTileServiceStateUpdate
import net.nymtech.nymvpn.util.extensions.toUserMessage
import net.nymtech.vpn.backend.Backend
Expand All @@ -20,7 +24,6 @@ import net.nymtech.vpn.model.Statistics
import nym_vpn_lib.BandwidthStatus
import nym_vpn_lib.VpnException
import timber.log.Timber
import java.time.Instant
import javax.inject.Inject
import javax.inject.Provider

Expand All @@ -29,10 +32,15 @@ class NymTunnelManager @Inject constructor(
private val notificationService: NotificationService,
private val backend: Provider<Backend>,
private val context: Context,
@ApplicationScope private val applicationScope: CoroutineScope,
) : TunnelManager {

private val _state = MutableStateFlow(TunnelState())
override val stateFlow: Flow<TunnelState> = _state.asStateFlow()
override val stateFlow: Flow<TunnelState> = _state.onStart {
_state.update {
it.copy(isMnemonicStored = isMnemonicStored())
}
}.stateIn(applicationScope, SharingStarted.WhileSubscribed(Constants.SUBSCRIPTION_TIMEOUT), TunnelState())

@get:Synchronized @set:Synchronized
private var running: Boolean = false
Expand All @@ -51,8 +59,7 @@ class NymTunnelManager @Inject constructor(
override suspend fun start(fromBackground: Boolean) {
runCatching {
if (running) return Timber.w("Vpn already running")
val credentialExpiry = settingsRepository.getCredentialExpiry()
if (credentialExpiry.isInvalid()) return onInvalidCredential(credentialExpiry)
if (!isMnemonicStored()) return onMissingMnemonic()
val intent = VpnService.prepare(context)
if (intent != null) return launchVpnPermissionNotification()
val entryCountry = settingsRepository.getFirstHopCountry()
Expand All @@ -71,9 +78,23 @@ class NymTunnelManager @Inject constructor(
}
}

override suspend fun importCredential(credential: String): Result<Instant?> {
return kotlin.runCatching {
backend.get().importCredential(credential)
override suspend fun storeMnemonic(mnemonic: String) {
backend.get().storeMnemonic(mnemonic)
emitMnemonicStored(true)
}

override suspend fun isMnemonicStored(): Boolean {
return backend.get().isMnemonicStored()
}

override suspend fun removeMnemonic() {
backend.get().removeMnemonic()
emitMnemonicStored(false)
}

private fun emitMnemonicStored(stored: Boolean) {
_state.update {
it.copy(isMnemonicStored = stored)
}
}

Expand Down Expand Up @@ -104,16 +125,12 @@ class NymTunnelManager @Inject constructor(
emitState(state)
}

private fun onInvalidCredential(expiry: Instant?) {
val message = if (expiry == null) {
context.getString(R.string.missing_credential)
} else {
context.getString(R.string.exception_cred_invalid)
}
private fun onMissingMnemonic() {
val message = context.getString(R.string.missing_mnemonic)
if (NymVpn.isForeground()) {
emitMessage(BackendMessage.Failure(VpnException.InvalidCredential(details = message)))
} else {
launchCredentialNotification(message)
launchMnemonicNotification(message)
}
}

Expand All @@ -134,9 +151,9 @@ class NymTunnelManager @Inject constructor(
}
}

private fun launchCredentialNotification(description: String) {
private fun launchMnemonicNotification(description: String) {
notificationService.showNotification(
title = context.getString(R.string.credential_failed_message),
title = context.getString(R.string.connection_failed),
description = description,
)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -2,12 +2,13 @@ package net.nymtech.nymvpn.service.tunnel

import kotlinx.coroutines.flow.Flow
import net.nymtech.vpn.backend.Tunnel
import java.time.Instant

interface TunnelManager {
suspend fun stop()
suspend fun start(fromBackground: Boolean = true)
suspend fun importCredential(credential: String): Result<Instant?>
suspend fun storeMnemonic(credential: String)
suspend fun isMnemonicStored(): Boolean
suspend fun removeMnemonic()
val stateFlow: Flow<TunnelState>
fun getState(): Tunnel.State
}
Original file line number Diff line number Diff line change
Expand Up @@ -8,4 +8,5 @@ data class TunnelState(
val state: Tunnel.State = Tunnel.State.Down,
val statistics: Statistics = Statistics(),
val backendMessage: BackendMessage = BackendMessage.None,
val isMnemonicStored: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -10,4 +10,5 @@ data class AppUiState(
val gateways: Gateways = Gateways(),
val state: Tunnel.State = Tunnel.State.Down,
val backendMessage: BackendMessage = BackendMessage.None,
val isMnemonicStored: Boolean = false,
)
Original file line number Diff line number Diff line change
Expand Up @@ -28,7 +28,7 @@ constructor(
private val settingsRepository: SettingsRepository,
private val gatewayRepository: GatewayRepository,
@Native private val gatewayService: GatewayService,
tunnelManager: TunnelManager,
private val tunnelManager: TunnelManager,
) : ViewModel() {

private val _navBarState = MutableStateFlow(NavBarState())
Expand All @@ -45,6 +45,7 @@ constructor(
gateways,
manager.state,
manager.backendMessage,
isMnemonicStored = manager.isMnemonicStored,
)
}.stateIn(
viewModelScope,
Expand Down Expand Up @@ -77,6 +78,10 @@ constructor(
}
}

fun logout() = viewModelScope.launch {
tunnelManager.removeMnemonic()
}

fun onErrorReportingSelected() = viewModelScope.launch {
settingsRepository.setErrorReporting(!uiState.value.settings.errorReportingEnabled)
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -209,7 +209,6 @@ class MainActivity : ComponentActivity() {
composable<Route.Settings> {
SettingsScreen(
appViewModel,
navController,
appState,
)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,18 +1,17 @@
package net.nymtech.nymvpn.ui.common.buttons

import androidx.compose.foundation.layout.padding
import androidx.compose.material3.Switch
import androidx.compose.runtime.Composable
import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.scale
import androidx.compose.ui.unit.dp
import net.nymtech.nymvpn.util.extensions.scaledHeight

@Composable
fun ScaledSwitch(checked: Boolean, modifier: Modifier = Modifier, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) {
fun ScaledSwitch(checked: Boolean, onClick: (checked: Boolean) -> Unit, enabled: Boolean = true) {
Switch(
checked,
{ onClick(it) },
modifier =
modifier.padding(0.dp),
enabled = enabled,
Modifier.scale((52.dp.scaledHeight() / 52.dp)),
)
}
Original file line number Diff line number Diff line change
Expand Up @@ -174,10 +174,6 @@ fun AnalyticsScreen(appViewModel: AppViewModel, navController: NavController, ap
ScaledSwitch(
appUiState.settings.errorReportingEnabled,
onClick = { appViewModel.onErrorReportingSelected() },
modifier =
Modifier
.height(32.dp.scaledHeight())
.width(52.dp.scaledWidth()),
)
},
),
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,7 +74,6 @@ import net.nymtech.nymvpn.ui.theme.iconSize
import net.nymtech.nymvpn.util.Constants
import net.nymtech.nymvpn.util.extensions.buildCountryNameString
import net.nymtech.nymvpn.util.extensions.goFromRoot
import net.nymtech.nymvpn.util.extensions.isInvalid
import net.nymtech.nymvpn.util.extensions.openWebUrl
import net.nymtech.nymvpn.util.extensions.scaledHeight
import net.nymtech.nymvpn.util.extensions.scaledWidth
Expand Down Expand Up @@ -321,7 +320,7 @@ fun MainScreen(appViewModel: AppViewModel, appUiState: AppUiState, autoStart: Bo
testTag = Constants.CONNECT_TEST_TAG,
onClick = {
scope.launch {
if (appUiState.settings.credentialExpiry.isInvalid()
if (!appUiState.isMnemonicStored
) {
return@launch navController.goFromRoot(Route.Credential)
}
Expand Down
Original file line number Diff line number Diff line change
@@ -1,14 +1,18 @@
package net.nymtech.nymvpn.ui.screens.permission

import android.net.VpnService
import androidx.compose.foundation.background
import androidx.compose.foundation.layout.Arrangement
import androidx.compose.foundation.layout.Box
import androidx.compose.foundation.layout.Column
import androidx.compose.foundation.layout.Row
import androidx.compose.foundation.layout.WindowInsets
import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.navigationBars
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.layout.width
import androidx.compose.foundation.layout.windowInsetsPadding
import androidx.compose.foundation.shape.RoundedCornerShape
import androidx.compose.foundation.text.ClickableText
import androidx.compose.material.icons.Icons
Expand Down Expand Up @@ -37,6 +41,7 @@ import net.nymtech.nymvpn.ui.common.navigation.LocalNavController
import net.nymtech.nymvpn.ui.common.navigation.NavBarState
import net.nymtech.nymvpn.ui.common.navigation.NavIcon
import net.nymtech.nymvpn.ui.common.navigation.NavTitle
import net.nymtech.nymvpn.ui.common.snackbar.SnackbarController
import net.nymtech.nymvpn.ui.theme.CustomTypography
import net.nymtech.nymvpn.util.extensions.launchVpnSettings
import net.nymtech.nymvpn.util.extensions.navigateAndForget
Expand All @@ -47,6 +52,7 @@ import net.nymtech.nymvpn.util.extensions.scaledWidth
fun PermissionScreen(appViewModel: AppViewModel, permission: Permission) {
val context = LocalContext.current
val navController = LocalNavController.current
val snackbar = SnackbarController.current

LaunchedEffect(Unit) {
appViewModel.onNavBarStateChange(
Expand All @@ -65,7 +71,7 @@ fun PermissionScreen(appViewModel: AppViewModel, permission: Permission) {
modifier = Modifier
.fillMaxSize()
.padding(horizontal = 16.dp.scaledWidth())
.padding(vertical = 24.dp),
.padding(vertical = 24.dp).windowInsetsPadding(WindowInsets.navigationBars),
horizontalAlignment = Alignment.CenterHorizontally,
verticalArrangement = Arrangement.SpaceBetween,
) {
Expand Down Expand Up @@ -136,7 +142,11 @@ fun PermissionScreen(appViewModel: AppViewModel, permission: Permission) {
Column(verticalArrangement = Arrangement.spacedBy(16.dp, Alignment.Bottom)) {
MainStyledButton(
onClick = {
navController.navigateAndForget(Route.Main(true))
if (VpnService.prepare(context) == null) {
navController.navigateAndForget(Route.Main(true))
} else {
snackbar.showMessage(context.getString(R.string.permission_required))
}
},
content = { Text(stringResource(R.string.try_reconnecting), style = CustomTypography.labelHuge) },
)
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -34,7 +34,7 @@ fun ScannerScreen(viewModel: ScannerViewModel = hiltViewModel()) {
this.setStatusText("")
this.decodeSingle { result ->
result.text?.let { barCodeOrQr ->
viewModel.onCredentialImport(barCodeOrQr)
viewModel.onMnemonicImport(barCodeOrQr)
}
}
}
Expand Down
Loading

0 comments on commit f74befe

Please sign in to comment.