diff options
| author | David Göransson <david.goransson@mullvad.net> | 2024-05-29 17:18:29 +0200 |
|---|---|---|
| committer | David Göransson <david.goransson@mullvad.net> | 2024-05-29 17:18:29 +0200 |
| commit | ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8 (patch) | |
| tree | 9d085bc81caed9409e3a4360490c06c2da4fbba8 /android/lib/shared/src | |
| parent | 8e14a8d4287af66a57a98db79d3ac320c2dad4a1 (diff) | |
| parent | 767b97eda756f4ec4e67fb5fa2ae664277291e8f (diff) | |
| download | mullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.tar.xz mullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.zip | |
Merge branch 'android-grpc'
Diffstat (limited to 'android/lib/shared/src')
7 files changed, 225 insertions, 0 deletions
diff --git a/android/lib/shared/src/main/AndroidManifest.xml b/android/lib/shared/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..cc947c5679 --- /dev/null +++ b/android/lib/shared/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest /> diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt new file mode 100644 index 0000000000..432d113fba --- /dev/null +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/AccountRepository.kt @@ -0,0 +1,84 @@ +package net.mullvad.mullvadvpn.lib.shared + +import arrow.core.Either +import arrow.core.raise.nullable +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.flow.MutableSharedFlow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.distinctUntilChanged +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.merge +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService +import net.mullvad.mullvadvpn.lib.model.AccountData +import net.mullvad.mullvadvpn.lib.model.AccountToken +import net.mullvad.mullvadvpn.lib.model.CreateAccountError +import net.mullvad.mullvadvpn.lib.model.DeviceState +import net.mullvad.mullvadvpn.lib.model.LoginAccountError +import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken +import org.joda.time.DateTime + +class AccountRepository( + private val managementService: ManagementService, + private val deviceRepository: DeviceRepository, + val scope: CoroutineScope +) { + + private val _mutableAccountDataCache: MutableSharedFlow<AccountData> = MutableSharedFlow() + + private val _isNewAccount: MutableStateFlow<Boolean> = MutableStateFlow(false) + val isNewAccount: StateFlow<Boolean> = _isNewAccount + val accountData: StateFlow<AccountData?> = + merge( + managementService.deviceState.filterNotNull().map { deviceState -> + when (deviceState) { + is DeviceState.LoggedIn -> { + managementService.getAccountData(deviceState.accountToken).getOrNull() + } + DeviceState.LoggedOut, + DeviceState.Revoked -> null + } + }, + _mutableAccountDataCache + ) + .distinctUntilChanged() + .stateIn(scope = scope, SharingStarted.Eagerly, null) + + suspend fun createAccount(): Either<CreateAccountError, AccountToken> = + managementService.createAccount().onRight { _isNewAccount.update { true } } + + suspend fun login(accountToken: AccountToken): Either<LoginAccountError, Unit> = + managementService.loginAccount(accountToken) + + suspend fun logout() { + managementService.logoutAccount() + _isNewAccount.update { false } + } + + suspend fun fetchAccountHistory(): AccountToken? = + managementService.getAccountHistory().getOrNull() + + suspend fun clearAccountHistory() = managementService.clearAccountHistory() + + suspend fun getAccountData(): AccountData? = nullable { + val deviceState = ensureNotNull(deviceRepository.deviceState.value as? DeviceState.LoggedIn) + + val accountData = + managementService.getAccountData(deviceState.accountToken).getOrNull().bind() + + // Update stateflow cache + _mutableAccountDataCache.emit(accountData) + accountData + } + + suspend fun getWebsiteAuthToken(): WebsiteAuthToken? = + managementService.getWebsiteAuthToken().getOrNull() + + internal suspend fun onVoucherRedeemed(newExpiry: DateTime) { + accountData.value?.copy(expiryDate = newExpiry)?.let { _mutableAccountDataCache.emit(it) } + } +} diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt new file mode 100644 index 0000000000..6ea373e426 --- /dev/null +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxy.kt @@ -0,0 +1,26 @@ +package net.mullvad.mullvadvpn.lib.shared + +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService +import net.mullvad.mullvadvpn.lib.model.ConnectError + +class ConnectionProxy( + private val managementService: ManagementService, + private val vpnPermissionRepository: VpnPermissionRepository +) { + val tunnelState = managementService.tunnelState + + suspend fun connect(): Either<ConnectError, Boolean> = either { + ensure(vpnPermissionRepository.hasVpnPermission()) { ConnectError.NoVpnPermission } + managementService.connect().bind() + } + + suspend fun connectWithoutPermissionCheck(): Either<ConnectError, Boolean> = + managementService.connect() + + suspend fun disconnect() = managementService.disconnect() + + suspend fun reconnect() = managementService.reconnect() +} diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt new file mode 100644 index 0000000000..b1b8f4fa41 --- /dev/null +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/DeviceRepository.kt @@ -0,0 +1,36 @@ +package net.mullvad.mullvadvpn.lib.shared + +import arrow.core.Either +import kotlinx.coroutines.CoroutineDispatcher +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.stateIn +import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService +import net.mullvad.mullvadvpn.lib.model.AccountToken +import net.mullvad.mullvadvpn.lib.model.DeleteDeviceError +import net.mullvad.mullvadvpn.lib.model.Device +import net.mullvad.mullvadvpn.lib.model.DeviceId +import net.mullvad.mullvadvpn.lib.model.DeviceState +import net.mullvad.mullvadvpn.lib.model.GetDeviceListError + +class DeviceRepository( + private val managementService: ManagementService, + dispatcher: CoroutineDispatcher = Dispatchers.IO +) { + val deviceState: StateFlow<DeviceState?> = + managementService.deviceState.stateIn( + CoroutineScope(dispatcher), + SharingStarted.Eagerly, + null + ) + + suspend fun removeDevice( + accountToken: AccountToken, + deviceId: DeviceId + ): Either<DeleteDeviceError, Unit> = managementService.removeDevice(accountToken, deviceId) + + suspend fun deviceList(accountToken: AccountToken): Either<GetDeviceListError, List<Device>> = + managementService.getDeviceList(accountToken) +} diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt new file mode 100644 index 0000000000..a5783a832e --- /dev/null +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VoucherRepository.kt @@ -0,0 +1,13 @@ +package net.mullvad.mullvadvpn.lib.shared + +import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService + +class VoucherRepository( + private val managementService: ManagementService, + private val accountRepository: AccountRepository +) { + suspend fun submitVoucher(voucher: String) = + managementService.submitVoucher(voucher).onRight { + accountRepository.onVoucherRedeemed(it.newExpiryDate) + } +} diff --git a/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt new file mode 100644 index 0000000000..b97c60316c --- /dev/null +++ b/android/lib/shared/src/main/kotlin/net/mullvad/mullvadvpn/lib/shared/VpnPermissionRepository.kt @@ -0,0 +1,11 @@ +package net.mullvad.mullvadvpn.lib.shared + +import android.content.Context +import android.net.VpnService +import net.mullvad.mullvadvpn.lib.common.util.getAlwaysOnVpnAppName + +class VpnPermissionRepository(private val applicationContext: Context) { + fun hasVpnPermission(): Boolean = VpnService.prepare(applicationContext) == null + + fun getAlwaysOnVpnAppName() = applicationContext.getAlwaysOnVpnAppName() +} diff --git a/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt new file mode 100644 index 0000000000..74ab4f6b64 --- /dev/null +++ b/android/lib/shared/src/test/kotlin/net/mullvad/mullvadvpn/lib/shared/ConnectionProxyTest.kt @@ -0,0 +1,54 @@ +package net.mullvad.mullvadvpn.lib.shared + +import io.mockk.coVerify +import io.mockk.every +import io.mockk.mockk +import io.mockk.unmockkAll +import kotlinx.coroutines.test.runTest +import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Test + +class ConnectionProxyTest { + + private val mockManagementService: ManagementService = mockk(relaxed = true) + private val mockVpnPermissionRepository: VpnPermissionRepository = mockk() + + private val connectionProxy: ConnectionProxy = + ConnectionProxy( + managementService = mockManagementService, + vpnPermissionRepository = mockVpnPermissionRepository + ) + + @Test + fun `connect with vpn permission allowed should call managementService connect`() = runTest { + every { mockVpnPermissionRepository.hasVpnPermission() } returns true + connectionProxy.connect() + coVerify(exactly = 1) { mockManagementService.connect() } + } + + @Test + fun `connect with vpn permission not allowed should not call managementService connect`() = + runTest { + every { mockVpnPermissionRepository.hasVpnPermission() } returns false + connectionProxy.connect() + coVerify(exactly = 0) { mockManagementService.connect() } + } + + @Test + fun `disconnect should call managementService disconnect`() = runTest { + connectionProxy.disconnect() + coVerify(exactly = 1) { mockManagementService.disconnect() } + } + + @Test + fun `reconnect should call managementService reconnect`() = runTest { + connectionProxy.reconnect() + coVerify(exactly = 1) { mockManagementService.reconnect() } + } + + @AfterEach + fun tearDown() { + unmockkAll() + } +} |
