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 ""