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