diff --git a/android/app/src/main/AndroidManifest.xml b/android/app/src/main/AndroidManifest.xml index f6f60cdd1fab..73827199cf7d 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 5c3bf9bb77fe..6492b3d0bfe8 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 @@ -252,6 +252,7 @@ fun VpnSettings( onSelectQuantumResistanceSetting = vm::onSelectQuantumResistanceSetting, onWireguardPortSelected = vm::onWireguardPortSelected, onObfuscationPortSelected = vm::onObfuscationPortSelected, + onToggleConnectOnStart = vm::onToggleConnectOnStart ) } @@ -288,6 +289,7 @@ fun VpnSettingsScreen( onSelectQuantumResistanceSetting: (quantumResistant: QuantumResistantState) -> Unit = {}, onWireguardPortSelected: (port: Constraint) -> Unit = {}, onObfuscationPortSelected: (port: Constraint) -> Unit = {}, + onToggleConnectOnStart: (Boolean) -> Unit = {} ) { var expandContentBlockersState by rememberSaveable { mutableStateOf(false) } var expandUdp2TcpPortSettings by rememberSaveable { mutableStateOf(false) } @@ -316,33 +318,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.connectOnStart, + onCellClicked = { newValue -> onToggleConnectOnStart(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..6fa70366e99e 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 connectOnStart: Boolean ) { val selectObfuscationPortEnabled = selectedObfuscation != SelectedObfuscation.Off @@ -41,6 +42,7 @@ data class VpnSettingsUiState( customWireguardPort: Constraint.Only? = null, availablePortRanges: List = emptyList(), systemVpnSettingsAvailable: Boolean = false, + connectOnStart: Boolean = false, ) = VpnSettingsUiState( mtu, @@ -55,7 +57,8 @@ data class VpnSettingsUiState( selectedWireguardPort, customWireguardPort, availablePortRanges, - systemVpnSettingsAvailable + systemVpnSettingsAvailable, + connectOnStart ) } } 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 70153de61969..b5fa9e9e6bd0 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 @@ -15,8 +16,10 @@ import net.mullvad.mullvadvpn.lib.model.ApiAccessMethodName import net.mullvad.mullvadvpn.lib.model.GeoLocationId 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.ChangelogRepository +import net.mullvad.mullvadvpn.repository.ConnectOnStartRepository import net.mullvad.mullvadvpn.repository.CustomListsRepository import net.mullvad.mullvadvpn.repository.InAppNotificationController import net.mullvad.mullvadvpn.repository.PrivacyDisclaimerRepository @@ -98,6 +101,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))) } @@ -122,6 +129,7 @@ val uiModule = module { single { VoucherRepository(get(), get()) } single { SplitTunnelingRepository(get()) } single { ApiAccessRepository(get()) } + single { ConnectOnStartRepository(get(), get(named(BOOT_COMPLETED_RECEIVER_COMPONENT_NAME))) } single { AccountExpiryNotificationUseCase(get()) } single { TunnelStateNotificationUseCase(get()) } @@ -188,7 +196,7 @@ val uiModule = module { viewModel { SettingsViewModel(get(), get(), IS_PLAY_BUILD) } viewModel { SplashViewModel(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()) } @@ -232,3 +240,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..2fdd24b5179e --- /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 == "android.intent.action.BOOT_COMPLETED") { + context?.let { startAndConnectTunnel(context) } + } + } + + private fun startAndConnectTunnel(context: Context) { + // Check for vpn permission + if (VpnService.prepare(context) == null) { + 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/ConnectOnStartRepository.kt b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ConnectOnStartRepository.kt new file mode 100644 index 000000000000..0b22a298d786 --- /dev/null +++ b/android/app/src/main/kotlin/net/mullvad/mullvadvpn/repository/ConnectOnStartRepository.kt @@ -0,0 +1,42 @@ +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_ENABLED +import android.content.pm.PackageManager.DONT_KILL_APP +import kotlinx.coroutines.flow.MutableStateFlow + +class ConnectOnStartRepository( + private val packageManager: PackageManager, + private val bootCompletedComponentName: ComponentName +) { + val connectOnStart = MutableStateFlow(isConnectOnStart()) + + fun setConnectOnStart(connect: Boolean) { + packageManager.setComponentEnabledSetting( + bootCompletedComponentName, + if (connect) { + COMPONENT_ENABLED_STATE_ENABLED + } else { + COMPONENT_ENABLED_STATE_DISABLED + }, + DONT_KILL_APP + ) + + connectOnStart.value = isConnectOnStart() + } + + private fun isConnectOnStart(): Boolean = + when (packageManager.getComponentEnabledSetting(bootCompletedComponentName)) { + COMPONENT_ENABLED_STATE_DEFAULT -> BOOT_COMPLETED_DEFAULT_STATE + COMPONENT_ENABLED_STATE_ENABLED -> true + COMPONENT_ENABLED_STATE_DISABLED -> false + 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 a0a38ef6f8c9..d6d85d313c5e 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.ConnectOnStartRepository import net.mullvad.mullvadvpn.repository.RelayListRepository import net.mullvad.mullvadvpn.repository.SettingsRepository import net.mullvad.mullvadvpn.usecase.SystemVpnSettingsUseCase @@ -46,6 +47,7 @@ class VpnSettingsViewModel( private val repository: SettingsRepository, private val relayListRepository: RelayListRepository, private val systemVpnSettingsUseCase: SystemVpnSettingsUseCase, + private val connectOnStartRepository: ConnectOnStartRepository, private val dispatcher: CoroutineDispatcher = Dispatchers.IO ) : ViewModel() { @@ -59,7 +61,8 @@ class VpnSettingsViewModel( repository.settingsUpdates, relayListRepository.portRanges, customPort, - ) { settings, portRanges, customWgPort -> + connectOnStartRepository.connectOnStart + ) { settings, portRanges, customWgPort, connectOnStart -> VpnSettingsViewModelState( mtuValue = settings?.tunnelOptions?.wireguard?.mtu, isAutoConnectEnabled = settings?.autoConnect ?: false, @@ -77,7 +80,8 @@ class VpnSettingsViewModel( customWireguardPort = customWgPort, availablePortRanges = portRanges, systemVpnSettingsAvailable = - systemVpnSettingsUseCase.systemVpnSettingsAvailable() + systemVpnSettingsUseCase.systemVpnSettingsAvailable(), + connectOnStart = connectOnStart ) } .stateIn( @@ -245,6 +249,10 @@ class VpnSettingsViewModel( } } + fun onToggleConnectOnStart(connect: Boolean) { + viewModelScope.launch(dispatcher) { connectOnStartRepository.setConnectOnStart(connect) } + } + 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..bb1ffa784314 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 connectOnStart: Boolean, ) { fun toUiState(): VpnSettingsUiState = VpnSettingsUiState( @@ -38,7 +39,8 @@ data class VpnSettingsViewModelState( selectedWireguardPort, customWireguardPort, availablePortRanges, - systemVpnSettingsAvailable + systemVpnSettingsAvailable, + connectOnStart ) companion object { @@ -56,7 +58,8 @@ data class VpnSettingsViewModelState( selectedWireguardPort = Constraint.Any, customWireguardPort = null, availablePortRanges = emptyList(), - systemVpnSettingsAvailable = false + systemVpnSettingsAvailable = false, + connectOnStart = false ) } } diff --git a/android/lib/resource/src/main/res/values/strings.xml b/android/lib/resource/src/main/res/values/strings.xml index f7fafc72ffcb..ad75dd415138 100644 --- a/android/lib/resource/src/main/res/values/strings.xml +++ b/android/lib/resource/src/main/res/values/strings.xml @@ -385,4 +385,6 @@ Delete method? Failed to set to current - API not reachable Failed to set to current - Unknown reason + Connect on start + Automatically connect on device start-up