diff --git a/android/CHANGELOG.md b/android/CHANGELOG.md
index 5c632d7c9e2f..fc05c7e91dba 100644
--- a/android/CHANGELOG.md
+++ b/android/CHANGELOG.md
@@ -27,6 +27,10 @@ Line wrap the file at 100 chars. Th
- Add DAITA (Defence against AI-guided Traffic Analysis) setting.
- Add WireGuard over Shadowsocks.
- Add feature indicators to the main view along with redesigning the connection details.
+- Add new "Connect on device start-up" setting for devices without system VPN settings.
+
+### Removed
+- Legacy auto-connect feature.
### Changed
- Update colors in the app to be more in line with material design.
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 a15875477a07..42a939250216 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
@@ -47,8 +47,6 @@ class VpnSettingsScreenTest {
// Arrange
setContentWithTheme { VpnSettingsScreen(state = VpnSettingsUiState.createDefault()) }
- onNodeWithText("Auto-connect (legacy)").assertExists()
-
onNodeWithTag(LAZY_LIST_TEST_TAG)
.performScrollToNode(hasTestTag(LAZY_LIST_LAST_ITEM_TEST_TAG))
@@ -524,6 +522,43 @@ class VpnSettingsScreenTest {
verify { mockOnShowCustomPortDialog.invoke() }
}
+ @Test
+ fun ensureConnectOnStartIsShownWhenSystemVpnSettingsAvailableIsFalse() =
+ composeExtension.use {
+ // Arrange
+ setContentWithTheme {
+ VpnSettingsScreen(
+ state = VpnSettingsUiState.createDefault(systemVpnSettingsAvailable = false)
+ )
+ }
+
+ // Assert
+ onNodeWithText("Connect on device start-up").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 device start-up").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 19e468783c75..8dc3c4e38551 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/preview/VpnSettingsUiStatePreviewParameterProvider.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt
index e583e6ea0ca6..c041f5516c52 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/compose/preview/VpnSettingsUiStatePreviewParameterProvider.kt
@@ -19,7 +19,6 @@ class VpnSettingsUiStatePreviewParameterProvider : PreviewParameterProvider Unit = {},
onToggleBlockAds: (Boolean) -> Unit = {},
onToggleBlockMalware: (Boolean) -> Unit = {},
- onToggleAutoConnect: (Boolean) -> Unit = {},
onToggleLocalNetworkSharing: (Boolean) -> Unit = {},
onDisableDaita: () -> Unit = {},
onToggleBlockAdultContent: (Boolean) -> Unit = {},
@@ -303,6 +301,7 @@ fun VpnSettingsScreen(
onWireguardPortSelected: (port: Constraint) -> Unit = {},
navigateToShadowSocksSettings: () -> Unit = {},
navigateToUdp2TcpSettings: () -> Unit = {},
+ onToggleAutoStartAndConnectOnBoot: (Boolean) -> Unit = {},
) {
var expandContentBlockersState by rememberSaveable { mutableStateOf(false) }
val biggerPadding = 54.dp
@@ -330,33 +329,28 @@ 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) {
+ } 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.auto_connect_footer_legacy,
+ R.string.connect_on_start_footer,
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)
+ )
+ }
}
+
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 7884f199f2c4..eede76ff7c01 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
@@ -11,7 +11,6 @@ import net.mullvad.mullvadvpn.viewmodel.CustomDnsItem
data class VpnSettingsUiState(
val mtu: Mtu?,
- val isAutoConnectEnabled: Boolean,
val isLocalNetworkSharingEnabled: Boolean,
val isDaitaEnabled: Boolean,
val isCustomDnsEnabled: Boolean,
@@ -25,6 +24,7 @@ data class VpnSettingsUiState(
val customWireguardPort: Port?,
val availablePortRanges: List,
val systemVpnSettingsAvailable: Boolean,
+ val autoStartAndConnectOnBoot: Boolean,
) {
val isCustomWireguardPort =
selectedWireguardPort is Constraint.Only &&
@@ -33,7 +33,6 @@ data class VpnSettingsUiState(
companion object {
fun createDefault(
mtu: Mtu? = null,
- isAutoConnectEnabled: Boolean = false,
isLocalNetworkSharingEnabled: Boolean = false,
isDaitaEnabled: Boolean = false,
isCustomDnsEnabled: Boolean = false,
@@ -47,10 +46,10 @@ data class VpnSettingsUiState(
customWireguardPort: Port? = null,
availablePortRanges: List = emptyList(),
systemVpnSettingsAvailable: Boolean = false,
+ autoStartAndConnectOnBoot: Boolean = false,
) =
VpnSettingsUiState(
mtu,
- isAutoConnectEnabled,
isLocalNetworkSharingEnabled,
isDaitaEnabled,
isCustomDnsEnabled,
@@ -64,6 +63,7 @@ data class VpnSettingsUiState(
customWireguardPort,
availablePortRanges,
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 b08708826d35..61d491094a12 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
@@ -102,6 +105,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))) }
@@ -127,6 +134,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()) }
@@ -195,7 +208,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()) }
@@ -226,3 +239,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..9f153e724b9a
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/receiver/BootCompletedReceiver.kt
@@ -0,0 +1,30 @@
+package net.mullvad.mullvadvpn.receiver
+
+import android.content.BroadcastReceiver
+import android.content.Context
+import android.content.Intent
+import android.net.VpnService
+import co.touchlab.kermit.Logger
+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
+ Logger.i("AutoStart on boot and connect, hasVpnPermission: $hasVpnPermission")
+ 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..3fc0f38c7c65
--- /dev/null
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/AutoStartAndConnectOnBootRepository.kt
@@ -0,0 +1,49 @@
+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
+import kotlinx.coroutines.flow.StateFlow
+
+class AutoStartAndConnectOnBootRepository(
+ private val packageManager: PackageManager,
+ private val bootCompletedComponentName: ComponentName,
+) {
+ private val _autoStartAndConnectOnBoot = MutableStateFlow(isAutoStartAndConnectOnBoot())
+ val autoStartAndConnectOnBoot: StateFlow = _autoStartAndConnectOnBoot
+
+ fun setAutoStartAndConnectOnBoot(enabled: Boolean) {
+ packageManager.setComponentEnabledSetting(
+ bootCompletedComponentName,
+ if (enabled) {
+ 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/repository/SettingsRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
index 0fa5e69940dd..8be8d2ae7ed2 100644
--- a/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
+++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/SettingsRepository.kt
@@ -68,8 +68,6 @@ class SettingsRepository(
suspend fun setObfuscation(value: ObfuscationMode) = managementService.setObfuscation(value)
- suspend fun setAutoConnect(isEnabled: Boolean) = managementService.setAutoConnect(isEnabled)
-
suspend fun setLocalNetworkSharing(isEnabled: Boolean) =
managementService.setAllowLan(isEnabled)
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 3baeda244a10..7829d0ce24cc 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
@@ -28,6 +28,7 @@ import net.mullvad.mullvadvpn.lib.model.Port
import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
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() {
@@ -55,13 +57,14 @@ class VpnSettingsViewModel(
private val customPort = MutableStateFlow(null)
private val vmState =
- combine(repository.settingsUpdates, relayListRepository.portRanges, customPort) {
- settings,
- portRanges,
- customWgPort ->
+ combine(
+ repository.settingsUpdates,
+ relayListRepository.portRanges,
+ customPort,
+ autoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot,
+ ) { settings, portRanges, customWgPort, autoStartAndConnectOnBoot ->
VpnSettingsViewModelState(
mtuValue = settings?.tunnelOptions?.wireguard?.mtu,
- isAutoConnectEnabled = settings?.autoConnect ?: false,
isLocalNetworkSharingEnabled = settings?.allowLan ?: false,
isDaitaEnabled = settings?.isDaitaEnabled() ?: false,
isCustomDnsEnabled = settings?.isCustomDnsEnabled() ?: false,
@@ -78,6 +81,7 @@ class VpnSettingsViewModel(
customWireguardPort = customWgPort,
availablePortRanges = portRanges,
systemVpnSettingsAvailable = systemVpnSettingsUseCase(),
+ autoStartAndConnectOnBoot = autoStartAndConnectOnBoot,
)
}
.stateIn(
@@ -109,14 +113,6 @@ class VpnSettingsViewModel(
}
}
- fun onToggleAutoConnect(isEnabled: Boolean) {
- viewModelScope.launch(dispatcher) {
- repository.setAutoConnect(isEnabled).onLeft {
- _uiSideEffect.send(VpnSettingsSideEffect.ShowToast.GenericError)
- }
- }
- }
-
fun onToggleLocalNetworkSharing(isEnabled: Boolean) {
viewModelScope.launch(dispatcher) {
repository.setLocalNetworkSharing(isEnabled).onLeft {
@@ -254,6 +250,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 31d5515a3c90..624716198d6f 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
@@ -11,7 +11,6 @@ import net.mullvad.mullvadvpn.lib.model.QuantumResistantState
data class VpnSettingsViewModelState(
val mtuValue: Mtu?,
- val isAutoConnectEnabled: Boolean,
val isLocalNetworkSharingEnabled: Boolean,
val isDaitaEnabled: Boolean,
val isCustomDnsEnabled: Boolean,
@@ -25,6 +24,7 @@ data class VpnSettingsViewModelState(
val customWireguardPort: Port?,
val availablePortRanges: List,
val systemVpnSettingsAvailable: Boolean,
+ val autoStartAndConnectOnBoot: Boolean,
) {
val isCustomWireguardPort =
selectedWireguardPort is Constraint.Only &&
@@ -33,7 +33,6 @@ data class VpnSettingsViewModelState(
fun toUiState(): VpnSettingsUiState =
VpnSettingsUiState(
mtuValue,
- isAutoConnectEnabled,
isLocalNetworkSharingEnabled,
isDaitaEnabled,
isCustomDnsEnabled,
@@ -47,13 +46,13 @@ data class VpnSettingsViewModelState(
customWireguardPort,
availablePortRanges,
systemVpnSettingsAvailable,
+ autoStartAndConnectOnBoot,
)
companion object {
fun default() =
VpnSettingsViewModelState(
mtuValue = null,
- isAutoConnectEnabled = false,
isLocalNetworkSharingEnabled = false,
isDaitaEnabled = false,
isCustomDnsEnabled = false,
@@ -67,6 +66,7 @@ data class VpnSettingsViewModelState(
customWireguardPort = null,
availablePortRanges = emptyList(),
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..f29ba4c2df2b
--- /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 1e15b1956a31..5b878b52b925 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,6 +58,8 @@ class VpnSettingsViewModelTest {
fun setup() {
every { mockSettingsRepository.settingsUpdates } returns mockSettingsUpdate
every { mockRelayListRepository.portRanges } returns portRangeFlow
+ every { mockAutoStartAndConnectOnBootRepository.autoStartAndConnectOnBoot } returns
+ autoStartAndConnectOnBootFlow
viewModel =
VpnSettingsViewModel(
@@ -58,6 +67,7 @@ class VpnSettingsViewModelTest {
systemVpnSettingsUseCase = mockSystemVpnSettingsUseCase,
relayListRepository = mockRelayListRepository,
dispatcher = UnconfinedTestDispatcher(),
+ autoStartAndConnectOnBootRepository = mockAutoStartAndConnectOnBootRepository,
)
}
@@ -195,4 +205,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/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
index 514d4f83aa6b..3ffbe0fc4a2e 100644
--- 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
@@ -103,7 +103,6 @@ import net.mullvad.mullvadvpn.lib.model.RemoveApiAccessMethodError
import net.mullvad.mullvadvpn.lib.model.RemoveSplitTunnelingAppError
import net.mullvad.mullvadvpn.lib.model.SetAllowLanError
import net.mullvad.mullvadvpn.lib.model.SetApiAccessMethodError
-import net.mullvad.mullvadvpn.lib.model.SetAutoConnectError
import net.mullvad.mullvadvpn.lib.model.SetDaitaSettingsError
import net.mullvad.mullvadvpn.lib.model.SetDnsOptionsError
import net.mullvad.mullvadvpn.lib.model.SetObfuscationOptionsError
@@ -502,12 +501,6 @@ class ManagementService(
.mapLeft(SetObfuscationOptionsError::Unknown)
.mapEmpty()
- suspend fun setAutoConnect(isEnabled: Boolean): Either =
- Either.catch { grpc.setAutoConnect(BoolValue.of(isEnabled)) }
- .onLeft { Logger.e("Set auto connect error") }
- .mapLeft(SetAutoConnectError::Unknown)
- .mapEmpty()
-
suspend fun setAllowLan(allow: Boolean): Either =
Either.catch { grpc.setAllowLan(BoolValue.of(allow)) }
.onLeft { Logger.e("Set allow lan error") }
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
index 0f57ce58dc99..99b4554f0b2c 100644
--- 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
@@ -254,7 +254,6 @@ internal fun ManagementInterface.Settings.toDomain(): Settings =
obfuscationSettings = obfuscationSettings.toDomain(),
customLists = customLists.customListsList.map { it.toDomain() },
allowLan = allowLan,
- autoConnect = autoConnect,
tunnelOptions = tunnelOptions.toDomain(),
relayOverrides = relayOverridesList.map { it.toDomain() },
showBetaReleases = showBetaReleases,
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
deleted file mode 100644
index b2b3b74edf36..000000000000
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/SetAutoConnectError.kt
+++ /dev/null
@@ -1,5 +0,0 @@
-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/Settings.kt b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt
index f59d85184cfd..b3f1a2e8a071 100644
--- a/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt
+++ b/android/lib/model/src/main/kotlin/net/mullvad/mullvadvpn/lib/model/Settings.kt
@@ -8,7 +8,6 @@ data class Settings(
val obfuscationSettings: ObfuscationSettings,
val customLists: List,
val allowLan: Boolean,
- val autoConnect: Boolean,
val tunnelOptions: TunnelOptions,
val relayOverrides: List,
val showBetaReleases: Boolean,
diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml
index 338aae32eeaf..fc02f8cc0d79 100644
--- a/android/lib/resource/src/main/res/values/strings.xml
+++ b/android/lib/resource/src/main/res/values/strings.xml
@@ -403,4 +403,6 @@
In
%1$s more...
Connect
+ Connect on device start-up
+ Automatically connect on device start-up. Only works if the app has been granted the VPN permission.
diff --git a/gui/locales/messages.pot b/gui/locales/messages.pot
index a64416dca1e9..aac7d2de6faa 100644
--- a/gui/locales/messages.pot
+++ b/gui/locales/messages.pot
@@ -2308,6 +2308,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. Only works if the app has been granted the 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 ""
@@ -2317,6 +2320,9 @@ msgstr ""
msgid "Changes to DNS related settings might not go into effect immediately due to cached results."
msgstr ""
+msgid "Connect on device start-up"
+msgstr ""
+
msgid "Connecting..."
msgstr ""
diff --git a/mullvad-daemon/src/settings/mod.rs b/mullvad-daemon/src/settings/mod.rs
index d5a222cd021c..8a42ec620448 100644
--- a/mullvad-daemon/src/settings/mod.rs
+++ b/mullvad-daemon/src/settings/mod.rs
@@ -111,6 +111,9 @@ impl SettingsPersister {
if cfg!(target_os = "android") {
should_save |= !settings.tunnel_options.generic.enable_ipv6;
settings.tunnel_options.generic.enable_ipv6 = true;
+
+ // Auto-connect is managed by Android itself.
+ settings.auto_connect = false;
}
if crate::version::is_beta_version() {
should_save |= !settings.show_beta_releases;