diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md
index 903ffbde9441..17d52bcd2ada 100644
--- a/android/CHANGELOG.md
+++ b/android/CHANGELOG.md
@@ -24,6 +24,7 @@ Line wrap the file at 100 chars. Th
## [Unreleased]
### Added
- Add the ability to customize how the app talks to the api.
+- Add auto connect and start setting for devices without system vpn settings.
### Changed
- Migrate underlaying communication wtih daemon to gRPC. This also implies major changes and
diff --git a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt
index 33e783662e87..3ea7bc025fee 100644
--- a/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt
+++ b/android/app/src/androidTest/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreenTest.kt
@@ -54,8 +54,6 @@ class VpnSettingsScreenTest {
)
}
- onNodeWithText("Auto-connect (legacy)").assertExists()
-
onNodeWithTag(LAZY_LIST_TEST_TAG)
.performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG))
@@ -606,6 +604,43 @@ class VpnSettingsScreenTest {
verify { mockOnShowCustomPortDialog.invoke() }
}
+ @Test
+ fun ensureConnectOnStartIsShownWhenSystemVpnSettingsAvailableIsFalse() =
+ composeExtension.use {
+ // Arrange
+ setContentWithTheme {
+ VpnSettingsScreen(
+ state = VpnSettingsUiState.createDefault(systemVpnSettingsAvailable = false),
+ )
+ }
+
+ // Assert
+ onNodeWithText("Connect on boot").assertExists()
+ }
+
+ @Test
+ fun whenClickingOnConnectOnStartShouldCallOnToggleAutoStartAndConnectOnBoot() =
+ composeExtension.use {
+ // Arrange
+ val mockOnToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = mockk(relaxed = true)
+ setContentWithTheme {
+ VpnSettingsScreen(
+ state =
+ VpnSettingsUiState.createDefault(
+ systemVpnSettingsAvailable = false,
+ autoStartAndConnectOnBoot = false
+ ),
+ onToggleAutoStartAndConnectOnBoot = mockOnToggleAutoStartAndConnectOnBoot
+ )
+ }
+
+ // Act
+ onNodeWithText("Connect on boot").performClick()
+
+ // Assert
+ verify { mockOnToggleAutoStartAndConnectOnBoot.invoke(true) }
+ }
+
companion object {
private const val LOCAL_DNS_SERVER_WARNING =
"The local DNS server will not work unless you enable " +
diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml
index f69801a46dac..2b1e16b5f5f4 100644
--- a/android/app/src/main/AndroidManifest.xml
+++ b/android/app/src/main/AndroidManifest.xml
@@ -7,6 +7,7 @@
+
+
+
+
+
+
+
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
index d92694afc25a..2fc2c3b622f6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/screen/VpnSettingsScreen.kt
@@ -256,6 +256,7 @@ fun VpnSettings(
onSelectQuantumResistanceSetting = vm::onSelectQuantumResistanceSetting,
onWireguardPortSelected = vm::onWireguardPortSelected,
onObfuscationPortSelected = vm::onObfuscationPortSelected,
+ onToggleAutoStartAndConnectOnBoot = vm::onToggleAutoStartAndConnectOnBoot
)
}
@@ -292,6 +293,7 @@ fun VpnSettingsScreen(
onSelectQuantumResistanceSetting: (quantumResistant: QuantumResistantState) -> Unit = {},
onWireguardPortSelected: (port: Constraint) -> Unit = {},
onObfuscationPortSelected: (port: Constraint) -> Unit = {},
+ onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = {}
) {
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
var expandUdp2TcpPortSettings by rememberSaveable { mutableStateOf(false) }
@@ -320,33 +322,50 @@ fun VpnSettingsScreen(
text = stringResource(id = R.string.auto_connect_and_lockdown_mode_footer)
)
}
- }
- item {
- Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
- HeaderSwitchComposeCell(
- title = stringResource(R.string.auto_connect_legacy),
- isToggled = state.isAutoConnectEnabled,
- isEnabled = true,
- onCellClicked = { newValue -> onToggleAutoConnect(newValue) }
- )
- }
- item {
- SwitchComposeSubtitleCell(
- text =
- HtmlCompat.fromHtml(
- if (state.systemVpnSettingsAvailable) {
+ item {
+ Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
+ HeaderSwitchComposeCell(
+ title = stringResource(R.string.auto_connect_legacy),
+ isToggled = state.isAutoConnectEnabled,
+ isEnabled = true,
+ onCellClicked = { newValue -> onToggleAutoConnect(newValue) }
+ )
+ }
+ item {
+ SwitchComposeSubtitleCell(
+ text =
+ HtmlCompat.fromHtml(
textResource(
R.string.auto_connect_footer_legacy,
textResource(R.string.auto_connect_and_lockdown_mode)
- )
- } else {
- textResource(R.string.auto_connect_footer_legacy_tv)
- },
- HtmlCompat.FROM_HTML_MODE_COMPACT
- )
- .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold)
- )
+ ),
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ )
+ .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold)
+ )
+ }
+ } else {
+ item {
+ Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
+ HeaderSwitchComposeCell(
+ title = stringResource(R.string.connect_on_start),
+ isToggled = state.autoStartAndConnectOnBoot,
+ onCellClicked = { newValue -> onToggleAutoStartAndConnectOnBoot(newValue) }
+ )
+ SwitchComposeSubtitleCell(
+ text =
+ HtmlCompat.fromHtml(
+ textResource(
+ R.string.connect_on_start_footer,
+ textResource(R.string.auto_connect_and_lockdown_mode)
+ ),
+ HtmlCompat.FROM_HTML_MODE_COMPACT
+ )
+ .toAnnotatedString(boldFontWeight = FontWeight.ExtraBold)
+ )
+ }
}
+
item {
Spacer(modifier = Modifier.height(Dimens.cellLabelVerticalPadding))
HeaderSwitchComposeCell(
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
index 17eb69d380e8..c3b3b348c3f4 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/state/VpnSettingsUiState.kt
@@ -23,6 +23,7 @@ data class VpnSettingsUiState(
val customWireguardPort: Constraint?,
val availablePortRanges: List,
val systemVpnSettingsAvailable: Boolean,
+ val autoStartAndConnectOnBoot: Boolean
) {
val selectObfuscationPortEnabled = selectedObfuscation != SelectedObfuscation.Off
@@ -41,6 +42,7 @@ data class VpnSettingsUiState(
customWireguardPort: Constraint.Only? = null,
availablePortRanges: List = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
+ autoStartAndConnectOnBoot: Boolean = false,
) =
VpnSettingsUiState(
mtu,
@@ -55,7 +57,8 @@ data class VpnSettingsUiState(
selectedWireguardPort,
customWireguardPort,
availablePortRanges,
- systemVpnSettingsAvailable
+ systemVpnSettingsAvailable,
+ autoStartAndConnectOnBoot
)
}
}
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 371a30bdf1c5..4963e86906ec 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
@@ -1,5 +1,6 @@
package net.mullvad.mullvadvpn.di
+import android.content.ComponentName
import android.content.Context
import android.content.SharedPreferences
import android.content.pm.PackageManager
@@ -11,7 +12,9 @@ import net.mullvad.mullvadvpn.constant.IS_PLAY_BUILD
import net.mullvad.mullvadvpn.dataproxy.MullvadProblemReport
import net.mullvad.mullvadvpn.lib.payment.PaymentProvider
import net.mullvad.mullvadvpn.lib.shared.VoucherRepository
+import net.mullvad.mullvadvpn.receiver.BootCompletedReceiver
import net.mullvad.mullvadvpn.repository.ApiAccessRepository
+import net.mullvad.mullvadvpn.repository.AutoStartAndConnectOnBootRepository
import net.mullvad.mullvadvpn.repository.ChangelogRepository
import net.mullvad.mullvadvpn.repository.CustomListsRepository
import net.mullvad.mullvadvpn.repository.InAppNotificationController
@@ -97,6 +100,10 @@ val uiModule = module {
single { androidContext().packageManager }
single(named(SELF_PACKAGE_NAME)) { androidContext().packageName }
+ single(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME)) {
+ ComponentName(androidContext(), BootCompletedReceiver::class.java)
+ }
+
viewModel { SplitTunnelingViewModel(get(), get(), Dispatchers.Default) }
single { ApplicationsProvider(get(), get(named(SELF_PACKAGE_NAME))) }
@@ -123,6 +130,12 @@ val uiModule = module {
single { ApiAccessRepository(get()) }
single { NewDeviceRepository() }
single { SplashCompleteRepository() }
+ single {
+ AutoStartAndConnectOnBootRepository(
+ get(),
+ get(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME))
+ )
+ }
single { AccountExpiryNotificationUseCase(get()) }
single { TunnelStateNotificationUseCase(get()) }
@@ -187,7 +200,7 @@ val uiModule = module {
viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) }
viewModel { SplashViewModel(get(), get(), get(), get()) }
viewModel { VoucherDialogViewModel(get()) }
- viewModel { VpnSettingsViewModel(get(), get(), get()) }
+ viewModel { VpnSettingsViewModel(get(), get(), get(), get()) }
viewModel { WelcomeViewModel(get(), get(), get(), get(), isPlayBuild = IS_PLAY_BUILD) }
viewModel { ReportProblemViewModel(get(), get()) }
viewModel { ViewLogsViewModel(get()) }
@@ -215,3 +228,4 @@ val uiModule = module {
const val SELF_PACKAGE_NAME = "SELF_PACKAGE_NAME"
const val APP_PREFERENCES_NAME = "${BuildConfig.APPLICATION_ID}.app_preferences"
+const val BOOT_COMPLETED_RECEIVER_COMPONENT_NAME = "BOOT_COMPLETED_RECEIVER_COMPONENT_NAME"
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt
new file mode 100644
index 000000000000..e4cb0977d221
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt
@@ -0,0 +1,28 @@
+package net.mullvad.mullvadvpn.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.net.VpnService
+import net.mullvad.mullvadvpn.lib.common.constant.KEY_CONNECT_ACTION
+import net.mullvad.mullvadvpn.lib.common.constant.VPN_SERVICE_CLASS
+
+class BootCompletedReceiver : BroadcastReceiver() {
+ override fun onReceive(context: Context?, intent: Intent?) {
+ if (intent?.action == Intent.ACTION_BOOT_COMPLETED) {
+ context?.let { startAndConnectTunnel(context) }
+ }
+ }
+
+ private fun startAndConnectTunnel(context: Context) {
+ val hasVpnPermission = VpnService.prepare(context) == null
+ if (hasVpnPermission) {
+ val intent =
+ Intent().apply {
+ setClassName(context.packageName, VPN_SERVICE_CLASS)
+ action = KEY_CONNECT_ACTION
+ }
+ context.startForegroundService(intent)
+ }
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt
new file mode 100644
index 000000000000..8ebb3b126628
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt
@@ -0,0 +1,47 @@
+package net.mullvad.mullvadvpn.repository
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_DISABLED_USER
+import android.content.pm.PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+import android.content.pm.PackageManager.DONT_KILL_APP
+import kotlinx.coroutines.flow.MutableStateFlow
+
+class AutoStartAndConnectOnBootRepository(
+ private val packageManager: PackageManager,
+ private val bootCompletedComponentName: ComponentName
+) {
+ val autoStartAndConnectOnBoot = MutableStateFlow(isAutoStartAndConnectOnBoot())
+
+ fun setAutoStartAndConnectOnBoot(connect: Boolean) {
+ packageManager.setComponentEnabledSetting(
+ bootCompletedComponentName,
+ if (connect) {
+ COMPONENT_ENABLED_STATE_ENABLED
+ } else {
+ COMPONENT_ENABLED_STATE_DISABLED
+ },
+ DONT_KILL_APP
+ )
+
+ autoStartAndConnectOnBoot.value = isAutoStartAndConnectOnBoot()
+ }
+
+ private fun isAutoStartAndConnectOnBoot(): Boolean =
+ when (packageManager.getComponentEnabledSetting(bootCompletedComponentName)) {
+ COMPONENT_ENABLED_STATE_DEFAULT -> BOOT_COMPLETED_DEFAULT_STATE
+ COMPONENT_ENABLED_STATE_ENABLED -> true
+ COMPONENT_ENABLED_STATE_DISABLED -> false
+ COMPONENT_ENABLED_STATE_DISABLED_USER,
+ COMPONENT_ENABLED_STATE_DISABLED_UNTIL_USED ->
+ error("Enabled setting only applicable for application")
+ else -> error("Unknown component enabled setting")
+ }
+
+ companion object {
+ private const val BOOT_COMPLETED_DEFAULT_STATE = false
+ }
+}
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
index 1e9a335951d3..dee70091a4f6 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModel.kt
@@ -27,6 +27,7 @@ import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
import net.mullvad.mullvadvpn.lib.model.SelectedObfuscation
import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
+import net.mullvad.mullvadvpn.repository.AutoStartAndConnectOnBootRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
@@ -46,6 +47,7 @@ class VpnSettingsViewModel(
private val repository: SettingsRepository,
private val relayListRepository: RelayListRepository,
private val systemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase,
+ private val autoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository,
private val dispatcher: CoroutineDispatcher = Dispatchers.IO
) : ViewModel() {
@@ -59,7 +61,8 @@ class VpnSettingsViewModel(
repository.settingsUpdates,
relayListRepository.portRanges,
customPort,
- ) { settings, portRanges, customWgPort ->
+ autoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot
+ ) { settings, portRanges, customWgPort, autoStartAndConnectOnBoot ->
VpnSettingsViewModelState(
mtuValue = settings?.tunnelOptions?.wireguard?.mtu,
isAutoConnectEnabled = settings?.autoConnect ?: false,
@@ -76,7 +79,8 @@ class VpnSettingsViewModel(
selectedWireguardPort = settings?.getWireguardPort() ?: Constraint.Any,
customWireguardPort = customWgPort,
availablePortRanges = portRanges,
- systemVpnSettingsAvailable = systemVpnSettingsUseCase()
+ systemVpnSettingsAvailable = systemVpnSettingsUseCase(),
+ autoStartAndConnectOnBoot = autoStartAndConnectOnBoot
)
}
.stateIn(
@@ -244,6 +248,12 @@ class VpnSettingsViewModel(
}
}
+ fun onToggleAutoStartAndConnectOnBoot(autoStartAndConnect: Boolean) {
+ viewModelScope.launch(dispatcher) {
+ autoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(autoStartAndConnect)
+ }
+ }
+
private fun updateDefaultDnsOptionsViaRepository(contentBlockersOption: DefaultDnsOptions) =
viewModelScope.launch(dispatcher) {
repository
diff --git a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
index d8be8d1cf27e..796efcb74b60 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelState.kt
@@ -23,6 +23,7 @@ data class VpnSettingsViewModelState(
val customWireguardPort: Constraint?,
val availablePortRanges: List,
val systemVpnSettingsAvailable: Boolean,
+ val autoStartAndConnectOnBoot: Boolean,
) {
fun toUiState(): VpnSettingsUiState =
VpnSettingsUiState(
@@ -38,7 +39,8 @@ data class VpnSettingsViewModelState(
selectedWireguardPort,
customWireguardPort,
availablePortRanges,
- systemVpnSettingsAvailable
+ systemVpnSettingsAvailable,
+ autoStartAndConnectOnBoot
)
companion object {
@@ -56,7 +58,8 @@ data class VpnSettingsViewModelState(
selectedWireguardPort = Constraint.Any,
customWireguardPort = null,
availablePortRanges = emptyList(),
- systemVpnSettingsAvailable = false
+ systemVpnSettingsAvailable = false,
+ autoStartAndConnectOnBoot = false
)
}
}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepositoryTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepositoryTest.kt
new file mode 100644
index 000000000000..11b2b6d0a92d
--- /dev/null
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepositoryTest.kt
@@ -0,0 +1,73 @@
+package net.mullvad.mullvadvpn.repository
+
+import android.content.ComponentName
+import android.content.pm.PackageManager
+import app.cash.turbine.test
+import io.mockk.Runs
+import io.mockk.every
+import io.mockk.just
+import io.mockk.mockk
+import io.mockk.verify
+import kotlinx.coroutines.test.runTest
+import org.junit.jupiter.api.Assertions.assertEquals
+import org.junit.jupiter.api.BeforeEach
+import org.junit.jupiter.api.Test
+
+class AutoStartAndConnectOnBootRepositoryTest {
+
+ private val mockPackageManager: PackageManager = mockk()
+ private val mockComponentName: ComponentName = mockk()
+
+ private lateinit var autoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository
+
+ @BeforeEach
+ fun setUp() {
+ every { mockPackageManager.getComponentEnabledSetting(mockComponentName) } returns
+ PackageManager.COMPONENT_ENABLED_STATE_DEFAULT
+
+ autoStartAndConnectOnBootRepository =
+ AutoStartAndConnectOnBootRepository(
+ packageManager = mockPackageManager,
+ bootCompletedComponentName = mockComponentName
+ )
+ }
+
+ @Test
+ fun `autoStartAndConnectOnBoot should emit false when default state is returned by package manager`() =
+ runTest {
+ // Assert
+ autoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot.test {
+ assertEquals(false, awaitItem())
+ }
+ }
+
+ @Test
+ fun `when setting autoStartAndConnectOnBoot to true should call package manager and update autoStartAndConnectOnBoot`() =
+ runTest {
+ // Arrange
+ val targetState = true
+ every {
+ mockPackageManager.setComponentEnabledSetting(
+ mockComponentName,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP
+ )
+ } just Runs
+ every { mockPackageManager.getComponentEnabledSetting(mockComponentName) } returns
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED
+
+ // Act, Assert
+ autoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot.test {
+ assertEquals(false, awaitItem()) // Default state
+ autoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(targetState)
+ verify {
+ mockPackageManager.setComponentEnabledSetting(
+ mockComponentName,
+ PackageManager.COMPONENT_ENABLED_STATE_ENABLED,
+ PackageManager.DONT_KILL_APP
+ )
+ }
+ assertEquals(targetState, awaitItem())
+ }
+ }
+}
diff --git a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
index add2ee8580d5..6772d1ddd331 100644
--- a/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
+++ b/android/app/src/test/kotlin/net/mullvad/mullvadvpn/viewmodel/VpnSettingsViewModelTest.kt
@@ -3,11 +3,14 @@ package net.mullvad.mullvadvpn.viewmodel
import androidx.lifecycle.viewModelScope
import app.cash.turbine.test
import arrow.core.right
+import io.mockk.Runs
import io.mockk.coEvery
import io.mockk.coVerify
import io.mockk.every
+import io.mockk.just
import io.mockk.mockk
import io.mockk.unmockkAll
+import io.mockk.verify
import kotlin.test.assertEquals
import kotlin.test.assertIs
import kotlinx.coroutines.cancel
@@ -26,6 +29,7 @@ import net.mullvad.mullvadvpn.lib.model.Settings
import net.mullvad.mullvadvpn.lib.model.TunnelOptions
import net.mullvad.mullvadvpn.lib.model.WireguardConstraints
import net.mullvad.mullvadvpn.lib.model.WireguardTunnelOptions
+import net.mullvad.mullvadvpn.repository.AutoStartAndConnectOnBootRepository
import net.mullvad.mullvadvpn.repository.RelayListRepository
import net.mullvad.mullvadvpn.repository.SettingsRepository
import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsAvailableUseCase
@@ -41,9 +45,12 @@ class VpnSettingsViewModelTest {
private val mockSystemVpnSettingsUseCase: SystemVpnSettingsAvailableUseCase =
mockk(relaxed = true)
private val mockRelayListRepository: RelayListRepository = mockk()
+ private val mockAutoStartAndConnectOnBootRepository: AutoStartAndConnectOnBootRepository =
+ mockk()
private val mockSettingsUpdate = MutableStateFlow(null)
private val portRangeFlow = MutableStateFlow(emptyList())
+ private val autoStartAndConnectOnBootFlow = MutableStateFlow(false)
private lateinit var viewModel: VpnSettingsViewModel
@@ -51,13 +58,16 @@ class VpnSettingsViewModelTest {
fun setup() {
every { mockSettingsRepository.settingsUpdates } returns mockSettingsUpdate
every { mockRelayListRepository.portRanges } returns portRangeFlow
+ every { mockAutoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot } returns
+ autoStartAndConnectOnBootFlow
viewModel =
VpnSettingsViewModel(
repository = mockSettingsRepository,
systemVpnSettingsUseCase = mockSystemVpnSettingsUseCase,
relayListRepository = mockRelayListRepository,
- dispatcher = UnconfinedTestDispatcher()
+ dispatcher = UnconfinedTestDispatcher(),
+ autoStartAndConnectOnBootRepository = mockAutoStartAndConnectOnBootRepository
)
}
@@ -188,4 +198,36 @@ class VpnSettingsViewModelTest {
assertEquals(systemVpnSettingsAvailable, awaitItem().systemVpnSettingsAvailable)
}
}
+
+ @Test
+ fun `when autoStartAndConnectOnBoot is true then uiState should be autoStart=true`() = runTest {
+ // Arrange
+ val connectOnStart = true
+
+ // Act
+ autoStartAndConnectOnBootFlow.value = connectOnStart
+
+ // Assert
+ viewModel.uiState.test {
+ assertEquals(connectOnStart, awaitItem().autoStartAndConnectOnBoot)
+ }
+ }
+
+ @Test
+ fun `calling onToggleAutoStartAndConnectOnBoot should call autoStartAndConnectOnBoot`() =
+ runTest {
+ // Arrange
+ val targetState = true
+ every {
+ mockAutoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(targetState)
+ } just Runs
+
+ // Act
+ viewModel.onToggleAutoStartAndConnectOnBoot(targetState)
+
+ // Assert
+ verify {
+ mockAutoStartAndConnectOnBootRepository.setAutoStartAndConnectOnBoot(targetState)
+ }
+ }
}
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 367b635879de..5f6523901ffb 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -383,4 +383,6 @@
Delete method?
Failed to set to current - API not reachable
Failed to set to current - Unknown reason
+ Connect on boot
+ Automatically connect on device start up. Please make sure the app has VPN permission.
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index 7aea156fb992..846ba7a1f552 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -2198,6 +2198,9 @@ msgstr ""
msgid "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."
msgstr ""
+msgid "Automatically connect on device start up. Please make sure the app has VPN permission."
+msgstr ""
+
msgid "Automatically connect when the app launches. This setting will be replaced with a new connect on device start-up feature in a future update."
msgstr ""
@@ -2207,6 +2210,9 @@ msgstr ""
msgid "Changes to DNS related settings might not go into effect immediately due to cached results."
msgstr ""
+msgid "Connect on boot"
+msgstr ""
+
msgid "Connecting..."
msgstr ""