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;