summaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authorDavid Göransson <david.goransson@mullvad.net>2026-02-09 14:52:39 +0100
committerDavid Göransson <david.goransson@mullvad.net>2026-02-09 14:52:39 +0100
commit65e1e0bc25fce156600934d1061c7f8efd7dcb53 (patch)
treede2f334cb053b10b6783a6cc3d7ebc8926a2a1e6
parentdce76c4e2f01b87922e6b4c540a9883ea7cd5065 (diff)
parent04b8e731b0f71ad4b1ff90dada13504207d79da8 (diff)
downloadmullvadvpn-pre-investigate-migration-from-cjs-to-esm-tb-4h-des-1739.tar.xz
mullvadvpn-pre-investigate-migration-from-cjs-to-esm-tb-4h-des-1739.zip
Merge branch 'split-tunneling-feature-module'pre-investigate-migration-from-cjs-to-esm-tb-4h-des-1739
-rw-r--r--android/app/build.gradle.kts1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/SplitTunnelingContentKey.kt7
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt43
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt1
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt2
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt4
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt17
-rw-r--r--android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt44
-rw-r--r--android/gradle/libs.versions.toml4
-rw-r--r--android/lib/feature/splittunneling/impl/build.gradle.kts24
-rw-r--r--android/lib/feature/splittunneling/impl/src/androidTest/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt (renamed from android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt)6
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/AndroidManifest.xml4
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/ContentType.kt10
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingContentKey.kt1
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt)73
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt13
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt51
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt)62
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt)2
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt)7
-rw-r--r--android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/extensions/DrawableExtensions.kt (renamed from android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Drawable.kt)6
-rw-r--r--android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt (renamed from android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt)36
-rw-r--r--android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt (renamed from android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt)20
-rw-r--r--android/settings.gradle.kts12
25 files changed, 241 insertions, 211 deletions
diff --git a/android/app/build.gradle.kts b/android/app/build.gradle.kts
index 9a66c364c0..ae02154e2b 100644
--- a/android/app/build.gradle.kts
+++ b/android/app/build.gradle.kts
@@ -387,6 +387,7 @@ dependencies {
implementation(projects.lib.grpc)
implementation(projects.lib.endpoint)
implementation(projects.lib.feature.daita.impl)
+ implementation(projects.lib.feature.splittunneling.impl)
implementation(projects.lib.map)
implementation(projects.lib.model)
implementation(projects.lib.navigation)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/SplitTunnelingContentKey.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/SplitTunnelingContentKey.kt
deleted file mode 100644
index 28a123410f..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/constant/SplitTunnelingContentKey.kt
+++ /dev/null
@@ -1,7 +0,0 @@
-package net.mullvad.mullvadvpn.compose.constant
-
-object SplitTunnelingContentKey {
- const val EXCLUDED_APPLICATIONS = "excluded"
- const val SHOW_SYSTEM_APPLICATIONS = "show_system"
- const val INCLUDED_APPLICATIONS = "included"
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt
deleted file mode 100644
index adb79292af..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/SplitTunnelingUiStatePreviewParameterProvider.kt
+++ /dev/null
@@ -1,43 +0,0 @@
-package net.mullvad.mullvadvpn.compose.preview
-
-import androidx.compose.ui.tooling.preview.PreviewParameterProvider
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.applist.AppData
-import net.mullvad.mullvadvpn.lib.common.Lc
-import net.mullvad.mullvadvpn.lib.common.toLc
-import net.mullvad.mullvadvpn.viewmodel.Loading
-import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingUiState
-
-class SplitTunnelingUiStatePreviewParameterProvider :
- PreviewParameterProvider<Lc<Loading, SplitTunnelingUiState>> {
- override val values =
- sequenceOf(
- SplitTunnelingUiState(
- enabled = true,
- excludedApps =
- listOf(
- AppData(
- packageName = "my.package.a",
- name = "TitleA",
- iconRes = R.drawable.icon_android,
- ),
- AppData(
- packageName = "my.package.b",
- name = "TitleB",
- iconRes = R.drawable.icon_android,
- ),
- ),
- includedApps =
- listOf(
- AppData(
- packageName = "my.package.c",
- name = "TitleC",
- iconRes = R.drawable.icon_android,
- )
- ),
- showSystemApps = true,
- )
- .toLc(),
- Lc.Loading(Loading(enabled = true)),
- )
-}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
index 522920b9ce..667648d4fd 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/ConnectScreen.kt
@@ -77,8 +77,8 @@ import com.ramcosta.composedestinations.generated.destinations.OutOfTimeDestinat
import com.ramcosta.composedestinations.generated.destinations.SelectLocationDestination
import com.ramcosta.composedestinations.generated.destinations.ServerIpOverridesDestination
import com.ramcosta.composedestinations.generated.destinations.SettingsDestination
-import com.ramcosta.composedestinations.generated.destinations.SplitTunnelingDestination
import com.ramcosta.composedestinations.generated.destinations.VpnSettingsDestination
+import com.ramcosta.composedestinations.generated.splittunneling.destinations.SplitTunnelingDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import com.ramcosta.composedestinations.result.ResultRecipient
import kotlinx.coroutines.launch
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
index 9bc5d916c0..3f2d6e53c8 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/MullvadApp.kt
@@ -43,6 +43,7 @@ annotation class MainGraph {
@ExternalDestination<DaitaDestination>
@ExternalDestination<DaitaDirectOnlyInfoDestination>
@ExternalDestination<DaitaDirectOnlyConfirmationDestination>
+ // @ExternalDestination<SplitTunnelingDestination>
companion object Includes
}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt
index 26110c45fe..bce63ad94e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SettingsScreen.kt
@@ -28,8 +28,8 @@ import com.ramcosta.composedestinations.generated.destinations.AppearanceDestina
import com.ramcosta.composedestinations.generated.destinations.MultihopDestination
import com.ramcosta.composedestinations.generated.destinations.NotificationSettingsDestination
import com.ramcosta.composedestinations.generated.destinations.ReportProblemDestination
-import com.ramcosta.composedestinations.generated.destinations.SplitTunnelingDestination
import com.ramcosta.composedestinations.generated.destinations.VpnSettingsDestination
+import com.ramcosta.composedestinations.generated.splittunneling.destinations.SplitTunnelingDestination
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import net.mullvad.mullvadvpn.R
import net.mullvad.mullvadvpn.compose.extensions.createUriHook
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
index 745a220510..a02096cae6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/di/UiModule.kt
@@ -5,13 +5,14 @@ import android.os.Build
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.MainScope
import net.mullvad.mullvadvpn.BuildConfig
-import net.mullvad.mullvadvpn.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.compose.screen.location.LocationBottomSheetState
import net.mullvad.mullvadvpn.compose.screen.location.RelayListScrollConnection
import net.mullvad.mullvadvpn.compose.util.BackstackObserver
import net.mullvad.mullvadvpn.constant.IS_FDROID_BUILD
import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD
import net.mullvad.mullvadvpn.feature.daita.impl.DaitaViewModel
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.SplitTunnelingViewModel
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.lib.model.RelayListType
import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
import net.mullvad.mullvadvpn.lib.repository.ApiAccessRepository
@@ -104,7 +105,6 @@ import net.mullvad.mullvadvpn.viewmodel.SelectPortViewModel
import net.mullvad.mullvadvpn.viewmodel.ServerIpOverridesViewModel
import net.mullvad.mullvadvpn.viewmodel.SettingsViewModel
import net.mullvad.mullvadvpn.viewmodel.SplashViewModel
-import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
import net.mullvad.mullvadvpn.viewmodel.ViewLogsViewModel
import net.mullvad.mullvadvpn.viewmodel.VoucherDialogViewModel
import net.mullvad.mullvadvpn.viewmodel.VpnSettingsViewModel
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt
index fc6acfeb6c..20d8f4fc8e 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/util/PackageManagerExtensions.kt
@@ -1,18 +1 @@
package net.mullvad.mullvadvpn.util
-
-import android.content.pm.PackageManager
-import android.graphics.drawable.Drawable
-
-fun PackageManager.getApplicationIconOrNull(packageName: String): Drawable? =
- try {
- getApplicationIcon(packageName)
- } catch (e: PackageManager.NameNotFoundException) {
- // Name not found is thrown if the application is not installed
- null
- } catch (e: IllegalArgumentException) {
- // IllegalArgumentException is thrown if the application has an invalid icon
- null
- } catch (e: OutOfMemoryError) {
- // OutOfMemoryError is thrown if the icon is too large
- null
- }
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
deleted file mode 100644
index fdb828dae0..0000000000
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelState.kt
+++ /dev/null
@@ -1,44 +0,0 @@
-package net.mullvad.mullvadvpn.viewmodel
-
-import net.mullvad.mullvadvpn.applist.AppData
-import net.mullvad.mullvadvpn.lib.common.Lc
-import net.mullvad.mullvadvpn.lib.common.toLc
-import net.mullvad.mullvadvpn.lib.model.AppId
-
-data class SplitTunnelingViewModelState(
- val enabled: Boolean = false,
- val excludedApps: Set<AppId> = emptySet(),
- val allApps: List<AppData>? = null,
- val showSystemApps: Boolean = false,
-) {
- fun toUiState(isModal: Boolean): Lc<Loading, SplitTunnelingUiState> {
- return allApps
- ?.partition { appData ->
- if (enabled) {
- excludedApps.contains(AppId(appData.packageName))
- } else {
- false
- }
- }
- ?.let { (excluded, included) ->
- SplitTunnelingUiState(
- enabled = enabled,
- excludedApps = excluded.sortedWith(descendingByNameComparator),
- includedApps =
- if (showSystemApps) {
- included
- } else {
- included.filter { appData -> !appData.isSystemApp }
- }
- .sortedWith(descendingByNameComparator),
- showSystemApps = showSystemApps,
- isModal = isModal,
- )
- .toLc()
- } ?: Lc.Loading(Loading(enabled = enabled, isModal))
- }
-
- companion object {
- private val descendingByNameComparator = compareBy<AppData> { it.name.lowercase() }
- }
-}
diff --git a/android/gradle/libs.versions.toml b/android/gradle/libs.versions.toml
index 9a32a80fce..22e4020023 100644
--- a/android/gradle/libs.versions.toml
+++ b/android/gradle/libs.versions.toml
@@ -71,6 +71,8 @@ protobuf = "4.33.4"
protobuf-gradle-plugin = "0.9.6"
turbine = "1.2.1"
annotation-jvm = "1.9.1"
+junit-version = "4.13.2"
+material = "1.10.0"
[libraries]
accompanist-drawablepainter = { module = "com.google.accompanist:accompanist-drawablepainter", version.ref = "drawablepainter" }
@@ -166,6 +168,8 @@ mockk = { module = "io.mockk:mockk", version.ref = "mockk" }
mockk-android = { module = "io.mockk:mockk-android", version.ref = "mockk" }
protobuf-kotlin-lite = { module = "com.google.protobuf:protobuf-kotlin-lite", version.ref = "protobuf" }
turbine = { module = "app.cash.turbine:turbine", version.ref = "turbine" }
+junit = { group = "junit", name = "junit", version.ref = "junit-version" }
+material = { group = "com.google.android.material", name = "material", version.ref = "material" }
[plugins]
android-application = { id = "com.android.application", version.ref = "android-gradle-plugin" }
diff --git a/android/lib/feature/splittunneling/impl/build.gradle.kts b/android/lib/feature/splittunneling/impl/build.gradle.kts
new file mode 100644
index 0000000000..9a4ae9d26d
--- /dev/null
+++ b/android/lib/feature/splittunneling/impl/build.gradle.kts
@@ -0,0 +1,24 @@
+plugins {
+ alias(libs.plugins.mullvad.android.library)
+ alias(libs.plugins.mullvad.android.library.feature.impl)
+ alias(libs.plugins.mullvad.android.library.compose)
+ alias(libs.plugins.kotlin.android)
+ alias(libs.plugins.kotlin.parcelize)
+ alias(libs.plugins.kotlin.ksp)
+}
+
+android {
+ namespace = "net.mullvad.mullvadvpn.feature.splittunneling.impl"
+ ksp { arg("compose-destinations.moduleName", "splittunneling") }
+}
+
+dependencies {
+ implementation(projects.lib.repository)
+
+ implementation(libs.koin.compose)
+ implementation(libs.arrow)
+
+ // Destinations
+ implementation(libs.compose.destinations)
+ ksp(libs.compose.destinations.ksp)
+}
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt b/android/lib/feature/splittunneling/impl/src/androidTest/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt
index b2404a8599..6ab53115a1 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreenTest.kt
+++ b/android/lib/feature/splittunneling/impl/src/androidTest/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreenTest.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.compose.screen
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
import android.graphics.drawable.Drawable
import androidx.compose.ui.test.ExperimentalTestApi
@@ -9,13 +9,11 @@ import io.mockk.MockKAnnotations
import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verify
-import net.mullvad.mullvadvpn.applist.AppData
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData
import net.mullvad.mullvadvpn.lib.common.Lc
import net.mullvad.mullvadvpn.lib.common.toLc
import net.mullvad.mullvadvpn.screen.test.createEdgeToEdgeComposeExtension
import net.mullvad.mullvadvpn.screen.test.setContentWithTheme
-import net.mullvad.mullvadvpn.viewmodel.Loading
-import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingUiState
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.BeforeEach
import org.junit.jupiter.api.Test
diff --git a/android/lib/feature/splittunneling/impl/src/main/AndroidManifest.xml b/android/lib/feature/splittunneling/impl/src/main/AndroidManifest.xml
new file mode 100644
index 0000000000..8bdb7e14b3
--- /dev/null
+++ b/android/lib/feature/splittunneling/impl/src/main/AndroidManifest.xml
@@ -0,0 +1,4 @@
+<?xml version="1.0" encoding="utf-8"?>
+<manifest xmlns:android="http://schemas.android.com/apk/res/android">
+
+</manifest>
diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/ContentType.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/ContentType.kt
new file mode 100644
index 0000000000..3b4c2cac79
--- /dev/null
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/ContentType.kt
@@ -0,0 +1,10 @@
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
+
+internal object ContentType {
+ const val HEADER = 1
+ const val ITEM = 2
+ const val OTHER_ITEM = 3
+ const val DESCRIPTION = 4
+ const val SPACER = 5
+ const val PROGRESS = 6
+}
diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingContentKey.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingContentKey.kt
new file mode 100644
index 0000000000..9e2baa62fd
--- /dev/null
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingContentKey.kt
@@ -0,0 +1 @@
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt
index 394bd0d6d1..9a2ee16158 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/SplitTunnelingScreen.kt
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingScreen.kt
@@ -1,5 +1,6 @@
-package net.mullvad.mullvadvpn.compose.screen
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
+import android.content.pm.PackageManager
import android.graphics.drawable.Drawable
import android.os.Parcelable
import androidx.compose.animation.AnimatedVisibilityScope
@@ -11,8 +12,11 @@ import androidx.compose.foundation.layout.fillMaxSize
import androidx.compose.foundation.layout.height
import androidx.compose.foundation.layout.padding
import androidx.compose.foundation.lazy.LazyColumn
+import androidx.compose.foundation.lazy.LazyItemScope
import androidx.compose.foundation.lazy.LazyListScope
import androidx.compose.foundation.lazy.LazyListState
+import androidx.compose.foundation.lazy.itemsIndexed
+import androidx.compose.material3.HorizontalDivider
import androidx.compose.material3.MaterialTheme
import androidx.compose.runtime.Composable
import androidx.compose.runtime.LaunchedEffect
@@ -26,6 +30,7 @@ import androidx.compose.ui.Modifier
import androidx.compose.ui.draw.alpha
import androidx.compose.ui.focus.FocusDirection
import androidx.compose.ui.focus.FocusManager
+import androidx.compose.ui.graphics.Color
import androidx.compose.ui.platform.LocalContext
import androidx.compose.ui.platform.LocalFocusManager
import androidx.compose.ui.res.stringResource
@@ -34,21 +39,15 @@ import androidx.compose.ui.tooling.preview.PreviewParameter
import androidx.lifecycle.compose.collectAsStateWithLifecycle
import androidx.lifecycle.compose.dropUnlessResumed
import com.ramcosta.composedestinations.annotation.Destination
+import com.ramcosta.composedestinations.annotation.ExternalModuleGraph
import com.ramcosta.composedestinations.navigation.DestinationsNavigator
import kotlinx.coroutines.Dispatchers
import kotlinx.coroutines.launch
import kotlinx.parcelize.Parcelize
-import net.mullvad.mullvadvpn.R
-import net.mullvad.mullvadvpn.applist.AppData
-import net.mullvad.mullvadvpn.compose.constant.CommonContentKey
-import net.mullvad.mullvadvpn.compose.constant.ContentType
-import net.mullvad.mullvadvpn.compose.constant.SplitTunnelingContentKey
-import net.mullvad.mullvadvpn.compose.extensions.itemWithDivider
-import net.mullvad.mullvadvpn.compose.extensions.itemsIndexedWithDivider
-import net.mullvad.mullvadvpn.compose.preview.SplitTunnelingUiStatePreviewParameterProvider
-import net.mullvad.mullvadvpn.compose.util.hasValidSize
-import net.mullvad.mullvadvpn.compose.util.isBelowMaxByteSize
import net.mullvad.mullvadvpn.core.animation.SlideInFromRightTransition
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.extensions.hasValidSize
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.extensions.isBelowMaxByteSize
import net.mullvad.mullvadvpn.lib.common.Lc
import net.mullvad.mullvadvpn.lib.model.FeatureIndicator
import net.mullvad.mullvadvpn.lib.ui.component.NavigateBackIconButton
@@ -65,10 +64,6 @@ import net.mullvad.mullvadvpn.lib.ui.theme.AppTheme
import net.mullvad.mullvadvpn.lib.ui.theme.Dimens
import net.mullvad.mullvadvpn.lib.ui.theme.color.AlphaDisabled
import net.mullvad.mullvadvpn.lib.ui.theme.color.AlphaVisible
-import net.mullvad.mullvadvpn.util.getApplicationIconOrNull
-import net.mullvad.mullvadvpn.viewmodel.Loading
-import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingUiState
-import net.mullvad.mullvadvpn.viewmodel.SplitTunnelingViewModel
import org.koin.androidx.compose.koinViewModel
@Preview("ShowAppList|Loading")
@@ -93,7 +88,7 @@ private fun PreviewSplitTunnelingScreen(
@Parcelize data class SplitTunnelingNavArgs(val isModal: Boolean = false) : Parcelable
@OptIn(ExperimentalSharedTransitionApi::class)
-@Destination<MainGraph>(
+@Destination<ExternalModuleGraph>(
style = SlideInFromRightTransition::class,
navArgs = SplitTunnelingNavArgs::class,
)
@@ -377,3 +372,49 @@ private fun Lc<Loading, SplitTunnelingUiState>.enabled(): Boolean =
is Lc.Loading -> this.value.enabled
is Lc.Content -> this.value.enabled
}
+
+fun PackageManager.getApplicationIconOrNull(packageName: String): Drawable? =
+ try {
+ getApplicationIcon(packageName)
+ } catch (e: PackageManager.NameNotFoundException) {
+ // Name not found is thrown if the application is not installed
+ null
+ } catch (e: IllegalArgumentException) {
+ // IllegalArgumentException is thrown if the application has an invalid icon
+ null
+ } catch (e: OutOfMemoryError) {
+ // OutOfMemoryError is thrown if the icon is too large
+ null
+ }
+
+object CommonContentKey {
+ const val DESCRIPTION = "description"
+ const val PROGRESS = "progress"
+}
+
+private inline fun <T> LazyListScope.itemsIndexedWithDivider(
+ items: List<T>,
+ noinline key: ((index: Int, item: T) -> Any)? = null,
+ crossinline contentType: (index: Int, item: T) -> Any? = { _, _ -> null },
+ crossinline itemContent: @Composable LazyItemScope.(index: Int, item: T) -> Unit,
+) =
+ itemsIndexed(items = items, key = key, contentType = contentType) { index, item ->
+ itemContent(index, item)
+ HorizontalDivider(color = Color.Transparent)
+ }
+
+private inline fun LazyListScope.itemWithDivider(
+ key: Any? = null,
+ contentType: Any? = null,
+ crossinline itemContent: @Composable LazyItemScope.() -> Unit,
+) =
+ item(key = key, contentType = contentType) {
+ itemContent()
+ HorizontalDivider(color = Color.Transparent)
+ }
+
+internal object SplitTunnelingContentKey {
+ const val EXCLUDED_APPLICATIONS = "excluded"
+ const val SHOW_SYSTEM_APPLICATIONS = "show_system"
+ const val INCLUDED_APPLICATIONS = "included"
+}
diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt
new file mode 100644
index 0000000000..7bb091ea1c
--- /dev/null
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiState.kt
@@ -0,0 +1,13 @@
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
+
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData
+
+data class Loading(val enabled: Boolean = false, val isModal: Boolean = false)
+
+data class SplitTunnelingUiState(
+ val enabled: Boolean = false,
+ val excludedApps: List<AppData> = emptyList(),
+ val includedApps: List<AppData> = emptyList(),
+ val showSystemApps: Boolean = false,
+ val isModal: Boolean = false,
+)
diff --git a/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt
new file mode 100644
index 0000000000..94524e1909
--- /dev/null
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingUiStatePreviewParameterProvider.kt
@@ -0,0 +1,51 @@
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
+
+import androidx.compose.ui.tooling.preview.PreviewParameterProvider
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData
+import net.mullvad.mullvadvpn.lib.common.Lc
+import net.mullvad.mullvadvpn.lib.common.toLc
+import net.mullvad.mullvadvpn.lib.ui.resource.R
+
+class SplitTunnelingUiStatePreviewParameterProvider :
+ PreviewParameterProvider<Lc<Loading, SplitTunnelingUiState>> {
+ override val values =
+ sequenceOf(
+ SplitTunnelingUiState(
+ enabled = true,
+ excludedApps = excludedApps,
+ includedApps = includedApps,
+ showSystemApps = true,
+ )
+ .toLc(),
+ SplitTunnelingUiState(
+ enabled = true,
+ excludedApps = excludedApps,
+ includedApps = includedApps.filter { !it.isSystemApp },
+ showSystemApps = false,
+ )
+ .toLc(),
+ Lc.Loading(Loading(enabled = true)),
+ )
+}
+
+private val excludedApps =
+ listOf(
+ AppData(packageName = "my.package.a", name = "TitleA", iconRes = R.drawable.icon_android),
+ AppData(packageName = "my.package.b", name = "TitleB", iconRes = R.drawable.icon_android),
+ AppData(
+ packageName = "my.package.c",
+ name = "TitleC (System app)",
+ iconRes = R.drawable.icon_android,
+ isSystemApp = true,
+ ),
+ )
+private val includedApps =
+ listOf(
+ AppData(packageName = "my.package.d", name = "TitleD", iconRes = R.drawable.icon_android),
+ AppData(
+ packageName = "my.package.e",
+ name = "TitleE (System app)",
+ iconRes = R.drawable.icon_android,
+ isSystemApp = true,
+ ),
+ )
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt
index ce1dc92b75..3a9a4d5b58 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModel.kt
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModel.kt
@@ -1,22 +1,22 @@
-package net.mullvad.mullvadvpn.viewmodel
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
import androidx.lifecycle.SavedStateHandle
import androidx.lifecycle.ViewModel
import androidx.lifecycle.viewModelScope
-import com.ramcosta.composedestinations.generated.destinations.SplitTunnelingDestination
+import com.ramcosta.composedestinations.generated.splittunneling.destinations.SplitTunnelingDestination
import kotlinx.coroutines.CoroutineDispatcher
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.flow.SharingStarted
import kotlinx.coroutines.flow.StateFlow
import kotlinx.coroutines.flow.WhileSubscribed
import kotlinx.coroutines.flow.combine
-import kotlinx.coroutines.flow.map
import kotlinx.coroutines.flow.stateIn
import kotlinx.coroutines.launch
-import net.mullvad.mullvadvpn.applist.AppData
-import net.mullvad.mullvadvpn.applist.ApplicationsProvider
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.lib.common.Lc
import net.mullvad.mullvadvpn.lib.common.constant.VIEW_MODEL_STOP_TIMEOUT
+import net.mullvad.mullvadvpn.lib.common.toLc
import net.mullvad.mullvadvpn.lib.model.AppId
import net.mullvad.mullvadvpn.lib.repository.SplitTunnelingRepository
@@ -31,29 +31,37 @@ class SplitTunnelingViewModel(
private val allApps = MutableStateFlow<List<AppData>?>(null)
private val showSystemApps = MutableStateFlow(false)
- private val vmState: StateFlow<SplitTunnelingViewModelState> =
+ val uiState: StateFlow<Lc<Loading, SplitTunnelingUiState>> =
combine(
splitTunnelingRepository.excludedApps,
splitTunnelingRepository.splitTunnelingEnabled,
allApps,
showSystemApps,
) { excludedApps, enabled, allApps, showSystemApps ->
- SplitTunnelingViewModelState(
- excludedApps = excludedApps,
- enabled = enabled,
- allApps = allApps,
- showSystemApps = showSystemApps,
- )
- }
- .stateIn(
- viewModelScope,
- SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
- SplitTunnelingViewModelState(),
- )
+ if (allApps == null) {
+ return@combine Lc.Loading(Loading(enabled = enabled, isModal = navArgs.isModal))
+ }
- val uiState =
- vmState
- .map { it.toUiState(navArgs.isModal) }
+ val (excludedApps, includedApps) =
+ allApps.partition { appData ->
+ if (enabled) {
+ excludedApps.contains(AppId(appData.packageName))
+ } else {
+ false
+ }
+ }
+
+ SplitTunnelingUiState(
+ enabled = enabled,
+ excludedApps = excludedApps,
+ includedApps =
+ if (showSystemApps) includedApps
+ else includedApps.filter { appData -> !appData.isSystemApp },
+ showSystemApps = showSystemApps,
+ isModal = navArgs.isModal,
+ )
+ .toLc()
+ }
.stateIn(
viewModelScope,
SharingStarted.WhileSubscribed(VIEW_MODEL_STOP_TIMEOUT),
@@ -87,16 +95,6 @@ class SplitTunnelingViewModel(
}
private suspend fun fetchApps() {
- appsProvider.getAppsList().let { appsList -> allApps.emit(appsList) }
+ appsProvider.apps().let { appsList -> allApps.emit(appsList) }
}
}
-
-data class Loading(val enabled: Boolean = false, val isModal: Boolean = false)
-
-data class SplitTunnelingUiState(
- val enabled: Boolean = false,
- val excludedApps: List<AppData> = emptyList(),
- val includedApps: List<AppData> = emptyList(),
- val showSystemApps: Boolean = false,
- val isModal: Boolean = false,
-)
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt
index 16b6ce70c3..9c199567bf 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/AppData.kt
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/AppData.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.applist
+package net.mullvad.mullvadvpn.feature.splittunneling.impl.applist
data class AppData(
val packageName: String,
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt
index e38bd77409..c913a865c2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProvider.kt
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProvider.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.applist
+package net.mullvad.mullvadvpn.feature.splittunneling.impl.applist
import android.Manifest
import android.content.pm.ApplicationInfo
@@ -12,7 +12,9 @@ class ApplicationsProvider(
hasInternetPermission(appInfo.packageName) && !isSelfApplication(appInfo.packageName)
}
- fun getAppsList(): List<AppData> {
+ private val descendingByNameComparator = compareBy<AppData> { it.name.lowercase() }
+
+ fun apps(): List<AppData> {
return packageManager
.getInstalledApplications(PackageManager.GET_META_DATA)
.asSequence()
@@ -26,6 +28,7 @@ class ApplicationsProvider(
)
}
.toList()
+ .sortedWith(descendingByNameComparator)
}
private fun hasInternetPermission(packageName: String): Boolean {
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Drawable.kt b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/extensions/DrawableExtensions.kt
index 7d7e2605eb..08d15fff21 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/util/Drawable.kt
+++ b/android/lib/feature/splittunneling/impl/src/main/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/extensions/DrawableExtensions.kt
@@ -1,11 +1,11 @@
-package net.mullvad.mullvadvpn.compose.util
+package net.mullvad.mullvadvpn.feature.splittunneling.impl.extensions
import android.graphics.drawable.BitmapDrawable
import android.graphics.drawable.Drawable
private const val MAX_BITMAP_SIZE_BYTES = 100 * 1024 * 1024 // 100MB
-fun Drawable.isBelowMaxByteSize(): Boolean =
+internal fun Drawable.isBelowMaxByteSize(): Boolean =
if (this is BitmapDrawable) bitmap.byteCount < MAX_BITMAP_SIZE_BYTES else true
-fun Drawable.hasValidSize(): Boolean = intrinsicHeight > 0 && intrinsicWidth > 0
+internal fun Drawable.hasValidSize(): Boolean = intrinsicHeight > 0 && intrinsicWidth > 0
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt
index de0a8656cd..0a3cdcd4ac 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/SplitTunnelingViewModelTest.kt
+++ b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/SplitTunnelingViewModelTest.kt
@@ -1,9 +1,9 @@
-package net.mullvad.mullvadvpn.viewmodel
+package net.mullvad.mullvadvpn.feature.splittunneling.impl
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import arrow.core.right
-import com.ramcosta.composedestinations.generated.navargs.toSavedStateHandle
+import com.ramcosta.composedestinations.generated.splittunneling.navargs.toSavedStateHandle
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
@@ -18,9 +18,8 @@ import kotlinx.coroutines.cancel
import kotlinx.coroutines.flow.MutableStateFlow
import kotlinx.coroutines.test.UnconfinedTestDispatcher
import kotlinx.coroutines.test.runTest
-import net.mullvad.mullvadvpn.applist.AppData
-import net.mullvad.mullvadvpn.applist.ApplicationsProvider
-import net.mullvad.mullvadvpn.compose.screen.SplitTunnelingNavArgs
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.AppData
+import net.mullvad.mullvadvpn.feature.splittunneling.impl.applist.ApplicationsProvider
import net.mullvad.mullvadvpn.lib.common.Lc
import net.mullvad.mullvadvpn.lib.common.test.TestCoroutineRule
import net.mullvad.mullvadvpn.lib.model.AppId
@@ -65,7 +64,7 @@ class SplitTunnelingViewModelTest {
assertIs<Lc.Loading<Loading>>(actualState)
assertEquals(initialExpectedState, actualState)
- verify(exactly = 1) { mockedApplicationsProvider.getAppsList() }
+ verify(exactly = 1) { mockedApplicationsProvider.apps() }
}
@Test
@@ -199,31 +198,8 @@ class SplitTunnelingViewModelTest {
}
}
- @Test
- fun `apps should be sorted by name in descending order`() = runTest {
- // Arrange
- val app1 = AppData("com.example.app1", 0, "App A")
- val app2 = AppData("com.example.app2", 0, "App B")
- val app3 = AppData("com.example.app3", 0, "App Z")
- val appList = listOf(app2, app1, app3)
- val expectedState =
- SplitTunnelingUiState(
- enabled = true,
- includedApps = listOf(app1, app2, app3),
- showSystemApps = false,
- )
- initTestSubject(appList = appList)
-
- // Assert
- testSubject.uiState.test {
- val actualState = awaitItem()
- assertIs<Lc.Content<SplitTunnelingUiState>>(actualState)
- assertEquals(expectedState, actualState.value)
- }
- }
-
private fun initTestSubject(appList: List<AppData>) {
- every { mockedApplicationsProvider.getAppsList() } returns appList
+ every { mockedApplicationsProvider.apps() } returns appList
testSubject =
SplitTunnelingViewModel(
mockedApplicationsProvider,
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt
index efa97c6ab0..2ffbb8f34a 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/applist/ApplicationsProviderTest.kt
+++ b/android/lib/feature/splittunneling/impl/src/test/java/net/mullvad/mullvadvpn/feature/splittunneling/impl/applist/ApplicationsProviderTest.kt
@@ -1,4 +1,4 @@
-package net.mullvad.mullvadvpn.applist
+package net.mullvad.mullvadvpn.feature.splittunneling.impl.applist
import android.Manifest
import android.annotation.SuppressLint
@@ -8,6 +8,7 @@ import io.mockk.every
import io.mockk.mockk
import io.mockk.unmockkAll
import io.mockk.verifyAll
+import kotlin.test.assertEquals
import net.mullvad.mullvadvpn.lib.common.test.assertLists
import org.junit.jupiter.api.AfterEach
import org.junit.jupiter.api.Test
@@ -55,7 +56,7 @@ class ApplicationsProviderTest {
createApplicationInfo(selfPackageName, internet = true, launch = true),
)
- val result = testSubject.getAppsList()
+ val result = testSubject.apps()
val expected =
listOf(
AppData(launchWithInternetPackageName, 0, launchWithInternetPackageName),
@@ -107,6 +108,21 @@ class ApplicationsProviderTest {
}
}
+ @SuppressLint("UseCheckPermission")
+ @Test
+ fun `apps should be returned in descending order`() {
+ val packageNames = listOf("b", "d", "c", "a", "e")
+
+ every {
+ mockedPackageManager.getInstalledApplications(PackageManager.GET_META_DATA)
+ } returns packageNames.map { createApplicationInfo(it, launch = true, internet = true) }
+
+ val actual = testSubject.apps()
+ val expected = packageNames.sorted().map { AppData(it, 0, it) }
+
+ assertEquals(expected, actual)
+ }
+
private fun createApplicationInfo(
packageName: String,
launch: Boolean = false,
diff --git a/android/settings.gradle.kts b/android/settings.gradle.kts
index c49aab0e36..b4698eb222 100644
--- a/android/settings.gradle.kts
+++ b/android/settings.gradle.kts
@@ -20,17 +20,15 @@ dependencyResolutionManagement {
}
includeBuild("rust-android-gradle-plugin")
+
includeBuild("gradle/build-logic")
enableFeaturePreview("TYPESAFE_PROJECT_ACCESSORS")
rootProject.name = "MullvadVPN"
-include(
- ":app",
- ":service",
- ":tile"
-)
+include(":app", ":service", ":tile")
+
include(
":lib:billing",
":lib:common",
@@ -38,6 +36,7 @@ include(
":lib:grpc",
":lib:endpoint",
":lib:feature:daita:impl",
+ ":lib:feature:splittunneling:impl",
":lib:map",
":lib:model",
":lib:navigation",
@@ -53,8 +52,9 @@ include(
":lib:ui:tag",
":lib:ui:theme",
":lib:ui:util",
- ":lib:usecase"
+ ":lib:usecase",
)
+
include(
":test",
":test:arch",