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 | |
| parent | 8e14a8d4287af66a57a98db79d3ac320c2dad4a1 (diff) | |
| parent | 767b97eda756f4ec4e67fb5fa2ae664277291e8f (diff) | |
| download | mullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.tar.xz mullvadvpn-ad90145a5d86d8c1e6e70f2238f11edf5e50f8d8.zip | |
Merge branch 'android-grpc'
Diffstat (limited to 'android/lib')
221 files changed, 3031 insertions, 1735 deletions
diff --git a/android/lib/billing/build.gradle.kts b/android/lib/billing/build.gradle.kts index 26cc345556..6bb4e5e7a6 100644 --- a/android/lib/billing/build.gradle.kts +++ b/android/lib/billing/build.gradle.kts @@ -49,12 +49,15 @@ dependencies { //Model implementation(project(Dependencies.Mullvad.modelLib)) - //IPC - implementation(project(Dependencies.Mullvad.ipcLib)) - //Payment library implementation(project(Dependencies.Mullvad.paymentLib)) + //Either + implementation(Dependencies.Arrow.core) + + // Management service + implementation(project(Dependencies.Mullvad.daemonGrpc)) + // Test dependencies testRuntimeOnly(Dependencies.junitEngine) diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt index 76df623ada..8b3ad66171 100644 --- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepository.kt @@ -15,15 +15,14 @@ import net.mullvad.mullvadvpn.lib.billing.extension.toPaymentStatus import net.mullvad.mullvadvpn.lib.billing.extension.toPurchaseResult import net.mullvad.mullvadvpn.lib.billing.model.BillingException import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent +import net.mullvad.mullvadvpn.lib.model.PlayPurchase +import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken import net.mullvad.mullvadvpn.lib.payment.PaymentRepository import net.mullvad.mullvadvpn.lib.payment.ProductIds import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult import net.mullvad.mullvadvpn.lib.payment.model.VerificationResult -import net.mullvad.mullvadvpn.model.PlayPurchase -import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult -import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult class BillingPaymentRepository( private val billingRepository: BillingRepository, @@ -74,19 +73,20 @@ class BillingPaymentRepository( // Get transaction id emit(PurchaseResult.FetchingObfuscationId) - val obfuscatedId: String = - when (val result = initialisePurchase()) { - is PlayPurchaseInitResult.Ok -> result.obfuscatedId - else -> { - emit(PurchaseResult.Error.TransactionIdError(productId, null)) - return@flow - } - } + val obfuscatedId: PlayPurchasePaymentToken = + initialisePurchase() + .fold( + { + emit(PurchaseResult.Error.TransactionIdError(productId, null)) + return@flow + }, + { it } + ) val result = billingRepository.startPurchaseFlow( productDetails = productDetails, - obfuscatedId = obfuscatedId, + obfuscatedId = obfuscatedId.value, activityProvider = activityProvider ) @@ -115,11 +115,13 @@ class BillingPaymentRepository( emit(PurchaseResult.Completed.Pending) } else { emit(PurchaseResult.VerificationStarted) - if (verifyPurchase(event.purchases.first()) == PlayPurchaseVerifyResult.Ok) { - emit(PurchaseResult.Completed.Success) - } else { - emit(PurchaseResult.Error.VerificationError(null)) - } + emit( + verifyPurchase(event.purchases.first()) + .fold( + { PurchaseResult.Error.VerificationError(null) }, + { PurchaseResult.Completed.Success } + ) + ) } } PurchaseEvent.UserCanceled -> emit(event.toPurchaseResult()) @@ -135,13 +137,12 @@ class BillingPaymentRepository( val purchases = purchasesResult.nonPendingPurchases() if (purchases.isNotEmpty()) { emit(VerificationResult.VerificationStarted) - val verificationResult = verifyPurchase(purchases.first()) emit( - when (verificationResult) { - is PlayPurchaseVerifyResult.Error -> - VerificationResult.Error.VerificationError(null) - PlayPurchaseVerifyResult.Ok -> VerificationResult.Success - } + verifyPurchase(purchases.first()) + .fold( + { VerificationResult.Error.VerificationError(null) }, + { VerificationResult.Success } + ) ) } else { emit(VerificationResult.NothingToVerify) @@ -152,16 +153,13 @@ class BillingPaymentRepository( } } - private suspend fun initialisePurchase(): PlayPurchaseInitResult { - return playPurchaseRepository.initializePlayPurchase() - } + private suspend fun initialisePurchase() = playPurchaseRepository.initializePlayPurchase() - private suspend fun verifyPurchase(purchase: Purchase): PlayPurchaseVerifyResult { - return playPurchaseRepository.verifyPlayPurchase( + private suspend fun verifyPurchase(purchase: Purchase) = + playPurchaseRepository.verifyPlayPurchase( PlayPurchase( productId = purchase.products.first(), - purchaseToken = purchase.purchaseToken, + purchaseToken = PlayPurchasePaymentToken(purchase.purchaseToken), ) ) - } } diff --git a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt index ac71372f76..8e89cb8f95 100644 --- a/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt +++ b/android/lib/billing/src/main/kotlin/net/mullvad/mullvadvpn/lib/billing/PlayPurchaseRepository.kt @@ -1,33 +1,11 @@ package net.mullvad.mullvadvpn.lib.billing -import kotlinx.coroutines.flow.first -import net.mullvad.mullvadvpn.lib.ipc.Event -import net.mullvad.mullvadvpn.lib.ipc.MessageHandler -import net.mullvad.mullvadvpn.lib.ipc.Request -import net.mullvad.mullvadvpn.lib.ipc.events -import net.mullvad.mullvadvpn.model.PlayPurchase -import net.mullvad.mullvadvpn.model.PlayPurchaseInitError -import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult -import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyError -import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult +import net.mullvad.mullvadvpn.lib.daemon.grpc.ManagementService +import net.mullvad.mullvadvpn.lib.model.PlayPurchase -class PlayPurchaseRepository(private val messageHandler: MessageHandler) { - suspend fun initializePlayPurchase(): PlayPurchaseInitResult { - val result = messageHandler.trySendRequest(Request.InitPlayPurchase) +class PlayPurchaseRepository(private val managementService: ManagementService) { + suspend fun initializePlayPurchase() = managementService.initializePlayPurchase() - return if (result) { - messageHandler.events<Event.PlayPurchaseInitResultEvent>().first().result - } else { - PlayPurchaseInitResult.Error(PlayPurchaseInitError.OtherError) - } - } - - suspend fun verifyPlayPurchase(purchase: PlayPurchase): PlayPurchaseVerifyResult { - val result = messageHandler.trySendRequest(Request.VerifyPlayPurchase(purchase)) - return if (result) { - messageHandler.events<Event.PlayPurchaseVerifyResultEvent>().first().result - } else { - PlayPurchaseVerifyResult.Error(PlayPurchaseVerifyError.OtherError) - } - } + suspend fun verifyPlayPurchase(purchase: PlayPurchase) = + managementService.verifyPlayPurchase(purchase) } diff --git a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt index c4d1b04905..ad716cd30c 100644 --- a/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt +++ b/android/lib/billing/src/test/kotlin/net/mullvad/mullvadvpn/lib/billing/BillingPaymentRepositoryTest.kt @@ -1,6 +1,8 @@ package net.mullvad.mullvadvpn.lib.billing import app.cash.turbine.test +import arrow.core.left +import arrow.core.right import com.android.billingclient.api.BillingClient.BillingResponseCode import com.android.billingclient.api.BillingResult import com.android.billingclient.api.ProductDetails @@ -17,14 +19,13 @@ import kotlinx.coroutines.test.runTest import net.mullvad.mullvadvpn.lib.billing.extension.toPaymentProduct import net.mullvad.mullvadvpn.lib.billing.model.PurchaseEvent import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule +import net.mullvad.mullvadvpn.lib.model.PlayPurchaseInitError +import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken +import net.mullvad.mullvadvpn.lib.model.PlayPurchaseVerifyError import net.mullvad.mullvadvpn.lib.payment.model.PaymentAvailability import net.mullvad.mullvadvpn.lib.payment.model.PaymentProduct import net.mullvad.mullvadvpn.lib.payment.model.ProductId import net.mullvad.mullvadvpn.lib.payment.model.PurchaseResult -import net.mullvad.mullvadvpn.model.PlayPurchaseInitError -import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult -import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyError -import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult import org.junit.jupiter.api.BeforeEach import org.junit.jupiter.api.Test import org.junit.jupiter.api.extension.ExtendWith @@ -170,7 +171,7 @@ class BillingPaymentRepositoryTest { coEvery { mockBillingRepository.queryProducts(listOf(mockProductId.value)) } returns mockProductDetailsResult coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Error(PlayPurchaseInitError.OtherError) + PlayPurchaseInitError.OtherError.left() // Act, Assert paymentRepository.purchaseProduct(mockProductId, mockk()).test { @@ -206,7 +207,7 @@ class BillingPaymentRepositoryTest { ) } returns mockBillingResult coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK") + PlayPurchasePaymentToken("MOCK").right() // Act, Assert paymentRepository.purchaseProduct(mockProductId, mockk()).test { @@ -241,7 +242,7 @@ class BillingPaymentRepositoryTest { ) } returns mockBillingResult coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok(mockObfuscatedId) + PlayPurchasePaymentToken(mockObfuscatedId).right() // Act, Assert paymentRepository.purchaseProduct(mockProductId, mockk()).test { @@ -283,9 +284,9 @@ class BillingPaymentRepositoryTest { ) } returns mockBillingResult coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK-ID") + PlayPurchasePaymentToken("MOCK-ID").right() coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns - PlayPurchaseVerifyResult.Error(PlayPurchaseVerifyError.OtherError) + PlayPurchaseVerifyError.OtherError.left() // Act, Assert paymentRepository.purchaseProduct(mockProductId, mockk()).test { @@ -326,9 +327,8 @@ class BillingPaymentRepositoryTest { ) } returns mockBillingResult coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK") - coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns - PlayPurchaseVerifyResult.Ok + PlayPurchasePaymentToken("MOCK").right() + coEvery { mockPlayPurchaseRepository.verifyPlayPurchase(any()) } returns Unit.right() // Act, Assert paymentRepository.purchaseProduct(mockProductId, mockk()).test { @@ -368,7 +368,7 @@ class BillingPaymentRepositoryTest { ) } returns mockBillingResult coEvery { mockPlayPurchaseRepository.initializePlayPurchase() } returns - PlayPurchaseInitResult.Ok("MOCK") + PlayPurchasePaymentToken("MOCK").right() // Act, Assert paymentRepository.purchaseProduct(mockProductId, mockk()).test { diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassesAndActions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassNames.kt index 09210ffa03..1636bbd46f 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassesAndActions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/ClassNames.kt @@ -2,13 +2,8 @@ package net.mullvad.mullvadvpn.lib.common.constant // Do not use in cases where the application id is expected since the application id will differ // between different builds. -private const val MULLVAD_PACKAGE_NAME = "net.mullvad.mullvadvpn" +internal const val MULLVAD_PACKAGE_NAME = "net.mullvad.mullvadvpn" // Classes const val MAIN_ACTIVITY_CLASS = "$MULLVAD_PACKAGE_NAME.ui.MainActivity" const val VPN_SERVICE_CLASS = "$MULLVAD_PACKAGE_NAME.service.MullvadVpnService" - -// Actions -const val KEY_CONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.connect_action" -const val KEY_DISCONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.disconnect_action" -const val KEY_QUIT_ACTION = "$MULLVAD_PACKAGE_NAME.quit_action" diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt new file mode 100644 index 0000000000..ea420f2d0a --- /dev/null +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/IntentActions.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.common.constant + +// Actions +const val KEY_CONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.connect_action" +const val KEY_DISCONNECT_ACTION = "$MULLVAD_PACKAGE_NAME.disconnect_action" +const val KEY_REQUEST_VPN_PERMISSION = "$MULLVAD_PACKAGE_NAME.request_vpn_permission" diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt new file mode 100644 index 0000000000..d2ae3f1871 --- /dev/null +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/constant/LogTag.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.common.constant + +const val TAG = "mullvad" diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt index 42f0663967..bf94c80778 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/CommonFlowUtils.kt @@ -1,47 +1,9 @@ package net.mullvad.mullvadvpn.lib.common.util -import android.content.ComponentName -import android.content.Context -import android.content.Intent -import android.content.ServiceConnection -import android.os.IBinder -import android.util.Log -import kotlin.coroutines.EmptyCoroutineContext -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.channels.awaitClose import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.callbackFlow -import net.mullvad.mullvadvpn.model.ServiceResult +import kotlinx.coroutines.flow.firstOrNull +import kotlinx.coroutines.withTimeoutOrNull -fun <T> SendChannel<T>.safeOffer(element: T): Boolean { - return runCatching { trySend(element).isSuccess }.getOrDefault(false) -} - -fun Context.bindServiceFlow(intent: Intent, flags: Int = 0): Flow<ServiceResult> = callbackFlow { - val connectionCallback = - object : ServiceConnection { - override fun onServiceConnected(className: ComponentName, binder: IBinder) { - safeOffer(ServiceResult(binder)) - } - - override fun onServiceDisconnected(className: ComponentName) { - safeOffer(ServiceResult.NOT_CONNECTED) - bindService(intent, this, flags) - } - } - - bindService(intent, connectionCallback, flags) - - awaitClose { - safeOffer(ServiceResult.NOT_CONNECTED) - - Dispatchers.Default.dispatch(EmptyCoroutineContext) { - try { - unbindService(connectionCallback) - } catch (e: IllegalArgumentException) { - Log.e("mullvad", "Cannot unbind as no binding exists.") - } - } - } +suspend fun <T> Flow<T>.firstOrNullWithTimeout(timeMillis: Long): T? { + return withTimeoutOrNull(timeMillis) { firstOrNull() } } diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt index 8ef70dad92..d714dae327 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ContextExtensions.kt @@ -4,15 +4,20 @@ import android.content.Context import android.content.Intent import android.net.Uri import android.provider.Settings -import net.mullvad.mullvadvpn.lib.common.R import net.mullvad.mullvadvpn.lib.common.util.SdkUtils.getInstalledPackagesList +import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken private const val ALWAYS_ON_VPN_APP = "always_on_vpn_app" -fun Context.openAccountPageInBrowser(authToken: String) { - startActivity( - Intent(Intent.ACTION_VIEW, Uri.parse(getString(R.string.account_url) + "?token=$authToken")) - ) +fun createAccountUri(accountUri: String, websiteAuthToken: WebsiteAuthToken?): Uri { + val urlString = buildString { + append(accountUri) + if (websiteAuthToken != null) { + append("?token=") + append(websiteAuthToken.value) + } + } + return Uri.parse(urlString) } fun Context.getAlwaysOnVpnAppName(): String? { diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt deleted file mode 100644 index 7fc37a752c..0000000000 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/DispatchingFlow.kt +++ /dev/null @@ -1,45 +0,0 @@ -package net.mullvad.mullvadvpn.lib.common.util - -import java.util.concurrent.ConcurrentHashMap -import kotlin.reflect.KClass -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedSendChannelException -import kotlinx.coroutines.channels.SendChannel -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.consumeAsFlow - -class DispatchingFlow<T : Any>(private val upstream: Flow<T>) : Flow<T> { - private val subscribers = ConcurrentHashMap<KClass<out T>, SendChannel<T>>() - - fun <V : T> subscribe(variant: KClass<V>, capacity: Int = Channel.CONFLATED): Flow<V> { - val channel = Channel<V>(capacity) - - // This is safe because `collect` will only send to this channel if the instance class is V - @Suppress("UNCHECKED_CAST") - subscribers[variant] = channel as SendChannel<T> - - return channel.consumeAsFlow() - } - - fun <V : T> unsubscribe(variant: KClass<V>) = subscribers.remove(variant) - - @InternalCoroutinesApi - override suspend fun collect(collector: FlowCollector<T>) { - upstream.collect { event -> - try { - subscribers[event::class]?.send(event) - } catch (closedException: ClosedSendChannelException) { - subscribers.remove(event::class) - } - - collector.emit(event) - } - - subscribers.clear() - } -} - -fun <T : Any> Flow<T>.dispatchTo(configureSubscribers: DispatchingFlow<T>.() -> Unit) = - DispatchingFlow(this).also(configureSubscribers) diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt index f906ee8f6d..2c9554a842 100644 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt +++ b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/ErrorStateExtension.kt @@ -2,9 +2,9 @@ package net.mullvad.mullvadvpn.lib.common.util import android.content.Context import net.mullvad.mullvadvpn.lib.common.R -import net.mullvad.talpid.tunnel.ErrorState -import net.mullvad.talpid.tunnel.ErrorStateCause -import net.mullvad.talpid.tunnel.ParameterGenerationError +import net.mullvad.mullvadvpn.lib.model.ErrorState +import net.mullvad.mullvadvpn.lib.model.ErrorStateCause +import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError import net.mullvad.talpid.util.addressString fun ErrorState.getErrorNotificationResources(context: Context): ErrorNotificationMessage { @@ -48,8 +48,8 @@ fun ErrorStateCause.errorMessageId(): Int { is ErrorStateCause.InvalidDnsServers -> R.string.invalid_dns_servers is ErrorStateCause.AuthFailed -> R.string.auth_failed is ErrorStateCause.Ipv6Unavailable -> R.string.ipv6_unavailable - is ErrorStateCause.SetFirewallPolicyError -> R.string.set_firewall_policy_error - is ErrorStateCause.SetDnsError -> R.string.set_dns_error + is ErrorStateCause.FirewallPolicyError -> R.string.set_firewall_policy_error + is ErrorStateCause.DnsError -> R.string.set_dns_error is ErrorStateCause.StartTunnelError -> R.string.start_tunnel_error is ErrorStateCause.IsOffline -> R.string.is_offline is ErrorStateCause.TunnelParameterError -> { diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt deleted file mode 100644 index 448d96778f..0000000000 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/Intermittent.kt +++ /dev/null @@ -1,87 +0,0 @@ -package net.mullvad.mullvadvpn.lib.common.util - -import kotlin.properties.Delegates.observable -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.launch -import kotlinx.coroutines.sync.Mutex -import kotlinx.coroutines.sync.Semaphore -import kotlinx.coroutines.sync.withLock -import kotlinx.coroutines.sync.withPermit -import net.mullvad.talpid.util.EventNotifier - -// Wrapper to allow awaiting for intermittent values. -// -// Wraps a property that is changed from time to time and that can become unavailable (null). This -// behaves in a way similar to `CompletableDeferred`, but the value can be set and reset multiple -// times. -// -// Calling `await` will either provide the value if it's available, or suspend until it becomes -// available and then return it. -// -// Calling `update` will set the internal value after it guarantees that no other coroutine is -// currently reading the value (through a permit from the semaphore). After the value is set, it -// provides a permit to the semaphore so that suspended coroutines can use the new value. -// -// Extra initialization can be done on the intermittent value when it becomes available and before -// it is provided to the awaiting coroutines, through the use of listener callbacks. These are -// called after the value is updated but before it is made available to the coroutines. -class Intermittent<T> { - private val notifier = EventNotifier<T?>(null) - private val semaphore = Semaphore(1, 1) - private val writeLock = Mutex() - - private var updateJob: Job? = null - private var value by notifier.notifiable() - - // When the internal value is updated, listeners can be notified before the awaiting coroutines - // resume execution. This allows performing any extra initialization before the value is made - // available for usage. - fun registerListener(id: Any, listener: (T?) -> Unit) = notifier.subscribe(id, listener) - - fun unregisterListener(id: Any) = notifier.unsubscribe(id) - - suspend fun await(): T { - return semaphore.withPermit { value!! } - } - - suspend fun update(newValue: T?) { - writeLock.withLock { - if (newValue != value) { - if (value != null) { - semaphore.acquire() - } - - // This will trigger the listeners to run before the awaiting coroutines resume - value = newValue - - if (newValue != null) { - semaphore.release() - } - } - } - } - - // Helper method that spawns a coroutine to update the value. - fun spawnUpdate(newValue: T?) { - synchronized(this@Intermittent) { - val previousUpdate = updateJob - - updateJob = - GlobalScope.launch(Dispatchers.Default) { - previousUpdate?.join() - update(newValue) - } - } - } - - // Helper method that provides a simple way to change the wrapped value. - // The method returns a property delegate that will spawn a coroutine to update the wrapped - // value every time the property is written to. - fun source() = observable<T?>(null) { _, _, newValue -> spawnUpdate(newValue) } - - fun onDestroy() { - notifier.unsubscribeAll() - } -} diff --git a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt b/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt deleted file mode 100644 index edb76ed4ae..0000000000 --- a/android/lib/common/src/main/kotlin/net/mullvad/mullvadvpn/lib/common/util/JobTracker.kt +++ /dev/null @@ -1,91 +0,0 @@ -package net.mullvad.mullvadvpn.lib.common.util - -import kotlinx.coroutines.Dispatchers -import kotlinx.coroutines.GlobalScope -import kotlinx.coroutines.Job -import kotlinx.coroutines.async -import kotlinx.coroutines.launch - -class JobTracker { - private val jobs = HashMap<Long, Job>() - private val reaperJobs = HashMap<Long, Job>() - private val namedJobs = HashMap<String, Long>() - - private var jobIdCounter = 0L - - fun newJob(job: Job): Long { - synchronized(jobs) { - val jobId = jobIdCounter - - jobIdCounter += 1 - - jobs.put(jobId, job) - - reaperJobs.put( - jobId, - GlobalScope.launch(Dispatchers.Default) { - job.join() - - synchronized(jobs) { jobs.remove(jobId) } - } - ) - - return jobId - } - } - - fun newJob(name: String, job: Job): Long { - synchronized(namedJobs) { - cancelJob(name) - - val newJobId = newJob(job) - - namedJobs.put(name, newJobId) - - return newJobId - } - } - - fun newBackgroundJob(name: String, jobBody: suspend () -> Unit): Long { - return newJob(name, GlobalScope.launch(Dispatchers.Default) { jobBody() }) - } - - fun newUiJob(name: String, jobBody: suspend () -> Unit): Long { - return newJob(name, GlobalScope.launch(Dispatchers.Main) { jobBody() }) - } - - suspend fun <T> runOnBackground(jobBody: suspend () -> T): T { - val job = GlobalScope.async(Dispatchers.Default) { jobBody() } - - newJob(job) - - return job.await() - } - - fun cancelJob(name: String) { - synchronized(namedJobs) { namedJobs.remove(name)?.let { oldJobId -> cancelJob(oldJobId) } } - } - - fun cancelJob(jobId: Long) { - synchronized(jobs) { - jobs.remove(jobId)?.cancel() - reaperJobs.remove(jobId)?.cancel() - } - } - - fun cancelAllJobs() { - synchronized(jobs) { - for (job in jobs.values) { - job.cancel() - } - - for (job in reaperJobs.values) { - job.cancel() - } - - jobs.clear() - reaperJobs.clear() - namedJobs.clear() - } - } -} diff --git a/android/lib/daemon-grpc/build.gradle.kts b/android/lib/daemon-grpc/build.gradle.kts new file mode 100644 index 0000000000..ed33aa4d09 --- /dev/null +++ b/android/lib/daemon-grpc/build.gradle.kts @@ -0,0 +1,85 @@ +import com.google.protobuf.gradle.proto + +plugins { + id(Dependencies.Plugin.androidLibraryId) + id(Dependencies.Plugin.kotlinAndroidId) + id(Dependencies.Plugin.kotlinParcelizeId) + id(Dependencies.Plugin.protobufId) version Versions.Plugin.protobuf + id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5 +} + +android { + namespace = "net.mullvad.mullvadvpn.lib.daemon.grpc" + compileSdk = Versions.Android.compileSdkVersion + + defaultConfig { minSdk = Versions.Android.minSdkVersion } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { jvmTarget = Versions.jvmTarget } + + lint { + lintConfig = file("${rootProject.projectDir}/config/lint.xml") + abortOnError = true + warningsAsErrors = true + } + + sourceSets { + getByName("main") { + proto { srcDir("${rootProject.projectDir}/../mullvad-management-interface/proto") } + } + } +} + +protobuf { + protoc { artifact = "com.google.protobuf:protoc:${Versions.Grpc.protobufVersion}" } + plugins { + create("java") { artifact = "io.grpc:protoc-gen-grpc-java:${Versions.Grpc.grpcVersion}" } + create("grpc") { artifact = "io.grpc:protoc-gen-grpc-java:${Versions.Grpc.grpcVersion}" } + create("grpckt") { + artifact = "io.grpc:protoc-gen-grpc-kotlin:${Versions.Grpc.grpcKotlinVersion}:jdk8@jar" + } + } + generateProtoTasks { + all().forEach { + it.plugins { + create("java") { option("lite") } + create("grpc") { option("lite") } + create("grpckt") { option("lite") } + } + it.builtins { create("kotlin") { option("lite") } } + } + } +} + +dependencies { + implementation(project(Dependencies.Mullvad.commonLib)) + implementation(project(Dependencies.Mullvad.modelLib)) + implementation(project(Dependencies.Mullvad.talpidLib)) + + implementation(Dependencies.jodaTime) + implementation(Dependencies.Kotlin.stdlib) + implementation(Dependencies.KotlinX.coroutinesCore) + implementation(Dependencies.KotlinX.coroutinesAndroid) + + implementation(Dependencies.Grpc.grpcOkHttp) + implementation(Dependencies.Grpc.grpcAndroid) + implementation(Dependencies.Grpc.grpcKotlinStub) + implementation(Dependencies.Grpc.protobufLite) + implementation(Dependencies.Grpc.protobufKotlinLite) + + implementation(Dependencies.Arrow.core) + implementation(Dependencies.Arrow.optics) + + testImplementation(project(Dependencies.Mullvad.commonTestLib)) + testImplementation(Dependencies.Kotlin.test) + testImplementation(Dependencies.KotlinX.coroutinesTest) + testImplementation(Dependencies.MockK.core) + testImplementation(Dependencies.turbine) + testImplementation(Dependencies.junitApi) + testRuntimeOnly(Dependencies.junitEngine) + testImplementation(Dependencies.junitParams) +} diff --git a/android/lib/ipc/src/main/AndroidManifest.xml b/android/lib/daemon-grpc/src/main/AndroidManifest.xml index cc947c5679..cc947c5679 100644 --- a/android/lib/ipc/src/main/AndroidManifest.xml +++ b/android/lib/daemon-grpc/src/main/AndroidManifest.xml diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt new file mode 100644 index 0000000000..80b79b707a --- /dev/null +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/ManagementService.kt @@ -0,0 +1,565 @@ +package net.mullvad.mullvadvpn.lib.daemon.grpc + +import android.net.LocalSocketAddress +import android.util.Log +import arrow.core.Either +import arrow.optics.copy +import arrow.optics.dsl.index +import arrow.optics.typeclasses.Index +import com.google.protobuf.BoolValue +import com.google.protobuf.Empty +import com.google.protobuf.StringValue +import com.google.protobuf.UInt32Value +import io.grpc.ConnectivityState +import io.grpc.Status +import io.grpc.StatusException +import io.grpc.android.UdsChannelBuilder +import java.net.InetAddress +import kotlinx.coroutines.CoroutineScope +import kotlinx.coroutines.Dispatchers +import kotlinx.coroutines.Job +import kotlinx.coroutines.asExecutor +import kotlinx.coroutines.async +import kotlinx.coroutines.awaitAll +import kotlinx.coroutines.cancel +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow +import kotlinx.coroutines.flow.SharingStarted +import kotlinx.coroutines.flow.StateFlow +import kotlinx.coroutines.flow.filterNotNull +import kotlinx.coroutines.flow.map +import kotlinx.coroutines.flow.mapNotNull +import kotlinx.coroutines.flow.stateIn +import kotlinx.coroutines.flow.update +import kotlinx.coroutines.launch +import kotlinx.coroutines.withContext +import mullvad_daemon.management_interface.ManagementInterface +import mullvad_daemon.management_interface.ManagementServiceGrpcKt +import net.mullvad.mullvadvpn.lib.common.constant.TAG +import net.mullvad.mullvadvpn.lib.daemon.grpc.mapper.fromDomain +import net.mullvad.mullvadvpn.lib.daemon.grpc.mapper.toDomain +import net.mullvad.mullvadvpn.lib.daemon.grpc.util.LogInterceptor +import net.mullvad.mullvadvpn.lib.daemon.grpc.util.connectivityFlow +import net.mullvad.mullvadvpn.lib.model.AccountData +import net.mullvad.mullvadvpn.lib.model.AccountToken +import net.mullvad.mullvadvpn.lib.model.AddSplitTunnelingAppError +import net.mullvad.mullvadvpn.lib.model.AppId +import net.mullvad.mullvadvpn.lib.model.AppVersionInfo as ModelAppVersionInfo +import net.mullvad.mullvadvpn.lib.model.ClearAllOverridesError +import net.mullvad.mullvadvpn.lib.model.ConnectError +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.CreateAccountError +import net.mullvad.mullvadvpn.lib.model.CreateCustomListError +import net.mullvad.mullvadvpn.lib.model.CustomList as ModelCustomList +import net.mullvad.mullvadvpn.lib.model.CustomListAlreadyExists +import net.mullvad.mullvadvpn.lib.model.CustomListId +import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.DeleteCustomListError +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 as ModelDeviceState +import net.mullvad.mullvadvpn.lib.model.DnsOptions as ModelDnsOptions +import net.mullvad.mullvadvpn.lib.model.DnsOptions +import net.mullvad.mullvadvpn.lib.model.DnsState as ModelDnsState +import net.mullvad.mullvadvpn.lib.model.GetAccountDataError +import net.mullvad.mullvadvpn.lib.model.GetAccountHistoryError +import net.mullvad.mullvadvpn.lib.model.GetDeviceListError +import net.mullvad.mullvadvpn.lib.model.GetDeviceStateError +import net.mullvad.mullvadvpn.lib.model.LoginAccountError +import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings as ModelObfuscationSettings +import net.mullvad.mullvadvpn.lib.model.Ownership as ModelOwnership +import net.mullvad.mullvadvpn.lib.model.PlayPurchase +import net.mullvad.mullvadvpn.lib.model.PlayPurchaseInitError +import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken +import net.mullvad.mullvadvpn.lib.model.PlayPurchaseVerifyError +import net.mullvad.mullvadvpn.lib.model.Providers +import net.mullvad.mullvadvpn.lib.model.QuantumResistantState as ModelQuantumResistantState +import net.mullvad.mullvadvpn.lib.model.RedeemVoucherError +import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess +import net.mullvad.mullvadvpn.lib.model.RelayConstraints +import net.mullvad.mullvadvpn.lib.model.RelayItem +import net.mullvad.mullvadvpn.lib.model.RelayItemId as ModelRelayItemId +import net.mullvad.mullvadvpn.lib.model.RelayList as ModelRelayList +import net.mullvad.mullvadvpn.lib.model.RelayList +import net.mullvad.mullvadvpn.lib.model.RelaySettings +import net.mullvad.mullvadvpn.lib.model.RemoveSplitTunnelingAppError +import net.mullvad.mullvadvpn.lib.model.SetAllowLanError +import net.mullvad.mullvadvpn.lib.model.SetAutoConnectError +import net.mullvad.mullvadvpn.lib.model.SetDnsOptionsError +import net.mullvad.mullvadvpn.lib.model.SetObfuscationOptionsError +import net.mullvad.mullvadvpn.lib.model.SetRelayLocationError +import net.mullvad.mullvadvpn.lib.model.SetWireguardConstraintsError +import net.mullvad.mullvadvpn.lib.model.SetWireguardMtuError +import net.mullvad.mullvadvpn.lib.model.SetWireguardQuantumResistantError +import net.mullvad.mullvadvpn.lib.model.Settings as ModelSettings +import net.mullvad.mullvadvpn.lib.model.SettingsPatchError +import net.mullvad.mullvadvpn.lib.model.TunnelState as ModelTunnelState +import net.mullvad.mullvadvpn.lib.model.UnknownCustomListError +import net.mullvad.mullvadvpn.lib.model.UpdateCustomListError +import net.mullvad.mullvadvpn.lib.model.WebsiteAuthToken +import net.mullvad.mullvadvpn.lib.model.WireguardConstraints as ModelWireguardConstraints +import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData as ModelWireguardEndpointData +import net.mullvad.mullvadvpn.lib.model.addresses +import net.mullvad.mullvadvpn.lib.model.customOptions +import net.mullvad.mullvadvpn.lib.model.location +import net.mullvad.mullvadvpn.lib.model.ownership +import net.mullvad.mullvadvpn.lib.model.providers +import net.mullvad.mullvadvpn.lib.model.relayConstraints +import net.mullvad.mullvadvpn.lib.model.state +import net.mullvad.mullvadvpn.lib.model.wireguardConstraints + +@Suppress("TooManyFunctions") +class ManagementService( + rpcSocketPath: String, + private val extensiveLogging: Boolean, + private val scope: CoroutineScope, +) { + private var job: Job? = null + + private val channel = + UdsChannelBuilder.forPath(rpcSocketPath, LocalSocketAddress.Namespace.FILESYSTEM).build() + + val connectionState: StateFlow<GrpcConnectivityState> = + channel + .connectivityFlow() + .map(ConnectivityState::toDomain) + .stateIn(scope, SharingStarted.Eagerly, channel.getState(false).toDomain()) + + private val grpc = + ManagementServiceGrpcKt.ManagementServiceCoroutineStub(channel) + .withExecutor(Dispatchers.IO.asExecutor()) + .let { + if (extensiveLogging) { + it.withInterceptors(LogInterceptor()) + } else it + } + .withWaitForReady() + + private val _mutableDeviceState = MutableStateFlow<ModelDeviceState?>(null) + val deviceState: Flow<ModelDeviceState> = _mutableDeviceState.filterNotNull() + + private val _mutableTunnelState = MutableStateFlow<ModelTunnelState?>(null) + val tunnelState: Flow<ModelTunnelState> = _mutableTunnelState.filterNotNull() + + private val _mutableSettings = MutableStateFlow<ModelSettings?>(null) + val settings: Flow<ModelSettings> = _mutableSettings.filterNotNull() + + private val _mutableVersionInfo = MutableStateFlow<ModelAppVersionInfo?>(null) + val versionInfo: Flow<ModelAppVersionInfo> = _mutableVersionInfo.filterNotNull() + + private val _mutableRelayList = MutableStateFlow<RelayList?>(null) + val relayList: Flow<RelayList> = _mutableRelayList.filterNotNull() + + val relayCountries: Flow<List<RelayItem.Location.Country>> = + relayList.mapNotNull { it.countries } + + val wireguardEndpointData: Flow<ModelWireguardEndpointData> = + relayList.mapNotNull { it.wireguardEndpointData } + + fun start() { + // Just to ensure that connection is set up since the connection won't be setup without a + // call to the daemon + if (job != null) { + error("ManagementService already started") + } + + job = scope.launch { subscribeEvents() } + } + + fun stop() { + job?.cancel(message = "ManagementService stopped") + ?: error("ManagementService already stopped") + job = null + } + + private suspend fun subscribeEvents() = + withContext(Dispatchers.IO) { + launch { + grpc.eventsListen(Empty.getDefaultInstance()).collect { event -> + if (extensiveLogging) { + Log.d(TAG, "Event: $event") + } + @Suppress("WHEN_ENUM_CAN_BE_NULL_IN_JAVA") + when (event.eventCase) { + ManagementInterface.DaemonEvent.EventCase.TUNNEL_STATE -> + _mutableTunnelState.update { event.tunnelState.toDomain() } + ManagementInterface.DaemonEvent.EventCase.SETTINGS -> + _mutableSettings.update { event.settings.toDomain() } + ManagementInterface.DaemonEvent.EventCase.RELAY_LIST -> + _mutableRelayList.update { event.relayList.toDomain() } + ManagementInterface.DaemonEvent.EventCase.VERSION_INFO -> + _mutableVersionInfo.update { event.versionInfo.toDomain() } + ManagementInterface.DaemonEvent.EventCase.DEVICE -> + _mutableDeviceState.update { event.device.newState.toDomain() } + ManagementInterface.DaemonEvent.EventCase.REMOVE_DEVICE -> {} + ManagementInterface.DaemonEvent.EventCase.EVENT_NOT_SET -> {} + ManagementInterface.DaemonEvent.EventCase.NEW_ACCESS_METHOD -> {} + } + } + } + getInitialServiceState() + } + + suspend fun getDevice(): Either<GetDeviceStateError, ModelDeviceState> = + Either.catch { grpc.getDevice(Empty.getDefaultInstance()) } + .map { it.toDomain() } + .mapLeft { GetDeviceStateError.Unknown(it) } + + suspend fun getDeviceList(token: AccountToken): Either<GetDeviceListError, List<Device>> = + Either.catch { grpc.listDevices(StringValue.of(token.value)) } + .map { it.devicesList.map(ManagementInterface.Device::toDomain) } + .mapLeft { GetDeviceListError.Unknown(it) } + + suspend fun removeDevice( + token: AccountToken, + deviceId: DeviceId + ): Either<DeleteDeviceError, Unit> = + Either.catch { + grpc.removeDevice( + ManagementInterface.DeviceRemoval.newBuilder() + .setAccountToken(token.value) + .setDeviceId(deviceId.value.toString()) + .build(), + ) + } + .mapEmpty() + .mapLeft { DeleteDeviceError.Unknown(it) } + + suspend fun connect(): Either<ConnectError, Boolean> = + Either.catch { grpc.connectTunnel(Empty.getDefaultInstance()).value } + .mapLeft(ConnectError::Unknown) + + suspend fun disconnect(): Boolean = grpc.disconnectTunnel(Empty.getDefaultInstance()).value + + suspend fun reconnect(): Boolean = grpc.reconnectTunnel(Empty.getDefaultInstance()).value + + private suspend fun getTunnelState(): ModelTunnelState = + grpc.getTunnelState(Empty.getDefaultInstance()).toDomain() + + private suspend fun getSettings(): ModelSettings = + grpc.getSettings(Empty.getDefaultInstance()).toDomain() + + private suspend fun getDeviceState(): ModelDeviceState = + grpc.getDevice(Empty.getDefaultInstance()).toDomain() + + private suspend fun getRelayList(): ModelRelayList = + grpc.getRelayLocations(Empty.getDefaultInstance()).toDomain() + + private suspend fun getVersionInfo(): ModelAppVersionInfo = + grpc.getVersionInfo(Empty.getDefaultInstance()).toDomain() + + suspend fun logoutAccount() { + grpc.logoutAccount(Empty.getDefaultInstance()) + } + + suspend fun loginAccount(accountToken: AccountToken): Either<LoginAccountError, Unit> = + Either.catch { grpc.loginAccount(StringValue.of(accountToken.value)) } + .mapLeftStatus { + when (it.status.code) { + Status.Code.UNAUTHENTICATED -> LoginAccountError.InvalidAccount + Status.Code.RESOURCE_EXHAUSTED -> + LoginAccountError.MaxDevicesReached(accountToken) + Status.Code.UNAVAILABLE -> LoginAccountError.RpcError + else -> LoginAccountError.Unknown(it) + } + } + .mapEmpty() + + suspend fun clearAccountHistory() { + grpc.clearAccountHistory(Empty.getDefaultInstance()) + } + + suspend fun getAccountHistory(): Either<GetAccountHistoryError, AccountToken?> = + Either.catch { + val history = grpc.getAccountHistory(Empty.getDefaultInstance()) + if (history.hasToken()) { + AccountToken(history.token.value) + } else { + null + } + } + .mapLeft(GetAccountHistoryError::Unknown) + + private suspend fun getInitialServiceState() { + withContext(Dispatchers.IO) { + awaitAll( + async { _mutableTunnelState.update { getTunnelState() } }, + async { _mutableDeviceState.update { getDeviceState() } }, + async { _mutableSettings.update { getSettings() } }, + async { _mutableVersionInfo.update { getVersionInfo() } }, + async { _mutableRelayList.update { getRelayList() } }, + ) + } + } + + suspend fun getAccountData( + accountToken: AccountToken + ): Either<GetAccountDataError, AccountData> = + Either.catch { grpc.getAccountData(StringValue.of(accountToken.value)).toDomain() } + .mapLeft(GetAccountDataError::Unknown) + + suspend fun createAccount(): Either<CreateAccountError, AccountToken> = + Either.catch { + val accountTokenStringValue = grpc.createNewAccount(Empty.getDefaultInstance()) + AccountToken(accountTokenStringValue.value) + } + .mapLeft(CreateAccountError::Unknown) + + suspend fun setDnsOptions(dnsOptions: ModelDnsOptions): Either<SetDnsOptionsError, Unit> = + Either.catch { grpc.setDnsOptions(dnsOptions.fromDomain()) } + .mapLeft(SetDnsOptionsError::Unknown) + .mapEmpty() + + suspend fun setDnsState(dnsState: ModelDnsState): Either<SetDnsOptionsError, Unit> = + Either.catch { + val currentDnsOptions = getSettings().tunnelOptions.dnsOptions + val updated = DnsOptions.state.set(currentDnsOptions, dnsState) + grpc.setDnsOptions(updated.fromDomain()) + } + .mapLeft(SetDnsOptionsError::Unknown) + .mapEmpty() + + suspend fun setCustomDns(index: Int, address: InetAddress): Either<SetDnsOptionsError, Unit> = + Either.catch { + val currentDnsOptions = getSettings().tunnelOptions.dnsOptions + val updatedDnsOptions = + DnsOptions.customOptions.addresses + .index(Index.list(), index) + .set(currentDnsOptions, address) + + grpc.setDnsOptions(updatedDnsOptions.fromDomain()) + } + .mapLeft(SetDnsOptionsError::Unknown) + .mapEmpty() + + suspend fun addCustomDns(address: InetAddress): Either<SetDnsOptionsError, Unit> = + Either.catch { + val currentDnsOptions = getSettings().tunnelOptions.dnsOptions + val updatedDnsOptions = + DnsOptions.customOptions.addresses.modify(currentDnsOptions) { it + address } + grpc.setDnsOptions(updatedDnsOptions.fromDomain()) + } + .mapLeft(SetDnsOptionsError::Unknown) + .mapEmpty() + + suspend fun deleteCustomDns(address: InetAddress): Either<SetDnsOptionsError, Unit> = + Either.catch { + val currentDnsOptions = getSettings().tunnelOptions.dnsOptions + val updatedDnsOptions = + DnsOptions.customOptions.addresses.modify(currentDnsOptions) { it - address } + grpc.setDnsOptions(updatedDnsOptions.fromDomain()) + } + .mapLeft(SetDnsOptionsError::Unknown) + .mapEmpty() + + suspend fun setWireguardMtu(value: Int): Either<SetWireguardMtuError, Unit> = + Either.catch { grpc.setWireguardMtu(UInt32Value.of(value)) } + .mapLeft(SetWireguardMtuError::Unknown) + .mapEmpty() + + suspend fun resetWireguardMtu(): Either<SetWireguardMtuError, Unit> = + Either.catch { grpc.setWireguardMtu(UInt32Value.newBuilder().clearValue().build()) } + .mapLeft(SetWireguardMtuError::Unknown) + .mapEmpty() + + suspend fun setWireguardQuantumResistant( + value: ModelQuantumResistantState + ): Either<SetWireguardQuantumResistantError, Unit> = + Either.catch { grpc.setQuantumResistantTunnel(value.toDomain()) } + .mapLeft(SetWireguardQuantumResistantError::Unknown) + .mapEmpty() + + // Todo needs to be more advanced + suspend fun setRelaySettings(value: RelaySettings) { + grpc.setRelaySettings(value.fromDomain()) + } + + suspend fun setObfuscationOptions( + value: ModelObfuscationSettings + ): Either<SetObfuscationOptionsError, Unit> = + Either.catch { grpc.setObfuscationSettings(value.fromDomain()) } + .mapLeft(SetObfuscationOptionsError::Unknown) + .mapEmpty() + + suspend fun setAutoConnect(isEnabled: Boolean): Either<SetAutoConnectError, Unit> = + Either.catch { grpc.setAutoConnect(BoolValue.of(isEnabled)) } + .mapLeft(SetAutoConnectError::Unknown) + .mapEmpty() + + suspend fun setAllowLan(allow: Boolean): Either<SetAllowLanError, Unit> = + Either.catch { grpc.setAllowLan(BoolValue.of(allow)) } + .mapLeft(SetAllowLanError::Unknown) + .mapEmpty() + + suspend fun setRelayLocation(location: ModelRelayItemId): Either<SetRelayLocationError, Unit> = + Either.catch { + val currentRelaySettings = getSettings().relaySettings + val updatedRelaySettings = + RelaySettings.relayConstraints.location.set( + currentRelaySettings, + Constraint.Only(location), + ) + grpc.setRelaySettings(updatedRelaySettings.fromDomain()) + } + .mapLeft(SetRelayLocationError::Unknown) + .mapEmpty() + + suspend fun createCustomList( + name: CustomListName + ): Either<CreateCustomListError, CustomListId> = + Either.catch { grpc.createCustomList(StringValue.of(name.value)) } + .map { CustomListId(it.value) } + .mapLeftStatus { + when (it.status.code) { + Status.Code.ALREADY_EXISTS -> CustomListAlreadyExists + else -> UnknownCustomListError(it) + } + } + + suspend fun updateCustomList(customList: ModelCustomList): Either<UpdateCustomListError, Unit> = + Either.catch { grpc.updateCustomList(customList.fromDomain()) } + .mapLeft(::UnknownCustomListError) + .mapEmpty() + + suspend fun deleteCustomList(id: CustomListId): Either<DeleteCustomListError, Unit> = + Either.catch { grpc.deleteCustomList(StringValue.of(id.value)) } + .mapLeft(::UnknownCustomListError) + .mapEmpty() + + suspend fun clearAllRelayOverrides(): Either<ClearAllOverridesError, Unit> = + Either.catch { grpc.clearAllRelayOverrides(Empty.getDefaultInstance()) } + .mapLeft(ClearAllOverridesError::Unknown) + .mapEmpty() + + suspend fun applySettingsPatch(json: String): Either<SettingsPatchError, Unit> = + Either.catch { grpc.applyJsonSettings(StringValue.of(json)) } + .mapLeftStatus { + when (it.status.code) { + // Currently we only get invalid argument errors from daemon via gRPC + Status.Code.INVALID_ARGUMENT -> SettingsPatchError.ParsePatch + else -> SettingsPatchError.ApplyPatch + } + } + .mapEmpty() + + suspend fun setWireguardConstraints( + value: ModelWireguardConstraints + ): Either<SetWireguardConstraintsError, Unit> = + Either.catch { + val relaySettings = getSettings().relaySettings + val updated = + RelaySettings.relayConstraints.wireguardConstraints.set(relaySettings, value) + grpc.setRelaySettings(updated.fromDomain()) + } + .mapLeft(SetWireguardConstraintsError::Unknown) + .mapEmpty() + + suspend fun setOwnershipAndProviders( + ownershipConstraint: Constraint<ModelOwnership>, + providersConstraint: Constraint<Providers> + ): Either<SetWireguardConstraintsError, Unit> = + Either.catch { + val relaySettings = getSettings().relaySettings + val updated = + relaySettings.copy { + inside(RelaySettings.relayConstraints) { + RelayConstraints.providers set providersConstraint + RelayConstraints.ownership set ownershipConstraint + } + } + grpc.setRelaySettings(updated.fromDomain()) + } + .mapLeft(SetWireguardConstraintsError::Unknown) + .mapEmpty() + + suspend fun setOwnership( + ownership: Constraint<ModelOwnership> + ): Either<SetWireguardConstraintsError, Unit> = + Either.catch { + val relaySettings = getSettings().relaySettings + val updated = RelaySettings.relayConstraints.ownership.set(relaySettings, ownership) + grpc.setRelaySettings(updated.fromDomain()) + } + .mapLeft(SetWireguardConstraintsError::Unknown) + .mapEmpty() + + suspend fun setProviders( + providersConstraint: Constraint<Providers> + ): Either<SetWireguardConstraintsError, Unit> = + Either.catch { + val relaySettings = getSettings().relaySettings + val updated = + RelaySettings.relayConstraints.providers.set(relaySettings, providersConstraint) + grpc.setRelaySettings(updated.fromDomain()) + } + .mapLeft(SetWireguardConstraintsError::Unknown) + .mapEmpty() + + suspend fun submitVoucher(voucher: String): Either<RedeemVoucherError, RedeemVoucherSuccess> = + Either.catch { grpc.submitVoucher(StringValue.of(voucher)).toDomain() } + .mapLeftStatus { + when (it.status.code) { + Status.Code.INVALID_ARGUMENT, + Status.Code.NOT_FOUND -> RedeemVoucherError.InvalidVoucher + Status.Code.ALREADY_EXISTS, + Status.Code.RESOURCE_EXHAUSTED -> RedeemVoucherError.VoucherAlreadyUsed + Status.Code.UNAVAILABLE -> RedeemVoucherError.RpcError + else -> RedeemVoucherError.Unknown(it) + } + } + + suspend fun initializePlayPurchase(): Either<PlayPurchaseInitError, PlayPurchasePaymentToken> = + Either.catch { grpc.initPlayPurchase(Empty.getDefaultInstance()).toDomain() } + .mapLeft { PlayPurchaseInitError.OtherError } + + suspend fun verifyPlayPurchase(purchase: PlayPurchase): Either<PlayPurchaseVerifyError, Unit> = + Either.catch { grpc.verifyPlayPurchase(purchase.fromDomain()) } + .mapLeft { PlayPurchaseVerifyError.OtherError } + .mapEmpty() + + suspend fun addSplitTunnelingApp(app: AppId): Either<AddSplitTunnelingAppError, Unit> = + Either.catch { grpc.addSplitTunnelApp(StringValue.of(app.value)) } + .mapLeft(AddSplitTunnelingAppError::Unknown) + .mapEmpty() + + suspend fun removeSplitTunnelingApp(app: AppId): Either<RemoveSplitTunnelingAppError, Unit> = + Either.catch { grpc.removeSplitTunnelApp(StringValue.of(app.value)) } + .mapLeft(RemoveSplitTunnelingAppError::Unknown) + .mapEmpty() + + suspend fun setSplitTunnelingState( + enabled: Boolean + ): Either<RemoveSplitTunnelingAppError, Unit> = + Either.catch { grpc.setSplitTunnelState(BoolValue.of(enabled)) } + .mapLeft(RemoveSplitTunnelingAppError::Unknown) + .mapEmpty() + + suspend fun getWebsiteAuthToken(): Either<Throwable, WebsiteAuthToken> = + Either.catch { grpc.getWwwAuthToken(Empty.getDefaultInstance()) } + .map { WebsiteAuthToken.fromString(it.value) } + + private fun <A> Either<A, Empty>.mapEmpty() = map {} + + private inline fun <B, C> Either<Throwable, B>.mapLeftStatus( + f: (StatusException) -> C + ): Either<C, B> = mapLeft { + if (it is StatusException) { + f(it) + } else { + throw it + } + } +} + +sealed interface GrpcConnectivityState { + data object Connecting : GrpcConnectivityState + + data object Ready : GrpcConnectivityState + + data object Idle : GrpcConnectivityState + + data object TransientFailure : GrpcConnectivityState + + data object Shutdown : GrpcConnectivityState +} diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparator.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparator.kt new file mode 100644 index 0000000000..a1b1d3b092 --- /dev/null +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparator.kt @@ -0,0 +1,33 @@ +package net.mullvad.mullvadvpn.lib.daemon.grpc + +import net.mullvad.mullvadvpn.lib.model.RelayItem + +internal object RelayNameComparator : Comparator<RelayItem.Location.Relay> { + override fun compare(o1: RelayItem.Location.Relay, o2: RelayItem.Location.Relay): Int { + val partitions1 = o1.name.split(regex) + val partitions2 = o2.name.split(regex) + return if (partitions1.size > partitions2.size) partitions1 compareWith partitions2 + else -(partitions2 compareWith partitions1) + } + + private infix fun List<String>.compareWith(other: List<String>): Int { + this.forEachIndexed { index, s -> + if (other.size <= index) return 1 + val partsCompareResult = compareStringOrInt(other[index], s) + if (partsCompareResult != 0) return partsCompareResult + } + return 0 + } + + private fun compareStringOrInt(s1: String, s2: String): Int { + val int1 = s1.toIntOrNull() + val int2 = s2.toIntOrNull() + return if (int1 == null || int2 == null || int1 == int2) { + s2.compareTo(s1) + } else { + int2.compareTo(int1) + } + } + + private val regex = "(?<=\\d)(?=\\D)|(?<=\\D)(?=\\d)".toRegex() +} diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt new file mode 100644 index 0000000000..df4625228f --- /dev/null +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/FromDomain.kt @@ -0,0 +1,140 @@ +package net.mullvad.mullvadvpn.lib.daemon.grpc.mapper + +import mullvad_daemon.management_interface.ManagementInterface +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions +import net.mullvad.mullvadvpn.lib.model.CustomList +import net.mullvad.mullvadvpn.lib.model.CustomListId +import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions +import net.mullvad.mullvadvpn.lib.model.DnsOptions +import net.mullvad.mullvadvpn.lib.model.DnsState +import net.mullvad.mullvadvpn.lib.model.GeoLocationId +import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings +import net.mullvad.mullvadvpn.lib.model.Ownership +import net.mullvad.mullvadvpn.lib.model.PlayPurchase +import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken +import net.mullvad.mullvadvpn.lib.model.Port +import net.mullvad.mullvadvpn.lib.model.Providers +import net.mullvad.mullvadvpn.lib.model.RelayItemId +import net.mullvad.mullvadvpn.lib.model.RelaySettings +import net.mullvad.mullvadvpn.lib.model.WireguardConstraints + +internal fun Constraint<RelayItemId>.fromDomain(): ManagementInterface.LocationConstraint = + ManagementInterface.LocationConstraint.newBuilder() + .apply { + when (this@fromDomain) { + is Constraint.Any -> {} + is Constraint.Only -> { + when (val relayItemId = value) { + is CustomListId -> setCustomList(relayItemId.value) + is GeoLocationId -> setLocation(relayItemId.fromDomain()) + } + } + } + } + .build() + +internal fun Constraint<Providers>.fromDomain(): List<String> = + when (this) { + is Constraint.Any -> emptyList() + is Constraint.Only -> value.providers.map { it.value } + } + +internal fun DnsOptions.fromDomain(): ManagementInterface.DnsOptions = + ManagementInterface.DnsOptions.newBuilder() + .setState(state.fromDomain()) + .setCustomOptions(customOptions.fromDomain()) + .setDefaultOptions(defaultOptions.fromDomain()) + .build() + +internal fun DnsState.fromDomain(): ManagementInterface.DnsOptions.DnsState = + when (this) { + DnsState.Default -> ManagementInterface.DnsOptions.DnsState.DEFAULT + DnsState.Custom -> ManagementInterface.DnsOptions.DnsState.CUSTOM + } + +internal fun CustomDnsOptions.fromDomain(): ManagementInterface.CustomDnsOptions = + ManagementInterface.CustomDnsOptions.newBuilder() + .addAllAddresses(addresses.map { it.hostAddress }) + .build() + +internal fun DefaultDnsOptions.fromDomain(): ManagementInterface.DefaultDnsOptions = + ManagementInterface.DefaultDnsOptions.newBuilder() + .setBlockAds(blockAds) + .setBlockGambling(blockGambling) + .setBlockMalware(blockMalware) + .setBlockTrackers(blockTrackers) + .setBlockAdultContent(blockAdultContent) + .setBlockSocialMedia(blockSocialMedia) + .build() + +internal fun ObfuscationSettings.fromDomain(): ManagementInterface.ObfuscationSettings = + ManagementInterface.ObfuscationSettings.newBuilder() + .setSelectedObfuscation(selectedObfuscation.toDomain()) + .setUdp2Tcp(udp2tcp.toDomain()) + .build() + +internal fun GeoLocationId.fromDomain(): ManagementInterface.GeographicLocationConstraint = + ManagementInterface.GeographicLocationConstraint.newBuilder() + .apply { + when (val id = this@fromDomain) { + is GeoLocationId.Country -> setCountry(id.countryCode) + is GeoLocationId.City -> setCountry(id.countryCode.countryCode).setCity(id.cityCode) + is GeoLocationId.Hostname -> + setCountry(id.country.countryCode) + .setCity(id.city.cityCode) + .setHostname(id.hostname) + } + } + .build() + +internal fun CustomList.fromDomain(): ManagementInterface.CustomList = + ManagementInterface.CustomList.newBuilder() + .setId(id.value) + .setName(name.value) + .addAllLocations(locations.map { it.fromDomain() }) + .build() + +internal fun WireguardConstraints.fromDomain(): ManagementInterface.WireguardConstraints = + when (port) { + is Constraint.Any -> ManagementInterface.WireguardConstraints.newBuilder().build() + is Constraint.Only -> + ManagementInterface.WireguardConstraints.newBuilder() + .setPort((port as Constraint.Only<Port>).value.value) + .build() + } + +internal fun Ownership.fromDomain(): ManagementInterface.Ownership = + when (this) { + Ownership.MullvadOwned -> ManagementInterface.Ownership.MULLVAD_OWNED + Ownership.Rented -> ManagementInterface.Ownership.RENTED + } + +internal fun RelaySettings.fromDomain(): ManagementInterface.RelaySettings = + ManagementInterface.RelaySettings.newBuilder() + .setNormal( + ManagementInterface.NormalRelaySettings.newBuilder() + .setTunnelType(ManagementInterface.TunnelType.WIREGUARD) + .setWireguardConstraints(relayConstraints.wireguardConstraints.fromDomain()) + .setOpenvpnConstraints(ManagementInterface.OpenvpnConstraints.getDefaultInstance()) + .setLocation(relayConstraints.location.fromDomain()) + .setOwnership(relayConstraints.ownership.fromDomain()) + .addAllProviders(relayConstraints.providers.fromDomain()) + .build() + ) + .build() + +internal fun Constraint<Ownership>.fromDomain(): ManagementInterface.Ownership = + when (this) { + Constraint.Any -> ManagementInterface.Ownership.ANY + is Constraint.Only -> value.fromDomain() + } + +internal fun PlayPurchasePaymentToken.fromDomain(): ManagementInterface.PlayPurchasePaymentToken = + ManagementInterface.PlayPurchasePaymentToken.newBuilder().setToken(value).build() + +internal fun PlayPurchase.fromDomain(): ManagementInterface.PlayPurchase = + ManagementInterface.PlayPurchase.newBuilder() + .setPurchaseToken(purchaseToken.fromDomain()) + .setProductId(productId) + .build() diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt new file mode 100644 index 0000000000..636e6c5176 --- /dev/null +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/mapper/ToDomain.kt @@ -0,0 +1,540 @@ +@file:Suppress("TooManyFunctions") + +package net.mullvad.mullvadvpn.lib.daemon.grpc.mapper + +import io.grpc.ConnectivityState +import java.net.InetAddress +import java.net.InetSocketAddress +import java.util.UUID +import mullvad_daemon.management_interface.ManagementInterface +import net.mullvad.mullvadvpn.lib.daemon.grpc.GrpcConnectivityState +import net.mullvad.mullvadvpn.lib.daemon.grpc.RelayNameComparator +import net.mullvad.mullvadvpn.lib.model.AccountData +import net.mullvad.mullvadvpn.lib.model.AccountId +import net.mullvad.mullvadvpn.lib.model.AccountToken +import net.mullvad.mullvadvpn.lib.model.ActionAfterDisconnect +import net.mullvad.mullvadvpn.lib.model.AppId +import net.mullvad.mullvadvpn.lib.model.AppVersionInfo +import net.mullvad.mullvadvpn.lib.model.Constraint +import net.mullvad.mullvadvpn.lib.model.CustomDnsOptions +import net.mullvad.mullvadvpn.lib.model.CustomList +import net.mullvad.mullvadvpn.lib.model.CustomListId +import net.mullvad.mullvadvpn.lib.model.CustomListName +import net.mullvad.mullvadvpn.lib.model.DefaultDnsOptions +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.DnsOptions +import net.mullvad.mullvadvpn.lib.model.DnsState +import net.mullvad.mullvadvpn.lib.model.Endpoint +import net.mullvad.mullvadvpn.lib.model.ErrorState +import net.mullvad.mullvadvpn.lib.model.ErrorStateCause +import net.mullvad.mullvadvpn.lib.model.GeoIpLocation +import net.mullvad.mullvadvpn.lib.model.GeoLocationId +import net.mullvad.mullvadvpn.lib.model.Mtu +import net.mullvad.mullvadvpn.lib.model.ObfuscationEndpoint +import net.mullvad.mullvadvpn.lib.model.ObfuscationSettings +import net.mullvad.mullvadvpn.lib.model.ObfuscationType +import net.mullvad.mullvadvpn.lib.model.Ownership +import net.mullvad.mullvadvpn.lib.model.ParameterGenerationError +import net.mullvad.mullvadvpn.lib.model.PlayPurchasePaymentToken +import net.mullvad.mullvadvpn.lib.model.Port +import net.mullvad.mullvadvpn.lib.model.PortRange +import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId +import net.mullvad.mullvadvpn.lib.model.Providers +import net.mullvad.mullvadvpn.lib.model.QuantumResistantState +import net.mullvad.mullvadvpn.lib.model.RedeemVoucherSuccess +import net.mullvad.mullvadvpn.lib.model.RelayConstraints +import net.mullvad.mullvadvpn.lib.model.RelayItem +import net.mullvad.mullvadvpn.lib.model.RelayItemId +import net.mullvad.mullvadvpn.lib.model.RelayList +import net.mullvad.mullvadvpn.lib.model.RelayOverride +import net.mullvad.mullvadvpn.lib.model.RelaySettings +import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation +import net.mullvad.mullvadvpn.lib.model.Settings +import net.mullvad.mullvadvpn.lib.model.SplitTunnelSettings +import net.mullvad.mullvadvpn.lib.model.TransportProtocol +import net.mullvad.mullvadvpn.lib.model.TunnelEndpoint +import net.mullvad.mullvadvpn.lib.model.TunnelOptions +import net.mullvad.mullvadvpn.lib.model.TunnelState +import net.mullvad.mullvadvpn.lib.model.Udp2TcpObfuscationSettings +import net.mullvad.mullvadvpn.lib.model.WireguardConstraints +import net.mullvad.mullvadvpn.lib.model.WireguardEndpointData +import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions +import org.joda.time.Instant + +internal fun ManagementInterface.TunnelState.toDomain(): TunnelState = + when (stateCase!!) { + ManagementInterface.TunnelState.StateCase.DISCONNECTED -> + TunnelState.Disconnected( + location = + with(disconnected) { + if (hasDisconnectedLocation()) { + disconnectedLocation.toDomain() + } else null + }, + ) + ManagementInterface.TunnelState.StateCase.CONNECTING -> + TunnelState.Connecting( + endpoint = connecting.relayInfo.tunnelEndpoint.toDomain(), + location = + with(connecting.relayInfo) { + if (hasLocation()) { + location.toDomain() + } else null + } + ) + ManagementInterface.TunnelState.StateCase.CONNECTED -> + TunnelState.Connected( + endpoint = connected.relayInfo.tunnelEndpoint.toDomain(), + location = + with(connected.relayInfo) { + if (hasLocation()) { + location.toDomain() + } else { + null + } + } + ) + ManagementInterface.TunnelState.StateCase.DISCONNECTING -> + TunnelState.Disconnecting( + actionAfterDisconnect = disconnecting.afterDisconnect.toDomain(), + ) + ManagementInterface.TunnelState.StateCase.ERROR -> + TunnelState.Error(errorState = error.errorState.toDomain()) + ManagementInterface.TunnelState.StateCase.STATE_NOT_SET -> + TunnelState.Disconnected( + location = disconnected.disconnectedLocation.toDomain(), + ) + } + +internal fun ManagementInterface.GeoIpLocation.toDomain(): GeoIpLocation = + GeoIpLocation( + ipv4 = + if (hasIpv4()) { + InetAddress.getByName(ipv4) + } else { + null + }, + ipv6 = + if (hasIpv6()) { + InetAddress.getByName(ipv6) + } else { + null + }, + country = country, + city = city, + latitude = latitude, + longitude = longitude, + hostname = hostname + ) + +internal fun ManagementInterface.TunnelEndpoint.toDomain(): TunnelEndpoint = + TunnelEndpoint( + endpoint = + with(address) { + val indexOfSeparator = indexOfLast { it == ':' } + val ipPart = + address.substring(0, indexOfSeparator).filter { it !in listOf('[', ']') } + val portPart = address.substring(indexOfSeparator + 1) + + Endpoint( + address = InetSocketAddress(InetAddress.getByName(ipPart), portPart.toInt()), + protocol = protocol.toDomain() + ) + }, + quantumResistant = quantumResistant, + obfuscation = + if (hasObfuscation()) { + obfuscation.toDomain() + } else { + null + } + ) + +internal fun ManagementInterface.ObfuscationEndpoint.toDomain(): ObfuscationEndpoint = + ObfuscationEndpoint( + endpoint = + Endpoint(address = InetSocketAddress(address, port), protocol = protocol.toDomain()), + obfuscationType = obfuscationType.toDomain() + ) + +internal fun ManagementInterface.ObfuscationType.toDomain(): ObfuscationType = + when (this) { + ManagementInterface.ObfuscationType.UDP2TCP -> ObfuscationType.Udp2Tcp + ManagementInterface.ObfuscationType.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized obfuscation type") + } + +internal fun ManagementInterface.TransportProtocol.toDomain(): TransportProtocol = + when (this) { + ManagementInterface.TransportProtocol.TCP -> TransportProtocol.Tcp + ManagementInterface.TransportProtocol.UDP -> TransportProtocol.Udp + ManagementInterface.TransportProtocol.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized transport protocol") + } + +internal fun ManagementInterface.AfterDisconnect.toDomain(): ActionAfterDisconnect = + when (this) { + ManagementInterface.AfterDisconnect.NOTHING -> ActionAfterDisconnect.Nothing + ManagementInterface.AfterDisconnect.RECONNECT -> ActionAfterDisconnect.Reconnect + ManagementInterface.AfterDisconnect.BLOCK -> ActionAfterDisconnect.Block + ManagementInterface.AfterDisconnect.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized action after disconnect") + } + +internal fun ManagementInterface.ErrorState.toDomain(): ErrorState = + ErrorState( + cause = + when (cause!!) { + ManagementInterface.ErrorState.Cause.AUTH_FAILED -> + ErrorStateCause.AuthFailed(authFailedError.name) + ManagementInterface.ErrorState.Cause.IPV6_UNAVAILABLE -> + ErrorStateCause.Ipv6Unavailable + ManagementInterface.ErrorState.Cause.SET_FIREWALL_POLICY_ERROR -> + policyError.toDomain() + ManagementInterface.ErrorState.Cause.SET_DNS_ERROR -> ErrorStateCause.DnsError + ManagementInterface.ErrorState.Cause.START_TUNNEL_ERROR -> + ErrorStateCause.StartTunnelError + ManagementInterface.ErrorState.Cause.TUNNEL_PARAMETER_ERROR -> + ErrorStateCause.TunnelParameterError(parameterError.toDomain()) + ManagementInterface.ErrorState.Cause.IS_OFFLINE -> ErrorStateCause.IsOffline + ManagementInterface.ErrorState.Cause.VPN_PERMISSION_DENIED -> + ErrorStateCause.VpnPermissionDenied + ManagementInterface.ErrorState.Cause.SPLIT_TUNNEL_ERROR -> + ErrorStateCause.StartTunnelError + ManagementInterface.ErrorState.Cause.UNRECOGNIZED, + ManagementInterface.ErrorState.Cause.NEED_FULL_DISK_PERMISSIONS, + ManagementInterface.ErrorState.Cause.CREATE_TUNNEL_DEVICE -> + throw IllegalArgumentException("Unrecognized error state cause") + }, + isBlocking = !hasBlockingError() + ) + +internal fun ManagementInterface.ErrorState.FirewallPolicyError.toDomain(): + ErrorStateCause.FirewallPolicyError = + when (type!!) { + ManagementInterface.ErrorState.FirewallPolicyError.ErrorType.GENERIC -> + ErrorStateCause.FirewallPolicyError.Generic + ManagementInterface.ErrorState.FirewallPolicyError.ErrorType.LOCKED, + ManagementInterface.ErrorState.FirewallPolicyError.ErrorType.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized firewall policy error") + } + +internal fun ManagementInterface.ErrorState.GenerationError.toDomain(): ParameterGenerationError = + when (this) { + ManagementInterface.ErrorState.GenerationError.NO_MATCHING_RELAY -> + ParameterGenerationError.NoMatchingRelay + ManagementInterface.ErrorState.GenerationError.NO_MATCHING_BRIDGE_RELAY -> + ParameterGenerationError.NoMatchingBridgeRelay + ManagementInterface.ErrorState.GenerationError.NO_WIREGUARD_KEY -> + ParameterGenerationError.NoWireguardKey + ManagementInterface.ErrorState.GenerationError.CUSTOM_TUNNEL_HOST_RESOLUTION_ERROR -> + ParameterGenerationError.CustomTunnelHostResultionError + ManagementInterface.ErrorState.GenerationError.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized parameter generation error") + } + +internal fun ManagementInterface.Settings.toDomain(): Settings = + Settings( + relaySettings = relaySettings.toDomain(), + obfuscationSettings = obfuscationSettings.toDomain(), + customLists = customLists.customListsList.map { it.toDomain() }, + allowLan = allowLan, + autoConnect = autoConnect, + tunnelOptions = tunnelOptions.toDomain(), + relayOverrides = relayOverridesList.map { it.toDomain() }, + showBetaReleases = showBetaReleases, + splitTunnelSettings = splitTunnel.toDomain() + ) + +internal fun ManagementInterface.RelayOverride.toDomain(): RelayOverride = + RelayOverride( + hostname = hostname, + ipv4AddressIn = if (hasIpv4AddrIn()) InetAddress.getByName(ipv4AddrIn) else null, + ipv6AddressIn = if (hasIpv6AddrIn()) InetAddress.getByName(ipv6AddrIn) else null + ) + +internal fun ManagementInterface.RelaySettings.toDomain(): RelaySettings = + when (endpointCase) { + ManagementInterface.RelaySettings.EndpointCase.CUSTOM -> + throw IllegalArgumentException("CustomTunnelEndpoint is not supported") + ManagementInterface.RelaySettings.EndpointCase.NORMAL -> RelaySettings(normal.toDomain()) + ManagementInterface.RelaySettings.EndpointCase.ENDPOINT_NOT_SET -> + throw IllegalArgumentException("RelaySettings endpoint not set") + else -> throw NullPointerException("RelaySettings endpoint is null") + } + +internal fun ManagementInterface.NormalRelaySettings.toDomain(): RelayConstraints = + RelayConstraints( + location = location.toDomain(), + providers = providersList.toDomain(), + ownership = ownership.toDomain(), + wireguardConstraints = wireguardConstraints.toDomain() + ) + +internal fun ManagementInterface.LocationConstraint.toDomain(): Constraint<RelayItemId> = + when (typeCase) { + ManagementInterface.LocationConstraint.TypeCase.CUSTOM_LIST -> + Constraint.Only(CustomListId(customList)) + ManagementInterface.LocationConstraint.TypeCase.LOCATION -> + Constraint.Only(location.toDomain()) + ManagementInterface.LocationConstraint.TypeCase.TYPE_NOT_SET -> Constraint.Any + else -> throw IllegalArgumentException("Location constraint type is null") + } + +@Suppress("ReturnCount") +internal fun ManagementInterface.GeographicLocationConstraint.toDomain(): GeoLocationId { + val country = GeoLocationId.Country(country) + if (!hasCity()) { + return country + } + + val city = GeoLocationId.City(country, city) + if (!hasHostname()) { + return city + } + return GeoLocationId.Hostname(city, hostname) +} + +internal fun List<String>.toDomain(): Constraint<Providers> = + if (isEmpty()) Constraint.Any else Constraint.Only(Providers(map { ProviderId(it) }.toSet())) + +internal fun ManagementInterface.WireguardConstraints.toDomain(): WireguardConstraints = + WireguardConstraints( + port = + if (hasPort()) { + Constraint.Only(Port(port)) + } else { + Constraint.Any + }, + ) + +internal fun ManagementInterface.Ownership.toDomain(): Constraint<Ownership> = + when (this) { + ManagementInterface.Ownership.ANY -> Constraint.Any + ManagementInterface.Ownership.MULLVAD_OWNED -> Constraint.Only(Ownership.MullvadOwned) + ManagementInterface.Ownership.RENTED -> Constraint.Only(Ownership.Rented) + ManagementInterface.Ownership.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized ownership") + } + +internal fun ManagementInterface.ObfuscationSettings.toDomain(): ObfuscationSettings = + ObfuscationSettings( + selectedObfuscation = selectedObfuscation.toDomain(), + udp2tcp = udp2Tcp.toDomain() + ) + +internal fun ManagementInterface.ObfuscationSettings.SelectedObfuscation.toDomain(): + SelectedObfuscation = + when (this) { + ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO -> SelectedObfuscation.Auto + ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF -> SelectedObfuscation.Off + ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP -> + SelectedObfuscation.Udp2Tcp + ManagementInterface.ObfuscationSettings.SelectedObfuscation.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized selected obfuscation") + } + +internal fun ManagementInterface.Udp2TcpObfuscationSettings.toDomain(): Udp2TcpObfuscationSettings = + if (hasPort()) { + Udp2TcpObfuscationSettings(Constraint.Only(Port(port))) + } else { + Udp2TcpObfuscationSettings(Constraint.Any) + } + +internal fun ManagementInterface.CustomList.toDomain(): CustomList = + CustomList( + id = CustomListId(id), + name = CustomListName.fromString(name), + locations = locationsList.map { it.toDomain() } + ) + +internal fun ManagementInterface.TunnelOptions.toDomain(): TunnelOptions = + TunnelOptions(wireguard = wireguard.toDomain(), dnsOptions = dnsOptions.toDomain()) + +internal fun ManagementInterface.TunnelOptions.WireguardOptions.toDomain(): WireguardTunnelOptions = + WireguardTunnelOptions( + mtu = if (hasMtu()) Mtu(mtu) else null, + quantumResistant = quantumResistant.toDomain(), + ) + +internal fun ManagementInterface.QuantumResistantState.toDomain(): QuantumResistantState = + when (state) { + ManagementInterface.QuantumResistantState.State.AUTO -> QuantumResistantState.Auto + ManagementInterface.QuantumResistantState.State.ON -> QuantumResistantState.On + ManagementInterface.QuantumResistantState.State.OFF -> QuantumResistantState.Off + ManagementInterface.QuantumResistantState.State.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized quantum resistant state") + else -> throw NullPointerException("Quantum resistant state is null") + } + +internal fun ManagementInterface.DnsOptions.toDomain(): DnsOptions = + DnsOptions( + state = state.toDomain(), + defaultOptions = defaultOptions.toDomain(), + customOptions = customOptions.toDomain() + ) + +internal fun ManagementInterface.DnsOptions.DnsState.toDomain(): DnsState = + when (this) { + ManagementInterface.DnsOptions.DnsState.DEFAULT -> DnsState.Default + ManagementInterface.DnsOptions.DnsState.CUSTOM -> DnsState.Custom + ManagementInterface.DnsOptions.DnsState.UNRECOGNIZED -> + throw IllegalArgumentException("Unrecognized dns state") + } + +internal fun ManagementInterface.DefaultDnsOptions.toDomain() = + DefaultDnsOptions( + blockAds = blockAds, + blockMalware = blockMalware, + blockAdultContent = blockAdultContent, + blockGambling = blockGambling, + blockSocialMedia = blockSocialMedia, + blockTrackers = blockTrackers + ) + +internal fun ManagementInterface.CustomDnsOptions.toDomain() = + CustomDnsOptions(addressesList.map { InetAddress.getByName(it) }) + +internal fun QuantumResistantState.toDomain(): ManagementInterface.QuantumResistantState = + ManagementInterface.QuantumResistantState.newBuilder() + .setState( + when (this) { + QuantumResistantState.Auto -> ManagementInterface.QuantumResistantState.State.AUTO + QuantumResistantState.On -> ManagementInterface.QuantumResistantState.State.ON + QuantumResistantState.Off -> ManagementInterface.QuantumResistantState.State.OFF + } + ) + .build() + +internal fun SelectedObfuscation.toDomain(): + ManagementInterface.ObfuscationSettings.SelectedObfuscation = + when (this) { + SelectedObfuscation.Udp2Tcp -> + ManagementInterface.ObfuscationSettings.SelectedObfuscation.UDP2TCP + SelectedObfuscation.Auto -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.AUTO + SelectedObfuscation.Off -> ManagementInterface.ObfuscationSettings.SelectedObfuscation.OFF + } + +internal fun Udp2TcpObfuscationSettings.toDomain(): ManagementInterface.Udp2TcpObfuscationSettings = + when (val port = port) { + is Constraint.Any -> + ManagementInterface.Udp2TcpObfuscationSettings.newBuilder().clearPort().build() + is Constraint.Only -> + ManagementInterface.Udp2TcpObfuscationSettings.newBuilder() + .setPort(port.value.value) + .build() + } + +internal fun ManagementInterface.AppVersionInfo.toDomain(): AppVersionInfo = + AppVersionInfo( + supported = supported, + suggestedUpgrade = if (hasSuggestedUpgrade()) suggestedUpgrade else null + ) + +internal fun ConnectivityState.toDomain(): GrpcConnectivityState = + when (this) { + ConnectivityState.CONNECTING -> GrpcConnectivityState.Connecting + ConnectivityState.READY -> GrpcConnectivityState.Ready + ConnectivityState.IDLE -> GrpcConnectivityState.Idle + ConnectivityState.TRANSIENT_FAILURE -> GrpcConnectivityState.TransientFailure + ConnectivityState.SHUTDOWN -> GrpcConnectivityState.Shutdown + } + +internal fun ManagementInterface.RelayList.toDomain(): RelayList = + RelayList(countriesList.toDomain(), wireguard.toDomain()) + +internal fun ManagementInterface.WireguardEndpointData.toDomain(): WireguardEndpointData = + WireguardEndpointData(portRangesList.map { it.toDomain() }) + +internal fun ManagementInterface.PortRange.toDomain(): PortRange = PortRange(first..last) + +/** + * Convert from a list of ManagementInterface.RelayListCountry to a model.RelayList. Non-wireguard + * relays are filtered out. So are also cities that only contains non-wireguard relays and countries + * that does not have any cities. Countries, cities and relays are ordered by name. + */ +internal fun List<ManagementInterface.RelayListCountry>.toDomain(): + List<RelayItem.Location.Country> = + map(ManagementInterface.RelayListCountry::toDomain) + .filter { it.cities.isNotEmpty() } + .sortedBy { it.name } + +internal fun ManagementInterface.RelayListCountry.toDomain(): RelayItem.Location.Country { + val countryCode = GeoLocationId.Country(code) + return RelayItem.Location.Country( + countryCode, + name, + false, + citiesList + .map { city -> city.toDomain(countryCode) } + .filter { it.relays.isNotEmpty() } + .sortedBy { it.name } + ) +} + +internal fun ManagementInterface.RelayListCity.toDomain( + countryCode: GeoLocationId.Country +): RelayItem.Location.City { + val cityCode = GeoLocationId.City(countryCode, code) + return RelayItem.Location.City( + name = name, + id = cityCode, + expanded = false, + relays = + relaysList + .filter { it.endpointType == ManagementInterface.Relay.RelayType.WIREGUARD } + .map { it.toDomain(cityCode) } + .sortedWith(RelayNameComparator) + ) +} + +internal fun ManagementInterface.Relay.toDomain( + cityCode: GeoLocationId.City +): RelayItem.Location.Relay = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(cityCode, hostname), + active = active, + provider = + Provider( + ProviderId(provider), + ownership = if (owned) Ownership.MullvadOwned else Ownership.Rented + ) + ) + +internal fun ManagementInterface.Device.toDomain(): Device = + Device(DeviceId.fromString(id), name, Instant.ofEpochSecond(created.seconds).toDateTime()) + +internal fun ManagementInterface.DeviceState.toDomain(): DeviceState = + when (state) { + ManagementInterface.DeviceState.State.LOGGED_IN -> + DeviceState.LoggedIn(AccountToken(device.accountToken), device.device.toDomain()) + ManagementInterface.DeviceState.State.LOGGED_OUT -> DeviceState.LoggedOut + ManagementInterface.DeviceState.State.REVOKED -> DeviceState.Revoked + ManagementInterface.DeviceState.State.UNRECOGNIZED -> + throw IllegalArgumentException("Non valid device state") + else -> throw NullPointerException("Device state is null") + } + +internal fun ManagementInterface.AccountData.toDomain(): AccountData = + AccountData( + AccountId(UUID.fromString(id)), + expiryDate = Instant.ofEpochSecond(expiry.seconds).toDateTime() + ) + +internal fun ManagementInterface.VoucherSubmission.toDomain(): RedeemVoucherSuccess = + RedeemVoucherSuccess( + timeAdded = secondsAdded, + newExpiryDate = Instant.ofEpochSecond(newExpiry.seconds).toDateTime() + ) + +internal fun ManagementInterface.SplitTunnelSettings.toDomain(): SplitTunnelSettings = + SplitTunnelSettings( + enabled = enableExclusions, + excludedApps = appsList.map { AppId(it) }.toSet() + ) + +internal fun ManagementInterface.PlayPurchasePaymentToken.toDomain(): PlayPurchasePaymentToken = + PlayPurchasePaymentToken(value = token) diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt new file mode 100644 index 0000000000..fde87ecdd5 --- /dev/null +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/LogInterceptor.kt @@ -0,0 +1,20 @@ +package net.mullvad.mullvadvpn.lib.daemon.grpc.util + +import android.util.Log +import io.grpc.CallOptions +import io.grpc.Channel +import io.grpc.ClientCall +import io.grpc.ClientInterceptor +import io.grpc.MethodDescriptor +import net.mullvad.mullvadvpn.lib.common.constant.TAG + +internal class LogInterceptor : ClientInterceptor { + override fun <ReqT : Any?, RespT : Any?> interceptCall( + method: MethodDescriptor<ReqT, RespT>?, + callOptions: CallOptions?, + next: Channel? + ): ClientCall<ReqT, RespT> { + Log.d(TAG, "Intercepted call: ${method?.fullMethodName}") + return next!!.newCall(method, callOptions) + } +} diff --git a/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt new file mode 100644 index 0000000000..3f98ae93d8 --- /dev/null +++ b/android/lib/daemon-grpc/src/main/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/util/ManagedChannel.kt @@ -0,0 +1,24 @@ +package net.mullvad.mullvadvpn.lib.daemon.grpc.util + +import io.grpc.ConnectivityState +import io.grpc.ManagedChannel +import kotlin.coroutines.resume +import kotlin.coroutines.suspendCoroutine +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.callbackFlow +import kotlinx.coroutines.isActive + +internal fun ManagedChannel.connectivityFlow(): Flow<ConnectivityState> { + return callbackFlow { + var currentState = getState(false) + send(currentState) + + while (isActive) { + currentState = + suspendCoroutine<ConnectivityState> { + notifyWhenStateChanged(currentState) { it.resume(getState(false)) } + } + send(currentState) + } + } +} diff --git a/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt new file mode 100644 index 0000000000..42cf745510 --- /dev/null +++ b/android/lib/daemon-grpc/src/test/kotlin/net/mullvad/mullvadvpn/lib/daemon/grpc/RelayNameComparatorTest.kt @@ -0,0 +1,282 @@ +package net.mullvad.mullvadvpn.lib.daemon.grpc + +import io.mockk.mockk +import io.mockk.unmockkAll +import net.mullvad.mullvadvpn.lib.model.GeoLocationId +import net.mullvad.mullvadvpn.lib.model.Ownership +import net.mullvad.mullvadvpn.lib.model.Provider +import net.mullvad.mullvadvpn.lib.model.ProviderId +import net.mullvad.mullvadvpn.lib.model.RelayItem +import org.junit.jupiter.api.AfterEach +import org.junit.jupiter.api.Assertions.assertTrue +import org.junit.jupiter.api.Test + +class RelayNameComparatorTest { + + @AfterEach + fun tearDown() { + unmockkAll() + } + + @Test + fun `given two relays with same prefix but different numbers comparator should return lowest number first`() { + val relay9 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se9-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ), + ) + val relay10 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se10-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + relay9 assertOrderBothDirection relay10 + } + + @Test + fun `given two relays with same name with number in name comparator should return 0`() { + val relay9a = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se9-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay9b = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se9-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) + assertTrue(RelayNameComparator.compare(relay9b, relay9a) == 0) + } + + @Test + fun `comparator should be able to handle name of only numbers`() { + val relay001 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "001"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay1 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "1"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay3 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "3"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay100 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "100"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + relay001 assertOrderBothDirection relay1 + relay001 assertOrderBothDirection relay3 + relay1 assertOrderBothDirection relay3 + relay3 assertOrderBothDirection relay100 + } + + @Test + fun `given two relays with same name and without number comparator should return 0`() { + val relay9a = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay9b = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + assertTrue(RelayNameComparator.compare(relay9a, relay9b) == 0) + assertTrue(RelayNameComparator.compare(relay9b, relay9a) == 0) + } + + @Test + fun `given two relays with leading zeroes comparator should return lowest number first`() { + val relay001 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se001-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay005 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se005-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + relay001 assertOrderBothDirection relay005 + } + + @Test + fun `given 4 relays comparator should sort by prefix then number`() { + val relayAr2 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "ar2-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relayAr8 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "ar8-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relaySe5 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se5-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relaySe10 = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se10-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + relayAr2 assertOrderBothDirection relayAr8 + relayAr8 assertOrderBothDirection relaySe5 + relaySe5 assertOrderBothDirection relaySe10 + } + + @Test + fun `given two relays with same prefix and number comparator should sort by suffix`() { + val relay2c = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se2-cloud"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay2w = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se2-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + relay2c assertOrderBothDirection relay2w + } + + @Test + fun `given two relays with same prefix, but one with no suffix, the one with no suffix should come first`() { + val relay22a = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se22"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + val relay22b = + RelayItem.Location.Relay( + id = GeoLocationId.Hostname(city = mockk(), "se22-wireguard"), + active = false, + provider = + Provider( + providerId = ProviderId("Provider"), + ownership = Ownership.MullvadOwned + ) + ) + + relay22a assertOrderBothDirection relay22b + } + + private infix fun RelayItem.Location.Relay.assertOrderBothDirection( + other: RelayItem.Location.Relay + ) { + assertTrue(RelayNameComparator.compare(this, other) < 0) + assertTrue(RelayNameComparator.compare(other, this) > 0) + } +} diff --git a/android/lib/ipc/build.gradle.kts b/android/lib/intent-provider/build.gradle.kts index 35fa3c4f1e..f63a9c7f69 100644 --- a/android/lib/ipc/build.gradle.kts +++ b/android/lib/intent-provider/build.gradle.kts @@ -2,17 +2,13 @@ plugins { id(Dependencies.Plugin.androidLibraryId) id(Dependencies.Plugin.kotlinAndroidId) id(Dependencies.Plugin.kotlinParcelizeId) - id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5 } android { - namespace = "net.mullvad.mullvadvpn.lib.ipc" + namespace = "net.mullvad.mullvadvpn.lib.intent" compileSdk = Versions.Android.compileSdkVersion - defaultConfig { - minSdk = Versions.Android.minSdkVersion - testInstrumentationRunner = "androidx.test.runner.AndroidJUnitRunner" - } + defaultConfig { minSdk = Versions.Android.minSdkVersion } compileOptions { sourceCompatibility = JavaVersion.VERSION_17 @@ -26,16 +22,10 @@ android { abortOnError = true warningsAsErrors = true } + buildFeatures { buildConfig = true } } dependencies { - implementation(project(Dependencies.Mullvad.modelLib)) - implementation(Dependencies.Kotlin.stdlib) implementation(Dependencies.KotlinX.coroutinesAndroid) - - androidTestImplementation(Dependencies.junitApi) - androidTestImplementation(Dependencies.junitEngine) - androidTestImplementation(Dependencies.AndroidX.testRunner) - androidTestImplementation(Dependencies.Kotlin.test) } diff --git a/android/lib/intent-provider/src/main/AndroidManifest.xml b/android/lib/intent-provider/src/main/AndroidManifest.xml new file mode 100644 index 0000000000..cc947c5679 --- /dev/null +++ b/android/lib/intent-provider/src/main/AndroidManifest.xml @@ -0,0 +1 @@ +<manifest /> diff --git a/android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt b/android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt new file mode 100644 index 0000000000..86ad970b5d --- /dev/null +++ b/android/lib/intent-provider/src/main/kotlin/net/mullvad/mullvadvpn/lib/intent/IntentProvider.kt @@ -0,0 +1,16 @@ +package net.mullvad.mullvadvpn.lib.intent + +import android.content.Intent +import kotlinx.coroutines.flow.Flow +import kotlinx.coroutines.flow.MutableStateFlow + +class IntentProvider { + private val _intents = MutableStateFlow<Intent?>(null) + val intents: Flow<Intent?> = _intents + + fun setStartIntent(intent: Intent?) { + _intents.tryEmit(intent) + } + + fun getLatestIntent(): Intent? = _intents.value +} diff --git a/android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt b/android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt deleted file mode 100644 index a125af6059..0000000000 --- a/android/lib/ipc/src/androidTest/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlowTest.kt +++ /dev/null @@ -1,44 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import android.os.Bundle -import android.os.Looper -import android.os.Message -import android.os.Parcelable -import kotlin.test.assertEquals -import kotlinx.coroutines.flow.take -import kotlinx.coroutines.flow.toList -import kotlinx.coroutines.runBlocking -import kotlinx.parcelize.Parcelize -import org.junit.jupiter.api.Test - -class HandlerFlowTest { - val looper by lazy { Looper.getMainLooper() } - - val handler: HandlerFlow<Data?> by lazy { - HandlerFlow(looper) { message -> message.data.getParcelable(DATA_KEY) } - } - - @Test - fun test_message_extraction() { - sendMessage(Data(1)) - sendMessage(Data(2)) - sendMessage(Data(3)) - - val extractedData = runBlocking { handler.take(3).toList() } - - assertEquals(listOf(Data(1), Data(2), Data(3)), extractedData) - } - - private fun sendMessage(messageData: Data) { - val message = - Message().apply { data = Bundle().apply { putParcelable(DATA_KEY, messageData) } } - - handler.handleMessage(message) - } - - companion object { - const val DATA_KEY = "data" - - @Parcelize data class Data(val id: Int) : Parcelable - } -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt deleted file mode 100644 index efaa1b78f8..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/DispatchingHandler.kt +++ /dev/null @@ -1,53 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.util.Log -import java.util.concurrent.locks.ReentrantReadWriteLock -import kotlin.concurrent.withLock -import kotlin.reflect.KClass -import kotlinx.coroutines.flow.MutableSharedFlow -import kotlinx.coroutines.flow.asSharedFlow - -class DispatchingHandler<T : Any>(looper: Looper, private val extractor: (Message) -> T?) : - Handler(looper), MessageDispatcher<T> { - private val handlers = HashMap<KClass<out T>, (T) -> Unit>() - private val lock = ReentrantReadWriteLock() - - private val _parsedMessages = - MutableSharedFlow<T>(extraBufferCapacity = MESSAGES_BUFFER_CAPACITY) - val parsedMessages = _parsedMessages.asSharedFlow() - - @Deprecated("Use parsedMessages instead.") - override fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) { - lock.writeLock().withLock { - handlers.put(variant) { instance -> @Suppress("UNCHECKED_CAST") handler(instance as V) } - } - } - - override fun handleMessage(message: Message) { - lock.readLock().withLock { - val instance = extractor(message) - - if (instance != null) { - val handler = handlers.get(instance::class) - - handler?.invoke(instance) - _parsedMessages.tryEmit(instance) - } else { - Log.e("mullvad", "Dispatching handler received an unexpected message") - } - } - } - - fun onDestroy() { - lock.writeLock().withLock { handlers.clear() } - - removeCallbacksAndMessages(null) - } - - companion object { - private const val MESSAGES_BUFFER_CAPACITY = 10 - } -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt deleted file mode 100644 index 36ea17036e..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Event.kt +++ /dev/null @@ -1,86 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import android.os.Messenger -import kotlinx.parcelize.Parcelize -import net.mullvad.mullvadvpn.model.AccountCreationResult -import net.mullvad.mullvadvpn.model.AccountExpiry -import net.mullvad.mullvadvpn.model.AccountHistory -import net.mullvad.mullvadvpn.model.CreateCustomListResult -import net.mullvad.mullvadvpn.model.DeviceListEvent -import net.mullvad.mullvadvpn.model.DeviceState -import net.mullvad.mullvadvpn.model.LoginResult -import net.mullvad.mullvadvpn.model.PlayPurchaseInitResult -import net.mullvad.mullvadvpn.model.PlayPurchaseVerifyResult -import net.mullvad.mullvadvpn.model.RelayList -import net.mullvad.mullvadvpn.model.RemoveDeviceResult -import net.mullvad.mullvadvpn.model.Settings -import net.mullvad.mullvadvpn.model.SettingsPatchError -import net.mullvad.mullvadvpn.model.TunnelState -import net.mullvad.mullvadvpn.model.UpdateCustomListResult - -// Events that can be sent from the service -sealed class Event : Message.EventMessage() { - override val messageKey = MESSAGE_KEY - - @Parcelize data class AccountCreationEvent(val result: AccountCreationResult) : Event() - - @Parcelize data class AccountExpiryEvent(val expiry: AccountExpiry) : Event() - - @Parcelize data class AccountHistoryEvent(val history: AccountHistory) : Event() - - @Parcelize - data class AppVersionInfo(val versionInfo: net.mullvad.mullvadvpn.model.AppVersionInfo?) : - Event() - - @Parcelize data class AuthToken(val token: String?) : Event() - - @Parcelize data class CurrentVersion(val version: String?) : Event() - - @Parcelize data class DeviceStateEvent(val newState: DeviceState) : Event() - - @Parcelize data class DeviceListUpdate(val event: DeviceListEvent) : Event() - - @Parcelize - data class DeviceRemovalEvent(val deviceId: String, val result: RemoveDeviceResult) : Event() - - @Parcelize data class ListenerReady(val connection: Messenger, val listenerId: Int) : Event() - - @Parcelize data class LoginEvent(val result: LoginResult) : Event() - - @Parcelize data class NewRelayList(val relayList: RelayList?) : Event() - - @Parcelize data class SettingsUpdate(val settings: Settings?) : Event() - - @Parcelize data class SplitTunnelingUpdate(val excludedApps: List<String>?) : Event() - - @Parcelize data class TunnelStateChange(val tunnelState: TunnelState) : Event() - - @Parcelize - data class VoucherSubmissionResult( - val voucher: String, - val result: net.mullvad.mullvadvpn.model.VoucherSubmissionResult - ) : Event() - - @Parcelize data class PlayPurchaseInitResultEvent(val result: PlayPurchaseInitResult) : Event() - - @Parcelize - data class PlayPurchaseVerifyResultEvent(val result: PlayPurchaseVerifyResult) : Event() - - @Parcelize object VpnPermissionRequest : Event() - - @Parcelize data class CreateCustomListResultEvent(val result: CreateCustomListResult) : Event() - - @Parcelize data class UpdateCustomListResultEvent(val result: UpdateCustomListResult) : Event() - - @Parcelize data class ExportJsonSettingsResult(val json: String) : Event() - - @Parcelize data class ApplyJsonSettingsResult(val error: SettingsPatchError?) : Event() - - companion object { - private const val MESSAGE_KEY = "event" - - fun fromMessage(message: android.os.Message): Event? = fromMessage(message, MESSAGE_KEY) - } -} - -typealias EventDispatcher = MessageDispatcher<Event> diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt deleted file mode 100644 index 7b839a3658..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/HandlerFlow.kt +++ /dev/null @@ -1,38 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import android.os.Handler -import android.os.Looper -import android.os.Message -import android.util.Log -import kotlinx.coroutines.CancellationException -import kotlinx.coroutines.InternalCoroutinesApi -import kotlinx.coroutines.channels.Channel -import kotlinx.coroutines.channels.ClosedSendChannelException -import kotlinx.coroutines.channels.trySendBlocking -import kotlinx.coroutines.flow.Flow -import kotlinx.coroutines.flow.FlowCollector -import kotlinx.coroutines.flow.consumeAsFlow -import kotlinx.coroutines.flow.onCompletion - -class HandlerFlow<T>(looper: Looper, private val extractor: (Message) -> T) : - Handler(looper), Flow<T> { - private val channel = Channel<T>(Channel.UNLIMITED) - private val flow = channel.consumeAsFlow().onCompletion { removeCallbacksAndMessages(null) } - - @InternalCoroutinesApi - override suspend fun collect(collector: FlowCollector<T>) = flow.collect(collector) - - override fun handleMessage(message: Message) { - val extractedData = extractor(message) - - try { - channel.trySendBlocking(extractedData) - } catch (exception: ClosedSendChannelException) { - Log.w("mullvad", "Received a message after HandlerFlow was closed", exception) - removeCallbacksAndMessages(null) - } catch (exception: CancellationException) { - Log.w("mullvad", "Received a message after HandlerFlow was cancelled", exception) - removeCallbacksAndMessages(null) - } - } -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt deleted file mode 100644 index 7cc293b373..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Message.kt +++ /dev/null @@ -1,31 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import android.os.Bundle -import android.os.Message as RawMessage -import android.os.Parcelable - -sealed class Message(private val messageId: Int) : Parcelable { - abstract class EventMessage : Message(1) - - abstract class RequestMessage : Message(2) - - protected abstract val messageKey: String - - val message: RawMessage - get() = - RawMessage.obtain().also { message -> - message.what = messageId - message.data = Bundle() - message.data.putParcelable(messageKey, this) - } - - companion object { - internal fun <T : Parcelable> fromMessage(message: RawMessage, key: String): T? { - val data = message.data - - data.classLoader = Message::class.java.classLoader - - return data.getParcelable(key) - } - } -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt deleted file mode 100644 index 8bb6703479..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageDispatcher.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import kotlin.reflect.KClass - -interface MessageDispatcher<T : Any> { - fun <V : T> registerHandler(variant: KClass<V>, handler: (V) -> Unit) -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt deleted file mode 100644 index 04de35e3bd..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/MessageHandler.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import kotlin.reflect.KClass -import kotlinx.coroutines.flow.Flow - -interface MessageHandler { - fun <R : Event> events(klass: KClass<R>): Flow<R> - - fun trySendRequest(request: Request): Boolean -} - -inline fun <reified R : Event> MessageHandler.events(): Flow<R> { - return this.events(R::class) -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt deleted file mode 100644 index 4bcf871acc..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/Request.kt +++ /dev/null @@ -1,134 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc - -import android.os.Message as RawMessage -import android.os.Messenger -import java.net.InetAddress -import kotlinx.parcelize.Parcelize -import net.mullvad.mullvadvpn.model.Constraint -import net.mullvad.mullvadvpn.model.CustomList -import net.mullvad.mullvadvpn.model.DnsOptions -import net.mullvad.mullvadvpn.model.LocationConstraint -import net.mullvad.mullvadvpn.model.ObfuscationSettings -import net.mullvad.mullvadvpn.model.Ownership -import net.mullvad.mullvadvpn.model.PlayPurchase -import net.mullvad.mullvadvpn.model.Providers -import net.mullvad.mullvadvpn.model.QuantumResistantState -import net.mullvad.mullvadvpn.model.RelayOverride -import net.mullvad.mullvadvpn.model.WireguardConstraints - -// Requests that the service can handle -sealed class Request : Message.RequestMessage() { - override val messageKey = MESSAGE_KEY - - @Parcelize - @Deprecated("Use SetDnsOptions") - data class AddCustomDnsServer(val address: InetAddress) : Request() - - @Parcelize object Connect : Request() - - @Parcelize object CreateAccount : Request() - - @Parcelize object Disconnect : Request() - - @Parcelize data class ExcludeApp(val packageName: String) : Request() - - @Parcelize object FetchAccountExpiry : Request() - - @Parcelize object FetchAccountHistory : Request() - - @Parcelize object FetchAuthToken : Request() - - @Parcelize data class IncludeApp(val packageName: String) : Request() - - @Parcelize data class Login(val account: String?) : Request() - - @Parcelize object RefreshDeviceState : Request() - - @Parcelize object GetDevice : Request() - - @Parcelize data class GetDeviceList(val accountToken: String) : Request() - - @Parcelize data class RemoveDevice(val accountToken: String, val deviceId: String) : Request() - - @Parcelize object Logout : Request() - - @Parcelize object PersistExcludedApps : Request() - - @Parcelize object Reconnect : Request() - - @Parcelize data class RegisterListener(val listener: Messenger) : Request() - - @Parcelize object ClearAccountHistory : Request() - - @Parcelize - @Deprecated("Use SetDnsOptions") - data class RemoveCustomDnsServer(val address: InetAddress) : Request() - - @Parcelize - @Deprecated("Use SetDnsOptions") - data class ReplaceCustomDnsServer(val oldAddress: InetAddress, val newAddress: InetAddress) : - Request() - - @Parcelize data class SetAllowLan(val allow: Boolean) : Request() - - @Parcelize data class SetAutoConnect(val autoConnect: Boolean) : Request() - - @Parcelize - @Deprecated("Use SetDnsOptions") - data class SetEnableCustomDns(val enable: Boolean) : Request() - - @Parcelize data class SetEnableSplitTunneling(val enable: Boolean) : Request() - - @Parcelize data class SetRelayLocation(val locationConstraint: LocationConstraint) : Request() - - @Parcelize data class SetWireGuardMtu(val mtu: Int?) : Request() - - @Parcelize data class SubmitVoucher(val voucher: String) : Request() - - @Parcelize data object InitPlayPurchase : Request() - - @Parcelize data class VerifyPlayPurchase(val playPurchase: PlayPurchase) : Request() - - @Parcelize data class UnregisterListener(val listenerId: Int) : Request() - - @Parcelize data class VpnPermissionResponse(val isGranted: Boolean) : Request() - - @Parcelize data class SetDnsOptions(val dnsOptions: DnsOptions) : Request() - - @Parcelize data class SetObfuscationSettings(val settings: ObfuscationSettings?) : Request() - - @Parcelize - data class SetWireguardConstraints(val wireguardConstraints: WireguardConstraints) : Request() - - @Parcelize - data class SetWireGuardQuantumResistant(val quantumResistant: QuantumResistantState) : - Request() - - @Parcelize data object FetchRelayList : Request() - - @Parcelize - data class SetOwnershipAndProviders( - val ownership: Constraint<Ownership>, - val providers: Constraint<Providers> - ) : Request() - - @Parcelize data class CreateCustomList(val name: String) : Request() - - @Parcelize data class DeleteCustomList(val id: String) : Request() - - @Parcelize data class UpdateCustomList(val customList: CustomList) : Request() - - @Parcelize data object ClearAllRelayOverrides : Request() - - @Parcelize data class ApplyJsonSettings(val json: String) : Request() - - @Parcelize data object ExportJsonSettings : Request() - - @Parcelize data class SetRelayOverride(val override: RelayOverride) : Request() - - companion object { - private const val MESSAGE_KEY = "request" - - fun fromMessage(message: RawMessage): Request? = fromMessage(message, MESSAGE_KEY) - } -} diff --git a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt b/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt deleted file mode 100644 index 26cade5cb4..0000000000 --- a/android/lib/ipc/src/main/kotlin/net/mullvad/mullvadvpn/lib/ipc/extensions/MessengerExtensions.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.mullvad.mullvadvpn.lib.ipc.extensions - -import android.os.DeadObjectException -import android.os.Message -import android.os.Messenger -import android.os.RemoteException -import android.util.Log -import net.mullvad.mullvadvpn.lib.ipc.Event -import net.mullvad.mullvadvpn.lib.ipc.Request - -fun Messenger.trySendEvent(event: Event, logErrors: Boolean): Boolean { - return trySend(event.message, logErrors, event::class.qualifiedName) -} - -fun Messenger.trySendRequest(request: Request, logErrors: Boolean): Boolean { - return trySend(request.message, logErrors, request::class.qualifiedName) -} - -private fun Messenger.trySend(message: Message, logErrors: Boolean, messageName: String?): Boolean { - return try { - this.send(message) - true - } catch (deadObjectException: DeadObjectException) { - if (logErrors) { - Log.e( - "mullvad", - "Failed to send message ${messageName ?: "<missing>"} due to DeadObjectException" - ) - } - false - } catch (remoteException: RemoteException) { - if (logErrors) { - Log.e( - "mullvad", - "Failed to send message ${messageName ?: "<missing>"} due to RemoteException" - ) - } - false - } -} diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt index 6ce7690bc5..4047783825 100644 --- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt +++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/CameraAnimation.kt @@ -18,9 +18,9 @@ import net.mullvad.mullvadvpn.lib.map.internal.MAX_ANIMATION_MILLIS import net.mullvad.mullvadvpn.lib.map.internal.MAX_MULTIPLIER_PEAK_TIMING import net.mullvad.mullvadvpn.lib.map.internal.MIN_ANIMATION_MILLIS import net.mullvad.mullvadvpn.lib.map.internal.SHORT_ANIMATION_CUTOFF_MILLIS -import net.mullvad.mullvadvpn.model.LatLong -import net.mullvad.mullvadvpn.model.Latitude -import net.mullvad.mullvadvpn.model.Longitude +import net.mullvad.mullvadvpn.lib.model.LatLong +import net.mullvad.mullvadvpn.lib.model.Latitude +import net.mullvad.mullvadvpn.lib.model.Longitude @Composable fun animatedCameraPosition( diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt index a143a63cb8..b1ea1144f9 100644 --- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt +++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/Map.kt @@ -13,7 +13,7 @@ import net.mullvad.mullvadvpn.lib.map.data.GlobeColors import net.mullvad.mullvadvpn.lib.map.data.MapViewState import net.mullvad.mullvadvpn.lib.map.data.Marker import net.mullvad.mullvadvpn.lib.map.internal.MapGLSurfaceView -import net.mullvad.mullvadvpn.model.LatLong +import net.mullvad.mullvadvpn.lib.model.LatLong @Composable fun Map( diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt index d837bcadfc..b66b0ea657 100644 --- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt +++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/CameraPosition.kt @@ -1,7 +1,7 @@ package net.mullvad.mullvadvpn.lib.map.data import androidx.compose.runtime.Immutable -import net.mullvad.mullvadvpn.model.LatLong +import net.mullvad.mullvadvpn.lib.model.LatLong @Immutable data class CameraPosition(val latLong: LatLong, val zoom: Float, val verticalBias: Float) diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt index 9f464612f1..4d26348d45 100644 --- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt +++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/data/Marker.kt @@ -1,7 +1,7 @@ package net.mullvad.mullvadvpn.lib.map.data import androidx.compose.runtime.Immutable -import net.mullvad.mullvadvpn.model.LatLong +import net.mullvad.mullvadvpn.lib.model.LatLong @Immutable data class Marker( diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt index 41ac903fb1..887e64bebd 100644 --- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt +++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/MapGLRenderer.kt @@ -13,7 +13,7 @@ import net.mullvad.mullvadvpn.lib.map.data.LocationMarkerColors import net.mullvad.mullvadvpn.lib.map.data.MapViewState import net.mullvad.mullvadvpn.lib.map.internal.shapes.Globe import net.mullvad.mullvadvpn.lib.map.internal.shapes.LocationMarker -import net.mullvad.mullvadvpn.model.toRadians +import net.mullvad.mullvadvpn.lib.model.toRadians internal class MapGLRenderer(private val resources: Resources) : GLSurfaceView.Renderer { diff --git a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt index 9d03a540c5..c67a0a1bb7 100644 --- a/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt +++ b/android/lib/map/src/main/kotlin/net/mullvad/mullvadvpn/lib/map/internal/shapes/LocationMarker.kt @@ -12,7 +12,7 @@ import net.mullvad.mullvadvpn.lib.map.internal.VERTEX_COMPONENT_SIZE import net.mullvad.mullvadvpn.lib.map.internal.initArrayBuffer import net.mullvad.mullvadvpn.lib.map.internal.initShaderProgram import net.mullvad.mullvadvpn.lib.map.internal.toFloatArray -import net.mullvad.mullvadvpn.model.LatLong +import net.mullvad.mullvadvpn.lib.model.LatLong internal class LocationMarker(val colors: LocationMarkerColors) { diff --git a/android/lib/model/build.gradle.kts b/android/lib/model/build.gradle.kts index 7264c6041a..28a5804b5f 100644 --- a/android/lib/model/build.gradle.kts +++ b/android/lib/model/build.gradle.kts @@ -3,10 +3,11 @@ plugins { id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5 id(Dependencies.Plugin.kotlinAndroidId) id(Dependencies.Plugin.kotlinParcelizeId) + id(Dependencies.Plugin.ksp) version Versions.Plugin.ksp } android { - namespace = "net.mullvad.mullvadvpn.model" + namespace = "net.mullvad.mullvadvpn.lib.model" compileSdk = Versions.Android.compileSdkVersion defaultConfig { @@ -29,11 +30,12 @@ android { } dependencies { - implementation(project(Dependencies.Mullvad.talpidLib)) - implementation(Dependencies.jodaTime) implementation(Dependencies.Kotlin.stdlib) implementation(Dependencies.KotlinX.coroutinesAndroid) + implementation(Dependencies.Arrow.core) + implementation(Dependencies.Arrow.optics) + ksp(Dependencies.Arrow.opticsKsp) // Test dependencies testRuntimeOnly(Dependencies.junitEngine) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt new file mode 100644 index 0000000000..60395721d8 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountData.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +import org.joda.time.DateTime + +data class AccountData( + val id: AccountId, + val expiryDate: DateTime, +) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt new file mode 100644 index 0000000000..75550259fd --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountId.kt @@ -0,0 +1,10 @@ +package net.mullvad.mullvadvpn.lib.model + +import java.util.UUID + +@JvmInline +value class AccountId(val value: UUID) { + companion object { + fun fromString(value: String) = AccountId(UUID.fromString(value)) + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt new file mode 100644 index 0000000000..d03a0d6721 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AccountToken.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@JvmInline @Parcelize value class AccountToken(val value: String) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt new file mode 100644 index 0000000000..531fc1c073 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ActionAfterDisconnect.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class ActionAfterDisconnect { + Nothing, + Block, + Reconnect +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt new file mode 100644 index 0000000000..338162db8c --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AddSplitTunnelingAppError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +interface AddSplitTunnelingAppError { + data class Unknown(val throwable: Throwable) : AddSplitTunnelingAppError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt new file mode 100644 index 0000000000..0663b530a1 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppId.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline value class AppId(val value: String) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt new file mode 100644 index 0000000000..9af168bf28 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/AppVersionInfo.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class AppVersionInfo(val supported: Boolean, val suggestedUpgrade: String?) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt new file mode 100644 index 0000000000..980ea23961 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/BuildVersion.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class BuildVersion(val name: String, val code: Int) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt new file mode 100644 index 0000000000..ce1bc0af12 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ClearAllOverridesError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface ClearAllOverridesError { + data class Unknown(val throwable: Throwable) : ClearAllOverridesError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt new file mode 100644 index 0000000000..307a235314 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ConnectError.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface ConnectError { + data class Unknown(val throwable: Throwable) : ConnectError + + data object NoVpnPermission : ConnectError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt new file mode 100644 index 0000000000..95e7d95154 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Constraint.kt @@ -0,0 +1,21 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +sealed interface Constraint<out T> { + data object Any : Constraint<Nothing> + + @optics + data class Only<T>(val value: T) : Constraint<T> { + companion object + } + + fun getOrNull(): T? = + when (this) { + Any -> null + is Only -> value + } + + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt new file mode 100644 index 0000000000..eeeaf11fca --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateAccountError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed class CreateAccountError { + data class Unknown(val error: Throwable) : CreateAccountError() +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt new file mode 100644 index 0000000000..adbac22d9b --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CreateCustomListError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface CreateCustomListError + +data object CustomListAlreadyExists : CreateCustomListError diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt new file mode 100644 index 0000000000..4fd64b2892 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomDnsOptions.kt @@ -0,0 +1,9 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics +import java.net.InetAddress + +@optics +data class CustomDnsOptions(val addresses: List<InetAddress>) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt new file mode 100644 index 0000000000..ed43ac1097 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomList.kt @@ -0,0 +1,12 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class CustomList( + val id: CustomListId, + val name: CustomListName, + val locations: List<GeoLocationId> +) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListName.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomListName.kt index 5822eec2b3..186d74dc92 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListName.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/CustomListName.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DefaultDnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DefaultDnsOptions.kt index 69f4d4d220..6979320ce6 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DefaultDnsOptions.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DefaultDnsOptions.kt @@ -1,9 +1,8 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import arrow.optics.optics -@Parcelize +@optics data class DefaultDnsOptions( val blockAds: Boolean = false, val blockTrackers: Boolean = false, @@ -11,7 +10,7 @@ data class DefaultDnsOptions( val blockAdultContent: Boolean = false, val blockGambling: Boolean = false, val blockSocialMedia: Boolean = false, -) : Parcelable { +) { fun isAnyBlockerEnabled(): Boolean { return blockAds || blockTrackers || @@ -20,4 +19,6 @@ data class DefaultDnsOptions( blockGambling || blockSocialMedia } + + companion object } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt new file mode 100644 index 0000000000..d9c93c87cf --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteCustomListError.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface DeleteCustomListError diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt new file mode 100644 index 0000000000..1c6c54bcf0 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeleteDeviceError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface DeleteDeviceError { + data class Unknown(val error: Throwable) : DeleteDeviceError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt new file mode 100644 index 0000000000..e8303f0eca --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Device.kt @@ -0,0 +1,12 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize +import net.mullvad.mullvadvpn.lib.model.extensions.startCase +import org.joda.time.DateTime + +@Parcelize +data class Device(val id: DeviceId, private val name: String, val creationDate: DateTime) : + Parcelable { + fun displayName(): String = name.startCase() +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt new file mode 100644 index 0000000000..863d15fd67 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceId.kt @@ -0,0 +1,13 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import java.util.UUID +import kotlinx.parcelize.Parcelize + +@JvmInline +@Parcelize +value class DeviceId(val value: UUID) : Parcelable { + companion object { + fun fromString(value: String): DeviceId = DeviceId(UUID.fromString(value)) + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt new file mode 100644 index 0000000000..4546cd46b3 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DeviceState.kt @@ -0,0 +1,21 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +sealed class DeviceState : Parcelable { + @Parcelize + data class LoggedIn(val accountToken: AccountToken, val device: Device) : DeviceState() + + @Parcelize data object LoggedOut : DeviceState() + + @Parcelize data object Revoked : DeviceState() + + fun displayName(): String? { + return (this as? LoggedIn)?.device?.displayName() + } + + fun token(): AccountToken? { + return (this as? LoggedIn)?.accountToken + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsOptions.kt index 1ce3acc095..ae27e47457 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsOptions.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsOptions.kt @@ -1,11 +1,12 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import arrow.optics.optics -@Parcelize +@optics data class DnsOptions( val state: DnsState, val defaultOptions: DefaultDnsOptions, val customOptions: CustomDnsOptions -) : Parcelable +) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsState.kt index 9c8677ba7d..4bf053eef1 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DnsState.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/DnsState.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model enum class DnsState { Default, diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt new file mode 100644 index 0000000000..43a4cc41b1 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/EnableSplitTunnelingError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +interface EnableSplitTunnelingError { + data class Unknown(val throwable: Throwable) : EnableSplitTunnelingError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt new file mode 100644 index 0000000000..4eae8b08ec --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Endpoint.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +import java.net.InetSocketAddress + +data class Endpoint(val address: InetSocketAddress, val protocol: TransportProtocol) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt new file mode 100644 index 0000000000..fb7673b7b5 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorState.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class ErrorState(val cause: ErrorStateCause, val isBlocking: Boolean) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt new file mode 100644 index 0000000000..0ba63a4b08 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ErrorStateCause.kt @@ -0,0 +1,34 @@ +package net.mullvad.mullvadvpn.lib.model + +import java.net.InetAddress + +sealed class ErrorStateCause { + class AuthFailed(private val reason: String?) : ErrorStateCause() { + fun isCausedByExpiredAccount(): Boolean { + return reason == AUTH_FAILED_REASON_EXPIRED_ACCOUNT + } + + companion object { + private const val AUTH_FAILED_REASON_EXPIRED_ACCOUNT = "[EXPIRED_ACCOUNT]" + } + } + + data object Ipv6Unavailable : ErrorStateCause() + + sealed class FirewallPolicyError : ErrorStateCause() { + data object Generic : FirewallPolicyError() + } + + data object DnsError : ErrorStateCause() + + // Regression + data class InvalidDnsServers(val addresses: List<InetAddress>) : ErrorStateCause() + + data object StartTunnelError : ErrorStateCause() + + data class TunnelParameterError(val error: ParameterGenerationError) : ErrorStateCause() + + data object IsOffline : ErrorStateCause() + + data object VpnPermissionDenied : ErrorStateCause() +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GeoIpLocation.kt index 625de76b29..3334b458d7 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeoIpLocation.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GeoIpLocation.kt @@ -1,10 +1,7 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable import java.net.InetAddress -import kotlinx.parcelize.Parcelize -@Parcelize data class GeoIpLocation( val ipv4: InetAddress?, val ipv6: InetAddress?, @@ -13,4 +10,4 @@ data class GeoIpLocation( val latitude: Double, val longitude: Double, val hostname: String?, -) : Parcelable +) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt new file mode 100644 index 0000000000..6f3ba64848 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountDataError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface GetAccountDataError { + data class Unknown(val error: Throwable) : GetAccountDataError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt new file mode 100644 index 0000000000..7803a98ad1 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetAccountHistoryError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface GetAccountHistoryError { + data class Unknown(val error: Throwable) : GetAccountHistoryError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt new file mode 100644 index 0000000000..bcad016580 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceListError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface GetDeviceListError { + data class Unknown(val error: Throwable) : GetDeviceListError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt new file mode 100644 index 0000000000..675973ee1e --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/GetDeviceStateError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface GetDeviceStateError { + data class Unknown(val error: Throwable) : GetDeviceStateError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt index d6749a16a2..19f757ffc3 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LatLong.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LatLong.kt @@ -1,9 +1,9 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import kotlin.math.cos import kotlin.math.pow import kotlin.math.sqrt -import net.mullvad.mullvadvpn.model.Latitude.Companion.mean +import net.mullvad.mullvadvpn.lib.model.Latitude.Companion.mean data class LatLong(val latitude: Latitude, val longitude: Longitude) { diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Latitude.kt index 21d113f3bc..9b0cc7fbbe 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Latitude.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Latitude.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import kotlin.math.absoluteValue diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt new file mode 100644 index 0000000000..6530450d42 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ListDevicesError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +interface ListDevicesError { + data class Unknown(val throwable: Throwable) : ListDevicesError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt new file mode 100644 index 0000000000..1c58f80bee --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/LoginAccountError.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@Parcelize +sealed class LoginAccountError : Parcelable { + data object InvalidAccount : LoginAccountError() + + data class MaxDevicesReached(val accountToken: AccountToken) : LoginAccountError() + + data object RpcError : LoginAccountError() + + data class Unknown(val error: Throwable) : LoginAccountError() +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Longitude.kt index 9f73a6ff17..b772801da7 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Longitude.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Longitude.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import kotlin.math.absoluteValue diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt new file mode 100644 index 0000000000..68b4b71bd9 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Mtu.kt @@ -0,0 +1,28 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import arrow.core.Either +import arrow.core.raise.either +import arrow.core.raise.ensure +import kotlinx.parcelize.Parcelize + +@JvmInline +@Parcelize +value class Mtu(val value: Int) : Parcelable { + companion object { + fun fromString(value: String): Either<ParseMtuError, Mtu> = either { + val number = value.toIntOrNull() ?: raise(ParseMtuError.NotANumber) + ensure(number in MIN_VALUE..MAX_VALUE) { ParseMtuError.OutOfRange(number) } + Mtu(number) + } + + private const val MIN_VALUE = 1280 + private const val MAX_VALUE = 1420 + } +} + +sealed interface ParseMtuError { + data object NotANumber : ParseMtuError + + data class OutOfRange(val number: Int) : ParseMtuError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt new file mode 100644 index 0000000000..5dda03aa9d --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Notification.kt @@ -0,0 +1,25 @@ +package net.mullvad.mullvadvpn.lib.model + +import org.joda.time.Duration + +sealed interface Notification { + val actions: List<NotificationAction> + val ongoing: Boolean + val channelId: NotificationChannelId + + data class Tunnel( + override val channelId: NotificationChannelId, + val state: NotificationTunnelState, + override val actions: List<NotificationAction.Tunnel>, + override val ongoing: Boolean, + ) : Notification + + data class AccountExpiry( + override val channelId: NotificationChannelId, + override val actions: List<NotificationAction.AccountExpiry>, + val websiteAuthToken: WebsiteAuthToken?, + val durationUntilExpiry: Duration + ) : Notification { + override val ongoing: Boolean = false + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt new file mode 100644 index 0000000000..ec938a9fbf --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationAction.kt @@ -0,0 +1,20 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface NotificationAction { + + sealed interface AccountExpiry : NotificationAction { + data object Open : AccountExpiry + } + + sealed interface Tunnel : NotificationAction { + data object Connect : Tunnel + + data object Disconnect : Tunnel + + data object Cancel : Tunnel + + data object Dismiss : Tunnel + + data object RequestPermission : Tunnel + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt new file mode 100644 index 0000000000..166c20b826 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannel.kt @@ -0,0 +1,15 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface NotificationChannel { + val id: NotificationChannelId + + data object TunnelUpdates : NotificationChannel { + private const val CHANNEL_ID = "vpn_tunnel_status" + override val id: NotificationChannelId = NotificationChannelId(CHANNEL_ID) + } + + data object AccountUpdates : NotificationChannel { + private const val CHANNEL_ID = "mullvad_account_time" + override val id: NotificationChannelId = NotificationChannelId(CHANNEL_ID) + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt new file mode 100644 index 0000000000..c4231deb8c --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationChannelId.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline value class NotificationChannelId(val value: String) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt new file mode 100644 index 0000000000..9c20bf9420 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationId.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline value class NotificationId(val value: Int) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt new file mode 100644 index 0000000000..fffe86c247 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationTunnelState.kt @@ -0,0 +1,25 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface NotificationTunnelState { + data class Disconnected(val hasVpnPermission: Boolean) : NotificationTunnelState + + data object Connecting : NotificationTunnelState + + data object Connected : NotificationTunnelState + + data object Reconnecting : NotificationTunnelState + + data object Disconnecting : NotificationTunnelState + + sealed interface Error : NotificationTunnelState { + data object DeviceOffline : Error + + data object Blocking : Error + + data object VpnPermissionDenied : Error + + data object AlwaysOnVpn : Error + + data object Critical : Error + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt new file mode 100644 index 0000000000..00d64cbc3e --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/NotificationUpdate.kt @@ -0,0 +1,10 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface NotificationUpdate<out D> { + val notificationId: NotificationId + + data class Notify<D>(override val notificationId: NotificationId, val value: D) : + NotificationUpdate<D> + + data class Cancel(override val notificationId: NotificationId) : NotificationUpdate<Nothing> +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt new file mode 100644 index 0000000000..020ef8e5c1 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationEndpoint.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class ObfuscationEndpoint(val endpoint: Endpoint, val obfuscationType: ObfuscationType) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt index 19b5c0e5f2..b8a26973a2 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ObfuscationSettings.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationSettings.kt @@ -1,10 +1,11 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import arrow.optics.optics -@Parcelize +@optics data class ObfuscationSettings( val selectedObfuscation: SelectedObfuscation, val udp2tcp: Udp2TcpObfuscationSettings -) : Parcelable +) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt new file mode 100644 index 0000000000..cd71d645af --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ObfuscationType.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class ObfuscationType { + Udp2Tcp +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt new file mode 100644 index 0000000000..5257f944d3 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Ownership.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class Ownership { + MullvadOwned, + Rented +} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ParameterGenerationError.kt index b1504c676f..476aed1407 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ParameterGenerationError.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ParameterGenerationError.kt @@ -1,4 +1,4 @@ -package net.mullvad.talpid.tunnel +package net.mullvad.mullvadvpn.lib.model enum class ParameterGenerationError { NoMatchingRelay, diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt new file mode 100644 index 0000000000..9384f9f5b8 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchase.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class PlayPurchase(val productId: String, val purchaseToken: PlayPurchasePaymentToken) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt new file mode 100644 index 0000000000..6326bab8e8 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseInitError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class PlayPurchaseInitError { + OtherError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt new file mode 100644 index 0000000000..bfcae64d45 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchasePaymentToken.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline value class PlayPurchasePaymentToken(val value: String) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt new file mode 100644 index 0000000000..dc06b8ffbf --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PlayPurchaseVerifyError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class PlayPurchaseVerifyError { + OtherError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt new file mode 100644 index 0000000000..bcb5a8dd99 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Port.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import kotlinx.parcelize.Parcelize + +@JvmInline @Parcelize value class Port(val value: Int) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt new file mode 100644 index 0000000000..77767a1011 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/PortRange.kt @@ -0,0 +1,30 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcel +import android.os.Parcelable +import kotlinx.parcelize.Parceler +import kotlinx.parcelize.Parcelize +import kotlinx.parcelize.TypeParceler + +@JvmInline +@Parcelize +@TypeParceler<IntRange, IntRangeParceler> +value class PortRange(val value: IntRange) : Parcelable { + operator fun contains(port: Port): Boolean = port.value in value + + fun toFormattedString(): String = + if (value.first == value.last) { + value.first.toString() + } else { + "${value.first}-${value.last}" + } +} + +object IntRangeParceler : Parceler<IntRange> { + override fun create(parcel: Parcel) = IntRange(parcel.readInt(), parcel.readInt()) + + override fun IntRange.write(parcel: Parcel, flags: Int) { + parcel.writeInt(start) + parcel.writeInt(endInclusive) + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt new file mode 100644 index 0000000000..e704e9554d --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Provider.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class Provider(val providerId: ProviderId, val ownership: Ownership) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt new file mode 100644 index 0000000000..cc23c3e9b6 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/ProviderId.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline value class ProviderId(val value: String) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt new file mode 100644 index 0000000000..73cf9facdb --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Providers.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class Providers(val providers: Set<ProviderId>) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt new file mode 100644 index 0000000000..c77dab72d3 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/QuantumResistantState.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class QuantumResistantState { + Auto, + On, + Off +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt new file mode 100644 index 0000000000..d14a2f236b --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherError.kt @@ -0,0 +1,11 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed class RedeemVoucherError { + data object InvalidVoucher : RedeemVoucherError() + + data object VoucherAlreadyUsed : RedeemVoucherError() + + data object RpcError : RedeemVoucherError() + + data class Unknown(val error: Throwable) : RedeemVoucherError() +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt new file mode 100644 index 0000000000..9c81042b8c --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RedeemVoucherSuccess.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +import org.joda.time.DateTime + +data class RedeemVoucherSuccess(val timeAdded: Long, val newExpiryDate: DateTime) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt new file mode 100644 index 0000000000..f3573933e3 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayConstraints.kt @@ -0,0 +1,13 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class RelayConstraints( + val location: Constraint<RelayItemId>, + val providers: Constraint<Providers>, + val ownership: Constraint<Ownership>, + val wireguardConstraints: WireguardConstraints, +) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt new file mode 100644 index 0000000000..a31a6f67df --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItem.kt @@ -0,0 +1,88 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +sealed interface RelayItem { + val id: RelayItemId + val name: String + val active: Boolean + val hasChildren: Boolean + val expanded: Boolean + + @optics + data class CustomList( + override val id: CustomListId, + val customListName: CustomListName, + val locations: List<Location>, + override val expanded: Boolean, + ) : RelayItem { + override val name: String = customListName.value + + override val active + get() = locations.any { location -> location.active } + + override val hasChildren + get() = locations.isNotEmpty() + + companion object + } + + @optics + sealed interface Location : RelayItem { + override val id: GeoLocationId + + @optics + data class Country( + override val id: GeoLocationId.Country, + override val name: String, + override val expanded: Boolean, + val cities: List<City> + ) : Location { + val relays = cities.flatMap { city -> city.relays } + + override val active + get() = cities.any { city -> city.active } + + override val hasChildren + get() = cities.isNotEmpty() + + companion object + } + + @optics + data class City( + override val id: GeoLocationId.City, + override val name: String, + override val expanded: Boolean, + val relays: List<Relay> + ) : Location { + + override val active + get() = relays.any { relay -> relay.active } + + override val hasChildren + get() = relays.isNotEmpty() + + companion object + } + + @optics + data class Relay( + override val id: GeoLocationId.Hostname, + val provider: Provider, + override val active: Boolean, + ) : Location { + override val name: String = id.hostname + + override val hasChildren = false + override val expanded = false + + companion object + } + + companion object + } + + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt new file mode 100644 index 0000000000..da59481269 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayItemId.kt @@ -0,0 +1,48 @@ +package net.mullvad.mullvadvpn.lib.model + +import android.os.Parcelable +import arrow.optics.optics +import kotlinx.parcelize.Parcelize + +@optics +sealed interface RelayItemId : Parcelable { + companion object +} + +@optics +@Parcelize +@JvmInline +value class CustomListId(val value: String) : RelayItemId, Parcelable { + companion object +} + +@optics +sealed interface GeoLocationId : RelayItemId { + @optics + @Parcelize + data class Country(val countryCode: String) : GeoLocationId { + companion object + } + + @optics + @Parcelize + data class City(val countryCode: Country, val cityCode: String) : GeoLocationId { + companion object + } + + @optics + @Parcelize + data class Hostname(val city: City, val hostname: String) : GeoLocationId { + companion object + } + + val country: Country + get() = + when (this) { + is Country -> this + is City -> this.countryCode + is Hostname -> this.city.countryCode + } + + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt new file mode 100644 index 0000000000..39e43a713e --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayList.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.model + +data class RelayList( + val countries: List<RelayItem.Location.Country>, + val wireguardEndpointData: WireguardEndpointData +) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayOverride.kt index f738218ee7..3bd0a2f0a1 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayOverride.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelayOverride.kt @@ -1,12 +1,13 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable +import arrow.optics.optics import java.net.InetAddress -import kotlinx.parcelize.Parcelize -@Parcelize +@optics data class RelayOverride( val hostname: String, val ipv4AddressIn: InetAddress?, val ipv6AddressIn: InetAddress? -) : Parcelable +) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt new file mode 100644 index 0000000000..ea40c980d0 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RelaySettings.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class RelaySettings(val relayConstraints: RelayConstraints) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt new file mode 100644 index 0000000000..d00272ec63 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveDeviceError.kt @@ -0,0 +1,9 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface RemoveDeviceError { + data object NotFound : RemoveDeviceError + + data object RpcError : RemoveDeviceError + + data class Unknown(val throwable: Throwable) : RemoveDeviceError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt new file mode 100644 index 0000000000..aa4dcfd8be --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/RemoveSplitTunnelingAppError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +interface RemoveSplitTunnelingAppError { + data class Unknown(val throwable: Throwable) : RemoveSplitTunnelingAppError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt new file mode 100644 index 0000000000..1651d61db7 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SelectedObfuscation.kt @@ -0,0 +1,7 @@ +package net.mullvad.mullvadvpn.lib.model + +enum class SelectedObfuscation { + Auto, + Off, + Udp2Tcp +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt new file mode 100644 index 0000000000..e30eba0d9e --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAllowLanError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetAllowLanError { + data class Unknown(val throwable: Throwable) : SetAllowLanError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt new file mode 100644 index 0000000000..b2b3b74edf --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetAutoConnectError { + data class Unknown(val throwable: Throwable) : SetAutoConnectError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt new file mode 100644 index 0000000000..8d72d8cebe --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetDnsOptionsError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetDnsOptionsError { + data class Unknown(val throwable: Throwable) : SetDnsOptionsError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt new file mode 100644 index 0000000000..d9c5acf650 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetObfuscationOptionsError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetObfuscationOptionsError { + data class Unknown(val throwable: Throwable) : SetObfuscationOptionsError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt new file mode 100644 index 0000000000..4606c46125 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetRelayLocationError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetRelayLocationError { + data class Unknown(val throwable: Throwable) : SetRelayLocationError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt new file mode 100644 index 0000000000..ccf8b4c8dc --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardConstraintsError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetWireguardConstraintsError { + data class Unknown(val throwable: Throwable) : SetWireguardConstraintsError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt new file mode 100644 index 0000000000..ca4f135fb1 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardMtuError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetWireguardMtuError { + data class Unknown(val throwable: Throwable) : SetWireguardMtuError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt new file mode 100644 index 0000000000..8121120c67 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetWireguardQuantumResistantError.kt @@ -0,0 +1,5 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface SetWireguardQuantumResistantError { + data class Unknown(val throwable: Throwable) : SetWireguardQuantumResistantError +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt index 847b80cd70..c5191531be 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Settings.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt @@ -1,16 +1,18 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize +import arrow.optics.optics -@Parcelize +@optics data class Settings( val relaySettings: RelaySettings, val obfuscationSettings: ObfuscationSettings, - val customLists: CustomListsSettings, + val customLists: List<CustomList>, val allowLan: Boolean, val autoConnect: Boolean, val tunnelOptions: TunnelOptions, - val relayOverrides: ArrayList<RelayOverride>, + val relayOverrides: List<RelayOverride>, val showBetaReleases: Boolean, -) : Parcelable + val splitTunnelSettings: SplitTunnelSettings +) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SettingsPatchError.kt index 5e3cb29911..1db1dc6f68 100644 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SettingsPatchError.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SettingsPatchError.kt @@ -1,10 +1,6 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -sealed class SettingsPatchError : Parcelable { +sealed class SettingsPatchError { // E.g hostname is number instead of String data class InvalidOrMissingValue(val value: String) : SettingsPatchError() diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt new file mode 100644 index 0000000000..a937d53bae --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SplitTunnelSettings.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class SplitTunnelSettings(val enabled: Boolean, val excludedApps: Set<AppId>) diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TransportProtocol.kt index 89fdedaba1..b25e3061be 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TransportProtocol.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TransportProtocol.kt @@ -1,4 +1,4 @@ -package net.mullvad.talpid.net +package net.mullvad.mullvadvpn.lib.model import android.os.Parcelable import kotlinx.parcelize.Parcelize diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt index 9c45833eb2..d715f16766 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/TunnelEndpoint.kt +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelEndpoint.kt @@ -1,11 +1,7 @@ -package net.mullvad.talpid.net +package net.mullvad.mullvadvpn.lib.model -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class TunnelEndpoint( val endpoint: Endpoint, val quantumResistant: Boolean, val obfuscation: ObfuscationEndpoint? -) : Parcelable +) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt new file mode 100644 index 0000000000..de1d760d30 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelOptions.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class TunnelOptions(val wireguard: WireguardTunnelOptions, val dnsOptions: DnsOptions) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt new file mode 100644 index 0000000000..3fae41802a --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/TunnelState.kt @@ -0,0 +1,35 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed class TunnelState { + data class Disconnected(val location: GeoIpLocation? = null) : TunnelState() + + data class Connecting(val endpoint: TunnelEndpoint?, val location: GeoIpLocation?) : + TunnelState() + + data class Connected(val endpoint: TunnelEndpoint, val location: GeoIpLocation?) : + TunnelState() + + data class Disconnecting(val actionAfterDisconnect: ActionAfterDisconnect) : TunnelState() + + data class Error(val errorState: ErrorState) : TunnelState() + + fun location(): GeoIpLocation? { + return when (this) { + is Connected -> location + is Connecting -> location + is Disconnecting -> null + is Disconnected -> location + is Error -> null + } + } + + fun isSecured(): Boolean { + return when (this) { + is Connected, + is Connecting, + is Disconnecting, -> true + is Disconnected -> false + is Error -> this.errorState.isBlocking + } + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt new file mode 100644 index 0000000000..7447f7a4cf --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Udp2TcpObfuscationSettings.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class Udp2TcpObfuscationSettings(val port: Constraint<Port>) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt new file mode 100644 index 0000000000..ef49018dca --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/UpdateCustomListError.kt @@ -0,0 +1,35 @@ +package net.mullvad.mullvadvpn.lib.model + +sealed interface UpdateCustomListNameError { + companion object { + fun from(error: UpdateCustomListError): UpdateCustomListNameError = + when (error) { + is NameAlreadyExists -> error + is UnknownCustomListError -> error + } + } +} + +sealed interface UpdateCustomListLocationsError { + companion object { + fun from(error: UpdateCustomListError): UpdateCustomListLocationsError = + when (error) { + is NameAlreadyExists -> error("Not supported error") + is UnknownCustomListError -> error + } + } +} + +sealed interface UpdateCustomListError + +data class NameAlreadyExists(val name: String) : UpdateCustomListError, UpdateCustomListNameError + +data class UnknownCustomListError(val throwable: Throwable) : + UpdateCustomListError, + UpdateCustomListNameError, + UpdateCustomListLocationsError, + CreateCustomListError, + DeleteCustomListError + +data class GetCustomListError(val id: CustomListId) : + UpdateCustomListLocationsError, UpdateCustomListNameError diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt new file mode 100644 index 0000000000..8ad9b85787 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WebsiteAuthToken.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +@JvmInline +value class WebsiteAuthToken private constructor(val value: String) { + companion object { + fun fromString(value: String) = WebsiteAuthToken(value) + } +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt new file mode 100644 index 0000000000..8affb81077 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardConstraints.kt @@ -0,0 +1,8 @@ +package net.mullvad.mullvadvpn.lib.model + +import arrow.optics.optics + +@optics +data class WireguardConstraints(val port: Constraint<Port>) { + companion object +} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt new file mode 100644 index 0000000000..8aff7d2895 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardEndpointData.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class WireguardEndpointData(val portRanges: List<PortRange>) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt new file mode 100644 index 0000000000..573f08213e --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/WireguardTunnelOptions.kt @@ -0,0 +1,3 @@ +package net.mullvad.mullvadvpn.lib.model + +data class WireguardTunnelOptions(val mtu: Mtu?, val quantumResistant: QuantumResistantState) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt new file mode 100644 index 0000000000..0df57eb057 --- /dev/null +++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/extensions/String.kt @@ -0,0 +1,6 @@ +package net.mullvad.mullvadvpn.lib.model.extensions + +fun String.startCase() = + split(" ").joinToString(" ") { word -> + word.replaceFirstChar { firstChar -> firstChar.uppercase() } + } diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt deleted file mode 100644 index f5137ebbb7..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountAndDevice.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class AccountAndDevice(val account_token: String, val device: Device) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt deleted file mode 100644 index 4bb4c61384..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountCreationResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class AccountCreationResult : Parcelable { - @Parcelize data class Success(val accountToken: String) : AccountCreationResult() - - @Parcelize object Failure : AccountCreationResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt deleted file mode 100644 index 6dda6b8352..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountData.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.mullvad.mullvadvpn.model - -data class AccountData(val expiry: String) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt deleted file mode 100644 index f856ef8c89..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountExpiry.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import org.joda.time.DateTime - -sealed class AccountExpiry : Parcelable { - @Parcelize data class Available(val expiryDateTime: DateTime) : AccountExpiry() - - @Parcelize data object Missing : AccountExpiry() - - fun date(): DateTime? { - return (this as? Available)?.expiryDateTime - } -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt deleted file mode 100644 index f003ee316b..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountHistory.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class AccountHistory : Parcelable { - @Parcelize data class Available(val accountToken: String) : AccountHistory() - - @Parcelize object Missing : AccountHistory() - - fun accountToken() = (this as? Available)?.accountToken -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt deleted file mode 100644 index 2aeca352d0..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AccountToken.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.mullvad.mullvadvpn.model - -@JvmInline value class AccountToken(val value: String) diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt deleted file mode 100644 index bbe99ce656..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/AppVersionInfo.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class AppVersionInfo(val supported: Boolean, val suggestedUpgrade: String?) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt deleted file mode 100644 index d9ca22b164..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Constraint.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class Constraint<T> : Parcelable { - @Parcelize @Suppress("PARCELABLE_PRIMARY_CONSTRUCTOR_IS_EMPTY") class Any<T> : Constraint<T>() - - @Parcelize data class Only<T : Parcelable>(val value: T) : Constraint<T>() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt deleted file mode 100644 index 73eaa209c8..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CreateCustomListResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class CreateCustomListResult : Parcelable { - @Parcelize data class Ok(val id: String) : CreateCustomListResult() - - @Parcelize data class Error(val error: CustomListsError) : CreateCustomListResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt deleted file mode 100644 index bbf029dd4d..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomDnsOptions.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import java.net.InetAddress -import kotlinx.parcelize.Parcelize - -@Parcelize data class CustomDnsOptions(val addresses: ArrayList<InetAddress>) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt deleted file mode 100644 index cdfa1b9687..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomList.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class CustomList( - val id: String, - val name: String, - val locations: ArrayList<GeographicLocationConstraint> -) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt deleted file mode 100644 index 83806af4f7..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsError.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -enum class CustomListsError { - CustomListExists, - OtherError -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt deleted file mode 100644 index 8a8c03ef05..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomListsSettings.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class CustomListsSettings(val customLists: ArrayList<CustomList>) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt deleted file mode 100644 index 72276c65e4..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/CustomTunnelEndpoint.kt +++ /dev/null @@ -1,3 +0,0 @@ -package net.mullvad.mullvadvpn.model - -class CustomTunnelEndpoint diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt deleted file mode 100644 index 0f0a55d05d..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Device.kt +++ /dev/null @@ -1,40 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Device( - val id: String, - private val name: String, - val pubkey: ByteArray, - val created: String -) : Parcelable { - // Generated by Android Studio - override fun equals(other: Any?): Boolean { - if (this === other) return true - if (javaClass != other?.javaClass) return false - - other as Device - - if (id != other.id) return false - if (name != other.name) return false - return pubkey.contentEquals(other.pubkey) - } - - // Generated by Android Studio - override fun hashCode(): Int { - var result = id.hashCode() - result = 31 * result + name.hashCode() - result = 31 * result + pubkey.contentHashCode() - return result - } - - fun displayName(): String = name.capitalizeFirstCharOfEachWord() -} - -private fun String.capitalizeFirstCharOfEachWord(): String { - return split(" ") - .joinToString(" ") { word -> word.replaceFirstChar { firstChar -> firstChar.uppercase() } } - .trimEnd() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt deleted file mode 100644 index 741108612d..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEvent.kt +++ /dev/null @@ -1,7 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class DeviceEvent(val cause: DeviceEventCause, val newState: DeviceState) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt deleted file mode 100644 index b4c1d21761..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceEventCause.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class DeviceEventCause : Parcelable { - LoggedIn, - LoggedOut, - Revoked, - Updated, - RotatedKey -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt deleted file mode 100644 index afe5982ed5..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceList.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mullvad.mullvadvpn.model - -sealed class DeviceList { - object Unavailable : DeviceList() - - data class Available(val devices: List<Device>) : DeviceList() - - object Error : DeviceList() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt deleted file mode 100644 index 7a2883617b..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceListEvent.kt +++ /dev/null @@ -1,15 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class DeviceListEvent : Parcelable { - @Parcelize - data class Available(val accountToken: String, val devices: List<Device>) : DeviceListEvent() - - @Parcelize object Error : DeviceListEvent() - - fun isAvailable(): Boolean { - return (this is Available) - } -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt deleted file mode 100644 index e43eae3e6b..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DevicePort.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class DevicePort(val id: String) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt deleted file mode 100644 index fb34c9e645..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/DeviceState.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class DeviceState : Parcelable { - @Parcelize object Initial : DeviceState() - - @Parcelize object Unknown : DeviceState() - - @Parcelize data class LoggedIn(val accountAndDevice: AccountAndDevice) : DeviceState() - - @Parcelize object LoggedOut : DeviceState() - - @Parcelize object Revoked : DeviceState() - - fun isUnknown(): Boolean { - return this is Unknown - } - - fun deviceName(): String? { - return (this as? LoggedIn)?.accountAndDevice?.device?.displayName() - } - - fun token(): String? { - return (this as? LoggedIn)?.accountAndDevice?.account_token - } -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt deleted file mode 100644 index 386257a72a..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GeographicLocationConstraint.kt +++ /dev/null @@ -1,28 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class GeographicLocationConstraint : Parcelable { - abstract val location: GeoIpLocation - - @Parcelize - data class Country(val countryCode: String) : GeographicLocationConstraint() { - override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, null, 0.0, 0.0, null) - } - - @Parcelize - data class City(val countryCode: String, val cityCode: String) : - GeographicLocationConstraint() { - override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, cityCode, 0.0, 0.0, null) - } - - @Parcelize - data class Hostname(val countryCode: String, val cityCode: String, val hostname: String) : - GeographicLocationConstraint() { - override val location: GeoIpLocation - get() = GeoIpLocation(null, null, countryCode, cityCode, 0.0, 0.0, hostname) - } -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt deleted file mode 100644 index 2e94266e2a..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/GetAccountDataResult.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.model - -sealed class GetAccountDataResult { - class Ok(val accountData: AccountData) : GetAccountDataResult() - - object InvalidAccount : GetAccountDataResult() - - object RpcError : GetAccountDataResult() - - object OtherError : GetAccountDataResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt deleted file mode 100644 index 0c9d331e3b..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LocationConstraint.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class LocationConstraint : Parcelable { - @Parcelize - data class Location(val location: GeographicLocationConstraint) : LocationConstraint() - - @Parcelize data class CustomList(val listId: String) : LocationConstraint() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt deleted file mode 100644 index 29fb68203d..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/LoginResult.kt +++ /dev/null @@ -1,13 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class LoginResult : Parcelable { - Ok, - InvalidAccount, - MaxDevicesReached, - RpcError, - OtherError -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt deleted file mode 100644 index 43037be676..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Ownership.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class Ownership : Parcelable { - MullvadOwned, - Rented -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt deleted file mode 100644 index 8ae46a07a9..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchase.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class PlayPurchase(val productId: String, val purchaseToken: String) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt deleted file mode 100644 index 39aebabbe2..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitError.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class PlayPurchaseInitError : Parcelable { - // TODO: Add more errors here. - OtherError -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt deleted file mode 100644 index 41407474af..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseInitResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class PlayPurchaseInitResult : Parcelable { - @Parcelize data class Ok(val obfuscatedId: String) : PlayPurchaseInitResult() - - @Parcelize data class Error(val error: PlayPurchaseInitError) : PlayPurchaseInitResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt deleted file mode 100644 index b0434c22f9..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyError.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class PlayPurchaseVerifyError : Parcelable { - // TODO: Add more errors here. - OtherError -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt deleted file mode 100644 index 7c5ee4d953..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PlayPurchaseVerifyResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class PlayPurchaseVerifyResult : Parcelable { - @Parcelize data object Ok : PlayPurchaseVerifyResult() - - @Parcelize data class Error(val error: PlayPurchaseVerifyError) : PlayPurchaseVerifyResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt deleted file mode 100644 index 52f495a7a7..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Port.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class Port(val value: Int) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt deleted file mode 100644 index 376f5ef7a4..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PortRange.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class PortRange(val from: Int, val to: Int) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt deleted file mode 100644 index d3c6aacba9..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Providers.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Suppress("ensure value classes property is named value") -@JvmInline -@Parcelize -value class Providers(val providers: HashSet<String>) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt deleted file mode 100644 index 169b6c3856..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/PublicKey.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class PublicKey(val key: ByteArray, val dateCreated: String) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt deleted file mode 100644 index a19267388a..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/QuantumResistantState.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class QuantumResistantState : Parcelable { - Auto, - On, - Off -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt deleted file mode 100644 index 461648209c..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Relay.kt +++ /dev/null @@ -1,16 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Relay( - val hostname: String, - val active: Boolean, - val owned: Boolean, - val provider: String, - val endpointData: RelayEndpointData -) : Parcelable { - val isWireguardRelay - get() = endpointData is RelayEndpointData.Wireguard -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt deleted file mode 100644 index 031b09bace..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayConstraints.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class RelayConstraints( - val location: Constraint<LocationConstraint>, - val providers: Constraint<Providers>, - val ownership: Constraint<Ownership>, - val wireguardConstraints: WireguardConstraints, -) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt deleted file mode 100644 index 86b3f0fa35..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayEndpointData.kt +++ /dev/null @@ -1,14 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class RelayEndpointData : Parcelable { - @Parcelize object Openvpn : RelayEndpointData() - - @Parcelize object Bridge : RelayEndpointData() - - @Parcelize - data class Wireguard(val wireguardRelayEndpointData: WireguardRelayEndpointData) : - RelayEndpointData() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt deleted file mode 100644 index 60d8b6dd35..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayList.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class RelayList( - val countries: ArrayList<RelayListCountry>, - val wireguardEndpointData: WireguardEndpointData -) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt deleted file mode 100644 index 2376609ced..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCity.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class RelayListCity(val name: String, val code: String, val relays: ArrayList<Relay>) : - Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt deleted file mode 100644 index d6d4b8ec6a..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelayListCountry.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class RelayListCountry( - val name: String, - val code: String, - val cities: ArrayList<RelayListCity> -) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt deleted file mode 100644 index 642046f1b8..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RelaySettings.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class RelaySettings : Parcelable { - @Parcelize data object CustomTunnelEndpoint : RelaySettings() - - @Parcelize data class Normal(val relayConstraints: RelayConstraints) : RelaySettings() - - fun relayConstraints(): RelayConstraints? = (this as? Normal)?.relayConstraints -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt deleted file mode 100644 index cc6e7db2bb..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceEvent.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class RemoveDeviceEvent(val accountToken: String, val newDevices: ArrayList<Device>) : - Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt deleted file mode 100644 index 67bf165a37..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/RemoveDeviceResult.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class RemoveDeviceResult : Parcelable { - Ok, - NotFound, - RpcError, - OtherError -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt deleted file mode 100644 index 8124bcc6a6..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/SelectedObfuscation.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class SelectedObfuscation : Parcelable { - Auto, - Off, - Udp2Tcp -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt deleted file mode 100644 index e597797e5a..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/ServiceResult.kt +++ /dev/null @@ -1,23 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.IBinder - -data class ServiceResult(val binder: IBinder?) { - enum class ConnectionState { - CONNECTED, - DISCONNECTED - } - - val connectionState: ConnectionState - get() { - return if (binder == null) { - ConnectionState.DISCONNECTED - } else { - ConnectionState.CONNECTED - } - } - - companion object { - val NOT_CONNECTED = ServiceResult(null) - } -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt deleted file mode 100644 index 108fd32e04..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelOptions.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class TunnelOptions(val wireguard: WireguardTunnelOptions, val dnsOptions: DnsOptions) : - Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt deleted file mode 100644 index 4ab925d014..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/TunnelState.kt +++ /dev/null @@ -1,46 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize -import net.mullvad.talpid.net.TunnelEndpoint -import net.mullvad.talpid.tunnel.ActionAfterDisconnect -import net.mullvad.talpid.tunnel.ErrorState - -sealed class TunnelState : Parcelable { - @Parcelize - data class Disconnected(val location: GeoIpLocation? = null) : TunnelState(), Parcelable - - @Parcelize - class Connecting(val endpoint: TunnelEndpoint?, val location: GeoIpLocation?) : - TunnelState(), Parcelable - - @Parcelize - class Connected(val endpoint: TunnelEndpoint, val location: GeoIpLocation?) : - TunnelState(), Parcelable - - @Parcelize - class Disconnecting(val actionAfterDisconnect: ActionAfterDisconnect) : - TunnelState(), Parcelable - - @Parcelize class Error(val errorState: ErrorState) : TunnelState(), Parcelable - - fun location(): GeoIpLocation? { - return when (this) { - is Connected -> location - is Connecting -> location - is Disconnecting -> null - is Disconnected -> location - is Error -> null - } - } - - fun isSecured(): Boolean { - return when (this) { - is Connected, - is Connecting, - is Disconnecting, -> true - is Disconnected -> false - is Error -> this.errorState.isBlocking - } - } -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt deleted file mode 100644 index f01bb35c6f..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/Udp2TcpObfuscationSettings.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class Udp2TcpObfuscationSettings(val port: Constraint<Int>) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt deleted file mode 100644 index ebfe9e8cd6..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/UpdateCustomListResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class UpdateCustomListResult : Parcelable { - @Parcelize data object Ok : UpdateCustomListResult() - - @Parcelize data class Error(val error: CustomListsError) : UpdateCustomListResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt deleted file mode 100644 index efe05e2f5c..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmission.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class VoucherSubmission(val timeAdded: Long, val newExpiry: String) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt deleted file mode 100644 index 1cf778400a..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionError.kt +++ /dev/null @@ -1,12 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class VoucherSubmissionError : Parcelable { - InvalidVoucher, - VoucherAlreadyUsed, - RpcError, - OtherError, -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt deleted file mode 100644 index 4163b782d4..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/VoucherSubmissionResult.kt +++ /dev/null @@ -1,10 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -sealed class VoucherSubmissionResult : Parcelable { - @Parcelize data class Ok(val submission: VoucherSubmission) : VoucherSubmissionResult() - - @Parcelize data class Error(val error: VoucherSubmissionError) : VoucherSubmissionResult() -} diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt deleted file mode 100644 index 1725b01f0f..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardConstraints.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class WireguardConstraints(val port: Constraint<Port>) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt deleted file mode 100644 index 0a21221bb0..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardEndpointData.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class WireguardEndpointData(val portRanges: ArrayList<PortRange>) : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt deleted file mode 100644 index 4a1930dd43..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardRelayEndpointData.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize object WireguardRelayEndpointData : Parcelable diff --git a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt deleted file mode 100644 index f4a869a4ea..0000000000 --- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/model/WireguardTunnelOptions.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.mullvadvpn.model - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class WireguardTunnelOptions(val mtu: Int?, val quantumResistant: QuantumResistantState) : - Parcelable diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatLongTest.kt index b8608ca55c..8abef5d9b3 100644 --- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatLongTest.kt +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatLongTest.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import kotlin.math.sqrt import org.junit.jupiter.api.Assertions.assertEquals diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatitudeTest.kt index c883f20bfc..214afef127 100644 --- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LatitudeTest.kt +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LatitudeTest.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import kotlin.math.absoluteValue import kotlin.test.assertEquals diff --git a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LongitudeTest.kt index 69d3445417..88017cdcea 100644 --- a/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/model/LongitudeTest.kt +++ b/android/lib/model/src/test/kotlin/net/mullvad/mullvadvpn/lib/model/LongitudeTest.kt @@ -1,4 +1,4 @@ -package net.mullvad.mullvadvpn.model +package net.mullvad.mullvadvpn.lib.model import kotlin.math.absoluteValue import kotlin.test.assertEquals diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index 38a839bed3..f9acacb5d9 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -80,19 +80,18 @@ <string name="vpn_settings_not_found">There is no VPN settings on your device</string> <string name="auto_connect_carousel_first_slide_top_text">The Auto-connect and Lockdown mode settings can be found in the Android system settings, follow this guide to enable one or both.</string> <string name="auto_connect_carousel_first_slide_bottom_text"> - <![CDATA[1. After clicking on the <b>Go to VPN settings</b> button below, click on the cogwheel next to the <b>Mullvad VPN</b> name.]]> + <![CDATA[1. After clicking on the <b>Go to VPN settings</b> button below, click on the cogwheel next to the <b>Mullvad VPN</b> name.]]> </string> <string name="auto_connect_carousel_second_slide_top_text">Auto-connect is called Always-on VPN in the Android system settings and it makes sure you are constantly connected to the VPN tunnel and auto connects after restart.</string> <string name="auto_connect_carousel_second_slide_bottom_text"> - <![CDATA[2. To enable Auto-connect, click on the toggle next to <b>Always-on VPN</b>.]]> - </string> + <![CDATA[2. To enable Auto-connect, click on the toggle next to <b>Always-on VPN</b>.]]> + </string> <string name="auto_connect_carousel_third_slide_top_text"> <![CDATA[The Lockdown mode blocks all internet access if the VPN tunnel is manually disconnected. <br/><b>Warning: This setting blocks split apps and the Local Network Sharing feature</b>.]]> </string> <string name="auto_connect_carousel_third_slide_bottom_text"> <![CDATA[3. To enable Lockdown mode, click on the toggle next to <b>Block connections without VPN</b>.]]> </string> - <string name="auto_connect_footer">Automatically connect to a server when the app launches.</string> <string name="wireguard_mtu">WireGuard MTU</string> <string name="wireguard_mtu_footer">Set WireGuard MTU value. Valid range: %1$d - %2$d.</string> @@ -281,7 +280,9 @@ <string name="loading_verifying">Verifying purchase...</string> <string name="copied_logs_to_clipboard">Copied logs to clipboard</string> <string name="auto_connect_legacy">Auto-connect (legacy)</string> - <string name="auto_connect_footer_legacy"><![CDATA[Please use the <b>Always-on</b> system setting instead by following the guide in <b>%s</b> above.]]></string> + <string name="auto_connect_footer_legacy"> + <![CDATA[Please use the <b>Always-on</b> system setting instead by following the guide in <b>%s</b> above.]]> + </string> <string name="custom_lists">Custom lists</string> <string name="all_locations">All locations</string> <string name="edit_lists">Edit lists</string> @@ -295,9 +296,7 @@ <string name="list_name">List name</string> <string name="locations">Locations</string> <string name="edit_locations">Edit locations</string> - <string name="delete_custom_list_confirmation_description"> - Delete \"%s\"? - </string> + <string name="delete_custom_list_confirmation_description">Delete \"%s\"?</string> <string name="custom_list_error_list_exists">Name is already taken.</string> <string name="update_list_name">Update list name</string> <string name="no_custom_lists_available">No custom lists available</string> @@ -344,4 +343,5 @@ <string name="settings_patch_error_recursion_limit">Recursion limit</string> <string name="settings_patch_success">Import successful, overrides active</string> <string name="overrides_cleared">Overrides cleared</string> + <string name="unsecured_vpn_permission_error">Unsecured (No VPN permission)</string> </resources> diff --git a/android/lib/shared/build.gradle.kts b/android/lib/shared/build.gradle.kts new file mode 100644 index 0000000000..88b5cfb3c9 --- /dev/null +++ b/android/lib/shared/build.gradle.kts @@ -0,0 +1,47 @@ +plugins { + id(Dependencies.Plugin.androidLibraryId) + id(Dependencies.Plugin.kotlinAndroidId) + id(Dependencies.Plugin.kotlinParcelizeId) + id(Dependencies.Plugin.junit5) version Versions.Plugin.junit5 +} + +android { + namespace = "net.mullvad.mullvadvpn.lib.shared" + compileSdk = Versions.Android.compileSdkVersion + + defaultConfig { minSdk = Versions.Android.minSdkVersion } + + compileOptions { + sourceCompatibility = JavaVersion.VERSION_17 + targetCompatibility = JavaVersion.VERSION_17 + } + + kotlinOptions { jvmTarget = Versions.jvmTarget } + + lint { + lintConfig = file("${rootProject.projectDir}/config/lint.xml") + abortOnError = true + warningsAsErrors = true + } + buildFeatures { buildConfig = true } +} + +dependencies { + implementation(project(Dependencies.Mullvad.commonLib)) + implementation(project(Dependencies.Mullvad.daemonGrpc)) + implementation(project(Dependencies.Mullvad.modelLib)) + + implementation(Dependencies.Arrow.core) + implementation(Dependencies.Kotlin.stdlib) + implementation(Dependencies.KotlinX.coroutinesAndroid) + implementation(Dependencies.jodaTime) + + testImplementation(Dependencies.Kotlin.test) + testImplementation(Dependencies.KotlinX.coroutinesTest) + testImplementation(Dependencies.MockK.core) + testImplementation(Dependencies.junitApi) + testImplementation(Dependencies.junitParams) + testImplementation(Dependencies.turbine) + testImplementation(project(Dependencies.Mullvad.commonTestLib)) + testRuntimeOnly(Dependencies.junitEngine) +} 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() + } +} diff --git a/android/lib/talpid/build.gradle.kts b/android/lib/talpid/build.gradle.kts index ac760a860e..00409f9482 100644 --- a/android/lib/talpid/build.gradle.kts +++ b/android/lib/talpid/build.gradle.kts @@ -25,6 +25,9 @@ android { } dependencies { + implementation(project(Dependencies.Mullvad.modelLib)) + implementation(Dependencies.Kotlin.stdlib) implementation(Dependencies.KotlinX.coroutinesAndroid) + implementation(Dependencies.AndroidX.lifecycleService) } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt index cdc16567e1..905f59f313 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/ConnectivityListener.kt @@ -7,7 +7,6 @@ import android.net.Network import android.net.NetworkCapabilities import android.net.NetworkRequest import kotlin.properties.Delegates.observable -import net.mullvad.talpid.util.EventNotifier class ConnectivityListener { private val availableNetworks = HashSet<Network>() @@ -21,22 +20,19 @@ class ConnectivityListener { override fun onLost(network: Network) { availableNetworks.remove(network) - isConnected = !availableNetworks.isEmpty() + isConnected = availableNetworks.isNotEmpty() } } private lateinit var connectivityManager: ConnectivityManager - val connectivityNotifier = EventNotifier(false) - + // Used by JNI var isConnected by observable(false) { _, oldValue, newValue -> if (newValue != oldValue) { if (senderAddress != 0L) { notifyConnectivityChange(newValue, senderAddress) } - - connectivityNotifier.notify(newValue) } } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt new file mode 100644 index 0000000000..efb29c31c6 --- /dev/null +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/LifecycleVpnService.kt @@ -0,0 +1,56 @@ +package net.mullvad.talpid + +import android.content.Intent +import android.net.VpnService +import android.os.IBinder +import androidx.annotation.CallSuper +import androidx.lifecycle.Lifecycle +import androidx.lifecycle.LifecycleOwner +import androidx.lifecycle.ServiceLifecycleDispatcher + +/** + * A VpnService that is also a [LifecycleOwner]. See source: + * https://cs.android.com/androidx/platform/frameworks/support/+/androidx-main:lifecycle/lifecycle-service/src/main/java/androidx/lifecycle/LifecycleService.kt?q=file:androidx%2Flifecycle%2FLifecycleService.kt%20class:androidx.lifecycle.LifecycleService + */ +open class LifecycleVpnService : VpnService(), LifecycleOwner { + + private val dispatcher = ServiceLifecycleDispatcher(this) + + @CallSuper + override fun onCreate() { + dispatcher.onServicePreSuperOnCreate() + super.onCreate() + } + + @CallSuper + override fun onBind(intent: Intent?): IBinder? { + dispatcher.onServicePreSuperOnBind() + return super.onBind(intent) + } + + @Deprecated("Deprecated in Java") + @Suppress("DEPRECATION") + @CallSuper + override fun onStart(intent: Intent?, startId: Int) { + dispatcher.onServicePreSuperOnStart() + super.onStart(intent, startId) + } + + // this method is added only to annotate it with @CallSuper. + // In usual Service, super.onStartCommand is no-op, but in LifecycleService + // it results in dispatcher.onServicePreSuperOnStart() call, because + // super.onStartCommand calls onStart(). + @CallSuper + override fun onStartCommand(intent: Intent?, flags: Int, startId: Int): Int { + return super.onStartCommand(intent, flags, startId) + } + + @CallSuper + override fun onDestroy() { + dispatcher.onServicePreSuperOnDestroy() + super.onDestroy() + } + + override val lifecycle: Lifecycle + get() = dispatcher.lifecycle +} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt index 76abde2a01..e89c841d25 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/TalpidVpnService.kt @@ -1,16 +1,17 @@ package net.mullvad.talpid -import android.net.VpnService import android.os.ParcelFileDescriptor import android.util.Log +import androidx.annotation.CallSuper import java.net.Inet4Address import java.net.Inet6Address import java.net.InetAddress import kotlin.properties.Delegates.observable -import net.mullvad.talpid.tun_provider.TunConfig +import net.mullvad.talpid.model.CreateTunResult +import net.mullvad.talpid.model.TunConfig import net.mullvad.talpid.util.TalpidSdkUtils.setMeteredIfSupported -open class TalpidVpnService : VpnService() { +open class TalpidVpnService : LifecycleVpnService() { private var activeTunStatus by observable<CreateTunResult?>(null) { _, oldTunStatus, _ -> val oldTunFd = @@ -29,17 +30,19 @@ open class TalpidVpnService : VpnService() { get() = activeTunStatus?.isOpen ?: false private var currentTunConfig = defaultTunConfig() - private var tunIsStale = false - - protected var disallowedApps: List<String>? = null + // Used by JNI val connectivityListener = ConnectivityListener() + @CallSuper override fun onCreate() { + super.onCreate() connectivityListener.register(this) } + @CallSuper override fun onDestroy() { + super.onDestroy() connectivityListener.unregister() } @@ -47,14 +50,13 @@ open class TalpidVpnService : VpnService() { synchronized(this) { val tunStatus = activeTunStatus - if (config == currentTunConfig && tunIsOpen && !tunIsStale) { + if (config == currentTunConfig && tunIsOpen) { return tunStatus!! } else { val newTunStatus = createTun(config) currentTunConfig = config activeTunStatus = newTunStatus - tunIsStale = false return newTunStatus } @@ -78,17 +80,13 @@ open class TalpidVpnService : VpnService() { synchronized(this) { activeTunStatus = null } } - fun markTunAsStale() { - synchronized(this) { tunIsStale = true } - } - private fun createTun(config: TunConfig): CreateTunResult { if (prepare(this) != null) { // VPN permission wasn't granted return CreateTunResult.PermissionDenied } - var invalidDnsServerAddresses = ArrayList<InetAddress>() + val invalidDnsServerAddresses = ArrayList<InetAddress>() val builder = Builder().apply { @@ -120,11 +118,7 @@ open class TalpidVpnService : VpnService() { addRoute(route.address, route.prefixLength.toInt()) } - disallowedApps?.let { apps -> - for (app in apps) { - addDisallowedApplication(app) - } - } + config.excludedPackages.forEach { app -> addDisallowedApplication(app) } setMtu(config.mtu) setBlocking(false) setMeteredIfSupported(false) diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt index 33f62026d6..089112e3ab 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/CreateTunResult.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/CreateTunResult.kt @@ -1,4 +1,4 @@ -package net.mullvad.talpid +package net.mullvad.talpid.model import java.net.InetAddress @@ -17,7 +17,7 @@ sealed class CreateTunResult { get() = true } - object PermissionDenied : CreateTunResult() + data object PermissionDenied : CreateTunResult() - object TunnelDeviceError : CreateTunResult() + data object TunnelDeviceError : CreateTunResult() } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/InetNetwork.kt index a8490b48bf..a9c257c3e4 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/InetNetwork.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/InetNetwork.kt @@ -1,4 +1,4 @@ -package net.mullvad.talpid.tun_provider +package net.mullvad.talpid.model import java.net.Inet6Address import java.net.InetAddress diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/TunConfig.kt index 7efd3f7763..955a6f4454 100644 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tun_provider/TunConfig.kt +++ b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/model/TunConfig.kt @@ -1,4 +1,4 @@ -package net.mullvad.talpid.tun_provider +package net.mullvad.talpid.model import java.net.InetAddress @@ -6,5 +6,6 @@ data class TunConfig( val addresses: ArrayList<InetAddress>, val dnsServers: ArrayList<InetAddress>, val routes: ArrayList<InetNetwork>, + val excludedPackages: ArrayList<String>, val mtu: Int ) diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt deleted file mode 100644 index 8937bd0122..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/Endpoint.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.talpid.net - -import android.os.Parcelable -import java.net.InetSocketAddress -import kotlinx.parcelize.Parcelize - -@Parcelize -data class Endpoint(val address: InetSocketAddress, val protocol: TransportProtocol) : Parcelable diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt deleted file mode 100644 index 9ec96b1494..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationEndpoint.kt +++ /dev/null @@ -1,8 +0,0 @@ -package net.mullvad.talpid.net - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -data class ObfuscationEndpoint(val endpoint: Endpoint, val obfuscationType: ObfuscationType) : - Parcelable diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt deleted file mode 100644 index 72409d9026..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/net/ObfuscationType.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mullvad.talpid.net - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class ObfuscationType : Parcelable { - Udp2Tcp -} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt deleted file mode 100644 index a62abaacd0..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ActionAfterDisconnect.kt +++ /dev/null @@ -1,11 +0,0 @@ -package net.mullvad.talpid.tunnel - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class ActionAfterDisconnect : Parcelable { - Nothing, - Block, - Reconnect -} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt deleted file mode 100644 index 070d190beb..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorState.kt +++ /dev/null @@ -1,6 +0,0 @@ -package net.mullvad.talpid.tunnel - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize data class ErrorState(val cause: ErrorStateCause, val isBlocking: Boolean) : Parcelable diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt deleted file mode 100644 index fc35e4e23e..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/ErrorStateCause.kt +++ /dev/null @@ -1,36 +0,0 @@ -package net.mullvad.talpid.tunnel - -import android.os.Parcelable -import java.net.InetAddress -import kotlinx.parcelize.Parcelize - -private const val AUTH_FAILED_REASON_EXPIRED_ACCOUNT = "[EXPIRED_ACCOUNT]" - -sealed class ErrorStateCause : Parcelable { - @Parcelize - class AuthFailed(private val reason: String?) : ErrorStateCause() { - fun isCausedByExpiredAccount(): Boolean { - return reason == AUTH_FAILED_REASON_EXPIRED_ACCOUNT - } - } - - @Parcelize data object Ipv6Unavailable : ErrorStateCause() - - @Parcelize - data class SetFirewallPolicyError(val firewallPolicyError: FirewallPolicyError) : - ErrorStateCause() - - @Parcelize data object SetDnsError : ErrorStateCause() - - @Parcelize - data class InvalidDnsServers(val addresses: ArrayList<InetAddress>) : ErrorStateCause() - - @Parcelize data object StartTunnelError : ErrorStateCause() - - @Parcelize - data class TunnelParameterError(val error: ParameterGenerationError) : ErrorStateCause() - - @Parcelize data object IsOffline : ErrorStateCause() - - @Parcelize data object VpnPermissionDenied : ErrorStateCause() -} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt deleted file mode 100644 index c6f19e71af..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/tunnel/FirewallPolicyError.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mullvad.talpid.tunnel - -import android.os.Parcelable -import kotlinx.parcelize.Parcelize - -@Parcelize -enum class FirewallPolicyError : Parcelable { - Generic -} diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt deleted file mode 100644 index 148b56eb45..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifier.kt +++ /dev/null @@ -1,76 +0,0 @@ -package net.mullvad.talpid.util - -import kotlin.properties.Delegates.observable - -// Manages listeners interested in receiving events of type T -// -// The listeners subscribe using an ID object. This ID is used later on for unsubscribing. The only -// requirement is that the object uses the default implementation of the `hashCode` and `equals` -// methods inherited from `Any` (or `Object` in Java). -// -// If the ID object class (or any of its super-classes) overrides `hashCode` or `equals`, -// unsubscribe might not work correctly. -class EventNotifier<T>(private val initialValue: T) { - private val listeners = LinkedHashMap<Any, (T) -> Unit>() - - var latestEvent = initialValue - private set - - fun notify(event: T) { - synchronized(this) { - latestEvent = event - - for (listener in listeners.values) { - listener(event) - } - } - } - - fun notifyIfChanged(event: T) { - synchronized(this) { - if (latestEvent != event) { - notify(event) - } - } - } - - fun subscribe(id: Any, listener: (T) -> Unit) { - subscribe(id, true, listener) - } - - fun subscribe(id: Any, startWithLatestEvent: Boolean, listener: (T) -> Unit) { - synchronized(this) { - listeners.put(id, listener) - if (startWithLatestEvent) listener(latestEvent) - } - } - - fun hasListeners(): Boolean { - synchronized(this) { - return !listeners.isEmpty() - } - } - - fun unsubscribe(id: Any) { - synchronized(this) { listeners.remove(id) } - } - - fun unsubscribeAll() { - synchronized(this) { listeners.clear() } - } - - fun notifiable() = observable(latestEvent) { _, _, newValue -> notify(newValue) } -} - -fun <T> autoSubscribable(id: Any, fallback: T, listener: (T) -> Unit) = - observable<EventNotifier<T>?>(null) { _, old, new -> - if (old != new) { - old?.unsubscribe(id) - - if (new == null) { - listener.invoke(fallback) - } else { - new.subscribe(id, listener) - } - } - } diff --git a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt b/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt deleted file mode 100644 index add362fcb1..0000000000 --- a/android/lib/talpid/src/main/kotlin/net/mullvad/talpid/util/EventNotifierExtensions.kt +++ /dev/null @@ -1,9 +0,0 @@ -package net.mullvad.talpid.util - -import kotlinx.coroutines.channels.awaitClose -import kotlinx.coroutines.flow.callbackFlow - -fun <T> EventNotifier<T>.callbackFlowFromSubscription(id: Any) = callbackFlow { - this@callbackFlowFromSubscription.subscribe(id) { this.trySend(it) } - awaitClose { this@callbackFlowFromSubscription.unsubscribe(id) } -} |
